Merge pull request 'fix(web): download button + server version display' (#37) from dev into main

This commit was merged in pull request #37.
This commit is contained in:
2026-05-11 03:20:39 +00:00
7 changed files with 93 additions and 25 deletions
+6 -1
View File
@@ -50,9 +50,11 @@ jobs:
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
echo "args=-t ${IMAGE}:${VERSION} -t ${IMAGE}:latest" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "::notice::Release build: ${VERSION} + latest"
else
echo "args=-t ${IMAGE}:main" >> "$GITHUB_OUTPUT"
echo "version=main" >> "$GITHUB_OUTPUT"
echo "::notice::Main-branch build: :main"
fi
@@ -93,4 +95,7 @@ jobs:
- name: Build and push
if: steps.guard.outputs.ready == 'true'
run: docker buildx build --push ${{ steps.tags.outputs.args }} .
run: |
docker buildx build \
--build-arg MINSTREL_VERSION="${{ steps.tags.outputs.version }}" \
--push ${{ steps.tags.outputs.args }} .
+7 -1
View File
@@ -15,7 +15,13 @@ COPY . .
# Overwrite the committed placeholder with the freshly-built SPA assets.
COPY --from=web /web/build ./web/build
ENV CGO_ENABLED=0
RUN go build -trimpath -ldflags="-s -w" -o /out/minstrel ./cmd/minstrel
# Version stamping: release.yml passes the git tag via MINSTREL_VERSION
# build-arg; local `docker build` falls back to "dev". Surfaced at
# /healthz for operator-side image-version verification.
ARG MINSTREL_VERSION=dev
RUN go build -trimpath \
-ldflags="-s -w -X 'git.fabledsword.com/bvandeusen/minstrel/internal/server.ServerVersion=${MINSTREL_VERSION}'" \
-o /out/minstrel ./cmd/minstrel
FROM debian:bookworm-slim
RUN apt-get update \
+1
View File
@@ -148,6 +148,7 @@ func (s *Server) handleHealthz(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{
"status": "ok",
"version": ServerVersion,
"min_client_version": MinClientVersion,
})
}
+10
View File
@@ -4,3 +4,13 @@ package server
// accepts. Bump it when a server change requires a paired client update;
// older clients see version_too_old at /healthz and refuse to operate.
const MinClientVersion = "0.1.0"
// ServerVersion is the deployed server image's version tag. Defaults to
// "dev" for local builds; overridden at link time via:
//
// -ldflags="-X 'git.fabledsword.com/bvandeusen/minstrel/internal/server.ServerVersion=v2026.05.10.2'"
//
// release.yml passes the git tag through MINSTREL_VERSION build-arg →
// Dockerfile ldflag. Surfaced at /healthz so operators can verify which
// image their container is running without exec'ing into it.
var ServerVersion = "dev"
+26 -16
View File
@@ -11,6 +11,10 @@
// bandwidth abuse on the APK stream, so this component only shows
// for logged-in users. Mount in Settings, never in pre-auth surfaces.
// Server returns snake_case (clientVersionResponse in
// internal/api/client_assets.go). Map to a camelCase struct for
// the rest of the component.
type WireInfo = { version: string; apk_url: string; size_bytes: number };
type Info = { version: string; apkUrl: string; sizeBytes: number };
let info = $state<Info | null>(null);
@@ -22,12 +26,12 @@
onMount(async () => {
try {
const body = await api.get<Partial<Info>>('/api/client/version');
if (!body.version || !body.apkUrl) return;
const body = await api.get<Partial<WireInfo>>('/api/client/version');
if (!body.version || !body.apk_url) return;
info = {
version: body.version,
apkUrl: body.apkUrl,
sizeBytes: body.sizeBytes ?? 0
apkUrl: body.apk_url,
sizeBytes: body.size_bytes ?? 0
};
} catch {
// 404 (no APK), 401 (somehow unauthed), network error — silent.
@@ -36,17 +40,23 @@
</script>
{#if info}
<a
href={info.apkUrl}
download="minstrel.apk"
class="mobile-app-download flex items-center gap-3 rounded-md border border-border bg-surface px-3 py-2 text-sm text-text-primary no-underline transition-colors hover:bg-surface-hover hover:text-accent focus-visible:ring-2 focus-visible:ring-accent"
>
<Smartphone size={18} strokeWidth={1.5} class="shrink-0 text-accent" />
<span class="flex flex-col leading-tight">
<span class="font-medium">Get the Android app</span>
<span class="text-xs text-text-secondary">
{info.version}{info.sizeBytes > 0 ? ` · ${formatSize(info.sizeBytes)}` : ''}
<section class="space-y-3 rounded border border-border bg-surface p-4">
<h2 class="text-lg font-semibold">Mobile app</h2>
<p class="text-sm text-text-secondary">
Install the Android app paired to this server. Sign in with the same account.
</p>
<a
href={info.apkUrl}
download="minstrel.apk"
class="mobile-app-download flex items-center gap-3 rounded-md border border-border bg-surface px-3 py-2 text-sm text-text-primary no-underline transition-colors hover:bg-surface-hover hover:text-accent focus-visible:ring-2 focus-visible:ring-accent"
>
<Smartphone size={18} strokeWidth={1.5} class="shrink-0 text-accent" />
<span class="flex flex-col leading-tight">
<span class="font-medium">Get the Android app</span>
<span class="text-xs text-text-secondary">
{info.version}{info.sizeBytes > 0 ? ` · ${formatSize(info.sizeBytes)}` : ''}
</span>
</span>
</span>
</a>
</a>
</section>
{/if}
@@ -0,0 +1,37 @@
<script lang="ts">
import { onMount } from 'svelte';
// Reads /healthz on mount and surfaces the deployed server version
// as understated text. Useful when verifying which image a container
// is actually running (came up debugging the in-app update flow when
// it wasn't obvious whether v2026.05.10.0 or .1 was deployed).
//
// /healthz is unauthenticated, so the bare fetch works without
// credentials. Renders nothing on parse failure or pre-version
// images that don't include the field — graceful degradation.
type Health = { status: string; version?: string };
let version = $state<string | null>(null);
onMount(async () => {
try {
const res = await fetch('/healthz');
if (!res.ok) return;
const body = (await res.json()) as Partial<Health>;
if (body.version && body.version !== 'dev') {
version = body.version;
} else if (body.version === 'dev') {
// Local dev images report "dev" — show it so the operator
// can tell they're not on a release tag.
version = 'dev';
}
} catch {
// network / parse error — silent.
}
});
</script>
{#if version}
<p class="text-xs text-text-secondary">Server {version}</p>
{/if}
+6 -7
View File
@@ -18,6 +18,7 @@
import { errCode } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
import MobileAppDownload from '$lib/components/MobileAppDownload.svelte';
import ServerVersion from '$lib/components/ServerVersion.svelte';
const queryClient = useQueryClient();
@@ -325,11 +326,9 @@
</ul>
</section>
<section class="space-y-3 rounded border border-border bg-surface p-4">
<h2 class="text-lg font-semibold">Mobile app</h2>
<p class="text-sm text-text-secondary">
Install the Android app paired to this server. Sign in with the same account.
</p>
<MobileAppDownload />
</section>
<MobileAppDownload />
<div class="pt-2 text-center">
<ServerVersion />
</div>
</div>