feat(diagnostics): the debug recorder — start, the pill, steps, screenshots, stop (E16 S2)

- the start dialog is the consent: level (Részletes/Teljes), what is recorded,
  retention and the cap (now served as diagnostics.session_minutes)
- the pill docks under the toasts: elapsed time, Step (an optional label,
  its own Escape layer), Screenshot (tab capture where the browser has it),
  Stop; compact on a phone
- while recording, lib/log tags every event and every request path (req,
  beacon, uploadReq via one identityHeaders) names the session; applyDiagnostics
  raises the level for the session's run and never lowers a raised one, and
  lapses at whichever ends first
- Stop sends what is waiting before the server closes the session; the cap
  says so and opens the composer
- Settings → Diagnosztika and the command palette start/step/stop it
- the evidence leaves the log upload's own request lines out
- the step shortcut is deliberately palette-only: the keymap's D1 rule bars a
  new modifier chord, and a bare letter collides with the players'
This commit is contained in:
2026-09-19 22:08:10 +02:00
parent 2d9595b0e5
commit 46fce4a767
26 changed files with 1347 additions and 89 deletions
+17
View File
@@ -53,6 +53,8 @@ export interface LogEvent {
request_id?: string;
tab_id: string;
version: string;
/** The debug session recording when the event was kept (E16). */
debug_session_id?: string;
}
/** Events kept. At ~0.5 KB each the ring stays around a megabyte — a long session's worth of
@@ -138,6 +140,20 @@ const TAB_ID = tabId();
// --- the logger -----------------------------------------------------------------------------------
let threshold: Level = "INFO";
let ring: LogEvent[] = [];
// The debug session this tab is recording into (E16), or null. Held HERE, the lowest layer, because
// two things below everything else need it: every kept event carries it, and `api/core` puts it on
// every request as `X-Debug-Session` — which is how the server tags its own lines of that request.
let debugSession: string | null = null;
/** Record into `id` from now on (null = stop). `lib/diagnostics` sets it from `me.debug_session`. */
export function setDebugSession(id: string | null): void {
debugSession = id;
}
/** The debug session recording right now, if any. */
export function currentDebugSession(): string | null {
return debugSession;
}
/** The capture level. INFO is the always-on "Alap"; DEBUG and TRACE come from the user's consented
* diagnostics level (E22 S5). */
@@ -207,6 +223,7 @@ function emit(
};
if (attrs && Object.keys(attrs).length) entry.attrs = redact(attrs) as Record<string, unknown>;
if (requestId) entry.request_id = requestId;
if (debugSession) entry.debug_session_id = debugSession;
ring.push(entry);
if (ring.length > RING_MAX) ring.splice(0, ring.length - RING_MAX);
notify(entry);