Persoenliche Angaben: fehlende settings-Zeile ist ein gueltiger Zustand

Neu angelegte Benutzer hatten keine settings-Zeile - die legte bisher nur
die Migration von Hand fuer den Admin an, POST /admin/users dagegen nicht.
Der Code ging aber davon aus, dass die Zeile immer existiert:

- /vorlagen lieferte fuer neue Benutzer 500: die View greift auf
  settings.name zu, bekam aber undefined.
- PUT /api/v1/settings verwarf Schreibzugriffe stillschweigend: das blanke
  UPDATE traf null Zeilen und meldete trotzdem success.

Statt Platzhalter-Zeilen zu provisionieren ist "keine Zeile" jetzt ueberall
ein gueltiger Zustand - genau wie bei prompts, design und app_state:

- loadSettings() (neben loadPrompts()/loadDesign()) liefert {} statt
  undefined; alle sechs Lesestellen gehen darueber.
- PUT /api/v1/settings ist ein Upsert, GET liefert {} statt leerem Body.
- Chat-Tool bewerbung_detail: JOIN auf jobangebote zusaetzlich ueber
  j.user_id = b.user_id, wie derselbe JOIN an anderer Stelle.

Repariert auch bereits angelegte Benutzer ohne Backfill-Migration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 22:16:45 +02:00
co-authored by Claude Opus 4.8
parent 0371aa85a5
commit b2e884d9b3
2 changed files with 35 additions and 12 deletions
+25 -9
View File
@@ -620,6 +620,22 @@ async function loadDesign() {
}
}
// The user's personal details (Persönliche Angaben). A user who has never saved
// the form has no settings row at all — that is a valid state, not an error, so
// this returns an empty object rather than undefined. Same contract as
// loadPrompts()/loadDesign(): "no row" means "nothing set yet". Every consumer
// (views, PDF generation, KI context) reads individual fields off the result,
// so they all degrade to empty instead of crashing.
async function loadSettings() {
try {
const row = await dbGet('SELECT * FROM settings WHERE user_id = ?', [currentUserId()]);
return row || {};
} catch (e) {
console.error('Konnte Persönliche Angaben nicht laden:', 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) {
@@ -779,7 +795,7 @@ async function runGeneration(bewerbungId, options = {}) {
if (!bewerbung) return;
const basisDokumente = await dbAll('SELECT * FROM basis_dokumente WHERE user_id = ? ORDER BY id ASC', [U]);
const basisAnhaenge = await dbAll('SELECT * FROM basis_anhaenge WHERE user_id = ? ORDER BY id ASC', [U]);
const settings = await dbGet('SELECT * FROM settings WHERE user_id = ?', [U]);
const settings = await loadSettings();
const prompts = await loadPrompts();
const design = await loadDesign();
@@ -1317,8 +1333,8 @@ initializeDatabase().then(async () => {
// Get settings
app.get('/api/settings', async (req, res) => {
try {
const settings = await dbGet('SELECT * FROM settings WHERE user_id = ?', [uid()]);
res.json(settings || {});
const settings = await loadSettings();
res.json(settings);
} catch (error) {
console.error('Error getting settings:', error);
res.status(500).json({ error: 'Serverfehler' });
@@ -1817,7 +1833,7 @@ initializeDatabase().then(async () => {
if (!bewerbung) return res.status(404).json({ error: 'Bewerbung nicht gefunden' });
const orig = await dbGet('SELECT * FROM emails WHERE id = ? AND bewerbung_id = ? AND user_id = ?', [req.body.email_id, id, uid()]);
if (!orig) return res.status(404).json({ error: 'Nachricht nicht gefunden' });
const settings = await dbGet('SELECT * FROM settings WHERE user_id = ?', [uid()]);
const settings = await loadSettings();
const draft = await generateEmailReply({
incoming: { from: orig.from_addr, subject: orig.subject, text: emailPlainText(orig) },
@@ -2136,7 +2152,7 @@ initializeDatabase().then(async () => {
res.render('vorlagen', {
basisDokumente,
basisAnhaenge,
settings: await dbGet('SELECT * FROM settings WHERE user_id = ?', [uid()]),
settings: await loadSettings(),
prompts: promptStore.list(await loadPrompts()),
designFelder: designStore.list(design),
designAngepasst: designStore.isAngepasst(design),
@@ -2276,7 +2292,7 @@ initializeDatabase().then(async () => {
// 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 user_id = ?', [uid()]);
const settings = await loadSettings();
const pdfs = renderDesignVorschau({
settings,
signatur: loadSignatur(),
@@ -2956,7 +2972,7 @@ initializeDatabase().then(async () => {
// assistant answer is streamed back via Server-Sent Events.
async function gatherChatContext() {
const [settings, profilRows, prompts] = await Promise.all([
dbGet('SELECT name FROM settings WHERE user_id = ?', [uid()]),
loadSettings(),
dbAll(
`SELECT inhalt FROM basis_dokumente
WHERE user_id = ? AND typ IN ('Lebenslauf', 'Profil/Kurzprofil') AND inhalt IS NOT NULL AND inhalt != ''
@@ -2970,7 +2986,7 @@ initializeDatabase().then(async () => {
// so the system prompt stays tiny regardless of how many bewerbungen exist.
const profil = (profilRows.map((r) => (r.inhalt || '').trim()).join('\n\n---\n\n')).slice(0, 1800);
const heute = new Date().toLocaleDateString('de-DE', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
return { heute, profil, prompts, settings: settings || {} };
return { heute, profil, prompts, settings };
}
// Ollama tool definitions the assistant can call to look up application data.
@@ -3065,7 +3081,7 @@ initializeDatabase().then(async () => {
b.interne_notizen, b.stellenbeschreibung, b.quelle_url,
j.kontakt_email AS ja_kontakt, j.ansprechpartner AS ja_ansprech, j.beschreibung AS ja_beschreibung
FROM bewerbungen b
LEFT JOIN jobangebote j ON j.verknuepfte_bewerbung_id = b.id
LEFT JOIN jobangebote j ON j.verknuepfte_bewerbung_id = b.id AND j.user_id = b.user_id
WHERE b.id = ? AND b.user_id = ?`,
[id, uid()]
);