Releases: jordibrouwer/nextdash
Release list
v1.2.1
One fix, in the Health view. Flagged hideFromModal in the index: it counts toward the version number and shows in Config → Overview → Latest update, but does not reopen the What's new modal, which still leads with v1.2.0. DASHBOARD_RELEASE and NEXTDASH_WHATS_NEW_DATA_VERSION are deliberately unchanged — those two tokens are what re-show the modal, and a one-fix release should not push a feature release off the front of it for everyone who has already read it.
What's new
- fix — usage no longer decides where a row sits.
never_openedandnot_opened_30_dayscost 10 points each, so opening a bookmark — the action the Broken filter's own note asks for — raised its score, and under the default worst-first sort carried the row away: measured on a 200-bookmark install, position 10 of 200 became position 188 while the reader was still working through the list. Both penalties are 0 now; the flags, the Unused and Stale tiles and filters, and the reason text on the row are unchanged. - fix — the score sort's tiebreak reads the flags with the two usage ones removed (
stableStatusRank) rather than the status. Status alone was not enough: opening a never-opened row that also has no preview turnsunusedintomissing-preview, a different rank, and the row travels on that instead. - fix — a row you have acted on keeps its position. Opening one under Unused, or re-checking one under Broken, takes it out of what the filter selects, and the list used to close the gap under the cursor. It stays at the position it held, dimmed and marked handled, until the list is asked a different question (filter, sort, search) or reloaded on purpose with
Ror Retest all — and when the view is left. - new — the score breakdown separates deductions from context: reasons that cost nothing are listed under worth knowing, at no cost to the score, because a full score with a deduction printed beneath it reads as broken arithmetic.
Docs
static/data/whats-new/v1.2.1.json, and the index entry ahead of v1.2.0 carryinghideFromModal: true.tests/health-list-stability.spec.jspins both halves against the real UI path — the row's own Open — and compares positions against the rows whose own score did not move, so a startup check landing mid-test cannot pass for a re-ranking. Both halves were falsified: restoring the penalties fails the score assertion, removing the anchoring fails the position one.tests/whats-new-hidden-release.spec.js: the real-index case flips to asserting v1.2.1 is hidden and that v1.2.0 still leads the modal; the constants case now asserts the two tokens are unchanged, and says why that is right here where bumping was right for v1.2.0. The four fixture-driven cases are untouched.MANUAL.mdandREADME.md: the score breakdown no longer lists the two usage penalties, and both describe the handled row and the sort key nothing can move.
v1.2.0 — 16 August 2026
The keyboard release, and the one where three views stop lying about what they know. Typing on the dashboard used to open bookmarks mid-word; the inbox counted links it was not showing; a failed health check worked out its cause and threw it away. All three are now what they appear to be.
What's new
Typing and keys
- fix — typing on the grid no longer opens anything by itself. A bookmark shortcut fired the instant the typed query matched it exactly, so which of your words survived depended on which other bookmarks you owned: on a 200-shortcut install, eight of thirteen ordinary words were swallowed mid-word. Typing now narrows the list and Enter opens the top result, with the exact shortcut match leading it — one keystroke more, and stable against every bookmark you add later.
- fix —
g,jandkno longer eat the first letter of a word. Letters reach the grid only while a row is selected (_letterMayActOnGrid); the arrows always pass,j/kstop propagation once they act, and theg/Gchord paintsdata-grid-keyson the body instead of swallowing the keystroke.cis the documented exception: it still creates a category unconditionally, because four specs pin that behaviour deliberately. - new — the mode prefix is drawn in front of the query line (
>search,:commands,?finders) with an × to clear it, andcloseSearch()now clears#search-queryinstead of leaving the old text behind. - new — an optional key legend under the bookmark grid, shown after the first keystroke and hidden again on Enter. Behavior → General → Show grid key legend, on for fresh installs (
Settings.ShowGridKeyLegend). - new — Show shortcut hints on toolbar icons is off for everyone, via a one-time migration (
migrateShortcutTooltipsDefaultOff, markerShortcutTooltipsOffMigrated). A default change alone would not have reached existing installs, which all carry the field in their stored settings. - new — the shortcut field warns about the letter you are assigning:
ShortcutKeys.gridKeyNote()says what the grid does with it and when, andusedShortcutsNote()lists the shortcuts the page has already spoken for.
Inbox
- fix — every count follows the active filter.
filterBaseItems()/narrowItems()feed the tiles, the pills and the badge, Mark all read becomes Mark shown read whileisNarrowed(), and the first tile is Active — asleep is deliberately excluded. - fix — snoozed links have a footer saying how many are asleep and when the first wakes, and
domainOptions()no longer offers a host whose only link is asleep. - new — Import beside the CSV and JSON exports (
openImportPicker,parseImportPayload,importFromFile), skipping links already present and reporting added / already there / rejected. - new —
markUnread(id)and a row-menu entry for it, and the fetched summary (previewDesc) is rendered on the row, searched, and carried into both exports.
Health
- fix — a failure records its class (
failureClass,HealthSample.Fail) so DNS, timeout, refused, TLS, redirect and content failures reach the incident list, the timeline and the CSV with a reason. Anything that was not an HTTP error storedUp:false, Code:0and showed nothing. - fix — a failed check is re-probed after
monitorConfirmDelayand only recorded if it fails again, so one dropped check no longer dents a month of uptime. - fix —
buildMonitorStatsreportsCoveredMs, andrenderUptimeTilesprints how much history is behind a window rather than labelling a week "30 days". - new — certificate expiry comes from every check:
liveHostsnow includesCheckStatusbookmarks, not only monitored ones. - new — a recovery names its duration,
TREND_SERIESgives the trend chart six series with a per-mode axis,/api/health/expectations-bulkmutes or unmutes a whole selection, andSettings.HealthCheckTimeoutSecondsreplaces the fixed three-second dial timeout.
Search
- new — negation.
parseSearchFiltersproducesfilters.not, andmatchesAdvancedFiltersruns the positive keys then the negative ones, sotag:dev -status:pinnedis expressible in the bar the way it always was in a collection rule. - new —
status:untagged,status:tagged,status:notedandstatus:unnoted;:trashas a command onto#config/data-backups/trash; andCtrl/Cmd+Enteron a result opens in a new tab regardless of the standing preference.
Dashboard and config
- new — undo for moves: single (
undoBookmarkCategoryMove) and cross-page bulk (undoBulkMoveToPage), each restoring from a snapshot so every bookmark returns to its own category.showGroupedNotificationtranslatedundoCallbackinto nothing — the bulk-delete undo it also gates has been dead since it shipped. - new — category icons (
openIconEditor, live preview into.category-title-icon) andAlt+←/→to move a category, announced to screen readers. - new —
setting-art.jsdraws the settings whose subject is a shape (grid, spacing, margins, density, dots, type size, layout version, bar position, flow), in config and in help. - fix — a settings change made elsewhere now reaches an open dashboard:
fetchDataRevisiontracks the settings revision (GetSettingsRevision) and routes a change throughrefreshAfterConfigSettingsUpdate. - fix — a failed automatic backup is visible:
autoBackupRunStaterecordsLastRunAt/LastRunErrorand the tile reports it. - new — a bookmark row in config carries a page › category breadcrumb pill, each half filtering the list.
- new — fresh installs start on Retro CRT (
defaultThemeID), with fold-all and shortcut hints off. Existing installs are untouched. - new — the extension can send a link straight to the inbox: two context-menu entries, a
quick-save-inboxcommand onCtrl/Cmd+Shift+U, and tags carried throughpostInboxLink.
Docs
static/data/whats-new/v1.2.0.jsonand the index entry ahead of v1.1.2.DASHBOARD_RELEASE(2026.08-dashboard-release-v1.2.0) andNEXTDASH_WHATS_NEW_DATA_VERSION(whats-new-v245) are both bumped: a feature release that left them alone would be announced to nobody who had already dismissed v1.1.2.tests/whats-new-hidden-release.spec.js: the real-index and constants cases move to v1.2.0. The four fixture-drivenhideFromModalcases are untouched.- Config → Overview gains three feature spotlights — typing filters/Enter opens, the health failure reason, and the inbox counts — with their five locale keys each in en/nl/de/fr.
MANUAL.mdandREADME.mdcarry the keyboard model, the inbox round, the health engine and the new settings; Config → Help and the printable cheat sheet were updated alongside them.
v1.1.2 — 15 August 2026
A release that exists to announce the one before it. v1.1.1 shipped hideFromModal so it would not displace v1.1.0 on the day both landed; that call is reversed here.
...
v1.2.0
The keyboard release, and the one where three views stop lying about what they know. Typing on the dashboard used to open bookmarks mid-word; the inbox counted links it was not showing; a failed health check worked out its cause and threw it away. All three are now what they appear to be.
What's new
Typing and keys
- fix — typing on the grid no longer opens anything by itself. A bookmark shortcut fired the instant the typed query matched it exactly, so which of your words survived depended on which other bookmarks you owned: on a 200-shortcut install, eight of thirteen ordinary words were swallowed mid-word. Typing now narrows the list and Enter opens the top result, with the exact shortcut match leading it — one keystroke more, and stable against every bookmark you add later.
- fix —
g,jandkno longer eat the first letter of a word. Letters reach the grid only while a row is selected (_letterMayActOnGrid); the arrows always pass,j/kstop propagation once they act, and theg/Gchord paintsdata-grid-keyson the body instead of swallowing the keystroke.cis the documented exception: it still creates a category unconditionally, because four specs pin that behaviour deliberately. - new — the mode prefix is drawn in front of the query line (
>search,:commands,?finders) with an × to clear it, andcloseSearch()now clears#search-queryinstead of leaving the old text behind. - new — an optional key legend under the bookmark grid, shown after the first keystroke and hidden again on Enter. Behavior → General → Show grid key legend, on for fresh installs (
Settings.ShowGridKeyLegend). - new — Show shortcut hints on toolbar icons is off for everyone, via a one-time migration (
migrateShortcutTooltipsDefaultOff, markerShortcutTooltipsOffMigrated). A default change alone would not have reached existing installs, which all carry the field in their stored settings. - new — the shortcut field warns about the letter you are assigning:
ShortcutKeys.gridKeyNote()says what the grid does with it and when, andusedShortcutsNote()lists the shortcuts the page has already spoken for.
Inbox
- fix — every count follows the active filter.
filterBaseItems()/narrowItems()feed the tiles, the pills and the badge, Mark all read becomes Mark shown read whileisNarrowed(), and the first tile is Active — asleep is deliberately excluded. - fix — snoozed links have a footer saying how many are asleep and when the first wakes, and
domainOptions()no longer offers a host whose only link is asleep. - new — Import beside the CSV and JSON exports (
openImportPicker,parseImportPayload,importFromFile), skipping links already present and reporting added / already there / rejected. - new —
markUnread(id)and a row-menu entry for it, and the fetched summary (previewDesc) is rendered on the row, searched, and carried into both exports.
Health
- fix — a failure records its class (
failureClass,HealthSample.Fail) so DNS, timeout, refused, TLS, redirect and content failures reach the incident list, the timeline and the CSV with a reason. Anything that was not an HTTP error storedUp:false, Code:0and showed nothing. - fix — a failed check is re-probed after
monitorConfirmDelayand only recorded if it fails again, so one dropped check no longer dents a month of uptime. - fix —
buildMonitorStatsreportsCoveredMs, andrenderUptimeTilesprints how much history is behind a window rather than labelling a week "30 days". - new — certificate expiry comes from every check:
liveHostsnow includesCheckStatusbookmarks, not only monitored ones. - new — a recovery names its duration,
TREND_SERIESgives the trend chart six series with a per-mode axis,/api/health/expectations-bulkmutes or unmutes a whole selection, andSettings.HealthCheckTimeoutSecondsreplaces the fixed three-second dial timeout.
Search
- new — negation.
parseSearchFiltersproducesfilters.not, andmatchesAdvancedFiltersruns the positive keys then the negative ones, sotag:dev -status:pinnedis expressible in the bar the way it always was in a collection rule. - new —
status:untagged,status:tagged,status:notedandstatus:unnoted;:trashas a command onto#config/data-backups/trash; andCtrl/Cmd+Enteron a result opens in a new tab regardless of the standing preference.
Dashboard and config
- new — undo for moves: single (
undoBookmarkCategoryMove) and cross-page bulk (undoBulkMoveToPage), each restoring from a snapshot so every bookmark returns to its own category.showGroupedNotificationtranslatedundoCallbackinto nothing — the bulk-delete undo it also gates has been dead since it shipped. - new — category icons (
openIconEditor, live preview into.category-title-icon) andAlt+←/→to move a category, announced to screen readers. - new —
setting-art.jsdraws the settings whose subject is a shape (grid, spacing, margins, density, dots, type size, layout version, bar position, flow), in config and in help. - fix — a settings change made elsewhere now reaches an open dashboard:
fetchDataRevisiontracks the settings revision (GetSettingsRevision) and routes a change throughrefreshAfterConfigSettingsUpdate. - fix — a failed automatic backup is visible:
autoBackupRunStaterecordsLastRunAt/LastRunErrorand the tile reports it. - new — a bookmark row in config carries a page › category breadcrumb pill, each half filtering the list.
- new — fresh installs start on Retro CRT (
defaultThemeID), with fold-all and shortcut hints off. Existing installs are untouched. - new — the extension can send a link straight to the inbox: two context-menu entries, a
quick-save-inboxcommand onCtrl/Cmd+Shift+U, and tags carried throughpostInboxLink.
Docs
static/data/whats-new/v1.2.0.jsonand the index entry ahead of v1.1.2.DASHBOARD_RELEASE(2026.08-dashboard-release-v1.2.0) andNEXTDASH_WHATS_NEW_DATA_VERSION(whats-new-v245) are both bumped: a feature release that left them alone would be announced to nobody who had already dismissed v1.1.2.tests/whats-new-hidden-release.spec.js: the real-index and constants cases move to v1.2.0. The four fixture-drivenhideFromModalcases are untouched.- Config → Overview gains three feature spotlights — typing filters/Enter opens, the health failure reason, and the inbox counts — with their five locale keys each in en/nl/de/fr.
MANUAL.mdandREADME.mdcarry the keyboard model, the inbox round, the health engine and the new settings; Config → Help and the printable cheat sheet were updated alongside them.
v1.1.2 — 15 August 2026
A release that exists to announce the one before it. v1.1.1 shipped hideFromModal so it would not displace v1.1.0 on the day both landed; that call is reversed here.
What's new
- fix — the
hideFromModalflag is dropped from the v1.1.1 index entry, so the modal lists it again. The flag only ever gated the modal: the release tag and Config → Overview → Latest update readindex[0]and have shown v1.1.1 since it shipped. - new —
DASHBOARD_RELEASE(2026.08-dashboard-release-v1.1.2) andNEXTDASH_WHATS_NEW_DATA_VERSION(whats-new-v244) are bumped, so the modal reopens once for every user. Unbumped they would have left the un-hiding invisible to anyone who had already dismissed it — the two tokens are the whole re-show mechanism, which is why v1.1.1 deliberately left them alone.
Docs
static/data/whats-new/v1.1.2.json, and the index entry ahead of v1.1.1.tests/whats-new-hidden-release.spec.js: the real-index case flips from asserting v1.1.1 is hidden to asserting nothing in the top three is, and that the modal lists both v1.1.2 and v1.1.1; the constants case moves to the v1.1.2 literals and says why bumping is right here where it was wrong there. The four fixture-driven cases are untouched — the flag still works, it is simply no longer in use.MANUAL.md: the What's new section now says a release can be recorded without being announced, and names v1.1.1 as the one that was.
v1.1.1 — 15 August 2026
Keyboard consistency, and the parts of the app that teach it. Flagged hideFromModal in the index: it counts toward the version number and shows in Config → Overview → Latest update, but does not reopen the What's new modal, which still leads with v1.1.0. DASHBOARD_RELEASE and NEXTDASH_WHATS_NEW_DATA_VERSION are deliberately unchanged — those tokens are what re-show the modal.
Keyboard
- new — the Shift+letter family is closed:
Shift+Einline edit andShift+Vpreview join move, tag, delete, checking, pin, share and reveal.;and[still work, undocumented, the way0still opens the inbox. Brackets mean previous / next sub-tab in config, and one pair of keys meaning two things was the reason to move preview off[. - new — share moves to
Shift+L, soShift+Salways opens config. It was the only key whose meaning depended on whether a row was selected, which took two cheat-sheet rows and a parenthesis to explain. The capture-phase handler still declines rather than swallows a key it has no row for — that is what keepsShift+SreachingsetupPageShortcutsover a selected row, andtests/shift-key-row-actions.spec.js(renamed fromshift-s-two-meanings.spec.js) pins it. - new — bare letters act on keydown.
C_HOLD_MSand the g-chord hold are gone, and with them the wholekeyuphandler inkeyboard-navigation.jsandSearchComponent.addShortcutLetter, which had no other caller._activateGChordModenow runs on the firstg;G_CHORD_MS(3s) is the only timer left. - new —
j/kaliasArrowDown/ArrowUpin the grid, matching config's section rail and bookmark list. - new —
Shift+Homefocus...
v1.1.2
A release that exists to announce the one before it. v1.1.1 shipped hideFromModal so it would not displace v1.1.0 on the day both landed; that call is reversed here.
What's new
- fix — the
hideFromModalflag is dropped from the v1.1.1 index entry, so the modal lists it again. The flag only ever gated the modal: the release tag and Config → Overview → Latest update readindex[0]and have shown v1.1.1 since it shipped. - new —
DASHBOARD_RELEASE(2026.08-dashboard-release-v1.1.2) andNEXTDASH_WHATS_NEW_DATA_VERSION(whats-new-v244) are bumped, so the modal reopens once for every user. Unbumped they would have left the un-hiding invisible to anyone who had already dismissed it — the two tokens are the whole re-show mechanism, which is why v1.1.1 deliberately left them alone.
Docs
static/data/whats-new/v1.1.2.json, and the index entry ahead of v1.1.1.tests/whats-new-hidden-release.spec.js: the real-index case flips from asserting v1.1.1 is hidden to asserting nothing in the top three is, and that the modal lists both v1.1.2 and v1.1.1; the constants case moves to the v1.1.2 literals and says why bumping is right here where it was wrong there. The four fixture-driven cases are untouched — the flag still works, it is simply no longer in use.MANUAL.md: the What's new section now says a release can be recorded without being announced, and names v1.1.1 as the one that was.
v1.1.1 — 15 August 2026
Keyboard consistency, and the parts of the app that teach it. Flagged hideFromModal in the index: it counts toward the version number and shows in Config → Overview → Latest update, but does not reopen the What's new modal, which still leads with v1.1.0. DASHBOARD_RELEASE and NEXTDASH_WHATS_NEW_DATA_VERSION are deliberately unchanged — those tokens are what re-show the modal.
Keyboard
- new — the Shift+letter family is closed:
Shift+Einline edit andShift+Vpreview join move, tag, delete, checking, pin, share and reveal.;and[still work, undocumented, the way0still opens the inbox. Brackets mean previous / next sub-tab in config, and one pair of keys meaning two things was the reason to move preview off[. - new — share moves to
Shift+L, soShift+Salways opens config. It was the only key whose meaning depended on whether a row was selected, which took two cheat-sheet rows and a parenthesis to explain. The capture-phase handler still declines rather than swallows a key it has no row for — that is what keepsShift+SreachingsetupPageShortcutsover a selected row, andtests/shift-key-row-actions.spec.js(renamed fromshift-s-two-meanings.spec.js) pins it. - new — bare letters act on keydown.
C_HOLD_MSand the g-chord hold are gone, and with them the wholekeyuphandler inkeyboard-navigation.jsandSearchComponent.addShortcutLetter, which had no other caller._activateGChordModenow runs on the firstg;G_CHORD_MS(3s) is the only timer left. - new —
j/kaliasArrowDown/ArrowUpin the grid, matching config's section rail and bookmark list. - new —
Shift+Homefocuses the category header viafocusCategoryHeader(), which reusesresolveFocusedCategoryEl.Deleteon a focused header callscategoryMenu.runAction('delete', …)instead of opening the menu;Shift+F10opens it. - fix —
handleContextMenutookclientX/clientYat face value, which a keyboard-raisedcontextmenureports as 0/0.menuPointFor()falls back to the row's rectangle; the category menu's own handler does the same. - fix —
resolveFocusedCategoryEltook an options argument::widthkeeps the first-category fallback because the palette names what it acts on,Shift+WpassesfallbackToFirst: falseand falls through. - fix —
search.jskept a code-name list (KeyM/KeyD/KeyT/KeyB) that had fallen behind the keys added since; it tests/^Key[A-Z]$/now. - fix —
setupPageShortcutswas the only typing guard in the app without anisContentEditablecheck.
Discoverability
- new —
DashboardContextMenurenders akbd.move-popover-item-keyper entry with a matchingaria-keyshortcuts, for all eleven actions that have a key.ShortcutFormat.ariaKeys()and.modifierLabel()are shared with the category menu, which had the chips but no aria. - new —
syncShortcutAriaHints()stampsaria-keyshortcutson the toolbar, the header buttons and the first nine page tabs. It runs independently ofshowShortcutTooltips: the tooltips are a desktop hover affordance that can be switched off, this is the only route the keys have to assistive technology.shortcutButtonDefs()replaces the three parallel lists that let the header row end up with tooltips and no aria. - fix — the side-rail legend printed the what's-new button's
★glyph in the same chip as real keys.
Cheat sheet
- fix —
buildPrintSectionspassed each row'sprintFallbackto the caller's label resolver as a fallback, andgenerate-cheatsheet.cjsresolves fromlocales/en.json, where the i18n check guarantees everycheatKeyexists. The short wording therefore never reached paper for any of the 23 rows that carry one.printLabel()uses it directly.validate-cheatsheet-registry.cjsgains a check built with the generator's resolver — with the identity resolver the other checks use, aprintFallbackwins either way and the check would pass against the bug. - new — printed rows for
k/j,Shift+EandShift+Home;nextDash-cheatsheet.html,nextDash-cheatsheet.pdfand thestatic/copy regenerated.
Config
- new —
logFilterActivityis a fourth value for the log viewer's Show control, matched onSource == "activity"bylogEntryMatchesFilter()rather than on severity. Activity lines already flowed intoserverLogSinkthroughlog.Printf("activity: …")andparseServerLogLinealready split the subsystem out; nothing about capture orNEXTDASH_ACTIVITY_LOGchanges. - new — Help → About carries the wordmark and three addresses:
nextdash.cc, GitHub, andjordibrw.nl. Thehelp-signaturesign-off is dropped, so each address appears once.
Dashboard
- fix —
.category-titlecentres its flex items instead of aligning them on the baseline. The+button hangs padding and a border below its own baseline, which made every header carrying it 1.44px taller; centring costs nothing, where aline-heighttall enough to swallow the button added 3px to every header..category-title--multilinekeepsflex-start. - fix —
*carries the same guards as!and., with an exception for its own modal so it still closes the recent list.
Analytics
- new —
settings-snapshotwidens from 21 to 45 fields and a newcontent-snapshotreports install size (bookmarks, pages, categories, tags, finders, collections, monitored, inbox totals). Every value is bucketed client-side;MAX_EVENT_PROPS = 50is Umami's per-event limit, which is why it is two events.analytics_content.gocounts server-side and returns""when telemetry is off —analytics_content_test.goproves it with a nil store, where any counting would panic. - fix — a custom theme reported its own id, which is
theme-<base36>-<4 chars>and random per install: not a name leak, but distinctive enough to follow one install across releases. It reportscustom.
Docs
MANUAL.mdandREADME.mdfollow the moved keys, the G chord, the category-header keys, the right-click menu chips, the log viewer's Activity only and the About tab.locales/{en,nl,de,fr}.json: the cheat-sheet rows for the moved keys, two new rows (navCategoryHeader,bmCategoryDeleteKey), a rewrittenconfig.helpKeyboardBody,keyboardFixedNoteGridNavwithout the hold,logLevelActivity,logActivityHint,helpAboutBody,helpSiteProject,helpSiteAuthor.static/nextdash-wordmark.pngis keyed out oflogo-ascii-on-black-large.pngby greenness rather than brightness; the existinglogo-ascii-transparent.pngclears only pixels darker than 40, which leaves the glow behind the middle of the word as a dark ellipse.- New:
tests/dashboard-keyboard-uniformity.spec.js,tests/config-help-about.spec.js,TestServerLogSinkActivityFilter. Updated: the c-hold and G-chord cases intests/create-page-category-from-dashboard.spec.jsandtests/dashboard-grid-shortcuts.spec.js, the category-menu chip case intests/dashboard-nice-to-haves.spec.js. go generate ./...for the changed CSS/JS.
v1.1.0 — 15 August 2026
The first minor since 1.0: a category can be wider than one column. DASHBOARD_RELEASE and NEXTDASH_WHATS_NEW_DATA_VERSION are bumped, so the modal reopens once.
Categories across columns
- new —
Category.Spread(spreadinbookmarks-{page}.json) marks a category as allowed to run across grid columns; uncategorized and the smart collections keep theirs insettings.categorySpreads[pageId][categoryId], the shapecategorySortModesalready uses.dashboard-category-span.jsowns both, so no caller has to know which applies. - new — the width itself is derived, never stored:
spanForCountismin(ceil(count / categoryItemLimit), effectiveColumns, 12). It was a number the user picked in the first draft; it became a switch because the number is implied by two settings that already exist, and a stored number goes stale the moment a bookmark is added. - new —
applyCategoryItemLimitmultiplies the limit by the span, so a spread category shows its limit once per column and stays the height of its neighbours.refreshAllCategorySpansruns fromfinishIncrementalRefresh, which is what makes an added bookmark bring the next column with it rather than waiting for a reload;settleSpanChangefollows that through to the "+ N more" cut and, in packed mode, to a full re-render. - new — the two settings rule out each other's extreme: `spreadUnavailable...
v1.1.1
Keyboard consistency, and the parts of the app that teach it. Flagged hideFromModal in the index: it counts toward the version number and shows in Config → Overview → Latest update, but does not reopen the What's new modal, which still leads with v1.1.0. DASHBOARD_RELEASE and NEXTDASH_WHATS_NEW_DATA_VERSION are deliberately unchanged — those tokens are what re-show the modal.
Keyboard
- new — the Shift+letter family is closed:
Shift+Einline edit andShift+Vpreview join move, tag, delete, checking, pin, share and reveal.;and[still work, undocumented, the way0still opens the inbox. Brackets mean previous / next sub-tab in config, and one pair of keys meaning two things was the reason to move preview off[. - new — share moves to
Shift+L, soShift+Salways opens config. It was the only key whose meaning depended on whether a row was selected, which took two cheat-sheet rows and a parenthesis to explain. The capture-phase handler still declines rather than swallows a key it has no row for — that is what keepsShift+SreachingsetupPageShortcutsover a selected row, andtests/shift-key-row-actions.spec.js(renamed fromshift-s-two-meanings.spec.js) pins it. - new — bare letters act on keydown.
C_HOLD_MSand the g-chord hold are gone, and with them the wholekeyuphandler inkeyboard-navigation.jsandSearchComponent.addShortcutLetter, which had no other caller._activateGChordModenow runs on the firstg;G_CHORD_MS(3s) is the only timer left. - new —
j/kaliasArrowDown/ArrowUpin the grid, matching config's section rail and bookmark list. - new —
Shift+Homefocuses the category header viafocusCategoryHeader(), which reusesresolveFocusedCategoryEl.Deleteon a focused header callscategoryMenu.runAction('delete', …)instead of opening the menu;Shift+F10opens it. - fix —
handleContextMenutookclientX/clientYat face value, which a keyboard-raisedcontextmenureports as 0/0.menuPointFor()falls back to the row's rectangle; the category menu's own handler does the same. - fix —
resolveFocusedCategoryEltook an options argument::widthkeeps the first-category fallback because the palette names what it acts on,Shift+WpassesfallbackToFirst: falseand falls through. - fix —
search.jskept a code-name list (KeyM/KeyD/KeyT/KeyB) that had fallen behind the keys added since; it tests/^Key[A-Z]$/now. - fix —
setupPageShortcutswas the only typing guard in the app without anisContentEditablecheck.
Discoverability
- new —
DashboardContextMenurenders akbd.move-popover-item-keyper entry with a matchingaria-keyshortcuts, for all eleven actions that have a key.ShortcutFormat.ariaKeys()and.modifierLabel()are shared with the category menu, which had the chips but no aria. - new —
syncShortcutAriaHints()stampsaria-keyshortcutson the toolbar, the header buttons and the first nine page tabs. It runs independently ofshowShortcutTooltips: the tooltips are a desktop hover affordance that can be switched off, this is the only route the keys have to assistive technology.shortcutButtonDefs()replaces the three parallel lists that let the header row end up with tooltips and no aria. - fix — the side-rail legend printed the what's-new button's
★glyph in the same chip as real keys.
Cheat sheet
- fix —
buildPrintSectionspassed each row'sprintFallbackto the caller's label resolver as a fallback, andgenerate-cheatsheet.cjsresolves fromlocales/en.json, where the i18n check guarantees everycheatKeyexists. The short wording therefore never reached paper for any of the 23 rows that carry one.printLabel()uses it directly.validate-cheatsheet-registry.cjsgains a check built with the generator's resolver — with the identity resolver the other checks use, aprintFallbackwins either way and the check would pass against the bug. - new — printed rows for
k/j,Shift+EandShift+Home;nextDash-cheatsheet.html,nextDash-cheatsheet.pdfand thestatic/copy regenerated.
Config
- new —
logFilterActivityis a fourth value for the log viewer's Show control, matched onSource == "activity"bylogEntryMatchesFilter()rather than on severity. Activity lines already flowed intoserverLogSinkthroughlog.Printf("activity: …")andparseServerLogLinealready split the subsystem out; nothing about capture orNEXTDASH_ACTIVITY_LOGchanges. - new — Help → About carries the wordmark and three addresses:
nextdash.cc, GitHub, andjordibrw.nl. Thehelp-signaturesign-off is dropped, so each address appears once.
Dashboard
- fix —
.category-titlecentres its flex items instead of aligning them on the baseline. The+button hangs padding and a border below its own baseline, which made every header carrying it 1.44px taller; centring costs nothing, where aline-heighttall enough to swallow the button added 3px to every header..category-title--multilinekeepsflex-start. - fix —
*carries the same guards as!and., with an exception for its own modal so it still closes the recent list.
Analytics
- new —
settings-snapshotwidens from 21 to 45 fields and a newcontent-snapshotreports install size (bookmarks, pages, categories, tags, finders, collections, monitored, inbox totals). Every value is bucketed client-side;MAX_EVENT_PROPS = 50is Umami's per-event limit, which is why it is two events.analytics_content.gocounts server-side and returns""when telemetry is off —analytics_content_test.goproves it with a nil store, where any counting would panic. - fix — a custom theme reported its own id, which is
theme-<base36>-<4 chars>and random per install: not a name leak, but distinctive enough to follow one install across releases. It reportscustom.
Docs
MANUAL.mdandREADME.mdfollow the moved keys, the G chord, the category-header keys, the right-click menu chips, the log viewer's Activity only and the About tab.locales/{en,nl,de,fr}.json: the cheat-sheet rows for the moved keys, two new rows (navCategoryHeader,bmCategoryDeleteKey), a rewrittenconfig.helpKeyboardBody,keyboardFixedNoteGridNavwithout the hold,logLevelActivity,logActivityHint,helpAboutBody,helpSiteProject,helpSiteAuthor.static/nextdash-wordmark.pngis keyed out oflogo-ascii-on-black-large.pngby greenness rather than brightness; the existinglogo-ascii-transparent.pngclears only pixels darker than 40, which leaves the glow behind the middle of the word as a dark ellipse.- New:
tests/dashboard-keyboard-uniformity.spec.js,tests/config-help-about.spec.js,TestServerLogSinkActivityFilter. Updated: the c-hold and G-chord cases intests/create-page-category-from-dashboard.spec.jsandtests/dashboard-grid-shortcuts.spec.js, the category-menu chip case intests/dashboard-nice-to-haves.spec.js. go generate ./...for the changed CSS/JS.
v1.1.0 — 15 August 2026
The first minor since 1.0: a category can be wider than one column. DASHBOARD_RELEASE and NEXTDASH_WHATS_NEW_DATA_VERSION are bumped, so the modal reopens once.
Categories across columns
- new —
Category.Spread(spreadinbookmarks-{page}.json) marks a category as allowed to run across grid columns; uncategorized and the smart collections keep theirs insettings.categorySpreads[pageId][categoryId], the shapecategorySortModesalready uses.dashboard-category-span.jsowns both, so no caller has to know which applies. - new — the width itself is derived, never stored:
spanForCountismin(ceil(count / categoryItemLimit), effectiveColumns, 12). It was a number the user picked in the first draft; it became a switch because the number is implied by two settings that already exist, and a stored number goes stale the moment a bookmark is added. - new —
applyCategoryItemLimitmultiplies the limit by the span, so a spread category shows its limit once per column and stays the height of its neighbours.refreshAllCategorySpansruns fromfinishIncrementalRefresh, which is what makes an added bookmark bring the next column with it rather than waiting for a reload;settleSpanChangefollows that through to the "+ N more" cut and, in packed mode, to a full re-render. - new — the two settings rule out each other's extreme:
spreadUnavailableReasonreturnsunlimited-itemswhencategoryItemLimitis 0, and the config select disables Unlimited whileanySpreadCategoryholds. Both say why where they are asked for. - new — routes:
DashboardCategoryMenu(entry flips to Back to one column, noaria-checkedbeside a label that already flips),Shift+Winkeyboard-navigation.js,:width on|off|all, adata-cat-spreadbutton per row in the categories editor, andAppearance → Layout → Categories across columnsfor the defaults and the reset. - new —
.category--widespans withgrid-column: span var(--category-span)and repeats the row's track pattern once per column, sosubgridalignment survives. The inner column gap is the grid gap plus2 × --category-inline-pad: a run of categories pads every column on both sides, a single wide box only its outer edges, and without that the inner columns sat 16px left of the grid.syncWideColumnTrackspins the shortcut and lead tracks, which are intrinsic and were sized per repeat by whichever rows happened to land in them. - new — a rule under the header spanning the block, and a
↔Nbadge in the header, both dropped while the category is collapsed or one column wide.
Packed columns
- new — packed keeps its round-robin flex columns while nothing spreads, and switches to a grid (
packed-masonry) the moment something does:--masonry-spanrows per category fromdashboard-packed-masonry.js, `grid-auto-...
v1.1.0
The first minor since 1.0: a category can be wider than one column. DASHBOARD_RELEASE and NEXTDASH_WHATS_NEW_DATA_VERSION are bumped, so the modal reopens once.
Categories across columns
- new —
Category.Spread(spreadinbookmarks-{page}.json) marks a category as allowed to run across grid columns; uncategorized and the smart collections keep theirs insettings.categorySpreads[pageId][categoryId], the shapecategorySortModesalready uses.dashboard-category-span.jsowns both, so no caller has to know which applies. - new — the width itself is derived, never stored:
spanForCountismin(ceil(count / categoryItemLimit), effectiveColumns, 12). It was a number the user picked in the first draft; it became a switch because the number is implied by two settings that already exist, and a stored number goes stale the moment a bookmark is added. - new —
applyCategoryItemLimitmultiplies the limit by the span, so a spread category shows its limit once per column and stays the height of its neighbours.refreshAllCategorySpansruns fromfinishIncrementalRefresh, which is what makes an added bookmark bring the next column with it rather than waiting for a reload;settleSpanChangefollows that through to the "+ N more" cut and, in packed mode, to a full re-render. - new — the two settings rule out each other's extreme:
spreadUnavailableReasonreturnsunlimited-itemswhencategoryItemLimitis 0, and the config select disables Unlimited whileanySpreadCategoryholds. Both say why where they are asked for. - new — routes:
DashboardCategoryMenu(entry flips to Back to one column, noaria-checkedbeside a label that already flips),Shift+Winkeyboard-navigation.js,:width on|off|all, adata-cat-spreadbutton per row in the categories editor, andAppearance → Layout → Categories across columnsfor the defaults and the reset. - new —
.category--widespans withgrid-column: span var(--category-span)and repeats the row's track pattern once per column, sosubgridalignment survives. The inner column gap is the grid gap plus2 × --category-inline-pad: a run of categories pads every column on both sides, a single wide box only its outer edges, and without that the inner columns sat 16px left of the grid.syncWideColumnTrackspins the shortcut and lead tracks, which are intrinsic and were sized per repeat by whichever rows happened to land in them. - new — a rule under the header spanning the block, and a
↔Nbadge in the header, both dropped while the category is collapsed or one column wide.
Packed columns
- new — packed keeps its round-robin flex columns while nothing spreads, and switches to a grid (
packed-masonry) the moment something does:--masonry-spanrows per category fromdashboard-packed-masonry.js,grid-auto-flow: row dense, aResizeObserverper category. Bands were tried first and left a hole the height of the tallest column beside the wide block. - fix —
syncCategoriesFromDomread the columns in document order while the render filled them round-robin. Those are not each other's inverse, so every category drag rewrote the order into one that redistributed differently and scrambled the page.readCategoryElementsInOrderis the single inverse now, keyed on what is in the DOM, andgetExistingCategoriesdelegates to it. - fix —
syncDashboardGridLayoutrebuildsclassNamewholesale and droppedpacked-masonry, so any settings refresh — including the one behind a window resize — left the categories as bare flex children of a row with no columns in it.
Config
- fix — the location memory was written only on the exits config knows about; the header's health, inbox and page buttons switch view around it.
restoreConfigHashnow saves on every move inside config andsetActiveViewstamps it on the way out. The TTL is 5 minutes (DashboardConfigandDashboardConfigLoaderboth), counted from leaving rather than from the last click inside. - new —
type: 'action'panel controls withbindPanelActions, used for Turn spreading off everywhere;NEW_THIS_RELEASEis the one place naming where the twinkle points, drawn on the section, the sub-tab and the panel. - new —
#category-context-menusizes to its content (width: max-content, capped at 24rem). The shared.move-popovercap is for the move/tag/delete pickers; at 16rem the widest row here needed 274px and got 237, so theShift+Wchip was trimmed away — and the French label is half again as long.
Config → Bookmarks
- new — the section gets the sub-tab strip five others already have: List and Settings. The nine settings sat after the list — fifty rows down by default and up to five hundred as the infinite scroll loads more, which also rules out jumping to the bottom of a list whose bottom moves as you approach it. Registered in
SUB_TABS,SUB_TAB_STATE,SUB_TAB_ATTRandSUB_TAB_SECTION, so the deep link, the remembered location and the arrow-key walk come for free;handleOverviewGoacceptsbmTab.
Views
- new — Health, Inbox and Config → Bookmarks draw one card from
feed-row.cssinstead of three copies. The rule was written out inhealth-view.css,dashboard-inbox.cssandconfig-view.cssand the copies had drifted into being byte-identical — one design, three places to change it, and no way to tell whether a difference was meant. What genuinely differs is a modifier:feed-row--with-selectfor Health's checkbox column, and the coloured left edge each view uses for its own state, including modern layout's inset redraw of it. Classic layout also gains the row focus ring only modern had. - new — the shape the three views disagreed on is settled: Inbox rounded its filter pills and buttons where Health squared them off; rounded won. Config → Bookmarks reads as a view rather than a settings panel — a header with a count, the search box sized like Health's, and tiles matching Health's without the stripe.
Discoverability
- new —
spread-notice.js(aNoticeCard) andspread-tutorial.js, a four-stepAppModalwalkthrough in the shape of the inbox and health tours, reachable afterwards fromHelp → Pages & categories. - fix —
NoticeCardbound an action withquerySelector, and a card naming its × throughdismissNameputs that attribute on the × as well. The × comes first, so it took the handler and the button sharing its name got none: the side rail's No thanks had been dead since the cards were unified.
Docs
MANUAL.md— the spreading section with the columns-per-bookmark-count table, and the config-memory paragraphs at five minutes. Config → Bookmarks is described with its two sub-tabs.README.md— the feature bullet,Shift+W,:width on|off, and the five-minute memory.CHANGELOG.md,static/data/whats-new/v1.1.0.json,index.json,whats-new-stub.js,tests/whats-new-hidden-release.spec.js, the four locale files, andgo generate ./...forasset_hashes_gen.go.
v1.0.4
A review pass over the bookmark grid and the menus around it, in five rounds: correctness first, then consistency between routes that do the same thing, then accessibility, then performance, then cleanup. DASHBOARD_RELEASE and NEXTDASH_WHATS_NEW_DATA_VERSION are bumped, so the modal reopens once.
The grid
- fix —
repaintBookmarkMutationSurfacespassedincremental: false, the one flagcanAttemptDataPatchrefuses outright, so every add, edit, delete, move and tag change tore down the whole grid — everyDragReorderinstance, the scroll offset and the focused row — for what is usually one changed row. The incremental path's own guards decide now:canAttemptDataPatchstill bails on a layout-settings change andcategoryStructureMatcheson a category added, removed or reordered. - new — the scroll offset is stored per page in
_pageScrollPositionsand consumed on return. Consumed, not kept:takeRememberedScrolldeletes the entry, so a later deliberate visit still starts at the top. - fix — the incremental renderer built a category's empty state without the
+ bookmarkbuttoncreateCategoryElementincludes. - fix — rows are built at
tabIndex = -1for the roving tab stop andKeyboardNavigationhands one of them a0only after walking the grid, which a render does not do — so Tab skipped the grid entirely from first paint.syncBookmarkGridA11yguarantees exactly one stop now, on the rowcurrentIndexpoints at rather than on row 0, which would walk the tab position away from the user on every repaint.
Selecting several bookmarks
- new —
deleteSelectedoffers an undo toast for 8s, restoring in ascending index order (each splice shifts what follows) and dropping the redundant trash entries afterwards viadropRestoredTrashEntries. - fix — a failed save left the rows spliced out of the model and the selection cleared, then returned silently. Both are restored and
showErrorNotificationis called. - fix — the toolbar was
prepended into#dashboard-layout, which carriesrole="grid"; arole="toolbar"among rows and rowgroups is invalid. It also rendered as a 90×278 vertical strip, because the grid is a flex row andwidth: 100%had nothing to fill. Inserted before the grid instead: full width, 44px tall, still sticky. - fix — the tags popover bound no scroll or resize handler and drifted away from its anchor. It reuses the shared reposition and outside-close helpers now.
Search
- new —
SearchComponent.FILTER_KEYSreplaces three hardcoded four-item lists.opened:andadded:parsed and filtered correctly but were absent fromgetFilterHintItems, unoffered as a prefix, and unrecognised by_isCompleteFilterQuery, so the completion list kept firing over real results. Their values are offered the way status values are, withneveronly underopened:.
Tag filters
- fix —
updateTagFilterIndicatorhad been reduced to a teardown that emptied#tag-filter-indicatoron every call, leaving the header element and its CSS doing nothing. It carries the active tags and the match count now, and hides itself while the in-grid banner is on screen so the chips are not shown twice.renderTagFilterBannergainedwithToolbarfor that.
Menus and popovers
- fix —
resolveRowBookmarkfell back to the URL for rows with no page-local index and searched the current page first. The same URL legitimately sits on more than one page, so a row belonging to another page resolved to the wrong bookmark before delete ever saw it. It matches on the row's visible label now; list order only decides when nothing distinguishes the candidates. Fixed in both copies —dashboard-bookmark-interactions-loader.jskeeps its own, and that is the one that runs on the first right-click of a session. - fix —
_persistBookmarkFieldswallowed every error and returned nothing while all three pin routes flippedpinnedthemselves. It applies the change (synchronously, before its first await, so the palette label still updates), reverts on failure, surfaces an error and returns a boolean; the callers no longer flip anything. - fix — the
Deletekey went throughAppModalwhileShift+Dand the context menu used the anchored popover. All three use the popover now. The modal stays for the inline editor, whose row is a form at that moment, and for a multi-row selection. - fix — one shared
_bindActionPopoverOutsideClosefor Move, Delete, Tag and the multi-select tags popover: it also closes on an outsidecontextmenu, and matches the anchor by containment rather than identity. - fix —
_focusActionPopoverItempicks its pattern from the container's role:aria-activedescendantwith focus on the box forrole="listbox", roving tabindex for menus. The tag popover's second capture-phase keydown listener is removed — document capture runs first and every branch callsstopImmediatePropagation, so it never ran. - new — the check-mode submenu entry sets
aria-expanded, opens withArrowRightand closes withArrowLeft, matching the Escape path that already existed. - new — the category menu shows
F2andDeleteasaria-hiddenchips. "Add category" gets none: it is a heldc, and a chip would promise a tap that goes to the shortcut search. - new — the category sort button opens on
ArrowDown, whicharia-haspopup="menu"already promised. - fix —
_restoreActionPopoverFocusfell back to focusing a detached anchor, a no-op that left focus on<body>. It falls through to the first row still on the grid.
Reduced motion
- fix —
dashboard.cssoverrode four selectors withanimation: none !important, contradictingreduced-motion.css, which uses0.01msprecisely soanimationendstill fires.bookmark-copy-flashandbookmark-pulsestayed on the row for the rest of the session. The 320ms wait before a cross-page move is skipped under reduced motion (ANIM.BOOKMARK_MOVE_OUT,prefersReducedMotion()).
Performance
aria-colindex/aria-colcountare constant and are stamped inpopulateBookmarkRowView;syncBookmarkGridA11yno longer does aquerySelectorper row on every render to rewrite them.- Click and auxclick are delegated to
#dashboard-layout— two listeners for the grid instead of two per row.contextmenustays per row:bindRowserves inbox cards too. restartRowAnimationreplaces five hand-written copies of remove/reflow/add plusanimationend.- The action popovers reposition once per frame instead of once per scroll event, and the toolbar tooltip's
pointermovesweep is frame-throttled with its header buttons cached.
Docs
MANUAL.md— theDeleterow in the shortcut table, and theShift+Sentries in the cheat sheet registry and all four locales.nextDash-cheatsheet.htmland both PDFs — unchanged this release; the affected rows are not in the printed sheet.static/data/whats-new/v1.0.4.json,index.json,whats-new-stub.js,tests/whats-new-hidden-release.spec.js,README.md.- 64 unused
const d = this.dash;bindings across 12 files; two JSDoc blocks moved to the functions they describe;CLICK_OUTSIDE_DELAY_MS,syncAllBookmarksMetadataand the public delegate forupdateBookmarkRowsCategoryInDomremoved. go generate ./...for the changed CSS, JS and locales.
v1.0.3 — 14 August 2026
One theme across most of it: openCount, lastOpened, createdAt, updatedAt and tags were written on every path, fed the built-in features, and reached almost none of the ones the user drives. Plus settings for Config → Bookmarks, which had none.
Unlike v1.0.1 and v1.0.2 this is not flagged hideFromModal, and both of those had the flag removed in this release — every version is visible in the What's new modal again. DASHBOARD_RELEASE and NEXTDASH_WHATS_NEW_DATA_VERSION are bumped, so the modal reopens once for everyone.
Config
- new — the remembered config location applies on every exit and every way back in, expiring after 15 minutes (
CONFIG_LAST_TTL_MS, stamped on the way out). It was saved only when leaving viaShift+HorShift+Iand cleared on every other exit, andresolveConfigOpenTargetadditionally requiredactiveViewto be health or inbox — so the common route, Escape out andShift+Sback in, always landed on Overview.clearLastConfigLocationhas no callers left and is removed. The expiry is duplicated indashboard-config-loader.js, which reads the same entry on a cold load before the module exists; an entry with nosavedAtpredates the expiry and is treated as stale.
Config → Bookmarks settings
- new — ten settings, all previously constants: stored sort (
configBookmarksSort), rows per load step, and the availability, pin and category a quick-added bookmark starts with; the interval a bookmark gets when switched to Monitor, including in bulk; thresholds for confirming a delete and a bulk icon refresh; the "not opened in N days" figure, which also drives the cleanup score; and the archive service template. - new —
clampBookmarkSettingsenforces every range server-side rather than leaving it to the controls, since the API is reachable without the browser. The archive template must contain{url}and start withhttp, so it can never become ajavascript:URL handed towindow.open. - fix — opening a bookmark from Config now respects
openInNewTab. The setting existed and the grid honoured it; this path forced a new tab regardless.
Reaching the data
- new —
opened:andadded:in the search parser, matched bymatchesAgeFilteragainsttoday/week/month/year, plusneverforopened:. An unrecognised word filters nothing rather than everything, which is the safer way round for a typo. - new — custom collection rules gain
pinned,untagged,notOpenedDaysandchangedDays.valuelessRuleFieldskeepspinnedanduntaggedwithout a...
v1.0.3
One theme across most of it: openCount, lastOpened, createdAt, updatedAt and tags were written on every path, fed the built-in features, and reached almost none of the ones the user drives. Plus settings for Config → Bookmarks, which had none.
Unlike v1.0.1 and v1.0.2 this is not flagged hideFromModal, and both of those had the flag removed in this release — every version is visible in the What's new modal again. DASHBOARD_RELEASE and NEXTDASH_WHATS_NEW_DATA_VERSION are bumped, so the modal reopens once for everyone.
Config
- new — the remembered config location applies on every exit and every way back in, expiring after 15 minutes (
CONFIG_LAST_TTL_MS, stamped on the way out). It was saved only when leaving viaShift+HorShift+Iand cleared on every other exit, andresolveConfigOpenTargetadditionally requiredactiveViewto be health or inbox — so the common route, Escape out andShift+Sback in, always landed on Overview.clearLastConfigLocationhas no callers left and is removed. The expiry is duplicated indashboard-config-loader.js, which reads the same entry on a cold load before the module exists; an entry with nosavedAtpredates the expiry and is treated as stale.
Config → Bookmarks settings
- new — ten settings, all previously constants: stored sort (
configBookmarksSort), rows per load step, and the availability, pin and category a quick-added bookmark starts with; the interval a bookmark gets when switched to Monitor, including in bulk; thresholds for confirming a delete and a bulk icon refresh; the "not opened in N days" figure, which also drives the cleanup score; and the archive service template. - new —
clampBookmarkSettingsenforces every range server-side rather than leaving it to the controls, since the API is reachable without the browser. The archive template must contain{url}and start withhttp, so it can never become ajavascript:URL handed towindow.open. - fix — opening a bookmark from Config now respects
openInNewTab. The setting existed and the grid honoured it; this path forced a new tab regardless.
Reaching the data
- new —
opened:andadded:in the search parser, matched bymatchesAgeFilteragainsttoday/week/month/year, plusneverforopened:. An unrecognised word filters nothing rather than everything, which is the safer way round for a typo. - new — custom collection rules gain
pinned,untagged,notOpenedDaysandchangedDays.valuelessRuleFieldskeepspinnedanduntaggedwithout a value, so the server no longer drops them as half-filled.changedDaysis the first readerupdatedAthas ever had on the dashboard. - new — a Recently added smart collection on
createdAt, off by default, with its own limit and page scope. The other four all key onlastOpenedoropenCount, so "what did I just add" was unanswerable. - new —
previewDescjoins the search haystack, scored below the note: the site's words rather than the user's. - new — drift reaches the dashboard header badge in the warning tier. A drifted link still returns 200, which is exactly why it needed a human.
Grid
- new — tag chips on rows, off by default, capped at a configurable two with the rest collapsing into
+N. Rendered inside.bookmark-linkrather than as a column: it is a subgrid whose columns align across every category, so an extra column would shift every row. Verified by measuring the shortcut column before and after — identical to the pixel, and pinned as a test. - new —
createSortControlsrebuilt as one active button plus a⋯menu. Four buttons per header, repeated per category, took more width than the bookmark names; the strip goes from ~90px to 19px, or 58px once something is sorted. - new — two sort modes,
addedandopens. - fix — the grid's
recentmode sorted bylastOpenedwhile Config used the same words forcreatedAt. Renamed toopened;normalizeSortModestill accepts the old value so stored categories keep working.
Keyboard and pointer
- new —
Shift+Ppin,Shift+Sshare,Shift+Rreveal in Health,tfilter to the row's tag. Each delegates to the existing implementation —_persistBookmarkField,shareBookmark,revealInHealth,toggleTagFilter— rather than adding a second path that could drift. - new — pin gains a right-click entry. It had
Shift+Pand:pinbut no pointer route at all from the grid, while every other one-bit row action had one. - fix —
Ctrl/Cmd+Enteropens in a new tab for that press alone.selectCurrentElementfired a bare.click(), which constructs noMouseEventand carries no modifier, so the keyboard had only the standingopenInNewTabpreference.
Inbox
- fix — triage swallowed every key but Escape. Its guard asked
dash.isModalOpen(), which counts the triage overlay itself, so the overlay blocked its own keyboard. Now asksisLayeredModalOpen()— whether something sits over triage.advance()was correct all along; nothing called it. Reported earlier and not reproducible then; the repro is a seeded queue of three and two presses ofj. - new — a seven-step one-time tour (
inbox-tutorial.js,inboxTutorialV1), fetched on demand rather than with the module: the inbox loads during bootstrap for the unread badge, so riding along would cost every session that never opens the view. Guards and structure mirrorhealth-tutorial.js. - fix —
resetOnboardingnow callsclearSeenTips(). It only clearedonboardingCompletedwhile its own dialog promised to replay "the welcome tour and tips" — those ids live indiscoverabilityState.seenTips, which it never touched, so no tour or tip had ever come back.tipsNotBeforeis cleared with them;seenSettingPromosis deliberately left alone.
Config → Help
- new — a fifth panel under Help → Inbox describing the one-time tour, and a matching entry in the help search index.
Docs
README.mdandMANUAL.md(§4.5, the config chapter and the shortcut table) corrected for the config location memory: three of the four places describing it stated the old rule.- Four tests in
config-dashboard-view.spec.jsasserted the restored section without resettingconfig.sectionfirst, whichcloseConfigViewleaves in memory — so they could not have failed. They reset it now. MANUAL.md§4.4 and §7.9 cover the tour; the Health tour's replay line was corrected — it claimed a button that did not do what it said, which is what turned up theresetOnboardingdefect.README.md,CHANGELOG.md, the What's new modal and Config → Overview updated for this release.- 45 locale keys per language for the tour, 2 for the help panel, in
en,nl,deandfr; the German and French reset hints were corrected against the real control labels. resetDashboardData()added to the e2e helpers, and used by four spec files that depend on what they find./api/resetre-seeds the defaults in ~39ms, so this is far cheaper than the per-spec fixtures estimated earlier. The suite as a whole is still not hermetic: ~166 files share one data directory.- Known and not addressed:
monitor-visibility-revealshows two flaky tests in group runs that pass on retry.
v1.0.2 — 14 August 2026
A repair release for the config view, from a full audit of it. Flagged hideFromModal in index.json like v1.0.1, so it does not reopen the What's new modal.
The recurring fault: a read that failed degraded to an empty list, and the next write posted that emptiness back as the complete state. Failure and emptiness were the same value, so a server blip during an edit destroyed real data behind a "Saved" badge.
Data integrity
- fix —
ensureCategoryOnPage(dashboard-config.js) turned a failed GET into a one-item POST that replaced every category on the page. Proven end to end: five categories in, one out, no error shown.SaveCategoriesByPage's guard cannot catch it, since it only rejects a zero-length list. Now throws rather than degrading. - fix —
loadFindersandloadCategoriesEditorset a_xLoadFailedflag instead of[];saveFindersandsaveCategoriesrefuse to write while it is set. - fix —
saveCategoriesreturns whether it saved and takes the page id as an argument, captured by each caller at edit time. It swallowed the error and returnedundefinedeither way, so the delete flow acted on a 409 as if it had worked — trash entry, undo toast and all. The page picker reassigns_catPageIdsynchronously, so a save in flight could also land on the wrong page. - fix —
SaveSettings(handlers.go) reports dropped collections in the response instead of discarding them behind{"status":"success"}. Surfaced when leaving the Collections tab, not per save: a half-filled row is the normal state while typing. - fix —
publishConfigSyncadded todashboard-config-sync.js. The listener, the pending-marker drain and four specs all existed; nothing ever published, so a second tab stayed stale until reloaded by hand. The specs write the markers themselves, so they passed either way.
Config
- fix —
refreshAllFaviconsposted a body-less request to/api/bookmarks/prefetch-icons, which is per-page and decodes the body first, so it answered 400 every time. Routed throughConfigFaviconPrefetch, assearch-commands.jsanddashboard-quickstart.jsalready did. - fix — the prefetch overlay drops
pointer-eventson completion. It covers the viewport atz-index: 12000and stayed up through the 900ms completion pause and the reloads after it, so the page read "Icons updated" while every click landed on the overlay. Found via a spec that timed out clicking Save;elementFromPointover the button returned the overlay. - fix —
guardUniqueNamerejects empty and over-long names. It only ever checked duplicates and delegated emptiness to callers that n...
v1.0.2
A repair release for the config view, from a full audit of it. Flagged hideFromModal in index.json like v1.0.1, so it does not reopen the What's new modal.
The recurring fault: a read that failed degraded to an empty list, and the next write posted that emptiness back as the complete state. Failure and emptiness were the same value, so a server blip during an edit destroyed real data behind a "Saved" badge.
Data integrity
- fix —
ensureCategoryOnPage(dashboard-config.js) turned a failed GET into a one-item POST that replaced every category on the page. Proven end to end: five categories in, one out, no error shown.SaveCategoriesByPage's guard cannot catch it, since it only rejects a zero-length list. Now throws rather than degrading. - fix —
loadFindersandloadCategoriesEditorset a_xLoadFailedflag instead of[];saveFindersandsaveCategoriesrefuse to write while it is set. - fix —
saveCategoriesreturns whether it saved and takes the page id as an argument, captured by each caller at edit time. It swallowed the error and returnedundefinedeither way, so the delete flow acted on a 409 as if it had worked — trash entry, undo toast and all. The page picker reassigns_catPageIdsynchronously, so a save in flight could also land on the wrong page. - fix —
SaveSettings(handlers.go) reports dropped collections in the response instead of discarding them behind{"status":"success"}. Surfaced when leaving the Collections tab, not per save: a half-filled row is the normal state while typing. - fix —
publishConfigSyncadded todashboard-config-sync.js. The listener, the pending-marker drain and four specs all existed; nothing ever published, so a second tab stayed stale until reloaded by hand. The specs write the markers themselves, so they passed either way.
Config
- fix —
refreshAllFaviconsposted a body-less request to/api/bookmarks/prefetch-icons, which is per-page and decodes the body first, so it answered 400 every time. Routed throughConfigFaviconPrefetch, assearch-commands.jsanddashboard-quickstart.jsalready did. - fix — the prefetch overlay drops
pointer-eventson completion. It covers the viewport atz-index: 12000and stayed up through the 900ms completion pause and the reloads after it, so the page read "Icons updated" while every click landed on the overlay. Found via a spec that timed out clicking Save;elementFromPointover the button returned the overlay. - fix —
guardUniqueNamerejects empty and over-long names. It only ever checked duplicates and delegated emptiness to callers that never picked it up, so a cleared name saved as""and two emptied names stopped colliding, since""is never taken. Server clamps to the same 60 characters viaclampEntityName(rune-wise, throughtruncateRunes), because the API is reachable without the browser. - new —
checkedAt,nextBackupAtandtotalKeptare rendered. All three were shipped by the server and read by nothing;totalKeptwas silently excluded from the inbox conversion sum while the panel above showed it as its own tile. - fix — four
config.*keys were missing from every locale (addBookmarkBtn,bookmarkNotFound,clearBookmarkFilters,saved).
Accessibility
- fix —
labelSettingsControls()names every schema-rendered control after the panel is drawn. Labels render as<span class="config-field-label">, not<label for>, so selects and number inputs across Behavior and much of Appearance had no accessible name. Done in one pass rather than at ~30 render sites that would drift. Ranges also getaria-valuetext, since0.85is not what the UI shows. - fix —
captureControlPanelFocus()restores focus and caret acrossrepaintActiveControlPanels. Controls bind onchange, which fires while the control still has focus, and the repaint replaces the whole body — so the next Tab started from the top of the page.repaintTagsBodyalready did this, with a comment explaining the hazard. - fix —
bindSubTabStripre-focuses on the next frame rather than testingtarget.isConnectedimmediately. Appearance activates throughrender(), which had not run yet, so the branch was skipped and focus landed on<body>— one ArrowRight killed every press after it. - fix — both hand-rolled confirm dialogs handle Tab through
FocusTrapUtils.trapTabKey. They declaredaria-modal="true"and only handled Escape, so focus wandered into the page behind, including on Reset all data.
Import and export
- new — theme import (
importThemeFromFile,normalizeImportedTheme), reusing Duplicate's id and naming logic so it lands as a new theme and cannot overwrite the palette on screen. A JSON file with no colour values is refused. - new — CSV import (
importBookmarksCSV,parseBookmarksCSV). Hand-written parser rather thansplit(','): the export quotes every field so a note can hold a comma, a doubled quote or a newline. Columns are matched by header, so reordering them in a spreadsheet still imports. Rows go through/api/bookmarks/import-browser, keeping URL de-duplication server-side. - new — trash search across name, URL, tag, category and origin page, plus tick boxes and a bulk restore that runs per item so one failure cannot take the batch with it. Select-all covers what the search shows.
- new —
SavedSearchadded toSettings, normalized bynormalizeSavedSearches(trimmed, incomplete entries dropped, capped at ten). Saved searches lived only inlocalStorage, so they were in no ZIP backup; anything left there is migrated on first read.
Removed
- Page archiving.
Settings.ArchivedPageIdshad defaults and a migration, and MANUAL and README described it as working and said where to find it, but no line of JS ever read it.
Docs
- Release dates now name the day in both the changelog and the modal, per the new convention; v1.0.0 and v1.0.1 backfilled.
- Twelve spec files fixed. None were product regressions: seeded inbox items read before the view had loaded them;
showRecentButtonasserted on the Display tab after it moved to Toolbar (eight failures from one line);appearanceTabset beforeopenConfigView, which resets it;.first()matching a bookmark's smart-collection copy, whose row carries no page-local index;role="feed"asserted on#dashboard-layoutfor health, which renders it on.health-view-feed; cheat-sheet rows asserted visible inside collapsed<details>;last-opened-format.jsstill counted as lazy after8115b0e7made it eager; a hardcoded help-tab list now read fromHELP_TABS; analytics regexes demanding the retiredvYYYY.scheme; andconfig-info-resetdemanding anℹfor five fields whose own comment inFIELD_METAexplains they carry none. - Known and not addressed: the suite is not hermetic. All 170 spec files share one data directory with no reset between tests, so parallel and sequential runs drop different handfuls. Every file that failed the last full run passes on its own.
v1.0.1 — 13 August 2026
A patch release on top of v1.0.0. Recorded here and versioned normally, but flagged hideFromModal in index.json so it does not reopen the What's new modal in front of users who have just read the 1.0 entry — see v2026.09.09.1 for the same treatment.
Health
- new — the collection trend chart moved out of
.health-view-toolbar-actions, where it was aflex: 1 1 6remtrack competing with the button row for space, into a new.health-view-note-rowshared with the filter note. Reserving a column beside the buttons viapadding-rightwas tried first and reverted: it stranded whichever button ran out of room on a line of its own. - new — the chart gained a midpoint gridline, an endpoint dot, and a
100%axis label rendered outside the SVG. The label sits outside becausepreserveAspectRatio="none"stretches the viewBox to the layout box and would distort any type inside it. Only the ceiling is labelled; a 50% label was added and removed as one number too many. - new — per-day hover readout. Hit zones are absolutely positioned
<button>elements sized in CSS percentages rather than SVG geometry, for the samepreserveAspectRatioreason: coordinates inside the viewBox drift away from where the pointer actually is.trendPointLabel()readsHealthTrendPoint.t(Unix ms), not ad/dayfield — an earlier draft assumed the latter and produced empty labels. A null reading renders "no reading" rather than 0%. - new —
showTrendExplainer()behind anℹbeside the chart, kept separate fromshowHealthExplainer()so the fixed-axis and gap rationale is not buried in the view-wide text.
Update check
- fix —
fetchGitHubLatestReleaseread GitHub's/releases/latest, which resolves "latest" bypublished_atrather than by version. A patch published on the calendar line after v1.0.0 would be named there and then correctly rejected bycompareReleaseTags, so a genuinely newer release was never announced in the modal or in Config → Overview. Now reads the release listing and orders it withcompareReleaseTags, falling back to/releases/latestwhen the listing cannot be read. - fix — the listing URL is derived from
githubLatestReleaseURLviareleaseListURL()rather than declared as a second package var. As two independent vars,TestFetchGitHubLatestReleasestubbed one and reached the real api.github.com with the other, passing against live data.
Docs
- Three inbox shortcuts shipped in v1.0.0 without reaching
KeyboardViewLegends:Shift+↑/↓(extend selection),Ctrl/Cmd+A(select all) andR(refresh). Added toINBOX_VIEW, which feeds both the inline legend and the cheat sheet. Printable sheet: 55 → 58 rows;nextDash-cheatsheet.htmland both PDFs regenerated. TestWhatsNewStubReleaseConstantsasserted a literal2026.07prefix onDASHBOARD_RELEASE; the regex now a...
v1.0.1
A patch release on top of v1.0.0. Recorded here and versioned normally, but flagged hideFromModal in index.json so it does not reopen the What's new modal in front of users who have just read the 1.0 entry — see v2026.09.09.1 for the same treatment.
Health
- new — the collection trend chart moved out of
.health-view-toolbar-actions, where it was aflex: 1 1 6remtrack competing with the button row for space, into a new.health-view-note-rowshared with the filter note. Reserving a column beside the buttons viapadding-rightwas tried first and reverted: it stranded whichever button ran out of room on a line of its own. - new — the chart gained a midpoint gridline, an endpoint dot, and a
100%axis label rendered outside the SVG. The label sits outside becausepreserveAspectRatio="none"stretches the viewBox to the layout box and would distort any type inside it. Only the ceiling is labelled; a 50% label was added and removed as one number too many. - new — per-day hover readout. Hit zones are absolutely positioned
<button>elements sized in CSS percentages rather than SVG geometry, for the samepreserveAspectRatioreason: coordinates inside the viewBox drift away from where the pointer actually is.trendPointLabel()readsHealthTrendPoint.t(Unix ms), not ad/dayfield — an earlier draft assumed the latter and produced empty labels. A null reading renders "no reading" rather than 0%. - new —
showTrendExplainer()behind anℹbeside the chart, kept separate fromshowHealthExplainer()so the fixed-axis and gap rationale is not buried in the view-wide text.
Update check
- fix —
fetchGitHubLatestReleaseread GitHub's/releases/latest, which resolves "latest" bypublished_atrather than by version. A patch published on the calendar line after v1.0.0 would be named there and then correctly rejected bycompareReleaseTags, so a genuinely newer release was never announced in the modal or in Config → Overview. Now reads the release listing and orders it withcompareReleaseTags, falling back to/releases/latestwhen the listing cannot be read. - fix — the listing URL is derived from
githubLatestReleaseURLviareleaseListURL()rather than declared as a second package var. As two independent vars,TestFetchGitHubLatestReleasestubbed one and reached the real api.github.com with the other, passing against live data.
Docs
- Three inbox shortcuts shipped in v1.0.0 without reaching
KeyboardViewLegends:Shift+↑/↓(extend selection),Ctrl/Cmd+A(select all) andR(refresh). Added toINBOX_VIEW, which feeds both the inline legend and the cheat sheet. Printable sheet: 55 → 58 rows;nextDash-cheatsheet.htmland both PDFs regenerated. TestWhatsNewStubReleaseConstantsasserted a literal2026.07prefix onDASHBOARD_RELEASE; the regex now accepts both the calendar and semver suffix shapes.TestBuildUpdateStatusDetectsNewerReleaseusedv9999.99.99.9as its newer upstream. That first segment is abovecalendarVersionFloor(1000), so it is read as a calendar tag and correctly loses to semver — the fixture was wrong, not the comparison. Nowv99.0.0.- New
tests/health-trend-placement.spec.js(6 specs). Locale keys for the trend title, axis, hover and explainer added to en/nl/de/fr. go generate ./...regeneratedasset_hashes_gen.gofor the changed JS and CSS.
v1.0.0 — August 2026
The first release under semantic versioning, and deliberate milestone: after a long run of bug fixing and consolidation has left the app feeling finished and stable rather than in flux. From here 1.x.0 carries features and 1.0.x carries fixes; see v2026.09.09.3 for the scheme change itself, which shipped the comparison logic ahead of this tag.
The bulk of this release is correctness work. A recurring class of bug ran through it: an operation that reported success for something it had not done — a move that could lose the bookmark, a category save that silently no-opped, an add that discarded the item it had just accepted. Several were found by tracing a pattern already solved correctly elsewhere in the same file.
Data integrity
- fix —
_moveBookmarkToPage(dashboard-inline-edit.js) did a double whole-list read-modify-write: GET both pages, splice/push in memory, POST both back. A concurrent write to either page was clobbered, and a source save landing while the target save failed lost the bookmark from both. Now uses the single-itemPOST /api/bookmarks/add+DELETE /api/bookmarksendpoints, each atomic under the store lock. - fix —
bulkMoveTagFilterToPage(dashboard-tag-filter.js) had the same shape for a whole batch. Converted to per-item add+delete viaPromise.allSettled, with a partial-failure toast. - fix —
deleteRemoteBookmarkInlineread the whole source page, spliced, and POSTed it back, racing any concurrent write. Now uses the single-item DELETE endpoint.saveRemoteBookmarkEditdeliberately left as-is: no single-item update endpoint exists, and emulating one as delete+add would move the bookmark to the end of its page. - fix —
SaveCategoriesByPagereturnednilfor an empty-list save while bookmarks still referenced a category, changing nothing. Now returnsErrCategoriesStillReferenced, mapped to 409 byrespondCategoriesSaveError. - fix — category rename without
originalIdfell back to matching by array position, so dropping a middle category reassigned later categories' bookmarks. Positional fallback removed. - fix —
AddInboxLinktrimmed at capacity after appending, discarding an item with an olderAddedAtwhile returning success. Now usestrimInboxItemsKeepingplus a survival check, mirroringRestoreInboxLink. - fix —
DeleteBookmarkFromPageskipped read-cache invalidation. - fix — concurrent page/category creation could clobber each other.
- fix —
fetchBookmarkPreviewcached error pages as valid previews.
API and validation
- fix —
PUT /api/inboxskipped every validationPOSTperforms: novalidateBookmarkURL, nosanitizeBookmarkIcon. It was the one route that would store ajavascript:URL or a private address underallowLocalBookmarks:false, and the only path where a client could writeIcon. - fix — an empty inbox URL returned 500 for a client error.
validateBookmarkURLintentionally permits empty strings (bookmarks may have none), so the check belongs in the handler. - fix —
GetCategories/SaveCategoriesaccepted a nonexistent page, materialisingbookmarks-N.jsonas a side effect. Both now 404 viapageExists. - fix —
GetBookmarkssilently returned200 []for a missingpage/allparam. - fix — bookmark
Name/Category/Notewere never trimmed server-side, unlikeTags/Icon. - new —
POST /api/categories?dryRun=1reports what a category save would do — which bookmarks move, which are orphaned, which submitted categories carry nooriginalId— and writes nothing. SharesbuildCategoryRemapwith the real save so the preview cannot drift from it. - new — inbox text fields are bounded (
clampInboxLinkFields), applied on add, patch and restore.inbox.jsonis rewritten whole on every mutation, so an unbounded field is paid for by every later request. - fix —
PATCH /api/inboxstoredreadAtverbatim, including negative and far-future values, whilesnoozedUntildirectly above it was carefully clamped. - new —
PATCH /api/inboxacceptstags(a pointer, so clearing is expressible).
Inbox
- new — tags are rendered as filter chips, editable from the row menu, matched by search, and included in both exports.
InboxLink.Tagshad existed and been normalised since the field was added, with no UI at all. - new — a Stats panel reads
/api/inbox-stats, previously consumed only by the config view. Promote rate is measured against triaged (promoted + deleted) rather than added. - new — bulk promote, bulk open, bulk copy links, Shift+click and Shift+arrow range selection,
Ctrl/Cmd+Aselect-all. - new — the right-click menu carries the inbox's own actions rather than the bookmark menu's.
- new —
Rre-fetches the feed, wiring uploadAndRender({refresh}), which had no caller. - fix — a failed load rendered the empty state; now a distinct panel with a Retry button, matching the health view.
- fix — the keyboard cursor is no longer dropped after a delete.
- fix —
Escapeand view-level keys ran below the empty-list guard, so clearing ticks was impossible exactly when a filter hid them. - fix — a consumed
?ib_id=deep link kept clearingsearchQuery/domainFilteron every later render. - fix — triage's delete ignored
deleteItemWithUndo's return value;bulkSnoozediscarded its results entirely;bulkDeletereported partial success as success and snapshotted survivors for undo. - fix — capacity eviction is reported to the client, and evicted items' icons are cleaned up (previously only the explicit DELETE path called
removeUnusedIconFile). - new — a polite live region announces the row count after each render.
Health
- new — an
orphaned-categoryissue type: bookmarks whoseCategoryid matches no category on their page. Detect-only, following theshortcut-conflictprecedent. Category save and browser import now invalidate the health report cache, which they did not need to before. - fix — header badge polling moved from a fixed 60s interval to the server's cache TTL with exponential backoff, and no longer double-fetches on tab return (
dashboard.jsalready refreshed there). - new — header restructured to two rows matching
.inbox-header; the trend chart moved into the toolbar.
Dashboard
- fix —
_smartCollectionFilterNeedsCrossPageDatahad been collapsed into!_isSmartCollectionPageAllowedin an earlier commit; the two answer different questions and diverge on t...
v1.0.0
The first release under semantic versioning, and deliberate milestone: after a long run of bug fixing and consolidation has left the app feeling finished and stable rather than in flux. From here 1.x.0 carries features and 1.0.x carries fixes; see v2026.09.09.3 for the scheme change itself, which shipped the comparison logic ahead of this tag.
The bulk of this release is correctness work. A recurring class of bug ran through it: an operation that reported success for something it had not done — a move that could lose the bookmark, a category save that silently no-opped, an add that discarded the item it had just accepted. Several were found by tracing a pattern already solved correctly elsewhere in the same file.
Data integrity
- fix —
_moveBookmarkToPage(dashboard-inline-edit.js) did a double whole-list read-modify-write: GET both pages, splice/push in memory, POST both back. A concurrent write to either page was clobbered, and a source save landing while the target save failed lost the bookmark from both. Now uses the single-itemPOST /api/bookmarks/add+DELETE /api/bookmarksendpoints, each atomic under the store lock. - fix —
bulkMoveTagFilterToPage(dashboard-tag-filter.js) had the same shape for a whole batch. Converted to per-item add+delete viaPromise.allSettled, with a partial-failure toast. - fix —
deleteRemoteBookmarkInlineread the whole source page, spliced, and POSTed it back, racing any concurrent write. Now uses the single-item DELETE endpoint.saveRemoteBookmarkEditdeliberately left as-is: no single-item update endpoint exists, and emulating one as delete+add would move the bookmark to the end of its page. - fix —
SaveCategoriesByPagereturnednilfor an empty-list save while bookmarks still referenced a category, changing nothing. Now returnsErrCategoriesStillReferenced, mapped to 409 byrespondCategoriesSaveError. - fix — category rename without
originalIdfell back to matching by array position, so dropping a middle category reassigned later categories' bookmarks. Positional fallback removed. - fix —
AddInboxLinktrimmed at capacity after appending, discarding an item with an olderAddedAtwhile returning success. Now usestrimInboxItemsKeepingplus a survival check, mirroringRestoreInboxLink. - fix —
DeleteBookmarkFromPageskipped read-cache invalidation. - fix — concurrent page/category creation could clobber each other.
- fix —
fetchBookmarkPreviewcached error pages as valid previews.
API and validation
- fix —
PUT /api/inboxskipped every validationPOSTperforms: novalidateBookmarkURL, nosanitizeBookmarkIcon. It was the one route that would store ajavascript:URL or a private address underallowLocalBookmarks:false, and the only path where a client could writeIcon. - fix — an empty inbox URL returned 500 for a client error.
validateBookmarkURLintentionally permits empty strings (bookmarks may have none), so the check belongs in the handler. - fix —
GetCategories/SaveCategoriesaccepted a nonexistent page, materialisingbookmarks-N.jsonas a side effect. Both now 404 viapageExists. - fix —
GetBookmarkssilently returned200 []for a missingpage/allparam. - fix — bookmark
Name/Category/Notewere never trimmed server-side, unlikeTags/Icon. - new —
POST /api/categories?dryRun=1reports what a category save would do — which bookmarks move, which are orphaned, which submitted categories carry nooriginalId— and writes nothing. SharesbuildCategoryRemapwith the real save so the preview cannot drift from it. - new — inbox text fields are bounded (
clampInboxLinkFields), applied on add, patch and restore.inbox.jsonis rewritten whole on every mutation, so an unbounded field is paid for by every later request. - fix —
PATCH /api/inboxstoredreadAtverbatim, including negative and far-future values, whilesnoozedUntildirectly above it was carefully clamped. - new —
PATCH /api/inboxacceptstags(a pointer, so clearing is expressible).
Inbox
- new — tags are rendered as filter chips, editable from the row menu, matched by search, and included in both exports.
InboxLink.Tagshad existed and been normalised since the field was added, with no UI at all. - new — a Stats panel reads
/api/inbox-stats, previously consumed only by the config view. Promote rate is measured against triaged (promoted + deleted) rather than added. - new — bulk promote, bulk open, bulk copy links, Shift+click and Shift+arrow range selection,
Ctrl/Cmd+Aselect-all. - new — the right-click menu carries the inbox's own actions rather than the bookmark menu's.
- new —
Rre-fetches the feed, wiring uploadAndRender({refresh}), which had no caller. - fix — a failed load rendered the empty state; now a distinct panel with a Retry button, matching the health view.
- fix — the keyboard cursor is no longer dropped after a delete.
- fix —
Escapeand view-level keys ran below the empty-list guard, so clearing ticks was impossible exactly when a filter hid them. - fix — a consumed
?ib_id=deep link kept clearingsearchQuery/domainFilteron every later render. - fix — triage's delete ignored
deleteItemWithUndo's return value;bulkSnoozediscarded its results entirely;bulkDeletereported partial success as success and snapshotted survivors for undo. - fix — capacity eviction is reported to the client, and evicted items' icons are cleaned up (previously only the explicit DELETE path called
removeUnusedIconFile). - new — a polite live region announces the row count after each render.
Health
- new — an
orphaned-categoryissue type: bookmarks whoseCategoryid matches no category on their page. Detect-only, following theshortcut-conflictprecedent. Category save and browser import now invalidate the health report cache, which they did not need to before. - fix — header badge polling moved from a fixed 60s interval to the server's cache TTL with exponential backoff, and no longer double-fetches on tab return (
dashboard.jsalready refreshed there). - new — header restructured to two rows matching
.inbox-header; the trend chart moved into the toolbar.
Dashboard
- fix —
_smartCollectionFilterNeedsCrossPageDatahad been collapsed into!_isSmartCollectionPageAllowedin an earlier commit; the two answer different questions and diverge on the default empty scope, silently disabling cross-page loading. Both restored, sharing only the id normalisation. - fix — the staleness fingerprint omitted
pinned,checkStatus,iconandnote. - fix —
saveSettingsswallowed its error without logging. - fix — the translation-fallback pattern never fell back on a missing key.
- fix — multi-select Clear referenced a nonexistent
keyboardNavproperty. - fix — smart collection headers could be renamed via long-press/double-click.
- fix —
copySelectedLinkshad noexecCommandfallback for plain-HTTP installs. - fix — the inline-edit save hint always showed Ctrl, never Cmd on macOS.
- fix — multi-select Move/Tags buttons lacked
aria-haspopup/aria-expanded; the recent-bookmarks skeleton lackedaria-busy. - fix — the double
/api/data-revisionround-trip on tab refocus is debounced. - new — bulk tag-filter delete offers an undo toast.
- new —
_applyLoadedPageData's full-container view identity moved into oneFULL_CONTAINER_VIEWStable instead of three hand-copied checks. - new —
NoticeCard(static/js/notice-card.js) is the shared bottom-left card; the analytics and side-rail notices were rewritten onto it. The push/outage notice was removed entirely. - perf —
noteDataMutation(pageID)scopes read-cache invalidation to the page a write touched.
Docs
static/data/whats-new/v1.0.0.jsonadded;index.jsongains the entry first.static/js/whats-new-stub.js— both tokens bumped to2026.08-dashboard-release-v1.0.0/whats-new-v240.tests/whats-new-hidden-release.spec.js— constants test renamed and both literals updated.CHANGELOG.md,README.md,MANUAL.mdupdated;Config → Helpgains a v1.0.0 note.go generate ./...regeneratedasset_hashes_gen.go.