Commit Graph
18 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5 fe683595df M10.7e: desktop Sync settings screen (task 2108)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 11s
CI & Build / Build & push image (push) Successful in 33s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 1m32s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m4s
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
2026-07-26 00:30:37 -04:00
bvandeusenandClaude Opus 4.8 5c045aed63 M6 1902b: facet bar + saved views UI (frontend)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 37s
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
2026-07-23 08:45:56 -04:00
bvandeusenandClaude Opus 4.8 69bf04e948 M6 1901: URL capture with link-preview unfurl (SSRF-hardened)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Python tests (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Build & push image (push) Successful in 33s
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
2026-07-23 08:05:24 -04:00
bvandeusenandClaude Opus 4.8 b5f545f655 M6 1900: any-file attachments + audio memos (broaden beyond images)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 7s
CI & Build / Build & push image (push) Successful in 32s
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
2026-07-22 23:37:43 -04:00
bvandeusenandClaude Opus 4.8 3c76b50a9c Sync 2: device-token bearer auth + linked-devices UI (M8)
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 33s
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
2026-07-22 22:59:52 -04:00
bvandeusenandClaude Opus 4.8 333ab9ce74 Import: ThoughtSync-native round-trip + Google Keep Takeout
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 33s
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
2026-07-22 21:24:02 -04:00
bvandeusenandClaude Opus 4.8 cc828559ff M6: export — download all your notes as a zip
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 9s
CI & Build / Build & push image (push) Successful in 33s
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
2026-07-22 16:48:49 -04:00
bvandeusenandClaude Opus 4.8 efbf981a2a M6: version history — note revisions + restore
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 7s
CI & Build / Build & push image (push) Successful in 30s
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
2026-07-22 16:08:30 -04:00
bvandeusenandClaude Opus 4.8 ae0c748507 M6: label management — usage counts + merge
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 8s
CI & Build / Python tests (push) Successful in 16s
CI & Build / Build & push image (push) Successful in 50s
Extends the existing label manager (create/rename/color/delete) with the two missing maintenance tools (task 1904), so the label list stays clean — which matters more now that #tags mint labels automatically.

Backend: GET /api/labels returns a per-label note count (one grouped query); new POST /api/labels/<id>/merge moves the source label's notes onto a target and deletes the source (repoint via delete+reinsert to avoid mutating the composite PK; preserves via_tag; dedupes notes already on the target). Body #tags are NOT rewritten, so a tag-sourced label re-mints on next edit if its #tag text remains — a documented nuance.

Frontend: LabelsModal shows each label's note count and a 'merge into…' picker; also restores the previously-missing close (x) and merge icons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
2026-07-22 12:36:50 -04:00
bvandeusenandClaude Opus 4.8 95b0e30fc7 M6: browse notes by creation date (Timeline lens)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 1m2s
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
2026-07-22 12:26:35 -04:00
bvandeusenandClaude Opus 4.8 5e225075f3 m4: drag-to-reorder polish — grip handle + drop affordances
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 26s
Refine the masonry drag interaction (task 1869) so reordering reads as
intentional instead of an accidental, feedbackless jump:

- explicit grip handle (top-left, hover-revealed, board views only) gates
  dragging — a plain click or text-select no longer starts a drag
- source card dims while dragging; the card under the pointer shows a
  brand ring + slight lift, so the drop position is clear before release
- proper move cursor via dataTransfer effectAllowed/dropEffect
- dragleave uses a relatedTarget guard so the target ring doesn't flicker
  over child elements
- clear drag state on dragend (BoardView clears the tracked source even
  when a drag is cancelled off-target)
- add a "grip" icon to the shared Icon set

Native HTML5 DnD stays library-free; it remains a desktop/mouse
affordance (touch reorder is the M5 Android client's domain).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
2026-07-21 08:33:29 -04:00
bvandeusenandClaude Opus 4.8 c57982d910 M3 reminders: notes.remind_at + Reminders view
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 7s
CI & Build / Build & push image (push) Successful in 32s
- Migration 0010: notes.remind_at (nullable tz). PATCH accepts remind_at
  (ISO set / null clear); GET /api/notes/reminders (soonest first, non-trashed);
  serialize includes remind_at.
- Frontend: datetime util (local<->ISO, format, overdue); notes store setReminder;
  editor datetime-local picker + clear; card reminder chip (overdue = red);
  sidebar Reminders entry + /reminders view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
2026-07-20 08:12:14 -04:00
bvandeusenandClaude Opus 4.8 ad006ccb58 M3 graph view: /api/graph + force-directed SVG
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 28s
- 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
2026-07-20 08:05:45 -04:00
bvandeusenandClaude Opus 4.8 e4c898cd1b M2 attachments: image upload + owner-scoped media serving
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 34s
- 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
2026-07-19 22:21:26 -04:00
bvandeusenandClaude Opus 4.8 31be66ac60 M2 checklists: note kind + items backend + editor/card UI
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 27s
- Migration 0006: notes.kind ('text'|'list') + note_items (text, checked,
  position). Item API: add/update(toggle)/delete/reorder; PATCH note kind; note
  responses include kind + items[] (merged in one query alongside labels).
- notes store: kind/items on Note, setKind/addItem/updateItem/deleteItem.
- NoteChecklist component (toggle/add/edit/delete items); rendered read-only-ish
  on cards (checkboxes toggle) and editable in the editor.
- Editor: convert text<->checklist (body lines become items on convert to list).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
2026-07-19 22:13:25 -04:00
bvandeusenandClaude Opus 4.8 2046600a95 M2 labels frontend: sidebar shell + label filter, chips, picker, manage
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 13s
CI & Build / Build & push image (push) Successful in 34s
- 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
2026-07-19 21:56:14 -04:00
bvandeusenandClaude Opus 4.8 68f7b260de M1.5 frontend: admin Settings UI + config store + register gating
CI & Build / Python lint (push) Successful in 3s
CI & Build / Python tests (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 31s
- 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
2026-07-19 18:28:56 -04:00
bvandeusenandClaude Opus 4.8 0f604f9a26 M1: masonry board UI — quick-add, note cards, editor, views
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 30s
- notes Pinia store (load/create/pin/archive/color/trash/restore/delete) with
  view-aware reconcile; api client patch/del methods.
- colors.ts palette (10 keys → light/dark card + swatch tints).
- QuickAdd (collapsed → expand, title/body/color, click-outside/Esc to save),
  NoteCard (color tint, click-to-edit, hover action bar), NoteEditor modal,
  ColorPicker, inline Icon set (no icon dep).
- BoardView rewrite: Notes/Archive/Trash routes, CSS-columns masonry, Pinned +
  Others sections, per-view empty states, sign-out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
2026-07-19 16:08:53 -04:00