feat: BL-07–BL-10 — notes search, weather refresh, vCard import/export, PWA offline page

- Notes: client-side full-text search bar (filters title + content)
- Dashboard: weather refresh button + 30-min auto-refresh interval
- Contacts: vCard 3.0 export per contact (GET /:id/vcard); vCard import
  via file input with client-side parser (FN, TEL, EMAIL, ADR, NOTE, CATEGORIES)
- PWA: /offline.html served when network unavailable; cached in app-shell (sw v20)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ulas
2026-03-31 10:35:03 +02:00
parent 0defc3c589
commit 4fe4f6cb38
10 changed files with 374 additions and 10 deletions
+38
View File
@@ -160,4 +160,42 @@ router.get('/meta', (_req, res) => {
}
});
/**
* GET /api/v1/contacts/:id/vcard
* Kontakt als vCard 3.0 (.vcf) exportieren.
* Response: text/vcard Dateidownload
*/
router.get('/:id/vcard', (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const contact = db.get().prepare('SELECT * FROM contacts WHERE id = ?').get(id);
if (!contact) return res.status(404).json({ error: 'Kontakt nicht gefunden', code: 404 });
const esc = (v) => String(v || '').replace(/\n/g, '\\n').replace(/,/g, '\\,').replace(/;/g, '\\;');
const lines = [
'BEGIN:VCARD',
'VERSION:3.0',
`FN:${esc(contact.name)}`,
`N:${esc(contact.name)};;;;`,
];
if (contact.phone) lines.push(`TEL;TYPE=VOICE:${esc(contact.phone)}`);
if (contact.email) lines.push(`EMAIL:${esc(contact.email)}`);
if (contact.address) lines.push(`ADR;TYPE=HOME:;;${esc(contact.address)};;;;`);
if (contact.notes) lines.push(`NOTE:${esc(contact.notes)}`);
if (contact.category) lines.push(`CATEGORIES:${esc(contact.category)}`);
lines.push('END:VCARD');
const vcf = lines.join('\r\n');
const filename = encodeURIComponent(contact.name.replace(/[^a-zA-Z0-9-_ ]/g, '_')) + '.vcf';
res.setHeader('Content-Type', 'text/vcard; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.send(vcf);
} catch (err) {
console.error('[contacts/GET /:id/vcard]', err);
res.status(500).json({ error: 'Interner Fehler', code: 500 });
}
});
module.exports = router;