The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
125 lines
4.8 KiB
TypeScript
125 lines
4.8 KiB
TypeScript
import { useCallback } from "react";
|
|
import { useIsMutating, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import { api, type DebugSession, type DebugSessionLevel } from "../lib/api";
|
|
import { notifyActionError } from "../lib/actionErrors";
|
|
import { captureTab } from "../lib/captureTab";
|
|
import {
|
|
announceSessionChange,
|
|
closeRecorderStart,
|
|
nextStep,
|
|
openComposer,
|
|
setStepPrompt,
|
|
} from "../lib/debugRecorder";
|
|
import { flushNow, liveSession } from "../lib/diagnostics";
|
|
import { log } from "../lib/log";
|
|
import { notify } from "../lib/notifications";
|
|
import { qk } from "../lib/queryKeys";
|
|
import { updateMeCache, useMe } from "../lib/useMe";
|
|
|
|
/** The debug recording's actions, shared by everything that drives it: the pill, Settings →
|
|
* Diagnosztika and the command palette. The session itself is `me.debug_session` — the server's
|
|
* answer, so a reload, another tab or the cap all show the same thing. */
|
|
export function useDebugRecording() {
|
|
const { t } = useTranslation();
|
|
const qc = useQueryClient();
|
|
const me = useMe();
|
|
const session = liveSession(me?.debug_session);
|
|
const setSession = (next: DebugSession | null) => {
|
|
updateMeCache(qc, (prev) => ({ ...prev, debug_session: next }));
|
|
void qc.invalidateQueries({ queryKey: qk.debugSessions(), exact: true });
|
|
announceSessionChange();
|
|
setStepPrompt(null); // a prompt left open belongs to the session that ended
|
|
};
|
|
|
|
const start = useMutation({
|
|
mutationFn: (level: DebugSessionLevel) => api.startDebugSession(level),
|
|
meta: { failTitle: "recorder.start.failed" },
|
|
onSuccess: (r) => {
|
|
setSession(r.session);
|
|
closeRecorderStart();
|
|
},
|
|
});
|
|
|
|
const stop = useMutation({
|
|
mutationKey: STOP_KEY,
|
|
mutationFn: async (id: string) => {
|
|
log.info("debug.stop", "recording stopped by the user");
|
|
// The events of the last seconds before Stop are the ones closest to what went wrong. Sent
|
|
// while the session still tags them: once it is closed the server no longer does.
|
|
await flushNow();
|
|
return api.stopDebugSession(id);
|
|
},
|
|
meta: { failTitle: "recorder.pill.stopFailed" },
|
|
onSuccess: (r) => {
|
|
setSession(null);
|
|
notify({ level: "success", message: t("recorder.pill.stopped") });
|
|
openComposer(r.session.id);
|
|
},
|
|
});
|
|
|
|
const shoot = useMutation({
|
|
mutationKey: SHOOT_KEY,
|
|
mutationFn: async ({ id, image }: { id: string; image: Blob }) =>
|
|
api.addDebugScreenshot(id, image),
|
|
meta: { failTitle: "recorder.pill.shotFailed" },
|
|
onSuccess: (r) => {
|
|
updateMeCache(qc, (prev) =>
|
|
prev.debug_session
|
|
? {
|
|
...prev,
|
|
debug_session: {
|
|
...prev.debug_session,
|
|
screenshots: prev.debug_session.screenshots + 1,
|
|
},
|
|
}
|
|
: prev,
|
|
);
|
|
// In the evidence too, where the step list is drafted from: which step it belongs to.
|
|
log.info("debug.screenshot", "screenshot taken", { screenshot: r.screenshot.id });
|
|
notify({ level: "success", message: t("recorder.pill.shotSaved") });
|
|
},
|
|
});
|
|
|
|
// In flight anywhere, not only through this hook's own observer: the pill remounts when it moves
|
|
// to another toast host, and a Stop still running must keep its button busy there.
|
|
const stopping = useIsMutating({ mutationKey: STOP_KEY }) > 0;
|
|
const shooting = useIsMutating({ mutationKey: SHOOT_KEY }) > 0;
|
|
|
|
/** Mark the next step; `label` is what the user says they just did (may be empty). */
|
|
const sessionId = session?.id;
|
|
const markStep = useCallback(
|
|
(label: string) => {
|
|
if (!sessionId) return;
|
|
const n = nextStep(sessionId);
|
|
const text = label.trim();
|
|
log.info("debug.step", text || `step ${n}`, { n, ...(text ? { label: text } : {}) });
|
|
notify({ level: "info", message: t("recorder.pill.stepMarked", { n }) });
|
|
},
|
|
[sessionId, t],
|
|
);
|
|
|
|
/** Capture this tab and attach it. `hide` runs around the capture so the pill is not in the
|
|
* frame. Nothing happens when the user cancels the browser's picker. */
|
|
const screenshot = async (hide: (hidden: boolean) => void) => {
|
|
if (!session) return;
|
|
hide(true);
|
|
let image: Blob | null = null;
|
|
try {
|
|
// Two frames: the pill's `invisible` must be painted before the capture reads the tab.
|
|
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
image = await captureTab();
|
|
} catch (err) {
|
|
notifyActionError(err, t("recorder.pill.shotFailed"));
|
|
}
|
|
hide(false);
|
|
if (image) shoot.mutate({ id: session.id, image });
|
|
};
|
|
|
|
return { session, start, stop, stopping, shoot, shooting, markStep, screenshot };
|
|
}
|
|
|
|
const STOP_KEY = ["debug-session", "stop"];
|
|
const SHOOT_KEY = ["debug-session", "screenshot"];
|