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
+49
View File
@@ -0,0 +1,49 @@
import { createStore } from "./store";
// The debug recorder's app-wide state (E16) — what several surfaces open or read, held outside any
// one of them: Settings, the command palette and the recorder itself all open the start dialog, and
// the composer opens for whichever session just ended.
const startOpen = createStore(false);
/** Open the start dialog (Settings → Diagnosztika, the palette). */
export const openRecorderStart = (): void => startOpen.set(true);
export const closeRecorderStart = (): void => startOpen.set(false);
export const useRecorderStartOpen = (): boolean => startOpen.use();
const composing = createStore<string | null>(null);
/** Open the issue composer for session `id` — after Stop, when the cap ends it, from Settings. */
export const openComposer = (id: string): void => composing.set(id);
export const closeComposer = (): void => composing.set(null);
export const useComposerSession = (): string | null => composing.use();
// Step markers are numbered per session. The count survives a reload (sessionStorage, per tab): a
// reproduction is often "reload and it happens again", and step 4 coming back as step 1 would make
// the list the issue is drafted from contradict itself.
const STEP_KEY = "siftlode.debugSteps";
function readSteps(): Record<string, number> {
try {
const raw = JSON.parse(sessionStorage.getItem(STEP_KEY) ?? "{}") as unknown;
return raw && typeof raw === "object" ? (raw as Record<string, number>) : {};
} catch {
return {};
}
}
const steps = createStore<Record<string, number>>(readSteps());
/** How many steps session `id` has marked in this tab. */
export const useStepCount = (id: string): number => steps.use()[id] ?? 0;
/** Take the next step number for session `id`. */
export function nextStep(id: string): number {
const n = (steps.get()[id] ?? 0) + 1;
// Only this session's count is kept: the others ended, and their numbers are in their events.
steps.set({ [id]: n });
try {
sessionStorage.setItem(STEP_KEY, JSON.stringify(steps.get()));
} catch {
// Storage off: the count lives until the reload, which is all it is for.
}
return n;
}