Kein fertiger Stand, sondern ein Zwischenstand: Hier wurde im September 2026 angefangen, die App auf „Kurbeler" umzubenennen — Wortmarke, Icons, brand.ts, Benachrichtigungen, das Benutzerverzeichnis. Weitergegangen ist es dann im eigenen Repo `~/Dokumente/Projekte/kurbeler`, das die vollständige Historie dieses Repos mitgenommen hat. Committet wird das hier nicht, weil es gebraucht würde, sondern damit es nicht als Haufen unversionierter Dateien im Arbeitsverzeichnis verrottet. Wer in zwei Jahren nachsieht, findet so eine Geschichte statt eines Rätsels — und das Stammtisch-Wappen, das nur hier je existiert hat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LTw6xVYgjMy9AQtfs1Gdyu
221 lines
6.2 KiB
JavaScript
221 lines
6.2 KiB
JavaScript
/// <reference path="../pb_data/types.d.ts" />
|
|
|
|
// Wer erfährt wovon — und was davon in die Warteschlange kommt.
|
|
//
|
|
// Eigene Datei, weil PocketBase jeden Hook in einer eigenen JS-Laufzeit
|
|
// ausführt: Was neben onRecordAfterCreateSuccess steht, sieht der nächste
|
|
// Handler nicht. Er holt sich seine Helfer per require().
|
|
|
|
const PREFS = 'notification_prefs'
|
|
const QUEUE = 'notifications'
|
|
|
|
/**
|
|
* Was gilt, solange niemand etwas eingestellt hat.
|
|
*
|
|
* Die Linie ist bewusst gezogen: An heißt „betrifft mich", aus heißt „ist
|
|
* viel". Wer zu einem Event eingetragen wird, will das wissen — das passiert
|
|
* ihm. Jeder Kommentar an jedem Trail des Teams passiert dagegen ständig und
|
|
* meistens anderen; wer das will, schaltet es ein.
|
|
*
|
|
* Diese Voreinstellungen gelten auch dann, wenn ein Konto nie in die
|
|
* Einstellungen geschaut hat: Ein Datensatz in notification_prefs entsteht
|
|
* erst beim ersten Speichern.
|
|
*/
|
|
const DEFAULTS = {
|
|
enabled: true,
|
|
|
|
event_created: true, // neues Event im Team
|
|
event_participation: true, // du wurdest eingetragen, dein Status änderte sich
|
|
event_changed: true, // Termin oder Ort eines Events verschoben
|
|
trail_comment: false, // jeder Kommentar an jedem Trail
|
|
trail_marker: false, // jede neue Meldung an der Strecke
|
|
trail_status: true, // ein Trail wurde gesperrt oder wieder frei
|
|
team_member: false, // jemand ist ins Team gekommen
|
|
}
|
|
|
|
/** Überschriften der Arten, wie sie in der Zusammenfassung stehen. */
|
|
const LABELS = {
|
|
event_created: 'Neue Events',
|
|
event_participation: 'Deine Teilnahmen',
|
|
event_changed: 'Geänderte Events',
|
|
trail_comment: 'Kommentare',
|
|
trail_marker: 'Meldungen an der Strecke',
|
|
trail_status: 'Trail-Status',
|
|
team_member: 'Neu im Team',
|
|
}
|
|
|
|
/**
|
|
* Einstellungen eines Kontos, ergänzt um alles, was nicht drinsteht.
|
|
*
|
|
* Ein fehlender Datensatz ist kein Sonderfall, sondern der Normalfall bei
|
|
* jedem, der die Einstellungen nie geöffnet hat — er bekommt DEFAULTS.
|
|
*/
|
|
function prefsOf(app, userId) {
|
|
let record = null
|
|
try {
|
|
record = app.findFirstRecordByFilter(PREFS, 'user = {:user}', { user: userId })
|
|
} catch (err) {
|
|
record = null
|
|
}
|
|
|
|
if (!record) {
|
|
return DEFAULTS
|
|
}
|
|
|
|
const out = { enabled: record.getBool('enabled') }
|
|
for (const key in DEFAULTS) {
|
|
if (key !== 'enabled') {
|
|
out[key] = record.getBool(key)
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
/** Will dieses Konto diese Art von Meldung? */
|
|
function wants(app, userId, type) {
|
|
const prefs = prefsOf(app, userId)
|
|
return !!prefs.enabled && !!prefs[type]
|
|
}
|
|
|
|
/**
|
|
* Die IDs einer Relation als gewöhnliches Array — wie im Verzeichnis-Modul.
|
|
* `get()` gäbe je nach Feld einen String oder eine Go-Slice zurück.
|
|
*/
|
|
function idsOf(record, field) {
|
|
const raw = record.getStringSlice(field) || []
|
|
const out = []
|
|
|
|
for (let i = 0; i < raw.length; i++) {
|
|
if (raw[i]) {
|
|
out.push(raw[i])
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
/** Alle Konten eines Teams: Mitglieder und der Owner. */
|
|
function teamMembers(app, teamId) {
|
|
let team
|
|
try {
|
|
team = app.findRecordById('teams', teamId)
|
|
} catch (err) {
|
|
return []
|
|
}
|
|
|
|
const ids = idsOf(team, 'users')
|
|
const owner = idsOf(team, 'owner')[0]
|
|
if (owner && ids.indexOf(owner) === -1) {
|
|
ids.push(owner)
|
|
}
|
|
|
|
return ids
|
|
}
|
|
|
|
/**
|
|
* Einen Eintrag in die Warteschlange legen — für ein Konto.
|
|
*
|
|
* Wirft nie. Ein Auslöser hängt immer an einem Speichervorgang, den jemand
|
|
* gerade ausgelöst hat: Ein neues Event darf nicht daran scheitern, dass ein
|
|
* Hinweis nicht geschrieben werden konnte.
|
|
*/
|
|
function queueFor(app, userId, entry) {
|
|
if (!userId || !entry || !entry.type) {
|
|
return
|
|
}
|
|
|
|
// Der Auslöser selbst bekommt keine Meldung über sein eigenes Tun. Wer
|
|
// ein Event anlegt, weiß, dass es eins gibt.
|
|
if (entry.actor && entry.actor === userId) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
if (!wants(app, userId, entry.type)) {
|
|
return
|
|
}
|
|
|
|
const record = new Record(app.findCollectionByNameOrId(QUEUE))
|
|
record.set('user', userId)
|
|
record.set('type', entry.type)
|
|
record.set('title', entry.title || '')
|
|
record.set('body', entry.body || '')
|
|
record.set('link', entry.link || '')
|
|
if (entry.team) {
|
|
record.set('team', entry.team)
|
|
}
|
|
app.save(record)
|
|
} catch (err) {
|
|
console.log('[notifications] konnte nicht einreihen:', err)
|
|
}
|
|
}
|
|
|
|
/** Denselben Eintrag für ein ganzes Team einreihen. */
|
|
function queueForTeam(app, teamId, entry) {
|
|
const members = teamMembers(app, teamId)
|
|
const withTeam = Object.assign({}, entry, { team: teamId })
|
|
|
|
for (let i = 0; i < members.length; i++) {
|
|
queueFor(app, members[i], withTeam)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Das Konto hinter einem Fahrer — oder leer, wenn er keins hat.
|
|
* Ohne Konto gibt es keine Adresse und damit nichts zu benachrichtigen.
|
|
*/
|
|
function userOfRider(app, riderId) {
|
|
try {
|
|
return app.findRecordById('riders', riderId).getString('user')
|
|
} catch (err) {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
/** Anzeigename eines Kontos für Sätze wie „X hat kommentiert". */
|
|
function nameOf(app, userId) {
|
|
if (!userId) {
|
|
return 'Jemand'
|
|
}
|
|
|
|
try {
|
|
const u = app.findRecordById('users', userId)
|
|
return u.getString('name') || u.getString('email') || 'Jemand'
|
|
} catch (err) {
|
|
return 'Jemand'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Einträge als versendet abhaken. Einzeln, damit ein hängender Datensatz
|
|
* nicht die ganze Gruppe mitreißt und morgen alles doppelt rausgeht.
|
|
*/
|
|
function markSent(app, items) {
|
|
const now = new Date().toISOString().replace('T', ' ')
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
try {
|
|
items[i].set('sent', now)
|
|
app.save(items[i])
|
|
} catch (err) {
|
|
console.log('[notifications] konnte nicht abhaken:', err)
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
PREFS: PREFS,
|
|
QUEUE: QUEUE,
|
|
DEFAULTS: DEFAULTS,
|
|
LABELS: LABELS,
|
|
prefsOf: prefsOf,
|
|
wants: wants,
|
|
idsOf: idsOf,
|
|
teamMembers: teamMembers,
|
|
queueFor: queueFor,
|
|
queueForTeam: queueForTeam,
|
|
userOfRider: userOfRider,
|
|
nameOf: nameOf,
|
|
markSent: markSent,
|
|
}
|