Design der Unterlagen unter Vorlagen einstellbar

Akzentfarbe, Sidebar-Hintergrund, Foto (an/aus, eckig/rund) und Schriftgroesse
lagen als Konstanten im Renderer. Sie kommen jetzt aus lib/design.js und sind
unter Vorlagen waehlbar, inklusive Vorschau-PDF mit Musterinhalten - die zeigt
das Ergebnis, ohne dass die KI laufen muss.

Bewusst geschlossen gehalten: die Palette ist eine kurze Liste gedeckter Toene
(kein freier Color-Picker), Grauwerte und Grundlayout bleiben fest. Die
Schriftgroesse ist nur der Startwert der Seitenskalierung - der Lebenslauf passt
weiterhin garantiert auf eine Seite.

Das Theme wird durch die Render-Funktionen gereicht statt als Modulzustand
gesetzt, damit spaeter mehrere Bewerber parallel generieren koennen, ohne sich
gegenseitig das Design umzustellen.

Rundes Foto: das Bild fuellt den Kreis formatfuellend und zentriert (Clip statt
Skalieren), sonst wuerde ein Hochformat-Foto im Quadrat gestaucht.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 00:51:36 +02:00
co-authored by Claude Opus 4.8
parent 65a5943993
commit 9f7349af4e
4 changed files with 546 additions and 128 deletions
+95 -1
View File
@@ -27,9 +27,10 @@ const multer = require('multer');
}
})();
const { generateApplicationDocuments, generateEmailReply } = require('./lib/documents');
const { generateApplicationDocuments, generateEmailReply, renderDesignVorschau } = require('./lib/documents');
const chat = require('./lib/chat');
const promptStore = require('./lib/prompts');
const designStore = require('./lib/design');
const mailer = require('./lib/mailer');
const { createExternalApi } = require('./lib/api');
const { buildOpenApiSpec } = require('./lib/openapi');
@@ -439,6 +440,18 @@ async function loadPrompts() {
}
}
// The user's design overrides as { key: value }. Unset keys keep the defaults
// from lib/design.js. Read fresh per generation, like the prompts.
async function loadDesign() {
try {
const rows = await dbAll('SELECT key, value FROM design');
return Object.fromEntries(rows.map((r) => [r.key, r.value]));
} catch (e) {
console.error('Konnte Design nicht laden, nutze Standardwerte:', e.message);
return {};
}
}
// Match an incoming message to an application: first via In-Reply-To/References
// pointing at one of our sent messages, then by sender = a previous recipient.
async function matchBewerbung(msg) {
@@ -598,6 +611,7 @@ async function runGeneration(bewerbungId, options = {}) {
const basisAnhaenge = await dbAll('SELECT * FROM basis_anhaenge ORDER BY id ASC');
const settings = await dbGet('SELECT * FROM settings WHERE id = 1');
const prompts = await loadPrompts();
const design = await loadDesign();
// Only the explicitly selected extra attachments are enclosed (default: none).
const anlagenIds = Array.isArray(options.anlagenIds) ? options.anlagenIds.map(Number) : [];
@@ -614,6 +628,7 @@ async function runGeneration(bewerbungId, options = {}) {
basisDokumente,
settings,
prompts,
design,
// Names of the selected attachments so the cover letter (and the LLM) lists
// exactly these under "Anlagen".
zusatzAnlagen: selectedAnhaenge.map((a) => a.name || a.dateiname),
@@ -967,6 +982,18 @@ function initializeDatabase() {
)
`, () => {});
// Design choices for the generated PDFs (accent colour, sidebar tint,
// photo shape, font size). Same contract as `prompts`: only deviations
// are stored, and lib/design.js validates every value on read, so a
// stale row can never produce a broken document.
db.run(`
CREATE TABLE IF NOT EXISTS design (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`, () => {});
db.run(`
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY CHECK (id = 1),
@@ -1908,10 +1935,14 @@ initializeDatabase().then(() => {
try {
const basisDokumente = await dbAll('SELECT * FROM basis_dokumente ORDER BY id ASC');
const basisAnhaenge = await dbAll('SELECT * FROM basis_anhaenge ORDER BY id ASC');
const design = await loadDesign();
res.render('vorlagen', {
basisDokumente,
basisAnhaenge,
prompts: promptStore.list(await loadPrompts()),
designFelder: designStore.list(design),
designAngepasst: designStore.isAngepasst(design),
designFotoAn: designStore.settings(design).foto_anzeigen === '1',
hasSignatur: Boolean(currentSignaturFile()),
hasFoto: Boolean(currentFotoFile()),
basisTypOptions: BASIS_TYP_OPTIONS,
@@ -1962,6 +1993,69 @@ initializeDatabase().then(() => {
}
});
// ----- Design of the generated PDFs -----
// Save the design choices. Values are validated by lib/design.js on read, and
// a choice equal to the default is stored as a deletion, so the DB only ever
// holds real deviations.
app.post('/vorlagen/design', async (req, res) => {
try {
const gewaehlt = designStore.settings(req.body);
for (const [key, value] of Object.entries(gewaehlt)) {
if (value === designStore.DEFAULTS[key]) {
await dbRun('DELETE FROM design WHERE key = ?', [key]);
} else {
await dbRun(
`INSERT INTO design (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`,
[key, value]
);
}
}
res.redirect('/vorlagen#design');
} catch (error) {
console.error('Error saving design:', error);
res.status(500).send('Serverfehler');
}
});
// Back to the shipped design.
app.post('/vorlagen/design/reset', async (req, res) => {
try {
await dbRun('DELETE FROM design');
res.redirect('/vorlagen#design');
} catch (error) {
console.error('Error resetting design:', error);
res.status(500).send('Serverfehler');
}
});
// Preview PDF (Anschreiben or Lebenslauf) with sample content — lets the user
// see a design choice without spending an LLM run. Query params override the
// saved design, so the form can preview a selection before it is saved.
app.get('/vorlagen/design/vorschau/:doc.pdf', async (req, res) => {
try {
const welches = req.params.doc === 'lebenslauf' ? 'lebenslauf' : 'anschreiben';
const gespeichert = await loadDesign();
// Only known design keys from the query are honoured; design.settings()
// drops anything invalid.
const design = { ...gespeichert, ...req.query };
const settings = await dbGet('SELECT * FROM settings WHERE id = 1');
const pdfs = renderDesignVorschau({
settings,
signatur: loadSignatur(),
bewerbungsfoto: loadFoto(),
design,
});
res.type('application/pdf');
res.setHeader('Content-Disposition', `inline; filename="Vorschau_${welches}.pdf"`);
res.send(pdfs[welches]);
} catch (error) {
console.error('Error rendering design preview:', error);
res.status(500).send('Vorschau konnte nicht erzeugt werden');
}
});
// Add a base document
app.post('/vorlagen', async (req, res) => {
try {