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
Fast, cross-device labelling: type #groceries in a note and it becomes
the "groceries" label. The body is the source of truth for tag-labels;
manual picker labels stay independent (rule 28 — additive).
- note_labels.via_tag (migration 0013) marks tag-sourced attachments.
- parse_tags(): #tag at start-of-body or after whitespace, needs a
letter (so #2024, URL #frags, mid#word are ignored). unit-tested.
- _reconcile_tags() on create + body-update: attach labels for current
#tags (find-or-create, case-insensitive), detach tag-labels whose tag
was removed; never touches manual (via_tag=false) rows.
- label picker (set_note_labels + editor onLabelsChange) now preserves
tag-labels on save, so a picker action can't strip a label the #tag
still mandates.
- serialize via_tag; card/editor chips render tag-labels as "#name",
and the editor hides the × on them (remove by editing the tag text).
- LabelPicker builds manual NoteLabels (via_tag:false).
Fourth and final item of M4.5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
Checklist notes existed (M2) but could only be made by creating a text
note and toggling it in the editor — so they were undiscoverable. Now
the board's quick-add can make one in one shot.
- create endpoint accepts kind + items: POST /api/notes with
kind:"list" and items:[...] creates a checklist note and its items
atomically. A list note is non-empty when it has a title or ≥1 item.
- quick-add gets a checklist toggle (checkbox icon): flip it and each
body line becomes an item on save; placeholder switches to
"One item per line"; resets to a plain note after close.
- notes store create() accepts kind + items; parse_list_items() helper
(trims, drops blanks) with a unit test.
- title placeholders now read "Title (optional)" in quick-add too.
Third item of M4.5. Display/editing of checklists was already built in
M2; this closes the creation gap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
The [[ autocomplete only matched note names, so you could only link a
note you could name. Now it searches note NAME *and* body, so you can
link by recalling any phrase.
- new GET /api/notes/link-search?q= — owner-scoped, non-trashed;
substring ILIKE on display_title OR body; ranked name-first, then
name-prefix, then recency; empty q returns recent notes as
suggestions. Deterministic (no semantic/AI search); the FTS index
still powers the heavier /search. LIKE wildcards in q are escaped.
- editor [[ autocomplete now calls link-search (debounced 120ms)
instead of filtering the cached titles index; excludes the note
itself; inserts the matched note's display name as [[Name]].
- unit tests for the LIKE-escaping + a link-search auth guard.
Second item of M4.5; builds on the display_title work (every note has
a name to link to). Command-palette content search is a natural
follow-on, left out to keep this focused.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
Capture starts in the body, so forcing a title feels odd and body-only
notes had no name — which made them unlinkable. Fix both: persist a
display_title = explicit title if set, else the note's first non-empty
body line (deterministic, no AI). The title field stays optional.
- migration 0012: notes.display_title (NOT NULL, best-effort backfill;
the app recomputes precisely on next save)
- derive_display_title() helper, set on create + update
- drive the /titles index, backlinks, graph edges + node labels, and
[[wiki-link]] resolution off display_title so body-only notes are
nameable, findable (command palette / [[ autocomplete), and linkable
- rename-repoint generalized: inbound [[Old Name]] links now survive a
name change via the first body line too, not just an explicit title
- unit tests for the derivation (explicit wins, first non-empty line,
blank/empty, length cap)
- frontend: display_title on the Note type; title field placeholder now
reads "Title (optional)"
First item of M4.5 (frictionless input & recall); unblocks the linking
work. Card rendering unchanged (no first-line duplication).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
Renaming a note now rewrites [[Old Title]] references (and their note_links
rows) in every note that links to it, so backlinks survive the rename
instead of silently orphaning. Pure/case-only renames are skipped since they
still resolve. New pure helper rewrite_link_title() (DB-free unit tests) does
the token rewrite; _rename_inbound_links() applies it across owner-scoped,
non-trashed sources.
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 0009: note_links (source_id, target_norm). Parse [[...]] from body on
create/update and rewrite the source's links. GET /api/notes/titles (owner
{id,title} index for client-side resolution); GET /api/notes/<id>/backlinks.
- Frontend: titles store; LinkedText renders [[Title]] styled on cards; editor
shows Links (outgoing, resolve/create-on-click) + Linked-from (backlinks),
clicking navigates the editor to the target note (board + search).
- notes store: fetchOne, createTitled. DB-free link-parser tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
- Migration 0008: notes.position (int). Board orders pinned -> position ->
updated_at; new notes created at top (max position + 1). POST /api/notes/reorder
assigns positions from the given order (owner-scoped).
- notes store: position on Note, position-aware sort, optimistic reorder().
- NoteCard reorderable (native HTML5 draggable + dragstart/drop); BoardView moves
the dragged note before the drop target and persists.
Note: drag on a CSS-columns masonry has imperfect during-drag visuals (columns
reflow); order persists correctly. Candidate for a polish pass / layout tweak.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
- Migration 0007: note_attachments (path/mime/size). Upload POST
/api/notes/<id>/attachments (multipart, png/jpeg/gif/webp, 12MB cap via
MAX_CONTENT_LENGTH) stored under Config.media_root() (first use of DATA_DIR);
owner/ACL-scoped GET serves the file (nosniff); DELETE removes row + file.
Note responses include attachments[].
- Frontend: notes store uploadAttachment (FormData)/deleteAttachment; editor
image button + paste-to-upload + thumbnail grid with remove; card shows the
first image as a cover.
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