Skip to content

feat(kanban/dashboard): board archive/restore/hard-delete UX + per-stage timestamps - #27599

Closed
nnnet wants to merge 1 commit into
NousResearch:mainfrom
nnnet:pr/upstream-kanban-dashboard-improvements
Closed

feat(kanban/dashboard): board archive/restore/hard-delete UX + per-stage timestamps#27599
nnnet wants to merge 1 commit into
NousResearch:mainfrom
nnnet:pr/upstream-kanban-dashboard-improvements

Conversation

@nnnet

@nnnet nnnet commented May 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Two related dashboard improvements bundled into one PR:

  1. Board lifecycle in the dashboard — archive/restore/hard-delete via UI with safety rails (hard-delete only reachable on already-archived boards).
  2. Per-stage timestamps on cards — cards show "created_age / in-stage_age ago" instead of only created_at.

1. Board lifecycle UX

New backend endpoints

  • POST /boards/{slug}/archive — soft archive (board dir moved to boards/_archived/<slug>-<ts>/); recoverable.
  • POST /boards/{slug}/restore — un-archive.
  • DELETE /boards/{slug}?delete=true — hard-delete (default delete=false is archive for backwards compat).
  • cascade=true query — opt-in to operate on non-empty boards (refuses with HTTP 409 + task_count otherwise). Default is refuse — accidental clicks on a populated board do not silently wipe data.

Dashboard

  • Toolbar: [+ New board] [Archive] — Archive is the panel toggle. No Delete button on toolbar (was a source of foot-guns).
  • Archive viewer header has a new [ToArchive] action that moves the currently active board into the archive (cascade for non-empty). Hidden for default (cannot be archived).
  • Archive viewer rows for archived boards show [Restore] and a new [Delete] — Delete is the only path to permanent hard-delete and is only reachable here, after the board is already archived. Two-step confirm.
  • After archive/hard-delete: optimistic local filter removes the slug from the selector immediately + loadBoardList() refetches.

Backend hardening

  • hard_delete_board auto-switches the current pointer to default if the deleted board was active. Previously returned HTTP 409 "switch first" — pure friction since the dashboard always intends to remove the current board.
  • kanban_db.connect() refuses to auto-resurrect a deleted board. Previously path.parent.mkdir(parents=True, exist_ok=True) silently recreated boards/<slug>/ if any stale caller (frontend with cached ?board=<gone> query, dispatcher tick before list refresh, current pointer not cleared) opened a connection to the gone slug. Now raises FileNotFoundError for non-default boards whose parent dir doesn't exist. Callers that wrap connect() in try/except (dispatcher tick, _board_counts) degrade gracefully.
  • Selector filter: GET /boards defaults to include_archived=False.

Dispatcher safety (verified)

  • list_boards(include_archived=False) excludes archived boards from _tick_once iteration — archived boards are quiescent.
  • Tasks with status='archived' are skipped by both the promoter (WHERE status='todo') and the claim path (WHERE status='ready').
  • restore_task sets status='todo' — next tick promotes to ready, tick after claims. No state loss across archive→restore.

2. Per-stage timestamps on cards

Backend

  • _task_dict() emits entered_status_at (unix seconds) — time the task entered its current status. Derived from existing task_events audit log (kinds: promoted → ready, claimed → running, completed → done, blocked → blocked, archived → archived, restored/claim_rejected → todo rollback).
  • Batch query (_batch_entered_status_at) avoids N+1 on the board list endpoint. Index idx_events_task makes lookup O(log n) per task.
  • No DB schema change — the data was already in task_events.

Frontend

  • Card age label: "3h / 12m ago" when entered_status_at !== created_at (single ago suffix). Falls back to plain "3h ago" when the task never moved (status=todo without transitions).
  • Edge cases handled: timeAgo() special strings like "just now" / "yesterday" render without a trailing ago ("3h / just now").

Tests

  • tests/plugins/kanban/test_board_archive_api.py — 540 lines, comprehensive: archive default-refusal, restore, cascade rules, hard-delete refusal-by-default, selector filter, switch-then-archive flow.
  • tests/plugins/kanban/test_dashboard_stage_entered_time.py — 323 lines, one test per status type + batch query correctness + edge cases.
  • tests/plugins/test_kanban_dashboard_plugin.py — added bundle regression incl. node --check syntax guard (subprocess.run; pytest.skip if node not on PATH).

Bundle patching note

plugins/kanban/dashboard/dist/index.js is hand-patched (no build step in this tree). Every edit was validated with node --check plus a mock-load that asserts register() is called. The new test_dashboard_bundle_syntax_valid enforces this in CI so a future patch can't accidentally break the IIFE.

Test plan

  • pytest tests/plugins/kanban/ tests/plugins/test_kanban_dashboard_plugin.py passes
  • Archive a non-empty board via UI → appears in Archive viewer → Restore → board back in selector with all tasks
  • Hard-delete the same board from archive row → board + all tasks wiped, doesn't reappear after refresh
  • On a board with movement, cards show two timestamps via slash; on fresh todo tasks, only one

…age timestamps

Two related dashboard improvements bundled into one branch:

1. Board lifecycle in the dashboard
   - New endpoints: POST /boards/{slug}/archive (soft, recoverable),
     POST /boards/{slug}/restore, DELETE /boards/{slug} with `delete=true`
     (hard) and `cascade=true` (non-empty integrity opt-in).
   - Archive panel viewer now shows archived boards + archived tasks
     with per-row Restore. Hard-delete is reachable ONLY from an
     archived board row (Archive → review → Delete workflow), not
     from the main toolbar — accidental wipe-of-active-board is gone.
   - Toolbar: [+ New board] [Archive] (toggle viewer).
   - Archive viewer header has a [ToArchive] action button that moves
     the currently-active board into the archive (cascade for non-empty).
   - Backend: hard_delete_board auto-switches the `current` pointer to
     `default` if the deleted board was active (was previously HTTP 409
     "switch first" — friction).
   - Selector hides archived boards (`list_boards(include_archived=False)`
     default for `GET /boards`); the dashboard list_boards endpoint
     reloads after archive/hard-delete with an optimistic local filter
     so the deleted slug disappears immediately.
   - kanban_db.connect(): refuses to auto-resurrect a deleted board.
     Previously `mkdir(parents=True, exist_ok=True)` silently recreated
     `boards/<slug>/` when a stale caller (frontend with cached ?board=
     query, dispatcher tick before list refresh, current pointer not
     cleared) passed the gone slug. Now raises FileNotFoundError for
     non-default boards whose parent dir doesn't exist.

2. Per-stage timestamps on cards
   - Backend: `_task_dict` now emits `entered_status_at` (time the task
     entered its current status), computed from `task_events` via batch
     query (no N+1) — derived from existing audit-log events (promoted /
     claimed / completed / blocked / archived / restored). No DB schema
     change.
   - Frontend: card age label renders `created_age / in_stage_age ago`
     (single "ago" suffix); falls back to just `created_age` when the
     task never moved (status=todo without transitions).

Tests: 23 new (board archive API: 540 lines; entered_status_at: 323
lines; bundle regression incl. `node --check` syntax guard). Existing
suite unaffected.

Bundle is hand-patched (no build step) — every edit was verified with
`node --check`, and `test_dashboard_bundle_syntax_valid` (subprocess
node --check) is now in CI to catch IIFE parse errors that would
silently kill register() in the browser.
@alt-glitch alt-glitch added type/feature New feature or request comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have labels May 17, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks @nnnet — closing this one. 2767 LOC including a regenerated dist bundle is too cross-cutting for mechanical salvage. The archive/restore/hard-delete UX is good direction but the bundle conflicts and the per-stage timestamps schema decision deserve a focused fresh PR. Appreciate the work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants