Sign out was a trap on the desktop, not an action. It nulls the synthetic local
user and redirects to /login, but the offline adapter rejects every sign-in with
"there's no account to sign in to" — so the only way back into your own notes was
to restart the app. There is nothing to sign out of; the notes are on this
machine either way.
Linked devices was a quieter version of the same thing: it lists the tokens a
SERVER has issued to native clients, and the desktop is one of those clients, so
offline the list is always empty and issuing a token rejects. Its actual
relationship with a server already has a home at /sync. Also hid the account
name, which named a login the app doesn't have.
/account is now blocked in the router too, not merely hidden — the mirror of the
existing requiresDesktop guard — so a typed URL or a restored history entry
can't reach the dead end either. Deliberately not applied to /login and
/register: bouncing those on desktop would loop against the requiresAuth guard
whenever a session is missing.
The launch flash is the window painting before the webview does, showing the
platform default white through the gap — worst on a dark-mode desktop, and
widened by the software rendering we force on Linux. Set from the live system
theme rather than app.windows[].backgroundColor, because that config carries one
static colour and either choice would fix half of users while introducing the
same flash for the other half.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The surface that turns the engine into a feature (rule 27). Desktop-only —
the web build IS a server's UI, so a "connect a server" screen there would be
nonsense; the route redirects to the board and the nav entry is hidden.
UNLINKED IS THE RESTING STATE, not an incomplete setup. The empty case leads
with "Working offline on this device — everything works without a server",
because a screen that framed the default as a problem would push people into
configuring something they may never need. The app is local-first; this is
opt-in.
Probe before credentials. "Check" shows who actually answered — site name,
version, and the M10.6 verdict — before any password or token is typed. An
incompatible server is shown in red and the sign-in fields never appear, so
you cannot hand a credential to something that can't use it. `degraded` names
the missing capabilities rather than staying quiet and letting a feature
mysteriously do nothing.
Both credential paths, matching the Rust side: email+password (a fresh
install has no session to mint a token from) or a pasted device token (for
anyone who'd rather not type a password into a desktop app). Secrets are
cleared from component state the moment they're exchanged.
Disconnect states plainly that the token stays valid server-side and points
at Account -> Linked devices, rather than implying a remote revoke that
didn't happen (issue 2110). Wording avoids "revoke" for exactly that reason.
Push rejections are surfaced verbatim after a sync, never swallowed — a
duplicate label name is the realistic case and only a person can resolve it.
Adds schema v3: last_sync_at. The cursor can't answer "am I up to date?" —
it's a revision watermark, not a time, and it doesn't move at all when a sync
legitimately finds nothing new, so "synced a moment ago, nothing new" would
be indistinguishable from "never synced". Stamped only after BOTH halves of
the cycle succeed; a stamp after a partial cycle would claim currency the
data doesn't have. Cleared on unlink so a new server can't inherit it.
run_cycle now returns the post-cycle status, so the UI updates from one
round-trip instead of chasing every sync with a status call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
There was essentially no logging — useless for proving the app renders across
different environments. Add real observability:
- tauri-plugin-log -> stdout (so `2>&1 | tee` captures a run) AND a persistent
file in the app log dir (grabbable after the fact on any machine). Level Info.
- Startup diagnostics: app version, OS/arch, the Linux display/session stack
(XDG_SESSION_TYPE, desktop, Wayland/X11, GDK_BACKEND), the WebKit render-
hardening vars actually in effect, resolved log + data dirs, DB open/migrate
result, and note/label counts.
- log_event command + a frontend logEvent() helper: boot line (data source +
WebKit user-agent) from main.ts, first-route config/session/destination from
the router guard, and — via the bridge invoke() wrapper — every failed Tauri
command named with its error, so a broken basic function is self-identifying.
Task 2040.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
A temporal recall path — find a note by WHEN it was captured, not just what it contains (task 1903, first of the M6 recall items).
Backend: list_notes gains an optional created_at range (created_after / created_before, half-open interval) + sort=created; also lays groundwork for the richer-search facets (task 1902). New _parse_iso_dt helper with a DB-free unit test.
Frontend: a Timeline view (sidebar nav + 'g t' + command palette) grouping active notes newest-first into local-time buckets (Today / Yesterday / Earlier this week / this month / Month YYYY), plus an optional From/To date filter. Built as a lens on the same NoteCard masonry, consistent with the existing Reminders/Search views.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
- graph blueprint: GET /api/graph resolves note_links to target notes by
normalized title (owner-scoped, non-trashed, self-excluded) → {nodes, edges}
of connected notes.
- GraphView: hand-rolled force simulation (repulsion + edge springs + centering,
cooling over ~400 frames), SVG nodes/edges, click a node to open it (reuses the
editor with link navigation). Sidebar Graph entry + route; empty state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
- Migration 0005: generated tsvector column (title A + body B) + GIN index on
notes; GET /api/notes/search?q= (websearch_to_tsquery, ts_rank, ACL-scoped,
excludes trash), labels merged into results.
- Persistent AppShell layout (parent route + <RouterView> children) so the new
top search box keeps focus across board/search/label navigation.
- SearchView (debounced live search from the shell → /search?q=, results masonry,
no-match empty state); BoardView/SearchView render inside the shared shell.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
- AppShell: persistent left sidebar (Notes · labels · Archive · Trash) + top bar
(site name, admin Settings, sign out); BoardView now renders inside it.
- labels store (list/create/rename/delete); Note gains labels[]; notes store
gains setLabels + label-aware reconcile + /api/notes?label= loading.
- /label/:id route → label-filtered board.
- LabelPicker (tag a note, create-on-the-fly) in the editor; label chips shown
on cards and in the editor; LabelsModal to create/rename/delete labels.
- api client PUT; new icons (note/tag/pencil/plus/check); nav-link styles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
- config store (public /api/config: site_name, allow_registration, version),
loaded in the router guard; site name drives the board header.
- session User gains is_admin.
- /settings route with requiresAdmin guard + admin-only gear link in the header;
SettingsView renders grouped typed fields (text/number/toggle), saves via
PATCH /api/settings with saved/error feedback, refreshes public config.
- Register link hidden + /register route blocked when registration is closed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
- Vite + Vue 3.5 + Pinia + vue-router + Tailwind (brand accent #F5C518),
dark-mode aware, deterministic package-lock.json for `npm ci`.
- Session store (fetchMe/login/register/logout) over a credentials:'include'
fetch client; router guards (requiresAuth / guestOnly) with lazy /me resolve.
- BaseButton + BaseInput primitives (focus rings, loading, error states).
- LoginView, RegisterView, and an authed BoardView shell with an empty state
for the M1 masonry board — all at v1 polish (rule 24).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm