fix(diagnostics): deep-review round 2 — a send that claims, not one that blocks
The send no longer holds the session's row lock while it talks to the tracker: it claims the session, gives the lock back, and checkpoints each step. A Stop is never held behind a send, while a draft, a screenshot or a second send are refused at once instead of queueing. The claim ages out, so a request that died mid-send does not strand the session. What the composer says is what happened: an attachment that went is no longer named as one that did not (the missing body edit is its own line), and the issue's screenshot count comes from the server when the list cannot be read, so a failed read no longer blocks saving the draft. The zip is fetched as a request, so its pace is a message and not a saved error page. Screenshots of a finished report can be removed again; only one still being sent is held. A recording's log elevation can no longer outlive it. The step prompt keeps the keyboard (the page's keys stay quiet) and traps Tab itself. An image pasted into a dialog opened over the composer stays that dialog's.
This commit is contained in:
@@ -225,6 +225,8 @@ function reportFailure(err: HttpError, quiet: boolean): void {
|
||||
// are safe to repeat). Used to recover from the keepalive race without surfacing a 502.
|
||||
export interface ReqConfig {
|
||||
idempotent?: boolean;
|
||||
// Return the `Response` itself instead of parsed JSON — for a file (see the return below).
|
||||
raw?: boolean;
|
||||
// Suppress the global error modal for 4xx/5xx so the caller can show the error inline
|
||||
// (used by the auth forms — a wrong password shouldn't pop a blocking dialog).
|
||||
quiet?: boolean;
|
||||
@@ -358,6 +360,10 @@ export async function req<T = unknown>(
|
||||
reportFailure(err, !!cfg.quiet);
|
||||
throw err;
|
||||
}
|
||||
// A file (`cfg.raw`): the answer itself, for the caller to read as a blob. A download that went
|
||||
// out as a plain `<a href>` instead would carry no refusal the app could show — the browser
|
||||
// would simply save the error.
|
||||
if (cfg.raw) return r as T;
|
||||
// The single audited cast in this layer: the server contract is asserted here, once, so
|
||||
// every typed api.* method (T comes from its declared return type) is checked from here up.
|
||||
return (r.status === 204 ? null : await r.json()) as T;
|
||||
|
||||
@@ -55,6 +55,8 @@ export interface DebugSession {
|
||||
issue_url: string | null;
|
||||
/** Files a send that stopped half-way has still to deliver (retrying sends only these). */
|
||||
pending_files: string[];
|
||||
/** Whether the issue's body has still to be edited to show the screenshots it already carries. */
|
||||
images_pending: boolean;
|
||||
screenshots: number;
|
||||
/** The issue being written from it (the composer saves as it goes); null until it first does. */
|
||||
draft: DebugDraft | null;
|
||||
@@ -102,12 +104,13 @@ export const debugSessionsApi = {
|
||||
}),
|
||||
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;
|
||||
/** The report as a zip, built from the SAVED draft — a request, not a navigation, so a refusal
|
||||
* (the export's own pace) reaches the user as a message instead of a saved error page. */
|
||||
debugExport: async (id: string): Promise<{ blob: Blob; name: string }> => {
|
||||
const r = await req<Response>(`${SESSIONS}/${id}/export`, {}, { raw: true });
|
||||
const disposition = r.headers.get("content-disposition") ?? "";
|
||||
const name = /filename="([^"]+)"/.exec(disposition)?.[1] ?? `siftlode-debug-${id}.zip`;
|
||||
return { blob: await r.blob(), name };
|
||||
},
|
||||
/** Send the SAVED draft to the issue tracker, or finish a send that stopped half-way. */
|
||||
sendDebugIssue: (id: string): Promise<{ ok: true; session: DebugSession }> =>
|
||||
|
||||
Reference in New Issue
Block a user