From 42cd7a7b4d13f9dbf8c068a790a636f962062e86 Mon Sep 17 00:00:00 2001 From: tonym Date: Mon, 15 Jun 2026 22:12:18 -0500 Subject: [PATCH] =?UTF-8?q?v0.8.0=20=E2=80=94=20Update-available=20detecti?= =?UTF-8?q?on=20(version=20chip=20+=20changelog)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Footer becomes a changelog chip and shows an 'Update available' badge when a newer APP_VERSION exists upstream. Detection reads src/lib/version.ts from the Gitea default branch (dms model) and compares — self-contained, no OTM control plane. Adds src/lib/update-check.ts and GET /api/version (also a running-version probe for the upcoming one-click rebuild). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/api/version/route.ts | 19 ++++++++ src/components/layout/footer.tsx | 83 +++++++++++++++++++++++++++++--- src/lib/changelog.ts | 7 +++ src/lib/update-check.ts | 60 +++++++++++++++++++++++ src/lib/version.ts | 2 +- 5 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 src/app/api/version/route.ts create mode 100644 src/lib/update-check.ts diff --git a/src/app/api/version/route.ts b/src/app/api/version/route.ts new file mode 100644 index 0000000..08d2bb9 --- /dev/null +++ b/src/app/api/version/route.ts @@ -0,0 +1,19 @@ +import { NextResponse } from "next/server"; +import { requireAuth } from "@/lib/auth"; +import { getUpdateStatus } from "@/lib/update-check"; + +export const dynamic = "force-dynamic"; + +// Running-version probe + upstream update check. Used by the footer chip, and +// (later) polled by the self-update UI to detect when the rebuilt instance is +// back online on the new version. +export async function GET() { + try { + await requireAuth(); + } catch { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + return NextResponse.json(await getUpdateStatus(), { + headers: { "Cache-Control": "no-store" }, + }); +} diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index f01a77e..428df76 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -1,12 +1,83 @@ -import Link from "next/link"; +"use client"; + +import { useEffect, useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { APP_VERSION } from "@/lib/version"; +import { CHANGELOG } from "@/lib/changelog"; +import type { UpdateStatus } from "@/lib/update-check"; export function Footer() { + const [open, setOpen] = useState(false); + const [status, setStatus] = useState(null); + + useEffect(() => { + let cancelled = false; + fetch("/api/version") + .then((r) => (r.ok ? r.json() : null)) + .then((d) => { + if (!cancelled) setStatus(d); + }) + .catch(() => {}); + return () => { + cancelled = true; + }; + }, []); + + const updateAvailable = status?.updateAvailable && status.latest; + return ( -
- - Moon Base v{APP_VERSION} - -
+ <> +
+ {updateAvailable && ( + + Update available → v{status!.latest} + + )} + +
+ + + + + Changelog + + {updateAvailable && ( +

+ A newer version (v{status!.latest}) is available. You're on v{APP_VERSION}. +

+ )} +
+ {CHANGELOG.map((entry) => ( +
+
+ v{entry.version} + {entry.date} +
+
    + {entry.changes.map((c, i) => ( +
  • {c}
  • + ))} +
+
+ ))} +
+
+
+ ); } diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts index a17c654..51aa3e9 100644 --- a/src/lib/changelog.ts +++ b/src/lib/changelog.ts @@ -8,6 +8,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + version: "0.8.0", + date: "2026-06-15", + changes: [ + "The app now tells you when a newer version is available — a little \"Update available\" badge appears in the bottom bar, and the version number opens the full changelog. (One-click updating is coming next.)", + ], + }, { version: "0.7.0", date: "2026-06-15", diff --git a/src/lib/update-check.ts b/src/lib/update-check.ts new file mode 100644 index 0000000..7020296 --- /dev/null +++ b/src/lib/update-check.ts @@ -0,0 +1,60 @@ +import { APP_VERSION } from "./version"; + +// Detects whether a newer version has been pushed upstream by reading +// src/lib/version.ts from the Gitea repo's default branch and comparing its +// APP_VERSION to the running one. Self-contained — no OTM control plane needed. +// Reuses the same GITEA_* env as the suggestions module. +const GITEA_URL = process.env.GITEA_URL; +const GITEA_TOKEN = process.env.GITEA_TOKEN; +const GITEA_REPO = process.env.GITEA_REPO; +const GITEA_BRANCH = process.env.GITEA_BRANCH || "master"; + +export type UpdateStatus = { + version: string; + latest: string | null; + updateAvailable: boolean; +}; + +/** Read APP_VERSION from src/lib/version.ts on the upstream branch. null on any failure. */ +export async function fetchUpstreamVersion(): Promise { + if (!GITEA_URL || !GITEA_TOKEN || !GITEA_REPO) return null; + try { + const url = `${GITEA_URL}/api/v1/repos/${GITEA_REPO}/contents/src/lib/version.ts?ref=${GITEA_BRANCH}`; + const res = await fetch(url, { + headers: { Authorization: `token ${GITEA_TOKEN}` }, + cache: "no-store", + }); + if (!res.ok) return null; + const json = await res.json(); + if (!json?.content) return null; + const decoded = Buffer.from( + json.content, + json.encoding === "base64" ? "base64" : "utf8" + ).toString("utf8"); + const m = decoded.match(/APP_VERSION\s*=\s*["']([^"']+)["']/); + return m ? m[1] : null; + } catch { + return null; + } +} + +/** Numeric dotted compare. -1 if ab. */ +export function compareVersions(a: string, b: string): number { + const pa = a.split(".").map((n) => parseInt(n, 10) || 0); + const pb = b.split(".").map((n) => parseInt(n, 10) || 0); + const len = Math.max(pa.length, pb.length); + for (let i = 0; i < len; i++) { + const d = (pa[i] || 0) - (pb[i] || 0); + if (d !== 0) return d > 0 ? 1 : -1; + } + return 0; +} + +export async function getUpdateStatus(): Promise { + const latest = await fetchUpstreamVersion(); + return { + version: APP_VERSION, + latest, + updateAvailable: latest != null && compareVersions(latest, APP_VERSION) > 0, + }; +} diff --git a/src/lib/version.ts b/src/lib/version.ts index c0a374d..89cd851 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -1,2 +1,2 @@ // Bump on every user-visible release. Changelog entries live in `src/lib/changelog.ts`. -export const APP_VERSION = "0.7.0"; +export const APP_VERSION = "0.8.0";