Skip to content

v1.3.0

Latest

Choose a tag to compare

@jordibrouwer jordibrouwer released this 19 Aug 10:33

Twenty-four changes across 148 files, +14,585 / −3,343 lines, and one subject running through all of it — the life of a link. How it gets in when you are not at the desktop that has the extension; how you find out it has rotted; how the tidying becomes something you can finish rather than a number you learn to ignore; and how a page you saved tells you it has published something. The fourth strand is speed: the dashboard's first load is about five times lighter than it was.

What's new

Capture — from wherever you are

  • newPWA share target. Installed as an app, nextDash appears in the phone's share sheet; a shared link lands in the Inbox and the app opens on it. Android often puts the address inside the shared text rather than in the URL field, so the first http(s) address in either is used, with trailing sentence punctuation left to the sentence.
  • newGET /add?url=…&title=…, and a bookmarklet built for you in Config → Help → Inbox carrying this install's origin. Both routes are plain GETs because neither a share sheet nor a bookmarklet can set a header; on an install with a write token, NEXTDASH_CAPTURE_TOKEN opens these two routes and nothing else.
  • newintegrations/: a shell script, two Raycast commands (one for a URL, one for the frontmost tab in five browsers), a Dropzone action, a Ulauncher extension, and Alfred and Apple Shortcuts recipes. No binary bundles — steps you can read instead.
  • newmanifest shortcuts for Inbox, Health and Config, so a long-press on the installed app opens the view rather than the dashboard.
  • fix — duplicate detection looks everywhere, not only at the page being saved to, and answers with the conflicting bookmark's name, page and category rather than a bare 409. Same page is refused; another page asks, with the existing bookmark as a link and Save anyway to file a second copy. Moves send allowDuplicate, since a move is an add followed by a delete.

Link rot

  • newbroken since: the first failure of a run is recorded, so a row can say failing for three weeks rather than only broken. Carried into the filters, the row, the export and the report.
  • newsoft 404 detection: a monitored check can spot a page that answers 200 while saying not found. One bounded body read per check, which is why it is a switch beside the check timeout rather than always on.
  • newgroup by site: one host down reads as one problem instead of forty.
  • newarchive fallback: point a dead link at its last Web Archive capture, keeping the original address in the note.
  • newhistory days: the trend can be read over a longer window, and a window the samples cannot fill says so rather than pretending.
  • newRot report in the toolbar: what has gone, moved or been rewritten, has been failing for over a month, is broken and never opened, and what broke this week. Everything in it was already in the report; what was missing was somewhere it added up.
  • fix — the Rot report button was never styled, and rendered as a bare user-agent button between four styled ones.

Cleaning as a ritual

  • new — a card in the corner of the dashboard names what is waiting — “10 links to review: 4 broken, 3 never opened, 3 not opened in a year” — and starts a bounded session over the worst ten in the health view's Work through. Below five waiting links it stays quiet.
  • new — the session ends: a count of what was dealt with, Another ten while more are waiting, and Done for today which puts the offer away until the next local day. Skipping is not handling, so the count is honest.

Fresh

  • new — a page's <link rel="alternate"> feed is noted while previews are fetched, polled on the background re-check interval with a conditional request, and a bookmark carries a count of what has been published since you last opened it. A Fresh smart collection lists them newest publication first.
  • new — opening the bookmark clears the count; lastOpened is the whole of the read state. No article list, no titles, no OPML — deliberately not a feed reader.
  • new — off by default, under Behavior → Status & health → Fresh; switching it on polls once immediately rather than waiting for the scheduler.

The dashboard, in place

  • newShift + F filters the page you are on: a slim bar narrows the rows, hides the categories left empty, and keeps the layout, the cursor and any selection.
  • newthe scroll offset is remembered per page, across a view change and a reload, for as long as the tab is open. Switch it off under Behavior → General.
  • new — a selection can be pinned and switched to Periodic or Monitor in one action, and a bulk tag change can be undone for eight seconds.
  • newShift + Alt + ← / → moves the focused bookmark into the category beside it; smart collections are skipped, being a query rather than a place.
  • newwhat typing a bookmark shortcut does is a setting: open the moment it matches (default), open after a short pause, or press Enter. Inbox settings moved to a tab of their own, and Search gained the three-card control with an interactive .
  • fixthe default is open the moment it matches again, and v1.2.0's change to press Enter is recorded here as a mistake. The reasoning in v1.2.0 was that an instant shortcut can swallow an ordinary word, and which words survive depends on the shortcuts you own. That is true, and it is the rare case: what the change actually did was charge every shortcut, on every use, a second keystroke to prevent it — which takes away the reason to have a shortcut at all. The collision has two answers that cost nothing when it is not happening: the two other modes, and renaming the shortcut that clashes.
  • fix — the new default reaches existing installs. shortcutOpenMode is written into every settings file v1.2.0 touched, so a default change alone would have reached nobody. migrateShortcutOpenModeDefaultInstant moves installs still carrying enter — the value nobody chose — to instant once, behind shortcutOpenModeInstantMigrated; delay is left alone, because it can only be there because someone picked it, and the marker means anyone who sets Enter back keeps it through the next restart.
  • fix — the category header's glyph follows the favicon harmonisation setting, instead of staying at full saturation beside recoloured icons.
  • fix — the filter bar is placed above the grid rather than inside it. As the layout's first child it was fine while the layout was a CSS grid and wrong in every other shape: packed columns lay it out as a flex row that does not wrap, so the bar became a column of its own — squeezed to a fraction of its width and pushed off the left edge of the window (x = −21 on a three-column page). It also kept a search field inside role="grid" and gave masonry a child that is not a category.

Config

  • newConfig → Help → Tips filters itself. Fifty-three tips in nine groups, and the search in the header returns whole panels — the wrong grain when you are after the one key that does the thing. The field narrows to the line, counts what is left, says so when nothing matches, and hides a group whose rows have all gone. Rows are hidden rather than re-rendered, so the cursor stays where you are typing.
  • newa topic that continues on another tab says so. Health and Monitoring answer different questions on purpose, but the split left no thread: seven panels now carry a Continues in line — availability to uptime and alerts, working through the list to drift, the walkthrough to expected responses and maintenance windows, and back. The button writes the same #config/help/<tab>/<panel> hash the panel's own link copies, so clicking it and following a shared link land in the same place.
  • new — the Server log panel says whether it is on for you, like the four panels that already did. It is off by default, which is exactly when a reader follows the prose and finds nothing where it says. Fresh deliberately has no such line: it is a paragraph inside the monitoring prose rather than a panel, and hanging the line on a panel about something else would say something untrue.
  • new — the version panel reads the release index instead of carrying the number in four translated strings. It read nextDash 1.1.0 for two releases after 1.1.0, because the fourth file was the one nobody remembered. The ★ modal, Overview → Latest update and this heading now agree by construction.
  • fix — on a narrow screen, a config section reached by hash or by a restored visit is scrolled into view properly. The strip carries scroll-snap-type: x proximity, which re-snapped the nearest-fit scroll to the closest button boundary and left Help hanging 36px over the right edge with 112px of scroll still available. It scrolls the strip itself now, aligned to the start as the snap expects.
  • fixHelp opened with empty panels when its prose did not arrive. That text is a locale scope of its own — a third of the translation file, fetched beside the config module — and every panel falls back to nothing, so a page of headings with no text under them was the failure mode. A core locale load replaces the whole bundle, so one landing after the help scope had merged (a reload onto a #config/help link, a settings sync while config is open) wiped it, and switching help tabs repainted from the same empty bundle. The core load now keeps the help keys when the language has not changed, and every help repaint asks the bundle whether the prose is really there rather than trusting a flag.
  • newAppearance → Button bar is one tab for one object: where the bar sits (the five positions, each drawn) and what it carries (main buttons and extras, with Show all / Hide all and a count). The position was a panel on Layout and the twelve toggles were two panels on Toolbar & tabs, two tabs further along, so moving the bar and hiding a button on it were separate errands. Toolbar & tabs keeps the Header group, which is a different strip. Everything that pointed at the old homes now points here: the settings search, the side-rail card's Open button bar settings, both Overview → New features entries, Help → Appearance, the manual and the README.
  • new — the settings search matches a setting's current value as well as its name, and shows it after the location, with switches read as On/Off.
  • new — settings that stay server-wide while Keep settings on this device only is on carry an all devices mark; the switch itself is marked this device.
  • newDuplicate a page (with its categories, and optionally its bookmarks) or a category (with its width, icon and sort). Copies start with empty check and open history, and without the original's shortcut.
  • newedit a name or shortcut in the row: double-click the title or click the shortcut pill — including the faint + on a bookmark that has none. A shortcut already in use is named as you type it and refused on Enter, checked across every page.
  • new — a tag in Most used tags opens Config → Bookmarks filtered to it.

Speed

  • new — 99 scripts and 42 stylesheets are served as two bundles, the generated theme is inlined, Help's translations load with Help, and each view's stylesheet arrives when the view is opened. First load: 169 requests and 915 KB → 30 requests and 685 KB. NEXTDASH_BUNDLE=off restores the individual tags.
  • new — config's cold open is 74 ms → 55 ms, the bookmark list draws 2757 → 1762 nodes, and the module is 1000 KB → 884 KB with the statistics renderers loading beside their own section.

Docs

  • static/data/whats-new/v1.3.0.json and the index entry ahead of v1.2.1. DASHBOARD_RELEASE and NEXTDASH_WHATS_NEW_DATA_VERSION are bumped, which is what reopens the modal for everyone — right for a feature release, where v1.2.1 deliberately left both alone.
  • tests/whats-new-hidden-release.spec.js: the shipped-index case now asserts v1.3.0 leads the index and the modal, that v1.2.1 is still recorded and still hidden behind it, and that the two constants name v1.3.0. The four fixture-driven cases that prove the mechanism are untouched.
  • Config → Help gained the release's subjects in every tab that owns one — the shortcut modes, the cross-page duplicate question, coming back where you were, Fresh, the review session and the Rot report, finding a setting by value, and the capture routes — plus eleven new Tips, in four languages.
  • The cheat sheet gained Shift + F and Shift + Alt + ← / →, and the printable sheet now carries every key the modal does — 12 sections, 164 rows, three A4 pages in two columns, with 32 one-off palette commands left off and counted. It was a curated subset policed by a 70-row one-page budget, which made the paper a smaller product than the app: a key added to the modal was not on the sheet, and which keys made the cut was decided by whoever ran out of page first. The short printFallback wording stays, because a printed row that reads in one line is still the point; the registry check is coverage now rather than a ceiling, and it compares row by row within a section — the same chord means different things in different views, and a key-only index answered for the wrong row.
  • Every Help article now opens with a drawing of its own subject rather than only the panels whose topic was a shape: the three search prefixes in the field they are typed into, the health tiles in the colours the rows wear, a certificate meter with the day the warning starts marked on it, a maintenance window shaded into a day, the boundary the statistics never cross. Thirteen new shapes joined the SettingArt vocabulary — steps, keycaps, state legends, a typed query, bars, a trend line, a meter, a day strip, switches, swatches, colour plates, a bookmark row and config's own rail — so config, help and the tours keep drawing the same things the same way. Labels inside a drawing go through t(); 200 OK, Shift + H and data/*.json do not. Seventy-one caption strings, in four languages.
  • tests/config-setting-art.spec.js checks coverage rather than a hand-counted number: every prose panel on every Help tab must open with a drawing. Its paste-route case was stale — the setting moved to Behavior → Quick add & inbox — and had been failing since.
  • Config → Overview → New features carries the six headline items of this release, newest first — Capture, the cross-page duplicate question, the Rot report, the review session, Fresh, and the Button bar tab.
  • MANUAL.md and README.md cover every feature above; the Help version panel names 1.3.0.
  • 240 dead help strings are gone — 60 per language, 408 KB, describing the config that was replaced: tab groups called System/Dashboard/Extras, archiving pages, an Essentials/Advanced toggle. Nothing in the app had read them since that config went; the only things that named them were scripts/apply-help-prose.js and scripts/help-prose-content.js, generators for a help system that no longer exists, which go with them. en.json is 591 KB → 494 KB, and the 23 of those strings that were never translated into German or French stop being a translation debt.
  • npm run validate:help-i18n checks Help the way the cheat sheet has been checked for a while: a key English has and another language does not is fatal, since the fallback is the English wording and renders as a translation nobody got round to. Strings identical to English and strings the other locales still carry are reported, not failed — failing on those would make an accurate translation impossible to ship. It found helpIntro untranslated in German and French on its first run; 272 help strings, complete in four languages, plus inlineEditHintMac, which existed only in English.
  • The server re-narrows a locale file when it changes on disk. The core/help split is cached per language and scope, and it was cached for the life of the process — so an edit to a translation was invisible until someone restarted the server, which is the one feedback loop that matters while writing them. The cached entry now carries the file's modification time and size. Served off disk it also revalidates (no-cache plus an ETag and a 304) rather than being held for a day; a binary serving its embedded copy keeps the day, because there nothing can change under a running process.
  • tests/config-help-navigation.spec.js pins the tips filter, the cross-tab jump and the version heading; locale_scope_cache_test.go pins the cache following the file and the revalidation.
  • tests/config-help-prose-loads.spec.js covers the empty-help failure from both ends: prose that goes missing while the view is open comes back on the next repaint, and a core locale load no longer drops the help scope out from under it.
  • tests/help-current-features.spec.js pins the help prose, the tips and the two cheat-sheet keys against the rendered UI, so the next release cannot quietly leave them behind.
  • tests/config-help-health.spec.js was stale from the Health/Monitoring tab split — nine of its fifteen cases failed before this release. Expectations follow the tabs now, and its translation case waits for Help's own scoped strings rather than racing them.

v1.2.1 — 17 August 2026

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_opened and not_opened_30_days cost 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 turns unused into missing-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 R or 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 carrying hideFromModal: true.
  • tests/health-list-stability.spec.js pins 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.md and README.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.
  • fixg, j and k no longer eat the first letter of a word. Letters reach the grid only while a row is selected (_letterMayActOnGrid); the arrows always pass, j/k stop propagation once they act, and the g/G chord paints data-grid-keys on the body instead of swallowing the keystroke. c is 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, and closeSearch() now clears #search-query instead 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).
  • newShow shortcut hints on toolbar icons is off for everyone, via a one-time migration (migrateShortcutTooltipsDefaultOff, marker ShortcutTooltipsOffMigrated). 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, and usedShortcutsNote() 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 while isNarrowed(), 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.
  • newImport beside the CSV and JSON exports (openImportPicker, parseImportPayload, importFromFile), skipping links already present and reporting added / already there / rejected.
  • newmarkUnread(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 stored Up:false, Code:0 and showed nothing.
  • fix — a failed check is re-probed after monitorConfirmDelay and only recorded if it fails again, so one dropped check no longer dents a month of uptime.
  • fixbuildMonitorStats reports CoveredMs, and renderUptimeTiles prints how much history is behind a window rather than labelling a week "30 days".
  • new — certificate expiry comes from every check: liveHosts now includes CheckStatus bookmarks, not only monitored ones.
  • new — a recovery names its duration, TREND_SERIES gives the trend chart six series with a per-mode axis, /api/health/expectations-bulk mutes or unmutes a whole selection, and Settings.HealthCheckTimeoutSeconds replaces the fixed three-second dial timeout.

Search

  • new — negation. parseSearchFilters produces filters.not, and matchesAdvancedFilters runs the positive keys then the negative ones, so tag:dev -status:pinned is expressible in the bar the way it always was in a collection rule.
  • newstatus:untagged, status:tagged, status:noted and status:unnoted; :trash as a command onto #config/data-backups/trash; and Ctrl/Cmd+Enter on 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. showGroupedNotification translated undoCallback into nothing — the bulk-delete undo it also gates has been dead since it shipped.
  • new — category icons (openIconEditor, live preview into .category-title-icon) and Alt+←/→ to move a category, announced to screen readers.
  • newsetting-art.js draws 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: fetchDataRevision tracks the settings revision (GetSettingsRevision) and routes a change through refreshAfterConfigSettingsUpdate.
  • fix — a failed automatic backup is visible: autoBackupRunState records LastRunAt/LastRunError and 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-inbox command on Ctrl/Cmd+Shift+U, and tags carried through postInboxLink.

Docs

  • static/data/whats-new/v1.2.0.json and the index entry ahead of v1.1.2. DASHBOARD_RELEASE (2026.08-dashboard-release-v1.2.0) and NEXTDASH_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-driven hideFromModal cases 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.md and README.md carry 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 hideFromModal flag 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 read index[0] and have shown v1.1.1 since it shipped.
  • newDASHBOARD_RELEASE (2026.08-dashboard-release-v1.1.2) and NEXTDASH_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+E inline edit and Shift+V preview join move, tag, delete, checking, pin, share and reveal. ; and [ still work, undocumented, the way 0 still 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, so Shift+S always 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 keeps Shift+S reaching setupPageShortcuts over a selected row, and tests/shift-key-row-actions.spec.js (renamed from shift-s-two-meanings.spec.js) pins it.
  • new — bare letters act on keydown. C_HOLD_MS and the g-chord hold are gone, and with them the whole keyup handler in keyboard-navigation.js and SearchComponent.addShortcutLetter, which had no other caller. _activateGChordMode now runs on the first g; G_CHORD_MS (3s) is the only timer left.
  • newj/k alias ArrowDown/ArrowUp in the grid, matching config's section rail and bookmark list.
  • newShift+Home focuses the category header via focusCategoryHeader(), which reuses resolveFocusedCategoryEl. Delete on a focused header calls categoryMenu.runAction('delete', …) instead of opening the menu; Shift+F10 opens it.
  • fixhandleContextMenu took clientX/clientY at face value, which a keyboard-raised contextmenu reports as 0/0. menuPointFor() falls back to the row's rectangle; the category menu's own handler does the same.
  • fixresolveFocusedCategoryEl took an options argument: :width keeps the first-category fallback because the palette names what it acts on, Shift+W passes fallbackToFirst: false and falls through.
  • fixsearch.js kept a code-name list (KeyM/KeyD/KeyT/KeyB) that had fallen behind the keys added since; it tests /^Key[A-Z]$/ now.
  • fixsetupPageShortcuts was the only typing guard in the app without an isContentEditable check.

Discoverability

  • newDashboardContextMenu renders a kbd.move-popover-item-key per entry with a matching aria-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.
  • newsyncShortcutAriaHints() stamps aria-keyshortcuts on the toolbar, the header buttons and the first nine page tabs. It runs independently of showShortcutTooltips: 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

  • fixbuildPrintSections passed each row's printFallback to the caller's label resolver as a fallback, and generate-cheatsheet.cjs resolves from locales/en.json, where the i18n check guarantees every cheatKey exists. The short wording therefore never reached paper for any of the 23 rows that carry one. printLabel() uses it directly. validate-cheatsheet-registry.cjs gains a check built with the generator's resolver — with the identity resolver the other checks use, a printFallback wins either way and the check would pass against the bug.
  • new — printed rows for k/j, Shift+E and Shift+Home; nextDash-cheatsheet.html, nextDash-cheatsheet.pdf and the static/ copy regenerated.

Config

  • newlogFilterActivity is a fourth value for the log viewer's Show control, matched on Source == "activity" by logEntryMatchesFilter() rather than on severity. Activity lines already flowed into serverLogSink through log.Printf("activity: …") and parseServerLogLine already split the subsystem out; nothing about capture or NEXTDASH_ACTIVITY_LOG changes.
  • newHelp → About carries the wordmark and three addresses: nextdash.cc, GitHub, and jordibrw.nl. The help-signature sign-off is dropped, so each address appears once.

Dashboard

  • fix.category-title centres 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 a line-height tall enough to swallow the button added 3px to every header. .category-title--multiline keeps flex-start.
  • fix* carries the same guards as ! and ., with an exception for its own modal so it still closes the recent list.

Analytics

  • newsettings-snapshot widens from 21 to 45 fields and a new content-snapshot reports install size (bookmarks, pages, categories, tags, finders, collections, monitored, inbox totals). Every value is bucketed client-side; MAX_EVENT_PROPS = 50 is Umami's per-event limit, which is why it is two events. analytics_content.go counts server-side and returns "" when telemetry is off — analytics_content_test.go proves 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 reports custom.

Docs

  • MANUAL.md and README.md follow 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 rewritten config.helpKeyboardBody, keyboardFixedNoteGridNav without the hold, logLevelActivity, logActivityHint, helpAboutBody, helpSiteProject, helpSiteAuthor.
  • static/nextdash-wordmark.png is keyed out of logo-ascii-on-black-large.png by greenness rather than brightness; the existing logo-ascii-transparent.png clears 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 in tests/create-page-category-from-dashboard.spec.js and tests/dashboard-grid-shortcuts.spec.js, the category-menu chip case in tests/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

  • newCategory.Spread (spread in bookmarks-{page}.json) marks a category as allowed to run across grid columns; uncategorized and the smart collections keep theirs in settings.categorySpreads[pageId][categoryId], the shape categorySortModes already uses. dashboard-category-span.js owns both, so no caller has to know which applies.
  • new — the width itself is derived, never stored: spanForCount is min(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.
  • newapplyCategoryItemLimit multiplies the limit by the span, so a spread category shows its limit once per column and stays the height of its neighbours. refreshAllCategorySpans runs from finishIncrementalRefresh, which is what makes an added bookmark bring the next column with it rather than waiting for a reload; settleSpanChange follows 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: spreadUnavailableReason returns unlimited-items when categoryItemLimit is 0, and the config select disables Unlimited while anySpreadCategory holds. Both say why where they are asked for.
  • new — routes: DashboardCategoryMenu (entry flips to Back to one column, no aria-checked beside a label that already flips), Shift+W in keyboard-navigation.js, :width on|off|all, a data-cat-spread button per row in the categories editor, and Appearance → Layout → Categories across columns for the defaults and the reset.
  • new.category--wide spans with grid-column: span var(--category-span) and repeats the row's track pattern once per column, so subgrid alignment survives. The inner column gap is the grid gap plus 2 × --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. syncWideColumnTracks pins 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 ↔N badge 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-span rows per category from dashboard-packed-masonry.js, grid-auto-flow: row dense, a ResizeObserver per category. Bands were tried first and left a hole the height of the tallest column beside the wide block.
  • fixsyncCategoriesFromDom read 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. readCategoryElementsInOrder is the single inverse now, keyed on what is in the DOM, and getExistingCategories delegates to it.
  • fixsyncDashboardGridLayout rebuilds className wholesale and dropped packed-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. restoreConfigHash now saves on every move inside config and setActiveView stamps it on the way out. The TTL is 5 minutes (DashboardConfig and DashboardConfigLoader both), counted from leaving rather than from the last click inside.
  • newtype: 'action' panel controls with bindPanelActions, used for Turn spreading off everywhere; NEW_THIS_RELEASE is the one place naming where the twinkle points, drawn on the section, the sub-tab and the panel.
  • new#category-context-menu sizes to its content (width: max-content, capped at 24rem). The shared .move-popover cap is for the move/tag/delete pickers; at 16rem the widest row here needed 274px and got 237, so the Shift+W chip 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_ATTR and SUB_TAB_SECTION, so the deep link, the remembered location and the arrow-key walk come for free; handleOverviewGo accepts bmTab.

Views

  • new — Health, Inbox and Config → Bookmarks draw one card from feed-row.css instead of three copies. The rule was written out in health-view.css, dashboard-inbox.css and config-view.css and 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-select for 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

  • newspread-notice.js (a NoticeCard) and spread-tutorial.js, a four-step AppModal walkthrough in the shape of the inbox and health tours, reachable afterwards from Help → Pages & categories.
  • fixNoticeCard bound an action with querySelector, and a card naming its × through dismissName puts 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, and go generate ./... for asset_hashes_gen.go.