3958db0a8bbbab0090177cad3753ce8aad77e7a7
98
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3958db0a8b |
desktop M10.4: fix stmt lifetime in reminders/search
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m20s
Both chained `stmt.query_map(...)?.collect()?` as the block's tail expression, so the prepared statement (a block local) was dropped before the borrow held by the mapped rows ended (E0597). Bind `let rows` first, matching every other query in the file. rustc error, so this also unblocks test + the release build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
41cf4b3598 |
desktop M10.5: wire the offline local source; boot to board with no server
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 34s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 57s
adapters/local.ts implements the repository seam over the M10.4 Tauri commands via invoke (camelCase args -> the commands' snake_case params); adapters/index.ts now selects local when running in the desktop shell, rest on web. bridge.ts exports invoke for it. This is the commit that resolves the black screen: a fresh desktop launch reads config + session + notes from the on-device SQLite core, so the router's auth gate passes with a synthetic local user and the board renders with zero server and zero account. Account auth, device linking, attachment upload, URL unfurl and file import reject with a "connect a server" message (no offline meaning yet); everything else — board, editor, capture, search, filters, labels, checklists, reminders — works fully offline. desktop.yml also now rebuilds the app on frontend adapter/bridge changes, since the desktop bundle embeds the frontend. Task 1994. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
dbe1425bd2 |
desktop M10.4: apply rustfmt (line wrapping only)
cargo fmt --check output from CI run 2849, applied verbatim: wraps long fn signatures, query/execute calls, and method chains. No logic change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
ed1b3aa814 |
desktop M10.4: Rust local SQLite store + Tauri commands
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 24s
The on-device core that makes the desktop app work with no server and no login. - rusqlite (bundled SQLite, so no system libsqlite dependency to vary across builds); uuid v4 ids; RFC3339/Date.toISOString-compatible timestamps. - Schema mirroring the note model: notes, labels, note_labels (with via_tag), checklist_items, attachments, link_previews, note_revisions, saved_filters, plus per-row sync_revision/dirty + a sync_state row for the M10.7 engine. user_version-gated migrations. - derive.rs: pure [[wiki-link]] + #tag scanners (mirror the frontend inline rules, no regex dep) with unit tests; #tags re-sync via_tag labels on save, [[links]] drive backlinks at query time (derived, never stored). - store.rs: the full repository surface (facet/label/date/text list, create, PATCH-semantics update, pin/archive/color/kind, checklist items, labels CRUD + merge, reminders complete/snooze, reorder, trash/restore/delete, revisions + restore, titles/search/backlinks/link-search, saved filters). - commands.rs: ~38 #[tauri::command]s over a Mutex<Connection> in managed state. - lib.rs: opens the DB in the platform app-data dir on setup; synthetic offline config/user so the auth-gated router resolves with no login. Attachment upload / URL unfurl / import are intentionally deferred (network/file concerns); adapters/local.ts (M10.5) wires all of the above via invoke. Task 1993. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
20cf15c99c |
desktop M10.3: frontend data-source adapter seam (repo interface + rest.ts)
Extract a typed repository interface (adapters/repo.ts) from the scattered store/view -> api.* calls, backed by adapters/rest.ts (verbatim HTTP mapping) and selected through adapters/index.ts. Every store and the notes-facing views now depend on `repo`, never the HTTP client directly -- the seam the offline local source (M10.5, over Tauri invoke) plugs into next. Behavior-preserving for web: rest.ts maps each semantic method to the exact endpoint the code called before; query-string and multipart building moved out of the stores/views into rest.ts (the one place that knows the URL shape). Client-side logic (reconcile/sort/optimistic reorder/toasts) stays in the stores. GraphView + admin SettingsView keep direct api calls -- out of the offline-core scope (M10.5 is board/editor/capture/search/filter/labels/ checklists/reminders). Task 1992. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
b08cdb92b5 |
desktop AppImage: de-bundle host-coupled graphics libs (fixes black window)
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m14s
Tauri's AppImage bundles the build host's graphics/display stack (libEGL/libGL/libdrm/libgbm/libwayland-* + Mesa dri drivers) into usr/lib. On many end-user systems (Arch/CachyOS, NVIDIA, Wayland) those clash with the running kernel driver + Mesa and abort with EGL_BAD_PARAMETER -> a black window (issue 2021). They load before any renderer choice, so the runtime env fallbacks can't rescue it; per the AppImage excludelist they must come from the host. Add a CI-only post-build step (desktop/packaging/appimage/debundle-graphics.sh) that extracts the built AppImage, strips exactly that graphics/display subset (keeping webkit/gtk bundled for portability), and repackages in place so the app falls through to the system's graphics libs. Wired into desktop.yml between the Tauri build and the artifact upload. Task 2023. Makes the AppImage the zero-install taste-test vehicle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
98d918dda2 |
M10 (task 2022, issue 2021): Arch pacman package + commit icon set (native, system libs)
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m27s
Native-first fix for the AppImage black window: a PKGBUILD that builds from source,
linked against SYSTEM graphics libs, so it renders on the host driver.
- desktop/packaging/arch/{PKGBUILD,thoughtsync.desktop,README.md}: `makepkg -si`
installs /usr/bin/thoughtsync + .desktop + icon; deps webkit2gtk-4.1/gtk3/...;
builds the frontend + `cargo build --release` (no tauri-cli). Uses the host
graphics stack -> avoids EGL_BAD_PARAMETER.
- Commit the icon set (desktop/src-tauri/icons/*.png, un-gitignored) so BOTH
`cargo build` (pacman) and `cargo tauri build` (deb/appimage) work without a
generate step; bundle.icon -> the 4 committed PNGs; drop the `cargo tauri icon`
step from desktop.yml.
- Broaden the WebKit software-render hardening (lib.rs) to ALL Linux (was
AppImage-scoped) so the native build also renders if system WebKit is finicky.
Can't CI-test the PKGBUILD (Arch-only; CI is Debian) -- operator builds locally.
desktop.yml re-verifies the deb+AppImage build with the committed icons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
|
||
|
|
3f5b87682e |
issue 2021: harden Linux WebKit rendering (fixes black AppImage window)
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m12s
WebKitGTK's DMA-BUF/EGL renderer fails to init on many Linux GPU/driver/Wayland setups -> 'EGL_BAD_PARAMETER' -> black window (known WebKitGTK issue, not app code). Per Tauri's Linux-graphics guidance, set the software-fallback env vars at startup before the webview is created, scoped to AppImage launches (native installs keep GPU accel): __NV_DISABLE_EXPLICIT_SYNC / WEBKIT_DISABLE_DMABUF_RENDERER / WEBKIT_DISABLE_COMPOSITING_MODE, each only if the user already set it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
7ca09e9a01 |
desktop CI: pin upload-artifact@v3 (Forgejo rejects v4 protocol)
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m9s
The build produces the .deb + .AppImage fine; upload-artifact@v4 failed with GHESNotSupportedError (Forgejo has no v4 artifact API). v3 uses the older protocol the instance accepts, so the bundles become downloadable from the run for hand-testing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
404e256760 |
M10 (2013): rustfmt integration.rs (wrap perm chain)
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m3s
cargo fmt --check wanted the fs::metadata(p).map_err(..)?.permissions() chain wrapped (>100 cols). Fixes desktop build #2829; the frontend already typechecked clean in that run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
877a6a572f |
M10 (task 2013): integrated AppImage — app self-integration (OOBE + Account toggle)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 9s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 26s
CI & Build / Build & push image (push) Successful in 33s
The Linux AppImage can now install itself into the applications menu, so it behaves like an installed app instead of a loose file. - Rust (desktop/src-tauri/src/integration.rs): integration_status / integrate_desktop / unintegrate_desktop commands — detect $APPIMAGE, copy the AppImage to ~/Applications, write ~/.local/share/applications/thoughtsync.desktop + embedded icon, update-desktop-database. Registered in lib.rs. - Frontend: withGlobalTauri exposes window.__TAURI__.core.invoke; desktop/bridge.ts (isDesktop + typed invoke, NO @tauri-apps/api dep -> web bundle unaffected); DesktopIntegrationPrompt (first-run OOBE, remembered) mounted in App.vue; AccountView "Desktop app" add/remove control. All desktop-guarded -> no-ops on web. - desktop.yml: upload the .deb + .AppImage as a run artifact (continue-on-error) so the build is downloadable for hand-testing. Verified by CI: ci.yml (vue-tsc) for the frontend, desktop.yml (cargo + tauri build) for the Rust + AppImage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
1607c77ee5 |
M10.8: desktop CI lane (.forgejo/workflows/desktop.yml) + app-icon source
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m19s
Separate workflow (not ci.yml) so the ~20-40min Rust/AppImage build only runs on desktop/** changes, not every backend/frontend push. Builds on the ci-tauri:1.97 image: frontend build -> `cargo tauri icon app-icon.png` (from a committed 1024px source PNG, sidestepping SVG-input questions) -> cargo fmt --check -> clippy -D warnings -> cargo test -> `cargo tauri build` (deb + AppImage). APPIMAGE_EXTRACT_AND_RUN=1 for FUSE-less CI containers. Verifies the M10.2 scaffold end-to-end and is the foundation for the integrated-AppImage work (task 2013). Also updates ci-requirements.md with the desktop lane. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
db24d7ea18 |
M10.2: Tauri v2 desktop scaffold (desktop/) — Vue frontend as the webview
New desktop/ Tauri v2 project (Linux-first, cross-platform-ready): - src-tauri: Cargo.toml (lib + thin main.rs shim), build.rs, lib.rs (Builder entry point), tauri.conf.json (frontendDist -> ../../frontend/dist, devUrl :5173, deb+appimage bundles), capabilities/default.json (core:default), .gitignore. - The shared Vue 3 frontend is the sibling ../frontend; before-commands cd via "$(git rev-parse --show-toplevel)/frontend" since frontend and src-tauri are siblings, not nested. - Icons generated from frontend/public/icon.svg via `cargo tauri icon` in CI (M10.8), not committed. Boots the shared UI in a native window. The local data adapter (M10.3/M10.5) and CI build verification (M10.8) follow. desktop/** is not yet in the CI paths filter — added with the desktop lane in M10.8. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
36b8f65dc6 |
M9 S1d: split the notes.py monolith into a cohesive package
The 1574-line notes.py becomes a `notes/` package. The heavy shared logic moves
into focused modules; the route handlers + blueprint registration stay together
in __init__ so registration is trivially correct (most routes have no CI
auth-test that would otherwise catch a route silently dropping out):
- notes/_bp.py — the Blueprint (isolated so route modules could import it
without a cycle; also the seam for a later route split).
- notes/serialize.py — note (+labels/items/attachments/previews) serialization.
- notes/links.py — [[wiki-link]] + #tag parsing and reconciliation.
- notes/recurrence.py — recurring-reminder next-occurrence math.
- notes/helpers.py — display-title/empty/filter/owner-fetch + filename/slug utils.
- notes/import_export.py — export markdown + Keep/native import specs + zip budget.
- notes/__init__.py — the `/api/notes` routes + re-exports the external surface
(app.py imports `bp`; sync.py + tests import helpers).
Pure reorganization — no behavior change (routes/helpers moved verbatim). Callers
(app.py, sync.py, test_notes.py) are unchanged: `from thoughtsync.notes import X`
resolves via the package __init__ (rule 22 — the package replaces the module).
No import cycle (nothing in the package's dep chain imports notes; only app.py +
sync.py consume it). New test_all_note_routes_registered asserts all 29 route
endpoints are attached, so CI catches any module that fails to register. Runtime
DB behavior operator-verified on deploy (no Postgres CI lane).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
|
||
|
|
44a5466793 |
M9 S5 (frontend): shared BaseModal for the standard modals + BaseInput in Account
- BaseModal.vue (new): the backdrop + dialog-panel shell (dimmed fixed overlay, bordered rounded panel, role=dialog, close on Escape + backdrop mousedown). Caller sizes/pads/shadows the panel via `panelClass`, picks start/center `align`, and sets an `ariaLabel` for header-less panels. - LabelsModal, CommandPalette, and the AppShell keyboard-shortcuts overlay drop their hand-rolled backdrop+panel shells and slot their content into BaseModal (~12 lines of overlay boilerplate each → gone). - NoteEditor deliberately keeps its own shell: its backdrop mousedown is drag-guarded and its Esc/⌘-Enter handling is bespoke (unsaved-edit safety), so folding it in would risk regressing the app's core editing surface (rule 28). - AccountView's one device-name field now uses the shared BaseInput. SettingsView is intentionally NOT converted — its rows are a horizontal label+control pattern (checkbox/number/text, direct value mutation), a different shape than BaseInput's vertical form field. Frontend-only; CI vue-tsc is the type/template gate (no local typecheck, rule 10). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
bdc5504419 |
M9 S5 (backend): _serialize_device adopts common.iso()
The two `x.isoformat() if x else None` copies in auth's device serializer now use the shared iso() helper — completes the isoformat-idiom sweep outside notes.py (auth + sync done; notes.py's remain, tied to its split). _serialize_user is unchanged (no datetime, no cross-module duplicate) and stays in auth.py rather than relocating for no DRY gain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
9c679d0ab9 |
M9 S5: guard the login ?redirect= against open redirect
LoginView handed route.query.redirect straight to router.replace, so a crafted link like /login?redirect=//evil.com (or a backslash variant) could bounce a just-authenticated user off-site. safeRedirect() now only follows an in-app absolute path — a single leading slash, rejecting "//host" and "/\\host" (and anything without a leading slash, i.e. absolute/scheme URLs) → falls back to "/". Frontend-only; CI vue-tsc is the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
be34fe8619 |
M9 S4: sync adopts serialization/parse_dt toolkit + normalizes push oracle
DRY: - serialize.py: serialize_label_sync(label) = base serialize_label + the delta-only fields (sync_revision/purged_at/created_at via iso()). sync's changes() adopts it; the local _serialize_label_row near-dup is gone. - sync adopts common.parse_dt (drops the byte-identical _parse_client_dt; 4 call sites) and common.iso for the note delta augmentation. (Manual-label reconciliation was already shared in S3.) Fully folding the note re-augmentation into the serializer waits on the notes.py split. - test_sync: drops the now-redundant _parse_client_dt test (parse_dt is covered in test_notes) + its dead import. Security (issue — push existence-oracle): a foreign-owned id on push was rejected with "not yours", distinguishing "another user's note" from a free id. A legit client only pushes ids of notes it created, so that branch is only hit by a probe (or ~0-prob UUID collision) — now a GENERIC "cannot apply" rejection that doesn't confirm the id exists. The residual create-vs-reject status difference is inherent to client-chosen ids over a global PK and is practically unexploitable (a shared note already exposes its id to recipients). Sync behavior operator-verified on deploy (no Postgres CI lane). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
e1cf63e875 |
M9 S3 (frontend): consolidate local-date helpers into notes/datetime.ts
The created-date range facets built local-day bounds by hand in two places (FilterBar's onTo/toInput, TimelineView's buildQuery) — parse a "YYYY-MM-DD", shift a day for the half-open upper bound, format back. - datetime.ts: parseLocalDate() / addLocalDays() (non-mutating) / formatLocalDay(). - TimelineView: drops its inline localDate() + the +1-day Date math. - FilterBar: drops its inline isoDay() + the setDate(±1) mutations. Behavior-preserving and deliberately NOT unifying output: Timeline still emits UTC (.toISOString()) bounds, FilterBar still emits naive-local "…T00:00:00" strings — only the shared primitives are extracted. (The naive-vs-UTC divergence is a separate backend-datetime-semantics question, flagged for later, not silently changed.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
590d3ff2f6 |
M9 S3 (frontend): recall views adopt useNoteList + AsyncState/EmptyState
The Search / Timeline / Reminders views each hand-rolled the same items+loading+error scaffold, retry button, and editor-host glue. - useNoteList(fetcher, fallbackError) (new): the load-a-note-list scaffold (items/loading/error + a load() that never leaves a half-state). Views supply just the fetcher; the refs drive <AsyncState>. - SearchView / TimelineView / RemindersView: adopt useNoteList + useNoteEditor + <AsyncState>/<EmptyState>; drop the local list/loading/error refs, the duplicated retry blocks, and the notes.items-shadowing navigate glue. - GraphView: editor host now via useNoteEditor (openNode/closeEditor/onNavigate collapse to navigate); loading/error via <AsyncState>. Its two empty states keep inline markup/buttons, so they stay custom (not forced into EmptyState). - reminders store: fetchReminders() is the single owner of /api/notes/reminders; both the background poll (check) and RemindersView read through it. Frontend-only; CI vue-tsc is the type gate (no local typecheck, rule 10). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
f1033da75e |
M9 S3 (backend): organize routes adopt toolkit + shared label reconciliation
DRY across the "organize/recall" backend surface:
- serialize.py (new): serialize_label(label) — the base {id,name,color}
shape. labels.py builds on it (adds count); sync deltas will (S4).
- labeling.py (new): resolve_owned_label_ids() + reconcile_manual_labels()
— the "set a note's MANUAL (picker) labels, leave the via_tag rows alone"
logic was duplicated line-for-line between notes.set_note_labels and
sync._apply_note_manual_labels. Now one home; both adopt it (removes the
redundant `chosen`==owned recompute in notes). Behavior-preserving.
- labels.py: json_error/not_found/parse_uuid, colors.normalize_color, and
serialize_label; dropped local LABEL_COLORS + _normalize_label_color
(NOTE_COLORS is the single palette) and `import uuid` (rule 22).
- saved_filters.py: json_error/not_found/parse_uuid for its 2 uuid parses
+ error shapes.
- graph.py: no change — no error/uuid/palette-normalize duplication to fold.
Test: DB-free test_serialize_label_shape guards the base shape.
sync.py's reconciliation swap is behavior-identical; operator-verified on
deploy (no Postgres CI lane).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
|
||
|
|
e8e0d86413 |
M9 S2: fold raw multipart fetches into api.postForm
Both notes-store uploads (uploadAttachment, importNotes) hand-rolled the same fetch + resp.json() + !ok error parsing that api.client already does. Add `api.postForm<T>(path, form)`: request() now detects a FormData body and lets the browser set the multipart Content-Type (skipping the JSON header + stringify), reusing the shared error handling — so the two uploads gain network-error handling and the 5xx infra toast they lacked. A too-large import returns 413 (< 500), so it still throws for inline display rather than toasting. DRY: net -13 lines; no raw fetch() remains in the stores. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
3a4c3c8164 |
S2: fix zip-bomb on import + SVG stored-XSS on attachment download
M9 section S2 — two real security fixes in notes.py: - Zip decompression bomb (issue #1980): note import read each zip entry with a whole-entry zf.read() and no cap, so a small archive could inflate to GBs and exhaust memory/disk. Add _ImportBudget — streams entries with a per-entry (64MB) and cumulative (512MB) decompressed cap, raising _ImportTooLarge past either; reject >10k entries up front; abort → 413 with the transaction rolled back. - SVG stored-XSS (issue #1981): attachment download served anything image/* inline, so an image/svg+xml attachment could execute script in-origin — and notes are shareable (rule 47), so this hit shared-note viewers. Inline now allowlists the trusted raster types only (png/jpeg/gif/webp); svg/html/xml/etc. download. Verified py_compile + ruff. Runtime (importing a bomb, opening an SVG) is operator-verified on deploy — no Postgres CI lane. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
18ca4d4db4 |
S1: frontend infra — AsyncState/EmptyState + useNoteEditor, adopted in BoardView
M9 section S1, commit 4 (frontend). Introduce the shared UI primitives the 7 data views + 5 editor hosts were hand-rolling: - components/AsyncState.vue: the loading / error(+Retry) wrapper (emits `retry`). - components/EmptyState.vue: the centered title/subtitle "nothing here" block. - composables/useNoteEditor.ts: the editing/open/close/navigate glue every editor host duplicated, as one controller (onClose hook + local-list resolution for [[wiki-link]] navigation). BoardView adopts all three: its three hand-rolled loading/error/empty blocks collapse into <AsyncState> + <EmptyState>, and its editor glue into useNoteEditor. The other views + editor hosts adopt these in the Organize (S3) and Auth (S5) sections. Behavior-preserving. Frontend has no local typecheck (rule 10); CI's vue-tsc is the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
61f7c40db7 |
S1: iso() datetime helper + one shared colour normalizer (colors.py)
M9 section S1, commit 3 — two more shared-toolkit pieces: - common.iso(dt): the "x.isoformat() if x else None" idiom (repeated 20+ times across every serializer) as one helper. Adopted in Note.serialize() and the revision serializer; other serializers adopt it in their sections. - colors.py: NOTE_COLORS (canonical, on the model) + a single normalize_color(). notes.py now imports the palette + normalizer from here and drops its local copy. labels.py's identical LABEL_COLORS/_normalize_label_color fold into this in the Organize section (S3); sync in S4. normalize_color and NOTE_COLORS remain importable from thoughtsync.notes (used by tests + sync), so nothing downstream breaks. common has no in-app imports, so the model→common→colors chain has no cycle. Behavior-preserving. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
6abc82c783 |
S1: unify error responses + id parsing (responses.py) across notes.py
M9 section S1, commit 2. Add src/thoughtsync/responses.py — the app's single
JSON-error shape and the two guards that pair with it:
- json_error(message, status): the one ({"error": ...}, code) builder
- not_found(): the standard 404, by far the most common note-route error
- parse_uuid(raw): parse a path/body id, None on malformed → pair with not_found()
notes.py adopts them everywhere: ~25 hand-built `jsonify({"error":"not found"}),404`
collapse to not_found(); ~20 other error returns to json_error(...); ~13 repeated
`try: uuid.UUID(x) except: ...` blocks to parse_uuid(). Behavior-preserving — same
bodies and status codes, one definition. jsonify stays for the success responses.
Other blueprints (auth, labels, saved_filters, sync, settings_api) adopt the same
helpers in their own M9 sections.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
|
||
|
|
2abed7132c |
S1: shared value helpers (parse_dt/coerce_bool) + auto-Secure session cookie
M9 hardening/DRY pass — section S1, commit 1 (the shared-toolkit foundation): - Add src/thoughtsync/common.py with parse_dt() and coerce_bool(): one home for the ISO-date and truthy-flag coercions that were duplicated across modules. notes.py adopts them and deletes _parse_iso_dt, _iso_to_dt and _truthy (rule 22 — old copies removed; callers, incl. tests, updated). - Security: the session cookie is now marked Secure automatically on any request that arrived over HTTPS (directly or via a proxy's X-Forwarded-Proto), via a SecureCookieSessionInterface override. Hardens HTTPS deployments without breaking plain-HTTP LAN installs — no config. Behavior-preserving refactor + one security hardening. The backend serialization layer, the json_error sweep, and the notes.py split follow as their own commits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
3c78060051 |
Header: use the app icon instead of the "TS" text badge (links home)
The header showed a plain "TS" text badge while the real brand icon (public/icon.svg — the yellow knowledge-graph tile) was only used as the favicon/PWA icon. Swap the badge for the actual icon, and make the logo + name a RouterLink to the board (standard "logo goes home"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
1df70c53bd |
Capture: Enter-to-start + on-board notice; dismiss discards a new note
Live-pass feedback: make the "waiting for input" state visible and starting a note more deliberate, and let dismiss cancel an accidental note. - New notes are now a confirm-to-keep dialog: Esc OR click-away DISCARD a brand-new, not-yet-persisted note (so an accidental keystroke / type-to- compose never litters); Ctrl/Cmd+Enter, the footer button, or Shift+Enter commit it. A compose already persisted by a rich action, and any existing note, still close-and-save on dismiss. The compose footer button is now a filled "Add note" so the save path is unmistakable. - Enter (board, nothing focused) starts a new note; a subtle dashed on-board notice — "Press Enter or start typing to add a note" — makes capture discoverable instead of silent (click it to compose too). Type-to-compose stays as the fast path. - Empty-state copy + shortcuts help updated (Enter/c for a new note). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
675b3aa248 |
Capture UX: "+ New" header button + type-to-compose (two-mode board keys)
Live-pass feedback: the wide "Take a note…" bar showed through behind the
compose modal and felt redundant. Per operator choice, remove the bar and
make capture header-button + keyboard driven.
- Removed the board's inline "Take a note…" trigger bar.
- AppShell gains a "+ New" header button (next to search) — navigates to the
board if needed, then opens the compose modal. The `c` shortcut now does
the same (unified with newNote()).
- Type-to-compose: on the board with no card focused, any other single
printable key opens a new note SEEDED with that key (AppShell onKeydown
fall-through, after the reserved / c ? g shortcuts). Seed travels via
ui.composeSeed → NoteEditor's new `initialBody` prop; caret placed at end.
- Two-mode board keyboard (BoardView): RESTING = arrows enter browse, letters
type-to-compose; BROWSING (a card focused) = j/k move, e/x/# act, Enter
opens, Esc exits to resting. ui.boardCardFocused tells the global handler
to stand down while browsing so it doesn't swallow card keys.
- Updated the shortcuts help + the empty-state copy ("Hit + New — or just
start typing").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
|
||
|
|
f9f1b77d37 |
Unify compose + edit onto one surface (modal-only editor)
Live-pass feedback: compose and edit still felt like different surfaces. They already shared one component (task 1920), but rendered as two frames — an inline in-flow box (compose) vs a modal overlay (edit) — which read as two designs. Per operator choice, make BOTH the modal. NoteEditor is now modal-only (rule 22 — the inline frame is fully removed): dropped the `inline`/`autofocus` props, the collapsed "Take a note" frame, `expanded`, `open()`, `commitInline`, `autoGrow`, and the outside-click commit. Compose vs edit is purely note=null vs a note. Esc / Ctrl+Enter / backdrop / Done all commit-and-close (create in compose, save in edit); Shift+Enter still saves & starts a fresh note in compose (now gated on isCreate, not the frame). Edit-only sections gate on !isCreate. BoardView: the always-expanded inline composer becomes a slim "Take a note…" trigger bar that opens the SAME modal with an empty note; the `c` shortcut does likewise. One surface for capture and editing — and the seam the card→editor grow animation (1914) will hook into. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
d1bc91a54a |
M6 1908b: foreground reminder delivery + recurrence/snooze UI (frontend)
Completes 1908 without Web Push. While the app is open, due reminders now actually surface; recurring reminders + snooze/done are manageable. - reminders store (singleton): polls /api/notes/reminders every 45s while the app is open; each due reminder fires ONCE as a toast (with an "Open" action) and, if the user opts in, a page-context OS Notification — no service worker, no PWA. Silently primes a stale backlog on first load; only announces recently-due ones. AppShell starts/stops it. - Editor reminder section: a Repeat picker (Does not repeat / Daily / Weekly / Monthly / Yearly) + Done (advances a recurring reminder / clears a one-off) + Snooze 1h/1d, shown when a reminder is set. - RemindersView rebuilt as a chronological list: per row a due time + recurrence badge + Done / 1h / 1d, click to open; plus an "Enable notifications" opt-in and a note that background alerts come with native. - Note type gains `recurrence`; notes store setRecurrence / completeReminder / snoozeReminder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
882d4206aa |
M6 1908a: recurring reminders + complete/snooze (backend, no web-push)
Per operator: skip Web Push; build the rest of reminder delivery. This is
the client-agnostic half — the model + logic that foreground/native
delivery drives.
- notes.recurrence (migration 0022): daily/weekly/monthly/yearly or null.
update_note accepts it (cleared when the reminder is cleared); rides
export/import + sync push. Serialized on the note.
- Pure next_occurrence(remind_at, recurrence, after): the next fire strictly
after `after`, rolling past missed occurrences; _add_months clamps the day
to the target month (Jan 31 → Feb 28).
- POST /api/notes/<id>/reminder/complete — a recurring reminder advances to
its next occurrence; a one-off clears. POST .../reminder/snooze {minutes}
→ remind_at = now + minutes (1 min .. 30 days).
No VAPID / push-subscription / service-worker — foreground + native delivery
land in the UI commit and the native clients.
Tests (DB-free): normalize_recurrence; next_occurrence (daily/weekly/
monthly-clamp/skip-missed/yearly/none); complete + snooze auth-guards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
|
||
|
|
5c045aed63 |
M6 1902b: facet bar + saved views UI (frontend)
The dead-simple facet bar over the board + saved views in the sidebar,
completing task 1902. Filter state lives in the URL query, so a filtered
board is a shareable lens and a saved view is just a link ("one space,
many lenses").
- notes/facets.ts: facetsFromQuery / facetsToQuery / facetCount helpers.
- FilterBar.vue (board only): a "Filters (N)" toggle expanding to text
search + color swatches + label chips + has-reminder / has-attachment /
Lists / Notes toggles + a created-date range; Clear + "Save view".
Each control writes the URL query (router.replace).
- notes store: load(view, label, facets) builds the query; NoteFacets type
+ activeFacets; import reload preserves active facets.
- savedFilters store + sidebar "Views" section (each a query-link, delete
on hover); loaded on mount.
- BoardView derives facets from the query, reloads on facet change (ignores
?open=), and shows a "no notes match these filters" empty state.
- Backend: saved-filter param whitelist keys on `label` (matches the
repeatable ?label= query) so saved views keep their labels. New filter
icon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
|
||
|
|
5fdc124c77 |
M6 1902a: richer facet query + saved-filters storage (backend)
GET /api/notes gains combinable, AND-ed facets alongside the existing filter/date/sort: multiple ?label= (notes with ALL), ?color, ?kind, ?has_reminder, ?has_attachment, and ?q (full-text over title+body, ranked) — so the facet bar's text box searches, not just filters. All optional; invalid color/kind → 400. saved_filters table (migration 0021) + /api/saved-filters CRUD (list / create / rename+repoint / delete, owner-scoped). `params` is a JSON facet dict mirroring the query surface; clean_params() whitelists facet keys so a saved view can't accumulate junk. Tests (DB-free): _truthy, clean_params key-whitelisting, saved-filters auth-guards. UI (facet bar + saved-views sidebar) lands next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
69bf04e948 |
M6 1901: URL capture with link-preview unfurl (SSRF-hardened)
Paste a link → fetch its OpenGraph/meta preview (title, description, image,
site) and show a rich card. User-triggered + persisted (never auto-fetches;
cached so it never re-fetches). Opt-in via a new admin setting
enable_url_unfurl (default on, rule 26).
Security (the whole point of this task): a new dependency-free unfurl.py
does the fetch with layered SSRF defenses — http/https only; resolve the
host and reject EVERY non-public address (private/loopback/link-local/
reserved/multicast/unspecified — blocks 169.254.169.254 etc.); connect to
the vetted IP with SNI so DNS-rebinding can't slip through; ≤3 redirects
each re-validated; 5s timeout; 512 KB cap; text/html only; blocking IO in a
worker thread. No server-side image fetch — the og:image URL is loaded by
the browser.
- note_link_previews table (migration 0020), one per (note, url); serialized
inline on notes (+ rides the sync pull feed read-only).
- POST /api/notes/<id>/unfurl {url} (owner-scoped, setting-gated, 502 on
fetch failure); DELETE /api/notes/<id>/previews/<id>.
- enable_url_unfurl exposed in public config so the UI hides the affordance
when disabled.
Frontend: LinkPreview.vue card; editor detects URLs in the body and offers a
"Preview <domain>" chip per un-previewed link (ensureDraft first), renders
preview cards with remove; card shows previews read-only. New link icon;
notes-store unfurl()/deletePreview().
Tests (DB-free): is_public_ip range blocking, validate_url scheme/parts,
extract_preview (OG + <title> fallback + relative-image resolve), endpoint
auth-guards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
|
||
|
|
b5f545f655 |
M6 1900: any-file attachments + audio memos (broaden beyond images)
A note can now carry any file, not just images — PDFs, documents, audio memos, etc. "Dump anything" capture. Backend: - note_attachments.filename (migration 0019) records the original name for download + display. - Upload drops the image-only mime gate: accepts any type, derives the storage extension from the filename, and enforces a DB-backed per-file cap — new setting max_attachment_mb (default 25, rule 25). App body ceiling raised 12→64 MB (also lifts the import-zip / sync-push limits); the per-file cap is the effective attachment limit. - Serve sets Content-Disposition: images inline, everything else downloads with its original (header-sanitized) filename. - Import (native + Keep Takeout) now brings in ANY attachment, not just images — completing the Keep audio-memo gap; preserves filename + sha256. - Attachment metadata (delta feed + REST) carries filename. Frontend: - Editor renders attachments by kind: images inline (thumbnail), audio via an inline <audio> player, any other file as a download chip (paperclip + filename + size). File picker accepts any type; "Attach a file". - Card previews the first image; non-image files show as compact chips. Tests (DB-free): _safe_filename, _attachment_ext, _header_filename. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
7dd74d2946 |
Sync 6: sync protocol doc (docs/sync.md)
The contract the Tauri/Android clients implement against: device-token auth, the shared-sequence revision cursor, note-as-sync-unit (+ derived links/tags not synced), trash vs purge tombstones, pull (GET /changes) + push (POST /push) request/response shapes, last-write-wins + history conflict policy, attachment blob sync by id + sha256, and the idempotent/resumable sync cycle (initial since=0 + resume). Docs only — CI paths exclude *.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
0ca244be3d |
Sync 5: attachment blob sync — client id + content hash (M8)
Let native clients sync attachment blobs deterministically: - note_attachments gains sha256 (migration 0018, nullable, no backfill). The delta feed's attachment metadata now carries size + sha256 so a client knows exactly which blobs it already has (dedupe) and can verify integrity after download. - Upload accepts an optional client-supplied attachment id (multipart form field), so a file attached offline keeps its identity across sync; re-uploading an id the note already has is an idempotent no-op. The server hashes the stored bytes (sha256) on upload. Download by id already exists (owner/shared scoped). Frontend Attachment type carries the new optional size/sha256. (Still image-only mimes — broadening to any-file is task 1900. Blob sync behavior is operator-verified on deploy.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
68abaa0f3f |
Sync 4: push endpoint POST /api/sync/push (LWW + history snapshot)
The core conflict-resolution step. Applies a batch of client changes,
additive + owner-scoped, with last-write-wins by client edit-time — and a
version-history snapshot on every overwrite so nothing is ever lost.
client_wins(client_edited_at, server_edited_at): apply iff client >= server;
a missing client time never overwrites a real server edit; a missing server
time (new row) yields. Notes compare against updated_at; labels gain an
updated_at (migration 0017, backfilled from created_at) as their LWW field.
Notes:
- upsert with a client-supplied id: create if absent, else LWW-apply the
full note state (title/body/color/kind/pins/trash/remind/position/items/
manual label_ids) with the same ripple as a web edit — derive_display_title,
_rewrite_links, _reconcile_tags (#tags), _rename_inbound_links. Overwriting
an existing title/body snapshots the old version into note_revisions first.
A resurrected tombstone clears purged_at.
- delete: purge tombstone (drop children + attachment files, clear content,
set purged_at), LWW-guarded so a newer server edit survives a stale delete.
Labels: upsert (create/rename/recolor) + delete (detach from notes, tombstone),
LWW-guarded; per-owner name-uniqueness clash on a different id is rejected
rather than raising.
Response: per-item {status: created|applied|kept|noop|rejected, sync_revision};
the client pulls afterward to converge. Whole-note semantics (client sends the
full state, not a partial patch).
Tests (DB-free): client_wins across all edit-time combinations; _parse_client_dt;
push auth-guard. Apply behavior + triggers operator-verified on deploy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
|
||
|
|
8e40ea1188 |
Sync 3: pull endpoint GET /api/sync/changes (M8)
Delta pull for native clients: returns every note + label the caller owns whose sync_revision advanced past ?since=<cursor>, ascending by revision, paginated (?limit, default 500 / max 1000), with the next cursor + has_more. since=0 is a full initial sync. Web app unaffected (new blueprint). Notes and labels share one revision sequence, so the cursor is a single watermark. _page_cursor() handles the two-stream paging: when either stream fills its page, it advances only to the SMALLER of the two page boundaries so nothing between the cursor and the next pull is skipped. Notes reuse _serialize_notes (items/labels/attachments inline) + sync_revision + purged_at (tombstone); labels carry name/color/purged_at/sync_revision. Returns ALL of the owner's notes regardless of state (active/archived/ trash/purged) — a client mirrors everything. Registered sync blueprint. Tests (DB-free): changes auth-guard; _parse_since / _clamp_limit validation; _page_cursor across empty / drained / one-full / both-full. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
3c76b50a9c |
Sync 2: device-token bearer auth + linked-devices UI (M8)
Native clients (Tauri/Android) authenticate sync with a long-lived device bearer token, alongside the existing web session cookie. Backend: - security.py: generate_token() (secrets.token_urlsafe) + hash_token() (SHA-256 — device tokens are already high-entropy, so no slow KDF; keeps per-request bearer auth cheap). Only the hash is stored. - device_tokens table (migration 0016): id, user_id, token_hash (unique), name, created_at, last_used_at. - login_required now accepts `Authorization: Bearer <token>` OR the session cookie. Session path stays DB-free (fast); bearer path looks up the token hash, sets g.user_id, and stamps last_used_at. - Endpoints: POST /api/auth/device-login (public; email+password → token, the native first-link flow), POST /api/auth/devices (session/bearer → token, web "link a device"), GET /api/auth/devices (list), DELETE /api/auth/devices/<id> (revoke). All owner-scoped; token shown once. Frontend: - Per-user (not admin) /account view "Linked devices": create a token (one-time reveal + copy), list devices (name, linked/last-synced), revoke with confirm. Top-bar device icon for all users; devices Pinia store. Tests (DB-free): token hash determinism + uniqueness; device endpoints auth-guard (401 without auth, before DB); device-login input validation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
58b88d2622 |
Sync 1: revision + tombstone schema (M8 sync hub foundation)
The delta-sync substrate for the local-first native clients (Tauri, Android). No API behavior change — pure infrastructure; triggers are operator-verified on deploy (no Postgres CI lane). Migration 0015: - CREATE SEQUENCE sync_revision_seq. - notes + labels gain sync_revision (bigint) + purged_at (tombstone), with existing rows backfilled to distinct increasing revisions. - Trigger ts_set_sync_revision() BEFORE INSERT OR UPDATE on notes+labels stamps a fresh monotonic revision from the sequence, so no mutation site can forget to bump it (robustness over app-level bumps). - Trigger ts_bump_parent_note_revision() AFTER INS/UPD/DEL on note_items, note_attachments, note_labels re-bumps the parent note, since a note syncs as a whole (items/labels/attachments travel inline). - Indexes (owner_id, sync_revision) on notes + labels for the delta pull WHERE sync_revision > cursor. purged_at is the hard-delete tombstone (distinct from deleted_at = trash) so an offline client learns a row is gone instead of resurrecting it. Model columns added to Note + Label (nullable; trigger populates them). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
333ab9ce74 |
Import: ThoughtSync-native round-trip + Google Keep Takeout
Complete the export/import pair (task 1907). POST /api/notes/import takes
an uploaded .zip and appends its notes — never overwriting existing ones.
Two formats, auto-detected:
- ThoughtSync export: recognized by its notes.json (app == thoughtsync);
round-trips title/body/color/kind/pinned/archived/remind_at/timestamps/
labels/items and re-attaches image media from the zip.
- Google Keep Takeout: each Keep <note>.json → a note. Maps title,
textContent/listContent (+ checked), labels, Keep color enum (nearest
palette match), isPinned/isArchived, isTrashed (→ trash), created/edited
microsecond timestamps; folds annotation URLs into the body; resolves
attachment filePaths relative to the note's folder.
Imported notes reuse create_note's derivation + reconciliation:
display-title derive, #tag reconcile, [[wiki-link]] rewrite. Explicit
labels attach as manual (via_tag=false); inline #tags reconcile as tags.
Image attachments copied into media storage; non-image types (e.g. Keep
audio) skipped until any-file attachments land.
Frontend: an Import control in the sidebar (next to Export) — hidden file
input + FormData POST + result toast ("Imported N notes (M skipped)"),
reloading the board + labels. New upload icon; notes-store importNotes().
Tests: import auth-guard + pure-helper coverage (_usec_to_dt, _keep_spec
list/text/color/annotation/attachment mapping, _native_spec round-trip).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
|
||
|
|
1417479729 |
Graph: label hubs cluster notes; unlinked notes float by default
Add labels as first-class nodes in the graph so tags act as visual clustering hubs, and let unlinked notes float in the space by default — per operator request (the graph is a light auxiliary lens, not a focal surface). Backend (graph.py): - Emit a label-hub node (id "label:<uuid>", kind "label", #name, label color) for every label attached to a live note. - Emit note -> label membership edges (kind "label") alongside the existing wiki-link edges (now kind "link"). Frontend (GraphView.vue): - Render hubs as larger ringed nodes; membership edges dashed with a slightly longer spring rest so notes ring their hub. - Default showAll (unlinked notes float) to true; add a Show labels toggle (default on) that hides hubs + membership edges. - Click a label hub -> that label's board lens (/label/<id>); note clicks still open the editor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
cc828559ff |
M6: export — download all your notes as a zip
Data portability / no lock-in (task 1907, export half). GET /api/notes/export streams a zip of the caller's notes: a machine-readable notes.json (notes + labels + items + reminders + attachment refs), a human-readable Markdown file per note (frontmatter + body / checklist), and the attachment media. Sidebar 'Export' link (same-origin GET, session cookie) downloads it. _slugify unit-tested. Import (Google Keep Takeout) is the follow-up increment of this task. Pure backend + small frontend; no migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
efbf981a2a |
M6: version history — note revisions + restore
A note's title+body is snapshotted on each edit that changes either, so an accidental overwrite can be viewed and restored (task 1906). Underwrites 'dump freely, nothing is lost'. Backend: note_revisions table (migration 0014) + NoteRevision model; update_note records a revision of the PRE-edit state whenever title/body changes; GET /api/notes/<id>/revisions (newest 50) and POST /api/notes/<id>/revisions/<rev_id>/restore (snapshots the current state first so restore is itself undoable, then applies the revision with the usual title/body ripple — display name, links, #tags, backlinks). Title+body only in v1. Frontend: a History toggle in the modal editor opens a panel of past versions (timestamp + preview) with per-row Restore. Store gains fetchRevisions/restoreRevision. Migration 0014 runs on deploy; DB behavior operator-verified (no Postgres CI lane). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
47974f62ee |
M7: editor keyboard + one-click card toolbars (live-pass fixes)
From the unified-editor live pass (task 1922): - Esc now returns to the base Notes board from any view (AppShell); an open editor consumes Esc first (its handler stops propagation) so it closes before navigating. - Ctrl/Cmd+Enter = finish & close in BOTH frames — it already closed the modal editor; now it also commits+collapses the board composer (matches the email/chat 'send' convention). Shift+Enter still saves & starts a new note. - Fix: the inline composer committed on document mousedown, collapsing and shifting the board before a click landed — so clicking another card's pin/archive/trash took two clicks. Commit on click (bubble phase) instead; the target's action fires first, then the composer closes. Pure frontend. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
261d8fcac5 |
M6: lightweight markdown rendering on note cards
Render a small Markdown subset in the read view — headings (#..###), bullet/ordered lists, blockquote, fenced code, and inline **bold** / *italic* / `code` — alongside the existing [[wiki-links]] (task 1905). Capture stays plain text; only the card render is formatted. Hand-rolled dependency-free parser (notes/markdown.ts) rendered as Vue vnodes (MarkdownText/MarkdownInline), never v-html, so there is no HTML-injection surface — matches the no-heavy-dep ethos. Headings require '# ' (space), so a #tag (no space) stays plain text and is never mistaken for a heading; the two coexist. Replaces LinkedText (links-only) on the card; LinkedText deleted. Pure frontend — no backend/DB/migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
bb33d5c495 |
M7: unify compose + edit into one NoteEditor (DRY)
The 'Take a note' composer (QuickAdd) and the note editor were two components with duplicated field logic and unequal capabilities — composing lacked [[ links, labels, images, reminders. Merge them into a single NoteEditor with two frames: inline on the board (compose), modal on a card (edit). Same fields, styling, [[ autocomplete and toolbar in both (task 1920). Compose is now a draft that persists on first real action: on commit-with-content, or on the first label/image/reminder/checklist use (ensureDraft) — so no empty-note litter. Rich controls light up once there's content; note-lifecycle actions (pin/archive/trash, links/backlinks) stay in the modal. notes.create() now returns the created note (for the draft id). QuickAdd.vue deleted; BoardView renders <NoteEditor inline>. Pure frontend — no backend/DB/migration. Sets up the editor<->card morph (task 1914). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |