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) <noreply@anthropic.com>
61 lines
2.1 KiB
TypeScript
61 lines
2.1 KiB
TypeScript
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<string | null> {
|
|
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 a<b, 0 if equal, 1 if a>b. */
|
|
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<UpdateStatus> {
|
|
const latest = await fetchUpstreamVersion();
|
|
return {
|
|
version: APP_VERSION,
|
|
latest,
|
|
updateAvailable: latest != null && compareVersions(latest, APP_VERSION) > 0,
|
|
};
|
|
}
|