- Display credentials in responsive cards with quota progress bars - Add request statistics and detailed health and cooldown information - Consolidate nine credential settings into a dialog with partial updates - Reuse Codex quota reset logic and confirmation across pages - Move notes above card actions and hide the healthy status indicator - Add translations and regression coverage
144 lines
7.5 KiB
TypeScript
144 lines
7.5 KiB
TypeScript
import { useMemo, useSyncExternalStore } from 'react';
|
|
import { ChevronDown, CircleHelp, Clock3, Info, PauseCircle, ShieldAlert } from 'lucide-react';
|
|
import { useI18n } from '../i18n';
|
|
import {
|
|
authFileHealth,
|
|
cooldownReasonKey,
|
|
normalizeAuthFileCooldowns,
|
|
summarizeAuthFileCooldowns,
|
|
type AuthFileCooldownSnapshot,
|
|
} from '../services/authFileHealth';
|
|
import './AuthFileHealthStatus.css';
|
|
|
|
let clockNow = Date.now();
|
|
let timer: ReturnType<typeof setInterval> | undefined;
|
|
const listeners = new Set<() => void>();
|
|
const getClockSnapshot = () => clockNow;
|
|
const subscribeClock = (listener: () => void) => {
|
|
listeners.add(listener);
|
|
if (!timer) {
|
|
clockNow = Date.now();
|
|
timer = setInterval(() => {
|
|
clockNow = Date.now();
|
|
listeners.forEach((notify) => notify());
|
|
}, 1000);
|
|
}
|
|
return () => {
|
|
listeners.delete(listener);
|
|
if (!listeners.size) {
|
|
clearInterval(timer);
|
|
timer = undefined;
|
|
}
|
|
};
|
|
};
|
|
|
|
type HealthProps = {
|
|
file: Record<string, unknown>;
|
|
snapshot?: AuthFileCooldownSnapshot;
|
|
};
|
|
|
|
export function AuthFileHealthStatus({ file, receivedAtMs, observedAt }: {
|
|
file: Record<string, unknown>;
|
|
receivedAtMs: number;
|
|
observedAt?: string;
|
|
}) {
|
|
const snapshot = useMemo(
|
|
() => normalizeAuthFileCooldowns(file.cooldowns, receivedAtMs, observedAt),
|
|
[file, receivedAtMs, observedAt],
|
|
);
|
|
return snapshot?.records?.length
|
|
? <TimedHealthStatus file={file} snapshot={snapshot} />
|
|
: <HealthStatus file={file} snapshot={snapshot} nowMs={receivedAtMs} />;
|
|
}
|
|
|
|
function TimedHealthStatus(props: HealthProps) {
|
|
const nowMs = useSyncExternalStore(subscribeClock, getClockSnapshot, getClockSnapshot);
|
|
return <HealthStatus {...props} nowMs={nowMs} />;
|
|
}
|
|
|
|
function HealthStatus({ file, snapshot, nowMs }: HealthProps & { nowMs: number }) {
|
|
const { t, formatNumber, formatDate } = useI18n();
|
|
const health = authFileHealth(file);
|
|
const cooldown = summarizeAuthFileCooldowns(snapshot, nowMs);
|
|
const hasTimers = cooldown.rows.length > 0;
|
|
const healthy = health.label === 'authFiles.health.active';
|
|
if (healthy && !hasTimers && snapshot?.records !== null) return null;
|
|
const hasDetails = hasTimers || Boolean(health.message);
|
|
const reasonSet = new Set(cooldown.active.map(({ record }) => cooldownReasonKey(record.reason)));
|
|
if (reasonSet.has('authFiles.health.reason.credentialQuota')) reasonSet.delete('authFiles.health.reason.quota');
|
|
const reasons = Array.from(reasonSet);
|
|
const title = health.disabled ? t(health.label)
|
|
: cooldown.elapsed ? t('authFiles.health.elapsed')
|
|
: cooldown.active.length ? reasons.map((key) => t(key)).join(' · ')
|
|
: healthy ? t('authFiles.health.unknownCooldown') : t(health.label);
|
|
const tone = health.disabled || (healthy && !hasTimers) ? 'neutral' : hasTimers ? 'warning' : health.tone;
|
|
const separateState = hasTimers && !health.disabled && (
|
|
health.status === 'pending' || health.status === 'refreshing'
|
|
|| health.label === 'authFiles.health.reason.invalidGrant'
|
|
|| health.label === 'authFiles.health.reason.unauthorized'
|
|
|| health.label === 'authFiles.health.reason.tokenExpired'
|
|
);
|
|
const Icon = health.disabled ? PauseCircle : hasTimers ? Clock3
|
|
: tone === 'error' ? ShieldAlert
|
|
: tone === 'warning' || tone === 'info' ? Info : CircleHelp;
|
|
const duration = (seconds: number) => seconds < 60
|
|
? t('authFiles.health.seconds', { count: formatNumber(seconds) })
|
|
: seconds < 3600 ? t('authFiles.health.minutes', { count: formatNumber(Math.ceil(seconds / 60)) })
|
|
: t('authFiles.health.hours', { count: formatNumber(Math.ceil(seconds / 3600)) });
|
|
const scope = cooldown.credentialWide
|
|
? cooldown.modelCount ? t('authFiles.health.credentialModels', { count: cooldown.modelCount })
|
|
: t('authFiles.health.credential')
|
|
: cooldown.modelCount ? t('authFiles.health.models', { count: cooldown.modelCount }) : '';
|
|
const formatTimestamp = (value: string) => formatDate(value, {
|
|
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
|
|
});
|
|
const summary = <>
|
|
<Icon size={16} className="auth-health-icon" aria-hidden="true" />
|
|
<span className="auth-health-summary-content">
|
|
<span className="auth-health-heading"><strong>{title}</strong>{scope ? <span>{scope}</span> : null}</span>
|
|
{separateState ? <span className="auth-health-hint">{t('authFiles.health.coreStatus', { status: t(health.label) })}</span> : null}
|
|
{health.message && !hasTimers && !health.label.startsWith('authFiles.health.reason.')
|
|
? <span className="auth-health-message-preview">{health.message}</span> : null}
|
|
{cooldown.elapsed ? <span className="auth-health-hint">{t('authFiles.health.expiredHint')}</span> : null}
|
|
{snapshot?.records === null && !healthy ? <span className="auth-health-hint">{t('authFiles.health.unknownCooldown')}</span> : null}
|
|
{snapshot === undefined && health.tone === 'error' ? <span className="auth-health-hint">{t('authFiles.health.missingCooldown')}</span> : null}
|
|
</span>
|
|
{cooldown.active.length > 0 ? <span className="auth-health-countdown">
|
|
{t('authFiles.health.earliest', { time: duration(cooldown.earliestSeconds) })}
|
|
</span> : null}
|
|
{hasDetails ? <span className="auth-health-disclosure"><span>{t('authFiles.health.details')}</span><ChevronDown size={14} aria-hidden="true" /></span> : null}
|
|
</>;
|
|
|
|
return (
|
|
<section className={`auth-file-health ${tone}${hasDetails ? ' detailed' : ''}`} aria-label={t('authFiles.health.title')}>
|
|
{hasDetails ? <details>
|
|
<summary className="auth-health-summary">{summary}</summary>
|
|
<div className="auth-health-body">
|
|
{hasTimers && !healthy ? <p className="auth-health-core-state">{t('authFiles.health.coreStatus', { status: t(health.label) })}</p> : null}
|
|
{health.message ? <div className="auth-health-core-message">
|
|
<span>{t('authFiles.health.coreMessage')}</span><p>{health.message}</p>
|
|
</div> : null}
|
|
<ul className="auth-health-records">
|
|
{cooldown.rows.map(({ record, remainingSeconds }, index) => (
|
|
<li key={`${record.scope}:${record.model ?? ''}:${index}`}>
|
|
<div className="auth-health-record-heading">
|
|
<strong>{record.scope === 'credential' ? t('authFiles.health.credentialScope') : record.model}</strong>
|
|
<span>{remainingSeconds > 0 ? t('authFiles.health.remaining', { time: duration(remainingSeconds) }) : t('authFiles.health.waiting')}</span>
|
|
</div>
|
|
<div className="auth-health-record-meta">
|
|
<span>{t(cooldownReasonKey(record.reason))}</span>
|
|
{record.httpStatus !== undefined ? <span>HTTP {record.httpStatus}</span> : null}
|
|
{record.backoffLevel !== undefined ? <span title={t('authFiles.health.backoffHint')}>{t('authFiles.health.backoff', { level: record.backoffLevel })}</span> : null}
|
|
</div>
|
|
<div className="auth-health-hint">{t('authFiles.health.deadline')} <time dateTime={record.retryAt}>{formatTimestamp(record.retryAt)}</time></div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{snapshot?.observedAt ? <p className="auth-health-hint">{t('authFiles.health.observed')} <time dateTime={snapshot.observedAt}>{formatTimestamp(snapshot.observedAt)}</time></p> : null}
|
|
{hasTimers ? <p className="auth-health-hint">{t('authFiles.health.note')}</p> : null}
|
|
</div>
|
|
</details> : <div className="auth-health-summary">{summary}</div>}
|
|
</section>
|
|
);
|
|
}
|