feat(kanban/dashboard): board archive/restore/hard-delete UX + per-stage timestamps - #27599
Closed
nnnet wants to merge 1 commit into
Closed
feat(kanban/dashboard): board archive/restore/hard-delete UX + per-stage timestamps#27599nnnet wants to merge 1 commit into
nnnet wants to merge 1 commit into
Conversation
…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.
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related dashboard improvements bundled into one PR:
1. Board lifecycle UX
New backend endpoints
POST /boards/{slug}/archive— soft archive (board dir moved toboards/_archived/<slug>-<ts>/); recoverable.POST /boards/{slug}/restore— un-archive.DELETE /boards/{slug}?delete=true— hard-delete (defaultdelete=falseis archive for backwards compat).cascade=truequery — opt-in to operate on non-empty boards (refuses with HTTP 409 +task_countotherwise). Default is refuse — accidental clicks on a populated board do not silently wipe data.Dashboard
[+ New board] [Archive]— Archive is the panel toggle. No Delete button on toolbar (was a source of foot-guns).[ToArchive]action that moves the currently active board into the archive (cascade for non-empty). Hidden fordefault(cannot be archived).[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.loadBoardList()refetches.Backend hardening
hard_delete_boardauto-switches thecurrentpointer todefaultif 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. Previouslypath.parent.mkdir(parents=True, exist_ok=True)silently recreatedboards/<slug>/if any stale caller (frontend with cached?board=<gone>query, dispatcher tick before list refresh,currentpointer not cleared) opened a connection to the gone slug. Now raisesFileNotFoundErrorfor non-default boards whose parent dir doesn't exist. Callers that wrapconnect()in try/except (dispatcher tick,_board_counts) degrade gracefully.GET /boardsdefaults toinclude_archived=False.Dispatcher safety (verified)
list_boards(include_archived=False)excludes archived boards from_tick_onceiteration — archived boards are quiescent.status='archived'are skipped by both the promoter (WHERE status='todo') and the claim path (WHERE status='ready').restore_tasksetsstatus='todo'— next tick promotes toready, tick after claims. No state loss across archive→restore.2. Per-stage timestamps on cards
Backend
_task_dict()emitsentered_status_at(unix seconds) — time the task entered its current status. Derived from existingtask_eventsaudit log (kinds:promoted→ ready,claimed→ running,completed→ done,blocked→ blocked,archived→ archived,restored/claim_rejected→ todo rollback)._batch_entered_status_at) avoids N+1 on the board list endpoint. Indexidx_events_taskmakes lookup O(log n) per task.task_events.Frontend
"3h / 12m ago"whenentered_status_at !== created_at(singleagosuffix). Falls back to plain"3h ago"when the task never moved (status=todo without transitions).timeAgo()special strings like"just now"/"yesterday"render without a trailingago("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 --checksyntax guard (subprocess.run; pytest.skip if node not on PATH).Bundle patching note
plugins/kanban/dashboard/dist/index.jsis hand-patched (no build step in this tree). Every edit was validated withnode --checkplus a mock-load that assertsregister()is called. The newtest_dashboard_bundle_syntax_validenforces 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.pypasses