feat: Gitea Webhook, IMAP, Settings & Deployment docs

- Webhook route and Gitea integration
- IMAP service and Nextcloud/Porkbun setup docs
- Settings UI improvements and API updates
- SSH/Webhook fix prompt for emailsorter.webklar.com
- Bootstrap, config and AI sorter updates
This commit is contained in:
2026-01-31 15:00:00 +01:00
parent 7e7ec1013b
commit cbb225c001
24 changed files with 2173 additions and 32 deletions

View File

@@ -417,7 +417,8 @@ Subject: ${subject}
Preview: ${snippet?.substring(0, 500) || 'No preview'}
RESPONSE FORMAT (JSON ONLY):
{"category": "category_key", "confidence": 0.0-1.0, "reason": "brief explanation"}
{"category": "category_key", "confidence": 0.0-1.0, "reason": "brief explanation", "assignedTo": "name_label_id_or_name_or_null"}
If the email is clearly FOR a specific worker (e.g. "für Max", "an Anna", subject/body mentions them), set assignedTo to that worker's id or name. Otherwise set assignedTo to null or omit it.
Respond ONLY with the JSON object.`
@@ -438,6 +439,15 @@ Respond ONLY with the JSON object.`
result.category = 'review'
}
// Validate assignedTo against name labels (id or name)
if (result.assignedTo && preferences.nameLabels?.length) {
const match = preferences.nameLabels.find(
l => l.enabled && (l.id === result.assignedTo || l.name === result.assignedTo)
)
if (!match) result.assignedTo = null
else result.assignedTo = match.id || match.name
}
return result
} catch (error) {
log.error('AI categorization failed', { error: error.message })
@@ -484,7 +494,8 @@ EMAILS:
${emailList}
RESPONSE FORMAT (JSON ARRAY ONLY):
[{"index": 0, "category": "key"}, {"index": 1, "category": "key"}, ...]
[{"index": 0, "category": "key", "assignedTo": "id_or_name_or_null"}, ...]
If an email is clearly FOR a specific worker, set assignedTo to that worker's id or name. Otherwise omit or null.
Respond ONLY with the JSON array.`
@@ -515,9 +526,16 @@ Respond ONLY with the JSON array.`
return emails.map((email, i) => {
const result = parsed.find(r => r.index === i)
const category = result?.category && CATEGORIES[result.category] ? result.category : 'review'
let assignedTo = result?.assignedTo || null
if (assignedTo && preferences.nameLabels?.length) {
const match = preferences.nameLabels.find(
l => l.enabled && (l.id === assignedTo || l.name === assignedTo)
)
assignedTo = match ? (match.id || match.name) : null
}
return {
email,
classification: { category, confidence: 0.8, reason: 'Batch' },
classification: { category, confidence: 0.8, reason: 'Batch', assignedTo },
}
})
} catch (error) {
@@ -578,6 +596,14 @@ Respond ONLY with the JSON array.`
}
}
// Name labels (workers) assign email to a person when clearly for them
if (preferences.nameLabels?.length) {
const activeNameLabels = preferences.nameLabels.filter(l => l.enabled)
if (activeNameLabels.length > 0) {
parts.push(`NAME LABELS (workers) assign email to ONE person when the email is clearly FOR that person (e.g. "für Max", "an Anna", "Max bitte prüfen", subject/body mentions them):\n${activeNameLabels.map(l => `- id: "${l.id}", name: "${l.name}"${l.keywords?.length ? `, keywords: ${JSON.stringify(l.keywords)}` : ''}`).join('\n')}\nIf the email is for a specific worker, set "assignedTo" to that label's id or name. Otherwise omit assignedTo.`)
}
}
return parts.length > 0 ? `USER PREFERENCES:\n${parts.join('\n')}\n` : ''
}