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.