-
v9.4.0
Stablereleased this
2026-07-18 19:21:46 +00:00 | 35 commits to main since this releaseComposition Mode (#128)
A motif is now an authored artifact rather than a reverse-chron dump. Its owner can manually order the motif's entries and interleave short prose/heading blocks between them, so the public page and Markdown export read as an essay with evidence — setup → evidence → counter-evidence → conclusion.
- A new "Compose" mode on the motif page: drag-to-reorder (with ▲/▼ fallback for touch) plus add/edit/delete prose blocks.
- Entries and prose blocks share one ordered sequence (migration 056:
entry_motifs.position+ amotif_blockstable; prose sanitized on write). - The public page, the motif Atom feed, and the Markdown export all render
ORDER BY position NULLS LAST, created_at DESCand interleave blocks — so any motif you haven't curated renders exactly as before.
API suite 52 files / 373 tests · app suite 14 files / 76 tests · builds green.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v9.3.0
Stablereleased this
2026-07-18 17:11:27 +00:00 | 40 commits to main since this releaseThree self-contained v10-backlog research-workflow utilities.
OCR — image text into searchable content (#118)
On image upload the configured vision model also transcribes visible text (lyrics, captions, on-screen text). It's offered on the media card — "Show extracted text (OCR)" → "Add transcribed text to entry" — and only appended to the entry's content on explicit accept (sanitized on write), at which point it's full-text searchable. Nothing is applied automatically; no-ops when AI isn't configured. Migration 055 adds
media.ocr_text.Mastodon + Instagram import adapters (#121)
Two new archive parsers behind the existing adapter interface: Mastodon
outbox.json(ActivityPub — original posts only, boosts skipped, HTML stripped) and Instagram posts JSON (caption +creation_timestamp), each mapped to entries with the rightsource/source_iddedup shape. New/api/import/{mastodon,instagram}endpoints + Import-page cards.Offline read cache (#122)
Recently-viewed entries and motifs are now readable offline via an app-managed, user-scoped IndexedDB cache (separate from the capture queue). When a fetch fails offline the detail page renders the cached copy with a banner; the cache clears on logout. This does not relax the invariant that the service worker never caches
/api/*.
API suite 51 files / 368 tests · app suite 14 files / 76 tests · builds green.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
Source code (ZIP)
-
v9.2.0
Stablereleased this
2026-07-18 16:34:21 +00:00 | 47 commits to main since this releaseTwo more v10-backlog research-workflow enhancements.
Works Registry (#127)
The studied media — a music video, film, or album — is now a first-class object, so an observation can be about a work, not merely from a source. A project-scoped, soft-deletable
worksregistry (migration 054) with full CRUD in Settings → Works (title, artist, release date, canonical URL). Assign works to an entry inline on its detail page; filter Entries and Timeline by work to pull up every observation on a given video.Deferred follow-ups (tracked on #127): release-date pivot on Timeline, works-as-sections in Markdown export / public motif pages, and bootstrap-by-clustering existing source URLs.
Path Finder (#129)
"How are these two observations related?" Pick any two entries (or an entry and a motif) and get the shortest chain linking them — through typed connections and shared motifs — rendered as a graph on the Insights page, with typed edge labels and clickable nodes. A bounded bipartite BFS (
GET /api/connections/path) treats motifs as nodes so a hub motif can't explode the search, is depth-capped, and is scoped to the active project. "No path found" is an explicit outcome.
Full API suite: 50 files / 353 tests green. Also fixed a test-isolation bug (
clearEntryDatadidn't truncateworks/entry_works).Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
Source code (ZIP)
-
v7.3.0
Stablereleased this
2026-07-17 23:28:22 +00:00 | 92 commits to main since this releaseTea Leaves v7.3.0 — UI, mobile & accessibility
Released 2026-07-17
The user-facing rough edges from the 2026-07-15 audit — silent failures, dead affordances, and accessibility gaps on the surfaces people actually touch. Twelve issues (#105–#116).
✨ Highlights
- No more silent failures. Failed entry/motif deletes, removes, and connection deletes now surface errors (#106); a failing search shows a distinct "Search failed" state instead of the empty prompt (#107); and an offline capture the server rejects is flagged with its reason and offered Retry/Discard instead of retrying forever invisibly (#108).
- Entries browse past 50 (#105). A "Load more" pager reaches a project's full history, not just the first page.
- Public pages degrade gracefully (#109). A render error on a shared/embedded page shows the recoverable fallback instead of a blank screen.
- QuickCapture image drop actually attaches (#110) — real upload, not placeholder text — and the PWA "New Entry" shortcut opens a working create form (#111).
- Accessibility pass (#112–#114). ≥44px touch targets on capture and graph controls;
role="dialog"+ focus trap + aria-labels on the modals; and a WCAG-AA contrast sweep of muted text across the app. - Safer defederation (#115). The kill-switch now shows a non-mutating impact preview (editors/contributions/entries affected) in an in-app type-to-confirm dialog, replacing the stacked native
confirm()/alert().
Upgrading
No migrations, no config changes. Hosts on the image-based stack update automatically — this tag publishes the
:stableimages that prod follows.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v7.2.1
Stablereleased this
2026-07-17 21:38:32 +00:00 | 96 commits to main since this releaseTea Leaves v7.2.1
Released 2026-07-17
A small patch — the first release cut through the new auto-deploy pipeline (#134).
✨ Changes
- Image-dedup upload warning (#136 follow-up). After v7.2.0 gave the API real perceptual image dedup, the media-attachment UI now surfaces it: uploading a near-duplicate shows a dismissible "same file / looks similar to an image you already have" banner linking to the existing entry. Non-destructive — the upload still succeeds.
- Auto-deploy (#134). CI now publishes channel-tagged images to the Forgejo registry (
main→:beta,vX.Y.Z→:stable), and both environments run them under a per-stack, label-scoped Watchtower that auto-redeploys on a new image — dev follows:beta(tracks main), prod follows:stable(release tags). The API self-migrates on boot, so an image swap migrates itself. Seedeploy/.
Upgrading: no migrations, no config changes. Hosts on the image-based stack update automatically; this is the release that produced the
:stableimages.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v7.2.0
Stablereleased this
2026-07-17 16:37:36 +00:00 | 100 commits to main since this releaseTea Leaves v7.2.0 — Reliability, Data Integrity & Docs
Released 2026-07-17
The Medium/Low remainder of the 2026-07-15 codebase audit — the things that rot quietly rather than break loudly — plus two bonus features and a long-standing CI flake finally root-caused. All 13 milestone issues, no breaking changes. Migrations
044–046(additive;046renames one column).🛡️ Data integrity
- Deleting a user no longer strips attachments off the entries it preserves (#98).
media.user_idwas stillON DELETE CASCADEwhileentries.user_idisSET NULL— so a departed collaborator's media was hard-deleted (and files orphaned on disk) even on entries the schema intends to keep. Migration044makes itSET NULL;entry_idremains the lifecycle anchor. - Soft-deleted content no longer leaks (#93, #94). The legacy motif share query served trashed motifs and entries; export leaked trashed rows across four queries (including one that embedded a trashed entry's content via a connection to a live one). CSV export cells starting
= + - @are now neutralised against spreadsheet formula injection. - Federation optimistic-lock TOCTOU (#96). The lock read happened on the pool before the write transaction, so a concurrent local edit could be silently clobbered by a stale remote update. Now
SELECT … FOR UPDATEinside the transaction; regression-tested with a real concurrent-edit race. - Media access is project-scoped, not uploader-scoped (#135). Every media route keyed off
media.user_id, so a collaborator's uploads were invisible to the project owner today, and (post-#98) a deleted uploader's preserved media was unreachable. Reads now scope to the entry's project; delete is stricter (uploader or project owner only). Multi-user tested.
⚙️ Reliability & ops
/healthprobes its dependencies (#97). It returnedokthe moment the event loop was alive; now it runsSELECT 1+ RedisPING(each bounded) and returns 503 when either is down. Added a containerHEALTHCHECKand gatedapponservice_healthy. (Note: Docker Compose doesn't restart on healthcheck failure — that's tracked in #137 → monitoring.)- CI builds the production images (#103) and asserts the migrations copy landed — a Dockerfile/peer-dep regression can no longer ship unverified.
- Bounded Redis job retention (#100), top-level crash handlers (#101), a project-scoped entry-list index (#95), and the AP signature freshness window widened 30 s → 5 min (#102) so clock-skewed federated peers stop being rejected as invalid.
✨ Features
- Semantic dedup Pass 2 is real (#99). It was documented as active but never existed — the duplicate-review queue could never fill. Now cosine similarity over
text_embeddingflags near-duplicate imports, with imports embedding inline (one AI call per post) and a dedicatedimport.dedupThreshold(default 0.95) so a themed archive doesn't flag itself. - Real perceptual image dedup (#136).
media.phashheld a SHA-256 of the bytes (matches only byte-identical files) under a misleading name. Renamed tocontent_sha256; added a 64-bit dHash so re-encoded/resized copies are caught. Threshold set from measurement — re-encodes 0–7 bits apart, distinct images ≥24, default 10 in the gap.
📚 Docs
- README documents v6.2 and v7 (#104) — it trailed the code by a major version; the federation trust model is now written down for a new operator. Categories/Insights added; two further stale claims fixed.
🧪 Internal
API suite grew to 268 tests (from 232). And the
API — typecheck & testsCI flake is root-caused and fixed: with no AI configured the app defaults to ollama atlocalhost:11434, so every entry-write test fired a fire-and-forget embedding fetch that piled up inside CI's Docker-in-Docker and starved the pg pool, pushinganalytics.test.tspast its 15 s timeout.test/setup.tsnow takes the suite off the network for AI. It was never the runner.Upgrading: migrations
044–046apply on boot; no config changes.046renamesmedia.phash→media.content_sha256and addsmedia.dhash.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Deleting a user no longer strips attachments off the entries it preserves (#98).
-
v7.1.0
Stablereleased this
2026-07-17 01:00:38 +00:00 | 109 commits to main since this releaseTea Leaves v7.1.0 — Core Hardening & Security
Released 2026-07-16
A pre-v8 hardening pass: fix the daily-use and recovery paths that were quietly broken, and lock down + test the v7 federation trust boundary before stacking v8's moderation layer on top of it. All 13 issues of the milestone, from the 2026-07-15 codebase audit. No new migrations; no breaking changes.
🔒 Security
AP inbox: signer bound to the claimed actor (#82)
verifyInboxSignatureonly proved the request was signed by thekeyIdowner — for every activity type except the entry-write path it never asserted that owner ==activity.actor. Anyone controlling a domain could sign a request while claiming to be an arbitrary third party: spoofed fediverse identities in public motif comments, forced Follow rows, a forged Delete/Undo. The binding is now hoisted to run for all activity types (Follow / Invite / Undo / Like / Announce / Create / Delete) immediately after signature verification →401on mismatch.AP inbox: SSRF guard on remote-controlled fetches (#81)
The unauthenticated inbox fetched attacker-supplied actor URLs and delivered to attacker-supplied inbox URLs with no host filtering, following redirects — blind SSRF into the homelab. New
lib/ssrfGuard.tsresolves the target and rejects loopback / RFC1918 / link-local / ULA / CGNAT, re-validating every redirect hop. Origins an admin has explicitly federated with bypass the denylist, so two homelab instances still federate over a private network (mirrors the #62 outbound allowlist).Backup routes are admin-only (#80)
/api/backup/*sat behind onlyrequireAuth, so any authenticated non-admin (e.g. a project collaborator) could read cloud-storage credentials, manage destinations and stage restores. NowrequireAdmin, andGET /destinationsreturns only the names ofextra_envkeys — never the values (editing is write-only).🛟 Recovery & reliability
Backup restore actually works (#84)
applyRestoreEntryomittedproject_id(NOT NULL →23502) and defaultedcertaintyto'possible', which isn't in the CHECK set (→23514). Every staged entry apply threw — at precisely the moment recovery is needed. Now sourcesproject_idfrom the staged data (falling back to the active project) and validatescertainty, covered end-to-end by an integration test.Graceful shutdown (#85)
Nothing handled SIGTERM, so every deploy hard-killed in-flight import/media/backup jobs, left BullMQ locks lingering, and could strand a backup at
status='running'forever. The API now drains workers, then closes the server, Redis and the pg pool, with a 30 s cap. The handler alone was inert:CMD ["npm", "start"]meant npm — not node — received the signal, so it never ran (exit 1,npm error signal SIGTERM). The image now execs node directly and compose runs tini as PID 1, which also reaps orphaned restic/pg_dump/ffmpeg children.Backups can't hang forever (#86)
restic/pg_dumpspawns had no timeout and noerrorhandler — an unreachable repo or a missing binary left the promise pending forever, silently blocking every future scheduled backup while the dashboard cheerfully showed "running" (so no alert fired). Spawns now have a 2 h kill-timer and surface spawn errors; a reconciliation pass fails any job stillrunningafter 3 h and fires the configured alert.Migrations run before workers (#87)
Workers and the backup scheduler started before
runMigrations(), and the scheduler's error was swallowed — on a first-ever boot backup scheduling silently never registered. Boot order is now migrate → workers → scheduler → listen.📱 Mobile daily-use
Quick Capture no longer throws away voice notes (#90)
Worse than reported: the recorder's target entry was always
nullwhile the panel was open, so every Quick Capture voice note was silently discarded on Stop — the feature had no working path at all. Tapping Record now creates the entry up-front so audio always has somewhere to land, and Save updates that entry rather than duplicating it.Import, Suggestions & Trash reachable on a phone (#89)
All three lived only in the desktop header — on <768px no on-screen control reached them. The bottom nav now ends in a More overflow sheet (Import, Suggestions, Trash, Settings — plus Admin/Federation for admins).
Deep links survive login (#91)
A logged-out user clicking an
/invites/:tokenlink landed on/entrieswith the token gone and no message. The target path now round-trips through the OIDC flow, validated as a same-origin relative path on the way in and out (absolute,//evil.comand/\evil.comrejected) so it can't become an open redirect.🐛 Ops / UI
- Uploads & imports over 1 MB (#83) — the bundled nginx set no
client_max_body_size, so its 1 MB default 413'd essentially every image, video and archive import before it reached the API. Now2gon the/api/block. - PWA install & favicon (#88) — every icon referenced by
index.html/manifest.json404'd; the install prompt and tab icon were broken. Addedfavicon.svg,apple-touch-icon.pngand the 192/512 maskable PNGs.
🧪 Internal
API suite grew to 238 tests (from 232): direct HTTP-signature verification tests (valid / replayed / tampered-digest / unsigned-digest / wrong-key / wrong-actor binding end-to-end), SSRF IP-classification tests, backup authorization + restore-apply coverage, and the open-redirect guard matrix — closing the audit's "the newest perimeter is the least tested" finding (#92).
Upgrading: no migrations and no config changes required. Deploys pick up
init: true/stop_grace_periodfromdocker-compose.yml, which the graceful shutdown depends on — redeploy with the compose file from this tag rather than an older local copy.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Uploads & imports over 1 MB (#83) — the bundled nginx set no
-
v7.0.0
Stablereleased this
2026-06-30 15:19:31 +00:00 | 130 commits to main since this releaseTea Leaves v7.0.0 — Cross-Instance Collaborative Editing
Released 2026-06-30
Extends the read-only federation layer into write-capable cross-instance collaboration: an invited researcher on another Tea Leaves instance can create and update entries in a host project over a signed ActivityPub channel, with a trust chain, conflict review, and an admin kill-switch. Migrations
041–043(additive).✨ Features
🤝 Cross-instance write path (#12 / #9 / #10 / #11)
- Invite a remote editor by their ActivityPub actor URL (
POST /api/projects/:id/invite-remote); their instance must be federated + enabled. - Signed write path — a
Create/Updateof aNoteaddressed to a project's AP context becomes an entry write. The signature is verified against the actor's cached key and bound to the activity'sactor(no actor spoofing); only accepted remote editors may write (no open inbox). Remote-authored entries carry attribution snapshotted from the actor. - Conflict resolution — optimistic lock on
updated_atvs the activity'spublished: non-conflicting writes apply automatically (last-write-wins), conflicts land in an owner review queue (apply / keep mine). - Schema:
project_collaborators.remote_actor_url+remote_contributions(migration041).
↔️ Editor-side round-trip (#62)
The other half: an invited editor's instance learns of the invite via a signed AP Invite (a new Shared with me page), signed-fetches the host project's entries (
GET /ap/projects/:id), and delivers signed Create/Update edits from a remote-project view — completing create and update end-to-end across instances. (migrations042–043)🛑 Admin defederation kill-switch (#61)
Sever a malicious or compromised instance: a blocked state that hard-rejects its inbound activity before signature processing, revokes its remote editors, quarantines its pending/conflict contributions, and optionally purges the entries its actors authored.
🔒 Security
Two hardenings caught by automated review during the build:
- Inbound signatures must now cover
(request-target)/date/digestwith the digest verified unconditionally — closes a replay / body-substitution actor-spoofing gap (also hardens the existing Follow/comment inbox). - Editor egress (Invite-supplied host URLs) is restricted to admin-federated origins — an SSRF guard.
🧪 Internal
API test suite grew to 218 tests (HTTP-signature harnesses for the signed read/write paths). No breaking changes; migrations
041–043apply on startup.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Invite a remote editor by their ActivityPub actor URL (
-
v6.2.0
Stablereleased this
2026-06-29 17:56:19 +00:00 | 158 commits to main since this releaseTea Leaves v6.2.0 — Core Improvements
Released 2026-06-29
The v6.2 milestone: a new project-scoped taxonomy, an analytics/visualization layer, and a clean split between workspace settings and instance administration. No breaking changes. One additive migration (
040_categories).✨ Features
🏷️ Categories for entries & motifs (#6)
A per-project taxonomy shared by entries and motifs — distinct from freeform tags: categories are project-scoped, curated, and carry an optional color. Full CRUD at
/api/categories, assignment on entry/motif forms via a create-on-the-fly picker, colored contrast-aware badges throughout, andcategory_idfiltering on the entry list, motif list, and timeline. Manage them in Settings → Categories. (migration040_categories)📊 Insights — analytics & data visualization (#7)
A new Insights page surfaces project-scoped analytics computed live from the active project: headline counts, monthly entry volume, certainty breakdown, motif-prevalence-over-time, and a force-directed motif co-occurrence web (motifs that share entries, weighted by overlap). Rendered with
recharts. Three read-only endpoints under/api/analytics(overview,motif-prevalence,network).⚙️ Settings / Admin redesign (#8)
The single sprawling Settings page mixed per-workspace controls with server-wide instance configuration. They're now cleanly split by audience:
- Admin (admin-only) gains AI, AI tools, Search, Media, Email (SMTP), Backup, and the Federation kill-switch, alongside Version / Storage quotas / Users / Audit Log.
- Settings keeps the workspace controls every user needs: Projects, Project templates, Tags, Categories, Integrations, Export.
- Security:
/api/settings(andtest-email) are now gated to admins — every key in the settings table is server-wide config.
🐛 Fixes
- Editing a Markdown entry no longer blanks the app (#52) — replaced the unmaintained
tiptap-markdownwith the official version-matched@tiptap/markdown, and added a top-levelErrorBoundaryso a future render error degrades gracefully instead of whiting out the page.
🧪 Internal
Frontend at 63 tests, API at 175 tests — including a non-admin
403regression test for the newly-gated settings routes.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v6.1.0
Stablereleased this
2026-06-25 23:31:33 +00:00 | 180 commits to main since this releaseTea Leaves v6.1.0 — Core Polish & Fixes
Released 2026-06-25
A small, focused release of self-contained core-app fixes (milestone v6.1) to land before the larger v6.2 work — plus a substantial round of frontend test coverage. No migration, no breaking changes.
✨ Fixes & improvements
🔗 Clickable motifs from an entry (#3)
On an entry, the linked motifs in the "Motifs" section rendered each title as inert text — there was no way to jump from an entry to a motif. Each linked motif is now a proper link to its detail page, with a hover affordance.
🌙 Readable motif graph in dark mode (#5)
The motif graph used hardcoded light node backgrounds, borders, and background dots, and its chrome (controls, minimap, edges) had no dark theme — leaving it illegible on a dark page. The graph now follows the app theme: node and background colors are themed for dark mode, driven by a new reactive
useIsDarkMode()hook.📄 Export all motifs as one Markdown document (#4)
A new
motifs.mdexport combines every motif into a single shareable Markdown document, with---separators under a document header — reusing the existing per-motif formatter. Available in Settings → Export → All motifs, beside the existingmotifs.json.🧪 Internal
Frontend test coverage. The app went from a single smoke test to 59 tests, covering the API wrapper, the sanitize/shortcuts utilities, the Tiptap editor, the auth/theme/offline-queue hooks, and the entry list/detail pages — plus a regression test for each fix above.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
Source code (ZIP)