feat: add multi-arch image publishing

This commit is contained in:
zhoujun0601
2026-08-13 11:28:33 -04:00
parent 4fa0a539fa
commit 618fc19bd7
12 changed files with 217 additions and 20 deletions
+12 -1
View File
@@ -1 +1,12 @@
<div id="root"></div><script type="module" src="/src/main.tsx"></script>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Codex Helper</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+26 -14
View File
@@ -90,6 +90,7 @@ function App() {
if (!s.initialized)
return (
<Setup
version={s.version}
onDone={() => {
setS({ ...s, initialized: true });
setAuth(true);
@@ -100,9 +101,15 @@ function App() {
<BrowserRouter>
<Routes>
{auth ? (
<Route path="*" element={<Shell logout={() => setAuth(false)} />} />
<Route
path="*"
element={<Shell version={s.version} logout={() => setAuth(false)} />}
/>
) : (
<Route path="*" element={<Login done={() => setAuth(true)} />} />
<Route
path="*"
element={<Login version={s.version} done={() => setAuth(true)} />}
/>
)}
</Routes>
</BrowserRouter>
@@ -116,7 +123,15 @@ const Splash = () => (
<div className="spinner" />
</main>
);
function Setup({ onDone }: { onDone: () => void }) {
function Brand({ version }: { version?: string }) {
return (
<div className="logo">
<Zap /> Codex Helper
{version && <span className="version-badge">v{version}</span>}
</div>
);
}
function Setup({ version, onDone }: { version: string; onDone: () => void }) {
const [form, set] = useState({
username: "admin",
password: "",
@@ -135,9 +150,7 @@ function Setup({ onDone }: { onDone: () => void }) {
return (
<main className="setup">
<section className="hero">
<div className="logo">
<Zap /> Codex Helper
</div>
<Brand version={version} />
<h1>
连接你的 Codex
<br />
@@ -146,6 +159,9 @@ function Setup({ onDone }: { onDone: () => void }) {
<p>一个容器内完成用量洞察、重置提醒与账户管理。</p>
</section>
<form className="panel form" onSubmit={submit}>
<div className="setup-mobile-brand">
<Brand version={version} />
</div>
<small>首次初始化 · 1 / 1</small>
<h2>创建管理员</h2>
<label>
@@ -181,7 +197,7 @@ function Setup({ onDone }: { onDone: () => void }) {
</main>
);
}
function Login({ done }: { done: () => void }) {
function Login({ version, done }: { version: string; done: () => void }) {
const [u, setU] = useState("admin"),
[p, setP] = useState(""),
[e, setE] = useState("");
@@ -199,9 +215,7 @@ function Login({ done }: { done: () => void }) {
}
}}
>
<div className="logo">
<Zap /> Codex Helper
</div>
<Brand version={version} />
<h2>欢迎回来</h2>
<label>
用户名
@@ -221,7 +235,7 @@ function Login({ done }: { done: () => void }) {
</main>
);
}
function Shell({ logout }: { logout: () => void }) {
function Shell({ version, logout }: { version: string; logout: () => void }) {
const nav = useNavigate();
const [theme, setTheme] = useState(localStorage.theme || "system");
useEffect(() => {
@@ -236,9 +250,7 @@ function Shell({ logout }: { logout: () => void }) {
return (
<div className="app">
<aside>
<div className="logo">
<Zap /> Codex Helper
</div>
<Brand version={version} />
<nav>
<button onClick={() => nav("/")}>
<Activity />
+17
View File
@@ -76,6 +76,17 @@ select {
color: var(--accent);
width: 22px;
}
.version-badge {
padding: 2px 6px;
border: 1px solid var(--border);
border-radius: 999px;
color: var(--muted);
font-size: 11px;
font-weight: 600;
letter-spacing: 0;
line-height: 1.2;
white-space: nowrap;
}
.setup {
min-height: 100vh;
display: grid;
@@ -84,6 +95,9 @@ select {
gap: 8vw;
padding: 7vw;
}
.setup-mobile-brand {
display: none;
}
.hero h1 {
font-size: clamp(42px, 5vw, 76px);
line-height: 1.05;
@@ -576,6 +590,9 @@ a {
.hero {
display: none;
}
.setup-mobile-brand {
display: block;
}
.app {
grid-template-columns: 1fr;
}
+34
View File
@@ -49,6 +49,40 @@ async function openSettings(page: Page) {
await expect(page.locator(".settings-loading")).toHaveCount(0);
}
test("shows the build version in the authenticated brand", async ({ page }) => {
await openSettings(page);
const badge = page.locator("aside .version-badge");
await expect(badge).toBeVisible();
await expect(badge).toHaveText("vtest");
});
test("shows the build version before setup", async ({ page }) => {
await page.route("**/api/v1/system/status", (route) =>
route.fulfill({ json: { initialized: false, appServer: false, version: "test" } }),
);
await page.goto("/");
const badge = page.locator(
(page.viewportSize()?.width ?? 0) <= 800
? ".setup-mobile-brand .version-badge"
: ".hero .version-badge",
);
await expect(badge).toBeVisible();
await expect(badge).toHaveText("vtest");
});
test("shows the build version on login", async ({ page }) => {
await page.route("**/api/v1/system/status", (route) =>
route.fulfill({ json: { initialized: true, appServer: false, version: "test" } }),
);
await page.route("**/api/v1/auth/me", (route) =>
route.fulfill({ status: 401, json: { error: "unauthorized" } }),
);
await page.goto("/");
const badge = page.locator(".login .version-badge");
await expect(badge).toBeVisible();
await expect(badge).toHaveText("vtest");
});
async function layout(page: Page) {
return page.evaluate(() => {
const box = (selector: string) => {