stammtisch-hersbruck/backend/pb_migrations/1757700000_notifications.js
Daniel Michelberger 279796111c wip: Der abgebrochene Kurbeler-Umbau, wie er hier liegengeblieben ist
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
2026-09-11 15:42:33 +02:00

158 lines
7.1 KiB
JavaScript

/// <reference path="../pb_data/types.d.ts" />
// Benachrichtigungen — eine Mail am Tag, und jeder bestimmt selbst, worüber.
//
// Zwei Collections, weil zwei verschiedene Dinge gebraucht werden:
//
// notification_prefs — was will jemand wissen? Ein Datensatz je Konto.
// notifications — was ist passiert? Die Warteschlange bis zum Versand.
//
// Warum eine Warteschlange und nicht sofort eine Mail? Weil sonst ein
// Trailkommentar, dem drei Antworten folgen, vier Mails auslöst — und ein
// Verein, der sich die nicht leisten kann, das Ganze am dritten Tag
// abschaltet. Der Cron-Job in pb_hooks/notifications.pb.js nimmt einmal
// täglich alles Unversendete, macht eine Mail daraus und hakt es ab.
//
// Die Warteschlange ist deshalb kein Zwischenspeicher, sondern der eigentliche
// Mechanismus: Der Auslöser schreibt hinein und ist fertig. Ob und wann etwas
// verschickt wird, entscheidet allein der Job. Ein Fehler beim Mailversand
// bringt damit auch nie das Speichern eines Events durcheinander.
//
// Wer nichts einstellt, hat trotzdem Voreinstellungen: `notification_prefs`
// entsteht erst beim ersten Speichern. Was ohne Datensatz gilt, steht als
// DEFAULTS in pb_hooks/notifications_lib.js — an einer Stelle, gültig für
// Auslöser und Oberfläche.
const OWN_RECORD = '@request.auth.id != "" && user = @request.auth.id'
migrate((app) => {
const users = app.findCollectionByNameOrId('users')
const teams = app.findCollectionByNameOrId('teams')
// --- Einstellungen ----------------------------------------------------
//
// Alles, was jemand über sich selbst festlegt. Die Regeln lassen nur den
// eigenen Datensatz zu — auch beim Anlegen, sonst schriebe jemand
// Einstellungen für ein fremdes Konto und bestimmte, was ein anderer
// erfährt.
try {
app.findCollectionByNameOrId('notification_prefs')
} catch {
const prefs = new Collection({
id: 'pbc_notification_prefs',
type: 'base',
name: 'notification_prefs',
listRule: OWN_RECORD,
viewRule: OWN_RECORD,
createRule: '@request.auth.id != "" && @request.body.user = @request.auth.id',
updateRule: OWN_RECORD,
deleteRule: OWN_RECORD,
fields: [
{
id: 'relation_prefs_user',
name: 'user',
type: 'relation',
required: true,
maxSelect: 1,
collectionId: users.id,
cascadeDelete: true,
},
// Hauptschalter. Aus heißt: gar keine Mails, egal was die
// einzelnen Schalter sagen. Er steht vor allem für den Fall,
// dass jemand für zwei Wochen Ruhe haben will, ohne sich
// danach alles neu zusammenzuklicken.
{ id: 'bool_prefs_enabled', name: 'enabled', type: 'bool' },
{ id: 'bool_prefs_event_created', name: 'event_created', type: 'bool' },
{ id: 'bool_prefs_event_participation', name: 'event_participation', type: 'bool' },
{ id: 'bool_prefs_event_changed', name: 'event_changed', type: 'bool' },
{ id: 'bool_prefs_trail_comment', name: 'trail_comment', type: 'bool' },
{ id: 'bool_prefs_trail_status', name: 'trail_status', type: 'bool' },
{ id: 'bool_prefs_trail_marker', name: 'trail_marker', type: 'bool' },
{ id: 'bool_prefs_team_member', name: 'team_member', type: 'bool' },
{ id: 'autodate_created', name: 'created', type: 'autodate', onCreate: true, onUpdate: false },
{ id: 'autodate_updated', name: 'updated', type: 'autodate', onCreate: true, onUpdate: true },
],
indexes: [
// Ein Datensatz je Konto. Zwei wären zwei Wahrheiten, und
// welche gilt, entschiede die Sortierung.
'CREATE UNIQUE INDEX `idx_notification_prefs_user` ON `notification_prefs` (`user`)',
],
})
app.save(prefs)
}
// --- Warteschlange ----------------------------------------------------
//
// Lesen darf jeder seine eigenen Einträge — daraus wird später einmal
// eine Glocke in der Oberfläche. Schreiben darf über die API niemand:
// Alle vier Regeln stehen auf null, was in PocketBase „nur Superuser"
// heißt. Gefüllt wird ausschließlich aus den Hooks, und die laufen an den
// Regeln vorbei. Sonst schriebe sich jeder selbst Meldungen — oder,
// schlimmer, anderen.
try {
app.findCollectionByNameOrId('notifications')
} catch {
const queue = new Collection({
id: 'pbc_notifications',
type: 'base',
name: 'notifications',
listRule: OWN_RECORD,
viewRule: OWN_RECORD,
createRule: null,
updateRule: null,
deleteRule: null,
fields: [
{
id: 'relation_notification_user',
name: 'user',
type: 'relation',
required: true,
maxSelect: 1,
collectionId: users.id,
cascadeDelete: true,
},
{
id: 'relation_notification_team',
name: 'team',
type: 'relation',
required: false,
maxSelect: 1,
collectionId: teams.id,
cascadeDelete: true,
},
// Welcher Schalter diesen Eintrag erlaubt hat. Steht mit
// drin, damit die Zusammenfassung nach Art gruppieren kann.
{ id: 'text_notification_type', name: 'type', type: 'text', required: true },
{ id: 'text_notification_title', name: 'title', type: 'text', required: true },
{ id: 'text_notification_body', name: 'body', type: 'text' },
// Relativer Pfad, kein vollständiger Link: Die Adresse der
// App steht in APP_URL und gehört nicht in jeden Datensatz.
{ id: 'text_notification_link', name: 'link', type: 'text' },
// Leer heißt: wartet auf die nächste Zusammenfassung.
{ id: 'date_notification_sent', name: 'sent', type: 'date' },
{ id: 'autodate_created', name: 'created', type: 'autodate', onCreate: true, onUpdate: false },
{ id: 'autodate_updated', name: 'updated', type: 'autodate', onCreate: true, onUpdate: true },
],
indexes: [
// Der Job fragt genau danach: alles Unversendete, nach Konto.
'CREATE INDEX `idx_notifications_pending` ON `notifications` (`user`, `sent`)',
],
})
app.save(queue)
}
}, (app) => {
try {
app.delete(app.findCollectionByNameOrId('notifications'))
} catch {
// Schon fort.
}
try {
app.delete(app.findCollectionByNameOrId('notification_prefs'))
} catch {
// Schon fort.
}
})