System Logs & Notifications
Updated: 2025-10-08 16:18
This chapter unifies the authoritative spec and the later tiles‑first routing update into a single, no‑wiggle‑room reference you can hand to any agent. It defines the data model, delivery matrix, tile badge routing, SSE, APIs, and governance.
1) Core concepts
- Single source of truth:
system_logsis the master, append‑only history. - Per‑user state, not copies:
notificationsstores each user’s visibility/read state for a master row; there is no per‑user duplication of log content. - No unhide: when a user marks an item “read”, it disappears from their bell dropdown and personal notifications page and stops counting toward badges. The master entry remains in
system_logs. - Targets (eligibility): a user is eligible when
targetsincludes'all','user:<uuid>'(them), or any'group:<uuid>'they belong to. - Deliver:
silent | normal | pushcontrols if/how it surfaces for eligible users. - Editable vocabularies:
log_types(multi‑tag) andlog_platforms(single) are maintained in Settings and validated by the server. - One entry point: all logging goes through
emitLog().
2) Data model (tables & indexes)
2.1 Master system log — system_logs
Key columns:
id uuid,ts timestamptz,message textcontext jsonb(structured details)types text[](multi‑tag; validated againstlog_types)party text(who or what originated it; e.g., user name/id, service)platform citext(FK tolog_platforms.slug)targets text[]—'all' | 'user:<uuid>' | 'group:<uuid>'deliver text CHECK ('silent'|'normal'|'push')display boolean(global soft‑hide; used by “Clear Logs” and purge rules)
Indexes:
- btree:
(ts DESC),(display),(deliver),(platform) - GIN:
(types),(targets),(context)
2.2 Per‑user notifications — notifications
user_id uuid,log_id uuid,display boolean default true,delivered_at timestamptz- Primary key
(user_id, log_id) - Insert a row only when the item should appear in the mini‑log or should count for badges (badge‑only entries use
display=false).
2.3 Tile badges — user_tile_badges
- Tracks unread counters per tile:
inbox,messages,tasks,calendar,profile(Settings has no badge). - API bumps via
bump_tile_badge(user_id, tile, +Δ).
2.4 Vocab registries & targeting
log_types(slug, name, active, protected)— multi‑tag set validated by server.log_platforms(slug, name, active)— single select validated by server.log_groups(id, slug, name, category, active)andlog_user_groups(user_id, group_id)— for'group:<uuid>'targeting.
3) Delivery behaviour (final matrix)
Per eligible user, considering Account Setting reduce_notifications (boolean):
false= “Everything”true= “Targeted only”
| deliver | targeted? | reduce = false (“Everything”) | reduce = true (“Targeted only”) |
|---|---|---|---|
| silent | any | System log only (no toast • no mini‑log • no badge) | same |
| normal | Yes | toast + mini‑log; badge if tile‑routed | if tile‑routed → badge‑only (insert notifications with display=false, bump badge); else → mini‑log only (display=true); no toast |
| normal | No | toast‑only (no mini‑log, no badge) | nothing |
| push | Yes | toast + mini‑log; badge if tile‑routed | same |
Offline users: DB inserts (notifications + badge bumps) happen at emit time; SSE merely reflects truth when online.
4) Tile badges — routing by Settings
Tiles with badges: inbox, messages, tasks, calendar, profile (Settings tile never tallies).
A log can map to at most one tile. Mapping is config‑driven via admin.app_settings.tile_routes:
{
"priority": ["messages","inbox","tasks","calendar","profile"], // first match wins
"rules": {
"inbox": [ {"by":"platform","anyOf":["Email"]}, {"by":"type","anyOf":["email"]} ],
"messages": [ {"by":"type","anyOf":["message"]} ],
"tasks": [ {"by":"context.kind","anyOf":["task_created","task_assigned","task_commented","task_status_changed"]} ],
"calendar": [ {"by":"context.kind","anyOf":["calendar_event_created","calendar_event_updated","calendar_event_invite"]} ],
"profile": [ {"by":"context.kind","anyOf":["profile_comment","profile_mention","profile_share"]} ]
}
}
Server helper routeTileFor(log) evaluates tiles in priority and returns a tile or null.
Rule kinds:
by:"platform"→ compare tolog.platformby:"type"→ intersect withlog.typesby:"context.kind"→ compare(log.context->>'kind')
Earlier drafts used the tile name “messenger”. We standardize on
messages. Use whatever display label you want in UI, but the tile key should bemessages.
5) emitLog() — the only way to write logs
Input (validated):
type EmitLogInput = {
message: string;
types?: string[]; // must exist & be active in log_types
party?: string | null;
platform?: string; // must exist & be active in log_platforms
targets?: string[]; // 'all' | 'user:<uuid>' | 'group:<uuid>'
deliver?: 'silent' | 'normal' | 'push';
context?: Record<string, any>;
};
Flow:
- Normalize defaults:
types=['info'],platform='Backend',targets=['all'],deliver='normal',display=true,context={}. - Validate
types/platformvia registries; validatetargetsand expand'group:<uuid>'to member user ids. - Insert 1 row into
system_logs(returnlog_id). - Fan‑out per eligible user:
- Decide tile via
routeTileFor(log). - Apply the delivery matrix (Section 3):
- mini‑log → insert into
notificationswithdisplay=true; sendevent: logSSE. - badge‑only → insert
notificationswithdisplay=false; bump tile badge; sendevent: badgeSSE. - toast‑only → no DB row; send
event: toastSSE.
- mini‑log → insert into
- Always push a consolidated
event: badgeafter any badge change.
- Decide tile via
Forbidden: direct inserts to system_logs or notifications outside emitLog().
6) SSE — events & endpoints
Endpoint: GET /api/notifications/stream (per‑user auth; retry/reconnect).
Events:
event: log→ a mini‑log itemevent: toast→ toast payload, no DB row createdevent: badge→{ badges: { inbox, messages, tasks, calendar, profile } }event: telemetry→ metrics (optional)event: vocab→ notify clients to refresh vocab dropdowns (optional)
7) REST API surface
Admin / System log
GET /api/logs→ filters:q, types, party, platform, targets, deliver, display, from, to, pagePATCH /api/logs/:id→ flipdisplay(admin)POST /api/logs/clear→ hidedisplay=trueolder than 7 days (senior mgmt/exec)POST /api/logs/export(range)POST /api/logs/export-allPOST /api/logs/purge→ exec only; enabled only after successful “Export All”
Users / Notifications
GET /api/notifications→ user’s mini‑log (visible entries only)PATCH /api/notifications/:logId→{ display:false }(mark read)GET /api/notifications/badges→ counters
Vocab (Settings‑managed)
GET/POST/PATCH /api/vocab/log_typesGET/POST/PATCH /api/vocab/platforms
Groups (App Settings)
GET/POST/PATCH /api/log-groupsGET /api/log-groups/:id/membersPUT /api/log-groups/:id/members→{ add: uuid[], remove: uuid[] }
8) Retention & governance
- notifications: purge after 161 days (23 weeks). System setting
notifications_keep_unreadcontrols whether unread (display=true) are protected. - system_logs:
- “Clear Logs” = bulk set
display=falsefor visible rows older than 7 days. - “Export”/“Export All”: include both
display=true/false. - “Purge” (exec‑only): allowed only after a full export; typical policy is purge hidden rows older than 180 days.
- “Clear Logs” = bulk set
9) UI behaviours
- Header bell (mini‑log): shows only
notifications.display=true; “Mark read” toggles to false. “Show more” goes to/me/notificationsfiltered to current user. - 3×3 Grid tiles: Calendar, Inbox, Settings, Profile, Tasks, Messages (five tally; Settings has none). Badges are pulled from
user_tile_badges; clicking a tile can zero its counter server‑side. - Admin Logs page: full filters, tag badges for
types, pill fordeliver, and quick toggles fordisplay. Buttons: Clear (role‑gated), Export, Export All, Purge (gated by Export All).
10) Performance & testing
- Indexes in Section 2 keep queries snappy; prefer pagination on
/api/logs. - Unit tests: emit validation; matrix permutations; tile routing; badge bumping.
- Integration: mini‑log visibility; toast‑only, badge‑only; SSE receiving; admin actions.
- E2E: role gates; persistence of Settings; vocab edits hot‑reload (
event:vocab).
11) Glossary (final names)
- Tiles:
inbox,messages,tasks,calendar,profile(Settings has no badge) - Deliver:
silent,normal,push - Targets:
'all','user:<uuid>','group:<uuid>'
Appendix — Minimal SQL pointers (already migrated elsewhere)
- Tables:
system_logs,notifications,user_tile_badges,log_types,log_platforms,log_groups,log_user_groups - Setting keys used here:
tile_routes,notifications_keep_unread,notifications_purge_cutoff_days,ui_toast_autohide_ms,log_level,log_redact_secrets