Every release of nextDash up to now has carried a date instead of a version number: v2026.09.09, v2026.09.09.1, v2026.09.09.2, and so on, a new tag almost every day through a long stretch of active development. v1.0.0, released today, breaks that pattern on purpose. It’s the first release under semantic versioning, and it’s a deliberate milestone rather than an incremental one: after a long run of bug fixing and consolidation, the app finally feels finished and stable rather than in flux. From here, 1.x.0 releases carry new features and 1.0.x releases carry fixes — a distinction the date-based scheme never made. The comparison logic for the new numbering actually shipped a few point releases earlier, under v2026.09.09.3, so v1.0.0 is really the moment the switch was made official.
What’s inside is almost entirely correctness work, and it follows one recurring shape: an operation that reported success for something it hadn’t actually done. A move that could silently lose the bookmark. A category save that no-opped without telling you. An add that discarded the very item it had just accepted. Several of these were caught by noticing that the same problem had already been solved correctly somewhere else in the same file — which says as much about the size the codebase has grown to as it does about the bugs themselves.
A pattern of silent failures
The clearest example is how moving a bookmark between pages used to work. _moveBookmarkToPage would read both the source and target page whole, splice the bookmark out of one array and push it into the other in memory, then write both pages back. If another request touched either page while that was happening, the write would get clobbered — and if the save to the target page failed after the source had already been removed, the bookmark vanished from both. It now goes through the single-item POST /api/bookmarks/add and DELETE /api/bookmarks endpoints instead, each atomic under the store’s own lock. The bulk version of the same move, for moving everything under a tag filter at once, had the identical shape and got the identical fix, converted to per-item add-plus-delete with a partial-failure toast so a batch that partly fails tells you which part.
A few of the smaller fixes in this vein are almost funny in hindsight. Saving an empty list of categories for a page, while bookmarks still referenced one of the categories being removed, used to just return success and change nothing — it now returns a proper error that the client understands. Renaming a category without an originalId used to fall back to matching by array position, so deleting a category in the middle of the list could quietly reassign a later category’s bookmarks to the wrong one; that fallback is gone. And the inbox had a capacity limit that trimmed the list after appending a new item, which meant the item most likely to get silently evicted was whichever one had the oldest timestamp at that exact moment — including, sometimes, the one you’d just added.
Tightening the API
One fix in this release is worth calling out specifically, because it’s a real security gap rather than a cosmetic bug: PUT /api/inbox skipped every validation step that the equivalent POST route performs. No URL validation, no icon sanitising. It was the one route that would happily store a javascript: URL, or a private network address even with allowLocalBookmarks turned off, and the only path in the whole API where a client could set an arbitrary Icon value directly. That’s closed now. In the same pass, an empty inbox URL — which is intentionally allowed, since a bookmark doesn’t have to have one — used to trigger a 500 server error instead of being handled as the valid case it is, and a couple of endpoints would silently create a data file for a page that didn’t exist rather than rejecting the request.
There’s a genuinely useful new endpoint alongside the fixes, too: POST /api/categories?dryRun=1 reports exactly what a category save would do — which bookmarks would move, which would end up orphaned, which submitted categories are missing an originalId — without writing anything. It shares its remapping logic with the real save path, so the preview can’t drift out of sync with what actually happens when you commit it.
The inbox grows up
Underneath the bug fixes, the Inbox quietly became a much more complete view in this release. Tags — which have existed on inbox items and been normalised in the data for a while, just with no interface for them — are now rendered as filter chips, editable straight from a row’s menu, matched by search, and included in both export formats. A small stats panel now reads live from the same inbox-stats endpoint the config screen already used. Bulk actions arrived properly: promote, open, and copy-link all work across a multi-selection, with shift-click and shift-arrow range selection and the usual select-all shortcut. The right-click menu on an inbox row now shows the inbox’s own actions instead of borrowing the regular bookmark menu, and pressing R refreshes the feed — a keyboard shortcut that existed in the code already but had no way to actually trigger it until now.
A handful of smaller inbox fixes round it out: a failed load now shows a proper retry panel instead of quietly rendering as if the inbox were empty, deleting an item no longer drops your keyboard cursor position, and Escape and other view-level shortcuts no longer get blocked by an empty-list guard at exactly the moment a filter has hidden everything. Items evicted when the inbox hits capacity are now reported to the client and have their icon files cleaned up properly, matching how an explicit delete already behaved.
Health and the rest of the dashboard
Health gains a new issue type: orphaned category, for a bookmark whose category ID doesn’t match anything on its page. It’s detect-only for now, following the same pattern the existing shortcut-conflict check uses. Category saves and browser imports now correctly invalidate the health report’s cache, which previously wasn’t necessary but now is, given how the data flow changed. The header badge that shows your health status also stopped polling on a fixed 60-second timer in favour of following the server’s own cache lifetime with backoff, and no longer double-fetches when you switch back to the tab.
The rest of the dashboard picked up a long tail of smaller correctness and polish fixes: a cross-page smart-collection check that had been accidentally collapsed into a different, narrower check in an earlier commit and quietly stopped working for the default case; a staleness check that forgot to account for a bookmark’s pinned state, check status, icon, or note when deciding whether to re-render it; multi-select buttons that were missing the ARIA attributes screen readers need; a copy-links action with no fallback for plain HTTP installs where the modern clipboard API isn’t available; and an inline-edit save hint that always told Mac users to press Ctrl. None of these are dramatic on their own, but together they’re the kind of cleanup that only happens when a project stops to catch its breath.
That’s really the story of v1.0.0: not a new feature to headline, but a line drawn under a long stretch of rapid, date-stamped releases, and a statement that what’s on the other side of that line is meant to be relied on. If you’re running an older build, this is a good one to update to before anything else.
Full technical release notes, commit by commit, are on GitHub.