Files
buchwerk/frontend/mock/server.js
2026-09-27 21:36:33 +02:00

134 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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}`))