feat(notification): add persistent notification

This commit is contained in:
Edouard Vanbelle
2026-09-11 22:08:35 +02:00
parent a6138aa4d9
commit 617ae4b424
32 changed files with 1911 additions and 28 deletions
+1
View File
@@ -120,6 +120,7 @@ rather than as a visible error.
| `OXICLOUD_GRANT_CLEANUP_ENABLED` | `true` | Background daemon that deletes expired rows from `storage.role_grants`. The authorization engine already filters expired grants out of every permission check at read time (`expires_at IS NULL OR expires_at > NOW()`), so leaving expired rows in place is a hygiene issue — not a security one. This daemon garbage-collects them daily. Set to `false` to keep every expired grant row forever (uncommon; a fresh install rarely wants this). |
| `OXICLOUD_GRANT_CLEANUP_GRACE_DAYS` | `15` | Days past a grant's `expires_at` before the row is eligible for deletion. The grace window preserves the audit / support answer to "what happened to my access?" for a couple of weeks past expiration. Values below 1 are legal but discouraged — the recommendation is **≥ 15 days**. Values above the actual grant TTL used by clients waste index space; a few weeks is the sweet spot. |
| `OXICLOUD_GRANT_CLEANUP_INTERVAL_HOURS` | `24` | How often the grant-cleanup daemon fires. Clamped to a minimum of 1 hour. Adjusting this doesn't change what gets deleted — only how promptly. Daily is fine for any realistic grant volume. |
| `OXICLOUD_NOTIFICATIONS_RETENTION_DAYS` | `30` | Retention window for **read** notification rows (`notif.notifications`). The `notifications_cleanup` scheduled job runs daily and deletes rows where `read_at IS NOT NULL` and `read_at < now() - retention_days`. Unread rows are preserved unconditionally — the whole point of the durable table is that a user offline for a month still sees the share-granted notice on next login. Clamped to a minimum of 1 (0 would purge every read row on every tick). Adjust down for compliance-sensitive deployments where "cleared once seen" matters; adjust up when operators expect users to reference old notifications for support. |
| `OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX` | `@drive` | Native WebDAV URL segment that renders the caller's drive list. Sanitized by trimming leading/trailing `/`. Three shapes: (1) default `@drive` — `/webdav/…` addresses the caller's default personal drive (back-compat), `/webdav/@drive/` returns the drive listing, `/webdav/@drive/<uuid\|name>/…` targets a specific drive. (2) empty string `""` — `/webdav/` IS the drive listing, `/webdav/<uuid\|name>/…` targets a specific drive, no default-drive shortcut. (3) any other string (e.g. `drives`) — same shape as `@drive` with that segment substituted. Only drives the caller has Read on via `role_grants` resolve. |
## Storage Entries (multi-entry, recommended)
+29 -20
View File
@@ -18,9 +18,9 @@ almost no extra scaffolding.
## Status — 2026-09-11
The `feat/message-bus` branch delivers **D + F + follow-ups shipped
end-to-end** on the FE and BE, verified by S1–S11 in the api-test
smoke suite plus manual multi-user E2E. Live today:
The `feat/message-bus` branch delivers **D + F + Job dashboard live +
follow-ups shipped end-to-end** on the FE and BE, verified by S1–S12
in the api-test smoke suite plus manual multi-user E2E. Live today:
- **Bus core** — `MessageBus` port + `InProcessMessageBus` +
`NoopReplicator`. `📤 bus publish` trace under
@@ -71,21 +71,29 @@ smoke suite plus manual multi-user E2E. Live today:
in every mutation entry point so `$state` reads don't leak into
caller `$effect` deps.
- **AuthZ tested** — S3 (folder no_read), S4 (nonexistent folder =
anti-enum parity), S9 (cross-user identity topic → `topic_forbidden`).
anti-enum parity), S9 (cross-user identity topic → `topic_forbidden`),
S12 (`job:*` non-admin denied — Class-3 role-scoped gate).
- **Ticket tested** — S10 (happy path), S11 (single-use replay
rejected).
- **Job dashboard live** — scheduler engine publishes
`JobRunStarted` / `JobRunEnded` on `Topic::Job(name)` around every
dispatch; `useJobTopic` + `AdminJobsPanel` subscribes to every
registered job's topic (keyed on the sorted name set so the 5 s
poll doesn't churn subs). State flips within a network hop instead
of waiting up to POLL_MS. Progress publishes are deferred (see
below); the polling refresh stays as fallback.
Active — still under Phase A, ordered by priority:
- **Job dashboard live** (next) — `JobRegistry` publishes step
progress + terminal state on `job:{id}`; the admin jobs view
subscribes and drops its polling. Small; same shape as folder-live.
Value: an operator who triggers a long-running job (backend
migration, thumbnail import, etc.) can navigate to another admin
page and come back without losing progress visibility.
- **Notifications table + bell** (E) — topic + producer + auto-sub
land here. Same pattern as `:authz`. Larger; unblocks Phase-B
`@mentions`.
- **Job progress publishes** (small follow-up to Job dashboard) —
handler-side per-run publisher + 3 s throttle so long jobs
(backend_migration, thumb_derived_import…) push `JobRunProgress`
events. Wire is already in place (`useJobTopic.onProgress`,
`MessageBusEvent::JobRunProgress`); waits for a per-run
`ProgressReporter` handle threaded into `JobHandler::run`.
Deferred — see the Roadmap section's `## Deferred` block and the
`project_message_bus_reconnect_gap` memory:
@@ -1242,13 +1250,13 @@ Ships the infrastructure and the two most visible consumers together.
`useReconnect` composable → folder view refetches after WS comes
back. Bridges the in-memory-bus "events lost during outage" gap
(see `project_message_bus_reconnect_gap` memory).
- **Job dashboard live** — TODO (next slice). `JobRegistry`
publishes step progress and terminal state on `job:{id}`; FE
job dashboard subscribes and replaces polling. Operator value:
once a long-running job is triggered (backend migration, thumb
import, blobs consistency…), the admin can navigate to another
page and come back without losing progress visibility — the WS
push keeps whatever component is subscribed up-to-date.
- **Job dashboard live** — SHIPPED 2026-09-11. Scheduler engine
publishes `JobRunStarted` + `JobRunEnded` on `Topic::Job(name)`
around every dispatch; `useJobTopic` + `AdminJobsPanel` subscribe
to every registered job's topic and flip state within a network
hop. Progress publishes deferred to a follow-up (needs a per-run
`ProgressReporter` threaded into `JobHandler::run`). The 5 s
poll stays as fallback.
- **Notifications table + bell** — TODO (Slice E). New
`notifications` table + `NotificationService` port; initial
ingesters for `share-granted`, `new-login-from-new-device`,
@@ -1261,9 +1269,10 @@ Ships the infrastructure and the two most visible consumers together.
which this plan sketches but doesn't ship (`rt_ws.rs` today drops
binary frames with a debug log).
Deliverables sized ~4 weeks end-to-end. Slice D (folder-live) and
Slice F (ticket flow) landed 2026-09-11. Slices E + collab are the
open work in Phase A.
Deliverables sized ~4 weeks end-to-end. Slice D (folder-live),
Slice F (ticket flow), and Job dashboard live all landed 2026-09-11.
Slice E (notifications bell) + collab are the remaining open work
in Phase A.
### Deferred — everything below is on the shelf