bvandeusenandClaude Opus 5 18a58fb5da
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 1m0s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m30s
Android (Tauri) / Android APK (debug) (push) Successful in 4m25s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 6m8s
Desktop (Tauri) / Update manifest (push) Successful in 4s
frontend: the board glides and the editor grows from its card (task 1914)
Two of M7's motion targets. prefers-reduced-motion was already in place from the
1999 pass and gates both of these for free.

FILTERED REFLOW. The three card grids become TransitionGroups sharing one
transition name, so "how the board moves" is defined once in CSS rather than
three times in markup. Vue's TransitionGroup does the FLIP itself — measure
before, measure after, transition the difference away — so no animation
dependency, which the task called for.

Leavers are deliberately NOT pulled out of flow with position:absolute, the usual
TransitionGroup trick. This masonry is CSS multi-column, and an absolutely
positioned child escapes its column to the container's origin: a note would fly
diagonally across the board on its way out. Keeping leavers in flow costs a small
settle when the element is finally removed, so the leave is the shortest of the
three durations.

EDITOR CONTINUITY. useNoteEditor.open() is the one place that knows which card
was clicked, so that is where the card's on-screen centre is captured; the editor
panel then scales from that point. Deliberately not a true shared-element morph:
scaling by the real card-to-panel ratio distorts the text on the way, and a card
is often a third of the modal, so an honest ratio reads as a zoom rather than a
transition. The task sanctioned a good-enough scale/position tween; this is that.

A point rather than a rect, because nothing needs the card's size and a point
survives the card being filtered away while the editor is open. Consumed on read,
so a compose — which has no card — cannot inherit the origin of whatever was
edited before it and grow from an arbitrary corner.

The animation lives inside NoteEditor rather than in the five views that render
it: the leave has to finish BEFORE the host unmounts, so the component owns its
own visibility and tells the host when it is done. visible starts true with
`appear`, because the panel lives inside that v-if and would not exist to measure
otherwise. The origin is measured with offsetLeft/offsetTop rather than
getBoundingClientRect — enter-from has already applied scale(0.94) by then, so
the bounding rect is of the shrunken panel and the origin would land off by a few
pixels. Offsets are layout geometry and ignore transforms.

Durations are 140-220ms. The brief is continuity, so a card should read as having
moved, not as having performed.

NOT verified: motion is a visual property and there is no frontend test lane, no
device, and no app run here. vue-tsc proves it compiles. Whether it FEELS right
is an operator live pass, which is what M7's own verification section asks for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:55:21 -04:00
2026-07-19 12:34:56 -04:00

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_URL is required. THOUGHTSYNC_SECRET_KEY is optional; if unset, a signing key is generated and persisted in the database (sessions survive restarts).
  • Uploaded images live under the thoughtsync-data volume at /var/thoughtsync.
  • The app waits for the database and runs migrations (alembic upgrade head) automatically on start.
  • Image tags: :latest (stable, built from main) · :dev (latest dev build) · :<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) — plain http://<host>:5000 won'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_URL is 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).

S
Description
Self-hosted personal thought-capture web app (FabledSword family) — a Google-Keep-style masonry post-it board for fast idea capture, growing into a lightweight second brain. Quart + Vue 3 + PostgreSQL.
Readme
3.2 MiB
2026-08-23 16:50:53 -04:00
Languages
Python 26.3%
Rust 23.9%
Kotlin 22.7%
Vue 13.3%
TypeScript 6.6%
Other 7.2%