feat(dashboard): add Debug Share to the System page (#38600)
* Port from google-gemini/gemini-cli#21541: back up corrupted config.yaml When config.yaml fails to parse, load_config() silently falls back to DEFAULT_CONFIG and leaves the broken file on disk. If the user then re-runs the setup wizard or hermes config set (both rewrite config.yaml), their broken-but-recoverable overrides are lost for good. Adapts the policy-file recovery from gemini-cli#21541: on the first parse warning for a given broken file, snapshot it to config.yaml.corrupt.<ts>.bak (best-effort, symlink-guarded, size-deduped) and tell the user where it landed. Unlike Gemini's version we deliberately do NOT reset config.yaml to a clean state — hermes never silently mutates user config, and leaving it means a hand-fixed file is re-read on the next load. Tests: 3 new cases (backup created + content preserved + original untouched; same-size backup dedup; symlink not copied). E2E verified with isolated HERMES_HOME and a real tab-indented broken config. * feat(dashboard): add Debug Share to the System page Surface `hermes debug share` in the dashboard. The System > Operations section gets a dedicated card that uploads a redacted report + full logs and returns the paste URLs as real, copyable links instead of a log tail. - debug.py: factor a pure build_debug_share() returning structured {urls, failures, redacted, auto_delete_seconds}; run_debug_share now calls it (CLI output unchanged). - web_server.py: POST /api/ops/debug-share runs the share core in a worker thread and returns the structured payload synchronously (the URLs are the whole point — not a backgrounded action). - api.ts: runDebugShare() + DebugShareResponse. - SystemPage.tsx: share card with a redaction toggle (on by default), per-link + copy-all buttons, and the 6h auto-delete countdown. - tests: build_debug_share core + endpoint (redact toggle, failure 502, token gate).
This commit is contained in:
@@ -853,6 +853,15 @@ export const api = {
|
||||
runDump: () => fetchJSON<ActionResponse>("/api/ops/dump", { method: "POST" }),
|
||||
runConfigMigrate: () =>
|
||||
fetchJSON<ActionResponse>("/api/ops/config-migrate", { method: "POST" }),
|
||||
runDebugShare: (opts?: { redact?: boolean; lines?: number }) =>
|
||||
fetchJSON<DebugShareResponse>("/api/ops/debug-share", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
redact: opts?.redact ?? true,
|
||||
lines: opts?.lines ?? 200,
|
||||
}),
|
||||
}),
|
||||
|
||||
|
||||
getCheckpoints: () => fetchJSON<CheckpointsResponse>("/api/ops/checkpoints"),
|
||||
@@ -906,6 +915,16 @@ export interface ActionResponse {
|
||||
update_command?: string;
|
||||
}
|
||||
|
||||
export interface DebugShareResponse {
|
||||
ok: boolean;
|
||||
// label -> paste URL, e.g. { Report: "https://paste.rs/abc", "agent.log": "..." }
|
||||
urls: Record<string, string>;
|
||||
// "label: error" strings for optional full-log uploads that failed.
|
||||
failures: string[];
|
||||
redacted: boolean;
|
||||
auto_delete_seconds: number;
|
||||
}
|
||||
|
||||
export interface SessionStoreStats {
|
||||
total: number;
|
||||
active_store: number;
|
||||
|
||||
Reference in New Issue
Block a user