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:
+10
-3
@@ -478,8 +478,10 @@ function createExternalApi(deps) {
|
||||
// --- Settings ------------------------------------------------------
|
||||
router.get('/settings', async (req, res) => {
|
||||
try {
|
||||
// A user who never saved their personal details has no settings row; that
|
||||
// is a valid state, so answer with an empty object instead of no body.
|
||||
const settings = await dbGet('SELECT * FROM settings WHERE user_id = ?', [uid()]);
|
||||
res.json(settings);
|
||||
res.json(settings || {});
|
||||
} catch (error) {
|
||||
console.error('API get settings error:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
@@ -489,9 +491,14 @@ function createExternalApi(deps) {
|
||||
router.put('/settings', async (req, res) => {
|
||||
try {
|
||||
const { name, adresse, kundennummer } = req.body || {};
|
||||
// Upsert, not UPDATE: a user without a settings row would otherwise match
|
||||
// zero rows and the write would be silently dropped.
|
||||
await dbRun(
|
||||
'UPDATE settings SET name = ?, adresse = ?, kundennummer = ? WHERE user_id = ?',
|
||||
[sanitizeInput(name), sanitizeInput(adresse), sanitizeInput(kundennummer), uid()]
|
||||
`INSERT INTO settings (user_id, name, adresse, kundennummer)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
name = excluded.name, adresse = excluded.adresse, kundennummer = excluded.kundennummer`,
|
||||
[uid(), sanitizeInput(name), sanitizeInput(adresse), sanitizeInput(kundennummer)]
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
|
||||
@@ -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()]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user