// Kleiner statischer Mock des Backends laut API.md für den Sichttest: node mock/server.js (Port 8020, PORT überschreibt) import http from 'node:http' const PORT = Number(process.env.PORT || 8020) const jetzt = Date.now() const iso = (minVor) => new Date(jetzt - minVor * 60000).toISOString() const topics = [ { name: 'Godot 4', titel: 'Godot 4', status: 'laeuft', laeuft: true, stufe: 'schreiben' }, { name: 'Symfony 7', titel: 'Symfony 7', status: 'neu', laeuft: false, stufe: '' }, ] const bericht = `## Ergebnis Kapitel 3 geschrieben, 4 Abschnitte, 6 900 Wörter. - Quellen: \`quellen/godot-docs/tutorials/scripting/gdscript/\` - Offen: Abschnitt 3.4 braucht ein Beispiel zu \`await\` | Abschnitt | Wörter | |---|---| | 3.1 | 1400 | | 3.2 | 1900 | ` const state = { name: 'Godot 4', titel: 'Godot 4', hinweise: 'für Entwickler, ab Version 4.6', status: 'laeuft', grund: '', stufe: 'schreiben', deutung: { titel: 'Godot 4', untertitel: 'Architektur, Konzepte und Praxis der Engine', buchtyp: 'werkzeug', zielgruppe: 'Entwickler mit Programmiererfahrung, neu in Godot', referenz: 'Godot 4.6, offizielle Dokumentation', deutung: 'Ein Werkzeugbuch: erklärt die Engine entlang ihrer Konzepte (Szenenbaum, Signale, GDScript) und zeigt jeweils die Praxis.' }, stufen: [ { id: 'recherche', titel: 'Thema deuten und Quellen beschaffen', status: 'fertig', fertig: 1, gesamt: 1, kosten_usd: 1.2, tokens: 250000, gestartet: iso(60), beendet: iso(52) }, { id: 'gliederung', titel: 'Gliederung', status: 'fertig', fertig: 1, gesamt: 1, kosten_usd: 0.8, tokens: 120000, gestartet: iso(52), beendet: iso(45) }, { id: 'schreiben', titel: 'Kapitel schreiben', status: 'laeuft', fertig: 3, gesamt: 18, kosten_usd: 4.1, tokens: 1100000, gestartet: iso(45), beendet: '' }, { id: 'pruefung', titel: 'Kapitel prüfen', status: 'laeuft', fertig: 1, gesamt: 18, kosten_usd: 0.9, tokens: 300000, gestartet: iso(30), beendet: '' }, { id: 'konsistenz', titel: 'Querverweise und Vorwort', status: 'offen', fertig: 0, gesamt: 1, kosten_usd: 0, tokens: 0, gestartet: '', beendet: '' }, { id: 'montage', titel: 'Buch zusammensetzen', status: 'offen', fertig: 0, gesamt: 1, kosten_usd: 0, tokens: 0, gestartet: '', beendet: '' }, ], kosten_usd: 7.0, tokens: 1770000, agenten: [ { id: 'recherche', stufe: 'recherche', name: 'Recherche', status: 'fertig', kosten_usd: 1.2, tokens: 250000, werkzeuge: 58, gestartet: iso(60), beendet: iso(52), bericht: '## Quellen\n\n12 Quellen geladen, davon 9 brauchbar.' }, { id: 'gliederung', stufe: 'gliederung', name: 'Gliederung', status: 'fertig', kosten_usd: 0.8, tokens: 120000, werkzeuge: 14, gestartet: iso(52), beendet: iso(45), bericht: 'Gliederung mit 18 Kapiteln in 4 Teilen.' }, { id: 'schreiben-01', stufe: 'schreiben', name: 'Kapitel 1 schreiben', status: 'fertig', kosten_usd: 0.9, tokens: 180000, werkzeuge: 41, gestartet: iso(45), beendet: iso(38), bericht }, { id: 'schreiben-02', stufe: 'schreiben', name: 'Kapitel 2 schreiben', status: 'fehler', kosten_usd: 0.3, tokens: 60000, werkzeuge: 9, gestartet: iso(45), beendet: iso(42), bericht: '' }, { id: 'schreiben-03', stufe: 'schreiben', name: 'Kapitel 3 schreiben', status: 'fertig', kosten_usd: 1.1, tokens: 220000, werkzeuge: 52, gestartet: iso(38), beendet: iso(30), bericht }, { id: 'schreiben-04', stufe: 'schreiben', name: 'Kapitel 4 schreiben', status: 'laeuft', kosten_usd: 0.4, tokens: 90000, werkzeuge: 17, gestartet: iso(8), beendet: '', bericht: '' }, { id: 'pruefung-01', stufe: 'pruefung', name: 'Kapitel 1 prüfen', status: 'fertig', kosten_usd: 0.5, tokens: 150000, werkzeuge: 22, gestartet: iso(30), beendet: iso(20), bericht: '## Prüfung\n\nAlle Aussagen belegt. Zwei Formulierungen korrigiert.' }, { id: 'pruefung-03', stufe: 'pruefung', name: 'Kapitel 3 prüfen', status: 'laeuft', kosten_usd: 0.4, tokens: 150000, werkzeuge: 30, gestartet: iso(12), beendet: '', bericht: '' }, ], ereignisse: Array.from({ length: 100 }, (_, i) => ({ zeit: iso(100 - i), agent: i % 3 ? 'schreiben-04' : 'pruefung-03', art: ['tool', 'text', 'start', 'ende', 'fehler'][i % 5], text: i % 5 === 0 ? 'Write buch/04-szenen.md' : i % 5 === 1 ? 'Ich lese jetzt die Dokumentation zum Szenenbaum und gleiche die Signale ab.' : `Ereignis ${i}`, })), } const text = (n) => Array.from({ length: n }, (_, i) => `Absatz ${i + 1}: Der Szenenbaum ist die zentrale Datenstruktur von Godot. Jeder Knoten hat genau einen Elternknoten, und Signale verbinden Knoten lose miteinander.`).join('\n\n') const code = '\n\n```gdscript\nextends Node2D\n\nsignal getroffen(schaden: int)\n\nfunc _ready() -> void:\n\tgetroffen.connect(_on_getroffen)\n\nfunc _on_getroffen(schaden: int) -> void:\n\tprint("Schaden: %d" % schaden)\n```\n\n| Knoten | Zweck |\n|---|---|\n| Node2D | 2D-Transform |\n| Control | UI |\n\n' const kap = (ord, titel, teil, status, n) => ({ id: String(ord).padStart(2, '0'), ord, titel, teil, datei: `buch/${String(ord).padStart(2, '0')}.md`, status, intro: status === 'fehlt' ? '' : `Worum es in Kapitel ${ord} geht: ${titel}. Dieses Kapitel führt in die Grundlagen ein.`, abschnitte: status === 'fehlt' ? [] : Array.from({ length: n }, (_, i) => ({ id: `${String(ord).padStart(2, '0')}-${i + 1}`, nr: `${ord}.${i + 1}`, titel: `Abschnitt ${i + 1} von ${titel}`, text: text(4) + (i === 0 ? code : ''), woerter: 1400, minuten: 8 })), woerter: status === 'fehlt' ? 0 : n * 1400, minuten: status === 'fehlt' ? 0 : n * 8, berichte: status === 'fehlt' ? { autor: '', pruefer: '' } : { autor: bericht, pruefer: status === 'geschrieben' ? '' : '## Prüfung\n\n- 3.1: belegt\n- 3.2: eine Zahl korrigiert' }, }) const guide = { titel: 'Godot 4', untertitel: 'Architektur, Konzepte und Praxis der Engine', status: 'laeuft', vorwort: 'Dieses Buch erklärt Godot 4 entlang seiner Konzepte.\n\nEs richtet sich an Entwickler, die die Engine **verstehen** wollen, nicht nur bedienen.', kapitel: [ kap(1, 'Die Engine im Überblick', 'Teil I – Grundlagen', 'konsistent', 3), kap(2, 'Szenen und Knoten', 'Teil I – Grundlagen', 'fehlt', 0), kap(3, 'GDScript', 'Teil I – Grundlagen', 'geprueft', 4), kap(4, 'Signale', 'Teil II – Praxis', 'geschrieben', 2), ], woerter: 12600, minuten: 72, gliederung: '# Gliederung\n\n## Teil I – Grundlagen\n\n1. Die Engine im Überblick\n2. Szenen und Knoten\n3. GDScript\n\n## Teil II – Praxis\n\n4. Signale', quellen: '# Quellen\n\n- Godot-Dokumentation 4.6 (docs.godotengine.org)\n- Engine-Quelltext, Tag 4.6-stable\n\n## Prüfmethode\n\nJedes Kapitel wird von einem unabhängigen Agenten gegen die Quellen geprüft.', pdf: false, } function json(res, code, body) { res.writeHead(code, { 'content-type': 'application/json' }) res.end(JSON.stringify(body)) } http.createServer((req, res) => { const url = new URL(req.url, 'http://x') const p = url.pathname let m if (req.method === 'GET' && p === '/api/topics') return json(res, 200, topics) if (req.method === 'POST' && p === '/api/topics') { let body = '' req.on('data', (c) => body += c) return req.on('end', () => { const { name } = JSON.parse(body || '{}') const t = { name, titel: name, status: 'neu', laeuft: false, stufe: '' } topics.push(t); json(res, 200, t) }) } if ((m = p.match(/^\/api\/topics\/([^/]+)$/)) && req.method === 'DELETE') { const i = topics.findIndex((t) => t.name === decodeURIComponent(m[1])) if (i >= 0) topics.splice(i, 1) return json(res, 200, { ok: true }) } if ((m = p.match(/^\/api\/topics\/([^/]+)\/(start|pause|reset)$/)) && req.method === 'POST') { const t = topics.find((t) => t.name === decodeURIComponent(m[1])) if (!t) return json(res, 404, { detail: 'unbekanntes Thema' }) t.status = m[2] === 'start' ? 'laeuft' : m[2] === 'pause' ? 'pausiert' : 'neu'; t.laeuft = t.status === 'laeuft' if (t.name === state.name) state.status = t.status return json(res, 200, { ok: true }) } if ((m = p.match(/^\/api\/topics\/([^/]+)\/state$/))) { const name = decodeURIComponent(m[1]) const t = topics.find((t) => t.name === name) if (!t) return json(res, 404, { detail: 'unbekanntes Thema' }) if (name === state.name) return json(res, 200, state) return json(res, 200, { name, titel: t.titel, hinweise: '', status: t.status, grund: '', stufe: '', deutung: null, stufen: state.stufen.map((s) => ({ ...s, status: 'offen', fertig: 0, kosten_usd: 0, tokens: 0, gestartet: '', beendet: '' })), kosten_usd: 0, tokens: 0, agenten: [], ereignisse: [] }) } if ((m = p.match(/^\/api\/topics\/([^/]+)\/guide$/))) { const name = decodeURIComponent(m[1]) if (name === state.name) return json(res, 200, guide) return json(res, 200, { titel: name, untertitel: '', status: 'neu', vorwort: '', kapitel: [], woerter: 0, minuten: 0, gliederung: '', quellen: '', pdf: false }) } if ((m = p.match(/^\/api\/topics\/([^/]+)\/guide\.md$/))) { res.writeHead(200, { 'content-type': 'text/markdown; charset=utf-8' }); return res.end('# ' + guide.titel + '\n') } if ((m = p.match(/^\/api\/topics\/([^/]+)\/buch\.pdf$/))) return json(res, 404, { detail: 'PDF noch nicht gebaut' }) json(res, 404, { detail: 'nicht gefunden: ' + p }) }).listen(PORT, () => console.log(`Mock-Backend auf http://localhost:${PORT}`))