Skip to content

v1.0.4

Choose a tag to compare

@jordibrouwer jordibrouwer released this 14 Aug 22:06
· 66 commits to main since this release

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

  • fixrepaintBookmarkMutationSurfaces passed incremental: false, the one flag canAttemptDataPatch refuses outright, so every add, edit, delete, move and tag change tore down the whole grid — every DragReorder instance, the scroll offset and the focused row — for what is usually one changed row. The incremental path's own guards decide now: canAttemptDataPatch still bails on a layout-settings change and categoryStructureMatches on a category added, removed or reordered.
  • new — the scroll offset is stored per page in _pageScrollPositions and consumed on return. Consumed, not kept: takeRememberedScroll deletes the entry, so a later deliberate visit still starts at the top.
  • fix — the incremental renderer built a category's empty state without the + bookmark button createCategoryElement includes.
  • fix — rows are built at tabIndex = -1 for the roving tab stop and KeyboardNavigation hands one of them a 0 only after walking the grid, which a render does not do — so Tab skipped the grid entirely from first paint. syncBookmarkGridA11y guarantees exactly one stop now, on the row currentIndex points at rather than on row 0, which would walk the tab position away from the user on every repaint.

Selecting several bookmarks

  • newdeleteSelected offers an undo toast for 8s, restoring in ascending index order (each splice shifts what follows) and dropping the redundant trash entries afterwards via dropRestoredTrashEntries.
  • fix — a failed save left the rows spliced out of the model and the selection cleared, then returned silently. Both are restored and showErrorNotification is called.
  • fix — the toolbar was prepended into #dashboard-layout, which carries role="grid"; a role="toolbar" among rows and rowgroups is invalid. It also rendered as a 90×278 vertical strip, because the grid is a flex row and width: 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

  • newSearchComponent.FILTER_KEYS replaces three hardcoded four-item lists. opened: and added: parsed and filtered correctly but were absent from getFilterHintItems, 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, with never only under opened:.

Tag filters

  • fixupdateTagFilterIndicator had been reduced to a teardown that emptied #tag-filter-indicator on 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. renderTagFilterBanner gained withToolbar for that.

Menus and popovers

  • fixresolveRowBookmark fell 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.js keeps its own, and that is the one that runs on the first right-click of a session.
  • fix_persistBookmarkField swallowed every error and returned nothing while all three pin routes flipped pinned themselves. 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 Delete key went through AppModal while Shift+D and 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 _bindActionPopoverOutsideClose for Move, Delete, Tag and the multi-select tags popover: it also closes on an outside contextmenu, and matches the anchor by containment rather than identity.
  • fix_focusActionPopoverItem picks its pattern from the container's role: aria-activedescendant with focus on the box for role="listbox", roving tabindex for menus. The tag popover's second capture-phase keydown listener is removed — document capture runs first and every branch calls stopImmediatePropagation, so it never ran.
  • new — the check-mode submenu entry sets aria-expanded, opens with ArrowRight and closes with ArrowLeft, matching the Escape path that already existed.
  • new — the category menu shows F2 and Delete as aria-hidden chips. "Add category" gets none: it is a held c, and a chip would promise a tap that goes to the shortcut search.
  • new — the category sort button opens on ArrowDown, which aria-haspopup="menu" already promised.
  • fix_restoreActionPopoverFocus fell 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

  • fixdashboard.css overrode four selectors with animation: none !important, contradicting reduced-motion.css, which uses 0.01ms precisely so animationend still fires. bookmark-copy-flash and bookmark-pulse stayed 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-colcount are constant and are stamped in populateBookmarkRowView; syncBookmarkGridA11y no longer does a querySelector per 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. contextmenu stays per row: bindRow serves inbox cards too.
  • restartRowAnimation replaces five hand-written copies of remove/reflow/add plus animationend.
  • The action popovers reposition once per frame instead of once per scroll event, and the toolbar tooltip's pointermove sweep is frame-throttled with its header buttons cached.

Docs

  • MANUAL.md — the Delete row in the shortcut table, and the Shift+S entries in the cheat sheet registry and all four locales.
  • nextDash-cheatsheet.html and 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, syncAllBookmarksMetadata and the public delegate for updateBookmarkRowsCategoryInDom removed.
  • 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 via Shift+H or Shift+I and cleared on every other exit, and resolveConfigOpenTarget additionally required activeView to be health or inbox — so the common route, Escape out and Shift+S back in, always landed on Overview. clearLastConfigLocation has no callers left and is removed. The expiry is duplicated in dashboard-config-loader.js, which reads the same entry on a cold load before the module exists; an entry with no savedAt predates 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.
  • newclampBookmarkSettings enforces 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 with http, so it can never become a javascript: URL handed to window.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

  • newopened: and added: in the search parser, matched by matchesAgeFilter against today/week/month/year, plus never for opened:. An unrecognised word filters nothing rather than everything, which is the safer way round for a typo.
  • new — custom collection rules gain pinned, untagged, notOpenedDays and changedDays. valuelessRuleFields keeps pinned and untagged without a value, so the server no longer drops them as half-filled. changedDays is the first reader updatedAt has 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 on lastOpened or openCount, so "what did I just add" was unanswerable.
  • newpreviewDesc joins 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-link rather 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.
  • newcreateSortControls rebuilt 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, added and opens.
  • fix — the grid's recent mode sorted by lastOpened while Config used the same words for createdAt. Renamed to opened; normalizeSortMode still accepts the old value so stored categories keep working.

Keyboard and pointer

  • newShift+P pin, Shift+S share, Shift+R reveal in Health, t filter 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+P and :pin but no pointer route at all from the grid, while every other one-bit row action had one.
  • fixCtrl/Cmd+Enter opens in a new tab for that press alone. selectCurrentElement fired a bare .click(), which constructs no MouseEvent and carries no modifier, so the keyboard had only the standing openInNewTab preference.

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 asks isLayeredModalOpen() — 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 of j.
  • 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 mirror health-tutorial.js.
  • fixresetOnboarding now calls clearSeenTips(). It only cleared onboardingCompleted while its own dialog promised to replay "the welcome tour and tips" — those ids live in discoverabilityState.seenTips, which it never touched, so no tour or tip had ever come back. tipsNotBefore is cleared with them; seenSettingPromos is 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.md and MANUAL.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.js asserted the restored section without resetting config.section first, which closeConfigView leaves 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 the resetOnboarding defect.
  • 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, de and fr; 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/reset re-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-reveal shows 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

  • fixensureCategoryOnPage (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.
  • fixloadFinders and loadCategoriesEditor set a _xLoadFailed flag instead of []; saveFinders and saveCategories refuse to write while it is set.
  • fixsaveCategories returns whether it saved and takes the page id as an argument, captured by each caller at edit time. It swallowed the error and returned undefined either way, so the delete flow acted on a 409 as if it had worked — trash entry, undo toast and all. The page picker reassigns _catPageId synchronously, so a save in flight could also land on the wrong page.
  • fixSaveSettings (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.
  • fixpublishConfigSync added to dashboard-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

  • fixrefreshAllFavicons posted 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 through ConfigFaviconPrefetch, as search-commands.js and dashboard-quickstart.js already did.
  • fix — the prefetch overlay drops pointer-events on completion. It covers the viewport at z-index: 12000 and 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; elementFromPoint over the button returned the overlay.
  • fixguardUniqueName rejects 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 via clampEntityName (rune-wise, through truncateRunes), because the API is reachable without the browser.
  • newcheckedAt, nextBackupAt and totalKept are rendered. All three were shipped by the server and read by nothing; totalKept was 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

  • fixlabelSettingsControls() 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 get aria-valuetext, since 0.85 is not what the UI shows.
  • fixcaptureControlPanelFocus() restores focus and caret across repaintActiveControlPanels. Controls bind on change, 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. repaintTagsBody already did this, with a comment explaining the hazard.
  • fixbindSubTabStrip re-focuses on the next frame rather than testing target.isConnected immediately. Appearance activates through render(), 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 declared aria-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 than split(','): 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.
  • newSavedSearch added to Settings, normalized by normalizeSavedSearches (trimmed, incomplete entries dropped, capped at ten). Saved searches lived only in localStorage, so they were in no ZIP backup; anything left there is migrated on first read.

Removed

  • Page archiving. Settings.ArchivedPageIds had 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; showRecentButton asserted on the Display tab after it moved to Toolbar (eight failures from one line); appearanceTab set before openConfigView, which resets it; .first() matching a bookmark's smart-collection copy, whose row carries no page-local index; role="feed" asserted on #dashboard-layout for health, which renders it on .health-view-feed; cheat-sheet rows asserted visible inside collapsed <details>; last-opened-format.js still counted as lazy after 8115b0e7 made it eager; a hardcoded help-tab list now read from HELP_TABS; analytics regexes demanding the retired vYYYY. scheme; and config-info-reset demanding an for five fields whose own comment in FIELD_META explains 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 a flex: 1 1 6rem track competing with the button row for space, into a new .health-view-note-row shared with the filter note. Reserving a column beside the buttons via padding-right was 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 because preserveAspectRatio="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 same preserveAspectRatio reason: coordinates inside the viewBox drift away from where the pointer actually is. trendPointLabel() reads HealthTrendPoint.t (Unix ms), not a d/day field — an earlier draft assumed the latter and produced empty labels. A null reading renders "no reading" rather than 0%.
  • newshowTrendExplainer() behind an beside the chart, kept separate from showHealthExplainer() so the fixed-axis and gap rationale is not buried in the view-wide text.

Update check

  • fixfetchGitHubLatestRelease read GitHub's /releases/latest, which resolves "latest" by published_at rather than by version. A patch published on the calendar line after v1.0.0 would be named there and then correctly rejected by compareReleaseTags, so a genuinely newer release was never announced in the modal or in Config → Overview. Now reads the release listing and orders it with compareReleaseTags, falling back to /releases/latest when the listing cannot be read.
  • fix — the listing URL is derived from githubLatestReleaseURL via releaseListURL() rather than declared as a second package var. As two independent vars, TestFetchGitHubLatestRelease stubbed 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) and R (refresh). Added to INBOX_VIEW, which feeds both the inline legend and the cheat sheet. Printable sheet: 55 → 58 rows; nextDash-cheatsheet.html and both PDFs regenerated.
  • TestWhatsNewStubReleaseConstants asserted a literal 2026.07 prefix on DASHBOARD_RELEASE; the regex now accepts both the calendar and semver suffix shapes.
  • TestBuildUpdateStatusDetectsNewerRelease used v9999.99.99.9 as its newer upstream. That first segment is above calendarVersionFloor (1000), so it is read as a calendar tag and correctly loses to semver — the fixture was wrong, not the comparison. Now v99.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 ./... regenerated asset_hashes_gen.go for the changed JS and CSS.

v1.0.0 — 13 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-item POST /api/bookmarks/add + DELETE /api/bookmarks endpoints, each atomic under the store lock.
  • fixbulkMoveTagFilterToPage (dashboard-tag-filter.js) had the same shape for a whole batch. Converted to per-item add+delete via Promise.allSettled, with a partial-failure toast.
  • fixdeleteRemoteBookmarkInline read the whole source page, spliced, and POSTed it back, racing any concurrent write. Now uses the single-item DELETE endpoint. saveRemoteBookmarkEdit deliberately 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.
  • fixSaveCategoriesByPage returned nil for an empty-list save while bookmarks still referenced a category, changing nothing. Now returns ErrCategoriesStillReferenced, mapped to 409 by respondCategoriesSaveError.
  • fix — category rename without originalId fell back to matching by array position, so dropping a middle category reassigned later categories' bookmarks. Positional fallback removed.
  • fixAddInboxLink trimmed at capacity after appending, discarding an item with an older AddedAt while returning success. Now uses trimInboxItemsKeeping plus a survival check, mirroring RestoreInboxLink.
  • fixDeleteBookmarkFromPage skipped read-cache invalidation.
  • fix — concurrent page/category creation could clobber each other.
  • fixfetchBookmarkPreview cached error pages as valid previews.

API and validation

  • fixPUT /api/inbox skipped every validation POST performs: no validateBookmarkURL, no sanitizeBookmarkIcon. It was the one route that would store a javascript: URL or a private address under allowLocalBookmarks:false, and the only path where a client could write Icon.
  • fix — an empty inbox URL returned 500 for a client error. validateBookmarkURL intentionally permits empty strings (bookmarks may have none), so the check belongs in the handler.
  • fixGetCategories/SaveCategories accepted a nonexistent page, materialising bookmarks-N.json as a side effect. Both now 404 via pageExists.
  • fixGetBookmarks silently returned 200 [] for a missing page/all param.
  • fix — bookmark Name/Category/Note were never trimmed server-side, unlike Tags/Icon.
  • newPOST /api/categories?dryRun=1 reports what a category save would do — which bookmarks move, which are orphaned, which submitted categories carry no originalId — and writes nothing. Shares buildCategoryRemap with the real save so the preview cannot drift from it.
  • new — inbox text fields are bounded (clampInboxLinkFields), applied on add, patch and restore. inbox.json is rewritten whole on every mutation, so an unbounded field is paid for by every later request.
  • fixPATCH /api/inbox stored readAt verbatim, including negative and far-future values, while snoozedUntil directly above it was carefully clamped.
  • newPATCH /api/inbox accepts tags (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.Tags had 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+A select-all.
  • new — the right-click menu carries the inbox's own actions rather than the bookmark menu's.
  • newR re-fetches the feed, wiring up loadAndRender({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.
  • fixEscape and 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 clearing searchQuery/domainFilter on every later render.
  • fix — triage's delete ignored deleteItemWithUndo's return value; bulkSnooze discarded its results entirely; bulkDelete reported 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-category issue type: bookmarks whose Category id matches no category on their page. Detect-only, following the shortcut-conflict precedent. 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.js already refreshed there).
  • new — header restructured to two rows matching .inbox-header; the trend chart moved into the toolbar.

Dashboard

  • fix_smartCollectionFilterNeedsCrossPageData had been collapsed into !_isSmartCollectionPageAllowed in 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, icon and note.
  • fixsaveSettings swallowed its error without logging.
  • fix — the translation-fallback pattern never fell back on a missing key.
  • fix — multi-select Clear referenced a nonexistent keyboardNav property.
  • fix — smart collection headers could be renamed via long-press/double-click.
  • fixcopySelectedLinks had no execCommand fallback 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 lacked aria-busy.
  • fix — the double /api/data-revision round-trip on tab refocus is debounced.
  • new — bulk tag-filter delete offers an undo toast.
  • new_applyLoadedPageData's full-container view identity moved into one FULL_CONTAINER_VIEWS table instead of three hand-copied checks.
  • newNoticeCard (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.
  • perfnoteDataMutation(pageID) scopes read-cache invalidation to the page a write touched.

Docs

  • static/data/whats-new/v1.0.0.json added; index.json gains the entry first.
  • static/js/whats-new-stub.js — both tokens bumped to 2026.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.md updated; Config → Help gains a v1.0.0 note.
  • go generate ./... regenerated asset_hashes_gen.go.