`complete_reminder` cleared `remind_at` and said so in its own comment — "(Recurrence advancement is a later refinement.)". So Done on a daily reminder was quietly the last time it ever fired. Reminder notifications made that much easier to hit, because Done is now a button in the notification shade. **The server already had this.** `src/thoughtsync/notes/recurrence.py` has done it correctly all along, which means the web behaved one way and the desktop and Android the other, on the same note, in the same account. This is a port of that file rather than a fresh implementation, kept behaviourally identical rather than merely similar: the same reminder can be completed from a browser or a client, and a disagreement would move it depending on which one you happened to use. The seven new tests in `core/src/local/recur.rs` mirror the Python suite case for case, including the one that matters most in practice — 31 January plus a month is 28 February, and the step after that is 28 March rather than back to the 31st. That clamp is sticky, and it is now asserted on both sides so a future "fix" to either has to change both. Advancement is measured from the reminder's own time, never from now, which is what keeps a 09:00 daily reminder at 09:00 when it is dealt with at 09:47. A phone left in a drawer for a fortnight rolls forward to tomorrow rather than arriving at fourteen pending occurrences of the same thing. Also matched from the server, and a latent bug of its own: the non-recurring branch now clears `recurrence` as well as `remind_at`. Before, completing a note that carried a rule left the rule behind with no reminder attached — invisible in every UI, since they only render recurrence when there is a reminder to recur from, and waiting to surprise whoever next set a time on that note. Documented rather than hidden, and shared with the server: the arithmetic is in UTC and a note carries no timezone, so a daily reminder crossing a DST boundary keeps its UTC time and shifts by an hour locally. Fixing that means a zone per note, which is a wire-format change. Verified in the CI image before pushing: fmt, clippy --all-targets -D warnings, and the full suite — core 89 to 96, ffi 11 to 12. The new FFI test walks the path the notification's Done button actually takes.
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). - 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).