Per-file activity feed: event store + API + timeline panel (#530, 2/4) #539

Closed
opened 2026-09-14 02:48:04 +00:00 by Cordy · 2 comments
Owner

Second slice of #530. A per-file activity timeline in the details pane ("X updated file in Space, date"), per the OpenCloud reference.

Needs: an event store recording who/what/when per path (upload, update, rename, move, copy, share created/removed, restore), GET /api/v1/activity?path= with paging, and the timeline panel in the details sidebar (#538).

Design decision first: reuse the audit log as the queryable source, or add a separate lighter event log. The audit log carries retention/compliance semantics (runtime toggle, retainMonths) that a UI feed should probably not be coupled to — leaning separate store under the statestore pattern (survives redeploys per #153), with a size/age cap. Rename/move must re-key or follow the path, same problem as tags (#540).

Backend TDD per house rules; blocked by nothing — the panel lands in #538's shell.

Second slice of #530. A per-file activity timeline in the details pane ("X updated file in Space, date"), per the OpenCloud reference. Needs: an event store recording who/what/when per path (upload, update, rename, move, copy, share created/removed, restore), `GET /api/v1/activity?path=` with paging, and the timeline panel in the details sidebar (#538). Design decision first: reuse the audit log as the queryable source, or add a separate lighter event log. The audit log carries retention/compliance semantics (runtime toggle, retainMonths) that a UI feed should probably not be coupled to — leaning separate store under the statestore pattern (survives redeploys per #153), with a size/age cap. Rename/move must re-key or follow the path, same problem as tags (#540). Backend TDD per house rules; blocked by nothing — the panel lands in #538's shell.
Author
Owner

Design note (build follows on approval-by-silence — flag anything off):

  • Separate event store, not the audit log, as the issue leans. The audit log carries compliance semantics (retention toggle, retainMonths, admin-only surface); a user-facing feed must not couple to any of that. New store under the statestore pattern (.cairn-state/activity.json), so it survives redeploys per #138/#153.
  • Ring buffer, not an unbounded log: capped at 5000 events and 90 days (admin-tunable later), enforced at write time — same no-background-daemon discipline as the versioning ADR. Event shape: {path, action, actor, at} with action ∈ upload, update, rename, move, copy, share-created, share-removed, restore.
  • Instrumentation rides the existing chokepoints the audit log already instruments — no new interception layer.
  • Rename/move re-keys the moved prefix's events in one pass through the move chokepoint (the same answer as tags #540 and versions #541 — third consumer, same reason).
  • API: GET /api/v1/activity?path=&limit= newest-first, resolveOwned-class path handling, hidden on delegated paths in v1 (consistent with the versioning ADR's recipient decision).
  • UI: the Activity row + panel land in the shipped pane (v0.6.220 shell), "actor · action · time" lines, inline empty state per the OpenCloud reference.
**Design note (build follows on approval-by-silence — flag anything off):** - **Separate event store, not the audit log**, as the issue leans. The audit log carries compliance semantics (retention toggle, retainMonths, admin-only surface); a user-facing feed must not couple to any of that. New store under the statestore pattern (`.cairn-state/activity.json`), so it survives redeploys per #138/#153. - **Ring buffer, not an unbounded log**: capped at 5000 events and 90 days (admin-tunable later), enforced at write time — same no-background-daemon discipline as the versioning ADR. Event shape: `{path, action, actor, at}` with action ∈ upload, update, rename, move, copy, share-created, share-removed, restore. - **Instrumentation rides the existing chokepoints** the audit log already instruments — no new interception layer. - **Rename/move re-keys** the moved prefix's events in one pass through the move chokepoint (the same answer as tags #540 and versions #541 — third consumer, same reason). - **API**: `GET /api/v1/activity?path=&limit=` newest-first, `resolveOwned`-class path handling, hidden on delegated paths in v1 (consistent with the versioning ADR's recipient decision). - **UI**: the Activity row + panel land in the shipped pane (v0.6.220 shell), "actor · action · time" lines, inline empty state per the OpenCloud reference.
Author
Owner

Shipped in v0.6.221 (PR #619, tag on b83ab7f), live on both dogfoods (files.c0rdyceps.ch and files-bao.c0rdyceps.ch).

Built as designed in the note above:

  • internal/activity: separate bounded event store at activity.json beside recents/favorites (atomic JSON via statestore). Write-time caps: 5000 events, 90 days. Explicitly not the #61 audit log — the #433 verb scanner got an exemption entry documenting exactly that.
  • Events: upload/update (a Stat before the write decides which), rename/move (activity.Kind: same directory = rename, different = move), copy, share-created/share-removed (both grants and public links), restore (trash, via a nil-safe TrashAPI.Act hook).
  • Move/rename re-keys history — exact path plus directory-prefix carry, so a folder move brings every child's history along.
  • GET /api/v1/activity?path=&limit= (limit ≤ 200, default 50), gated by a Stat through the caller's scoped store: paths you can't see yield errors, so delegated/foreign history stays hidden.
  • Pane: Activity drill-in row below Shares, panel rows are actor · action badge · time, inline empty state ("No activity yet"), i18n ×4. feat.activity probed at boot.

Two deliberate scope edges, flag if either bites during dogfooding:

  1. WebDAV ops don't record — only web-UI/API operations do (same stance as Recents #293). A Finder upload won't appear in the feed.
  2. WOPI/office saves don't record yet — they'll fold into the #541 versioning work where the 10-min coalescing lands.

Tests: store unit tests (caps, ordering, rekey, persistence, Kind), act helper tests, pure:act-label node test; red witnessed before impl on both Go and node sides.

Shipped in **v0.6.221** (PR #619, tag on b83ab7f), live on both dogfoods (files.c0rdyceps.ch and files-bao.c0rdyceps.ch). Built as designed in the note above: - `internal/activity`: separate bounded event store at `activity.json` beside recents/favorites (atomic JSON via statestore). Write-time caps: 5000 events, 90 days. Explicitly not the #61 audit log — the #433 verb scanner got an exemption entry documenting exactly that. - Events: `upload`/`update` (a Stat before the write decides which), `rename`/`move` (`activity.Kind`: same directory = rename, different = move), `copy`, `share-created`/`share-removed` (both grants and public links), `restore` (trash, via a nil-safe `TrashAPI.Act` hook). - Move/rename re-keys history — exact path plus directory-prefix carry, so a folder move brings every child's history along. - `GET /api/v1/activity?path=&limit=` (limit ≤ 200, default 50), gated by a Stat through the caller's scoped store: paths you can't see yield errors, so delegated/foreign history stays hidden. - Pane: Activity drill-in row below Shares, panel rows are actor · action badge · time, inline empty state ("No activity yet"), i18n ×4. `feat.activity` probed at boot. Two deliberate scope edges, flag if either bites during dogfooding: 1. **WebDAV ops don't record** — only web-UI/API operations do (same stance as Recents #293). A Finder upload won't appear in the feed. 2. **WOPI/office saves don't record yet** — they'll fold into the #541 versioning work where the 10-min coalescing lands. Tests: store unit tests (caps, ordering, rekey, persistence, Kind), `act` helper tests, `pure:act-label` node test; red witnessed before impl on both Go and node sides.
Cordy closed this issue 2026-09-18 16:19:50 +00:00
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#539
No description provided.