feat(ops-web): add operations console frontend

Vue 3 + Ant Design Vue SPA scaffold for the internal ops console. Ships
the auth, account list, and audit log views that consume the new ops-api
endpoints, plus the nginx/vite/typecheck plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 11:33:17 +08:00
parent f63e800f21
commit f5254f6a27
20 changed files with 1830 additions and 7 deletions
+61
View File
@@ -0,0 +1,61 @@
const STORAGE_KEY = "geo.ops-web.session";
export interface OperatorView {
id: number;
username: string;
display_name: string;
email: string | null;
role: string;
status: string;
}
export interface OpsSession {
accessToken: string | null;
expiresAt: number | null;
operator: OperatorView | null;
}
function emptySession(): OpsSession {
return { accessToken: null, expiresAt: null, operator: null };
}
function hasStorage(): boolean {
return typeof window !== "undefined" && "localStorage" in window;
}
export function readStoredSession(): OpsSession {
if (!hasStorage()) return emptySession();
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return emptySession();
const parsed = JSON.parse(raw) as Partial<OpsSession>;
return {
accessToken: typeof parsed.accessToken === "string" ? parsed.accessToken : null,
expiresAt: typeof parsed.expiresAt === "number" ? parsed.expiresAt : null,
operator:
parsed.operator && typeof parsed.operator === "object"
? (parsed.operator as OperatorView)
: null,
};
} catch {
return emptySession();
}
}
export function writeStoredSession(session: OpsSession): void {
if (!hasStorage()) return;
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
} catch {
// ignore quota / serialization errors
}
}
export function clearStoredSession(): void {
if (!hasStorage()) return;
try {
window.localStorage.removeItem(STORAGE_KEY);
} catch {
// noop
}
}