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:
@@ -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
@@ -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 \
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user