Came up debugging the in-app update flow — wasn't obvious which image
the container was actually running without exec'ing in. New flow:
### Server
- internal/server/version.go: new `var ServerVersion = "dev"`,
overridden via -ldflags at build time.
- /healthz response gains a "version" key alongside the existing
"status" + "min_client_version". Backward-compat: existing clients
ignore unknown JSON fields. Endpoint stays unauthenticated.
### Build
- Dockerfile: new `ARG MINSTREL_VERSION=dev`, threaded into the
go build -ldflags so the binary's ServerVersion is stamped at
link time. Default "dev" preserves local `docker build` ergonomics.
- .forgejo/workflows/release.yml: tags step also emits a `version`
output (the git tag for tag pushes, "main" for branch pushes);
build step passes it as `--build-arg MINSTREL_VERSION=...`.
### Web
- web/src/lib/components/ServerVersion.svelte: small understated
text ("Server v2026.05.10.2") that fetches /healthz on mount.
Renders nothing on parse failure or pre-version images.
- Mounted at the bottom of Settings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final phase of the in-app update flow. release.yml on tag pushes
fetches the APK that flutter.yml is attaching to the same release,
drops it into client/ in the build context, and the Dockerfile's
COPY client/ /app/client/ bakes it into the image.
### How it sequences
flutter.yml and release.yml both trigger on tag pushes and run in
parallel on different runners (flutter-ci vs go-ci). flutter.yml
typically finishes APK build + release attachment in 2-5 min.
release.yml polls the release page for up to 15 min for the APK to
appear, then proceeds. If the APK never lands (flutter.yml failure,
network hiccup), release.yml emits a warning and ships the image
without the bundled APK — server returns 404 from /api/client/version,
banner stays hidden, manual download from the release page still
works. Graceful degradation, not a blocker.
### Repo shape
- client/.gitkeep + client/README.md so the directory exists in git
and the COPY in the Dockerfile always finds something to copy
- .gitignore excludes client/minstrel.apk + .version so accidental
commits don't bloat the repo
- Dockerfile: COPY --chown=minstrel:minstrel client/ /app/client/
### Why polling vs cross-workflow trigger
Forgejo Actions' workflow_run support varies by runner version; the
polling approach is universally compatible. If/when we standardize on
a runner that handles workflow_run cleanly, the polling step can
become a `needs:` dependency.
Closes#397.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of the in-app update flow — server side. Endpoints serve the
bundled Android APK + sidecar version file from /app/client/.
Returns 404 gracefully when files aren't present, so dev environments
and pre-CI-wiring images degrade cleanly to "no update available."
- internal/api/client_assets.go: handleClientVersion + handleClientAPK.
Both unauthenticated (matches /healthz) so install flow doesn't
depend on a live session. APK served with proper
application/vnd.android.package-archive Content-Type +
http.ServeContent so Range requests work for resumable downloads
on flaky networks.
- Path resolves to /app/client/ by default; MINSTREL_CLIENT_APK_DIR
env var overrides for dev.
- Dockerfile creates /app/client/ + commented COPY hooks for the CI
sequencing phase.
- Tests cover all four states: missing apk, apk-but-no-version,
both present (200 with correct shape), apk stream (200 with
correct Content-Type + body bytes).
Phases 2 (Flutter client provider + banner + install intent + Android
manifest changes) and 3 (CI sequencing to bake the APK into the image)
land in follow-up commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Dockerfile created the minstrel user but never set a WORKDIR or
pre-created a writable data dir. With the default DataDir of "./data"
and CWD of "/", every MkdirAll attempt failed with "permission denied"
under the non-root user, and the boot code only logged a Warn before
continuing. Result: every artist-art enrichment failed at
"mkdir artist-art: mkdir data: permission denied"; the operator saw
"0 successes" with no signal as to why.
Three changes:
- Dockerfile: WORKDIR /app, pre-create /app/data with minstrel
ownership, and ENV MINSTREL_STORAGE_DATA_DIR=/app/data so the
binary doesn't fall back to the broken "./data" default even if
the operator forgets to set it.
- docker-compose.yml: explicit MINSTREL_STORAGE_DATA_DIR + a named
volume mounted at /app/data so cached art persists across
container recreates.
- cmd/minstrel/main.go: MkdirAll failure is now fatal. Silent breakage
here cascades into every cache (playlist covers, artist art,
album-cover fallbacks); refusing to start surfaces the misconfig
immediately. Also wires Enricher.DataDir from cfg in preparation
for the album-cover sidecar fallback (next commit).
Web assets are built in the node stage, copied into the go stage before
go build so //go:embed picks them up, then the minimal slim runtime
carries only the final binary and ffmpeg.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Builder uses golang:1.23-bookworm (matches runner-base:go-ci).
Runtime is debian:bookworm-slim with ffmpeg (server spec §3) plus a
non-root minstrel user. Release workflow builds from this Dockerfile.