535331c5b2b4f0a96589930dd194041375156322
330
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
535331c5b2 |
tests: one fixture for the note grammar, run by the core, the server and the web
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / integration (push) Failing after 27s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 1s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Failing after 12s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 4m25s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m28s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m19s
Desktop (Tauri) / Update manifest (push) Successful in 9s
Android / Kotlin + Rust (APK) (push) Canceled after 11m21s
The checklist grammar and the #tag rule are implemented three times (derive.rs, checklist.py/tags.py, markdown.ts), and the tag colour twice (colors.ts, DerivedTint.kt). Only Rust and Kotlin had tests. core/testdata/grammar.json now holds one set of cases (task lines, rendered items, tags, standalone-tag lifts and the tint hashes), and every suite reads it. - web: vitest, a dev dependency approved for #5166, with `npm test`. grammar.test.ts runs the fixture, and titles.test.ts pins #5165's palette fix. - ci.yml runs the web tests in the job the image build needs. desktop.yml's verify job runs them too, because the installers embed this frontend and can't see ci.yml's verdict (rule 177). - core: derive.rs reads the fixture. server: tests/test_grammar_fixture.py. - Android keeps its hand-written tint values; its doc now points at the fixture. The server is expected red here, on purpose. tags.py only takes a tag after whitespace and lets it start with a digit or `_`, while the core (the definition) takes any non-tag boundary and needs a letter. So `(#todo)` is a label on the phone and plain text on the server. The fix follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
af0389ed13 |
web: the command palette finds notes written since it was first opened
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
CI & Build / Python tests (push) Successful in 16s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / TypeScript typecheck (push) Successful in 11s
CI & Build / Python lint (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / integration (push) Successful in 41s
CI & Build / Build & push image (push) Successful in 58s
Desktop (Tauri) / Clippy, tests and rustfmt (push) Successful in 3m30s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m43s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m23s
Desktop (Tauri) / Update manifest (push) Successful in 4s
The palette's note list loaded once per session behind a `loaded` flag, and the `reload()` that would have cleared it had no caller. So a note written after the first open couldn't be found by name until the page reloaded (#5165, audit B4). The list is now fetched again on every open, and the last list stays visible meanwhile. The input takes focus before the fetch, and a failed fetch keeps the old list instead of breaking the palette. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8db9f3685b |
server: one save path for a note's text, so a restored link gets its preview
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / TypeScript typecheck (push) Successful in 11s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Clippy, tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Python tests (push) Successful in 16s
CI & Build / integration (push) Successful in 41s
CI & Build / Build & push image (push) Successful in 46s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
A body edit is a sequence: keep a revision, rename the note, lift #tags,
commit, queue link previews. It was written out in PATCH, the item routes,
restore and sync push, and the copies had drifted. Now they all call
`notes/body.py: write_body`, which says how the old text is kept ("session",
"always" for restore, "never" for a new note) and returns whether the text
changed. The routes commit through `_commit_note`, which queues previews after
the commit.
Fixes, both red on
|
||
|
|
86409e02b0 |
ci: drop the deliberate failure — the gate held on run 8437
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 12s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 14s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 7s
CI & Build / integration (push) Failing after 49s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Clippy, tests and rustfmt (push) Successful in 2m26s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m54s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m52s
Desktop (Tauri) / Update manifest (push) Successful in 7s
Run 8437 failed `verify` on purpose, and the Linux build, the Windows installer and the update manifest all reported skipped. This removes the red step, so this push is the other direction: a green verify still publishes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1a2f71e381 |
tests: every note-text write path is pinned, and two of them fail today
Integration tests for the edit sequence at each door: create, PATCH, ticking an item, restoring a revision and sync push. Two fail on today's code, on purpose: - restoring a revision never queues link previews, so a restored link stays a bare URL (#5164, audit B5); - a pushed note whose standalone #tag gets lifted stores the server's clock as its edit time instead of the client's, because the lift's second flush lets the column's onupdate overwrite it. The fix follows in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4d61b34b85 |
ci: the Windows installer waits for the Rust checks, like the Linux bundles do
Android / Build, or is the channel already serving this? (push) Successful in 2s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / TypeScript typecheck (push) Successful in 12s
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Successful in 33s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python tests (push) Successful in 17s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Build & push image (push) Successful in 33s
Desktop (Tauri) / Clippy, tests and rustfmt (push) Failing after 2m58s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
Clippy, the workspace tests and rustfmt move out of the Linux `build` job into their own `verify` job, and both publishing jobs need it. Before this, `windows` needed only `decide`, so on run 8411 a red clippy stopped the Linux lane while the Windows installer built and published to dev-rolling (#5184, rule 177). This commit also carries a deliberately failing step at the end of `verify`. It is the red half of the proof: both publishers must report `skipped`. The next commit removes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
be200641cd |
core: notes show their real created and edited times, and desktop search works
CI & Build / Build now, or wait for Android? (push) Canceled after 0s
CI & Build / TypeScript typecheck (push) Canceled after 0s
CI & Build / Python lint (push) Canceled after 0s
CI & Build / Python tests (push) Canceled after 0s
CI & Build / integration (push) Canceled after 0s
CI & Build / Build & push image (push) Canceled after 0s
Android / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 4m26s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 7m41s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Android / Kotlin + Rust (APK) (push) Successful in 10m41s
Both bugs were caught red by the tests in |
||
|
|
732fd7a827 |
core: the store tests pass clippy and rustfmt, so they reach the test step
CI & Build / Build now, or wait for Android? (push) Canceled after 0s
CI & Build / TypeScript typecheck (push) Canceled after 0s
CI & Build / Python lint (push) Canceled after 0s
CI & Build / Python tests (push) Canceled after 0s
CI & Build / integration (push) Canceled after 0s
CI & Build / Build & push image (push) Canceled after 0s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 3m48s
Desktop (Tauri) / Update manifest (push) Canceled after 0s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Canceled after 3m55s
Android / Kotlin + Rust (APK) (push) Canceled after 4m29s
|
||
|
|
7bc8e04518 |
core: the local store has tests, and two of them fail on today's code
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 8s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 33s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 1m45s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m46s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Canceled after 5m43s
store.rs had none, which is how two bugs reached desktop and Android unseen. These exercise every board facet, the timestamps, tags, revisions, items, trash, reminders and label merges against a real migrated schema. Two are expected RED on this commit, on purpose, so CI shows they catch what they were written for: - each_timestamp_comes_from_its_own_column / a_new_note_carries_both_timestamps: load_note reads created_at and updated_at one column too far right since |
||
|
|
c5f93cf9f1 |
rename: the sign-in screen shows Inkwell's mark, and every tab says Inkwell
Android / Build, or is the channel already serving this? (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python tests (push) Successful in 12s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / integration (push) Successful in 43s
CI & Build / TypeScript typecheck (push) Successful in 12s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build & push image (push) Successful in 1m0s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m28s
Desktop (Tauri) / Update manifest (push) Successful in 10s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 6m22s
The login and register screens still drew a hard-coded "TS" tile. They now use /icon.svg, the same mark the shell's header shows. Browser tabs took index.html's static <title> and never changed it, so every tab read the same, and some browsers showed the URL instead. usePageTitle, mounted once in App.vue, sets "<page> · <site name>". Routes outside the shell name themselves with meta.title. Board lenses use the lens name the header already shows, now in useLensName so the tab and the header read from one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6d082b2ad8 |
rename: the docs say Inkwell, and nothing else still says ThoughtSync by accident
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 16s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 4s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python tests (push) Successful in 17s
CI & Build / Build & push image (push) Skipped
CI & Build / integration (push) Successful in 50s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m14s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 6m2s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 8m36s
Step 6 of milestone 481. README, docs/*, ci-requirements.md, the desktop and Arch READMEs, alembic.ini, .gitignore, the frontend package name, the service worker's cache name (its activate handler deletes any cache by another name, so the old one is cleaned up), and the Android names in the release body. What still says thoughtsync does so on purpose (Scribe note 5071): - the desktop data crossover (crossover.rs) and its startup log - the old-export import marker - the "Upgrading from ThoughtSync" block in .env.example, and compose's pointer to it - the packages being retired: deb conflicts/replaces thought-sync, pacman thoughtsync and thoughtsync-desktop - the Android signing keyAlias, which names a key in the existing keystore - history: shipped alembic migrations, and the test-binary hashes that ci-requirements.md records from 2026-08-18 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
81cd719327 |
rename: Android is Inkwell — package, applicationId, uniffi class, assets
Step 4 of milestone 481 (Scribe note 5071: a full rename). - namespace and applicationId com.fabledsword.inkwell; the Kotlin package moves with them, and ktlint re-sorted the imports the rename reordered (checked locally with CI's ktlint 1.4.0 and detekt 1.23.7, both clean) - uniffi: class Inkwell in com.fabledsword.inkwell.core, InkwellApplication, InkwellTheme, Theme.Inkwell, log tags, prefs and work names, client agent inkwell-android - the lane publishes inkwell.apk / inkwell-android.json; fetch-clients, guard-forward, publish-release and write-manifest read the same names A new applicationId is a new app. The old ThoughtSync app keeps its own store and stays installed beside it. Notes cross over by syncing, and the old app is then removed by hand. Kept: the signing keyAlias is still "thoughtsync". It names the key inside the existing keystore, and the key, and so the certificate, are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
256fba3610 |
icon: Inkwell's mark is a black inkpot and quill on the brand yellow
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 8s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 43s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 4m25s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 7m13s
Desktop (Tauri) / Update manifest (push) Successful in 8s
Android / Kotlin + Rust (APK) (push) Canceled after 11m42s
Step 5 of milestone 481. Replaces the linked-notes constellation, which had been stale since note links were dropped (alembic 0024). The colour scheme stays. packaging/icons.py draws the mark once and renders every variant from it: the rounded tile (web, desktop), the maskable full-bleed web icon, and the Android adaptive foreground. The detail (shaft, vane splits, glint) is cut out of the ink with a mask rather than painted on in yellow, because the Android foreground is now transparent and its alpha is also the themed-icon silhouette. The old foreground was the opaque maskable tile, which a themed icon would have drawn as a solid square. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fe6f0746b2 |
rename: the desktop is Inkwell — crates, Tauri identity, data move, packaging
Step 3 of milestone 481 (Scribe note 5071: a full rename).
- crates thoughtsync-{core,desktop,ffi,uniffi-bindgen} → inkwell-*, the
Cargo.lock entries moved to match (checked with `cargo metadata --locked`)
- Tauri: productName "Inkwell", identifier com.fabledsword.inkwell, binary
`inkwell`, updater feed on bvandeusen/inkwell, store file inkwell.db
- client agent inkwell-desktop, headers X-Inkwell-Client/-Protocol (the server
reads neither), capture event inkwell://captured, display-version env
- .deb: conflicts + replaces thought-sync, so the updater's install retires the
old package instead of colliding on it. kebab-case("Inkwell") is `inkwell`, so
the package name finally matches the command and verify.sh now asserts it
- pacman: inkwell, conflicting with and replacing thoughtsync and
thoughtsync-desktop
- AppImage ~/Applications/Inkwell.AppImage, menu entry inkwell.desktop,
installer, release titles, desktop asset names in fetch-clients.sh
The one shim, chosen by the operator because it is the only copy of a
local-first user's notes: crossover.rs moves the old
com.fabledsword.thoughtsync app-data dir's contents into the new one on startup,
before the store opens, renaming thoughtsync.db and its -wal/-shm with it. It
skips when the new dir already has a store, and anything already in the new dir
wins (the installer writes its channel marker there first). Tested.
Android's Kotlin side (package, applicationId, uniffi class) is step 4. Its
release asset names stay thoughtsync.* until then.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
a706644455 |
rename: the server is Inkwell — package, env vars, image, compose, export marker
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 7s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python tests (push) Successful in 15s
CI & Build / integration (push) Successful in 45s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 4m17s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 7m33s
Desktop (Tauri) / Update manifest (push) Successful in 7s
Android / Kotlin + Rust (APK) (push) Successful in 11m25s
Step 2 of milestone 481. The operator chose a full rename (Scribe note 5071), so this goes past the display strings into the identities: - src/thoughtsync → src/inkwell; every import, the Dockerfile and both compose commands, alembic env, pyproject - THOUGHTSYNC_* → INKWELL_* (database URL, secret key, log level, tag/port/bind) - container data dir /var/thoughtsync → /var/inkwell - image git.fabledsword.com/bvandeusen/inkwell; Postgres user/db default inkwell; CI's integration service follows - the files the image serves are inkwell.*. fetch-clients.sh still fetches the thoughtsync-named release assets, because the lanes that publish them are renamed in steps 3 and 4 - exports are written with app "inkwell" Two deliberate exceptions, both because data rides on them: - compose volumes are now named explicitly and overridable (INKWELL_DB_VOLUME, INKWELL_DATA_VOLUME), so a deployment installed as ThoughtSync points at the volumes and DB identity it already has. .env.example says exactly what to set - import still accepts app "thoughtsync", because exports written before the rename are backups. Tested both ways Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f806e35d41 |
rename: the apps say Inkwell — web, desktop, Android and server strings
Android / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 11s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / integration (push) Successful in 58s
CI & Build / Build & push image (push) Skipped
CI & Build / Python tests (push) Successful in 15s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 4m17s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 7m50s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Android / Kotlin + Rust (APK) (push) Successful in 11m37s
ThoughtSync is renamed Inkwell ("Fabled Inkwell" in full; Scribe note 5071).
This is step 1 of milestone 481: every string a person reads in the running
apps. Identities installed clients depend on are deliberately untouched — the
Tauri productName (it derives the .deb Package: field), identifier and binary
name, applicationId, X-ThoughtSync-* headers, the export's app marker, env vars,
module and crate names.
- web: title, PWA manifest (name "Fabled Inkwell", short_name "Inkwell"),
offline page, icon labels, build labels, prompts, notification title
- server: site_name default, import error, link-preview User-Agent
- 0030: a stored site_name of exactly the old default follows the rename. The
Settings page saves every key, so most servers hold "ThoughtSync" without an
admin ever having chosen it; a name they typed is left alone
- desktop: window title, default device name, local-mode site name, log line
- android: app_name and the strings that name the app
- core: probe and compatibility messages
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
fb469ed73a |
update: wrap the dev-rolling feed assertion the way rustfmt wants
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 50s
CI & Build / Build & push image (push) Successful in 32s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m55s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m14s
Desktop (Tauri) / Update manifest (push) Successful in 4s
|
||
|
|
72968897ab |
channels: the dev channel publishes on dev-rolling, so its tag stops shadowing the branch
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python tests (push) Successful in 10s
CI & Build / integration (push) Successful in 44s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 3m20s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m20s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 9m13s
The rolling dev release lived on a tag named `dev`, beside the branch named `dev`. Once a clone had fetched tags, `git push origin dev` failed with "src refspec dev matches more than one" (Scribe #2184, note #3042), and every session had to know to spell out refs/heads/dev. The channel is still `dev` everywhere a person sees it: the app's setting, `install.sh --channel dev`, the stored pref. Only the release tag moves, to `dev-rolling`, matching roundtable-android. `stable` has no branch to collide with and keeps its name. - packaging/channel-tag.sh is the one channel -> tag mapping CI reads: the publish steps in android.yml and desktop.yml, the manifest job, fetch-clients.sh and guard-forward.sh. guard-forward exits 2 on an unmapped channel instead of fetching an empty URL and passing. - update.rs and install.sh carry their own copy because neither can run it; update.rs gains a test that no channel feed is named like a branch. - tests/test_channel_tag.py runs the script: no tag is a branch name, dev is exactly dev-rolling, an unknown channel fails with no output. - publish-release.sh titles the release "ThoughtSync dev (rolling)", so the tag name does not leak into what people read. TEMPORARY bridge: desktop apps installed before this have .../download/dev/latest.json compiled in. The dev manifest job sets BRIDGE_TAG=dev, and write-manifest.sh writes the same latest.json to the old `dev` release. Its URLs name dev-rolling assets, so those apps update once into a build that reads the new tag. The bridge, and the old release and tag, are removed once installed apps have crossed over. Until then the push still needs the explicit refspec, as ci-requirements.md now says. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DwoKYuw3qJmUUYsJeNherB |
||
|
|
53d51ce01c |
ci: artifact uploads move to stock upload-artifact@v7
CI & Build / Python lint (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 45s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m11s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m33s
Desktop (Tauri) / Update manifest (push) Successful in 6s
Android / Kotlin + Rust (APK) (push) Successful in 8m45s
The Android APK upload and both desktop bundle uploads (Linux and Windows) went through the bvandeusen fork mirror, with comments saying stock upload-artifact throws GHESNotSupportedError on this hostname. That stopped being true when the runner moved to gitea/runner 3.x, which edits the refusal out of the action bundle; stock upload v4-v7 and download v4-v8 were proven on 2026-09-10 (Scribe spike #3843) and the same swap is verified on four other repos. Artifact names, paths, if-no-files-found: error and the no continue-on-error stance are unchanged. ci-requirements.md now says stock v7 and keeps what is still true: @v3 uploads are invisible. Scribe snippet #2271, milestone 395. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DwoKYuw3qJmUUYsJeNherB |
||
|
|
cf2854a029 |
ktlint: a multiline .border() left the next '.' orphaned, exactly as #3110 records
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
CI & Build / Python lint (push) Successful in 5s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / TypeScript typecheck (push) Successful in 8s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 25s
CI & Build / Build & push image (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 7m2s
`standard:chain-method-continuation` on `LinkPreviewRow.kt:83`. The `.border(…)` call took three arguments across four lines, and the `.padding(…)` after it then began a line with a `.` — which the rule only accepts glued to the closing paren, `).padding(…)`. Issue #3110 hit this same rule in `NoteCard.kt` and recorded the fix: do not write the multiline element. Naming `shape`, `padH` and `padV` first collapses `.border` back to one line and removes the duplicated RoundedCornerShape at the same time, which is better than what ktlint was willing to accept. Also did what #3110's verification note says to do rather than fixing only the line the linter named: scanned every Kotlin file this branch touched for the same shape — a multiline chain element followed by a `.` on a new line — and found no others. ktlint reports one violation and stops, so a second would have cost another full Android lane. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c |
||
|
|
62338bb0a4 |
android: a link in a note renders as a link card, not a bare URL
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / integration (push) Successful in 22s
CI & Build / Build & push image (push) Skipped
CI & Build / Python tests (push) Successful in 11s
Android / Kotlin + Rust (APK) (push) Failing after 3m33s
The web and desktop have shown link previews since #2898; the phone showed the raw address. The data was already on the device — `Note.previews` is populated by the core and carried through the FFI — and nothing under `app/src/main` read the field. The three presentation rules are copied from `NoteCard.vue` rather than re-decided, so the same note reads the same way on every surface: * A note that is NOTHING but a URL renders as its preview and nothing else. Printing the address under a card that already says where it goes is saying the same thing twice, badly. * Links mentioned INSIDE a note get a compact strip at the FOOT of the card. Above the body would put a stranger's headline where the note's first line should be; the web learned that in M13. * Several stack. `LONE_URL` mirrors the web's `LONE_URL_RE` including the tolerated whitespace — if the two regexes disagree, one note reads as a card here and a paragraph there. Falling back to the URL is deliberate in all three of the cases that produce no preview: not a lone URL, not unfurled yet, or never unfurlable. A note written on the phone and not yet synced is permanently in the middle one, because the unfurl is server-side (`unfurl_queue.py`) and arrives on a later pull — so that state has to look deliberate, and showing the link does. No unfurl fetch was added here, and none should be: a phone fetching OG tags would be a second SSRF-hardened fetcher on the surface least able to afford the call. ## No image, and that is a question rather than an omission `LinkPreview.image_url` is a REMOTE third-party address — the web renders it straight from whatever host the link points at. Matching that here would have this app fetch images from arbitrary hosts, on a phone, on possibly metered data, and would make it the first image loading anywhere in this client: there is no loader, no cache, and not one `Image(` in the whole app today. That is a decision about privacy and data use, not a rendering detail, so the text card ships and the image is asked about rather than assumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c |
||
|
|
1a41373347 |
board: a note trashed from search results now leaves the results
Search for something, long-press a hit, Move to trash: the snackbar said it happened and the card sat there until the query next ran. Reachable from the editor's overflow too — both go through `mutate`. `mutate` kept the existing list whenever a search was running, with the reasoning recorded in place: search results are the answer to a query, not a live view, and running the BOARD query underneath them would replace the hits with the whole board. That is right about the board query and wrong about the note. A hit that no longer matches has left the answer, not just moved within it — pinning one and watching it not re-sort is fine; trashing one and watching it stay is not. So the search is re-run instead of the destination loaded. The results are still the answer to the query, just a current one, and it costs one local SQLite query — the same argument the surrounding comment already makes for reloading the board. Creating a note while searching still leaves the list alone: a new note that does not match the query has no business appearing in its results. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c |
||
|
|
729d0dadf1 |
editor: collect the refund — the web editor autosaves on an idle pause
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / integration (push) Successful in 22s
CI & Build / Build & push image (push) Successful in 36s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 1m59s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m29s
Desktop (Tauri) / Update manifest (push) Successful in 5s
#2971's engine work was already done and its benefit was never taken up here. Both engines coalesce revision snapshots to one per editing session — `src/thoughtsync/revisions.py::should_snapshot` and `store.rs`'s namesake, the server's applied on the PATCH path AND in `sync.py`, with four integration tests covering it. So a write has cost a write, not a write plus a revision, for some time. But this editor still wrote only on `close()`. That save-on-close existed BECAUSE writes were expensive; with the reason gone, all that was left was the cost — a tab closed mid-paragraph lost the paragraph, which is the one thing a notes app must not do. Android already debounces (`BoardViewModel`); the shared Vue editor did not, so web and desktop kept paying for a trade that had been cancelled. Now: a 1s idle pause writes. EDIT MODE ONLY, deliberately. In compose, `dismiss` discards a note that was never persisted so an accidental keystroke or a type-to-compose never litters the board. An autosave there would create the row and quietly take that behaviour away. Materialising a compose on first keystroke is a separate decision (#2967), not a side effect of this one. Three details that decide whether it is safe rather than merely present: * `flush` returns without writing while a save is in flight, so an autosave landing there would silently drop everything typed since that save began. It RE-ARMS instead of skipping. * Errors are swallowed and retried on the next pause. An autosave that interrupts typing with a message is worse than one that waits, and `close` still surfaces a real failure where the person is looking. * The timer is cancelled by `close`, by `dismiss` and on unmount, so nothing fires through a component during its leave animation or after it is gone. Checked and found harmless rather than assumed: `notes.reconcile` replaces the store's item but never touches `useNoteEditor`'s `editing` ref, so the `watch(() => props.note)` that calls `setBody` does not fire on a save. Were that not true, autosaving would have reset the field and the caret every second. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c |
||
|
|
23a61365da |
capture: the suggested shortcut is a UI affordance, so it lives in the UI
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 21s
CI & Build / Build & push image (push) Successful in 17s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m26s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m31s
Desktop (Tauri) / Update manifest (push) Successful in 5s
`-D warnings` failed the Linux lane on `constant SUGGESTED is never used`, and it was right — the suggestion is implemented in `bridge.ts` as SUGGESTED_CAPTURE_SHORTCUT, and nothing in Rust ever read the copy here. Deleted rather than exposed through a command. This side accepts any combination the OS will take; picking one to put in front of someone as a starting point is a UI decision, and a constant here would only be a second copy of a string one layer reads and the other does not. Worth noting what this run DID prove, since the previous one proved nothing: the lockfile gate passed and the Windows job built the NSIS installer end to end. So `tauri-plugin-global-shortcut`'s handler signature — the thing I could not verify without a toolchain — is correct, and the feature compiles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c |
||
|
|
6c0153be1e |
desktop: a global hotkey opens a small window to write in, now with its lockfile
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 13s
CI & Build / integration (push) Successful in 29s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 1m5s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 1m52s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 7m22s
Restores |
||
|
|
10ea15bef0 |
Revert the desktop hotkey: a new crate needs a Cargo.lock this machine cannot write
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 10s
CI & Build / integration (push) Successful in 19s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Build & push image (push) Successful in 36s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 1m52s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m7s
Desktop (Tauri) / Update manifest (push) Successful in 4s
`42e06da` added `tauri-plugin-global-shortcut` to Cargo.toml without updating
Cargo.lock, and every cargo invocation in CI passes `--locked`. Both desktop
jobs failed on the same line before compiling anything:
error: cannot update the lock file ... because --locked was passed
So this says nothing about whether the code is right — clippy never ran. The
gate did exactly its job.
There is no Rust toolchain on this workstation (rule 10 — CI verifies), and a
lockfile is the one artifact CI is deliberately forbidden to generate. Hand-
writing the entries is not a real option: it needs the exact checksum and the
whole transitive tree, and a wrong checksum fails harder than a missing one.
Reverted rather than left red, because a red `dev` blocks everything behind it
and the Android half of #1899 is green and unaffected at
|
||
|
|
42e06da576 |
desktop: a global hotkey opens a small window to write in, and nothing else
Android / Build, or is the channel already serving this? (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Python tests (push) Successful in 13s
CI & Build / integration (push) Successful in 21s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 30s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
CI & Build / Python lint (push) Successful in 5s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 8s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Failing after 35s
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Build & push image (push) Successful in 35s
The other half of #1899. Press the combination anywhere and a 520x220 window arrives over whatever you were doing; type, Ctrl/Cmd+Enter, it is gone. The board never comes forward, which is the whole point — bringing the app up to write one line is the friction this removes. ## There is no default shortcut, deliberately A global shortcut is the one setting here that can collide with software this app knows nothing about. Any default is a key combination taken away from something on somebody's machine, silently, at install time. So the feature is OFF until a combination is chosen, and choosing one is how it turns on. CommandOrControl+Shift+N is offered as a one-click suggestion, never applied on the user's behalf. ## Stored and live are reported separately `CaptureShortcut` carries both `shortcut` and `registered`, because they genuinely disagree: a combination another app grabbed first is saved and does nothing when pressed, and on Wayland a compositor may refuse global grabs outright. Saying only "your shortcut is X" would be a lie with a keystroke attached, so the settings row says "saved but isn't active — something else is holding it". `capture_shortcut_set` registers BEFORE storing, so a combination the system refuses is never written down as though it worked. Registration at startup is best-effort and logged: a shortcut that worked when it was chosen can be taken by something installed later, and the app must still open. ## Two windows, one database, no shared store The capture window runs a second copy of the frontend with its own Pinia stores, so a note saved there is invisible to the board until it is told. It is told — `capture_done(saved)` emits to `main`, and BoardView reloads. The emit failing is cosmetic (the note is already in SQLite) so it is logged, not raised. The window is opened at `index.html?capture=1` rather than at `/capture` because the bundled assets are served as FILES: a path with no file behind it 404s in the production build while routing fine under the dev server. The router turns the query into the route. It is hidden rather than closed on the way out, and it keeps its text. A capture interrupted by something more urgent is still there on the next press, which is what makes Escape safe to press. A failed save also keeps the window open holding the text — hiding it would throw away the only copy of something just written in order to report a problem you could retry your way out of. ## Where the setting lives Rule 25 says a tunable belongs in the UI, and this one has to be. It sits in the desktop's Sync screen beside the update channel, not in admin Settings: that screen is the SERVER's and bounces on desktop anyway, while this is a property of one installation on one machine. Persisted with the same `store::set_pref` the update channel uses. No @tauri-apps/api dependency was added — everything routes through `invoke` and the `withGlobalTauri` global, as the rest of the bridge does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c |
||
|
|
c8318c323a |
android: Share → ThoughtSync, and a "New note" entry in the selection toolbar
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 13s
CI & Build / integration (push) Successful in 22s
CI & Build / Build & push image (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 6m21s
Capture without opening the app first — the input half of #1899. Two ways in: the share sheet from anywhere, and the text-selection toolbar in any app's text field. ## The note is created, not pre-filled The obvious build is "open the editor on a draft holding the shared text". That silently loses it. `NoteEditorScreen`'s flush is guarded by `bodyText != note.body`, so a draft handed the text already has nothing to save — share a link, press back without typing, and it is gone. Which is exactly the shape of a share: the common case is walking away. So `captureShared` makes the row first and opens the editor on the real note. A share has already said "keep this"; creating it is what honours that, and back then leaves a saved note rather than a decision. ## launchMode="singleTop" The reminder notification adds FLAG_ACTIVITY_SINGLE_TOP to its own intent, which is why `onNewIntent` already worked there. A share intent is built by the OTHER app and nothing here can add a flag to it, so the activity has to declare it. Without that, every share while the app was running would stack a second MainActivity — a second view model, a second board, and a back press landing on a stale copy of the same app. ## Subject and text, both A browser sends EXTRA_SUBJECT as the page title and EXTRA_TEXT as the URL. Keeping both makes the note read as its title, because the core names a note by its first line — the difference between a board you can scan and a column of identical links. `distinct` because plenty of senders put the same string in both. The extras are removed on read, like the reminder's note id and for the same reason: the activity keeps its launch intent, so without consuming them a rotation would replay the share and mint the note again. ## Not included: images `image/*` is deliberately absent from the filter. Nothing in this app can create an attachment — the core has `delete_attachment` and no counterpart, and the FFI exposes neither. Declaring the mime type would put ThoughtSync in front of people in the share sheet for a job it cannot do, and fail after they had already chosen it. Adding it needs an attachment-creation path through the core, the FFI and sync, which is its own piece of work. The desktop half of #1899 — a global hotkey — is not in this commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c |
||
|
|
cc50812a86 |
ktlint: the Tags imports landed after SyncScreen, and SyncState sorts after that
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 23s
CI & Build / Build & push image (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 7m8s
`standard:import-ordering`. The two new imports were inserted by anchoring on `com.fabledsword.thoughtsync.ui.SyncScreen`, which looked like the right neighbour and is not — `SyncState` and `SyncViewModel` both sort after it, so Tags* wedged into the middle of the Sync block. Moved below `SyncViewModel`. Every import block in the five files this branch touched is now confirmed sorted, not just the one ktlint happened to reach first — it reports one violation and stops, so a second would have cost another full Android lane. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c |
||
|
|
1e54b80f15 |
android: a Tags screen, so the phone can do more than attach tags to a note
Android / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 21s
CI & Build / Build & push image (push) Skipped
Android / Kotlin + Rust (APK) (push) Failing after 3m28s
Android could list tags and mint new ones. It could not rename, recolour,
delete or merge one — and since the per-note colour picker was removed with
2949, tag colour is the ONLY colour control in the product, which meant an
Android-only session had no way to change any colour anywhere.
A destination reached from the drawer, not a modal. The web's LabelsModal is
a modal because a desktop can float one over the board; on a phone this is a
place you go to tidy up, and a full screen is what that is.
The manage entry is an action ON the drawer's Tags header rather than a row
in it, so it cannot be mistaken for a sixth lens. The header now renders even
when there are no tags: this screen is where you make the first one, and
hiding the way in until one exists is a door that only appears once you are
already inside.
## The two calls this needed
RENAME and MERGE deliberately do not follow the same rule, and the screen
says so rather than hiding it.
* A rename that lands on an existing name merges, older survives (3324).
That path is accident-prone — it is a text field, and a typo reaches it —
so it needs a rule that cannot depend on which way round it was typed.
The screen catches the collision against the LIST, not from what the core
returns: the survivor may be the tag being renamed, so an unchanged id
afterwards proves nothing. Then it asks before merging.
* An explicit merge keeps its direction. Here the person is choosing, and
the direction IS the intent — folding #grocery into #groceries is a
decision, and overriding it with age would refuse the thing they asked
for. The price is that the direction has to be unmissable, so the body
names the tag that stops existing and every row offered is the survivor.
Delete quotes the note count, because "it is on 40 notes" is a different
decision from "delete this tag?". The count comes from `list_labels`, the
only call the core populates one on. It also says that a tag written as #tag
in a body comes back on that note's next edit — deleting the row cannot
un-write the word, and that is better said than discovered.
## The board had to learn something
`Destination.WithLabel` holds an id, and deleting or merging a tag the board
is currently LOOKING at would strand it on a lens that queries a row which no
longer exists — permanently empty, escapable only via the drawer. So
`loadLabels` became `refreshLabels`: public, and it drops back to Notes when
the current lens is gone. A failed listing deliberately does NOT trigger that
fallback — "I could not read the tags" is not evidence that this one went.
Reused rather than rewritten: `ErrorBanner` (the board and editor already
share it), `MenuItem` from Panel.kt (it closes the menu before acting so a
dialog cannot open under a hanging menu), `PlainTextField`, and the
`NOTE_TINTS` palette — the screen consumes it and does not fork a copy.
`default` stays in the palette on purpose: a tag with that colour gets a hue
derived from its name, so it means "let it pick", and removing it would leave
no way back to that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c
|
||
|
|
550a34d8e2 |
fmt: rustfmt budgets macro arguments at 60 chars, not the 100-char line
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Python tests (push) Successful in 11s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / integration (push) Successful in 18s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m24s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m2s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Android / Kotlin + Rust (APK) (push) Successful in 7m45s
Both new assertions fit well inside the 100-column limit and both were still rejected. The governing setting is `fn_call_width` (60), applied to a macro's argument list: `survivor.id, older.id, "the older row is the one that survives"` is 62 characters, so rustfmt breaks it and pairs the two values on one line with the message beneath. The neighbouring `assert_eq!(survivor.name, "Grocery", "spelled the way the caller asked")` was accepted at 59 characters of arguments, which is the same rule agreeing rather than a different one. rustfmt's own output, pasted back. Second time this lane has caught the same class of thing in one session — the other was a method chain, budgeted at 60 by `chain_width`. Recorded so the next person reaches for the 60, not the 100. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c |
||
|
|
193dfb9e94 |
tags: renaming onto an existing tag merges them, and the older row survives
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 2m35s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m46s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Canceled after 5m37s
Android / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 21s
CI & Build / Build & push image (push) Skipped
The three surfaces did not agree on what renaming a tag onto a name another
one already holds should do, and none of the three answers was good.
I described this wrongly first time and the correction matters. The local
store does NOT silently create a duplicate: `idx_labels_name` is unique on
`lower(name)`, so the bare UPDATE in `rename_label` failed, and the user got
a raw SQLite "UNIQUE constraint failed" as their error message. The server
meanwhile answered 409 "a tag with that name already exists" — and only on
an EXACT match, because its constraint is on the raw name while every
client's index is on `lower(name)`.
That last part is the sharper bug. The server would happily hold "Groceries"
beside "groceries"; no synced client can store both. Creating that pair on
the web armed a pull that fails later, on a phone, in a path with no UI.
Operator's call: a rename onto an existing name means merge — typing an
existing tag's name onto this one says they are the same thing.
* `store::rename_label` and the server's PATCH now implement one rule.
THE OLDER ROW SURVIVES and takes the new spelling. Age rather than "the
one that already held the name", so that renaming A→B and B→A land on
the same survivor; otherwise the outcome depends on which way round
someone typed it, and two devices tidying the same pair disagree about
which id still exists. Ties go to the incumbent, so it stays
deterministic.
* The core reuses `merge_labels` rather than reimplementing the move. That
is the only place that knows to mark every affected NOTE dirty before
the delete cascades the membership rows away, which is what makes a
merge reach the server at all.
* The server's rename and its `/merge` route now share one `_merge_into`
helper, for the same reason.
* Both server lookups became case-INSENSITIVE, matching every client. The
create path is included: it was the one actually minting the unstorable
pair, so fixing only the rename would have left the door open.
* The web asks before merging, naming both note counts. A merge cannot be
undone by repeating it and is now reachable by a typo in a text field —
the same reasoning as the delete confirmation in #2116. The confirmation
lives in the shared store, so the desktop gets it too; the FFI does not
ask, because that belongs to the surface with a person in front of it.
* The web store detects the merge from the LIST, not the response: the
survivor may be the row we asked to rename, so an unchanged id proves
nothing.
Tests: three integration tests over a real database (both rename directions
land on the older row; a case-varied create returns the existing tag) and
two through the Android FFI, which is the binding the phone will use.
Also fixes a straggler from
|
||
|
|
8c7553d619 |
copy: the product says "tags" now, and the schema keeps saying Label
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 16s
CI & Build / integration (push) Successful in 23s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m7s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m17s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 8m4s
Two words for one concept cost real comprehension: over a single exchange
the operator concluded that auto-tagging did not exist (it does, in
`derive.rs`) and that a tag-management view did not exist (it does,
`LabelsModal.vue`). The `#` is how most of these get made, so the `#` wins
the noun.
User-visible strings only, on all three surfaces plus the server's errors.
`Label`, `NoteLabel`, `via_tag`, `label_id`, the tables, `/api/labels` and
the FFI names are all untouched — renaming those touches migrations and the
wire format to buy nothing a reader can see.
Two of these were more than a find-and-replace:
* Android's `label_from_tag` said "from #tag", sitting beside a chip that
already renders as `#name`. Once every one of them IS a tag that hint is
circular. What it actually tells you is that the note's BODY owns this
one — which is why it alone has no remove cross — so it now says "from
the text".
* The web's empty state said "No labels yet — create one above" while
Android's already mentioned the `#` route. The web now says it too. That
is the exact fact the operator did not have.
The paired `aria-label`s went with their `title`s; a screen reader saying
"label" while the tooltip says "tag" is the same confusion with a smaller
audience.
Left alone deliberately: `json_error("invalid label")` and
`"label_ids must be a list"` in `notes/__init__.py` name the `?label=` query
parameter and the `label_ids` request field. Those are wire surface, not the
word a person reads.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c
|
||
|
|
d838b27518 |
ffi: Kotlin could list and create a tag but never rename, recolour, delete or merge one
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Python tests (push) Successful in 10s
CI & Build / integration (push) Successful in 15s
CI & Build / Build & push image (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 7m0s
`core/src/local/store.rs` implements all seven label operations. The uniffi object exposed three of them, so Android could attach tags to a note and mint new ones, and could do nothing else with them ever. The four additions are pure passthrough, because reading the store showed both of the things #2963 said to check rather than assume are already handled there: * The note count exists. `Label` carries `count: Option<i64>` and `list_labels` computes it per row, excluding trashed notes — which is the number a delete confirmation should show. The single-label returns all end in `load_label` and leave it `None` on purpose, so a screen must read counts from the LIST and never from an operation's result. * Sync is free. `rename_label` and `set_label_color` set `dirty = 1`; `remove_label` records a pending delete; `merge_labels` records one for the source AND marks every note that carried it dirty before the delete cascades the membership rows away, because push sends `label_ids` per note. So no store change, no sync change, no count plumbing — the binding only. One divergence found and documented rather than fixed: renaming a tag onto an existing name is a 409 on the server (`labels.py:94`) and a silent duplicate in the local store. The desktop has always had this, calling the same `store::rename_label`; Android now inherits it. Deciding which side is right belongs with the screen (#2964), not with the binding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c |
||
|
|
a69159e562 |
fmt: rustfmt breaks the tuple-index chain, and the desktop lane means it
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Python tests (push) Successful in 10s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / integration (push) Successful in 23s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m11s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m27s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 8m21s
`cargo fmt --all --check` failed the desktop lane on one hunk in the new `client_headers_identify_app_and_protocol` test. Clippy and every test passed; only the formatter objected. rustfmt splits `client_headers()[0].1.starts_with(..)` across lines because an index followed by a tuple field followed by a call is a three-element chain, and it will not keep one on a single line inside a macro argument regardless of width. This is rustfmt's own output, pasted back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c |
||
|
|
c40916699b |
sync: the client header said "desktop" from every phone, and named the wrong version
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 10s
CI & Build / integration (push) Successful in 21s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 2m36s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m55s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 7m37s
`client_headers()` built `thoughtsync-desktop/{CARGO_PKG_VERSION}`, and both
halves were wrong.
This crate is compiled into the Android app as well as the desktop one, so
every phone in the field announced itself as a desktop. And CARGO_PKG_VERSION
here is the CORE crate's version — a number no build stamps and no user has
ever seen — where the thing a reader of that header wants is the app's own
build (note 3127 §5: with no version tags, the artifact's self-report is the
only answer to "which build is this?").
The core cannot know either value, so the host says them. `set_client_agent`
is a OnceLock the desktop fills in `run()` and Android fills in
`ThoughtSyncApplication.onCreate`, before anything can sync. A host that never
introduces itself sends `thoughtsync-unidentified/unknown` rather than a
plausible default: nothing reads this header today, which is exactly why a
wrong value could sit in it for months — the first person to look at a server
log is the first who could catch it, and only if what they see is obviously a
host that never said who it was.
Android's version comes from the INSTALLED package, through a new
`Context.installedVersionName()` that the foot of the Sync screen now shares.
One answer to "which build is on this phone", so the line a person quotes in a
bug report and the line in the server's log cannot disagree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c
|
||
|
|
ef418a8c92 |
buttons: one definition of the shape, worn by a <button> and by an <a>
Android / Build, or is the channel already serving this? (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Build now, or wait for Android? (push) Successful in 4s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 18s
CI & Build / Build & push image (push) Successful in 40s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m1s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m14s
Desktop (Tauri) / Update manifest (push) Successful in 6s
The download links added in
|
||
|
|
fd1e4ae487 |
downloads: five clients, and the page leads with the one that fits you
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 19s
CI & Build / Build & push image (push) Successful in 36s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m21s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m21s
Desktop (Tauri) / Update manifest (push) Successful in 5s
The Account page offered the APK and nothing else, because the APK was all the server held. Step 3 baked in four more, so the single card had to become a section — and five artifacts is exactly where a downloads page turns into a table of filenames and stops being a product. So it LEADS with what fits the machine asking, from the user agent, and keeps the rest quiet but visible. A wrong guess costs nothing: nothing is behind a disclosure and every other client is one click away. Linux gets all three at once, because the UA says "Linux" and nothing about dpkg or pacman — there is no better answer available. They are named for the distro rather than the package format, since a person knows which system they run and not necessarily which packaging it uses. The AppImage carries one clause of its own: it is 95 MB against 3, and it is also the only bundle that updates itself in place. Both facts belong to the same decision. macOS and iOS lead with nothing and say so. There is no build for either, and "There's no macOS build yet" is the difference between deliberate and broken. The version renders `unknown` rather than blank, and the download stays offered — not knowing which build it is, is not a reason to withhold it. Two things this did NOT do, both deliberate: The task asked for a Tauri case — do not offer the desktop app to someone already running it. That case cannot be reached: `/account` redirects to the board in the desktop app (requiresServer, router/index.ts), because device tokens are a server-side concept. A branch for it would be dead code. `.btn-link` mirrors BaseButton's declarations rather than replacing them. BaseButton is a <button> and cannot carry an href, and unifying the two would have put every button in the app into an operator pass that CI cannot check — for a cosmetic gain. The comment names the pair. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c |
||
|
|
8a75e5f340 |
clients: an unquoted 1.0.3504551 is not JSON, and every sidecar was one
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Python tests (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Build & push image (push) Skipped
CI & Build / integration (push) Successful in 16s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m27s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m0s
Android / Kotlin + Rust (APK) (push) Successful in 8m18s
`fetch-clients.sh` wrote `"version_code": %s` unquoted, which was right when the only ordering key in sight was Android's integer. The desktop's is Tauri's `1.0.<minutes>`, and unquoted that is not valid JSON at all — so `json.loads` raised on all four generated sidecars and the server advertised nothing. A silent zero, not an error: `_read` treats a malformed sidecar as "no client here", which is right for a corrupt drop-in and indistinguishable from this. Caught by running the real fetch against the live dev channel and feeding the result to the real resolver, rather than by reading the printf. Also makes `_resolve` wrap BOTH candidate roots in Path(). Only the first was, and the asymmetry fails the same quiet way: a str `/` str raises TypeError, `_read` catches it, and a perfectly good directory reads as empty. The whole pipeline now resolves end to end against the live channel — five of five platforms, every sidecar valid JSON, one human-readable version across all of them with each artifact keeping its own comparator type: android 2026.08.30.1711 code=3504552 57.6 MB linux-appimage 2026.08.30.1711 code='1.0.3504551' 95.3 MB signed linux-deb 2026.08.30.1711 code='1.0.3504551' 3.3 MB linux-pacman 2026.08.30.1711 code='1.0.3504551' 2.7 MB windows 2026.08.30.1711 code='1.0.3504551' 2.6 MB |
||
|
|
d2f9d316cf |
tests: 300 comes back as "300" from a platform whose key is not an integer
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 15s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Build & push image (push) Successful in 41s
The precedence test wrote `version_code=300` for all five platforms and compared the desktop's against the int it wrote. It comes back as `"300"`, because the module preserves each platform's own comparator type instead of flattening both to int — which is the behaviour the change it was testing had just introduced. A `coded()` helper now says which shape to expect and why, and the assertion runs over every non-Android platform rather than spot-checking `linux-deb`. The test caught a real inconsistency in itself precisely because it compared against a concrete value rather than round-tripping what it wrote. |
||
|
|
ff6e99eb62 |
image: bake every client in, not just the phone
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python tests (push) Failing after 15s
CI & Build / integration (push) Successful in 16s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m10s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m19s
Desktop (Tauri) / Update manifest (push) Successful in 10s
Android / Kotlin + Rust (APK) (push) Successful in 8m3s
~104 MB on top of ~85 MB, almost all of it the AppImage. That is what the product being complete costs (rule 23): a self-hoster gets a working app for their machine from the server holding their notes, with no account on a forge that is private. The AppImage is not optional within that — it is the only bundle that can replace itself in place, so a server without one cannot serve in-app updates to anybody. `packaging/fetch-clients.sh` replaces the inline fetch and writes the fixed names and sidecars `client_dist.py` reads. It never fails: a platform with nothing published means the server advertises nothing for it and the UI hides that download, and eight fetches must not become eight ways to redden a green lane. THE VERSION IS FETCHED, NOT DERIVED, and this is the part that would have been wrong the easy way. The obvious shortcut is `version.sh display desktop` in the image job — it has the checkout. But this commit may not be the commit the channel is serving: a push touching only `src/` does not rebuild the desktop, so the channel still holds an older build and a locally-derived version would describe those bytes with this commit's number. `client_dist.py`'s size check could not catch it, because size IS measured from the real file — it would sail through and lie about the version alone. So `write-manifest.sh` now publishes `thoughtsync-desktop.json` beside `latest.json`, from the same two values in the same breath, and only size/sha256 are measured at bake time. Which needed the prune's keep-list, or the sidecar would have been uploaded and deleted again in the same run — a fixed name is self-limiting, which is exactly why that list exists. `version_code` is NOT uniformly an integer, and coercing it was a leftover from the days when Android was the only platform. Android's must stay a JSON number: `ClientRelease` in core declares it `i64` and a string fails to deserialize on every phone in the field. The desktop's is Tauri's semver key `1.0.<minutes>` — the value its updater actually compares — and `int()` would have rejected every desktop sidecar CI writes. The table now says which is which, and tests pin both directions. Also retires the comment above the fetch step, which claimed the APK came from "always the rolling dev release" and mentioned `:<version>` images. M314 step 3 made the channel conditional in the code directly below it, and step 6 removed version-shaped image tags entirely. Verified against the live dev channel before pushing: the Android half resolves and exits 0, the desktop half degrades with a warning because the sidecar does not exist yet, and all five constructed bundle filenames return 200. |
||
|
|
ef8aa9340f |
clients: the server hands out five platforms, not "the Android client"
Android / Kotlin + Rust (APK) (push) Skipped
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / TypeScript typecheck (push) Successful in 8s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 15s
CI & Build / Build & push image (push) Successful in 30s
`client_dist.py` was written for one platform and everything structural in it was already right — drop-in beats baked, the pair must describe one build, absence is an ordinary answer, metadata public and bytes authenticated. This widens it to a table rather than building beside it. Its own docstring made the argument years before there was a second platform: a self-hoster should not need an account on someone else's forge to get the app for their own notes. Server side only. CI bakes nothing new until step 3 and the UI reads nothing new until step 4, so this lands green and inert. Five rows — android, linux-deb, linux-pacman, linux-appimage, windows — each naming its artifact, sidecar and mimetype. Fixed filenames, version only in the sidecar: a version-stamped name would force a glob, and a glob over a directory an operator drops files into is how you serve the older of two builds, which is the failure write-manifest.sh already carries a comment about. THE ANDROID NAMES AND ROUTE DO NOT MOVE. The lane publishes those exact filenames, clients in the field poll /api/client/android, and `android_client` stays on /api/config beside the new `clients` map. Renaming them to match the pattern would buy tidiness and strand every installed phone; retiring the key belongs to a later change made when nothing polls it, not to the change introducing its replacement. Fields were added, not moved — `ClientRelease` in core is a plain serde struct and ignores what it does not know. PRECEDENCE IS PER PLATFORM, which is the trap the table introduces. "First directory holding anything wins" would mean dropping in an APK silently retracts the four desktop downloads. Pinned by a test. The AppImage needs a third file. It is the only bundle that replaces itself in place, so the updater verifies a minisign signature before it does — and a bundle that cannot be verified cannot be offered. A missing or empty `.sig` therefore makes it absent rather than merely unsigned, and the signature travels WITH the version so an updater can never pair one build's version with another's signature. The tests parametrize over the table instead of testing Android and trusting the rest. The bugs this module can have are not platform-specific, and a suite that only exercised one platform is how the other four would ship untested. |
||
|
|
f992439588 |
version: every surface can say which build it is, and two of them were lying
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m50s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 15s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m19s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Android / Kotlin + Rust (APK) (push) Successful in 7m59s
Note 3127 §5 removed version tags, so an artifact's self-report is now the only
answer to "which build is this?" — and nothing exists to contradict it when it
is wrong. Three surfaces gain a dim build line: the foot of the web rail, the
login screen, and the foot of Sync on Android.
The login screen because "I can't sign in" is a bug report like any other, and
requiring an account to read a build number withholds it from exactly the people
who can't get past that page. `/api/config` is already public.
Two of the values it was going to show were wrong, which is the part worth
knowing about.
The DESKTOP reported `env!("CARGO_PKG_VERSION")` from `config_get` and from the
startup log. `cargo tauri build --config '{"version": ...}'` overrides
tauri.conf.json, not Cargo's own metadata — so both read the literal `0.2.0` in
Cargo.toml, on every build ever shipped. They now read a display version baked in
by the lane through `option_env!`, hoisted to the crate root because two readers
of one fact is how this repo keeps producing 2181-2183. Not the ordering key
either: `1.0.<minutes>` is the opaque value Tauri's updater compares and must
never be shown to a person, and `update.rs` still reads it because a comparator
is exactly what it is (rule 149).
The SERVER fell back to `__version__` when APP_VERSION was absent, so a server
run from a checkout reported `0.2.0` — a real-looking version naming no build
anybody could obtain. `__init__.py` already asserted the honest answer was
"APP_VERSION being missing, which app.py already handles"; it did not, and a
comment claiming a behaviour two files away is how that stayed true-sounding.
Now an explicit "unknown", with the packaging version left where "unknown" is
not a legal value.
Android reads the INSTALLED package's versionName rather than BuildConfig, so it
reports what is actually on the phone.
Everything renders "unknown" rather than blank when it cannot say. A blank looks
like a layout bug; a plausible default cannot be caught by anything.
build.rs gets `rerun-if-env-changed` for the baked value: cargo does not track an
`option_env!` variable on its own, and the desktop lane having no cache today is
what makes that easy to forget the day one is added.
|
||
|
|
544cf72735 |
install: the stable fallback is dead now that stable publishes its own bundles
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python tests (push) Successful in 10s
CI & Build / integration (push) Successful in 19s
Android / Build, or is the channel already serving this? (push) Successful in 2s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Build & push image (push) Successful in 29s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m26s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m25s
Desktop (Tauri) / Update manifest (push) Successful in 4s
It existed for one window: `stable` was a manifest-only pointer at whatever `v*` tag had last been cut, and `stable` is the DEFAULT channel, so without the fallback `curl … | sh` was broken for everyone between step 3 landing and the first merge to `main`. That merge happened (`b6673c6`), and `stable` now holds its own signed bundles at 1.0.3503145 — AppImage, deb and pacman, all resolving by the one lookup both channels share. Kept as a fallback it stops being a safety net and becomes a mask: the branch only runs when `stable` has no bundles, which from here on means something is broken, and chasing a `v*` release instead of saying so is the wrong answer. The header now says the transition is finished and that neither channel should be special-cased again, because the shape of that code invites re-adding it. |
||
|
|
6e524ec616 |
guard: an empty channel killed the lane instead of passing it
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / integration (push) Successful in 15s
CI & Build / Build & push image (push) Skipped
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 9s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m33s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m14s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Android / Kotlin + Rust (APK) (push) Successful in 7m46s
The first merge to `main` took the Android lane down (run 4857): the decide job exited 1 in 0.16 seconds with no output at all, and the image build skipped behind it because a failing lane must not publish. `stable` had never published an APK, which the guard treats as a pass — there is nothing to go backwards from, and `[ -z "$published" ]` says so in a branch of its own. That branch was unreachable. `published="$(published_for ...)"` under `set -e` dies on the substitution before it, and everything the pipeline would have printed goes into the capture rather than the log. What decided which lookups had the bug is the last command in the pipeline. `sed` on empty input exits 0; `grep` exits 1. Three of the four end in `sed`. Android's version_code ends in `grep -oE '[0-9]+$'`, so it was the only one — and only on a channel with nothing on it, which is why a week of dev pushes never saw it. The tests now reach the half of the guard that talks to a feed, with `curl` shadowed on PATH so they stay hermetic: an empty channel passes and builds, a lower published version passes, a higher one fails the lane, and an equal Android code is refused because Android will not install it. |
||
|
|
c2fdc05e5c |
release: a tag builds nothing and carries a changelog instead
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 4s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m17s
Desktop (Tauri) / Update manifest (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 18s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m0s
Android / Kotlin + Rust (APK) (push) Successful in 8m4s
Step 7 of M314, the last one. Rule 22 — the old path comes out completely. ## A release stops building `desktop.yml` no longer triggers on `v*`, and its two `Publish release` steps are gone. `ci.yml` lost its tag trigger in step 6. So a tag now reaches exactly one lane: the new `release.yml`, which builds nothing. That is not a simplification for its own sake. The merge to `main` already published everything a user can receive — `:latest` + `:<sha>`, both channel feeds, the updater manifest. A tag rebuilding that source produces identical artifacts under identical names and re-pushes `:<sha>` with different bytes, which rule 145 forbids even when they match. ## So what a release is FOR The changelog (note 3127 §5). Two halves to "what am I running", and the version answers only the first: which build is this (the footer, /api/config, the APK's versionName) and what is in it that was not in the one I ran last month (nothing, until now). `packaging/release-notes.sh` derives it from git rather than a hand-maintained CHANGELOG, which drifts into recording what someone MEANT to ship. Capped at 60 entries with the omitted count stated — the first dated release spans 181 commits since `v0.1.0`, and a truncated list that does not say it is truncated is a lie. It publishes through `publish-release.sh` rather than making its own API calls, for the create-or-PATCH-on-409 path: a fixed-tag release that only ever POSTs keeps whatever body its first run wrote, which is #2182, and reimplementing that correctly in a second place is how it comes back. ## Retired `MANIFEST_TAG` and the whole branch behind it. It let the manifest live on a `stable` pointer release while the bundles sat on a versioned one — a split step 3 removed when `stable` started holding its own bundles. Nothing had passed it since; a parameter that can only ever receive its own default is a branch nobody exercises and a comment that goes stale, and its stale text was still telling readers the installable builds live on the versioned releases. `desktop/src-tauri/Cargo.toml`'s version and `thoughtsync/__init__.py`'s both now say out loud that they are not shipped values. The Cargo one carries the history worth keeping: the old scheme took its base from that line, so `0.2.<run>` on dev outranked a bare `0.2.0` on main, and the remedy was "remember to bump the minor before tagging" — documented in a comment, enforced nowhere. #2183 is what that looked like in the field. **That ritual is now formally dead**, and this is the deliberate act of killing it rather than a side effect. ## Still there on purpose `install.sh`'s transitional stable fallback. It cannot go until `main` has published to `stable` at least once, and that is gated on an operator request. Removing it now would break the DEFAULT install channel. #3147 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fa43c2f4e9 |
ci: a docs-only merge to main produced no image, so no :<sha> for that commit
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 15s
CI & Build / Build & push image (push) Successful in 17s
Rule 145 promises every push to `main` publishes a `:<sha>`, so any production
commit is addressable without a release ceremony. `ci.yml`'s `paths:` filter
quietly broke that: a commit touching only docs never triggered the lane, so
that commit had no image and no sha tag.
Pre-existing — the filter has always been there — but it is rule 145's guarantee
and step 6 is where the tag set is being made to match the rule, so it is this
step's to close.
Confirmed live on
|
||
|
|
a0c789b3ba |
docs: the image tag list said something step 6 stopped being true
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 2s
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
`:<git-sha>` is on `main` only now — a sha tag per dev push was a rollback target nobody had ever pulled — and `:<version>` never existed as an image tag after rule 145 was narrowed. Both were still documented. `docs/android-distribution.md` also said `:dev`, `:latest` and `:<version>` all ship a client, which is now two-thirds true and misses the more useful fact: the channel IS the image you run, so a stable server serves a stable client. Worth saying because until step 3 it was hard-wired to the dev release on every branch and did the opposite. This push is also the skip-if-exists verification. It touches neither client's file set, so both `decide` jobs should report the channel already serving the current version and skip a 6- and a 9-minute build — while the guard still runs on that path (§6.3). #3146 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
22a9a279b1 |
ci: one definition of what ships decides both the version and whether to build
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / integration (push) Successful in 16s
CI & Build / Build & push image (push) Skipped
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 10s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m5s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m24s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 8m12s
Step 6 of M314. Two changes that only make sense together.
## The image tag set rule 145 mandates
dev push -> :dev
main push -> :latest + :<sha>
a v* tag -> nothing; the trigger is gone
`:<sha>` was going out on EVERY branch — a rollback target nobody has ever
pulled, accumulating forever, for a channel whose entire contract is that it
moves. It is on main only now, where rollback matters and where gated merges
(rule 2) make it dozens per year rather than one per push.
No version-shaped image tag in any lane. Verified the way rule 145 asks — by
looking for a CONSUMER, not for whether one is imaginable: `docker-compose.yml`
is parameterised for a pin and the docs describe the option, but no compose
file, deploy script or CI job reads one.
## Skip-if-exists, adapted, because §4 assumes a registry §5 removed
Note 3127 §4 says to ask the registry whether that exact version exists. There
is no `:<version>` tag to ask about any more. What there IS, for both clients,
is a channel that publishes the version it serves — and that answers the same
question: if the channel already serves what this source derives, the artifact
would be byte-identical.
So the `paths:` filters are gone from the desktop and Android lanes, replaced
by a `decide` job reading the real file set. That duplication is not
theoretical: `packaging/` was added to the sets and not to the filters, so the
commit that fixed a derivation bug never ran on the two lanes it fixed
(
|
||
|
|
0ab7d94294 |
versioning: refuse to publish a version below what the channel already serves
CI & Build / Python tests (push) Successful in 17s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / integration (push) Successful in 21s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m13s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 6m21s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Android / Kotlin + Rust (APK) (push) Successful in 9m19s
Step 5 of M314, note 3127 §6.3. Everything else in this milestone derives a number and trusts it; this compares the derived value against what the channel is actually serving and fails the lane if it went down. Too-low is the unrecoverable direction: every installed client reports "up to date" forever, and no later build fixes it until one climbs back above the bad number. #2183 and #2993 are both that symptom. ## Two hazards, two mechanisms A shallow clone is now tested DIRECTLY, in `version.sh`, via `--is-shallow-repository`. The empty-result guard only caught the case where nothing matched — and run 4796 showed the worse one, where a partial match returned a real six-days-stale answer. Asking the question outright costs no network and covers artifacts with nothing published to compare against. `guard-forward.sh` handles the rest: a squash or rebase merge rewriting the committer date, a rebuild of an older commit, and clock skew between runners. ## The comparison is per artifact, and the operator differs desktop derived >= published commit time, so equality is the ORDINARY no-change case and `<=` would fail every build that changed nothing android derived > published build time, so equality means two builds in one minute — and Android refuses to install an APK whose versionCode does not RISE The server is deliberately unguarded: nothing compares its version, `:latest` moves regardless, and rule 145 removed the version tags that would be the published list. A too-low value there is a wrong date in a footer, not a stranded client. It still gets the shallow-clone check. ## Proved to fire, not assumed Cloned the repo, checked out a commit eight back, ran the guard against the LIVE dev feed: at the tip derived 1.0.3502151, published 1.0.3502151 -> pass eight back derived 1.0.3501535, published 1.0.3502151 -> FAILS android tip derived 3502171, published 3502152 -> pass stable derived 1.0.3502151, published 0.2.0 -> pass That last row is worth keeping: stable still advertises the bare `0.2.0` from the old Cargo.toml scheme, so the transition orders upward on BOTH channels, not just the one being exercised. A channel with nothing published passes rather than failing — otherwise the first publish to a new channel could never happen. The guard runs BEFORE the build in all three lanes, so a bad derivation costs seconds rather than a five-minute compile and a publish to undo. `compare` is exposed as an explicit mode so the ordering is testable without a network and inspectable without a push — 16 cases including `1.0.9 < 1.0.10`, which a string compare gets exactly backwards. #3145 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |