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