The test-shield (#test-shield) was added in loader.js (always fresh
via plugin manager ?v=time()) but the removal lives in runner.js
hideProgress(). runner.js was still cached at ?v=3 in the browser,
so the shield stayed after the run. Bumping to ?v=4 in loader.js +
all 42 test files forces a fresh runner.js load.
chore: bump version to 0.9.15
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New module public/js/modules/shortcuts.js:
* registerShortcut({key, ctrl, shift, alt, match, guard, priority,
section, label, keys, handler}) — declarative shortcut registration
with priority ordering, custom match predicates, and runtime guards.
* Single document-level keydown dispatcher iterates the registry
(sorted by priority desc), matches key+modifiers or custom match(),
checks guard(), fires handler with preventDefault.
* guards object: board(), editor(), detail(), search(), email(),
canvas(), bodyFocused(), noOverlay(), isOpen(id) — reusable
predicates that replace the repeated blocker-array patterns.
* renderShortcutHelp() auto-generates the #shortcuts overlay content
from the registry, grouped by section. New shortcuts added via
registerShortcut appear in the help screen automatically.
* Shortcuts with guard: () => false are help-screen-only entries for
element-level shortcuts handled by their own modules (editor
textarea, search input, canvas overlay, etc.).
Migration (public/js/app.js):
* The ~200-line monolithic keydown handler is replaced by ~80
declarative registerShortcut() calls.
* Escape chain: 10 separate registrations with priorities 100→20,
each with its own guard (quick-capture, detail/zen, modal, search,
tasks, gallery, calendar, shortcuts, help). A hidden: true flag
suppresses them from the help screen; one visible entry with
guard: () => false shows "Esc — close any overlay".
* Global combos: Ctrl+K (search), Ctrl+N (quick capture), Ctrl+Z
(undo), F1 (help), ? (shortcuts).
* Editor: Ctrl+S (save), Ctrl+Shift+S (save+close), Ctrl+Enter
(save+close). Element-level shortcuts (Tab indent, Alt+arrows,
autocomplete triggers) registered as help-screen-only.
* Board bare keys: a/f/j/n/t/c//, Tab/Shift+Tab, 1-9, arrow nav,
Enter → preview, Delete/Backspace selected.
* Detail: ←/→ group navigation, Enter → editor.
* Help-screen-only: search ↑↓/Enter, board mouse actions, canvas
tool shortcuts (B/E/L/R/O/F/T/⌫), mail ⌫/⇧⌫, m (plugin).
Removed from board.js: the document-level Delete/Backspace listener
(now in the central registry with the same logic).
Help screen (public/index.php):
* The hardcoded 3-column shortcut HTML is replaced by an empty
.shortcut-help-content div that renderShortcutHelp() fills at boot.
* CSS: .shortcut-help-content section gets break-inside:avoid +
margin-bottom for clean column flow.
Test stabilization:
* card-interactions.test.js: due-pill, overdue-border, and
child-hidden-on-board tests now set state.activeBucket='all'
before _reloadAll and poll for the DOM element (40×50ms) instead
of a fixed sleep(200). Eliminates the flaky "card not found" when
activeBucket was leaked by a prior test.
* editor-features.test.js: "save creates card and closes editor"
polls for #modal to lose .open class (40×50ms) instead of a fixed
sleep(400). doSave(true) does 2-3 serial API calls before
closeModal which sometimes exceeded the old budget.
chore: bump version to 0.9.13
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* card-interactions.test.js: group delete tests used `.dialog-ok`
(class selector) but the button has `id="dialog-ok"`. querySelector
returned null, the click was skipped, the dialog stayed open and
blocked every downstream test. Fixed to getElementById.
* runner.js closeAll(): added #dialog, #bucket-super-popover (remove),
and #gitea-overlay to the cleanup list. A failed test that leaves any
of these open no longer poisons the rest of the suite.
* runner.js copy handler: when there are failures or flaky tests, the
"copy" button now builds the clipboard text from lastResults —
only failed + flaky lines (suite name, test name, timing, error,
original error for retried). When everything passed, copies the
full output as before. lastResults is set in renderResults().
* bucket-context.test.js: move-left test used `[data-bucket]` to find
user bucket tabs, which misses super-bucket tiles (they carry
`data-bucket-super`). Changed to `[data-bucket-drag]` which is on
both regular and super-bucket tiles but not on fixed filters.
* context-menu.test.js: make-ticket pre-fill assertion now polls up
to 2s for the title input to be populated (async openCreateForm
repo-fetch was racing against the immediate assertion).
* card-join.test.js: clickJoin sleep bumped from 400ms → 800ms.
joinSelectedCards does 3+ serial API calls (fetch bodies, PATCH
winner, N×DELETE losers, _reloadAll) which sometimes exceeded the
old 400ms window on slow runs.
* Runner cache: RUNNER_V bumped from ?v=2 → ?v=3 in loader.js + all
42 test files so the browser picks up the new runner.js content
(copy handler, closeAll, retry progress bar) without a hard reload.
super-buckets: remember last selection per group
* buckets.js: module-scope lastSuperSelection Map tracks the last
active member per super-bucket parent. Updated in renderBar()
whenever activeBucket matches a group member. tileFor() uses it
as a fallback when the user is on a different bucket — the tile
shows the last-picked child's name and color instead of reverting
to the parent. The dropdown popover also highlights the last
selection when no member is currently active.
chore: bump version to 0.9.11
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* runner.js: shows a 30px fixed-top progress strip during runs (label
"test x/total:", current suite/test name with ellipsis on the left,
fixed 50%-wide progress fill on the right that turns danger-red on
the first failure). The DOM node is created in loader.js so it appears
immediately on shortcut press; runner.js only updates it. The fill
width is fixed at flex:0 0 50% so it doesn't jiggle as the test name
changes length. Suites now live on window._testSuites as a defensive
fallback so two runner instances stay in sync.
* Cache invalidation: every test file's `from './runner.js'` is now
`from './runner.js?v=2'`, and loader.js imports the runner with the
same RUNNER_V suffix. Same URL → one module instance → suite
registration and runAll see the same array. Bumping RUNNER_V (in
loader.js + every test file in lock-step) is the way to force a
re-fetch when runner.js changes.
* loader.js: ensureProgressBar() draws the strip via setProperty(...,
'important') with z-index 2147483647 attached to documentElement so
nothing in user CSS, scanline overlay (z 9999) or modals can occlude
it. Auto-run path (?test=1) also pre-renders the strip.
chore: bump version to 0.9.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>