Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs-site/src/content/docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ ocx status
ocx gui # open the dashboard on the live port
```

If you edit `config.json` while the proxy is running, `ocx status` and the dashboard warn that the
file no longer matches the loaded configuration. The proxy does not auto-reload it — restart to apply
the file (see [Web Dashboard](/guides/web-dashboard/#config-changes-while-the-proxy-is-running)).

## 3. Use Codex

Codex now talks to opencodex transparently:
Expand Down
9 changes: 9 additions & 0 deletions docs-site/src/content/docs/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ observation yet, loading, failed lookup with last-known values, and measured zer
states. **Quota check completed** means the read settled—not that a passive observation became
new or that every upstream measurement was refreshed.

### Config changes while the proxy is running

Saves made through OpenCodex — the CLI, management API, or dashboard — and live mutations the proxy
adopts update the resident configuration while it runs; no restart is needed for those. Only when
`config.json` is edited directly on disk — by another process or by hand — does the running proxy
keep serving its earlier snapshot. In that case the dashboard shows a config-change warning on the
Overview instead of pretending the new settings are live, and `ocx status` prints the same warning;
restart (`ocx service restart`, or `ocx restart` for a foreground proxy) to apply the on-disk edits.

## Model visibility

The **Models** switches show final Codex visibility: a routed model is on only when its provider allowlist includes it (or no allowlist is set) and it is not disabled. Turning a model on reconciles both filters atomically; **All on** clears the provider allowlist so newly discovered models are also on.
Expand Down
Binary file added docs/pr-assets/config-divergence-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ export const de: Record<TKey, string> = {
"dash.providers": "Anbieter",
"dash.tokens30d": "Tokens (30d)",
"dash.coverage": "{pct} Abdeckung",
"dash.configDiverged": "config.json wurde nach dem Laden durch den Proxy extern geändert — kein automatisches Neuladen; Neustart zum Anwenden",
"dash.mem.title": "Speicherbeobachtung",
"dash.mem.hint": "Schreibgeschützte Laufzeitdiagnose. Beobachteter Speicher ist max(RSS, external, ArrayBuffers), damit Windows-Working-Set-Trimming gebundenen Speicher nicht versteckt.",
"dash.mem.rss": "Resident Set (RSS)",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ export const en = {
"dash.providers": "Providers",
"dash.tokens30d": "Tokens (30d)",
"dash.coverage": "{pct} coverage",
"dash.configDiverged": "config.json changed on disk after the proxy loaded it — no automatic reload; restart to apply",
// memory observability card (read-only /api/system/memory)
"dash.mem.title": "Memory observability",
"dash.mem.hint": "Read-only runtime diagnostics. Observed memory is max(RSS, external, ArrayBuffers) so Windows working-set trimming does not hide committed retention.",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export const fr: Record<TKey, string> = {
"dash.providers": "Fournisseurs",
"dash.tokens30d": "Jetons (30 j)",
"dash.coverage": "Couverture : {pct}",
"dash.configDiverged": "config.json a changé sur le disque après le chargement du proxy — pas de rechargement automatique ; redémarrez pour appliquer",
"dash.mem.title": "Observabilité de la mémoire",
"dash.mem.hint": "Diagnostics d’exécution en lecture seule. La mémoire observée correspond à max(RSS, external, ArrayBuffers), afin que la réduction de l’ensemble de travail Windows ne masque pas la rétention allouée.",
"dash.mem.rss": "Ensemble résident (RSS)",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export const ja: Record<TKey, string> = {
"dash.providers": "プロバイダー",
"dash.tokens30d": "トークン (30日)",
"dash.coverage": "{pct} カバレッジ",
"dash.configDiverged": "config.json がプロキシ読み込み後に変更されました — 自動再読み込みはされません。再起動して適用",
"dash.mem.title": "メモリ可観測性",
"dash.mem.hint": "読み取り専用のランタイム診断。観測メモリは max(RSS, external, ArrayBuffers) で、Windows の working set trimming がコミット済み保持を隠さないようにします。",
"dash.mem.rss": "常駐メモリ (RSS)",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ export const ko: Record<TKey, string> = {
"dash.providers": "프로바이더",
"dash.tokens30d": "토큰 (30일)",
"dash.coverage": "커버리지 {pct}",
"dash.configDiverged": "config.json이 프록시가 로드된 후 변경되었습니다 — 자동으로 다시 로드되지 않습니다. 변경 사항을 적용하려면 재시작하세요.",
"dash.mem.title": "메모리 관찰",
"dash.mem.hint": "읽기 전용 런타임 진단. 관측 메모리는 max(RSS, external, ArrayBuffers)라 Windows working set trimming이 커밋된 보존 메모리를 숨기지 못합니다.",
"dash.mem.rss": "상주 메모리 (RSS)",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export const ru: Record<TKey, string> = {
"dash.providers": "Провайдеры",
"dash.tokens30d": "Токены (30 дн.)",
"dash.coverage": "{pct} покрытия",
"dash.configDiverged": "config.json изменился на диске после загрузки прокси — автоперезагрузки нет; перезапустите для применения",
"dash.mem.title": "Наблюдение за памятью",
"dash.mem.hint": "Диагностика среды выполнения только для чтения. Наблюдаемая память — max(RSS, external, ArrayBuffers), чтобы trimming рабочего набора Windows не скрывал удержанную память.",
"dash.mem.rss": "Резидентная память (RSS)",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export const tr: Record<TKey, string> = {
"dash.providers": "Sağlayıcılar",
"dash.tokens30d": "Jetonlar (30 gün)",
"dash.coverage": "%{pct} kapsam",
"dash.configDiverged": "config.json, proxy yüklendikten sonra diskte değişti — otomatik yeniden yükleme yok; uygulamak için yeniden başlatın",
"dash.mem.title": "Bellek izlenebilirliği",
"dash.mem.hint": "Salt okunur çalışma zamanı tanılamaları. Gözlemlenen bellek max(RSS, harici, ArrayBuffers) değeridir.",
"dash.mem.rss": "Yerleşik küme (RSS)",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export const zhTW: Record<TKey, string> = {
"dash.providers": "供應商",
"dash.tokens30d": "Token (30 天)",
"dash.coverage": "覆蓋率 {pct}",
"dash.configDiverged": "config.json 在代理載入後被修改 — 不會自動重載;請重啟後生效",
"dash.mem.title": "記憶體可觀測性",
"dash.mem.hint": "只讀執行時診斷。觀測記憶體為 max(RSS, external, ArrayBuffers),避免 Windows working set trimming 隱藏已提交的保留記憶體。",
"dash.mem.rss": "常駐記憶體 (RSS)",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ export const zh: Record<TKey, string> = {
"dash.providers": "提供方",
"dash.tokens30d": "Token (30 天)",
"dash.coverage": "覆盖率 {pct}",
"dash.configDiverged": "config.json 在代理加载后被修改 — 不会自动重载;请重启后生效",
"dash.mem.title": "内存可观测性",
"dash.mem.hint": "只读运行时诊断。观测内存为 max(RSS, external, ArrayBuffers),避免 Windows working set trimming 隐藏已提交的保留内存。",
"dash.mem.rss": "常驻内存 (RSS)",
Expand Down
55 changes: 55 additions & 0 deletions gui/src/pages/dashboard-core-poll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ export type DashboardSettingsPoll = {
startupHealthSeed: SettingsData["startupHealth"] | null | undefined;
};

export type DashboardConfigStatusPoll = {
/** Null when the endpoint is unavailable (older server) or the fetch failed. */
configDivergence: {
available: boolean;
residentVersion: string | null;
diskVersion: string | null;
diverged: boolean;
} | null;
};

export type DashboardMaModePoll = {
maMode: "v1" | "default" | "v2";
};
Expand Down Expand Up @@ -248,6 +258,51 @@ export async function fetchDashboardMaMode(
}
}

/**
* Resident-vs-disk config divergence from the running proxy. Older servers without
* /api/config/status report null; the dashboard must keep showing the normal state.
*/
export async function fetchDashboardConfigStatus(
apiBase: string,
signal: AbortSignal,
): Promise<DashboardConfigStatusPoll> {
try {
const response = await fetch(`${apiBase}/api/config/status`, { signal, cache: "no-store" });
if (!response.ok) return { configDivergence: null };
const data = await response.json() as {
residentVersion?: unknown;
diskVersion?: unknown;
diverged?: unknown;
};
// A status payload must actually carry both version fields; a malformed
// { diverged: true } body must not be mistaken for an available status. The
// CLI normalizer enforces the same presence rule (cli/status.ts).
if (
!Object.prototype.hasOwnProperty.call(data, "residentVersion")
|| !Object.prototype.hasOwnProperty.call(data, "diskVersion")
) return { configDivergence: null };
const residentVersion = data.residentVersion as string | null | undefined;
const diskVersion = data.diskVersion as string | null | undefined;
const diverged = data.diverged;
if (
!(residentVersion === null || typeof residentVersion === "string")
|| !(diskVersion === null || typeof diskVersion === "string")
|| typeof diverged !== "boolean"
) return { configDivergence: null };
return {
configDivergence: {
available: true,
residentVersion,
diskVersion,
diverged,
},
};
} catch (error) {
if (isAbortError(error, signal)) throw error;
return { configDivergence: null };
}
}

export async function fetchDashboardOverview(
apiBase: string,
signal: AbortSignal,
Expand Down
13 changes: 11 additions & 2 deletions gui/src/pages/dashboard-overview-head.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ export function DashboardOverviewHead({
healthLoading,
startupHealth,
projectConfigWarnings,
configDivergence,
maMode,
maBusy,
maHelpTriggerRef,
maHelpOpen,
setMaHelpOpen,
switchMaMode,
switchMaMode,
maError,
}: Pick<Dash, "locale" | "health" | "providers" | "usage30d" | "usageLoading" | "healthLoading" | "startupHealth" | "projectConfigWarnings" | "maMode" | "maBusy" | "maHelpTriggerRef" | "maHelpOpen" | "setMaHelpOpen" | "switchMaMode" | "maError">) {
}: Pick<Dash, "locale" | "health" | "providers" | "usage30d" | "usageLoading" | "healthLoading" | "startupHealth" | "projectConfigWarnings" | "configDivergence" | "maMode" | "maBusy" | "maHelpTriggerRef" | "maHelpOpen" | "setMaHelpOpen" | "switchMaMode" | "maError">) {
const t = useT();
const online = health?.status === "ok";

Expand Down Expand Up @@ -111,6 +112,14 @@ export function DashboardOverviewHead({
</div>
)}
</div>
{configDivergence?.available === true && configDivergence.diverged && (
<div className="startup-health-slot" role="alert">
<button type="button" className="startup-health-bar" onClick={() => navigateHash("settings")}>
<span className="dot dot-amber" aria-hidden="true" />
<span className="startup-health-bar__summary">{t("dash.configDiverged")}</span>
</button>
</div>
)}
</div>

{projectConfigWarnings.length > 0 && (
Expand Down
16 changes: 16 additions & 0 deletions gui/src/pages/use-dashboard-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ import {
fetchDashboardSettings,
fetchDashboardSidecars,
fetchDashboardUsage,
fetchDashboardConfigStatus,
fetchProjectConfigDiagnostics,
fetchStartupHealth,
normalizeInjectionSelection,
type DashboardEpochRefs,
type DashboardConfigStatusPoll,
} from "./dashboard-core-poll";
import { usageSummary30dResourceKey } from "../usage-summary-resource";
import {
Expand Down Expand Up @@ -124,6 +126,7 @@ export function useDashboardData(apiBase: string) {
const [settingsSaving, setSettingsSaving] = useState(false);
const [syncing, setSyncing] = useState(false);
const [maMode, setMaMode] = useState<MaMode>(() => cachedMaMode ?? "default");
const [configDivergence, setConfigDivergence] = useState<DashboardConfigStatusPoll["configDivergence"]>(null);
const [maBusy, setMaBusy] = useState(false);
const [maError, setMaError] = useState<string | null>(null);
const [maHelpOpen, setMaHelpOpen] = useState(false);
Expand Down Expand Up @@ -234,6 +237,13 @@ export function useDashboardData(apiBase: string) {
{ pollMs: 5000 },
);

const configStatusPoll = useKeyedClientResource(
`dashboard-config-status:${apiBase}`,
[apiBase],
(signal) => fetchDashboardConfigStatus(apiBase, signal),
{ pollMs: 15000 },
);

const sidecarPoll = useKeyedClientResource(
`dashboard-sidecars:${apiBase}`,
[apiBase],
Expand Down Expand Up @@ -324,6 +334,11 @@ export function useDashboardData(apiBase: string) {
writeSessionListCache(`${MA_MODE_CACHE_PREFIX}${apiBase}`, maModePoll.data.maMode);
}, [maModePoll.data, apiBase]);

useEffect(() => {
if (configStatusPoll.data === undefined) return;
setConfigDivergence(configStatusPoll.data.configDivergence);
}, [configStatusPoll.data]);

// Derived — avoids setState-on-prop-change for the resolved flag. Cache / poll / optimistic
// save (which writes the same cache key) all count as resolved for MA UI.
const maModeResolved = maModePoll.data !== undefined || cachedMaMode !== null;
Expand Down Expand Up @@ -774,6 +789,7 @@ export function useDashboardData(apiBase: string) {
modelQuery, setModelQuery,
expandedProviders, setExpandedProviders,
health, startupHealth, providers, models, settings, sidecar, shadowCall, usage30d,
configDivergence,
usageLoading: usagePoll.loading && !usage30d,
healthLoading: overviewPoll.loading && !health,
sidecarSaving, shadowCallSaving, modelsLoading, settingsSaving, syncing,
Expand Down
Loading
Loading