M304 step 5. The server already returns a derived `items` array, so the web kept working across the last two commits — but it was rendering every list TWICE: once as literal `- [ ] milk` bullets inside the body, and again as the separate NoteChecklist block underneath. This is the commit that makes the body the only place a checklist appears. markdown.ts gains a `task` block, matched BEFORE the plain bullet — which would otherwise swallow the marker and leave the brackets showing, the same ordering reason `code` is matched before emphasis in INLINE_RE. Each item carries its ordinal across the WHOLE document, because that is what an item's id means everywhere else now; counting per block would have made the second list's checkboxes toggle the first list's items. The card's preview clamp is why that ordinal is safe there: it only ever drops lines from the end, so a visible item's index is the same whether or not the body was truncated. MarkdownText emits a toggle rather than reaching for the store. Ticking a box rewrites a line of someone's note, and a renderer used in several places should not be the thing deciding that is allowed — the card passes `toggleable` and wires it, a read-only render does not and the boxes are inert. Not wrapped in a <label> either: on a card the text is the note's own words and clicking it opens the note, so only the box toggles. In the editor, the toolbar button stops revealing a section and inserts `- [ ] ` at the caret. That makes it the one toolbar action needing no persisted note to hang anything off — ensureDraft is gone from it, and it works on an empty compose box the moment it opens. Enter on a task line continues the list, and on an EMPTY one clears the marker; without that second half a list would be impossible to get out of. Indent and bullet are carried over rather than normalised, because continuing someone's `*` list with a `-` is an edit they did not ask for. NoteChecklist.vue is deleted (rule 22). The store's item methods stay: they are the repository seam the REST routes and Tauri commands both implement, not the old path. CI cannot check any of this beyond types — there are no frontend tests, only vue-tsc. It wants a real browser pass.
ThoughtSync
Self-hosted personal thought-capture web app in the FabledSword family — a
Google-Keep-style masonry post-it board for capturing disparate thoughts in
under a second, designed to grow into a lightweight second brain (labels, search,
[[wiki-links]], graph).
- Stack: Quart (async Python) + Vue 3 + PostgreSQL, Docker, Fabled-Git CI.
- Auth: native email + password (signed-cookie sessions). Multi-user with the family owner + direct-share + group-share ACL.
- Web-first. An Android companion is a later, conditional milestone.
Layout
src/thoughtsync/ Quart app (app factory, auth, models, ACL, config, db)
alembic/ async migrations (schema built via `alembic upgrade head`)
tests/ DB-free unit tests (pytest)
frontend/ Vue 3 + Vite + TypeScript + Tailwind SPA
Dockerfile 2-stage build (Vue -> python:3.12-slim runtime)
docker-compose.yml local app + Postgres stack
Development
Backend (needs a Postgres reachable at THOUGHTSYNC_DATABASE_URL):
pip install -e ".[dev]"
alembic upgrade head
hypercorn 'thoughtsync.app:create_app()' --bind 0.0.0.0:5000
Frontend (proxies /api to :5000):
cd frontend
npm install
npm run dev # http://localhost:5173
Or run it in Docker. Hot-reload dev stack (edit code, changes reload live) — Postgres + backend + Vite dev server, app at http://localhost:5173:
docker compose -f docker-compose.dev.yml up
Or the full production image (SPA baked in) at http://localhost:5000:
docker compose up --build
Deploy (self-host)
A complete two-container stack (app + Postgres) you can copy and run. Save it as
docker-compose.yml, change the two CHANGE_ME passwords (they must match), then
docker compose up -d:
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: thoughtsync
POSTGRES_PASSWORD: CHANGE_ME # change this
POSTGRES_DB: thoughtsync
volumes:
- thoughtsync-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U thoughtsync"]
interval: 5s
timeout: 5s
retries: 10
app:
image: git.fabledsword.com/bvandeusen/thoughtsync:latest # :dev for the current dev build
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
THOUGHTSYNC_DATABASE_URL: postgresql+asyncpg://thoughtsync:CHANGE_ME@db:5432/thoughtsync
volumes:
- thoughtsync-data:/var/thoughtsync # uploaded images; omit if you don't use attachments
ports:
- "5000:5000"
volumes:
thoughtsync-db:
thoughtsync-data:
Then open http://<host>:5000 and register — the first account becomes the admin.
- Only
THOUGHTSYNC_DATABASE_URLis required.THOUGHTSYNC_SECRET_KEYis optional; if unset, a signing key is generated and persisted in the database (sessions survive restarts). - Uploaded images live under the
thoughtsync-datavolume at/var/thoughtsync. - The app waits for the database and runs migrations (
alembic upgrade head) automatically on start. - Image tags:
:latest(stable, built frommain) ·:dev(latestdevbuild) ·:<git-sha>(immutable, for pinning / rollback). - Putting it on the public internet: there are four things to do first — close
registration, terminate TLS and forward
X-Forwarded-Proto, stop publishing the app port, and back up the attachment volume as well as the database. See docs/public-hosting.md, which also lists what the app hardens on its own and what it deliberately doesn't. - Install as an app (PWA): ThoughtSync is installable ("Add to Home Screen" / the
browser's install button) for an app-like window. Browsers only offer install over a
secure context, so put the app behind a reverse proxy terminating HTTPS (or reach
it via
localhost) — plainhttp://<host>:5000won't show the install prompt.
Milestones
- M0 — Foundation & Identity ✅: Quart+Vue+Postgres skeleton, native auth, sharing-ACL spine, Fabled-Git CI.
- M1 — Capture Core ✅: note model + masonry board (quick-add, colors, pin, edit-in-place, archive, trash+restore).
- M1.5 — Roles & DB-backed Settings ✅: first user is admin, admin Settings UI,
DATABASE_URLis the only required env. - M2 — Organize: labels/tags, search, checklists, attachments.
- M3 — Second-brain seeds:
[[wiki-links]], backlinks, graph view, reminders. - M4 — Launch hardening: responsive/PWA, settings UI, polish.
Work happens on dev; main is protected (PR-only).