feat(diagnostics): the issue composer — draft, review, screenshots, zip export (E16 S3)

After Stop (or at the cap, or from Settings → Diagnosztika's past sessions)
one dialog drafts the issue: title, description, the steps drafted from the
session's markers and page changes (a marker without a label is named by its
time), the environment (version, browser, system, window, language). Beside
it, what goes with it: the browser and server logs — each reviewable line by
line, an unticked line is not sent — and the screenshots, removable, with
paste / drop / pick to add more.

- it saves as it goes (debug_sessions.draft, migration 0090): the rendered
  markdown in the user's language, the excluded rows, and its own fields to
  reopen as it was; a sent session is read-only
- the zip export (issue.md, client.log, server.log, screenshots) is built
  from the saved draft by app/issues/bundle.py — the one definition the Gitea
  delivery (S4) will share; .log, not .jsonl, which Gitea refuses
- the log line's level tags and clock move to components/logLineParts so the
  review list draws a level the way both log viewers do
This commit is contained in:
2026-09-19 22:26:49 +02:00
parent 9f177557d2
commit 8d0083d4e4
23 changed files with 1620 additions and 35 deletions
+20
View File
@@ -54,6 +54,17 @@ export interface DebugSession {
/** The issue it was sent as, once sent. */
issue_url: string | null;
screenshots: number;
/** The issue being written from it (the composer saves as it goes); null until it first does. */
draft: DebugDraft | null;
}
/** A saved draft: the rendered text the export and the issue carry, plus the composer's own fields
* (`form`, lib/issueDraft `DraftForm`) to reopen it as it was. */
export interface DebugDraft {
title: string;
body: string;
excluded: number[];
form: Record<string, unknown>;
}
export interface DebugScreenshot {
@@ -87,6 +98,15 @@ export const debugSessionsApi = {
headers: { "Content-Type": image.type || "application/octet-stream", ...identityHeaders() },
body: image,
}),
saveDebugDraft: (id: string, draft: DebugDraft): Promise<{ ok: true; session: DebugSession }> =>
req(`${SESSIONS}/${id}/draft`, { method: "PUT", body: JSON.stringify(draft) }),
/** The issue as a zip, built from the SAVED draft. A navigation, not a `req` — so the account
* rides as `?account=`, as for every download. */
debugExportUrl: (id: string): string => {
const a = getActiveAccount();
const url = `${SESSIONS}/${id}/export`;
return a != null ? `${url}?account=${a}` : url;
},
removeDebugScreenshot: (id: string, shotId: number): Promise<{ ok: true; deleted: number }> =>
req(`${SESSIONS}/${id}/screenshots/${shotId}`, { method: "DELETE" }),
/** Where an `<img>` loads a screenshot from. An `<img>` cannot send the account header, so the