Files
appRobotDriver/server/RobotConfigService.js
2026-06-11 22:05:45 +02:00

177 lines
6.3 KiB
JavaScript

// 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 };