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
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Python lint (push) Successful in 2s
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 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.