diff --git a/apps/api/src/server.integration.test.ts b/apps/api/src/server.integration.test.ts index ed867e7..b8fa680 100644 --- a/apps/api/src/server.integration.test.ts +++ b/apps/api/src/server.integration.test.ts @@ -3843,7 +3843,7 @@ describe("@runroot/api integration", () => { } }); - it("serves acknowledge, list-acknowledged, inspect-acknowledgment, clear-acknowledgment, and apply paths through the network API", async () => { + it("serves acknowledgment and sign-off list, inspect, clear, and apply paths through the network API", async () => { const workspaceRoot = await mkdtemp( join(tmpdir(), "runroot-api-checklist-acknowledgment-"), ); @@ -4158,21 +4158,58 @@ describe("@runroot/api integration", () => { method: "POST", }, ); + const signoffResponse = await fetch( + `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/sign-off`, + { + body: JSON.stringify({ + signoffNote: "Backup signed off the acknowledged follow-up", + items: [ + { + item: "Validate queued follow-up", + state: "signed-off", + }, + { + item: "Close backup handoff", + state: "unsigned", + }, + ], + }), + headers: { + "content-type": "application/json", + }, + method: "POST", + }, + ); const acknowledgedResponse = await fetch( `${peerAddress}/audit/catalog/acknowledged`, ); + const signedOffResponse = await fetch( + `${peerAddress}/audit/catalog/signed-off`, + ); const inspectResponse = await fetch( `${peerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/acknowledgment`, ); + const inspectSignoffResponse = await fetch( + `${peerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/sign-off`, + ); const applyResponse = await fetch( `${peerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/apply`, ); + const clearSignoffResponse = await fetch( + `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/sign-off/clear`, + { + method: "POST", + }, + ); const clearResponse = await fetch( `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/acknowledgment/clear`, { method: "POST", }, ); + const signedOffAfterClearResponse = await fetch( + `${peerAddress}/audit/catalog/signed-off`, + ); const acknowledgedAfterClearResponse = await fetch( `${peerAddress}/audit/catalog/acknowledged`, ); @@ -4202,6 +4239,27 @@ describe("@runroot/api integration", () => { }; }; }; + const signoffPayload = (await signoffResponse.json()) as { + signoff: { + acknowledgment: { + acknowledgment: { + acknowledgmentNote?: string; + }; + attestation: { + attestation: { + attestationNote?: string; + }; + }; + }; + signoff: { + signoffNote?: string; + items: Array<{ + item: string; + state: "signed-off" | "unsigned"; + }>; + }; + }; + }; const acknowledgedPayload = (await acknowledgedResponse.json()) as { acknowledged: { items: Array<{ @@ -4237,6 +4295,43 @@ describe("@runroot/api integration", () => { totalCount: number; }; }; + const signedOffPayload = (await signedOffResponse.json()) as { + signedOff: { + items: Array<{ + acknowledgment: { + attestation: { + evidence: { + verification: { + resolution: { + blocker: { + progress: { + checklist: { + assignment: { + review: { + visibility: { + catalogEntry: { + entry: { + id: string; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; + signoff: { + signoffNote?: string; + }; + }>; + totalCount: number; + }; + }; const inspectPayload = (await inspectResponse.json()) as { acknowledgment: { acknowledgment: { @@ -4248,6 +4343,17 @@ describe("@runroot/api integration", () => { }; }; }; + const inspectSignoffPayload = (await inspectSignoffResponse.json()) as { + signoff: { + signoff: { + signoffNote?: string; + items: Array<{ + item: string; + state: "signed-off" | "unsigned"; + }>; + }; + }; + }; const applyPayload = (await applyResponse.json()) as { application: { application: { @@ -4265,6 +4371,13 @@ describe("@runroot/api integration", () => { }; }; }; + const clearSignoffPayload = (await clearSignoffResponse.json()) as { + signoff: { + signoff: { + signoffNote?: string; + }; + }; + }; const clearPayload = (await clearResponse.json()) as { acknowledgment: { acknowledgment: { @@ -4272,6 +4385,12 @@ describe("@runroot/api integration", () => { }; }; }; + const signedOffAfterClearPayload = + (await signedOffAfterClearResponse.json()) as { + signedOff: { + totalCount: number; + }; + }; const acknowledgedAfterClearPayload = (await acknowledgedAfterClearResponse.json()) as { acknowledged: { @@ -4292,6 +4411,7 @@ describe("@runroot/api integration", () => { expect(evidenceResponse.status).toBe(200); expect(attestationResponse.status).toBe(200); expect(acknowledgmentResponse.status).toBe(200); + expect(signoffResponse.status).toBe(200); expect( acknowledgmentPayload.acknowledgment.attestation.evidence.verification .verification.verificationNote, @@ -4319,6 +4439,26 @@ describe("@runroot/api integration", () => { }, ], ); + expect( + signoffPayload.signoff.acknowledgment.acknowledgment.acknowledgmentNote, + ).toBe("Backup acknowledged the attested follow-up"); + expect( + signoffPayload.signoff.acknowledgment.attestation.attestation + .attestationNote, + ).toBe("Backup attested the stable follow-up evidence"); + expect(signoffPayload.signoff.signoff.signoffNote).toBe( + "Backup signed off the acknowledged follow-up", + ); + expect(signoffPayload.signoff.signoff.items).toEqual([ + { + item: "Validate queued follow-up", + state: "signed-off", + }, + { + item: "Close backup handoff", + state: "unsigned", + }, + ]); expect(acknowledgedResponse.status).toBe(200); expect(acknowledgedPayload.acknowledged.totalCount).toBe(1); expect( @@ -4326,10 +4466,34 @@ describe("@runroot/api integration", () => { .verification.resolution.blocker.progress.checklist.assignment.review .visibility.catalogEntry.entry.id, ).toBe("catalog_entry_acknowledgment_api"); + expect(signedOffResponse.status).toBe(200); + expect(signedOffPayload.signedOff.totalCount).toBe(1); + expect( + signedOffPayload.signedOff.items[0]?.acknowledgment.attestation.evidence + .verification.resolution.blocker.progress.checklist.assignment.review + .visibility.catalogEntry.entry.id, + ).toBe("catalog_entry_acknowledgment_api"); + expect(signedOffPayload.signedOff.items[0]?.signoff.signoffNote).toBe( + "Backup signed off the acknowledged follow-up", + ); expect(inspectResponse.status).toBe(200); expect( inspectPayload.acknowledgment.acknowledgment.acknowledgmentNote, ).toBe("Backup acknowledged the attested follow-up"); + expect(inspectSignoffResponse.status).toBe(200); + expect(inspectSignoffPayload.signoff.signoff.signoffNote).toBe( + "Backup signed off the acknowledged follow-up", + ); + expect(inspectSignoffPayload.signoff.signoff.items).toEqual([ + { + item: "Validate queued follow-up", + state: "signed-off", + }, + { + item: "Close backup handoff", + state: "unsigned", + }, + ]); expect(applyResponse.status).toBe(200); expect(applyPayload.application.application.savedView.id).toBe( "saved_view_acknowledgment_api", @@ -4338,10 +4502,16 @@ describe("@runroot/api integration", () => { applyPayload.application.application.navigation.drilldowns[0]?.result .runId, ).toBe(queuedRun.id); + expect(clearSignoffResponse.status).toBe(200); + expect(clearSignoffPayload.signoff.signoff.signoffNote).toBe( + "Backup signed off the acknowledged follow-up", + ); expect(clearResponse.status).toBe(200); expect( clearPayload.acknowledgment.acknowledgment.acknowledgmentNote, ).toBe("Backup acknowledged the attested follow-up"); + expect(signedOffAfterClearResponse.status).toBe(200); + expect(signedOffAfterClearPayload.signedOff.totalCount).toBe(0); expect(acknowledgedAfterClearResponse.status).toBe(200); expect(acknowledgedAfterClearPayload.acknowledged.totalCount).toBe(0); } finally { diff --git a/apps/api/src/server.test.ts b/apps/api/src/server.test.ts index 5a1b448..fabdf0d 100644 --- a/apps/api/src/server.test.ts +++ b/apps/api/src/server.test.ts @@ -25,7 +25,7 @@ describe("@runroot/api", () => { expect(response.json()).toEqual({ status: "ok", project: "Runroot", - phase: 27, + phase: 28, }); }); diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index c61b7b0..80bf416 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -285,6 +285,12 @@ export function buildServer(options: BuildServerOptions = {}) { })), ); + app.get("/audit/catalog/signed-off", async (_request, reply) => + handleOperatorResponse(reply, async () => ({ + signedOff: await operator.listSignedOffCatalogEntries(), + })), + ); + app.post("/audit/saved-views", async (request, reply) => handleOperatorResponse(reply, async () => { const body = request.body as { @@ -534,6 +540,20 @@ export function buildServer(options: BuildServerOptions = {}) { }), ); + app.get("/audit/catalog/:catalogEntryId/sign-off", async (request, reply) => + handleOperatorResponse(reply, async () => { + const params = request.params as { + readonly catalogEntryId: string; + }; + + return { + signoff: await operator.getCatalogChecklistItemSignoff( + params.catalogEntryId, + ), + }; + }), + ); + app.get("/audit/catalog/:catalogEntryId", async (request, reply) => handleOperatorResponse(reply, async () => { const params = request.params as { @@ -870,6 +890,34 @@ export function buildServer(options: BuildServerOptions = {}) { }), ); + app.post("/audit/catalog/:catalogEntryId/sign-off", async (request, reply) => + handleOperatorResponse(reply, async () => { + const params = request.params as { + readonly catalogEntryId: string; + }; + const body = request.body as { + readonly signoffNote?: string; + readonly items?: unknown; + }; + const items = readChecklistItemSignoffItems(body?.items, "items"); + + if (items.length === 0) { + throw new OperatorInputError( + "items must include at least one checklist item signoff entry.", + ); + } + + return { + signoff: await operator.signOffCatalogEntry(params.catalogEntryId, { + ...(body?.signoffNote !== undefined + ? { signoffNote: body.signoffNote } + : {}), + items, + }), + }; + }), + ); + app.post( "/audit/catalog/:catalogEntryId/review/clear", async (request, reply) => @@ -1031,6 +1079,22 @@ export function buildServer(options: BuildServerOptions = {}) { }), ); + app.post( + "/audit/catalog/:catalogEntryId/sign-off/clear", + async (request, reply) => + handleOperatorResponse(reply, async () => { + const params = request.params as { + readonly catalogEntryId: string; + }; + + return { + signoff: await operator.clearCatalogChecklistItemSignoff( + params.catalogEntryId, + ), + }; + }), + ); + app.get("/audit/saved-views/:savedViewId/apply", async (request, reply) => handleOperatorResponse(reply, async () => { const params = request.params as { @@ -1598,3 +1662,30 @@ function readChecklistItemAcknowledgmentItems( return value; } + +function readChecklistItemSignoffItems( + value: unknown, + fieldName: string, +): readonly { + readonly item: string; + readonly state: "signed-off" | "unsigned"; +}[] { + if ( + !Array.isArray(value) || + !value.every( + (entry) => + typeof entry === "object" && + entry !== null && + "item" in entry && + typeof entry.item === "string" && + "state" in entry && + (entry.state === "signed-off" || entry.state === "unsigned"), + ) + ) { + throw new OperatorInputError( + `${fieldName} must be an array of { item, state } objects with state signed-off|unsigned.`, + ); + } + + return value; +} diff --git a/apps/web/src/app/runs/catalog/route.ts b/apps/web/src/app/runs/catalog/route.ts index c1c276c..9f41d90 100644 --- a/apps/web/src/app/runs/catalog/route.ts +++ b/apps/web/src/app/runs/catalog/route.ts @@ -551,6 +551,53 @@ export async function POST(request: Request) { return Response.redirect(redirectUrl, 303); } + if (intent === "sign-off") { + const catalogEntryId = readTrimmedFormValue(formData, "catalogEntryId"); + const signoffItems = readChecklistItemSignoffItems( + formData.get("signoffItems"), + ); + const signoffNoteValue = formData.get("signoffNote"); + + if (!catalogEntryId) { + appendFlashMessage( + redirectUrl, + "error", + "A catalog entry id is required to update checklist item sign-off metadata.", + ); + + return Response.redirect(redirectUrl, 303); + } + + if (signoffItems.length === 0) { + appendFlashMessage( + redirectUrl, + "error", + "At least one checklist item sign-off entry is required.", + ); + + return Response.redirect(redirectUrl, 303); + } + + const signoff = + await createRunrootApiClient().setAuditCatalogChecklistItemSignoff( + catalogEntryId, + { + ...(typeof signoffNoteValue === "string" + ? { signoffNote: signoffNoteValue } + : {}), + items: signoffItems, + }, + ); + + appendFlashMessage( + redirectUrl, + "notice", + `Checklist item sign-offs for ${signoff.acknowledgment.attestation.evidence.verification.resolution.blocker.progress.checklist.assignment.review.visibility.catalogEntry.entry.name} updated.`, + ); + + return Response.redirect(redirectUrl, 303); + } + if (intent === "clear-review") { const catalogEntryId = readTrimmedFormValue(formData, "catalogEntryId"); @@ -767,6 +814,33 @@ export async function POST(request: Request) { return Response.redirect(redirectUrl, 303); } + if (intent === "clear-sign-off") { + const catalogEntryId = readTrimmedFormValue(formData, "catalogEntryId"); + + if (!catalogEntryId) { + appendFlashMessage( + redirectUrl, + "error", + "A catalog entry id is required to clear checklist item sign-off metadata.", + ); + + return Response.redirect(redirectUrl, 303); + } + + const signoff = + await createRunrootApiClient().clearAuditCatalogChecklistItemSignoff( + catalogEntryId, + ); + + appendFlashMessage( + redirectUrl, + "notice", + `Checklist item sign-offs for ${signoff.acknowledgment.attestation.evidence.verification.resolution.blocker.progress.checklist.assignment.review.visibility.catalogEntry.entry.name} cleared.`, + ); + + return Response.redirect(redirectUrl, 303); + } + if (intent === "clear-progress") { const catalogEntryId = readTrimmedFormValue(formData, "catalogEntryId"); @@ -1216,3 +1290,49 @@ function readChecklistItemAcknowledgmentItems( }; }); } + +function readChecklistItemSignoffItems( + value: FormDataEntryValue | null, +): readonly { + readonly item: string; + readonly state: "signed-off" | "unsigned"; +}[] { + if (typeof value !== "string") { + return []; + } + + return value + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => { + const separatorIndex = line.indexOf(":"); + + if (separatorIndex < 0) { + return { + item: line, + state: "unsigned" as const, + }; + } + + const rawState = line.slice(0, separatorIndex).trim(); + const item = line.slice(separatorIndex + 1).trim(); + + if (item.length === 0) { + throw new Error( + "Checklist item sign-off lines require a non-empty item.", + ); + } + + if (rawState !== "signed-off" && rawState !== "unsigned") { + throw new Error( + `Checklist item sign-off state must be signed-off or unsigned for "${item}".`, + ); + } + + return { + item, + state: rawState, + }; + }); +} diff --git a/apps/web/src/app/runs/page.tsx b/apps/web/src/app/runs/page.tsx index 6c4efeb..1c8d878 100644 --- a/apps/web/src/app/runs/page.tsx +++ b/apps/web/src/app/runs/page.tsx @@ -9,6 +9,7 @@ import { ChecklistItemEvidencesView, ChecklistItemProgressView, ChecklistItemResolutionsView, + ChecklistItemSignoffsView, ChecklistItemVerificationsView, ConsoleShell, CrossRunAuditNavigationView, @@ -30,6 +31,7 @@ import { type ApiAuditCatalogChecklistItemEvidenceView, type ApiAuditCatalogChecklistItemProgressView, type ApiAuditCatalogChecklistItemResolutionView, + type ApiAuditCatalogChecklistItemSignoffView, type ApiAuditCatalogChecklistItemVerificationView, type ApiAuditCatalogEntryApplication, type ApiAuditCatalogReviewAssignmentView, @@ -64,6 +66,7 @@ export default async function RunsPage({ const drilldownFilters = readAuditDrilldownFilters(resolvedSearchParams); const [ runs, + signedOffEntries, acknowledgedEntries, blockedEntries, evidencedEntries, @@ -81,6 +84,7 @@ export default async function RunsPage({ catalogChecklistItemBlocker, catalogChecklistItemResolution, catalogChecklistItemEvidence, + catalogChecklistItemSignoff, catalogChecklistItemAcknowledgment, catalogChecklistItemAttestation, catalogChecklistItemVerification, @@ -90,6 +94,7 @@ export default async function RunsPage({ catalogReviewAssignment, ] = await Promise.all([ api.listRuns(), + api.listSignedOffAuditCatalogEntries(), api.listAcknowledgedAuditCatalogEntries(), api.listBlockedAuditCatalogEntries(), api.listEvidencedAuditCatalogEntries(), @@ -128,6 +133,11 @@ export default async function RunsPage({ .getAuditCatalogChecklistItemEvidence(catalogEntryId) .catch(() => undefined) : Promise.resolve(undefined), + catalogEntryId + ? api + .getAuditCatalogChecklistItemSignoff(catalogEntryId) + .catch(() => undefined) + : Promise.resolve(undefined), catalogEntryId ? api .getAuditCatalogChecklistItemAcknowledgment(catalogEntryId) @@ -176,6 +186,9 @@ export default async function RunsPage({ let activeCatalogChecklistItemEvidence: | ApiAuditCatalogChecklistItemEvidenceView | undefined; + let activeCatalogChecklistItemSignoff: + | ApiAuditCatalogChecklistItemSignoffView + | undefined; let activeCatalogChecklistItemAcknowledgment: | ApiAuditCatalogChecklistItemAcknowledgmentView | undefined; @@ -200,6 +213,7 @@ export default async function RunsPage({ activeCatalogChecklistItemBlocker = catalogChecklistItemBlocker; activeCatalogChecklistItemResolution = catalogChecklistItemResolution; activeCatalogChecklistItemEvidence = catalogChecklistItemEvidence; + activeCatalogChecklistItemSignoff = catalogChecklistItemSignoff; activeCatalogChecklistItemAcknowledgment = catalogChecklistItemAcknowledgment; activeCatalogChecklistItemAttestation = catalogChecklistItemAttestation; @@ -241,6 +255,12 @@ export default async function RunsPage({ ? { activeCatalogChecklistItemEvidence } : {})} /> + { } }); - it("renders, records, clears, and reapplies checklist item acknowledgments through the existing API surface", async () => { + it("renders, records, clears, and reapplies checklist item acknowledgments and sign-offs through the existing API surface", async () => { const workspaceRoot = await mkdtemp( join(tmpdir(), "runroot-web-checklist-acknowledgment-"), ); @@ -3086,9 +3086,34 @@ describe("@runroot/web integration", () => { expect(acknowledgmentResponse.status).toBe(303); + const signoffForm = new FormData(); + signoffForm.set("catalogEntryId", "catalog_entry_acknowledgment_web"); + signoffForm.set("intent", "sign-off"); + signoffForm.set( + "signoffItems", + "signed-off: Validate queued follow-up\nunsigned: Close backup handoff", + ); + signoffForm.set( + "signoffNote", + "Backup signed off the acknowledged follow-up", + ); + signoffForm.set( + "returnTo", + "/runs?catalogEntryId=catalog_entry_acknowledgment_web", + ); + + const signoffResponse = await mutateCatalog( + new Request("http://localhost/runs/catalog", { + body: signoffForm, + method: "POST", + }), + ); + + expect(signoffResponse.status).toBe(303); + process.env.RUNROOT_API_BASE_URL = peerAddress; - const acknowledgedMarkup = renderToStaticMarkup( + const signedOffMarkup = renderToStaticMarkup( await RunsPage({ searchParams: Promise.resolve({ catalogEntryId: "catalog_entry_acknowledgment_web", @@ -3096,19 +3121,64 @@ describe("@runroot/web integration", () => { }), ); - expect(acknowledgedMarkup).toContain("Checklist item acknowledgments"); - expect(acknowledgedMarkup).toContain("Queued acknowledgment preset"); - expect(acknowledgedMarkup).toContain("1/2 acknowledged"); - expect(acknowledgedMarkup).toContain("Validate queued follow-up"); - expect(acknowledgedMarkup).toContain("Close backup handoff"); - expect(acknowledgedMarkup).toContain( + expect(signedOffMarkup).toContain("Checklist item acknowledgments"); + expect(signedOffMarkup).toContain("Queued acknowledgment preset"); + expect(signedOffMarkup).toContain("1/2 acknowledged"); + expect(signedOffMarkup).toContain("Validate queued follow-up"); + expect(signedOffMarkup).toContain("Close backup handoff"); + expect(signedOffMarkup).toContain( "Backup acknowledged the attested follow-up", ); - expect(acknowledgedMarkup).toContain("Apply acknowledged preset"); - expect(acknowledgedMarkup).toContain( + expect(signedOffMarkup).toContain("Apply acknowledged preset"); + expect(signedOffMarkup).toContain( `Queued worker ${queuedRun.id} handed to backup`, ); - expect(acknowledgedMarkup).toContain("Active acknowledgments selected"); + expect(signedOffMarkup).toContain("Active acknowledgments selected"); + expect(signedOffMarkup).toContain("Checklist item sign-offs"); + expect(signedOffMarkup).toContain("1/2 signed off"); + expect(signedOffMarkup).toContain( + "Backup signed off the acknowledged follow-up", + ); + expect(signedOffMarkup).toContain("Apply signed-off preset"); + expect(signedOffMarkup).toContain("Active sign-offs selected"); + + process.env.RUNROOT_API_BASE_URL = ownerAddress; + + const clearSignoffForm = new FormData(); + clearSignoffForm.set( + "catalogEntryId", + "catalog_entry_acknowledgment_web", + ); + clearSignoffForm.set("intent", "clear-sign-off"); + clearSignoffForm.set( + "returnTo", + "/runs?catalogEntryId=catalog_entry_acknowledgment_web", + ); + + const clearSignoffResponse = await mutateCatalog( + new Request("http://localhost/runs/catalog", { + body: clearSignoffForm, + method: "POST", + }), + ); + + expect(clearSignoffResponse.status).toBe(303); + + process.env.RUNROOT_API_BASE_URL = peerAddress; + + const clearedSignoffMarkup = renderToStaticMarkup( + await RunsPage({ + searchParams: Promise.resolve({ + catalogEntryId: "catalog_entry_acknowledgment_web", + }), + }), + ); + + expect(clearedSignoffMarkup).toContain("Checklist item sign-offs"); + expect(clearedSignoffMarkup).toContain("No checklist item sign-offs yet"); + expect(clearedSignoffMarkup).not.toContain( + "Backup signed off the acknowledged follow-up", + ); process.env.RUNROOT_API_BASE_URL = ownerAddress; diff --git a/apps/web/src/components/console.tsx b/apps/web/src/components/console.tsx index 6db2d9d..11d6e24 100644 --- a/apps/web/src/components/console.tsx +++ b/apps/web/src/components/console.tsx @@ -22,6 +22,9 @@ import type { ApiAuditCatalogChecklistItemResolutionCollection, ApiAuditCatalogChecklistItemResolutionItem, ApiAuditCatalogChecklistItemResolutionView, + ApiAuditCatalogChecklistItemSignoffCollection, + ApiAuditCatalogChecklistItemSignoffItem, + ApiAuditCatalogChecklistItemSignoffView, ApiAuditCatalogChecklistItemVerificationCollection, ApiAuditCatalogChecklistItemVerificationItem, ApiAuditCatalogChecklistItemVerificationView, @@ -1378,6 +1381,157 @@ export function ChecklistItemAcknowledgmentsView({ ); } +export function ChecklistItemSignoffsView({ + activeCatalogChecklistItemSignoff, + signedOffEntries, +}: Readonly<{ + activeCatalogChecklistItemSignoff?: ApiAuditCatalogChecklistItemSignoffView; + signedOffEntries: ApiAuditCatalogChecklistItemSignoffCollection; +}>) { + return ( +
+
+
+
+ Phase 28 / Checklist Item Sign-Offs +
+

Checklist item sign-offs

+

+ Track thin per-item sign-offs and a single sign-off note on + acknowledged presets without turning the console into an approval + product, workflow engine, or collaboration surface. +

+
+
+ {signedOffEntries.totalCount} signed-off preset(s) +
+
+ + {activeCatalogChecklistItemSignoff ? ( +
+ Active sign-offs:{" "} + + { + activeCatalogChecklistItemSignoff.acknowledgment.attestation + .evidence.verification.resolution.blocker.progress.checklist + .assignment.review.visibility.catalogEntry.entry.name + } + + {" · "} + {formatSignoffSummary( + activeCatalogChecklistItemSignoff.signoff.items, + )} + {activeCatalogChecklistItemSignoff.signoff.signoffNote + ? ` · ${activeCatalogChecklistItemSignoff.signoff.signoffNote}` + : ""} +
+ ) : null} + + {signedOffEntries.items.length === 0 ? ( +

+ No checklist item sign-offs yet. Record acknowledgments first, then + save thin sign-off metadata through the shared operator seam. +

+ ) : ( +
    + {signedOffEntries.items.map((signoffView) => ( +
  1. +
    +
    + + { + signoffView.acknowledgment.attestation.evidence + .verification.resolution.blocker.progress.checklist + .assignment.review.visibility.catalogEntry.entry.name + } + +
    + {formatSignoffSummary(signoffView.signoff.items)} + {" · "} + {formatAcknowledgmentSummary( + signoffView.acknowledgment.acknowledgment.items, + )} + {" · "} + {formatAttestationSummary( + signoffView.acknowledgment.attestation.attestation.items, + )} +
    +
    + {formatTimestamp(signoffView.signoff.updatedAt)} +
    +
      + {signoffView.signoff.items.map((item) => ( +
    • + {item.state}: {item.item} +
    • + ))} +
    + {signoffView.signoff.signoffNote ? ( +

    {signoffView.signoff.signoffNote}

    + ) : null} +
    + operator {signoffView.signoff.operatorId} · scope{" "} + {signoffView.signoff.scopeId} +
    +
    + + Apply signed-off preset + +
    + + + + +
    +
    + {activeCatalogChecklistItemSignoff?.signoff.catalogEntryId === + signoffView.signoff.catalogEntryId ? ( +
    Active sign-offs selected
    + ) : null} +
  2. + ))} +
+ )} +
+ ); +} + export function CatalogReviewAssignmentsView({ activeCatalogReviewAssignment, assignedEntries, @@ -1613,6 +1767,7 @@ export function CatalogReviewSignalsView({ export function AuditViewCatalogsView({ activeCatalogEntry, activeCatalogChecklistItemBlocker, + activeCatalogChecklistItemSignoff, activeCatalogChecklistItemAcknowledgment, activeCatalogChecklistItemAttestation, activeCatalogChecklistItemEvidence, @@ -1620,6 +1775,7 @@ export function AuditViewCatalogsView({ activeCatalogChecklistItemVerification, activeCatalogChecklistItemProgress, assignedEntries, + signedOffEntries, acknowledgedEntries, attestedEntries, blockedEntries, @@ -1633,6 +1789,7 @@ export function AuditViewCatalogsView({ }: Readonly<{ activeCatalogEntry?: ApiAuditCatalogVisibilityView; activeCatalogChecklistItemBlocker?: ApiAuditCatalogChecklistItemBlockerView; + activeCatalogChecklistItemSignoff?: ApiAuditCatalogChecklistItemSignoffView; activeCatalogChecklistItemAcknowledgment?: ApiAuditCatalogChecklistItemAcknowledgmentView; activeCatalogChecklistItemAttestation?: ApiAuditCatalogChecklistItemAttestationView; activeCatalogChecklistItemEvidence?: ApiAuditCatalogChecklistItemEvidenceView; @@ -1640,6 +1797,7 @@ export function AuditViewCatalogsView({ activeCatalogChecklistItemVerification?: ApiAuditCatalogChecklistItemVerificationView; activeCatalogChecklistItemProgress?: ApiAuditCatalogChecklistItemProgressView; assignedEntries: ApiAuditCatalogReviewAssignmentCollection; + signedOffEntries: ApiAuditCatalogChecklistItemSignoffCollection; acknowledgedEntries: ApiAuditCatalogChecklistItemAcknowledgmentCollection; attestedEntries: ApiAuditCatalogChecklistItemAttestationCollection; blockedEntries: ApiAuditCatalogChecklistItemBlockerCollection; @@ -1739,6 +1897,17 @@ export function AuditViewCatalogsView({ ] as const, ), ); + const signoffsByCatalogEntryId = new Map( + signedOffEntries.items.map( + (item) => + [ + item.acknowledgment.attestation.evidence.verification.resolution + .blocker.progress.checklist.assignment.review.visibility + .catalogEntry.entry.id, + item, + ] as const, + ), + ); return (
@@ -1825,6 +1994,15 @@ export function AuditViewCatalogsView({ : acknowledgmentsByCatalogEntryId.get( catalogEntry.catalogEntry.entry.id, ); + const signoff = + activeCatalogChecklistItemSignoff?.acknowledgment.attestation + .evidence.verification.resolution.blocker.progress.checklist + .assignment.review.visibility.catalogEntry.entry.id === + catalogEntry.catalogEntry.entry.id + ? activeCatalogChecklistItemSignoff + : signoffsByCatalogEntryId.get( + catalogEntry.catalogEntry.entry.id, + ); const reviewSignal = reviewSignalsByCatalogEntryId.get( catalogEntry.catalogEntry.entry.id, ); @@ -1867,6 +2045,14 @@ export function AuditViewCatalogsView({ attestation.attestation.items, )}` : ""} + {acknowledgment + ? ` · ${formatAcknowledgmentSummary( + acknowledgment.acknowledgment.items, + )}` + : ""} + {signoff + ? ` · ${formatSignoffSummary(signoff.signoff.items)}` + : ""} @@ -2581,6 +2767,57 @@ export function AuditViewCatalogsView({ ) : null} + {acknowledgment ? ( +
+ + + +
+