feat(dashboard): GuruConnect v2 Support Codes view
Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 3m27s
Build and Test / Build Agent (Windows) (push) Successful in 7m11s
Build and Test / Security Audit (push) Successful in 4m32s
Build and Test / Build Summary (push) Has been skipped

Generate, list, and cancel attended-support codes (XXX-XXX-XXX), built
on the v2 codes API and existing UI primitives.

- Codes table: code in mono, status badge (pending+pulse/connected/
  completed/cancelled), bound client/machine, created-by, created
  (relative + absolute tooltip). Sticky header, skeleton load,
  actionable empty/error states.
- Generate opens a focused reveal modal showing the code large in
  JetBrains Mono with copy and a read-aloud instruction; the code is
  announced character-by-character for screen readers. Mint is ref-
  guarded so it creates exactly one code per open (no StrictMode dupe).
- Cancel via confirm dialog (POST /api/codes/:code/cancel), disabled for
  non-cancellable statuses; invalidates the codes query. List polls 7s.
- Shared API client now tolerates non-JSON 200 bodies, so the cancel
  endpoint's plain-text "Code cancelled" success no longer surfaces as a
  failure. Error-envelope handling unchanged.

Passed Code Review (no blockers after fixes) and local gates
(tsc/lint/build green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 13:59:18 -07:00
parent 67f3722b3c
commit 664f33d5ab
14 changed files with 750 additions and 8 deletions

View File

@@ -0,0 +1,55 @@
import {
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import * as codesApi from "../../api/codes";
import type { CreateCodeRequest } from "../../api/types";
const CODES_KEY = ["codes"] as const;
/**
* List the active support codes. Polls on a short interval because codes are
* short-lived: a `pending` code can be redeemed (-> `connected`) or expire out
* of the active set at any moment, and a tech who just read a code aloud is
* watching for exactly that transition. The interval is tight (like the
* sessions poll) so the redeem shows up on its own without a manual refresh.
*/
export function useSupportCodes() {
return useQuery({
queryKey: CODES_KEY,
queryFn: ({ signal }) => codesApi.listCodes(signal),
refetchInterval: 7_000,
staleTime: 3_500,
});
}
/**
* Generate a new support code, then invalidate the list so the new `pending`
* code appears in the table. The created code is returned to the caller so the
* generate flow can surface it prominently (it is read to the end user).
*/
export function useGenerateCode() {
const qc = useQueryClient();
return useMutation({
mutationFn: (body: CreateCodeRequest) => codesApi.createCode(body),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: CODES_KEY });
},
});
}
/**
* Cancel (revoke) a support code, then invalidate the list so the row drops out
* of the active set. Cancelling an un-redeemed code is irreversible, so the UI
* confirms first; this hook is the action behind that confirmation.
*/
export function useCancelCode() {
const qc = useQueryClient();
return useMutation({
mutationFn: (code: string) => codesApi.cancelCode(code),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: CODES_KEY });
},
});
}