// server/RobotConfigService.js // // Self-contained Express-Modul für die robot.json-Verwaltung. // Einbinden mit einer Zeile: // // const robotConfigService = require('./RobotConfigService'); // robotConfigService.register(app, { apiKey: process.env.ROBOT_API_KEY }); // // Routen: // GET /api/robot → aktuelle robot.json // PUT /api/robot → überschreibt robot.json, legt Snapshot an (Auth) // GET /api/robot/history → Liste aller Snapshots // GET /api/robot/history/:ts → einen bestimmten Snapshot abrufen 'use strict'; const express = require('express'); const fs = require('fs'); const fsp = require('fs/promises'); const path = require('path'); const crypto = require('crypto'); const DATA_DIR = path.join(__dirname, '..', 'data', 'robot'); const ROBOT_JSON = path.join(DATA_DIR, 'robot.json'); const KEY_FILE = path.join(DATA_DIR, '.apikey'); // ── Timestamp ──────────────────────────────────────────────────────────────── function makeTimestamp() { const now = new Date(); const p = (n, w = 2) => String(n).padStart(w, '0'); return `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}_${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`; } // ── API-Key-Auflösung ──────────────────────────────────────────────────────── function resolveApiKey(provided) { if (provided) return provided; try { return fs.readFileSync(KEY_FILE, 'utf8').trim(); } catch { fs.mkdirSync(DATA_DIR, { recursive: true }); const key = crypto.randomBytes(24).toString('hex'); fs.writeFileSync(KEY_FILE, key, 'utf8'); console.log(`[RobotConfigService] Kein ROBOT_API_KEY gesetzt — neuer Key generiert: ${key}`); console.log(`[RobotConfigService] Gespeichert in: ${KEY_FILE}`); return key; } } // ── Datei-Operationen ──────────────────────────────────────────────────────── async function readRobotJson() { const raw = await fsp.readFile(ROBOT_JSON, 'utf8'); return JSON.parse(raw); } async function writeRobotJson(data) { await fsp.mkdir(DATA_DIR, { recursive: true }); const ts = makeTimestamp(); const snapshotFile = `robot_${ts}.json`; try { const current = await fsp.readFile(ROBOT_JSON, 'utf8'); await fsp.writeFile(path.join(DATA_DIR, snapshotFile), current, 'utf8'); } catch { // Noch kein robot.json vorhanden — kein Snapshot nötig } await fsp.writeFile(ROBOT_JSON, JSON.stringify(data, null, 2), 'utf8'); await pruneSnapshots(); return { snapshotFile }; } async function listHistory() { try { const files = await fsp.readdir(DATA_DIR); return files .filter(f => /^robot_\d{8}_\d{6}\.json$/.test(f)) .sort() .reverse() .map(f => ({ filename: f, day: f.slice(6, 14), timestamp: f.slice(15, 21) })); } catch { return []; } } async function readSnapshot(ts) { const raw = await fsp.readFile(path.join(DATA_DIR, `robot_${ts}.json`), 'utf8'); return JSON.parse(raw); } // ── Pruning ────────────────────────────────────────────────────────────────── // // Regel: Pro Tag maximal 100 Snapshots. // Hat ein Tag mehr als 100, wird nur der neueste behalten. async function pruneSnapshots() { let files; try { files = await fsp.readdir(DATA_DIR); } catch { return; } const byDay = {}; for (const f of files) { if (!/^robot_\d{8}_\d{6}\.json$/.test(f)) continue; const day = f.slice(6, 14); if (!byDay[day]) byDay[day] = []; byDay[day].push(f); } for (const [day, dayFiles] of Object.entries(byDay)) { if (dayFiles.length <= 100) continue; dayFiles.sort(); // älteste zuerst const toDelete = dayFiles.slice(0, -1); // neueste behalten for (const f of toDelete) { try { await fsp.unlink(path.join(DATA_DIR, f)); } catch { /* ignorieren */ } } console.log(`[RobotConfigService] Tag ${day}: ${toDelete.length} Snapshots bereinigt, behalten: ${dayFiles[dayFiles.length - 1]}`); } } // ── Auth ───────────────────────────────────────────────────────────────────── function isAuthorized(req, key) { return (req.headers['authorization'] ?? '') === `Bearer ${key}`; } // ── Registrierung ───────────────────────────────────────────────────────────── function register(app, options = {}) { const apiKey = resolveApiKey(options.apiKey); const router = express.Router(); router.use(express.json({ limit: '5mb' })); router.get('/api/robot/history', async (req, res) => { try { return res.json({ history: await listHistory() }); } catch (err) { return res.status(500).json({ error: String(err) }); } }); router.get('/api/robot/history/:ts', async (req, res) => { try { return res.json(await readSnapshot(req.params.ts)); } catch { return res.status(404).json({ error: 'Snapshot nicht gefunden' }); } }); router.get('/api/robot', async (req, res) => { try { return res.json(await readRobotJson()); } catch { return res.status(404).json({ error: 'robot.json nicht gefunden' }); } }); router.put('/api/robot', async (req, res) => { if (!isAuthorized(req, apiKey)) { return res.status(401).json({ error: 'Unauthorized' }); } if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) { return res.status(400).json({ error: 'Body muss ein JSON-Objekt sein' }); } try { const result = await writeRobotJson(req.body); return res.json({ ok: true, ...result }); } catch (err) { return res.status(500).json({ error: String(err) }); } }); app.use(router); } module.exports = { register, readRobotJson, writeRobotJson, listHistory, readSnapshot };