Skip to content
8 changes: 6 additions & 2 deletions gui/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ let promptCancelled = false;
type AdminTokenPrompt = (verifyToken: AdminTokenVerifier) => Promise<string | null>;
let requestAdminToken: AdminTokenPrompt = promptForAdminToken;

/** Document path re-fetched to mint a fresh loopback GUI session (server injects meta tags). */
const SESSION_REBOOTSTRAP_PATH = "/";
/**
* Document path re-fetched to mint a fresh loopback GUI session (server injects meta tags).
* Deliberately NOT "/": the Vite dev server owns that route for the app shell, so the dev
* proxy forwards this dedicated extensionless path to the backend with the original host.
*/
const SESSION_REBOOTSTRAP_PATH = "/opencodex-session";
Comment thread
Wibias marked this conversation as resolved.
/** Safe authenticated read used to validate a raw admin token before closing the sign-in form. */
const ADMIN_TOKEN_VALIDATION_PATH = "/api/settings";

Expand Down
11 changes: 7 additions & 4 deletions gui/src/components/CodexAccountPool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ export default function CodexAccountPool({ apiBase, accountModeState = null, ban
const switchActionLabel = t(accountModeState === "direct" ? "codexAuth.prepareForPool" : "codexAuth.setAsNext");
const pauseBusy = pauseUpdatingId !== null || pausingExhausted;
const autoSwitchThreshold = autoSwitch.threshold ?? 0;
// The standalone Codex Auth page keeps the doctor-copy affordance; the embedded
// Providers workspace account surface does not.
const showDoctorCopy = !embedded;

return (
<div>
Expand Down Expand Up @@ -290,8 +293,8 @@ export default function CodexAccountPool({ apiBase, accountModeState = null, ban
pauseUpdatingId={pauseUpdatingId}
pauseBusy={pauseBusy}
onOpenReset={openResetPopup}
onCopyDoctor={copyDoctor}
doctorCopyOutcomeFor={doctorCopy.outcomeFor}
onCopyDoctor={showDoctorCopy ? copyDoctor : undefined}
doctorCopyOutcomeFor={showDoctorCopy ? doctorCopy.outcomeFor : undefined}
/>

<div className="section-sep">
Expand Down Expand Up @@ -322,8 +325,8 @@ export default function CodexAccountPool({ apiBase, accountModeState = null, ban
onReauth={openReauth}
onEditAlias={editAlias}
onRemove={remove}
onCopyDoctor={copyDoctor}
doctorCopyOutcomeFor={doctorCopy.outcomeFor}
onCopyDoctor={showDoctorCopy ? copyDoctor : undefined}
doctorCopyOutcomeFor={showDoctorCopy ? doctorCopy.outcomeFor : undefined}
/>
</>
)}
Expand Down
20 changes: 10 additions & 10 deletions gui/src/components/combo-workspace-add-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export function AddComboModal({
<IconX width={16} height={16} />
</button>
</div>
<p className="muted" style={{ marginTop: 0 }}>{t("cws.addSubtitle")}</p>
<p className="muted" style={{ marginTop: 0, maxWidth: "62ch", overflowWrap: "anywhere" }}>{t("cws.addSubtitle")}</p>
{error && <Notice tone="err">{error}</Notice>}
<div className="cwi-modal-form">
<div className="cwi-field">
Expand All @@ -118,7 +118,7 @@ export function AddComboModal({
model: comboPublicModelId(e.target.value, d.alias),
}))}
/>
<p className="muted" style={{ fontSize: 12, margin: "4px 0 0" }}>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{t("cws.field.idInternalHint")}
</p>
</div>
Expand All @@ -136,10 +136,10 @@ export function AddComboModal({
model: comboPublicModelId(d.id, e.target.value),
}))}
/>
<p className="muted" style={{ fontSize: 12, margin: "4px 0 0" }}>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{t("cws.field.aliasHint")}
</p>
<p className="muted" style={{ fontSize: 12, margin: "4px 0 0" }}>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{t("cws.field.idHint", {
model: draft.id.trim() ? comboPublicModelId(draft.id, draft.alias) : "…",
})}
Expand All @@ -152,7 +152,7 @@ export function AddComboModal({
disabled={busy}
onChange={(strategy) => setDraft((d) => ({ ...d, strategy }))}
/>
<p className="muted" style={{ fontSize: 12, margin: "6px 0 0" }}>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{draft.strategy === "failover" ? t("cws.strategy.failoverHint") : t("cws.strategy.roundRobinHint")}
</p>
</div>
Expand All @@ -165,7 +165,7 @@ export function AddComboModal({
allowedEfforts={allowedEfforts}
onChange={(defaultEffort) => setDraft((d) => ({ ...d, defaultEffort }))}
/>
<p className="muted" style={{ fontSize: 12, margin: "4px 0 0" }}>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{t("cws.field.defaultEffortHint")}
</p>
</div>
Expand All @@ -186,23 +186,23 @@ export function AddComboModal({
setDraft((d) => ({ ...d, stickyLimit }));
}}
/>
<p className="muted" style={{ fontSize: 12, margin: "4px 0 0" }}>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{t("cws.field.stickyLimitHint")}
</p>
</div>
)}
<div className="cwi-field">
<span className="field-label">{t("cws.targets")}</span>
<p className="muted" style={{ fontSize: 12, margin: "0 0 8px" }}>
{draft.strategy === "failover" ? t("cws.targets.failoverHint") : t("cws.targets.roundRobinHint")}
</p>
<TargetEditor
targets={draft.targets}
strategy={draft.strategy}
providers={providers}
models={models}
onChange={(targets) => setDraft((d) => ({ ...d, targets }))}
/>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{draft.strategy === "failover" ? t("cws.targets.failoverHint") : t("cws.targets.roundRobinHint")}
</p>
</div>
</div>
<div className="cwi-modal-actions">
Expand Down
16 changes: 8 additions & 8 deletions gui/src/components/combo-workspace-detail-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export function DetailPanel({
model: comboPublicModelId(e.target.value, d.alias),
}))}
/>
<p className="muted" style={{ fontSize: 12, margin: "4px 0 0" }}>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{isCreate
? t("cws.field.idInternalHint")
: t("cws.field.idHintEdit", { model: comboPublicModelId(draft.id, draft.alias) })}
Expand All @@ -207,7 +207,7 @@ export function DetailPanel({
model: comboPublicModelId(d.id, e.target.value),
}))}
/>
<p className="muted" style={{ fontSize: 12, margin: "4px 0 0" }}>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{t("cws.field.aliasHint")}
</p>
</div>
Expand All @@ -218,7 +218,7 @@ export function DetailPanel({
disabled={busy}
onChange={(strategy) => updateDraft((d) => ({ ...d, strategy }))}
/>
<p className="muted" style={{ fontSize: 12, margin: "6px 0 0" }}>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{draft.strategy === "failover" ? t("cws.strategy.failoverHint") : t("cws.strategy.roundRobinHint")}
</p>
</div>
Expand All @@ -231,7 +231,7 @@ export function DetailPanel({
allowedEfforts={allowedEfforts}
onChange={(defaultEffort) => updateDraft((d) => ({ ...d, defaultEffort }))}
/>
<p className="muted" style={{ fontSize: 12, margin: "4px 0 0" }}>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{t("cws.field.defaultEffortHint")}
</p>
</div>
Expand All @@ -256,22 +256,22 @@ export function DetailPanel({
)}
<div className="cwi-field">
<span className="field-label">{t("cws.targets")}</span>
<p className="muted" style={{ fontSize: 12, margin: "0 0 8px" }}>
{draft.strategy === "failover" ? t("cws.targets.failoverHint") : t("cws.targets.roundRobinHint")}
</p>
<TargetEditor
targets={draft.targets}
strategy={draft.strategy}
providers={providers}
models={models}
onChange={(targets) => updateDraft((d) => ({ ...d, targets }))}
/>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{draft.strategy === "failover" ? t("cws.targets.failoverHint") : t("cws.targets.roundRobinHint")}
</p>
</div>
</div>
) : (
<section className="pwi-section">
<h3 className="pwi-section-title">{t("cws.aboutTitle")}</h3>
<p className="muted" style={{ margin: 0 }}>{t("cws.aboutBody")}</p>
<p className="muted" style={{ margin: 0, maxWidth: "70ch", overflowWrap: "anywhere" }}>{t("cws.aboutBody")}</p>
</section>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,31 +159,32 @@ export default function AnthropicAccountPoolSettings({
: t("anthropicPool.disabledDesc")}
</div>
</div>
<label className="toggle" style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
<input
type="checkbox"
checked={enabled}
disabled={toggleDisabled}
onChange={(event) => {
const next = event.target.checked;
void save({
enabled: next,
threshold,
strategy,
stickyLimit,
});
}}
/>
<span>{enabled ? t("anthropicPool.on") : t("anthropicPool.off")}</span>
</label>
<button
type="button"
className={`toggle ${enabled ? "on" : ""}`}
disabled={toggleDisabled}
aria-pressed={enabled}
aria-label={t("anthropicPool.title")}
title={enabled ? t("anthropicPool.on") : t("anthropicPool.off")}
onClick={() => {
void save({
enabled: !enabled,
threshold,
strategy,
stickyLimit,
});
}}
>
<span className="toggle-knob" />
</button>
</div>

<div
role="alert"
className="card-sub"
style={{
marginTop: 10,
padding: "8px 10px",
padding: "10px 16px",
border: "1px solid var(--border, #c9a227)",
borderRadius: 6,
background: "color-mix(in srgb, var(--warn, #c9a227) 12%, transparent)",
Expand Down
11 changes: 0 additions & 11 deletions gui/src/components/provider-workspace/ProviderAuthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@ import type { WorkspaceItem } from "../../provider-workspace/catalog";
import { oauthAccountDisplayLabel, providerAuthSurface } from "../../provider-workspace/auth";
import { displayAccountId } from "../../lib/privacy";
import {
doctorCopyButtonLabel,
formatOAuthHealthLabel,
formatOAuthHealthSummary,
oauthHealthBadgeClass,
oauthHealthIsCooldown,
oauthHealthShowsDoctor,
oauthHealthShowsReauth,
} from "../../oauth-health-display";
import CodexAccountPool from "../CodexAccountPool";
Expand All @@ -26,7 +24,6 @@ import { useCopyFeedback } from "../use-copy-feedback";
import type { CodexAccountPoolController } from "../../hooks/useCodexAccountPool";
import type { AccountLoadState, OAuthAccountRow, ApiKeyRow, LoginHint, ProviderAuthHandlers } from "./types";

const DOCTOR_CMD = "ocx doctor";
const QUOTA_ENRICH_RESERVE_MS = 4_000;
const EMPTY_OAUTH_ACCOUNTS: OAuthAccountRow[] = [];
const EMPTY_API_KEYS: ApiKeyRow[] = [];
Expand Down Expand Up @@ -56,7 +53,6 @@ export default function ProviderAuthPanel({
const [keyBusy, setKeyBusy] = useState(false);
const [reserveQuotaSlots, setReserveQuotaSlots] = useState(false);
const deviceCodeCopy = useCopyFeedback<string>();
const doctorCopy = useCopyFeedback<string>();

// Soft &quota=1 enrichment lands after the local account list. Reserve stacked
// bar height briefly so bars don't shove rows when WHAM returns.
Expand Down Expand Up @@ -206,12 +202,10 @@ export default function ProviderAuthPanel({
const switching = switchingAccountId === account.id;
const healthStatus = account.health?.status;
const showReauth = Boolean(account.needsReauth) || oauthHealthShowsReauth(healthStatus);
const showDoctor = oauthHealthShowsDoctor(healthStatus);
const inCooldown = oauthHealthIsCooldown(healthStatus);
const maskedId = displayAccountId(account.id);
const healthLabel = formatOAuthHealthLabel(t, account.health);
const healthSummary = formatOAuthHealthSummary(t, item.name, account.id, account.health);
const copyDoctor = () => { doctorCopy.copy(DOCTOR_CMD, account.id); };
return (
<li key={account.id} className={`pwi-auth-acct${account.active ? " pwi-auth-acct--active" : ""}`}>
<div className={`pwi-auth-row${account.active ? " pwi-auth-row--active" : ""}`}>
Expand Down Expand Up @@ -248,11 +242,6 @@ export default function ProviderAuthPanel({
{t("pws.reauthenticate")}
</button>
)}
{showDoctor && (
<button type="button" className="btn btn-ghost btn-sm codex-auth-action-btn" onClick={copyDoctor}>
<span aria-live="polite">{doctorCopyButtonLabel(t, doctorCopy.outcomeFor(account.id))}</span>
</button>
)}
<button type="button" className="btn btn-ghost btn-sm"
onClick={() => void authHandlers.onEditAlias(item.name, "oauth", account.id, account.alias)}>
{t("prov.editAlias")}
Expand Down
95 changes: 95 additions & 0 deletions gui/src/components/provider-workspace/ProviderCapacityQuota.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* ProviderCapacityQuota — per-provider quota bars plus the aggregation context the
* quota API attaches for pooled Codex providers (estimate, excluded accounts, recovery
* rows, current-account breakdown). Shared by the aggregate dashboard and the
* single-provider Overview so both surfaces present the same capacity semantics.
*/
import { useT, useI18n } from "../../i18n/shared";
import {
accountQuotaFromReport,
capacityAggregationFromReport,
type CapacityWindowView,
type ProviderQuotaReportView,
} from "../../provider-workspace/report";
import { type QuotaWindowKey } from "../QuotaBars";
import QuotaBars from "../QuotaBars";

export function ProviderCapacityQuota({ report, pending }: { report: ProviderQuotaReportView; pending: boolean }) {
const t = useT();
const { locale } = useI18n();
const aggregation = capacityAggregationFromReport(report);
const primaryQuota = accountQuotaFromReport(report);
const showsAggregate = aggregation?.presentation === "aggregate";
const incompleteWindowKeys = new Set<QuotaWindowKey>();
const incompleteCustomWindowLabels = new Set<string>();
if (showsAggregate && aggregation) {
if (aggregation.fiveHour?.incomplete) incompleteWindowKeys.add("fiveHour");
if (aggregation.weekly?.incomplete) incompleteWindowKeys.add("weekly");
if (aggregation.monthly?.incomplete) incompleteWindowKeys.add("monthly");
for (const window of aggregation.customWindows ?? []) {
if (window.incomplete) incompleteCustomWindowLabels.add(window.label);
}
}
const recoveryRows: Array<{ key: number; label: string; window: CapacityWindowView }> = showsAggregate && aggregation ? [
...(aggregation.fiveHour ? [{ key: 0, label: t("codexAuth.fiveHour"), window: aggregation.fiveHour }] : []),
...(aggregation.weekly ? [{ key: 1, label: t("codexAuth.weekly"), window: aggregation.weekly }] : []),
...(aggregation.monthly ? [{ key: 2, label: t("codexAuth.monthly"), window: aggregation.monthly }] : []),
...(aggregation.customWindows ?? []).map((window, index) => ({ key: index + 3, label: window.label, window })),
] : [];
const formatPercent = (value: number) => new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value);
const formatRecoveryAt = (value: number) => new Intl.DateTimeFormat(locale, {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(value > 10_000_000_000 ? value : value * 1000));

return (
<>
{showsAggregate && <div className="pws-capacity-label">{t("pws.capacity.estimate")}</div>}
{(primaryQuota || pending) && (
<QuotaBars
quota={primaryQuota}
threshold={80}
t={t}
layout="stacked"
pending={pending}
incompleteWindowKeys={showsAggregate ? incompleteWindowKeys : undefined}
incompleteCustomWindowLabels={showsAggregate ? incompleteCustomWindowLabels : undefined}
/>
)}
{aggregation && (
<div className="pws-capacity-details">
{recoveryRows.flatMap(({ key, label, window }) => (
window.nextRecoveryAt !== undefined && window.nextRecoveryPercent !== undefined
? [<div className="pws-capacity-recovery" key={key}>
<span>{t("pws.capacity.nextRecovery")} · {label} · {formatRecoveryAt(window.nextRecoveryAt)}</span>
<strong>{t("pws.capacity.recoveryShare", { percent: formatPercent(window.nextRecoveryPercent) })}</strong>
</div>]
: []
))}
{showsAggregate && aggregation.currentAccount?.quota && (
<div className="pws-capacity-current">
<span className="pws-capacity-label">
{t("pws.capacity.currentAccount")}
{aggregation.currentAccount.plan ? ` · ${aggregation.currentAccount.plan}` : ""}
</span>
<QuotaBars quota={aggregation.currentAccount.quota} threshold={80} t={t} layout="stacked" />
</div>
)}
{aggregation.incomplete && aggregation.excludedAccounts > 0 && (
<div className="pws-capacity-incomplete">
{t("pws.capacity.incomplete", {
excluded: aggregation.excludedAccounts,
unknown: aggregation.unknownPlanAccounts,
})}
</div>
)}
{aggregation.partialWindowAccounts > 0 && (
<div className="pws-capacity-incomplete">
{t("pws.capacity.partial", { count: aggregation.partialWindowAccounts })}
</div>
)}
</div>
)}
</>
);
}
Loading
Loading