admin page fix

This commit is contained in:
user 2025-09-26 15:05:21 +03:00
parent d08a02b6e7
commit b64b7a3880
3 changed files with 136 additions and 10 deletions

View File

@ -13,12 +13,18 @@ export const App = () => {
const [, expand] = useExpand(); const [, expand] = useExpand();
useEffect(() => { useEffect(() => {
WebApp.enableClosingConfirmation(); if (!WebApp) {
expand(); return;
}
WebApp.setHeaderColor('#1d1d1b'); WebApp.enableClosingConfirmation?.();
WebApp.setBackgroundColor('#1d1d1b'); if (typeof expand === 'function') {
}, []); expand();
}
WebApp.setHeaderColor?.('#1d1d1b');
WebApp.setBackgroundColor?.('#1d1d1b');
}, [WebApp, expand]);
return ( return (
<Providers> <Providers>

View File

@ -52,6 +52,29 @@ const formatDate = (iso?: string | null) => {
return dateTimeFormatter.format(date); return dateTimeFormatter.format(date);
}; };
const formatUnknown = (value: unknown): string => {
if (value === null || value === undefined) {
return "—";
}
if (typeof value === "string") {
return value || "—";
}
if (typeof value === "number" || typeof value === "bigint") {
return numberFormatter.format(Number(value));
}
if (typeof value === "boolean") {
return value ? "Да" : "Нет";
}
if (value instanceof Date) {
return formatDate(value.toISOString());
}
try {
return JSON.stringify(value);
} catch (error) {
return String(value);
}
};
const Section = ({ const Section = ({
id, id,
title, title,
@ -661,7 +684,7 @@ export const AdminPage = () => {
<h3 className="text-sm font-semibold uppercase tracking-wide text-slate-400">Окружение</h3> <h3 className="text-sm font-semibold uppercase tracking-wide text-slate-400">Окружение</h3>
<dl className="mt-3 space-y-2"> <dl className="mt-3 space-y-2">
{Object.entries(env).map(([key, value]) => ( {Object.entries(env).map(([key, value]) => (
<InfoRow key={key} label={key}>{value ?? "—"}</InfoRow> <InfoRow key={key} label={key}>{formatUnknown(value)}</InfoRow>
))} ))}
</dl> </dl>
</div> </div>
@ -702,8 +725,8 @@ export const AdminPage = () => {
{service_config.map((item) => ( {service_config.map((item) => (
<tr key={item.key}> <tr key={item.key}>
<td className="px-3 py-2 font-mono text-xs text-slate-300">{item.key}</td> <td className="px-3 py-2 font-mono text-xs text-slate-300">{item.key}</td>
<td className="px-3 py-2">{item.value ?? "—"}</td> <td className="px-3 py-2">{formatUnknown(item.value)}</td>
<td className="px-3 py-2 font-mono text-xs text-slate-500">{item.raw ?? "—"}</td> <td className="px-3 py-2 font-mono text-xs text-slate-500">{formatUnknown(item.raw)}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@ -770,7 +793,7 @@ export const AdminPage = () => {
<tr key={task.id} className="hover:bg-slate-900/50"> <tr key={task.id} className="hover:bg-slate-900/50">
<td className="px-3 py-2 font-mono text-xs text-slate-300">{task.id}</td> <td className="px-3 py-2 font-mono text-xs text-slate-300">{task.id}</td>
<td className="px-3 py-2">{task.destination || "—"}</td> <td className="px-3 py-2">{task.destination || "—"}</td>
<td className="px-3 py-2">{task.amount || "—"}</td> <td className="px-3 py-2">{formatUnknown(task.amount)}</td>
<td className="px-3 py-2"><Badge tone={task.status === "done" ? "success" : task.status === "failed" ? "danger" : "neutral"}>{task.status}</Badge></td> <td className="px-3 py-2"><Badge tone={task.status === "done" ? "success" : task.status === "failed" ? "danger" : "neutral"}>{task.status}</Badge></td>
<td className="px-3 py-2">{task.epoch ?? "—"} · {task.seqno ?? "—"}</td> <td className="px-3 py-2">{task.epoch ?? "—"} · {task.seqno ?? "—"}</td>
<td className="px-3 py-2 font-mono text-xs text-slate-500">{task.transaction_hash || "—"}</td> <td className="px-3 py-2 font-mono text-xs text-slate-500">{task.transaction_hash || "—"}</td>
@ -836,7 +859,7 @@ export const AdminPage = () => {
</td> </td>
<td className="px-3 py-2">{node.version || "—"}</td> <td className="px-3 py-2">{node.version || "—"}</td>
<td className="px-3 py-2">{formatDate(node.last_seen)}</td> <td className="px-3 py-2">{formatDate(node.last_seen)}</td>
<td className="px-3 py-2">{node.notes || "—"}</td> <td className="px-3 py-2">{formatUnknown(node.notes)}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View File

@ -0,0 +1,97 @@
const TOKEN_STORAGE_KEY = '__admin_panel_token__';
const HEADER_STORAGE_KEY = '__admin_panel_header__';
const COOKIE_STORAGE_KEY = '__admin_panel_cookie__';
const DEFAULT_HEADER_NAME = 'X-Admin-Token';
const DEFAULT_COOKIE_NAME = 'admin_session';
const DEFAULT_MAX_AGE = 172800; // 48h
const isBrowser = typeof window !== 'undefined';
export type AdminAuthSnapshot = {
token: string | null;
headerName: string;
cookieName: string;
maxAge: number;
};
export const getAdminAuthSnapshot = (): AdminAuthSnapshot => {
if (!isBrowser) {
return {
token: null,
headerName: DEFAULT_HEADER_NAME,
cookieName: DEFAULT_COOKIE_NAME,
maxAge: DEFAULT_MAX_AGE,
};
}
const token = window.localStorage.getItem(TOKEN_STORAGE_KEY);
const headerName = window.localStorage.getItem(HEADER_STORAGE_KEY) || DEFAULT_HEADER_NAME;
const cookieName = window.localStorage.getItem(COOKIE_STORAGE_KEY) || DEFAULT_COOKIE_NAME;
return {
token,
headerName,
cookieName,
maxAge: DEFAULT_MAX_AGE,
};
};
export const persistAdminAuth = ({
token,
headerName,
cookieName,
maxAge,
}: {
token: string;
headerName?: string | null;
cookieName?: string | null;
maxAge?: number | null;
}) => {
if (!isBrowser) {
return;
}
window.localStorage.setItem(TOKEN_STORAGE_KEY, token);
if (headerName) {
window.localStorage.setItem(HEADER_STORAGE_KEY, headerName);
}
if (cookieName) {
window.localStorage.setItem(COOKIE_STORAGE_KEY, cookieName);
}
// Best effort: mirror cookie locally to support same-origin deployments.
const targetCookieName = cookieName || DEFAULT_COOKIE_NAME;
const cookieParts = [
`${targetCookieName}=${encodeURIComponent(token)}`,
'Path=/',
`Max-Age=${maxAge ?? DEFAULT_MAX_AGE}`,
'SameSite=Lax',
];
if (window.location.protocol === 'https:') {
cookieParts.push('Secure');
}
document.cookie = cookieParts.join('; ');
};
export const clearAdminAuth = () => {
if (!isBrowser) {
return;
}
const snapshot = getAdminAuthSnapshot();
window.localStorage.removeItem(TOKEN_STORAGE_KEY);
window.localStorage.removeItem(HEADER_STORAGE_KEY);
window.localStorage.removeItem(COOKIE_STORAGE_KEY);
if (snapshot.cookieName) {
const cookieParts = [
`${snapshot.cookieName}=`,
'Path=/',
'Max-Age=0',
'SameSite=Lax',
];
if (window.location.protocol === 'https:') {
cookieParts.push('Secure');
}
document.cookie = cookieParts.join('; ');
}
};