Generate an editable norm-compliant cover email per application

Alongside the tailored PDFs, the LLM now produces a short, DIN-style
Begleit-E-Mail (subject + body) grounded in the same data: same
salutation as the cover letter, the target role, a line or two of core
motivation, a pointer to the attached documents, and a friendly close.

Persisted in new bewerbungen columns (email_betreff, email_anschreiben)
and surfaced in an editable subject field + textarea on the detail page,
so the applicant can tweak it before sending; a copy button puts subject
and body on the clipboard. Values are stored raw and HTML-escaped on
render to avoid double-escaping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 15:20:56 +02:00
co-authored by Claude Opus 4.8
parent 0bfa5683fb
commit 3c553cadf5
3 changed files with 113 additions and 5 deletions
+28 -3
View File
@@ -254,7 +254,7 @@ async function runGeneration(bewerbungId) {
const basisAnhaenge = await dbAll('SELECT * FROM basis_anhaenge ORDER BY id ASC');
const settings = await dbGet('SELECT * FROM settings WHERE id = 1');
const documents = await generateApplicationDocuments({
const { documents, email } = await generateApplicationDocuments({
job: {
firma: bewerbung.firma,
stelle: bewerbung.stelle,
@@ -297,8 +297,9 @@ async function runGeneration(bewerbungId) {
}
await dbRun(
"UPDATE bewerbungen SET generierung_status = 'fertig', generierung_fehler = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
[bewerbungId]
"UPDATE bewerbungen SET generierung_status = 'fertig', generierung_fehler = NULL, " +
"email_betreff = ?, email_anschreiben = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
[(email && email.betreff) || '', (email && email.text) || '', bewerbungId]
);
console.log(`Bewerbungsunterlagen für #${bewerbungId} generiert (${documents.length} Dokument(e)).`);
} catch (error) {
@@ -340,6 +341,8 @@ function initializeDatabase() {
db.run('ALTER TABLE bewerbungen ADD COLUMN quelle_url TEXT', () => {
db.run('ALTER TABLE bewerbungen ADD COLUMN generierung_status TEXT', () => {
db.run('ALTER TABLE bewerbungen ADD COLUMN generierung_fehler TEXT', () => {
db.run('ALTER TABLE bewerbungen ADD COLUMN email_betreff TEXT', () => {
db.run('ALTER TABLE bewerbungen ADD COLUMN email_anschreiben TEXT', () => {
db.run('ALTER TABLE bewerbungen ADD COLUMN llm_notizen TEXT', () => {
// Chronological status changes, each with an optional comment
@@ -435,6 +438,8 @@ function initializeDatabase() {
});
});
});
});
});
});
});
});
@@ -774,6 +779,26 @@ initializeDatabase().then(() => {
}
});
// Save the editable e-mail cover text (Begleit-E-Mail) after the user tweaks it.
app.post('/bewerbung/:id/email', async (req, res) => {
try {
const { id } = req.params;
const bewerbung = await dbGet('SELECT id FROM bewerbungen WHERE id = ?', [id]);
if (!bewerbung) return res.status(404).send('Bewerbung nicht gefunden');
// Store raw; the value is HTML-escaped on render (EJS <%= %>), matching how
// the generated e-mail is stored. Sanitising here would double-escape.
await dbRun(
'UPDATE bewerbungen SET email_betreff = ?, email_anschreiben = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[String(req.body.email_betreff || ''), String(req.body.email_anschreiben || ''), id]
);
res.redirect('/bewerbung/' + id + '#email');
} catch (error) {
console.error('Error saving e-mail text:', error);
res.status(500).send('Serverfehler');
}
});
// Add a timeline entry (status change with date + comment)
app.post('/bewerbung/:id/verlauf', async (req, res) => {
try {