feat(ui): add app version display and API

- Add `getGitHash` utility to extract the git commit hash from the `.git/HEAD` file.
- Expose a new `/api/app-version` endpoint on the server to return the current version.
- Implement a `useAppVersion` hook to fetch the version asynchronously.
- Display the app version (e.g., `app version: a1b2c3d`) in the User Center UI for both authenticated and unauthenticated users.
- This helps in tracking which version of the application is currently running in production.
This commit is contained in:
kyuuseiryuu 2026-03-17 10:57:10 +09:00
parent 05200c8d48
commit f7cd596084
4 changed files with 56 additions and 4 deletions

View File

@ -0,0 +1,9 @@
import { useRequest } from "ahooks";
export const useAppVersion = () => {
const versionRequest = useRequest(
() => fetch('/api/app-version').then(res => res.json()).then(json => json.version),
{ debounceWait: 300 }
);
return versionRequest.data;
}

View File

@ -1,5 +1,5 @@
import { serve } from "bun";
import { getMatchInfo, verifyLogtoToken, xcxApi } from "./utils/server";
import { getGitHash, getMatchInfo, verifyLogtoToken, xcxApi } from "./utils/server";
import ics from 'ics';
import index from "./index.html";
import { getUidScore } from "./services/uidScoreStore";
@ -13,12 +13,17 @@ import type { IEventInfo } from "./types";
dayjs.extend(utc);
dayjs.extend(timezone);
console.debug('AppVersion: %s', await getGitHash(8));
const server = serve({
port: process.env.PORT || 3000,
routes: {
// Serve index.html for all unmatched routes.
"/*": index,
"/api/app-version": {
async GET() {
return Response.json({ version: await getGitHash(8) });
}
},
"/api/club/find": {
async GET(req) {
const searchParams = new URL(req.url).searchParams;

View File

@ -6,6 +6,7 @@ import { useLocation, useNavigate } from "react-router";
import { AUTH_CALLBACK_URL, USER_CENTER_URL } from "../utils/front";
import useAutoLogin from "../hooks/useAutoLogin";
import { LOGTO_DOMAIN } from "../utils/common";
import { useAppVersion } from "../hooks/useAppVersion";
enum modifyRoutes {
username = '/account/username',
@ -18,6 +19,20 @@ enum modifyRoutes {
const redirect = encodeURIComponent(USER_CENTER_URL);
function AppVersion({ version }: { version?: string }) {
if (!version) return null;
return (
<Divider>
<Typography.Text type="secondary">
<Flex gap={4}>
<span style={{ textTransform: 'uppercase' }}>app version:</span>
{version}
</Flex>
</Typography.Text>
</Divider>
);
}
export const UserCenter = () => {
const { signIn, isAuthenticated, signOut, getIdTokenClaims } = useLogto();
const { autoSignIn } = useAutoLogin();
@ -42,6 +57,7 @@ export const UserCenter = () => {
window.location.href = `${LOGTO_DOMAIN}${url}?redirect=${redirect}`;
}, []);
const app = App.useApp();
const version = useAppVersion();
if (!isAuthenticated) {
return (
<div className="app">
@ -54,6 +70,7 @@ export const UserCenter = () => {
>
</Button>
<AppVersion version={version} />
</div>
);
}
@ -102,6 +119,7 @@ export const UserCenter = () => {
</Flex>
</Flex>
</Flex>
<AppVersion version={version} />
</>
);
}

View File

@ -2,7 +2,7 @@ import type { Player } from "../types";
import * as cheerio from "cheerio";
import { XCXAPI } from "../services/xcxApi";
import { KAIQIU_BASE_URL, LOGTO_DOMAIN } from "./common";
import { RedisClient } from "bun";
import { RedisClient, file } from "bun";
import { createRemoteJWKSet, jwtVerify } from 'jose';
import { LOGTO_RESOURCE } from "./constants";
@ -11,11 +11,11 @@ const REQUIRED_ENVS = [
process.env.REDIS,
];
console.debug('ENVS: \n%s', REQUIRED_ENVS.join('\n'));
if (!REQUIRED_ENVS.every(v => !!v)) {
console.error('Missing required environment variables. Please check your .env');
process.exit(1);
}
export const REDIS_CACHE_HOUR = Number(process.env.REDIS_CACHE_HOUR) || 8;
console.debug('Cache hour: %s', REDIS_CACHE_HOUR);
@ -118,4 +118,24 @@ export const verifyLogtoToken = async (headers: Headers) => {
// console.debug('Payload', payload);
// Sub is the user ID, used for user identification
return payload
}
export async function getGitHash(length = 6): Promise<string> {
try {
// 1. 读取 HEAD 指针
const headContent = await file(".git/HEAD").text();
// 情况 A: 处于分支上 (内容格式为 "ref: refs/heads/master")
if (headContent.startsWith("ref:")) {
const refPath = headContent.replace("ref: ", "").trim();
const fullHash = await file(`.git/${refPath}`).text();
return fullHash.trim().substring(0, length);
}
// 情况 B: 处于分离头指针状态 (内容直接就是 Hash)
return headContent.trim().substring(0, length);
} catch (e) {
console.error("无法读取 Git 信息:", e);
return "unknown";
}
}