Signature image in cover letter + fix per-paragraph capitalization

- Only the first body paragraph continues the salutation (lowercase safe
  opener); every following paragraph is a new sentence and is capitalized.
  Handled deterministically so it no longer depends on the model.
- Signature: upload a signature image on the Vorlagen page (preview / replace /
  remove). It is embedded in the cover letter directly under the closing
  salutation, replacing the typed name; falls back to the name when absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 11:37:03 +02:00
co-authored by Claude Opus 4.8
parent cbdbbc07e3
commit 78128db422
3 changed files with 168 additions and 20 deletions
+81
View File
@@ -95,6 +95,48 @@ const uploadBasisAnhang = multer({
limits: { fileSize: 15 * 1024 * 1024 }, // 15 MB
}).single('datei');
// Directory + uploader for the applicant's signature image (used in the letter)
const signaturDir = path.join(dataDir, 'signatur');
if (!fs.existsSync(signaturDir)) {
fs.mkdirSync(signaturDir, { recursive: true });
}
const uploadSignatur = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => cb(null, signaturDir),
filename: (req, file, cb) => {
const ext = (path.extname(file.originalname) || '.png').toLowerCase();
cb(null, `signatur_${Date.now()}${ext}`);
},
}),
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
fileFilter: (req, file, cb) => cb(null, /^image\/(png|jpe?g)$/.test(file.mimetype)),
}).single('signatur');
// The single stored signature file, if any.
function currentSignaturFile() {
try {
const files = fs.readdirSync(signaturDir).filter((f) => !f.startsWith('.'));
return files.length ? path.join(signaturDir, files[0]) : null;
} catch (e) {
return null;
}
}
// Read the signature as a data URL + jsPDF format, for embedding in the letter.
function loadSignatur() {
const file = currentSignaturFile();
if (!file) return null;
try {
const ext = path.extname(file).toLowerCase();
const format = (ext === '.jpg' || ext === '.jpeg') ? 'JPEG' : 'PNG';
const mime = format === 'JPEG' ? 'image/jpeg' : 'image/png';
const b64 = fs.readFileSync(file).toString('base64');
return { dataUrl: `data:${mime};base64,${b64}`, format };
} catch (e) {
return null;
}
}
// Database setup
const dbPath = path.join(dataDir, 'bewerbungen.db');
const db = new sqlite3.Database(dbPath);
@@ -184,6 +226,8 @@ async function runGeneration(bewerbungId) {
zusatzAnlagen: basisAnhaenge.map((a) => a.name || a.dateiname),
// Free-text notes (company address, contact person, extra context) for the LLM.
llmNotizen: bewerbung.llm_notizen || '',
// Signature image placed under the closing salutation (instead of the typed name).
signatur: loadSignatur(),
});
let seq = 0;
@@ -749,6 +793,7 @@ initializeDatabase().then(() => {
res.render('vorlagen', {
basisDokumente,
basisAnhaenge,
hasSignatur: Boolean(currentSignaturFile()),
basisTypOptions: BASIS_TYP_OPTIONS,
hasApiKey: Boolean(process.env.OLLAMA_API_KEY),
hideSettings: true,
@@ -860,6 +905,42 @@ initializeDatabase().then(() => {
}
});
// ----- Signature (Unterschrift) -----
// Serve the current signature image (for the preview on the Vorlagen page)
app.get('/unterschrift', (req, res) => {
const file = currentSignaturFile();
if (!file) return res.status(404).send('Keine Unterschrift');
res.sendFile(file);
});
// Upload / replace the signature
app.post('/unterschrift', (req, res) => {
uploadSignatur(req, res, (err) => {
try {
if (err) console.error('Signature upload error:', err.message);
if (req.file) {
// keep only the newly uploaded file
fs.readdirSync(signaturDir).forEach((f) => {
if (f !== req.file.filename) fs.promises.unlink(path.join(signaturDir, f)).catch(() => {});
});
}
res.redirect('/vorlagen');
} catch (error) {
console.error('Error saving signature:', error);
res.status(500).send('Serverfehler');
}
});
});
// Delete the signature
app.post('/unterschrift/delete', (req, res) => {
try {
fs.readdirSync(signaturDir).forEach((f) => fs.promises.unlink(path.join(signaturDir, f)).catch(() => {}));
} catch (e) { /* ignore */ }
res.redirect('/vorlagen');
});
// Download a generated attachment
app.get('/anhaenge/:id/download', async (req, res) => {
try {