-
Notifications
You must be signed in to change notification settings - Fork 1
fix(ui): harden dashboard health summary #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9d2bf18
560d0a1
6b1cf59
a778144
0ad2816
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import { createHash } from "node:crypto"; | ||
| import { createInterface } from "node:readline/promises"; | ||
| import { stdin as input, stdout as output } from "node:process"; | ||
| import { promises as fs, existsSync } from "node:fs"; | ||
|
|
@@ -90,6 +91,10 @@ import { | |
| getCodexCliConfigPath, | ||
| loadCodexCliState, | ||
| } from "./codex-cli/state.js"; | ||
| import { | ||
| getLastCodexCliSyncRun, | ||
| getLatestCodexCliSyncRollbackPlan, | ||
| } from "./codex-cli/sync.js"; | ||
| import { setCodexCliActiveSelection } from "./codex-cli/writer.js"; | ||
| import { ANSI } from "./ui/ansi.js"; | ||
| import { confirm } from "./ui/confirm.js"; | ||
|
|
@@ -3350,6 +3355,17 @@ interface DoctorFixAction { | |
| message: string; | ||
| } | ||
|
|
||
| interface DashboardHealthSummary { | ||
| label: string; | ||
| hint?: string; | ||
| } | ||
|
|
||
| interface DoctorReadOnlySummary { | ||
| severity: DoctorSeverity; | ||
| label: string; | ||
| hint: string; | ||
| } | ||
|
|
||
| function hasPlaceholderEmail(value: string | undefined): boolean { | ||
| if (!value) return false; | ||
| const email = value.trim().toLowerCase(); | ||
|
|
@@ -3392,6 +3408,14 @@ function getDoctorRefreshTokenKey( | |
| return trimmed || undefined; | ||
| } | ||
|
|
||
| function getReadOnlyDoctorRefreshTokenFingerprint( | ||
| refreshToken: unknown, | ||
| ): string | undefined { | ||
| const token = getDoctorRefreshTokenKey(refreshToken); | ||
| if (!token) return undefined; | ||
| return createHash("sha256").update(token).digest("hex").slice(0, 12); | ||
| } | ||
|
|
||
| function applyDoctorFixes(storage: AccountStorageV3): { changed: boolean; actions: DoctorFixAction[] } { | ||
| let changed = false; | ||
| const actions: DoctorFixAction[] = []; | ||
|
|
@@ -3925,6 +3949,300 @@ async function runDoctor(args: string[]): Promise<number> { | |
| return summary.error > 0 ? 1 : 0; | ||
| } | ||
|
|
||
| function formatCountNoun( | ||
| count: number, | ||
| singular: string, | ||
| plural = `${singular}s`, | ||
| ): string { | ||
| return `${count} ${count === 1 ? singular : plural}`; | ||
| } | ||
|
|
||
| function summarizeLatestCodexCliSyncState(): { | ||
| label: string; | ||
| hint: string; | ||
| } { | ||
| const latestRun = getLastCodexCliSyncRun(); | ||
| if (!latestRun) { | ||
| return { | ||
| label: "none", | ||
| hint: "No Codex CLI sync result recorded yet.", | ||
| }; | ||
| } | ||
|
|
||
| const outcomeLabel = | ||
| latestRun.outcome === "changed" | ||
| ? "changed" | ||
| : latestRun.outcome === "noop" | ||
| ? "noop" | ||
| : latestRun.outcome === "disabled" | ||
| ? "disabled" | ||
| : latestRun.outcome === "unavailable" | ||
| ? "unavailable" | ||
| : "error"; | ||
| const when = formatRelativeDateShort(latestRun.runAt); | ||
| const counts = [ | ||
| latestRun.summary.addedAccountCount > 0 | ||
| ? `add ${latestRun.summary.addedAccountCount}` | ||
| : null, | ||
| latestRun.summary.updatedAccountCount > 0 | ||
| ? `update ${latestRun.summary.updatedAccountCount}` | ||
| : null, | ||
| latestRun.summary.destinationOnlyPreservedCount > 0 | ||
| ? `preserve ${latestRun.summary.destinationOnlyPreservedCount}` | ||
| : null, | ||
| latestRun.summary.selectionChanged ? "selection changed" : null, | ||
| ].filter((value): value is string => value !== null); | ||
| const hintParts = [ | ||
| `Latest sync ${outcomeLabel}${when ? ` ${when}` : ""}`, | ||
| counts.length > 0 ? counts.join(" | ") : "no account deltas recorded", | ||
| latestRun.message?.trim() || undefined, | ||
| ].filter((value): value is string => Boolean(value)); | ||
|
|
||
| return { | ||
| label: `${outcomeLabel}${when ? ` ${when}` : ""}`, | ||
| hint: hintParts.join(" | "), | ||
| }; | ||
| } | ||
|
|
||
| function summarizeReadOnlyDoctorState( | ||
| storage: AccountStorageV3 | null, | ||
| ): DoctorReadOnlySummary { | ||
| if (!storage || storage.accounts.length === 0) { | ||
| return { | ||
| severity: "warn", | ||
| label: "setup pending", | ||
| hint: "No accounts are configured yet.", | ||
| }; | ||
| } | ||
|
|
||
| const warnings: string[] = []; | ||
| const errors: string[] = []; | ||
| const rawActiveCandidate = | ||
| storage.activeIndexByFamily?.codex ?? storage.activeIndex; | ||
| const normalizedActiveCandidate = Number.isFinite(rawActiveCandidate) | ||
| ? Math.trunc(rawActiveCandidate) | ||
| : Number.NaN; | ||
| const activeExists = | ||
| normalizedActiveCandidate >= 0 && | ||
| normalizedActiveCandidate < storage.accounts.length; | ||
| if (!activeExists) { | ||
| errors.push("active index is out of range"); | ||
| } | ||
|
|
||
| const disabledCount = storage.accounts.filter( | ||
| (account) => account.enabled === false, | ||
| ).length; | ||
| if (disabledCount >= storage.accounts.length) { | ||
| errors.push("all accounts are disabled"); | ||
| } else if (disabledCount > 0) { | ||
| warnings.push(`${disabledCount} disabled`); | ||
| } | ||
|
|
||
| const seenRefreshTokens = new Set<string>(); | ||
| let duplicateTokenCount = 0; | ||
| let placeholderEmailCount = 0; | ||
| let likelyInvalidRefreshTokenCount = 0; | ||
| for (const account of storage.accounts) { | ||
| const token = getReadOnlyDoctorRefreshTokenFingerprint( | ||
| account.refreshToken, | ||
| ); | ||
| if (token) { | ||
| if (seenRefreshTokens.has(token)) { | ||
| duplicateTokenCount += 1; | ||
| } else { | ||
| seenRefreshTokens.add(token); | ||
| } | ||
| } | ||
| if (hasPlaceholderEmail(account.email)) { | ||
| placeholderEmailCount += 1; | ||
| } | ||
| if (hasLikelyInvalidRefreshToken(account.refreshToken)) { | ||
| likelyInvalidRefreshTokenCount += 1; | ||
| } | ||
| } | ||
| if (duplicateTokenCount > 0) { | ||
| warnings.push(formatCountNoun(duplicateTokenCount, "duplicate token")); | ||
| } | ||
| if (placeholderEmailCount > 0) { | ||
| warnings.push(formatCountNoun(placeholderEmailCount, "placeholder email")); | ||
| } | ||
| if (likelyInvalidRefreshTokenCount > 0) { | ||
| warnings.push( | ||
| formatCountNoun(likelyInvalidRefreshTokenCount, "invalid refresh token"), | ||
| ); | ||
| } | ||
|
|
||
| if (errors.length > 0) { | ||
| return { | ||
| severity: "error", | ||
| label: formatCountNoun(errors.length, "error"), | ||
| hint: errors.concat(warnings).join(" | "), | ||
| }; | ||
| } | ||
| if (warnings.length > 0) { | ||
| return { | ||
| severity: "warn", | ||
| label: formatCountNoun(warnings.length, "warning"), | ||
| hint: warnings.join(" | "), | ||
| }; | ||
| } | ||
| return { | ||
| severity: "ok", | ||
| label: "ok", | ||
| hint: "No read-only doctor issues detected.", | ||
| }; | ||
| } | ||
|
|
||
| function logLoginMenuHealthSummaryWarning( | ||
| context: string, | ||
| error: unknown, | ||
| ): void { | ||
| const errorLabel = getRedactedFilesystemErrorLabel(error); | ||
| log.warn(`${context} [${errorLabel}]`); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| function formatLoginMenuRollbackHint( | ||
| rollbackPlan: Awaited<ReturnType<typeof getLatestCodexCliSyncRollbackPlan>>, | ||
| rollbackPlanLoadFailed = false, | ||
| ): string { | ||
| if (rollbackPlanLoadFailed) { | ||
| return "rollback state unavailable"; | ||
| } | ||
| if (rollbackPlan.status === "ready") { | ||
| const accountCount = rollbackPlan.accountCount; | ||
| if ( | ||
| typeof accountCount === "number" && | ||
| Number.isFinite(accountCount) && | ||
| accountCount >= 0 | ||
| ) { | ||
| return `checkpoint ready for ${Math.trunc(accountCount)} account(s)`; | ||
| } | ||
| return "checkpoint ready"; | ||
| } | ||
| const reason = collapseWhitespace(rollbackPlan.reason); | ||
| if (reason.toLowerCase().includes("unavailable")) { | ||
| return reason; | ||
| } | ||
| return rollbackPlan.snapshot ? "checkpoint unavailable" : "no rollback checkpoint available"; | ||
| } | ||
|
|
||
| async function buildLoginMenuHealthSummary( | ||
| storage: AccountStorageV3, | ||
| ): Promise<DashboardHealthSummary | null> { | ||
| if (storage.accounts.length === 0) { | ||
| return null; | ||
| } | ||
| const enabledCount = storage.accounts.filter( | ||
| (account) => account.enabled !== false, | ||
| ).length; | ||
| const disabledCount = storage.accounts.length - enabledCount; | ||
| let actionableRestores: Awaited< | ||
| ReturnType<typeof getActionableNamedBackupRestores> | ||
| > = { | ||
| assessments: [], | ||
| allAssessments: [], | ||
| totalBackups: 0, | ||
| }; | ||
| let rollbackPlan: Awaited< | ||
| ReturnType<typeof getLatestCodexCliSyncRollbackPlan> | ||
| > = { | ||
| status: "unavailable", | ||
| reason: "rollback state unavailable", | ||
| snapshot: null, | ||
| }; | ||
| let rollbackPlanLoadFailed = false; | ||
| let restoreStateUnavailable = false; | ||
| let syncSummary = { | ||
| label: "unknown", | ||
| hint: "Sync state unavailable.", | ||
| }; | ||
| let doctorSummary: DoctorReadOnlySummary = { | ||
| severity: "warn", | ||
| label: "unknown", | ||
| hint: "Doctor state unavailable.", | ||
| }; | ||
| const [restoresResult, rollbackResult] = await Promise.allSettled([ | ||
| getActionableNamedBackupRestores({ | ||
| currentStorage: storage, | ||
| }), | ||
| getLatestCodexCliSyncRollbackPlan(), | ||
| ]); | ||
| if (restoresResult.status === "fulfilled") { | ||
| actionableRestores = restoresResult.value; | ||
| } else { | ||
| restoreStateUnavailable = true; | ||
| logLoginMenuHealthSummaryWarning( | ||
| "Failed to load login menu restore health summary state", | ||
| restoresResult.reason, | ||
| ); | ||
| } | ||
| if (rollbackResult.status === "fulfilled") { | ||
| rollbackPlan = rollbackResult.value; | ||
| } else { | ||
| rollbackPlanLoadFailed = true; | ||
| logLoginMenuHealthSummaryWarning( | ||
| "Failed to load login menu rollback health summary state", | ||
| rollbackResult.reason, | ||
| ); | ||
| } | ||
| try { | ||
| syncSummary = summarizeLatestCodexCliSyncState(); | ||
| } catch (error) { | ||
| logLoginMenuHealthSummaryWarning( | ||
| "Failed to summarize login menu sync state", | ||
| error, | ||
| ); | ||
| } | ||
| try { | ||
| doctorSummary = summarizeReadOnlyDoctorState(storage); | ||
| } catch (error) { | ||
| logLoginMenuHealthSummaryWarning( | ||
| "Failed to summarize login menu doctor state", | ||
| error, | ||
| ); | ||
| } | ||
| const rollbackHint = formatLoginMenuRollbackHint( | ||
| rollbackPlan, | ||
| rollbackPlanLoadFailed, | ||
| ); | ||
| const rollbackUnavailable = | ||
| rollbackPlanLoadFailed || | ||
| rollbackPlan.snapshot !== null || | ||
| collapseWhitespace(rollbackPlan.reason).toLowerCase().includes("unavailable"); | ||
| const restoreLabel = | ||
| restoreStateUnavailable | ||
| ? "unavailable" | ||
| : actionableRestores.totalBackups > 0 | ||
| ? `${actionableRestores.assessments.length}/${actionableRestores.totalBackups} ready` | ||
| : "none"; | ||
| const rollbackLabel = | ||
| rollbackPlan.status === "ready" | ||
| ? "ready" | ||
| : rollbackUnavailable | ||
| ? "unavailable" | ||
| : "none"; | ||
| const accountLabel = | ||
| disabledCount > 0 | ||
| ? `${enabledCount}/${storage.accounts.length} enabled` | ||
| : `${storage.accounts.length} active`; | ||
| const hintParts = [ | ||
| `Accounts: ${enabledCount} enabled / ${disabledCount} disabled / ${storage.accounts.length} total`, | ||
| `Sync: ${syncSummary.hint}`, | ||
| restoreStateUnavailable | ||
| ? "Restore backups: state unavailable" | ||
| : `Restore backups: ${actionableRestores.assessments.length} actionable of ${actionableRestores.totalBackups} total`, | ||
| `Rollback: ${rollbackHint}`, | ||
| `Doctor: ${doctorSummary.hint}`, | ||
| ]; | ||
|
|
||
| return { | ||
| label: | ||
| `Pool ${accountLabel} | Sync ${syncSummary.label} | Restore ${restoreLabel} | ` + | ||
| `Rollback ${rollbackLabel} | Doctor ${doctorSummary.label}`, | ||
| hint: hintParts.join(" | "), | ||
|
Comment on lines
+4104
to
+4242
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't collapse unreadable restore/rollback state into
possible fix function formatLoginMenuRollbackHint(
rollbackPlan: Awaited<ReturnType<typeof getLatestCodexCliSyncRollbackPlan>>,
rollbackPlanLoadFailed = false,
): string {
if (rollbackPlanLoadFailed) {
return "rollback state unavailable";
}
if (rollbackPlan.status === "ready") {
const accountCount = rollbackPlan.accountCount;
if (
typeof accountCount === "number" &&
Number.isFinite(accountCount) &&
accountCount >= 0
) {
return `checkpoint ready for ${Math.trunc(accountCount)} account(s)`;
}
return "checkpoint ready";
}
- return rollbackPlan.snapshot ? "checkpoint unavailable" : "no rollback checkpoint available";
+ const reason = collapseWhitespace(rollbackPlan.reason);
+ if (reason.toLowerCase().includes("unavailable")) {
+ return reason;
+ }
+ return rollbackPlan.snapshot ? "checkpoint unavailable" : "no rollback checkpoint available";
}
async function buildLoginMenuHealthSummary(
storage: AccountStorageV3,
): Promise<DashboardHealthSummary | null> {
@@
let rollbackPlanLoadFailed = false;
+ let restoreStateUnavailable = false;
@@
try {
actionableRestores = await getActionableNamedBackupRestores({
currentStorage: storage,
});
} catch (error) {
+ restoreStateUnavailable = true;
logLoginMenuHealthSummaryWarning(
- "Failed to load login menu restore health summary state",
+ "Failed to load login menu restore health summary state",
error,
);
}
@@
+ const rollbackHint = formatLoginMenuRollbackHint(
+ rollbackPlan,
+ rollbackPlanLoadFailed,
+ );
const restoreLabel =
- actionableRestores.totalBackups > 0
+ restoreStateUnavailable
+ ? "unavailable"
+ : actionableRestores.totalBackups > 0
? `${actionableRestores.assessments.length}/${actionableRestores.totalBackups} ready`
: "none";
- const rollbackLabel = rollbackPlan.status === "ready" ? "ready" : "none";
+ const rollbackLabel =
+ rollbackPlan.status === "ready"
+ ? "ready"
+ : rollbackHint.toLowerCase().includes("unavailable")
+ ? "unavailable"
+ : "none";
@@
- `Rollback: ${formatLoginMenuRollbackHint(rollbackPlan, rollbackPlanLoadFailed)}`,
+ `Rollback: ${rollbackHint}`, |
||
| }; | ||
| } | ||
|
|
||
| async function handleManageAction( | ||
| storage: AccountStorageV3, | ||
| menuResult: Awaited<ReturnType<typeof promptLoginMode>>, | ||
|
|
@@ -4061,12 +4379,16 @@ async function runAuthLogin(): Promise<number> { | |
| }); | ||
| } | ||
| } | ||
| const flaggedStorage = await loadFlaggedAccounts(); | ||
| const [flaggedStorage, healthSummary] = await Promise.all([ | ||
| loadFlaggedAccounts(), | ||
| buildLoginMenuHealthSummary(currentStorage), | ||
| ]); | ||
|
|
||
| const menuResult = await promptLoginMode( | ||
| toExistingAccountInfo(currentStorage, quotaCache, displaySettings), | ||
| { | ||
| flaggedCount: flaggedStorage.accounts.length, | ||
| healthSummary: healthSummary ?? undefined, | ||
| statusMessage: showFetchStatus ? () => menuQuotaRefreshStatus : undefined, | ||
| }, | ||
| ); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: ndycode/codex-multi-auth
Length of output: 234
🏁 Script executed:
Repository: ndycode/codex-multi-auth
Length of output: 2919
🏁 Script executed:
Repository: ndycode/codex-multi-auth
Length of output: 1497
🏁 Script executed:
Repository: ndycode/codex-multi-auth
Length of output: 1473
🏁 Script executed:
Repository: ndycode/codex-multi-auth
Length of output: 3215
🏁 Script executed:
Repository: ndycode/codex-multi-auth
Length of output: 457
🏁 Script executed:
Repository: ndycode/codex-multi-auth
Length of output: 808
add test coverage for
summarizeLatestCodexCliSyncStateand retry on file lock errors.blocking file i/o confirmed:
summarizeLatestCodexCliSyncState()callsgetLastCodexCliSyncRun()which callsreadLatestSyncHistorySync()at lib/sync-history.ts:261 usingreadFileSync(). in-memory cache at lib/codex-cli/sync.ts:153 prevents repeated blocks after first call, and fire-and-forget async load at lib/codex-cli/sync.ts:330 refills it. however, no regression tests exist forsummarizeLatestCodexCliSyncState()itself, andreadLatestSyncHistorySync()doesn't retry on EBUSY despite windows file lock risks—compare toRETRYABLE_REMOVE_CODESat lib/sync-history.ts:13. add vitest coverage for this function and implement EBUSY retry logic inreadLatestSyncHistorySync().🤖 Prompt for AI Agents