Fewer detours — make a page or category without leaving the bookmark form, act on a whole list of broken links at once, tag a dozen bookmarks in one click, and find every delete recoverable from the trash whichever screen it came from.
Bookmarks
- new — Tag the whole selection from the grid toolbar. Tagging was single-bookmark: the row's
Shift+Tpopover acts on one, so eight rows meant eight rounds. A Tags button now sits beside Move and lists every tag already in use, each in one of three states across the selection — a tick when all have it (clicking removes), a dash and on 2 of 3 when some do (clicking fills in the rest), plain when none do. The count is spelled out because add and remove mean different things for a mixed set and the click has to say which it will do. One page write for the whole selection rather than one per bookmark, with the previous tags restored if the save fails. Category needed nothing — it already lives in the Move popover beside the page list. The items aredivs using.move-popover-itemlike every other popover in the app; a<button>brought the browser's own background, border and font and stood out against the theme. The toolbar sits top-left, so the shared placement helper's centre-on-anchor pushed a tall list up over the bar — its top is aligned to the button instead (dashboard-multi-select.js). - new — Create a page or category from the form's dropdowns. Both selects lead with a create entry; picking it hides the select and swaps in a name input with Create and Cancel in the same field, so the form's height and the field's position never move. On confirm the entry is POSTed to
/api/pagesor/api/categories?page=…, the dropdown is rebuilt with the new entry selected, and the page nav is re-rendered so a new page appears as a tab without a reload. Categories are scoped to whatever the page dropdown currently shows — including a page created moments earlier in the same form, which is why the category create readspageSelect.valuerather than the page the form opened on. A duplicate name is refused inline, under the input, with the row left open: a toast would outlive the row it belongs to and leave the form looking like it had accepted the name. The id is slugged from the name and de-duplicated the way the config editor does it, so two categories named alike cannot collide (dashboard-inline-edit.js,dashboard.css,bookmark-form-modal.css). - fix — Escape closes an open create row, not the whole form. The form's Escape handler is registered on
documentin the capture phase, so it ran before the create row's own input listener could ever see the key — backing out of naming a category discarded the entire half-filled bookmark. The handler now looks for an open create row first and hands the key to it, because the row is the innermost thing on screen (dashboard-inline-edit.js). - fix — A save while the create row is open targets a real page. The select kept the
__new__sentinel as its value for as long as the row was open, so saving at that moment would have written__new__as the bookmark's page or category. The previous value is restored the instant the row opens; the row on screen, not the sentinel, is the pending state (dashboard-inline-edit.js).
Health
- new — Multi-select and a bulk bar. Health lists exactly what a clear-out starts from and then made you repair it one row at a time. Rows gain a select box;
xticks the one under the cursor and advances,Xtakes everything the filter shows,Ctrl/Cmd+Athe same, andCtrl/Shift+click work with the mouse. The bar is deliberately the same.config-bulk-barthe bookmarks editor uses — same placement above the list, same grouping, same select+Apply pairs — because Health is the second place you tick rows and act on them, and it should not be a second thing to learn. A first attempt invented a floating toolbar with a popover and was replaced. Actions: Set checking, Re-check, Open, Copy links, Delete, Clear selection (dashboard-health-multi-select.js,dashboard-health.js,health-view.css). - new —
POST /api/health/delete-bookmarks. Bulk delete could not loop the single-row endpoint: that takes{pageId, index}and removes whatever sits at that position, so every delete shifts the rows after it and a batch would remove the wrong bookmarks. The health report is cached and can be minutes old, compounding it. Each item now carries the URL the caller believes sits at that index — the same guardsetCheckModealready had — and a row whose stored URL disagrees is skipped and reported rather than deleted. Within a page the deletes run highest-index-first in one write, so removing one never invalidates the positions still to come, and every removed row is recorded to the trash (health_delete_bulk.go). - fix — Ticks survive a filter change, and the bar says what they reach. Pruning the selection to the active filter would silently discard it the moment someone switched from All to Broken; the selection is now measured against the whole report. Because Delete then reaches rows the filter hides, the bar names the hidden count with a Select only these escape — the same warning and wording Config → Bookmarks carries. The bar no longer depends on the feed existing either: a filter matching nothing renders an empty state, which is exactly when rows are still ticked and the warning matters most (
dashboard-health-multi-select.js). - fix — Escape reaches an open selection before the text-field guard. Ticking a row leaves focus on its checkbox — an
INPUT— and the view's Escape handler bailed out on inputs, so Escape did nothing after a click. The selection check now runs first, with a real text field still winning so Escape in the search box behaves as before (dashboard-health.js).
Trash
- fix — Config → Bookmarks writes deletes to the trash. Both its delete paths bypassed
DashboardTrashentirely, so a bookmark deleted there was gone once the 8-second undo toast expired, while the same delete from the dashboard stayed recoverable for 30 days — and Config is exactly where a clear-out happens, making it the delete most worth undoing and the least able to be. Rows are captured inside thewritePageBookmarksmutation, where the stored list still holds them at their real indices (that index is what restore puts them back at), and recorded only after the page write so a delete that did not persist leaves no phantom entry. The bulk path pushes one entry per row across all affected pages rather than collapsing them onto one (dashboard-config.js). - fix — Restoring onto a deleted page explains itself. The server already handled this correctly — 409, item returned to the trash, nothing lost — but the panel collapsed every failure into Could not complete that action, discarding the one message a user can act on. The missing-page case now names the reason and the remedy; everything else keeps the generic wording, which is right when there is no actionable cause (
dashboard-config.js).
Config
- new — Creating a page or category from the form, the Health bulk bar, and bulk tagging are announced in the Config → Overview new-features carousel, newest first, with five locale keys each in en, nl, de and fr. The form entry's CTA uses the existing
{openBookmarkForm: true}go-shape; the others use{view: 'health'}and{closeConfig: true}.
Tests
- new — 11 e2e tests covering the create entries, the inline row, page scoping, duplicate refusal, the Escape hand-off, the sentinel guard, and a bookmark saved into a category made from the form. Three were falsified against deliberately broken code to confirm each fails for its own reason (
bookmark-form-create-page-category.spec.js). - new — 10 Go tests on the bulk delete endpoint — index-shift correctness, the URL guard, out-of-range reporting, spanning pages, trash recording — with the descending sort and the URL check each falsified. 20 e2e tests on the Health bar, 9 on bulk tagging, 6 on the Config trash write and 4 on the missing-page restore, each new assertion broken first (
health_delete_bulk_test.go,health-multi-select.spec.js,dashboard-multi-select-tags.spec.js,config-bookmarks-trash.spec.js,config-trash-restore-missing-page.spec.js). - fix — The create buttons carry their own
.bookmark-inline-create-btnclass rather than reusing.bookmark-inline-action-btn, which marks the form's footer buttons. Reusing it put a second Cancel above the real one in the DOM and broke four existing specs that locate the form's cancel with.first(); the ambiguity was the defect, so the markup changed rather than the tests. - fix — The Health spec persists its filter to
localStorageand left it behind, sohealth-check-mode.spec.jsopened on the wrong filter and failed when the two ran in order. Cleared in anafterEach.
Docs
- fix — What's new modal, Config → Overview → Latest update, CHANGELOG, README and MANUAL for v2026.09.05.1;
DASHBOARD_RELEASE→2026.07-dashboard-release-v172,NEXTDASH_WHATS_NEW_DATA_VERSION→whats-new-v231. Docs stay out of the What's new modal, which is for user-facing change (whats-new-stub.js). - fix — The cheat sheet gains a Selecting several health rows section beside the grid's, and the shared health legend gains
x. Its rows are deliberately not printed: a heading plus two rows pushed the A4 one-pager from two pages to three, which is the spill its generator exists to avoid, and the printed Health view section already carriesxfrom the legend (keyboard-cheat-sheet-registry.js,keyboard-view-legends.js). - fix — Config → Help covers the Health bulk bar under Availability & health. Backups, import & export claimed "every delete goes through it" before that was true of Config → Bookmarks — it now names the three routes and what happens when the origin page is gone.
- fix — The multi-select key joined page, URL and name with two literal NUL bytes, which made the whole file binary to tooling: git recorded changes as
Bin … bytes, 0 insertions(+), 0 deletions(-), so every diff was unreviewable, and plaingrepskipped the file entirely. Written as a\u0000escape it produces the identical string at runtime and leaves the file as text (dashboard-multi-select.js).