filegrc 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -6
- package/model/index.js +4 -4
- package/model/v4.json +10052 -0
- package/package.json +2 -2
- package/src/agent.js +17 -5
- package/src/audit-preparation.js +17 -13
- package/src/audit-transition.js +7 -4
- package/src/batch-review.js +31 -8
- package/src/cli.js +43 -25
- package/src/collection-review.js +71 -25
- package/src/evidence-packet.js +76 -43
- package/src/external-reviewer.js +4 -4
- package/src/files.js +70 -6
- package/src/git.js +70 -13
- package/src/index.js +1 -0
- package/src/model-migration.js +631 -14
- package/src/obligations.js +14 -7
- package/src/program-path.js +49 -38
- package/src/program-readiness.js +94 -54
- package/src/program.js +52 -0
- package/src/reconciliation.js +2 -2
- package/src/server.js +50 -8
- package/src/setup.js +56 -21
- package/src/source-coverage.js +13 -9
- package/src/startup.js +1 -1
- package/src/state.js +17 -14
- package/src/timing.js +6 -0
- package/src/validate.js +100 -9
- package/src/web.js +709 -151
- package/src/workflow.js +47 -25
package/src/web.js
CHANGED
|
@@ -11,6 +11,21 @@ import {
|
|
|
11
11
|
import { PROGRAM_PATH, RESOURCE_INSTRUCTIONS, RESOURCE_PAGE_SUMMARIES } from "./program-path.js";
|
|
12
12
|
import { formatCalendarDate, formatLocalDateTime } from "./time.js";
|
|
13
13
|
|
|
14
|
+
export function dashboardProgramReadiness(programReadiness = {}) {
|
|
15
|
+
const progress = programReadiness.progress || {};
|
|
16
|
+
const lifecycle = programReadiness.operating
|
|
17
|
+
? { status: "Operating", tone: "good" }
|
|
18
|
+
: programReadiness.evidenceReady
|
|
19
|
+
? { status: "Evidence ready", tone: "good" }
|
|
20
|
+
: { status: "Needs work", tone: "warn" };
|
|
21
|
+
return {
|
|
22
|
+
percent: progress.percent ?? 0,
|
|
23
|
+
complete: progress.complete ?? 0,
|
|
24
|
+
total: progress.total ?? 0,
|
|
25
|
+
...lifecycle
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
export function renderIndex(state = null) {
|
|
15
30
|
const snapshot = state
|
|
16
31
|
? `<script id="filegrc-data" type="application/json">${safeJson(state)}</script>`
|
|
@@ -51,6 +66,7 @@ const READINESS_STAGES = SHARED_PROGRAM_STAGES.map((stage) => ({
|
|
|
51
66
|
number: String(stage.number)
|
|
52
67
|
}));
|
|
53
68
|
const RESOURCE_GUIDE_INSTRUCTIONS = ${JSON.stringify(RESOURCE_INSTRUCTIONS)};
|
|
69
|
+
const dashboardProgramReadiness = ${dashboardProgramReadiness.toString()};
|
|
54
70
|
const STAGE_PAGE_SUMMARIES = ${JSON.stringify({
|
|
55
71
|
...RESOURCE_PAGE_SUMMARIES,
|
|
56
72
|
"utility:audit-packet": "Review fieldwork readiness and build the indexed evidence packet."
|
|
@@ -65,12 +81,15 @@ let onboardingShade = null;
|
|
|
65
81
|
let onboardingStep = 0;
|
|
66
82
|
let onboardingDraft = null;
|
|
67
83
|
let onboardingBusy = false;
|
|
84
|
+
let onboardingSetupOnly = false;
|
|
68
85
|
let onboardingStillWorkingTimer = null;
|
|
69
86
|
let onboardingPendingDraft = false;
|
|
70
87
|
const resourceDetailRequests = new Map();
|
|
71
88
|
let resourceGuideCleanup = null;
|
|
72
89
|
let repositorySyncPollTimer = null;
|
|
73
90
|
let repositorySyncPollInFlight = false;
|
|
91
|
+
let mutationStateRefreshInFlight = false;
|
|
92
|
+
let mutationStateRefreshTimer = null;
|
|
74
93
|
|
|
75
94
|
start().catch((error) => {
|
|
76
95
|
root.innerHTML = '<main class="fatal"><h1>Could Not Load the Workspace</h1><pre></pre></main>';
|
|
@@ -85,7 +104,7 @@ async function start() {
|
|
|
85
104
|
window.addEventListener("scroll", positionCurrentOnboarding, true);
|
|
86
105
|
render();
|
|
87
106
|
scheduleRepositorySyncPoll();
|
|
88
|
-
if (!state.readOnly && rendererSettingsEntry()?.record.showOnboarding === true) {
|
|
107
|
+
if (!state.readOnly && rendererSettingsEntry()?.record.showOnboarding === true && !initialSetupSystem()) {
|
|
89
108
|
queueMicrotask(requestOnboarding);
|
|
90
109
|
}
|
|
91
110
|
}
|
|
@@ -191,6 +210,16 @@ function readinessStageForType(type) {
|
|
|
191
210
|
));
|
|
192
211
|
}
|
|
193
212
|
|
|
213
|
+
function activeProgram() {
|
|
214
|
+
return resourcesOfType("program").find(({ record }) => !["retired"].includes(record.status))?.record || state.workspace;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function reviewedRequirementIds() {
|
|
218
|
+
return new Set((activeProgram().requirementApplicability || [])
|
|
219
|
+
.filter(({ decision }) => ["applicable", "not-applicable"].includes(decision))
|
|
220
|
+
.map(({ requirementId }) => requirementId));
|
|
221
|
+
}
|
|
222
|
+
|
|
194
223
|
function renderSidebarUtility(utility, route, direct = false) {
|
|
195
224
|
const directClass = direct ? "nav-direct " : "";
|
|
196
225
|
if (utility === "obligation-board") return "";
|
|
@@ -256,31 +285,32 @@ function renderHome(main) {
|
|
|
256
285
|
'<div class="overview-grid"><section class="panel obligation-panel"><div class="panel-head"><div><p class="kicker">Policy obligations</p><h3>' + obligationHeading + '</h3></div><a href="#/stage/run">Open board</a></div>' + obligationPreview(previewObligations) + '</section>' +
|
|
257
286
|
'<section class="panel event-reminder-panel"><div class="panel-head"><div><p class="kicker">Event reminders</p><h3>Did Something Change?</h3></div><a href="#/stage/run?section=events">' + (acceptedEventTriggers.length ? "Trigger work" : "Review proposals") + '</a></div>' + eventReminderPreview(orderedPolicyEventTriggers(state.obligations.triggers).slice(0, 4)) + '</section>' +
|
|
258
287
|
auditPanel + '</div></div>';
|
|
259
|
-
main.querySelector("#resume-setup")?.addEventListener("click", requestOnboarding);
|
|
288
|
+
main.querySelector("#resume-setup")?.addEventListener("click", () => requestOnboarding({ setupOnly: Boolean(initialSetupSystem()) }));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function initialSetupSystem() {
|
|
292
|
+
const program = activeProgram();
|
|
293
|
+
return resourcesOfType("system").find(({ record }) => (program.systemIds || []).includes(record.id) && record.status !== "retired")?.record || null;
|
|
260
294
|
}
|
|
261
295
|
|
|
262
296
|
function initialSetupBanner() {
|
|
263
|
-
const
|
|
297
|
+
const program = activeProgram();
|
|
298
|
+
const system = initialSetupSystem();
|
|
264
299
|
if (!system) {
|
|
265
|
-
return '<section class="setup-banner"><div><p class="kicker">Setup
|
|
300
|
+
return '<section class="setup-banner"><div><p class="kicker">Setup</p><h3>Define the service in scope</h3><p>Name the service and choose the program goal.</p></div><button class="button primary" type="button" id="resume-setup">Start setup</button></section>';
|
|
266
301
|
}
|
|
267
|
-
const goal = programGoalFromKind(
|
|
302
|
+
const goal = programGoalFromKind(program.assuranceGoal);
|
|
268
303
|
const goalLabels = {
|
|
269
304
|
readiness: "Program Readiness",
|
|
270
305
|
"type-1": "SOC 2 Type 1",
|
|
271
306
|
"type-2": "SOC 2 Type 2"
|
|
272
307
|
};
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
: "Confirm the saved program goal: " + goalLabels[goal] + ".";
|
|
276
|
-
const completion = system.status === "planned"
|
|
277
|
-
? "Confirm the service scope to activate the planned service and continue to Step 1."
|
|
278
|
-
: "Confirm the service scope to close onboarding and continue to Step 1.";
|
|
279
|
-
return '<section class="setup-banner"><div><p class="kicker">Setup draft saved</p><h3>Review the initial service scope</h3><p>' + esc(system.title) + ' already has a saved service boundary.</p></div><ol><li>Review the saved service boundary.</li><li>' + esc(goalStep) + '</li><li>' + esc(completion) + '</li><li><button class="text-button" type="button" id="resume-setup">Resume setup</button></li></ol></section>';
|
|
308
|
+
const goalLabel = goal === "none" ? "Choose goal" : goalLabels[goal];
|
|
309
|
+
return '<section class="setup-banner"><div><p class="kicker">Setup draft</p><h3>' + esc(system.title) + '</h3><p>Review and confirm the initial scope.</p></div><div class="setup-draft-state"><span><small>Program goal</small><strong>' + esc(goalLabel) + '</strong></span><span><small>System</small><strong>' + esc(properCase(system.status)) + '</strong></span><button class="button primary" type="button" id="resume-setup">Resume setup</button></div></section>';
|
|
280
310
|
}
|
|
281
311
|
|
|
282
312
|
function readinessOverview() {
|
|
283
|
-
const progress =
|
|
313
|
+
const progress = dashboardProgramReadiness(state.programReadiness);
|
|
284
314
|
const nextHref = nextProgramStageHref();
|
|
285
315
|
const programStage = (id, href) => {
|
|
286
316
|
const stage = READINESS_STAGES.find((candidate) => candidate.id === id);
|
|
@@ -296,7 +326,7 @@ function readinessOverview() {
|
|
|
296
326
|
programStage("run", "#/stage/run"),
|
|
297
327
|
programStage("audit", "#/stage/audit")
|
|
298
328
|
];
|
|
299
|
-
return '<section class="readiness-map"><div class="readiness-map-head"><div><p class="kicker">SOC 2 program path</p><h3>Prepare, Operate, Then Audit</h3></div><div class="readiness-progress-summary"><div><span>Program readiness</span><strong>' + progress.percent + '%</strong><div class="progress"><span style="width:' + progress.percent + '%"></span></div><small>' + esc(progress.complete + " of " + progress.total + "
|
|
329
|
+
return '<section class="readiness-map"><div class="readiness-map-head"><div><p class="kicker">SOC 2 program path</p><h3>Prepare, Operate, Then Audit</h3></div><div class="readiness-progress-summary"><div><span>Program readiness</span><strong>' + progress.percent + '%</strong><b class="badge ' + esc(progress.tone) + '">' + esc(progress.status) + '</b><div class="progress"><span style="width:' + progress.percent + '%"></span></div><small>' + esc(progress.complete + " of " + progress.total + " readiness items complete") + '</small></div><a class="button primary" href="' + nextHref + '">Continue</a></div></div><div class="readiness-flow">' + stages.map(([title, body, href, status, tone], index) => '<a href="' + href + '"><span>' + (index + 1) + '</span><strong>' + esc(title) + '</strong><small>' + esc(body) + '</small><b class="readiness-state ' + esc(tone) + '">' + esc(status) + '</b></a>').join("") + '</div></section>';
|
|
300
330
|
}
|
|
301
331
|
|
|
302
332
|
function nextProgramStageHref() {
|
|
@@ -316,6 +346,10 @@ function renderStageOverview(main, stageId, params = new URLSearchParams()) {
|
|
|
316
346
|
main.innerHTML = '<div class="page stage-overview-page"><nav class="breadcrumbs"><a href="#/">Overview</a><span>/</span><span>' + esc(stage.title) + '</span></nav>' +
|
|
317
347
|
'<section class="stage-overview-hero"><div><p class="kicker">Step ' + esc(stage.number) + ' of 5</p><h2>' + esc(stage.title) + '</h2><p>' + esc(stage.summary) + '</p></div>' + stageProgressCard(progress) + '</section>' +
|
|
318
348
|
renderStagePageIndex(stage) + (stage.id === "controls" ? renderEvidenceReadiness() : "") + '</div>';
|
|
349
|
+
main.querySelector("[data-show-evidence-families]")?.addEventListener("click", (event) => {
|
|
350
|
+
main.querySelectorAll("[data-evidence-family-extra]").forEach((card) => { card.hidden = false; });
|
|
351
|
+
event.currentTarget.remove();
|
|
352
|
+
});
|
|
319
353
|
}
|
|
320
354
|
|
|
321
355
|
function workflowGuidance(options = {}) {
|
|
@@ -354,20 +388,54 @@ function collectionReviewPanel(type) {
|
|
|
354
388
|
if (!assessment) return "";
|
|
355
389
|
const configuration = assessment.configuration;
|
|
356
390
|
const current = assessment.status === "current";
|
|
391
|
+
const needsFirstRecord = collectionNeedsFirstRecord(type);
|
|
357
392
|
const reviewerNames = (assessment.review?.reviewedByIds || [])
|
|
358
393
|
.map((id) => state.resources.find(({ record }) => record.id === id)?.record.title || id);
|
|
394
|
+
const reviewNote = assessment.review?.rationale
|
|
395
|
+
? " Note: " + esc(assessment.review.rationale)
|
|
396
|
+
: "";
|
|
359
397
|
const reviewSummary = current
|
|
360
|
-
? '<p class="collection-review-result"><strong>' + esc(properCase(assessment.review.decision)) + '</strong><span>Reviewed ' + esc(formatCalendarDate(assessment.review.reviewedOn)) + (reviewerNames.length ? " by " + esc(reviewerNames.join(", ")) : "") + '
|
|
361
|
-
: '<p class="collection-review-result"><strong>' + (assessment.status === "stale" ? "Review again" : "Review required") + '</strong><span>' + esc(assessment.message) + '</span></p>';
|
|
362
|
-
|
|
398
|
+
? '<p class="collection-review-result"><strong>' + esc(properCase(assessment.review.decision)) + '</strong><span>Reviewed ' + esc(formatCalendarDate(assessment.review.reviewedOn)) + (reviewerNames.length ? " by " + esc(reviewerNames.join(", ")) : "") + "." + reviewNote + '</span></p>'
|
|
399
|
+
: '<p class="collection-review-result"><strong>' + (needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Review again" : "Review required") + '</strong><span>' + esc(assessment.message) + '</span></p>';
|
|
400
|
+
const action = state.readOnly
|
|
401
|
+
? ""
|
|
402
|
+
: needsFirstRecord
|
|
403
|
+
? '<a class="button primary" href="#/resources/' + encodeURIComponent(type) + '?new=1">Add first ' + esc(state.model.resources[type].title.toLowerCase()) + '</a>'
|
|
404
|
+
: '<button class="button ' + (current ? "" : "primary") + '" type="button" data-review-collection="' + esc(type) + '">' + (current ? "Review again" : "Review and confirm") + '</button>';
|
|
405
|
+
return '<section class="collection-review-panel panel ' + (current ? "current" : "required") + '"><div class="collection-review-head"><div><p class="kicker">Scope confirmation</p><h3>' + esc(configuration.title) + '</h3><p>' + esc(configuration.description) + '</p></div><span class="badge ' + (current ? "good" : "warn") + '">' + (current ? "Reviewed" : needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Stale" : "Review required") + '</span></div>' +
|
|
363
406
|
'<details ' + (current ? "" : "open") + '><summary>What to review</summary><ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></details>' +
|
|
364
|
-
'<div class="collection-review-foot">' + reviewSummary +
|
|
407
|
+
'<div class="collection-review-foot">' + reviewSummary + action + '</div></section>';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function collectionNeedsFirstRecord(type) {
|
|
411
|
+
const assessment = state.collectionReviews?.[type];
|
|
412
|
+
if (!assessment || assessment.recordCount) return false;
|
|
413
|
+
return !(assessment.configuration.decisions || []).some((decision) => (
|
|
414
|
+
decision === "zero-population" || decision === "externally-managed"
|
|
415
|
+
));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function resourceCreationAllowed(type) {
|
|
419
|
+
return type !== "program" || !resourcesOfType("program").some(({ record }) => record.status !== "retired");
|
|
365
420
|
}
|
|
366
421
|
|
|
367
|
-
function
|
|
422
|
+
function collectionEmptyState(type, definition) {
|
|
423
|
+
const assessment = state.collectionReviews?.[type];
|
|
424
|
+
if (!assessment) {
|
|
425
|
+
return definition.guidance?.emptyState || "No records exist. Use the page guidance to decide whether a record is required, then add only real program facts.";
|
|
426
|
+
}
|
|
427
|
+
return collectionNeedsFirstRecord(type)
|
|
428
|
+
? "No " + definition.pluralTitle.toLowerCase() + " have been added yet."
|
|
429
|
+
: "No records exist. Use the scope confirmation above to record an allowed empty-collection conclusion, or add the records management identified.";
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function resourceReviewCriteria(type, collapsed = false) {
|
|
368
433
|
const reviewPoints = state.model.resources[type]?.guidance?.reviewPoints || [];
|
|
369
434
|
if (!reviewPoints.length) return "";
|
|
370
|
-
|
|
435
|
+
const content = '<strong>Review criteria</strong><ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul>';
|
|
436
|
+
return collapsed
|
|
437
|
+
? '<details class="resource-review-criteria compact"><summary>Review criteria</summary><ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></details>'
|
|
438
|
+
: '<section class="resource-review-criteria">' + content + '</section>';
|
|
371
439
|
}
|
|
372
440
|
|
|
373
441
|
function recordWorkflowItems(type, id) {
|
|
@@ -393,7 +461,8 @@ function recordWorkflowCell(type, entry) {
|
|
|
393
461
|
if (!items.length) return '<span class="record-workflow-clear">No calculated action</span>';
|
|
394
462
|
const item = items[0];
|
|
395
463
|
const href = workflowItemHref(item) || "#/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(entry.record.id);
|
|
396
|
-
|
|
464
|
+
const title = type === "control" ? "Finish implementation" : item.title;
|
|
465
|
+
return '<a class="record-workflow-action" href="' + href + '"><span class="workflow-finding-status ' + esc(item.state) + '">' + esc(properCase(item.state)) + '</span><span><strong>' + esc(title) + '</strong><small>' + esc(stagePageItemDetail(item)) + (items.length > 1 ? " +" + (items.length - 1) + " more" : "") + '</small></span></a>';
|
|
397
466
|
}
|
|
398
467
|
|
|
399
468
|
function openCollectionReviewDialog(type) {
|
|
@@ -401,7 +470,9 @@ function openCollectionReviewDialog(type) {
|
|
|
401
470
|
if (!assessment) return;
|
|
402
471
|
const configuration = assessment.configuration;
|
|
403
472
|
const people = resourcesOfType("person").filter(({ record }) => record.status === "active");
|
|
404
|
-
const
|
|
473
|
+
const v4 = String(state.model.modelVersion) === "4";
|
|
474
|
+
const sourceType = v4 ? "component" : "system";
|
|
475
|
+
const systems = resourcesOfType(sourceType).filter(({ record }) => record.status === "active");
|
|
405
476
|
const allowedDecisions = configuration.decisions || ["complete"];
|
|
406
477
|
const defaultDecision = assessment.review?.decision
|
|
407
478
|
|| (!assessment.recordCount && allowedDecisions.includes("zero-population") ? "zero-population" : allowedDecisions[0]);
|
|
@@ -411,15 +482,17 @@ function openCollectionReviewDialog(type) {
|
|
|
411
482
|
const dialog = document.createElement("dialog");
|
|
412
483
|
dialog.className = "commit-dialog event-dialog collection-review-dialog";
|
|
413
484
|
dialog.setAttribute("aria-labelledby", "collection-review-dialog-title");
|
|
414
|
-
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Scope confirmation</p><h2 id="collection-review-dialog-title">Confirm ' + esc(configuration.title.toLowerCase()) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>' + esc(configuration.description) + '</p><section class="event-dialog-steps collection-review-checks"><strong>Before confirming</strong><ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></section><div class="form-grid"><label><span>Conclusion</span><select name="decision" required>' + decisions + '</select></label><label><span>Reviewer</span><select name="reviewerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + ((assessment.review?.reviewedByIds || []).includes(record.id) ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Reviewed on</span><input name="reviewedOn" type="date" required value="' + esc(assessment.review?.reviewedOn || currentDate()) + '"></label><label data-authoritative-system><span>Authoritative System</span><select name="
|
|
485
|
+
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Scope confirmation</p><h2 id="collection-review-dialog-title">Confirm ' + esc(configuration.title.toLowerCase()) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>' + esc(configuration.description) + '</p><section class="event-dialog-steps collection-review-checks"><strong>Before confirming</strong><ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></section><div class="form-grid"><label><span>Conclusion</span><select name="decision" required>' + decisions + '</select></label><label><span>Reviewer</span><select name="reviewerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + ((assessment.review?.reviewedByIds || []).includes(record.id) ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Reviewed on</span><input name="reviewedOn" type="date" required value="' + esc(assessment.review?.reviewedOn || currentDate()) + '"></label><label data-authoritative-system><span>Authoritative ' + (v4 ? "Component" : "System") + '</span><select name="authoritativeSourceId"><option value="">Select</option>' + systems.map(({ record }) => '<option value="' + esc(record.id) + '" ' + ((assessment.review?.authoritativeComponentId || assessment.review?.authoritativeSystemId) === record.id ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label class="full"><span>Review notes</span><textarea name="rationale" rows="3" required placeholder="Note what you confirmed and any scope decision that needs context.">' + esc(assessment.review?.rationale || "") + '</textarea></label></div><div class="workflow-preview" role="status"></div><div class="dialog-error" role="alert"></div><div class="dialog-actions"><span class="save-status review-save-status" role="status" aria-live="polite"></span><button type="button" class="button" data-event="cancel">Cancel</button><button type="submit" class="button primary" data-preview-collection-review>Preview confirmation</button></div></form>';
|
|
415
486
|
document.body.append(dialog);
|
|
416
487
|
dialog.showModal();
|
|
417
488
|
const form = dialog.querySelector("form");
|
|
489
|
+
const saveStatus = dialog.querySelector(".review-save-status");
|
|
490
|
+
const repositoryPrefetch = prefetchRepositoryForReview(saveStatus);
|
|
418
491
|
const systemField = dialog.querySelector("[data-authoritative-system]");
|
|
419
492
|
const syncDecision = () => {
|
|
420
493
|
const external = form.elements.decision.value === "externally-managed";
|
|
421
494
|
systemField.hidden = !external;
|
|
422
|
-
form.elements.
|
|
495
|
+
form.elements.authoritativeSourceId.required = external;
|
|
423
496
|
};
|
|
424
497
|
syncDecision();
|
|
425
498
|
dialog.querySelector(".icon-button").addEventListener("click", () => dialog.close());
|
|
@@ -443,7 +516,7 @@ function openCollectionReviewDialog(type) {
|
|
|
443
516
|
rationale: form.elements.rationale.value.trim(),
|
|
444
517
|
reviewedByIds: [form.elements.reviewerId.value],
|
|
445
518
|
reviewedOn: form.elements.reviewedOn.value,
|
|
446
|
-
authoritativeSystemId: form.elements.
|
|
519
|
+
[v4 ? "authoritativeComponentId" : "authoritativeSystemId"]: form.elements.authoritativeSourceId.value || undefined,
|
|
447
520
|
expectedRevision: assessment.reviewRevision || undefined
|
|
448
521
|
};
|
|
449
522
|
form.querySelectorAll("button,input,select,textarea").forEach((control) => { control.disabled = true; });
|
|
@@ -460,12 +533,15 @@ function openCollectionReviewDialog(type) {
|
|
|
460
533
|
dialog.querySelector(".workflow-preview").innerHTML = '<strong>Review before saving</strong><p>Save this confirmation for ' + preview.assessment.recordCount + ' current ' + esc(pluralize("record", preview.assessment.recordCount)) + '. If the collection or material scope changes, FileGRC will ask for another review.</p>';
|
|
461
534
|
dialog.querySelector("[data-preview-collection-review]").textContent = "Confirm and save";
|
|
462
535
|
} else {
|
|
536
|
+
saveStatus.textContent = "Validating and saving…";
|
|
537
|
+
const prefetch = await repositoryPrefetch;
|
|
463
538
|
const response = await localFetch("/api/collection-review", {
|
|
464
539
|
method: "POST",
|
|
465
|
-
headers: { "content-type": "application/json" },
|
|
466
|
-
body: JSON.stringify({ ...previewedPayload, confirmed: true })
|
|
540
|
+
headers: { "content-type": "application/json", prefer: "respond-async" },
|
|
541
|
+
body: JSON.stringify({ ...previewedPayload, confirmed: true, prefetchToken: prefetch?.token })
|
|
467
542
|
});
|
|
468
543
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
544
|
+
saveStatus.textContent = "Saved locally. Refreshing page…";
|
|
469
545
|
applyMutationState(await response.json());
|
|
470
546
|
dialog.close();
|
|
471
547
|
render();
|
|
@@ -566,11 +642,18 @@ function renderEvidenceReadiness() {
|
|
|
566
642
|
?.find((stage) => stage.id === "controls")
|
|
567
643
|
?.items.filter((item) => item.id.startsWith("source-family-")) || [];
|
|
568
644
|
const completeCount = items.filter((item) => item.status === "complete").length;
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
645
|
+
const visibleCount = 4;
|
|
646
|
+
const componentSources = items.some((item) => Array.isArray(item.sourceComponentIds));
|
|
647
|
+
const sourceType = componentSources ? "component" : "system";
|
|
648
|
+
const sourceLabel = componentSources ? "Components" : "Systems";
|
|
649
|
+
const cards = items.map((item, index) => {
|
|
650
|
+
const componentSources = Array.isArray(item.sourceComponentIds);
|
|
651
|
+
const sourceType = componentSources ? "component" : "system";
|
|
652
|
+
const sourceLabel = componentSources ? "Component" : "System";
|
|
653
|
+
const sources = (item.sourceComponentIds || item.sourceSystemIds || []).map((id) => {
|
|
654
|
+
const source = state.resources.find(({ record }) => record.type === sourceType && record.id === id)?.record;
|
|
655
|
+
const sourceCheck = (item.sourceComponentChecks || item.sourceSystemChecks || []).find((check) => (check.sourceComponentId || check.sourceSystemId) === id);
|
|
656
|
+
const complete = sourceCheck?.complete ?? (item.completeSourceComponentIds || item.completeSourceSystemIds || []).includes(id);
|
|
574
657
|
const status = complete
|
|
575
658
|
? "Ready"
|
|
576
659
|
: Object.entries(sourceCheck?.checks || {})
|
|
@@ -578,23 +661,26 @@ function renderEvidenceReadiness() {
|
|
|
578
661
|
.map(([name]) => evidenceSourceCheckLabel(name))
|
|
579
662
|
.join(", ") || "Needs details";
|
|
580
663
|
return source
|
|
581
|
-
? '<a class="evidence-map-source ' + (complete ? "complete" : "incomplete") + '" href="#/resource/
|
|
664
|
+
? '<a class="evidence-map-source ' + (complete ? "complete" : "incomplete") + '" href="#/resource/' + sourceType + '/' + encodeURIComponent(id) + '">' + esc(source.title) + '<small>' + esc(status) + '</small></a>'
|
|
582
665
|
: "";
|
|
583
666
|
}).join("");
|
|
584
667
|
const sourceAction = sources
|
|
585
668
|
? sources
|
|
586
|
-
: '<a class="button" href="#/resources/
|
|
669
|
+
: '<a class="button" href="#/resources/' + sourceType + '?new=1">Add source ' + sourceLabel + '</a>';
|
|
587
670
|
const method = item.operationRecordTypes?.length
|
|
588
671
|
? "FileGRC records: " + item.operationRecordTypes.map(properCase).join(", ")
|
|
589
672
|
: properCase(item.evidenceForm || "External evidence");
|
|
590
|
-
return '<article class="evidence-map-card ' + esc(item.status) + '"><div class="evidence-map-card-head"><div><span class="badge ' + (item.status === "complete" ? "good" : "warn") + '">' + (item.status === "complete" ? "Mapped" : "Needs mapping") + '</span><h3>' + esc(item.title) + '</h3></div><small>' + esc(method) + '</small></div><p>' + esc(item.description || item.message) + '</p>' +
|
|
591
|
-
(item.sourceKinds?.length ? '<div class="evidence-map-expectation"><strong>Source role</strong><span>' + item.sourceKinds.map((kind) =>
|
|
673
|
+
return '<article class="evidence-map-card ' + esc(item.status) + '"' + (index >= visibleCount ? ' data-evidence-family-extra hidden' : "") + '><div class="evidence-map-card-head"><div><span class="badge ' + (item.status === "complete" ? "good" : "warn") + '">' + (item.status === "complete" ? "Mapped" : "Needs mapping") + '</span><h3>' + esc(item.title) + '</h3></div><small>' + esc(method) + '</small></div><p>' + esc(item.description || item.message) + '</p>' +
|
|
674
|
+
(item.sourceKinds?.length ? '<div class="evidence-map-expectation"><strong>Source role</strong><span>' + item.sourceKinds.map((kind) => esc(properCase(kind))).join(" or ") + '</span></div>' : "") +
|
|
592
675
|
(item.evidencePrompt ? '<div class="evidence-map-expectation"><strong>Expected evidence</strong><span>' + esc(item.evidencePrompt) + '</span></div>' : "") +
|
|
593
676
|
(item.timing ? '<div class="evidence-map-expectation"><strong>When</strong><span>' + esc(item.timing) + '</span></div>' : "") +
|
|
594
677
|
'<div class="evidence-map-links"><div><small>Controls</small><div class="evidence-map-references">' + (item.controlIds || []).map((id) => formatReference(id)).join("") + '</div></div><div><small>Authoritative sources</small><div class="evidence-map-sources">' + sourceAction + '</div></div></div><p class="evidence-map-status">' + esc(item.message) + '</p></article>';
|
|
595
678
|
}).join("");
|
|
679
|
+
const more = items.length > visibleCount
|
|
680
|
+
? '<button class="button evidence-map-more" type="button" data-show-evidence-families>Show ' + (items.length - visibleCount) + ' more evidence families</button>'
|
|
681
|
+
: "";
|
|
596
682
|
const empty = '<section class="evidence-map-empty"><p class="kicker">Evidence readiness</p><h3>Select the program controls first</h3><p>Control implementation checks are generated from the selected Controls and their authoritative evidence sources.</p><a class="button primary" href="#/resources/control">Review Controls</a></section>';
|
|
597
|
-
return '<section class="evidence-map"><div class="evidence-map-head"><div><p class="kicker">Control implementation</p><h2>' + completeCount + ' of ' + items.length + ' evidence ' + (items.length === 1 ? "family" : "families") + ' ready</h2><p>Connect each Control to the
|
|
683
|
+
return '<section class="evidence-map"><div class="evidence-map-head"><div><p class="kicker">Control implementation</p><h2>' + completeCount + ' of ' + items.length + ' evidence ' + (items.length === 1 ? "family" : "families") + ' ready</h2><p>Connect each Control to the ' + sourceLabel + ' that produce its evidence.</p></div><div class="evidence-map-actions"><a class="button" href="#/resources/' + sourceType + '">Review ' + sourceLabel + '</a><a class="button primary" href="#/resources/control">Review Controls</a></div></div>' + (cards || empty) + more + '</section>';
|
|
598
684
|
}
|
|
599
685
|
|
|
600
686
|
function evidenceSourceCheckLabel(name) {
|
|
@@ -647,6 +733,7 @@ function stagePageItemDetail(item) {
|
|
|
647
733
|
? String(item.message || "").match(/^Complete (\d+) checks before implementation:/)
|
|
648
734
|
: null;
|
|
649
735
|
if (controlChecks) return controlChecks[1] + " implementation checks remain. Open the Control to review them.";
|
|
736
|
+
if (/^Review .+ before this page can be ready\.$/i.test(item.message || "")) return "Confirm the current scope.";
|
|
650
737
|
return item.message || workflowItemDetail(item);
|
|
651
738
|
}
|
|
652
739
|
|
|
@@ -740,7 +827,7 @@ function stagePageItems(stage, destination) {
|
|
|
740
827
|
|
|
741
828
|
function operationProgress() {
|
|
742
829
|
const program = state.programReadiness;
|
|
743
|
-
const goal = program?.target?.goal ||
|
|
830
|
+
const goal = program?.target?.goal || activeProgram().assuranceGoal || "none";
|
|
744
831
|
const asOf = program?.asOf || currentDate();
|
|
745
832
|
const candidateStarted = goal === "soc-2-type-2"
|
|
746
833
|
? Boolean(program?.target?.candidateCoverage?.kind === "range" && program.target.candidateCoverage.startsOn <= asOf)
|
|
@@ -800,15 +887,6 @@ function operationProgress() {
|
|
|
800
887
|
};
|
|
801
888
|
}
|
|
802
889
|
|
|
803
|
-
function programPathProgress() {
|
|
804
|
-
const progress = READINESS_STAGES.map((stage) => stageProgress(stage));
|
|
805
|
-
return progressFromCounts(
|
|
806
|
-
progress.reduce((sum, current) => sum + current.complete, 0),
|
|
807
|
-
progress.reduce((sum, current) => sum + current.total, 0),
|
|
808
|
-
"program milestone"
|
|
809
|
-
);
|
|
810
|
-
}
|
|
811
|
-
|
|
812
890
|
function progressFromCounts(complete, total, noun) {
|
|
813
891
|
if (!total) return { percent: 0, complete: 0, total: 0, status: "Nothing to review", tone: "neutral", detail: "No " + pluralize(noun, 2) + " are configured yet." };
|
|
814
892
|
const percent = Math.round((complete / total) * 100);
|
|
@@ -830,7 +908,7 @@ function sectionDestinations(section) {
|
|
|
830
908
|
destinations.push({ type, kind: "Record page", label: titleCase(definition.pluralTitle), href: "#/resources/" + encodeURIComponent(type), description: definition.description });
|
|
831
909
|
}
|
|
832
910
|
if (section.utility === "obligation-board") destinations.push({ utility: section.utility, kind: "Working page", label: "Work Queue", href: "#/stage/run", description: "Complete recurring work, Policy Event tasks, and assigned follow-up with its due windows and linked proof." });
|
|
833
|
-
if (section.utility === "audit-packet") destinations.push({ utility: section.utility, kind: "Working page", label: "Audit Evidence & Packet", href: "#/audit-packet", description: "Review filegrc and
|
|
911
|
+
if (section.utility === "audit-packet") destinations.push({ utility: section.utility, kind: "Working page", label: "Audit Evidence & Packet", href: "#/audit-packet", description: "Review filegrc Evidence and Evidence Artifacts, prepare fieldwork, and build the indexed packet." });
|
|
834
912
|
return destinations;
|
|
835
913
|
}
|
|
836
914
|
|
|
@@ -850,10 +928,10 @@ function renderExternalEvidenceSection() {
|
|
|
850
928
|
)).join("");
|
|
851
929
|
const createButton = state.readOnly
|
|
852
930
|
? ""
|
|
853
|
-
: '<button class="button primary" type="button" data-new-external-evidence>New
|
|
854
|
-
return '<section class="workflow-section external-evidence-section"><div class="section-head"><div><p class="kicker">Fixed artifacts and approved references</p><h2>
|
|
931
|
+
: '<button class="button primary" type="button" data-new-external-evidence>New Evidence Artifact</button>';
|
|
932
|
+
return '<section class="workflow-section external-evidence-section"><div class="section-head"><div><p class="kicker">Fixed artifacts and approved references</p><h2>Evidence Artifacts</h2><p>Add fixed artifacts only when they exist. Link each one to its source Component and the work it supports.</p></div><div class="page-actions">' +
|
|
855
933
|
createButton + '<a class="button" href="#/resources/evidence">View all</a></div></div><div class="external-evidence-list">' +
|
|
856
|
-
(recent || empty("No
|
|
934
|
+
(recent || empty("No Evidence Artifacts have been collected yet. Create one during operation only when a real artifact or approved external reference exists.")) +
|
|
857
935
|
'</div></section>';
|
|
858
936
|
}
|
|
859
937
|
|
|
@@ -1002,6 +1080,12 @@ function coverageEnd(coverage) {
|
|
|
1002
1080
|
}
|
|
1003
1081
|
|
|
1004
1082
|
function defaultClassificationId() {
|
|
1083
|
+
const classifications = resourcesOfType("classification")
|
|
1084
|
+
.filter(({ record }) => record.status === "active")
|
|
1085
|
+
.map(({ record }) => record.id);
|
|
1086
|
+
if (classifications.length) {
|
|
1087
|
+
return classifications.includes("internal") ? "internal" : classifications[0];
|
|
1088
|
+
}
|
|
1005
1089
|
const definitions = state.workspace.classificationDefinitions || {};
|
|
1006
1090
|
return Object.hasOwn(definitions, "internal") ? "internal" : Object.keys(definitions)[0] || "";
|
|
1007
1091
|
}
|
|
@@ -1064,7 +1148,7 @@ function obligationCompletionPlan(item) {
|
|
|
1064
1148
|
if (!currentPeopleForParties(item.ownerIds || []).length) {
|
|
1065
1149
|
return { type, blocked: "Assign current owner", href: "#/resource/obligation/" + encodeURIComponent(item.obligationId) };
|
|
1066
1150
|
}
|
|
1067
|
-
if (["access-review", "backup-test"].includes(type) && !(
|
|
1151
|
+
if (["access-review", "backup-test"].includes(type) && !(activeProgram().systemIds || []).some((id) => state.resources.some(({ record }) => record.id === id && record.status !== "retired"))) {
|
|
1068
1152
|
return { type, blocked: "Add system first", href: "#/resources/system?new=1" };
|
|
1069
1153
|
}
|
|
1070
1154
|
if (type === "vendor-review" && !resourcesOfType("vendor").some(({ record }) => record.status !== "terminated")) {
|
|
@@ -1116,7 +1200,7 @@ function obligationCompletionSeed(type, item, obligation) {
|
|
|
1116
1200
|
.filter((record) => record.status === "active" && !responsiblePeople.includes(record.id))
|
|
1117
1201
|
.map(({ id }) => id);
|
|
1118
1202
|
const reviewerPeople = independentPeople.length ? [independentPeople[0]] : [];
|
|
1119
|
-
const inScopeSystems = resourcesOfType("system").filter(({ record }) => (
|
|
1203
|
+
const inScopeSystems = resourcesOfType("system").filter(({ record }) => (activeProgram().systemIds || []).includes(record.id) && record.status !== "retired").map(({ record }) => record.id);
|
|
1120
1204
|
const activeVendors = resourcesOfType("vendor").filter(({ record }) => record.status !== "terminated").map(({ record }) => record.id);
|
|
1121
1205
|
const title = item.title + " · " + formatCalendarDate(item.dueWindowStart);
|
|
1122
1206
|
const common = { title };
|
|
@@ -1379,8 +1463,11 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
1379
1463
|
const definition = state.model.resources[type];
|
|
1380
1464
|
const reviewPoints = definition?.guidance?.reviewPoints || [];
|
|
1381
1465
|
const people = resourcesOfType("person").filter(({ record }) => record.status === "active");
|
|
1466
|
+
const reviewedRequirements = reviewedRequirementIds();
|
|
1382
1467
|
const pending = entries.filter(({ record }) => (
|
|
1383
|
-
|
|
1468
|
+
type === "requirement" && String(state.model.modelVersion) === "4"
|
|
1469
|
+
? !reviewedRequirements.has(record.id)
|
|
1470
|
+
: !record.applicabilityReview
|
|
1384
1471
|
|| type === "requirement" && record.applicability === "undetermined"
|
|
1385
1472
|
));
|
|
1386
1473
|
const dialog = document.createElement("dialog");
|
|
@@ -1393,10 +1480,12 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
1393
1480
|
const reviewChecks = reviewPoints.length
|
|
1394
1481
|
? '<section class="collection-review-checks"><strong>Before deciding</strong><ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></section>'
|
|
1395
1482
|
: "";
|
|
1396
|
-
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Batch review</p><h2 id="applicability-dialog-title">Review ' + esc(definition.pluralTitle) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>Record only decisions reviewed against the current service scope. Leave an item at Review later when management has not decided it.</p>' + reviewChecks + '<div class="form-grid review-context"><label><span>Reviewer</span><select name="reviewerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '">' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Reviewed on</span><input name="reviewedOn" type="date" required value="' + esc(currentDate()) + '"></label></div><div class="applicability-rows">' + (rows || empty("Every record already has a reviewed applicability decision.")) + '</div><div class="workflow-preview" role="status"></div><div class="dialog-error" role="alert"></div><div class="dialog-actions"><button type="button" class="button" data-event="cancel">Cancel</button><button type="submit" class="button primary" data-preview-review ' + (pending.length ? "" : "disabled") + '>Preview decisions</button></div></form>';
|
|
1483
|
+
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">Batch review</p><h2 id="applicability-dialog-title">Review ' + esc(definition.pluralTitle) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>Record only decisions reviewed against the current service scope. Leave an item at Review later when management has not decided it.</p>' + reviewChecks + '<div class="form-grid review-context"><label><span>Reviewer</span><select name="reviewerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '">' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Reviewed on</span><input name="reviewedOn" type="date" required value="' + esc(currentDate()) + '"></label></div><div class="applicability-rows">' + (rows || empty("Every record already has a reviewed applicability decision.")) + '</div><div class="workflow-preview" role="status"></div><div class="dialog-error" role="alert"></div><div class="dialog-actions"><span class="save-status review-save-status" role="status" aria-live="polite"></span><button type="button" class="button" data-event="cancel">Cancel</button><button type="submit" class="button primary" data-preview-review ' + (pending.length ? "" : "disabled") + '>Preview decisions</button></div></form>';
|
|
1397
1484
|
document.body.append(dialog);
|
|
1398
1485
|
dialog.showModal();
|
|
1399
1486
|
const form = dialog.querySelector("form");
|
|
1487
|
+
const saveStatus = dialog.querySelector(".review-save-status");
|
|
1488
|
+
const repositoryPrefetch = prefetchRepositoryForReview(saveStatus);
|
|
1400
1489
|
dialog.querySelector(".icon-button").addEventListener("click", () => dialog.close());
|
|
1401
1490
|
dialog.querySelector('[data-event="cancel"]').addEventListener("click", () => dialog.close());
|
|
1402
1491
|
dialog.addEventListener("close", () => dialog.remove());
|
|
@@ -1431,11 +1520,17 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
1431
1520
|
error.textContent = "Select at least one decision.";
|
|
1432
1521
|
return;
|
|
1433
1522
|
}
|
|
1523
|
+
const decisionIds = new Set(decisions.map(({ id }) => id));
|
|
1434
1524
|
const payload = {
|
|
1435
1525
|
decisions,
|
|
1436
1526
|
reviewedByIds: [form.elements.reviewerId.value],
|
|
1437
1527
|
reviewedOn: form.elements.reviewedOn.value,
|
|
1438
|
-
expectedRevisions: Object.fromEntries(
|
|
1528
|
+
expectedRevisions: Object.fromEntries([
|
|
1529
|
+
...entries.filter((entry) => decisionIds.has(entry.record.id)).map((entry) => [entry.record.id, entry.revision]),
|
|
1530
|
+
...(type === "requirement" && String(state.model.modelVersion) === "4"
|
|
1531
|
+
? state.resources.filter(({ record }) => record.id === activeProgram().id).map((entry) => [entry.record.id, entry.revision])
|
|
1532
|
+
: [])
|
|
1533
|
+
])
|
|
1439
1534
|
};
|
|
1440
1535
|
form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = true; });
|
|
1441
1536
|
try {
|
|
@@ -1448,15 +1543,18 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
1448
1543
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
1449
1544
|
const preview = await response.json();
|
|
1450
1545
|
previewedPayload = payload;
|
|
1451
|
-
dialog.querySelector(".workflow-preview").innerHTML = '<strong>Review before saving</strong><p>' + preview.reviewedIds.length + '
|
|
1546
|
+
dialog.querySelector(".workflow-preview").innerHTML = '<strong>Review before saving</strong><p>' + preview.reviewedIds.length + ' ' + pluralize("decision", preview.reviewedIds.length) + ' will be saved with the reviewer, review date, and current scope recorded automatically.</p>';
|
|
1452
1547
|
dialog.querySelector("[data-preview-review]").textContent = "Confirm and save";
|
|
1453
1548
|
} else {
|
|
1549
|
+
saveStatus.textContent = "Validating and saving…";
|
|
1550
|
+
const prefetch = await repositoryPrefetch;
|
|
1454
1551
|
const response = await localFetch("/api/applicability-review", {
|
|
1455
1552
|
method: "POST",
|
|
1456
|
-
headers: { "content-type": "application/json" },
|
|
1457
|
-
body: JSON.stringify({ ...previewedPayload, confirmed: true })
|
|
1553
|
+
headers: { "content-type": "application/json", prefer: "respond-async" },
|
|
1554
|
+
body: JSON.stringify({ ...previewedPayload, confirmed: true, prefetchToken: prefetch?.token })
|
|
1458
1555
|
});
|
|
1459
1556
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
1557
|
+
saveStatus.textContent = "Saved locally. Refreshing page…";
|
|
1460
1558
|
applyMutationState(await response.json());
|
|
1461
1559
|
dialog.close();
|
|
1462
1560
|
render();
|
|
@@ -1491,10 +1589,10 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
1491
1589
|
["Repository", state.git.available ? state.git.clean ? "Clean revision" : state.git.changes.length + " uncommitted" : "Git unavailable", "#/repository", state.git.clean ? "good" : "warn"],
|
|
1492
1590
|
["Engagement", selected ? selected.title : "No audit record", "#/resources/audit", selected ? "good" : "warn"],
|
|
1493
1591
|
["filegrc Evidence", filegrcRecords.length + " operating " + pluralize("record", filegrcRecords.length), "#/stage/run", "neutral"],
|
|
1494
|
-
["
|
|
1592
|
+
["Evidence Artifacts", evidence.length + " " + pluralize("record", evidence.length), "#/resources/evidence", evidence.length ? "good" : "neutral"],
|
|
1495
1593
|
["Policy work", state.obligations.counts.overdue ? state.obligations.counts.overdue + " overdue" : state.obligations.counts.blocked ? state.obligations.counts.blocked + " blocked" : state.obligations.counts.due ? state.obligations.counts.due + " due" : state.obligations.counts.proposed ? state.obligations.counts.proposed + " proposals" : "No work due", "#/stage/run", state.obligations.counts.overdue || state.obligations.counts.blocked ? "bad" : state.obligations.counts.due ? "warn" : state.obligations.counts.proposed ? "neutral" : "good"]
|
|
1496
1594
|
];
|
|
1497
|
-
const evidencePaths = '<section class="panel audit-evidence-paths"><div class="panel-head"><div><p class="kicker">Evidence workflow</p><h3>Review both evidence paths</h3><p>Use the formal audit date or period. Each selected control may need one or both paths.</p></div></div><div class="audit-evidence-path-grid"><a href="#/stage/run"><span class="step-label">filegrc Evidence</span><h4>Review operating records</h4><p>Confirm the applicable Step 4 records are complete and linked to their Controls.</p></a><a href="#/resources/evidence"><span class="step-label">
|
|
1595
|
+
const evidencePaths = '<section class="panel audit-evidence-paths"><div class="panel-head"><div><p class="kicker">Evidence workflow</p><h3>Review both evidence paths</h3><p>Use the formal audit date or period. Each selected control may need one or both paths.</p></div></div><div class="audit-evidence-path-grid"><a href="#/stage/run"><span class="step-label">filegrc Evidence</span><h4>Review operating records</h4><p>Confirm the applicable Step 4 records are complete and linked to their Controls.</p></a><a href="#/resources/evidence"><span class="step-label">Evidence Artifacts</span><h4>Review imported or referenced proof</h4><p>Confirm each artifact is fixed, verified, and linked to its source Component and Controls.</p></a></div></section>';
|
|
1498
1596
|
const dateFields = typeOne
|
|
1499
1597
|
? '<label><span>As-of date</span><input type="date" name="start" required value="' + esc(start) + '"></label>'
|
|
1500
1598
|
: '<label><span>Period start</span><input type="date" name="start" required value="' + esc(start) + '"></label><label><span>Period end</span><input type="date" name="end" required value="' + esc(end) + '"></label>';
|
|
@@ -1595,10 +1693,10 @@ function renderPacketResults(container, result) {
|
|
|
1595
1693
|
container.innerHTML = '<section class="metrics packet-metrics">' +
|
|
1596
1694
|
metric("filegrc Evidence", packet.summary.filegrcRecords, packet.summary.records + " total packet records", "neutral") +
|
|
1597
1695
|
metric("Obligations", packet.summary.obligationOccurrences, packet.summary.eventRuns + " event workflows", "neutral") +
|
|
1598
|
-
metric("
|
|
1696
|
+
metric("Evidence Artifacts", packet.summary.evidence, packet.summary.policies + " policies · " + packet.summary.controls + " controls", "neutral") +
|
|
1599
1697
|
metric("Review items", packet.summary.gaps, packet.summary.errors + " errors · " + packet.summary.warnings + " warnings", packet.summary.errors ? "bad" : packet.summary.warnings ? "warn" : "good") +
|
|
1600
1698
|
'</section><section class="panel packet-output"><div class="panel-head"><div><p class="kicker">' + (ready ? "filegrc management checks passed" : "Draft packet") + '</p><h3>' + esc(result.output) + '</h3></div>' + (result.packetUrl ? '<a class="button primary" href="' + esc(result.packetUrl) + '" target="_blank" rel="noreferrer">Open index</a>' : "") + '</div><p>The directory contains ' + result.files.length + ' files. ' + (ready ? "Verify the checksums, reconcile external deliveries, and let the engagement team confirm evidence sufficiency." : "Do not deliver it until every error is resolved and each warning has been reviewed.") + '</p></section>' +
|
|
1601
|
-
'<div class="dashboard-grid"><section class="panel span-2"><div class="panel-head"><h3>Coverage Gaps and Warnings</h3></div>' + (packet.gaps.length ? '<div class="packet-gaps">' + packet.gaps.map((gap) => '<div><span class="badge ' + (gap.severity === "error" ? "bad" : "warn") + '">' + esc(properCase(gap.severity)) + '</span><p>' + esc(gap.message) + '</p></div>').join("") + '</div>' : empty("No packet gaps were detected.")) + '</section><section class="panel"><div class="panel-head"><h3>Included filegrc Evidence</h3></div>' + (packet.filegrcRecords.length ? '<div class="packet-list">' + packet.filegrcRecords.slice(0, 12).map((item) => '<a href="#/resource/' + encodeURIComponent(item.type) + '/' + encodeURIComponent(item.id) + '"><strong>' + esc(item.title) + '</strong><small>' + esc(properCase(item.type)) + ' · ' + esc(item.primaryDate) + '</small></a>').join("") + '</div>' : empty("No filegrc Evidence matched.")) + '</section><section class="panel"><div class="panel-head"><h3>Included
|
|
1699
|
+
'<div class="dashboard-grid"><section class="panel span-2"><div class="panel-head"><h3>Coverage Gaps and Warnings</h3></div>' + (packet.gaps.length ? '<div class="packet-gaps">' + packet.gaps.map((gap) => '<div><span class="badge ' + (gap.severity === "error" ? "bad" : "warn") + '">' + esc(properCase(gap.severity)) + '</span><p>' + esc(gap.message) + '</p></div>').join("") + '</div>' : empty("No packet gaps were detected.")) + '</section><section class="panel"><div class="panel-head"><h3>Included filegrc Evidence</h3></div>' + (packet.filegrcRecords.length ? '<div class="packet-list">' + packet.filegrcRecords.slice(0, 12).map((item) => '<a href="#/resource/' + encodeURIComponent(item.type) + '/' + encodeURIComponent(item.id) + '"><strong>' + esc(item.title) + '</strong><small>' + esc(properCase(item.type)) + ' · ' + esc(item.primaryDate) + '</small></a>').join("") + '</div>' : empty("No filegrc Evidence matched.")) + '</section><section class="panel"><div class="panel-head"><h3>Included Evidence Artifacts</h3></div>' + (packet.evidence.length ? '<div class="packet-list">' + packet.evidence.slice(0, 12).map((item) => '<a href="#/resource/evidence/' + encodeURIComponent(item.id) + '"><strong>' + esc(item.title) + '</strong><small>' + esc(properCase(item.status)) + ' · ' + esc(properCase(item.artifactKind)) + '</small></a>').join("") + '</div>' : empty("No Evidence Artifacts matched.")) + '</section></div>';
|
|
1602
1700
|
}
|
|
1603
1701
|
|
|
1604
1702
|
function obligationPreview(items) {
|
|
@@ -1692,10 +1790,12 @@ function renderList(main, type, params = new URLSearchParams()) {
|
|
|
1692
1790
|
const values = [...new Set(observed)].sort();
|
|
1693
1791
|
return { name, label: field.label || humanize(name), values };
|
|
1694
1792
|
}).filter(({ values }) => values.length > 1);
|
|
1695
|
-
const createButton = !state.readOnly && !definition.singleton ? '<button class="button primary" id="new-resource">New ' + esc(definition.title.toLowerCase()) + '</button>' : "";
|
|
1793
|
+
const createButton = !state.readOnly && !definition.singleton && !collectionNeedsFirstRecord(type) && resourceCreationAllowed(type) ? '<button class="button primary" id="new-resource">New ' + esc(definition.title.toLowerCase()) + '</button>' : "";
|
|
1794
|
+
const reviewedRequirements = reviewedRequirementIds();
|
|
1696
1795
|
const hasPendingApplicability = entries.some(({ record }) => (
|
|
1697
|
-
|
|
1698
|
-
|
|
1796
|
+
type === "requirement" && String(state.model.modelVersion) === "4"
|
|
1797
|
+
? !reviewedRequirements.has(record.id)
|
|
1798
|
+
: !record.applicabilityReview || type === "requirement" && record.applicability === "undetermined"
|
|
1699
1799
|
));
|
|
1700
1800
|
const applicabilityButton = !state.readOnly
|
|
1701
1801
|
&& ["requirement", "control", "commitment", "complementary-control"].includes(type)
|
|
@@ -1723,7 +1823,7 @@ function renderList(main, type, params = new URLSearchParams()) {
|
|
|
1723
1823
|
const start = (pageNumber - 1) * LIST_PAGE_SIZE;
|
|
1724
1824
|
const visible = filtered.slice(start, start + LIST_PAGE_SIZE);
|
|
1725
1825
|
main.querySelector("#result-count").textContent = filtered.length + (filtered.length === 1 ? " record" : " records");
|
|
1726
|
-
main.querySelector("#record-rows").innerHTML = filtered.length ? visible.map((entry) => '<tr><td data-label="' + esc(fieldLabel(type, "title")) + '" data-primary-field><a class="record-title" href="#/resource/' + encodeURIComponent(type) + '/' + encodeURIComponent(entry.record.id) + '">' + esc(entry.record.title) + '</a></td>' + fields.map((name) => '<td data-label="' + esc(fieldLabel(type, name)) + '">' + (name === "$operationTracking" ? controlOperationTracking(entry.record) : name === "$workQueueStatus" ? obligationWorkQueueStatus(entry.record) : formatValue(name === "status" ? displayStatus(entry.record) : entry.record[name], name, type)) + '</td>').join("") + '<td data-label="Next action">' + recordWorkflowCell(type, entry) + '</td><td data-label="Git file"><code>' + esc(entry.relativePath.replace(/^data\//, "")) + '</code></td></tr>').join("") : '<tr><td colspan="' + (fields.length + 3) + '">' + empty(entries.length ? "No records match this filter." :
|
|
1826
|
+
main.querySelector("#record-rows").innerHTML = filtered.length ? visible.map((entry) => '<tr><td data-label="' + esc(fieldLabel(type, "title")) + '" data-primary-field><a class="record-title" href="#/resource/' + encodeURIComponent(type) + '/' + encodeURIComponent(entry.record.id) + '">' + esc(entry.record.title) + '</a></td>' + fields.map((name) => '<td data-label="' + esc(fieldLabel(type, name)) + '">' + (name === "$operationTracking" ? controlOperationTracking(entry.record) : name === "$workQueueStatus" ? obligationWorkQueueStatus(entry.record) : formatValue(name === "status" ? displayStatus(entry.record) : entry.record[name], name, type, true)) + '</td>').join("") + '<td data-label="Next action">' + recordWorkflowCell(type, entry) + '</td><td data-label="Git file"><code>' + esc(entry.relativePath.replace(/^data\//, "")) + '</code></td></tr>').join("") : '<tr><td colspan="' + (fields.length + 3) + '">' + empty(entries.length ? "No records match this filter." : collectionEmptyState(type, definition)) + '</td></tr>';
|
|
1727
1827
|
pagination.hidden = totalPages === 1;
|
|
1728
1828
|
previous.disabled = pageNumber === 1;
|
|
1729
1829
|
next.disabled = pageNumber === totalPages;
|
|
@@ -1765,7 +1865,7 @@ function renderList(main, type, params = new URLSearchParams()) {
|
|
|
1765
1865
|
main.querySelector("#new-resource")?.addEventListener("click", () => openEditor(type));
|
|
1766
1866
|
main.querySelector("#review-applicability")?.addEventListener("click", () => openApplicabilityReviewDialog(type, entries));
|
|
1767
1867
|
main.querySelector("[data-review-collection]")?.addEventListener("click", () => openCollectionReviewDialog(type));
|
|
1768
|
-
if (params.get("new") === "1" && !state.readOnly && !definition.singleton) queueMicrotask(() => openEditor(type));
|
|
1868
|
+
if (params.get("new") === "1" && !state.readOnly && !definition.singleton && resourceCreationAllowed(type)) queueMicrotask(() => openEditor(type));
|
|
1769
1869
|
if (params.get("review") === "1" && !state.readOnly) {
|
|
1770
1870
|
queueMicrotask(() => main.querySelector("#review-applicability")?.click());
|
|
1771
1871
|
}
|
|
@@ -1871,10 +1971,16 @@ function renderDetail(main, type, id) {
|
|
|
1871
1971
|
main.querySelector("[data-evidence-file]")?.click();
|
|
1872
1972
|
});
|
|
1873
1973
|
main.querySelector("[data-evidence-file]")?.addEventListener("change", async (event) => {
|
|
1874
|
-
const
|
|
1974
|
+
const input = event.currentTarget;
|
|
1975
|
+
const file = input.files?.[0];
|
|
1875
1976
|
if (!file) return;
|
|
1876
|
-
if (!
|
|
1877
|
-
|
|
1977
|
+
if (!await confirmAction({
|
|
1978
|
+
kicker: "Attach evidence",
|
|
1979
|
+
title: file.name,
|
|
1980
|
+
message: "Confirm its classification, retention, and repository access are appropriate.",
|
|
1981
|
+
confirmLabel: "Attach"
|
|
1982
|
+
})) {
|
|
1983
|
+
input.value = "";
|
|
1878
1984
|
return;
|
|
1879
1985
|
}
|
|
1880
1986
|
try {
|
|
@@ -1887,12 +1993,19 @@ function renderDetail(main, type, id) {
|
|
|
1887
1993
|
applyMutationState(await response.json());
|
|
1888
1994
|
render();
|
|
1889
1995
|
} catch (error) {
|
|
1996
|
+
input.value = "";
|
|
1890
1997
|
showError(error.message);
|
|
1891
1998
|
}
|
|
1892
1999
|
});
|
|
1893
2000
|
main.querySelectorAll("[data-detach-evidence]").forEach((button) => button.addEventListener("click", async () => {
|
|
1894
2001
|
const attachment = button.dataset.detachEvidence;
|
|
1895
|
-
if (!
|
|
2002
|
+
if (!await confirmAction({
|
|
2003
|
+
kicker: "Remove attachment",
|
|
2004
|
+
title: attachment,
|
|
2005
|
+
message: "Deletes this local file from the Evidence Artifact.",
|
|
2006
|
+
confirmLabel: "Remove",
|
|
2007
|
+
danger: true
|
|
2008
|
+
})) return;
|
|
1896
2009
|
try {
|
|
1897
2010
|
const response = await localFetch(
|
|
1898
2011
|
"/api/evidence-attachments/" + encodeURIComponent(entry.record.id) + "/" + encodeURIComponent(attachment)
|
|
@@ -1909,7 +2022,13 @@ function renderDetail(main, type, id) {
|
|
|
1909
2022
|
main.querySelector("#add-record-content")?.addEventListener("click", () => openEditor(type, entry, { addRecordContent: true }));
|
|
1910
2023
|
main.querySelectorAll("[data-edit-content]").forEach((button) => button.addEventListener("click", () => openContentEditor(entry, button.dataset.editContent)));
|
|
1911
2024
|
main.querySelector("#delete-resource")?.addEventListener("click", async () => {
|
|
1912
|
-
if (!
|
|
2025
|
+
if (!await confirmAction({
|
|
2026
|
+
kicker: "Delete record",
|
|
2027
|
+
title: entry.record.title,
|
|
2028
|
+
message: "Deletes this record and its Markdown. Use this only for mistakes and uncommitted drafts.",
|
|
2029
|
+
confirmLabel: "Delete",
|
|
2030
|
+
danger: true
|
|
2031
|
+
})) return;
|
|
1913
2032
|
try {
|
|
1914
2033
|
const response = await localFetch("/api/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(id) + "?revision=" + encodeURIComponent(entry.revision), { method: "DELETE" });
|
|
1915
2034
|
if (!response.ok) return showError(await responseMessage(response));
|
|
@@ -2175,13 +2294,10 @@ function renderTrunkRepository(main) {
|
|
|
2175
2294
|
const lastSync = repository.lastSuccessfulSynchronization
|
|
2176
2295
|
? formatLocalDateTime(repository.lastSuccessfulSynchronization)
|
|
2177
2296
|
: "No successful sync recorded by this server";
|
|
2178
|
-
const override = repository.developmentOverride
|
|
2179
|
-
? '<div class="repository-override"><span class="status-dot warn"></span><div><strong>Development write override active</strong><p>Browser writes stay local. FileGRC will not fetch, commit, or push while this server uses <code>--allow-non-authoritative-writes</code>.</p></div></div>'
|
|
2180
|
-
: "";
|
|
2181
2297
|
const validationBody = state.validation.diagnostics.length
|
|
2182
2298
|
? '<div class="diagnostics">' + state.validation.diagnostics.map((item) => '<div><span class="badge ' + item.severity + '">' + esc(properCase(item.severity)) + '</span><code>' + esc(item.path) + '</code><p>' + esc(item.message) + '</p></div>').join("") + '</div>'
|
|
2183
2299
|
: empty("No validation problems.");
|
|
2184
|
-
main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">Audit trail</p><h2>Repository State</h2><p>Browser saves use one authoritative branch. Record status represents draft, proposal, approval, and retirement; Git branches do not.</p><p class="repository-sync-status" role="status" aria-live="polite"></p></div><div class="page-actions">' + retryButton + onboardingButton + settingsLink + '<a class="button" href="#/resource/workspace/workspace">Workspace settings</a></div></div
|
|
2300
|
+
main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">Audit trail</p><h2>Repository State</h2><p>Browser saves use one authoritative branch. Record status represents draft, proposal, approval, and retirement; Git branches do not.</p><p class="repository-sync-status" role="status" aria-live="polite"></p></div><div class="page-actions">' + retryButton + onboardingButton + settingsLink + '<a class="button" href="#/resource/workspace/workspace">Workspace settings</a></div></div><section class="panel repository-state-banner"><span class="status-dot ' + repositoryStatusTone(repository.status) + '"></span><div><p class="kicker">Repository status</p><h3>' + esc(repository.label) + '</h3><p>' + esc(repository.message) + '</p></div></section><div class="dashboard-grid"><section class="panel"><div class="panel-head"><h3>Configured Repository</h3></div><dl class="metadata"><div><dt>Branch</dt><dd>' + esc(repository.authoritativeBranch) + '</dd></div><div><dt>Remote</dt><dd>' + esc(repository.remote) + '</dd></div><div><dt>Checkout</dt><dd>' + esc(state.git.branch || (state.git.available ? "Detached HEAD" : "Unavailable")) + '</dd></div><div><dt>Upstream</dt><dd>' + esc(repository.upstream || "Not configured") + '</dd></div></dl></section><section class="panel"><div class="panel-head"><h3>Synchronization</h3></div><dl class="metadata"><div><dt>Current commit</dt><dd><code>' + esc(repository.currentCommit || "Unavailable") + '</code></dd></div><div><dt>Upstream commit</dt><dd><code>' + esc(repository.upstreamCommit || "Unavailable") + '</code></dd></div><div><dt>Ahead</dt><dd>' + esc(repository.ahead ?? "Unknown") + '</dd></div><div><dt>Behind</dt><dd>' + esc(repository.behind ?? "Unknown") + '</dd></div><div><dt>Last sync</dt><dd>' + esc(lastSync) + '</dd></div></dl></section><section class="panel"><div class="panel-head"><h3>Safety Checks</h3></div><dl class="metadata"><div><dt>Whole worktree</dt><dd>' + (repository.wholeWorktreeClean === null ? "Unavailable" : repository.wholeWorktreeClean ? "Clean" : "Has changes") + '</dd></div><div><dt>Git operation</dt><dd>' + esc(repository.operationInProgress || "None") + '</dd></div><div><dt>Pending scope</dt><dd>' + (repository.pendingCommitsFilegrcOnly === false ? "Includes external files" : repository.pendingCommits?.length ? "FileGRC only" : "None") + '</dd></div></dl></section><section class="panel span-2"><div class="panel-head"><h3>Pending FileGRC-only Commits</h3></div>' + pending + '</section><section class="panel span-2"><div class="panel-head"><h3>Validation</h3><span class="badge ' + (state.validation.ok ? "good" : "bad") + '">' + (state.validation.ok ? "Passing" : "Needs attention") + '</span></div>' + validationBody + '</section></div></div>';
|
|
2185
2301
|
main.querySelectorAll("[data-git-action]").forEach((button) => button.addEventListener("click", () => runRepositoryGitAction(button.dataset.gitAction)));
|
|
2186
2302
|
main.querySelector("#start-onboarding")?.addEventListener("click", requestOnboarding);
|
|
2187
2303
|
}
|
|
@@ -2369,13 +2485,14 @@ function rendererSettingsEntry() {
|
|
|
2369
2485
|
return state.resources.find(({ record }) => record.type === "renderer-settings");
|
|
2370
2486
|
}
|
|
2371
2487
|
|
|
2372
|
-
function requestOnboarding() {
|
|
2488
|
+
function requestOnboarding({ setupOnly = false } = {}) {
|
|
2373
2489
|
if (state.readOnly || onboardingDialog || !rendererSettingsEntry()) return;
|
|
2374
2490
|
if (parseRoute().name !== "home") {
|
|
2375
2491
|
history.replaceState(null, "", "#/");
|
|
2376
2492
|
render();
|
|
2377
2493
|
}
|
|
2378
|
-
onboardingStep = 0;
|
|
2494
|
+
onboardingStep = setupOnly ? onboardingSteps().length - 1 : 0;
|
|
2495
|
+
onboardingSetupOnly = setupOnly;
|
|
2379
2496
|
onboardingDraft = initialOnboardingDraft();
|
|
2380
2497
|
onboardingBusy = false;
|
|
2381
2498
|
onboardingShade = document.createElement("div");
|
|
@@ -2400,23 +2517,25 @@ function requestOnboarding() {
|
|
|
2400
2517
|
onboardingDialog = null;
|
|
2401
2518
|
onboardingDraft = null;
|
|
2402
2519
|
onboardingBusy = false;
|
|
2520
|
+
onboardingSetupOnly = false;
|
|
2403
2521
|
onboardingPendingDraft = false;
|
|
2404
2522
|
});
|
|
2405
2523
|
renderOnboardingStep();
|
|
2406
2524
|
}
|
|
2407
2525
|
|
|
2408
2526
|
function initialOnboardingDraft() {
|
|
2409
|
-
const
|
|
2527
|
+
const program = activeProgram();
|
|
2528
|
+
const system = initialSetupSystem();
|
|
2410
2529
|
const owner = resourcesOfType("person").find(({ record }) => record.status === "active")?.record;
|
|
2411
2530
|
return {
|
|
2412
|
-
systemId:
|
|
2413
|
-
serviceName:
|
|
2414
|
-
scope:
|
|
2415
|
-
ownerId:
|
|
2416
|
-
criticality:
|
|
2417
|
-
classificationId:
|
|
2418
|
-
internetExposed:
|
|
2419
|
-
programGoal: programGoalFromKind(
|
|
2531
|
+
systemId: system?.id || "",
|
|
2532
|
+
serviceName: system?.title || "",
|
|
2533
|
+
scope: system?.boundary || system?.description || "",
|
|
2534
|
+
ownerId: system?.ownerIds?.[0] || owner?.id || "",
|
|
2535
|
+
criticality: system?.criticality || "high",
|
|
2536
|
+
classificationId: system?.classificationId || defaultClassificationId(),
|
|
2537
|
+
internetExposed: system?.internetExposed === false ? "false" : "true",
|
|
2538
|
+
programGoal: programGoalFromKind(program.assuranceGoal)
|
|
2420
2539
|
};
|
|
2421
2540
|
}
|
|
2422
2541
|
|
|
@@ -2425,60 +2544,51 @@ function onboardingSteps() {
|
|
|
2425
2544
|
target: ".repo-chip",
|
|
2426
2545
|
kicker: "Mental model",
|
|
2427
2546
|
title: "Files are the program",
|
|
2428
|
-
body: "
|
|
2547
|
+
body: "Source files live under data/. The UI, CLI, and direct edits change the same files. Git records the history.",
|
|
2429
2548
|
points: [
|
|
2430
|
-
"
|
|
2431
|
-
"
|
|
2432
|
-
"
|
|
2549
|
+
"JSON holds fields. Markdown holds long-form content.",
|
|
2550
|
+
"Record status tracks approval.",
|
|
2551
|
+
"Trunk-mode browser saves validate, commit, and push."
|
|
2433
2552
|
]
|
|
2434
2553
|
};
|
|
2435
2554
|
const path = {
|
|
2436
2555
|
target: ".readiness-map",
|
|
2437
2556
|
kicker: "Program model",
|
|
2438
2557
|
title: "Follow the audit chain",
|
|
2439
|
-
body: "Follow
|
|
2440
|
-
points: [
|
|
2441
|
-
"Define the people, criteria, service, Systems, and providers in scope.",
|
|
2442
|
-
"Approve the policies, implement the controls, then operate them and retain dated proof.",
|
|
2443
|
-
"Create the audit engagement when a CPA firm is involved or a real customer deadline requires it."
|
|
2444
|
-
]
|
|
2558
|
+
body: "Follow five steps. Each page shows the next decision.",
|
|
2445
2559
|
};
|
|
2446
2560
|
const operation = {
|
|
2447
2561
|
target: ".obligation-panel",
|
|
2448
2562
|
kicker: "Program operation",
|
|
2449
|
-
title: "
|
|
2450
|
-
body: "
|
|
2563
|
+
title: "Run work and record changes",
|
|
2564
|
+
body: "Work Queue holds scheduled and assigned tasks. Policy Events add tasks when a listed change occurs.",
|
|
2451
2565
|
points: [
|
|
2452
|
-
"
|
|
2453
|
-
"
|
|
2454
|
-
"The browser and CLI use the same schedules, event rules, and completion checks."
|
|
2566
|
+
"Open a task to see its owner, due date, and proof.",
|
|
2567
|
+
"Record events after they happen."
|
|
2455
2568
|
]
|
|
2456
2569
|
};
|
|
2457
2570
|
const auditPath = {
|
|
2458
2571
|
target: null,
|
|
2459
2572
|
kicker: "Report goal and audit",
|
|
2460
|
-
title: "Choose
|
|
2461
|
-
body:
|
|
2462
|
-
"SOC 2 is an independent CPA report on controls relevant to the selected Trust Services Criteria.",
|
|
2463
|
-
"Most customer requests focus on Security. Add another category only when the service and customer need call for it. Choose a management goal now, then create an Audit record only after a real CPA engagement or customer deadline exists."
|
|
2464
|
-
],
|
|
2573
|
+
title: "Choose a goal",
|
|
2574
|
+
body: "SOC 2 is an independent CPA report on controls tied to selected Trust Services Criteria.",
|
|
2465
2575
|
sections: [
|
|
2466
2576
|
{
|
|
2467
2577
|
title: "Type 1",
|
|
2468
|
-
body: "
|
|
2578
|
+
body: "Design and implementation at a point in time. Optional before Type 2."
|
|
2469
2579
|
},
|
|
2470
2580
|
{
|
|
2471
2581
|
title: "Type 2",
|
|
2472
|
-
body: "
|
|
2582
|
+
body: "Operation across a period. Evidence and populations must cover it."
|
|
2473
2583
|
}
|
|
2474
2584
|
],
|
|
2475
|
-
afterSections: "FileGRC prepares the records and packet. The CPA
|
|
2585
|
+
afterSections: "FileGRC prepares the records and evidence packet. The CPA tests and reports."
|
|
2476
2586
|
};
|
|
2477
2587
|
const setup = {
|
|
2478
2588
|
target: null,
|
|
2479
2589
|
kicker: "Initial scope",
|
|
2480
|
-
title: "Describe the service
|
|
2481
|
-
body: "Create the first
|
|
2590
|
+
title: "Describe the service in scope",
|
|
2591
|
+
body: "Create the first System and choose the program goal."
|
|
2482
2592
|
};
|
|
2483
2593
|
return [
|
|
2484
2594
|
files,
|
|
@@ -2505,10 +2615,10 @@ function renderOnboardingStep() {
|
|
|
2505
2615
|
? onboardingSetupForm()
|
|
2506
2616
|
: description + explanation + afterSections;
|
|
2507
2617
|
const finalActions = onboardingStep === steps.length - 1
|
|
2508
|
-
? '<span class="onboarding-save-status" role="status" aria-live="polite"></span><button class="button" type="button" data-onboarding="draft">Save as planned</button><button class="button primary" type="button" data-onboarding="next">Confirm
|
|
2618
|
+
? '<span class="onboarding-save-status" role="status" aria-live="polite"></span><button class="button" type="button" data-onboarding="draft">Save as planned</button><button class="button primary" type="button" data-onboarding="next">Confirm scope</button>'
|
|
2509
2619
|
: '<button class="button primary" type="button" data-onboarding="next">Next</button>';
|
|
2510
|
-
onboardingDialog.innerHTML = '<div class="onboarding-progress" style="--onboarding-step-count:' + steps.length + '" aria-label="Onboarding step ' + (onboardingStep + 1) + ' of ' + steps.length + '">' + progress + '</div><div class="onboarding-scroll"><div class="onboarding-head"><p class="kicker">' + esc(step.kicker) + ' · ' + (onboardingStep + 1) + ' of ' + steps.length + '</p><h2 id="onboarding-title">' + esc(titleCase(step.title)) + '</h2></div>' + body + '<div class="dialog-error" role="alert"></div></div><div class="dialog-actions onboarding-actions"><button class="button text-button onboarding-skip" type="button" data-onboarding="skip">Skip onboarding</button>' + (onboardingStep ? '<button class="button" type="button" data-onboarding="back">Back</button>' : "") + finalActions + '</div>';
|
|
2511
|
-
onboardingDialog.querySelector('[data-onboarding="skip"]').addEventListener("click", cancelOnboarding);
|
|
2620
|
+
onboardingDialog.innerHTML = '<div class="onboarding-progress" style="--onboarding-step-count:' + steps.length + '" aria-label="Onboarding step ' + (onboardingStep + 1) + ' of ' + steps.length + '">' + progress + '</div><div class="onboarding-scroll"><div class="onboarding-head"><p class="kicker">' + esc(step.kicker) + ' · ' + (onboardingStep + 1) + ' of ' + steps.length + '</p><h2 id="onboarding-title">' + esc(titleCase(step.title)) + '</h2></div>' + body + '<div class="dialog-error" role="alert"></div></div><div class="dialog-actions onboarding-actions"><button class="button text-button onboarding-skip" type="button" data-onboarding="skip">' + (onboardingSetupOnly ? "Close" : "Skip onboarding") + '</button>' + (onboardingStep ? '<button class="button" type="button" data-onboarding="back">Back</button>' : "") + finalActions + '</div>';
|
|
2621
|
+
onboardingDialog.querySelector('[data-onboarding="skip"]').addEventListener("click", onboardingSetupOnly ? closeOnboarding : cancelOnboarding);
|
|
2512
2622
|
onboardingDialog.querySelector('[data-onboarding="back"]')?.addEventListener("click", () => {
|
|
2513
2623
|
captureOnboardingForm();
|
|
2514
2624
|
onboardingStep -= 1;
|
|
@@ -2541,16 +2651,21 @@ function renderOnboardingStep() {
|
|
|
2541
2651
|
|
|
2542
2652
|
function onboardingSetupForm() {
|
|
2543
2653
|
const people = resourcesOfType("person").filter(({ record }) => record.status === "active");
|
|
2544
|
-
const
|
|
2654
|
+
const resourceClassifications = resourcesOfType("classification")
|
|
2655
|
+
.filter(({ record }) => record.status === "active")
|
|
2656
|
+
.map(({ record }) => record.id);
|
|
2657
|
+
const classifications = resourceClassifications.length
|
|
2658
|
+
? resourceClassifications
|
|
2659
|
+
: Object.keys(state.workspace.classificationDefinitions || {});
|
|
2545
2660
|
if (onboardingDraft.classificationId && !classifications.includes(onboardingDraft.classificationId)) {
|
|
2546
2661
|
classifications.push(onboardingDraft.classificationId);
|
|
2547
2662
|
}
|
|
2548
2663
|
const gitStatus = state.repository?.mode === "trunk"
|
|
2549
|
-
? '<div class="onboarding-git-status ' + (state.repository.status === "synced" ? "" : "warning") + '"><span class="status-dot ' + repositoryStatusTone(state.repository.status) + '"></span><span><strong>' + esc(state.repository.label) + '</strong><small>' + esc(state.repository.status === "synced" ? "Completing onboarding will save its related
|
|
2664
|
+
? '<div class="onboarding-git-status ' + (state.repository.status === "synced" ? "" : "warning") + '"><span class="status-dot ' + repositoryStatusTone(state.repository.status) + '"></span><span><strong>' + esc(state.repository.label) + '</strong><small>' + esc(state.repository.status === "synced" ? "Completing onboarding will save its related Workspace, Program, System, Component, and renderer changes in one local commit, then push it in the background." : state.repository.message) + '</small></span></div>'
|
|
2550
2665
|
: state.git.available && state.git.branch
|
|
2551
2666
|
? '<div class="onboarding-git-status"><span class="status-dot good"></span><span><strong>Manual repository mode</strong><small>Setup changes will stay local until you commit and synchronize them.</small></span></div>'
|
|
2552
2667
|
: '<div class="onboarding-git-status warning"><span class="status-dot warn"></span><span><strong>Git setup needed</strong><small>Manual-mode writes still work, but Git history is unavailable until the repository is configured.</small></span></div>';
|
|
2553
|
-
return '<p class="onboarding-body">' + esc(onboardingSteps().at(-1).body) + '</p>' + gitStatus + '<form id="onboarding-setup" class="onboarding-form"><label class="wide"><span>Service name</span><input name="serviceName" required maxlength="200" value="' + esc(onboardingDraft.serviceName) + '" placeholder="Customer-facing application"></label><label class="wide"><span>Scope description</span><textarea name="scope" required maxlength="2000" placeholder="What the service does and which production boundary is in scope">' + esc(onboardingDraft.scope) + '</textarea></label><label><span>Accountable owner</span><select name="ownerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === onboardingDraft.ownerId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Business criticality</span><select name="criticality" required>' + ["low", "medium", "high", "critical"].map((value) => '<option value="' + value + '" ' + (value === onboardingDraft.criticality ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Highest data classification</span><select name="classificationId" required>' + classifications.map((value) => '<option value="' + esc(value) + '" ' + (value === onboardingDraft.classificationId ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Internet exposed</span><select name="internetExposed" required><option value="true" ' + (onboardingDraft.internetExposed === "true" ? "selected" : "") + '>Yes</option><option value="false" ' + (onboardingDraft.internetExposed === "false" ? "selected" : "") + '>No</option></select></label><label class="wide"><span>Program goal</span><select name="programGoal" required><option value="none" ' + (onboardingDraft.programGoal === "none" ? "selected" : "") + '>No
|
|
2668
|
+
return '<p class="onboarding-body">' + esc(onboardingSteps().at(-1).body) + '</p>' + gitStatus + '<form id="onboarding-setup" class="onboarding-form"><label class="wide"><span>Service name</span><input name="serviceName" required maxlength="200" value="' + esc(onboardingDraft.serviceName) + '" placeholder="Customer-facing application"></label><label class="wide"><span>Scope description</span><textarea name="scope" required maxlength="2000" placeholder="What the service does and which production boundary is in scope">' + esc(onboardingDraft.scope) + '</textarea></label><label><span>Accountable owner</span><select name="ownerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === onboardingDraft.ownerId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Business criticality</span><select name="criticality" required>' + ["low", "medium", "high", "critical"].map((value) => '<option value="' + value + '" ' + (value === onboardingDraft.criticality ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Highest data classification</span><select name="classificationId" required>' + classifications.map((value) => '<option value="' + esc(value) + '" ' + (value === onboardingDraft.classificationId ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Internet exposed</span><select name="internetExposed" required><option value="true" ' + (onboardingDraft.internetExposed === "true" ? "selected" : "") + '>Yes</option><option value="false" ' + (onboardingDraft.internetExposed === "false" ? "selected" : "") + '>No</option></select></label><label class="wide"><span>Program goal</span><select name="programGoal" required><option value="none" ' + (onboardingDraft.programGoal === "none" ? "selected" : "") + '>No assurance goal yet</option><option value="readiness" ' + (onboardingDraft.programGoal === "readiness" ? "selected" : "") + '>Program readiness</option><option value="type-1" ' + (onboardingDraft.programGoal === "type-1" ? "selected" : "") + '>SOC 2 Type 1</option><option value="type-2" ' + (onboardingDraft.programGoal === "type-2" ? "selected" : "") + '>SOC 2 Type 2</option></select><small>Sets management intent. Audits and report periods are separate.</small></label></form><p class="onboarding-write-note">Planned saves a draft. Confirm scope activates it. Both add it to the Program.</p>';
|
|
2554
2669
|
}
|
|
2555
2670
|
|
|
2556
2671
|
function captureOnboardingForm() {
|
|
@@ -2830,8 +2945,9 @@ function openEditor(type, entry = null, options = {}) {
|
|
|
2830
2945
|
const recordContentItem = recordContent ? entry?.content?.[recordContent.slot] : null;
|
|
2831
2946
|
const editorDescription = options.description
|
|
2832
2947
|
|| implementationEditorDescription(type)
|
|
2833
|
-
||
|
|
2834
|
-
|
|
2948
|
+
|| conciseResourceDescription(definition)
|
|
2949
|
+
|| "Add the facts known now.";
|
|
2950
|
+
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">' + (entry ? "Edit record" : options.actionCompletion ? "Complete assigned work" : options.obligationCompletion ? "Record obligation work" : "Create record") + '</p><h2 id="resource-editor-title">' + esc(titleCase(entry?.record.title || record.title || definition.title)) + '</h2></div><button type="button" class="icon-button" data-editor-dismiss aria-label="Close">×</button></div><p>' + esc(editorDescription) + '</p>' + resourceReviewCriteria(type, true) + '<div class="form-grid">' + names.map((name) => editorField(type, name, fields[name], record[name], required.has(name) || conditionMatches(record, fields[name].requiredWhen), Boolean(entry), oneOf.has(name), activeOneOf.has(name))).join("") + '</div>' +
|
|
2835
2951
|
activeMarkdown.map((markdown) => {
|
|
2836
2952
|
const generated = !entry?.content?.[markdown.name];
|
|
2837
2953
|
const source = entry?.content?.[markdown.name]?.source
|
|
@@ -2850,6 +2966,8 @@ function openEditor(type, entry = null, options = {}) {
|
|
|
2850
2966
|
dialog.addEventListener("close", () => dialog.remove());
|
|
2851
2967
|
dialog.querySelectorAll("[data-editor-dismiss]").forEach((button) => button.addEventListener("click", () => dialog.close()));
|
|
2852
2968
|
wireEditorRequirements(dialog, record, fields, oneOfGroups, markdownDefinitions);
|
|
2969
|
+
wireStructuredObjectEditors(dialog);
|
|
2970
|
+
wireChoicePickers(dialog);
|
|
2853
2971
|
dialog.querySelector(".advanced-editor textarea").addEventListener("input", () => {
|
|
2854
2972
|
dialog.dataset.jsonDirty = "true";
|
|
2855
2973
|
dialog.querySelector("form").noValidate = true;
|
|
@@ -2935,22 +3053,79 @@ function openEditor(type, entry = null, options = {}) {
|
|
|
2935
3053
|
}
|
|
2936
3054
|
|
|
2937
3055
|
function implementationEditorDescription(type) {
|
|
3056
|
+
if (type === "person") {
|
|
3057
|
+
return "Record someone who owns, reviews, approves, or performs program work.";
|
|
3058
|
+
}
|
|
3059
|
+
if (type === "appointment") {
|
|
3060
|
+
return "Assign named authority to one person for a defined period.";
|
|
3061
|
+
}
|
|
3062
|
+
if (type === "program") {
|
|
3063
|
+
return "Choose the goal, scope, owners, criteria, controls, and risk method.";
|
|
3064
|
+
}
|
|
2938
3065
|
if (type === "system") {
|
|
2939
|
-
return "
|
|
3066
|
+
return "Define the service boundary, owners, information, and recovery needs.";
|
|
3067
|
+
}
|
|
3068
|
+
if (type === "component") {
|
|
3069
|
+
return "Connect this Component to its Systems and select any evidence roles.";
|
|
2940
3070
|
}
|
|
2941
3071
|
if (type === "control") {
|
|
2942
|
-
return "
|
|
3072
|
+
return "Describe the procedure, Systems, Components, and evidence sources.";
|
|
3073
|
+
}
|
|
3074
|
+
if (type === "evidence") {
|
|
3075
|
+
return "Add a retained export, report, screenshot, signed file, or approved external reference.";
|
|
3076
|
+
}
|
|
3077
|
+
if (type === "audit") {
|
|
3078
|
+
return "Record a real CPA engagement or a customer deadline that requires audit planning.";
|
|
3079
|
+
}
|
|
3080
|
+
if (type === "audit-request") {
|
|
3081
|
+
return "Track one auditor request, owner, due date, response, and evidence.";
|
|
3082
|
+
}
|
|
3083
|
+
if (type === "vendor") {
|
|
3084
|
+
return "Record a material external provider relationship.";
|
|
3085
|
+
}
|
|
3086
|
+
if (type === "classification") {
|
|
3087
|
+
return "Define one information handling level and its rank.";
|
|
3088
|
+
}
|
|
3089
|
+
if (type === "information-type") {
|
|
3090
|
+
return "Name an information category and its default handling level.";
|
|
3091
|
+
}
|
|
3092
|
+
if (type === "policy") {
|
|
3093
|
+
return "Define a program rule, its scope, owner, reviewer, and dates.";
|
|
3094
|
+
}
|
|
3095
|
+
if (type === "obligation") {
|
|
3096
|
+
return "Schedule recurring or event-driven work.";
|
|
3097
|
+
}
|
|
3098
|
+
if (type === "risk") {
|
|
3099
|
+
return "Track one threat or business impact and its treatment.";
|
|
3100
|
+
}
|
|
3101
|
+
if (type === "risk-assessment") {
|
|
3102
|
+
return "Evaluate a defined scope with the Program risk method.";
|
|
3103
|
+
}
|
|
3104
|
+
if (type === "finding") {
|
|
3105
|
+
return "Track a confirmed gap that needs its own remediation.";
|
|
3106
|
+
}
|
|
3107
|
+
if (type === "exception") {
|
|
3108
|
+
return "Record an approved, time-bound departure from a Policy or Control.";
|
|
3109
|
+
}
|
|
3110
|
+
if (type === "action-item") {
|
|
3111
|
+
return "Assign one dated follow-up task and its completion proof.";
|
|
2943
3112
|
}
|
|
2944
3113
|
return "";
|
|
2945
3114
|
}
|
|
2946
3115
|
|
|
3116
|
+
function conciseResourceDescription(definition) {
|
|
3117
|
+
const description = definition?.description?.trim() || "";
|
|
3118
|
+
return description.match(/^.*?[.!?](?:\s|$)/)?.[0]?.trim() || description;
|
|
3119
|
+
}
|
|
3120
|
+
|
|
2947
3121
|
function seedRecord(type, definition) {
|
|
2948
3122
|
const record = { id: createResourceId(type, "new", state.resources.map(({ record }) => record.id)), type, title: "" };
|
|
2949
3123
|
const fields = { ...state.model.commonFields, ...definition.fields };
|
|
2950
3124
|
for (const name of definition.required || []) {
|
|
2951
3125
|
const field = fields[name];
|
|
2952
3126
|
if (record[name] !== undefined) continue;
|
|
2953
|
-
if (field.
|
|
3127
|
+
if (field.default !== undefined) record[name] = structuredClone(field.default);
|
|
3128
|
+
else if (field.relation) {
|
|
2954
3129
|
const candidates = relationCandidates(field);
|
|
2955
3130
|
record[name] = field.type === "array" ? (candidates.length === 1 ? [candidates[0].record.id] : []) : (candidates.length === 1 ? candidates[0].record.id : "");
|
|
2956
3131
|
}
|
|
@@ -3004,12 +3179,18 @@ function renderRecordContentEditor(type, entry, options) {
|
|
|
3004
3179
|
if (!config) return "";
|
|
3005
3180
|
const item = entry?.content?.[config.slot];
|
|
3006
3181
|
const source = item?.source || "";
|
|
3007
|
-
const editor = '<label class="content-editor-field record-content-editor"><span>' + esc(config.label) + ' Markdown <small>optional</small></span><textarea data-record-content spellcheck="true" placeholder="
|
|
3182
|
+
const editor = '<label class="content-editor-field record-content-editor"><span>' + esc(config.label) + ' Markdown <small>optional</small></span><textarea data-record-content spellcheck="true" placeholder="' + esc(recordContentPlaceholder(type)) + '">' + esc(source) + '</textarea></label>';
|
|
3008
3183
|
if (config.mode === "default") return editor;
|
|
3009
3184
|
const open = item || options.addRecordContent;
|
|
3010
3185
|
return '<details class="record-content-details" ' + (open ? "open" : "") + '><summary>' + (item ? "Record Markdown" : "Add Record Markdown") + '</summary><p>Use this when the structured fields do not capture the full record.</p>' + editor + '</details>';
|
|
3011
3186
|
}
|
|
3012
3187
|
|
|
3188
|
+
function recordContentPlaceholder(type) {
|
|
3189
|
+
if (type === "system") return "Add architecture, dependencies, or boundary details.";
|
|
3190
|
+
if (type === "component") return "Add operating details or evidence retrieval steps.";
|
|
3191
|
+
return "Document the work, results, decisions, and follow-up.";
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3013
3194
|
function editorField(type, name, field, value, required, editing, oneOfRequired = false, oneOfActive = oneOfRequired) {
|
|
3014
3195
|
const label = fieldLabel(type, name);
|
|
3015
3196
|
const requiredMark = required || field.requiredWhen || oneOfRequired
|
|
@@ -3027,21 +3208,22 @@ function editorField(type, name, field, value, required, editing, oneOfRequired
|
|
|
3027
3208
|
if (field.relation && field.type === "array") {
|
|
3028
3209
|
const candidates = relationCandidates(field);
|
|
3029
3210
|
control = candidates.length
|
|
3030
|
-
? '<div class="checkbox-list">' + candidates.map(({ record }) => '<label><input type="checkbox" value="' + esc(record.id) + '" ' + ((value || []).includes(record.id) ? "checked" : "") + '><span>' + esc(record.title) + '<small>' + esc(state.model.resources[record.type].title
|
|
3031
|
-
: required ? '<select><option value="">No
|
|
3211
|
+
? '<div class="checkbox-list">' + candidates.map(({ record }) => '<label><input type="checkbox" value="' + esc(record.id) + '" ' + ((value || []).includes(record.id) ? "checked" : "") + '><span>' + esc(record.title) + '<small>' + esc(state.model.resources[record.type].title) + '</small></span></label>').join("") + '</div>'
|
|
3212
|
+
: required ? '<select><option value="">No matching ' + esc(relationTypeLabel(field, true).toLowerCase()) + ' yet</option></select>' : '<div class="missing-options">No matching ' + esc(relationTypeLabel(field, true).toLowerCase()) + ' yet.</div>';
|
|
3032
3213
|
return fieldWrap(name, "relation-array", label, requiredMark, control, help, required);
|
|
3033
3214
|
}
|
|
3034
3215
|
if (field.relation) {
|
|
3035
3216
|
const candidates = relationCandidates(field);
|
|
3036
3217
|
control = candidates.length
|
|
3037
|
-
? '<select><option value="">Select
|
|
3038
|
-
: required ? '<select><option value="">No
|
|
3218
|
+
? '<select><option value="">Select ' + esc(relationTypeLabel(field).toLowerCase()) + '</option>' + candidates.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (value === record.id ? "selected" : "") + '>' + esc(record.title) + ' · ' + esc(state.model.resources[record.type].title) + '</option>').join("") + '</select>'
|
|
3219
|
+
: required ? '<select><option value="">No matching ' + esc(relationTypeLabel(field, true).toLowerCase()) + ' yet</option></select>' : '<div class="missing-options">No matching ' + esc(relationTypeLabel(field, true).toLowerCase()) + ' yet.</div>';
|
|
3039
3220
|
return fieldWrap(name, "relation", label, requiredMark, control, help, required);
|
|
3040
3221
|
}
|
|
3041
3222
|
if (name === "classificationId") {
|
|
3042
|
-
const
|
|
3223
|
+
const classifications = resourcesOfType("classification").filter(({ record }) => record.status === "active");
|
|
3224
|
+
const values = classifications.length ? classifications.map(({ record }) => record.id) : Object.keys(state.workspace.classificationDefinitions || {});
|
|
3043
3225
|
control = '<select><option value="">Select</option>' + values.map((item) => '<option value="' + esc(item) + '" ' + (value === item ? "selected" : "") + '>' + esc(properCase(item)) + '</option>').join("") + '</select>';
|
|
3044
|
-
return fieldWrap(name, "string", label, requiredMark, control, "Defined by Workspace classificationDefinitions", required);
|
|
3226
|
+
return fieldWrap(name, "string", label, requiredMark, control, classifications.length ? "References an active Classification" : "Defined by Workspace classificationDefinitions", required);
|
|
3045
3227
|
}
|
|
3046
3228
|
if (field.type === "enum" || field.type === "rating" || field.type === "outcome") {
|
|
3047
3229
|
const values = field.values || (field.type === "rating" ? state.model.primitives.rating : state.model.primitives.outcome) || [];
|
|
@@ -3053,15 +3235,30 @@ function editorField(type, name, field, value, required, editing, oneOfRequired
|
|
|
3053
3235
|
return fieldWrap(name, "boolean", label, requiredMark, control, help, required);
|
|
3054
3236
|
}
|
|
3055
3237
|
if (field.type === "object") {
|
|
3238
|
+
const schema = state.model.objectTypes[field.objectType];
|
|
3239
|
+
if (schema?.additionalProperties === false) {
|
|
3240
|
+
control = '<div class="structured-object-fields" data-object-type="' + esc(field.objectType) + '">' + objectPropertyFields(schema, value || {}) + '</div>';
|
|
3241
|
+
return fieldWrap(name, "structured-object", label, requiredMark, control, "Fill only the details that apply.", required);
|
|
3242
|
+
}
|
|
3056
3243
|
control = '<textarea spellcheck="false" placeholder="{ }">' + esc(value === undefined ? "" : JSON.stringify(value, null, 2)) + '</textarea>';
|
|
3057
3244
|
return fieldWrap(name, "object", label, requiredMark, control, "JSON object", required);
|
|
3058
3245
|
}
|
|
3059
3246
|
if (field.type === "array") {
|
|
3247
|
+
if (field.items === "object") {
|
|
3248
|
+
const schema = state.model.objectTypes[field.itemObjectType];
|
|
3249
|
+
const items = Array.isArray(value) ? value : [];
|
|
3250
|
+
control = '<div class="object-array-items">' + items.map((item) => objectArrayItem(schema, item)).join("") + '</div><button class="button" type="button" data-add-object-item>Add ' + esc(humanize(field.itemObjectType || "item")) + '</button><template>' + objectArrayItem(schema, {}) + '</template>';
|
|
3251
|
+
return fieldWrap(name, "object-array", label, requiredMark, control, "Add one row for each relationship or scoped decision.", required);
|
|
3252
|
+
}
|
|
3253
|
+
if (name === "evidenceSourceKinds") {
|
|
3254
|
+
const selected = Array.isArray(value) ? value : [];
|
|
3255
|
+
const options = evidenceSourceRoleOptions();
|
|
3256
|
+
const summary = selected.length ? selected.length + " selected" : "Choose roles";
|
|
3257
|
+
control = '<details class="choice-picker"><summary><span data-choice-summary>' + esc(summary) + '</span></summary><div class="checkbox-list">' + options.map((option) => '<label><input type="checkbox" value="' + esc(option) + '" ' + (selected.includes(option) ? "checked" : "") + '><span>' + esc(properCase(option)) + '</span></label>').join("") + '</div></details>';
|
|
3258
|
+
return fieldWrap(name, "choice-array", label, requiredMark, control, "Select only roles this Component performs.", required);
|
|
3259
|
+
}
|
|
3060
3260
|
control = '<textarea placeholder="One value per line">' + esc((value || []).join("\n")) + '</textarea>';
|
|
3061
|
-
|
|
3062
|
-
? "One role per line. Use the roles required by the Controls this System supports, such as " + evidenceSourceRoleOptions().join(", ") + "."
|
|
3063
|
-
: "One value per line";
|
|
3064
|
-
return fieldWrap(name, "array", label, requiredMark, control, arrayHelp, required);
|
|
3261
|
+
return fieldWrap(name, "array", label, requiredMark, control, "One value per line", required);
|
|
3065
3262
|
}
|
|
3066
3263
|
if (["description", "statement", "scope", "rationale", "purpose"].some((part) => name.toLowerCase().includes(part))) {
|
|
3067
3264
|
control = '<textarea>' + esc(value ?? "") + '</textarea>';
|
|
@@ -3079,6 +3276,178 @@ function evidenceSourceRoleOptions() {
|
|
|
3079
3276
|
return [...new Set((state.model.evidenceSourceFamilies || []).flatMap(({ sourceKinds }) => sourceKinds || []))].sort();
|
|
3080
3277
|
}
|
|
3081
3278
|
|
|
3279
|
+
function wireChoicePickers(dialog) {
|
|
3280
|
+
dialog.querySelectorAll(".choice-picker").forEach((picker) => {
|
|
3281
|
+
const sync = () => {
|
|
3282
|
+
const count = picker.querySelectorAll('input[type="checkbox"]:checked').length;
|
|
3283
|
+
picker.querySelector("[data-choice-summary]").textContent = count ? count + " selected" : "Choose roles";
|
|
3284
|
+
};
|
|
3285
|
+
picker.addEventListener("change", sync);
|
|
3286
|
+
sync();
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
function objectPropertyFields(schema, value = {}) {
|
|
3291
|
+
const required = new Set(schema?.required || []);
|
|
3292
|
+
return Object.entries(schema?.properties || {}).map(([name, property]) => {
|
|
3293
|
+
const requiredNow = required.has(name) && !property.requiredWhen;
|
|
3294
|
+
const mark = required.has(name) || property.requiredWhen
|
|
3295
|
+
? '<span class="required-mark" ' + (requiredNow ? "" : "hidden") + '>Required</span>'
|
|
3296
|
+
: "";
|
|
3297
|
+
const stringMap = property.type === "object" && state.model.objectTypes[property.objectType]?.additionalProperties?.type === "string";
|
|
3298
|
+
let input;
|
|
3299
|
+
if (property.relation && property.type === "id") {
|
|
3300
|
+
const candidates = relationCandidates(property);
|
|
3301
|
+
input = '<select><option value="">Select ' + esc(relationTypeLabel(property).toLowerCase()) + '</option>' + candidates.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (value[name] === record.id ? "selected" : "") + '>' + esc(record.title) + ' · ' + esc(state.model.resources[record.type].title) + '</option>').join("") + '</select>';
|
|
3302
|
+
} else if (property.relation && property.type === "array") {
|
|
3303
|
+
const candidates = relationCandidates(property);
|
|
3304
|
+
input = candidates.length
|
|
3305
|
+
? '<div class="checkbox-list">' + candidates.map(({ record }) => '<label><input type="checkbox" value="' + esc(record.id) + '" ' + ((value[name] || []).includes(record.id) ? "checked" : "") + '><span>' + esc(record.title) + '<small>' + esc(state.model.resources[record.type].title) + '</small></span></label>').join("") + '</div>'
|
|
3306
|
+
: '<div class="missing-options">No matching ' + esc(relationTypeLabel(property, true).toLowerCase()) + ' yet.</div>';
|
|
3307
|
+
} else if (property.type === "array" && property.values) {
|
|
3308
|
+
input = '<div class="checkbox-list">' + property.values.map((option) => '<label><input type="checkbox" value="' + esc(option) + '" ' + ((value[name] || []).includes(option) ? "checked" : "") + '><span>' + esc(properCase(option)) + '</span></label>').join("") + '</div>';
|
|
3309
|
+
} else if (property.type === "array") {
|
|
3310
|
+
input = '<textarea placeholder="One value per line">' + esc((value[name] || []).join("\n")) + '</textarea>';
|
|
3311
|
+
} else if (property.type === "enum" || property.type === "rating") {
|
|
3312
|
+
const options = property.values || (property.registry ? Object.keys(state.model[property.registry] || {}) : state.model.primitives[property.type] || []);
|
|
3313
|
+
input = '<select><option value="">Select</option>' + options.map((option) => '<option value="' + esc(option) + '" ' + (value[name] === option ? "selected" : "") + '>' + esc(properCase(option)) + '</option>').join("") + '</select>';
|
|
3314
|
+
} else if (property.type === "object") {
|
|
3315
|
+
const nested = state.model.objectTypes[property.objectType];
|
|
3316
|
+
input = nested?.additionalProperties === false
|
|
3317
|
+
? '<div class="structured-object-fields nested" data-object-type="' + esc(property.objectType) + '">' + objectPropertyFields(nested, value[name] || {}) + '</div>'
|
|
3318
|
+
: nested?.additionalProperties?.type === "string"
|
|
3319
|
+
? stringMapEditor(value[name] || {}, name)
|
|
3320
|
+
: '<textarea spellcheck="false" placeholder="{ }">' + esc(value[name] === undefined ? "" : JSON.stringify(value[name], null, 2)) + '</textarea>';
|
|
3321
|
+
} else if (property.type === "boolean") {
|
|
3322
|
+
input = '<select><option value="">Not set</option><option value="true" ' + (value[name] === true ? "selected" : "") + '>Yes</option><option value="false" ' + (value[name] === false ? "selected" : "") + '>No</option></select>';
|
|
3323
|
+
} else {
|
|
3324
|
+
const multiline = ["rationale", "description", "notes"].some((part) => name.toLowerCase().includes(part));
|
|
3325
|
+
input = multiline
|
|
3326
|
+
? '<textarea>' + esc(value[name] || "") + '</textarea>'
|
|
3327
|
+
: '<input type="' + (property.type === "date" ? "date" : property.type === "timestamp" ? "datetime-local" : ["integer", "number"].includes(property.type) ? "number" : "text") + '" value="' + esc(property.type === "timestamp" && value[name] ? String(value[name]).slice(0, 16) : value[name] ?? "") + '"' + (property.minimum !== undefined ? ' min="' + esc(property.minimum) + '"' : "") + '>';
|
|
3328
|
+
}
|
|
3329
|
+
return '<div class="object-property' + (stringMap ? " string-map-property" : "") + '" data-object-field="' + esc(name) + '" data-object-kind="' + esc(property.type) + '" data-object-required="' + (required.has(name) || property.requiredWhen ? "true" : "false") + '"' + (property.requiredWhen ? ' data-object-required-when="' + esc(JSON.stringify(property.requiredWhen)) + '"' : "") + (property.allowedWhen ? ' data-object-allowed-when="' + esc(JSON.stringify(property.allowedWhen)) + '"' : "") + '><span class="object-property-label">' + esc(humanize(name)) + mark + '</span>' + input + '</div>';
|
|
3330
|
+
}).join("");
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
function objectArrayItem(schema, value = {}) {
|
|
3334
|
+
return '<fieldset class="object-array-item"><legend>' + esc(humanize(schema?.title || "Relationship")) + '</legend><div class="structured-object-fields">' + objectPropertyFields(schema, value) + '</div><button type="button" class="text-button" data-remove-object-item>Remove</button></fieldset>';
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
function stringMapEditor(value = {}, name = "item") {
|
|
3338
|
+
const entries = Object.entries(value);
|
|
3339
|
+
const row = ([key = "", item = ""] = []) => '<div class="string-map-row"><input data-map-key value="' + esc(key) + '" placeholder="Name"><input data-map-value value="' + esc(item) + '" placeholder="Value"><button type="button" class="text-button" data-remove-string-map aria-label="Remove ' + esc(humanize(name).toLowerCase()) + '">Remove</button></div>';
|
|
3340
|
+
return '<div class="string-map-editor"><div class="string-map-items">' + (entries.length ? entries.map(row).join("") : row()) + '</div><button type="button" class="button" data-add-string-map>Add ' + esc(humanize(name).replace(/s$/, "").toLowerCase()) + '</button><template>' + row() + '</template></div>';
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
function wireStructuredObjectEditors(dialog) {
|
|
3344
|
+
dialog.querySelectorAll('[data-kind="object-array"]').forEach((group) => {
|
|
3345
|
+
group.querySelector('[data-add-object-item]')?.addEventListener("click", () => {
|
|
3346
|
+
const template = group.querySelector("template");
|
|
3347
|
+
group.querySelector(".object-array-items").append(template.content.cloneNode(true));
|
|
3348
|
+
refresh(group.querySelector(".object-array-items > .object-array-item:last-child > .structured-object-fields"));
|
|
3349
|
+
});
|
|
3350
|
+
group.addEventListener("click", (event) => {
|
|
3351
|
+
const remove = event.target.closest("[data-remove-object-item]");
|
|
3352
|
+
if (!remove) return;
|
|
3353
|
+
remove.closest(".object-array-item")?.remove();
|
|
3354
|
+
});
|
|
3355
|
+
});
|
|
3356
|
+
dialog.querySelector("form").addEventListener("click", (event) => {
|
|
3357
|
+
const add = event.target.closest("[data-add-string-map]");
|
|
3358
|
+
if (add) {
|
|
3359
|
+
const editor = add.closest(".string-map-editor");
|
|
3360
|
+
editor.querySelector(".string-map-items").append(editor.querySelector("template").content.cloneNode(true));
|
|
3361
|
+
editor.querySelector(".string-map-row:last-child [data-map-key]").focus();
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
const remove = event.target.closest("[data-remove-string-map]");
|
|
3365
|
+
if (!remove) return;
|
|
3366
|
+
const editor = remove.closest(".string-map-editor");
|
|
3367
|
+
const rows = editor.querySelectorAll(".string-map-row");
|
|
3368
|
+
if (rows.length > 1) remove.closest(".string-map-row").remove();
|
|
3369
|
+
else rows[0].querySelectorAll("input").forEach((input) => { input.value = ""; });
|
|
3370
|
+
});
|
|
3371
|
+
const refresh = (container) => {
|
|
3372
|
+
const fields = [...container.querySelectorAll(":scope > [data-object-field]")];
|
|
3373
|
+
const values = Object.fromEntries(fields.map((field) => [field.dataset.objectField, readObjectProperty(field)]));
|
|
3374
|
+
const hasValue = Object.values(values).some((value) => value !== undefined && (!Array.isArray(value) || value.length) && (typeof value !== "object" || Array.isArray(value) || Object.keys(value).length));
|
|
3375
|
+
const group = container.closest("[data-field-group]");
|
|
3376
|
+
const active = Boolean(container.closest(".object-array-item")) || group?.dataset.required === "true" || hasValue;
|
|
3377
|
+
for (const field of fields) {
|
|
3378
|
+
const allowedWhen = field.dataset.objectAllowedWhen ? JSON.parse(field.dataset.objectAllowedWhen) : null;
|
|
3379
|
+
const requiredWhen = field.dataset.objectRequiredWhen ? JSON.parse(field.dataset.objectRequiredWhen) : null;
|
|
3380
|
+
field.hidden = Boolean(allowedWhen) && !conditionMatches(values, allowedWhen);
|
|
3381
|
+
const required = active && !field.hidden && (field.dataset.objectRequired === "true") && (!requiredWhen || conditionMatches(values, requiredWhen));
|
|
3382
|
+
field.querySelector(".required-mark")?.toggleAttribute("hidden", !required);
|
|
3383
|
+
const checkboxes = [...field.querySelectorAll(":scope > .checkbox-list input[type=checkbox]")];
|
|
3384
|
+
if (checkboxes.length) {
|
|
3385
|
+
const selected = checkboxes.findIndex((checkbox) => checkbox.checked);
|
|
3386
|
+
checkboxes.forEach((checkbox, index) => { checkbox.required = required && index === (selected < 0 ? 0 : selected); });
|
|
3387
|
+
} else {
|
|
3388
|
+
const map = field.querySelector(":scope > .string-map-editor");
|
|
3389
|
+
if (map) {
|
|
3390
|
+
[...map.querySelectorAll(".string-map-row")].forEach((row, index) => {
|
|
3391
|
+
const key = row.querySelector("[data-map-key]");
|
|
3392
|
+
const value = row.querySelector("[data-map-value]");
|
|
3393
|
+
const rowRequired = required && index === 0 || key.value.trim() || value.value.trim();
|
|
3394
|
+
key.required = Boolean(rowRequired);
|
|
3395
|
+
value.required = Boolean(rowRequired);
|
|
3396
|
+
});
|
|
3397
|
+
continue;
|
|
3398
|
+
}
|
|
3399
|
+
const control = field.querySelector(":scope > input, :scope > select, :scope > textarea");
|
|
3400
|
+
if (control) control.required = required;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
};
|
|
3404
|
+
dialog.querySelectorAll(".structured-object-fields").forEach(refresh);
|
|
3405
|
+
dialog.querySelector("form").addEventListener("input", (event) => {
|
|
3406
|
+
const container = event.target.closest(".structured-object-fields");
|
|
3407
|
+
if (container) refresh(container);
|
|
3408
|
+
});
|
|
3409
|
+
dialog.querySelector("form").addEventListener("change", (event) => {
|
|
3410
|
+
const container = event.target.closest(".structured-object-fields");
|
|
3411
|
+
if (container) refresh(container);
|
|
3412
|
+
});
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
function readObjectProperty(field) {
|
|
3416
|
+
if (field.hidden) return undefined;
|
|
3417
|
+
const kind = field.dataset.objectKind;
|
|
3418
|
+
if (kind === "array") {
|
|
3419
|
+
const checkboxes = [...field.querySelectorAll(":scope > .checkbox-list input[type=checkbox]")];
|
|
3420
|
+
if (checkboxes.length) return checkboxes.filter((checkbox) => checkbox.checked).map((checkbox) => checkbox.value);
|
|
3421
|
+
return (field.querySelector(":scope > textarea")?.value || "").split(/\n|,/).map((item) => item.trim()).filter(Boolean);
|
|
3422
|
+
}
|
|
3423
|
+
if (kind === "object") {
|
|
3424
|
+
const nested = field.querySelector(":scope > .structured-object-fields");
|
|
3425
|
+
if (nested) return readStructuredObject(nested);
|
|
3426
|
+
const map = field.querySelector(":scope > .string-map-editor");
|
|
3427
|
+
if (map) return readStringMap(map);
|
|
3428
|
+
const raw = field.querySelector(":scope > textarea")?.value.trim();
|
|
3429
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
3430
|
+
}
|
|
3431
|
+
const raw = field.querySelector(":scope > input, :scope > select, :scope > textarea")?.value ?? "";
|
|
3432
|
+
if (raw === "") return undefined;
|
|
3433
|
+
if (kind === "boolean") return raw === "true";
|
|
3434
|
+
if (["integer", "number"].includes(kind)) return Number(raw);
|
|
3435
|
+
if (kind === "timestamp") return new Date(raw).toISOString();
|
|
3436
|
+
return raw;
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
function readStringMap(container) {
|
|
3440
|
+
return Object.fromEntries([...container.querySelectorAll(".string-map-row")]
|
|
3441
|
+
.map((row) => [row.querySelector("[data-map-key]").value.trim(), row.querySelector("[data-map-value]").value.trim()])
|
|
3442
|
+
.filter(([key, value]) => key && value));
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
function readStructuredObject(container) {
|
|
3446
|
+
return Object.fromEntries([...container.querySelectorAll(":scope > [data-object-field]")]
|
|
3447
|
+
.map((field) => [field.dataset.objectField, readObjectProperty(field)])
|
|
3448
|
+
.filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length) && (typeof value !== "object" || Array.isArray(value) || Object.keys(value).length)));
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3082
3451
|
function fieldWrap(name, kind, label, requiredMark, control, help, required) {
|
|
3083
3452
|
const labelId = "field-label-" + name;
|
|
3084
3453
|
let labelledControl = control.replace(/^<([a-z]+)/, '<$1 aria-labelledby="' + esc(labelId) + '"');
|
|
@@ -3204,18 +3573,28 @@ function readGuidedRecord(dialog, base, fields) {
|
|
|
3204
3573
|
}
|
|
3205
3574
|
const kind = group.dataset.kind;
|
|
3206
3575
|
let value;
|
|
3207
|
-
if (kind === "relation-array") value = [...group.querySelectorAll('input[type="checkbox"]:checked')].map((input) => input.value);
|
|
3576
|
+
if (kind === "relation-array" || kind === "choice-array") value = [...group.querySelectorAll('input[type="checkbox"]:checked')].map((input) => input.value);
|
|
3208
3577
|
else {
|
|
3209
3578
|
const control = group.querySelector("input,select,textarea");
|
|
3210
3579
|
const raw = control?.value ?? "";
|
|
3211
3580
|
if (kind === "array") value = raw.split(/\n|,/).map((item) => item.trim()).filter(Boolean);
|
|
3581
|
+
else if (kind === "object-array") {
|
|
3582
|
+
value = [...group.querySelectorAll(".object-array-items > .object-array-item")]
|
|
3583
|
+
.map((item) => readStructuredObject(item.querySelector(":scope > .structured-object-fields")))
|
|
3584
|
+
.filter((item) => Object.keys(item).length);
|
|
3585
|
+
}
|
|
3586
|
+
else if (kind === "structured-object") value = readStructuredObject(group.querySelector(":scope > .structured-object-fields"));
|
|
3212
3587
|
else if (kind === "object") value = raw.trim() ? JSON.parse(raw) : undefined;
|
|
3213
3588
|
else if (kind === "boolean") value = raw === "" ? undefined : raw === "true";
|
|
3214
3589
|
else if (kind === "integer") value = raw === "" ? undefined : Number(raw);
|
|
3215
3590
|
else if (kind === "number") value = raw === "" ? undefined : Number(raw);
|
|
3216
3591
|
else value = raw;
|
|
3217
3592
|
}
|
|
3218
|
-
|
|
3593
|
+
const empty = value === ""
|
|
3594
|
+
|| value === undefined
|
|
3595
|
+
|| Array.isArray(value) && !value.length
|
|
3596
|
+
|| value && typeof value === "object" && !Array.isArray(value) && !Object.keys(value).length;
|
|
3597
|
+
if (empty && group.dataset.required !== "true") delete record[name];
|
|
3219
3598
|
else record[name] = value;
|
|
3220
3599
|
}
|
|
3221
3600
|
return record;
|
|
@@ -3225,8 +3604,20 @@ function relationCandidates(field) {
|
|
|
3225
3604
|
return state.resources.filter(({ record }) => field.relation.includes("*") || field.relation.includes(record.type));
|
|
3226
3605
|
}
|
|
3227
3606
|
|
|
3607
|
+
function relationTypeLabel(field, plural = false) {
|
|
3608
|
+
if (field.relation.includes("*")) return plural ? "Resources" : "resource";
|
|
3609
|
+
if (field.relation.length > 3) return plural ? "Resources" : "resource";
|
|
3610
|
+
const labels = field.relation.map((type) => (
|
|
3611
|
+
plural ? state.model.resources[type]?.pluralTitle : state.model.resources[type]?.title
|
|
3612
|
+
) || humanize(type));
|
|
3613
|
+
if (labels.length < 2) return labels.join("");
|
|
3614
|
+
if (labels.length === 2) return labels.join(" or ");
|
|
3615
|
+
return labels.slice(0, -1).join(", ") + ", or " + labels.at(-1);
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3228
3618
|
function relationHelp(field) {
|
|
3229
3619
|
if (field.relation.includes("*")) return "References any resource";
|
|
3620
|
+
if (field.relation.length > 3) return "References supported records";
|
|
3230
3621
|
const labels = field.relation.map((type) => state.model.resources[type]?.pluralTitle || type);
|
|
3231
3622
|
if (labels.length < 2) return "References " + labels.join("");
|
|
3232
3623
|
if (labels.length === 2) return "References " + labels.join(" or ");
|
|
@@ -3326,7 +3717,14 @@ function globalSearch(query) {
|
|
|
3326
3717
|
dialog.innerHTML = '<div class="dialog-head"><div><p class="kicker">Search</p><h2 id="search-results-title">' + esc(query) + '</h2></div><button class="icon-button" aria-label="Close">×</button></div><div class="result-list"></div><nav class="pagination search-pagination" aria-label="Search result pages" hidden><button class="button" type="button" data-search-page="previous">Previous</button><span class="page-status" aria-live="polite"></span><button class="button" type="button" data-search-page="next">Next</button></nav>';
|
|
3327
3718
|
document.body.append(dialog);
|
|
3328
3719
|
dialog.showModal();
|
|
3329
|
-
|
|
3720
|
+
const clearSearch = () => {
|
|
3721
|
+
const search = root.querySelector("#global-search");
|
|
3722
|
+
if (search?.value.trim() === query) search.value = "";
|
|
3723
|
+
};
|
|
3724
|
+
dialog.querySelector(".icon-button").onclick = () => {
|
|
3725
|
+
clearSearch();
|
|
3726
|
+
dialog.close();
|
|
3727
|
+
};
|
|
3330
3728
|
const results = dialog.querySelector(".result-list");
|
|
3331
3729
|
const pagination = dialog.querySelector(".search-pagination");
|
|
3332
3730
|
const previous = pagination.querySelector('[data-search-page="previous"]');
|
|
@@ -3337,7 +3735,10 @@ function globalSearch(query) {
|
|
|
3337
3735
|
const start = (pageNumber - 1) * SEARCH_PAGE_SIZE;
|
|
3338
3736
|
const visible = matches.slice(start, start + SEARCH_PAGE_SIZE);
|
|
3339
3737
|
results.innerHTML = visible.length ? visible.map(({ record }) => '<a href="#/resource/' + encodeURIComponent(record.type) + '/' + encodeURIComponent(record.id) + '"><strong>' + esc(record.title) + '</strong><small>' + esc(state.model.resources[record.type].title) + '</small></a>').join("") : empty("No matching records.");
|
|
3340
|
-
results.querySelectorAll("a").forEach((link) => link.onclick = () =>
|
|
3738
|
+
results.querySelectorAll("a").forEach((link) => link.onclick = () => {
|
|
3739
|
+
clearSearch();
|
|
3740
|
+
dialog.close();
|
|
3741
|
+
});
|
|
3341
3742
|
pagination.hidden = totalPages === 1;
|
|
3342
3743
|
previous.disabled = pageNumber === 1;
|
|
3343
3744
|
next.disabled = pageNumber === totalPages;
|
|
@@ -3356,7 +3757,10 @@ function globalSearch(query) {
|
|
|
3356
3757
|
results.scrollTop = 0;
|
|
3357
3758
|
});
|
|
3358
3759
|
renderResults();
|
|
3359
|
-
dialog.addEventListener("close", () =>
|
|
3760
|
+
dialog.addEventListener("close", () => {
|
|
3761
|
+
clearSearch();
|
|
3762
|
+
dialog.remove();
|
|
3763
|
+
});
|
|
3360
3764
|
}
|
|
3361
3765
|
|
|
3362
3766
|
function auditProgress(audit) {
|
|
@@ -3531,9 +3935,15 @@ function conditionMatchesValues(condition, valueFor) {
|
|
|
3531
3935
|
function conditionValueMatches(actual, expected) {
|
|
3532
3936
|
return Array.isArray(expected) ? expected.includes(actual) : actual === expected;
|
|
3533
3937
|
}
|
|
3534
|
-
function formatValue(value, field, type) {
|
|
3938
|
+
function formatValue(value, field, type, compact = false) {
|
|
3535
3939
|
if (value === undefined || value === null || value === "") return '<span class="muted">Not set</span>';
|
|
3536
3940
|
const definition = fieldDefinition(type, field);
|
|
3941
|
+
if (type === "program" && field === "requirementApplicability" && Array.isArray(value)) {
|
|
3942
|
+
const reviewed = value.filter(({ decision }) => ["applicable", "not-applicable"].includes(decision)).length;
|
|
3943
|
+
const pending = value.length - reviewed;
|
|
3944
|
+
const label = reviewed + " reviewed" + (pending ? " · " + pending + " to review" : "");
|
|
3945
|
+
return '<a class="tag relation" href="#/resources/requirement' + (pending ? "?review=1" : "") + '">' + esc(label) + '</a>';
|
|
3946
|
+
}
|
|
3537
3947
|
if (definition?.type === "date") return esc(formatCalendarDate(value));
|
|
3538
3948
|
if (definition?.type === "timestamp") return esc(formatLocalDateTime(value));
|
|
3539
3949
|
if (type === "obligation" && field === "status") {
|
|
@@ -3541,7 +3951,12 @@ function formatValue(value, field, type) {
|
|
|
3541
3951
|
return '<span class="badge ' + (value === "active" ? "neutral" : "status-" + esc(String(value))) + '">' + esc(label) + '</span>';
|
|
3542
3952
|
}
|
|
3543
3953
|
if (field === "status" || field.endsWith("Rating") || field === "severity" || field === "outcome") return '<span class="badge status-' + esc(String(value)) + '">' + esc(properCase(value)) + '</span>';
|
|
3544
|
-
if (Array.isArray(value)
|
|
3954
|
+
if (Array.isArray(value) && definition?.items === "object") return formatObjectArray(value, definition.itemObjectType, compact);
|
|
3955
|
+
if (Array.isArray(value)) {
|
|
3956
|
+
if (!value.length) return '<span class="muted">None</span>';
|
|
3957
|
+
if (field === "evidenceSourceKinds") return value.map((item) => '<span class="tag choice-tag">' + esc(properCase(item)) + '</span>').join(" ");
|
|
3958
|
+
return value.map((item) => formatReference(item)).join(" ");
|
|
3959
|
+
}
|
|
3545
3960
|
if (field === "sourceReference" && typeof value === "object") {
|
|
3546
3961
|
const href = safeExternalUrl(value.url);
|
|
3547
3962
|
if (href) return '<a class="external-source" href="' + esc(href) + '" target="_blank" rel="noopener noreferrer"><span><strong>' + esc(value.title || "Official source") + '</strong><small>' + esc(href) + '</small></span><b aria-hidden="true">↗</b></a>';
|
|
@@ -3553,6 +3968,33 @@ function formatValue(value, field, type) {
|
|
|
3553
3968
|
if (definition?.type === "enum") return esc(properCase(value));
|
|
3554
3969
|
return esc(String(value));
|
|
3555
3970
|
}
|
|
3971
|
+
|
|
3972
|
+
function formatObjectArray(items, objectType, compact = false) {
|
|
3973
|
+
if (!items.length) return '<span class="muted">None</span>';
|
|
3974
|
+
const schema = state.model.objectTypes[objectType];
|
|
3975
|
+
if (!schema) return '<span class="muted">' + items.length + ' ' + pluralize("item", items.length) + '</span>';
|
|
3976
|
+
return '<div class="object-value-list ' + (compact ? "compact" : "") + '">' + items.map((item) => {
|
|
3977
|
+
const notes = [];
|
|
3978
|
+
const facts = Object.entries(schema.properties || {}).flatMap(([name, property]) => {
|
|
3979
|
+
const value = item[name];
|
|
3980
|
+
if (value === undefined || value === null || value === "" || Array.isArray(value) && !value.length) return [];
|
|
3981
|
+
if (["rationale", "description", "notes"].includes(name)) {
|
|
3982
|
+
notes.push(String(value));
|
|
3983
|
+
return [];
|
|
3984
|
+
}
|
|
3985
|
+
const label = humanize(name.replace(/Ids?$/, ""));
|
|
3986
|
+
let display;
|
|
3987
|
+
if (property.relation && property.type === "id") display = formatReference(value);
|
|
3988
|
+
else if (property.relation && property.type === "array") display = value.map((id) => formatReference(id)).join(" ");
|
|
3989
|
+
else if (Array.isArray(value)) display = esc(value.map((entry) => properCase(entry)).join(", "));
|
|
3990
|
+
else if (property.type === "enum") display = esc(properCase(value));
|
|
3991
|
+
else if (property.type === "date") display = esc(formatCalendarDate(value));
|
|
3992
|
+
else display = esc(String(value));
|
|
3993
|
+
return ['<span><b>' + esc(label) + '</b>' + display + '</span>'];
|
|
3994
|
+
});
|
|
3995
|
+
return '<div class="object-value"><span class="object-value-facts">' + facts.join("") + '</span>' + (!compact && notes.length ? '<small>' + esc(notes.join(" ")) + '</small>' : "") + '</div>';
|
|
3996
|
+
}).join("") + '</div>';
|
|
3997
|
+
}
|
|
3556
3998
|
function controlOperationTracking(control) {
|
|
3557
3999
|
const obligations = resourcesOfType("obligation")
|
|
3558
4000
|
.map(({ record }) => record)
|
|
@@ -3636,13 +4078,97 @@ function pluralize(noun, count) {
|
|
|
3636
4078
|
if (/[^aeiou]y$/i.test(noun)) return noun.slice(0, -1) + "ies";
|
|
3637
4079
|
return noun + "s";
|
|
3638
4080
|
}
|
|
3639
|
-
function renderNotFound(main) {
|
|
4081
|
+
function renderNotFound(main) {
|
|
4082
|
+
const route = parseRoute();
|
|
4083
|
+
const definition = route.type ? state.model.resources[route.type] : null;
|
|
4084
|
+
const recordMissing = route.name === "detail" && definition;
|
|
4085
|
+
const title = recordMissing ? definition.title + " Not Found" : "Page Not Found";
|
|
4086
|
+
const message = recordMissing ? "This record may have been renamed or deleted." : "This page does not exist.";
|
|
4087
|
+
const collectionLink = recordMissing
|
|
4088
|
+
? '<a class="button primary" href="#/resources/' + encodeURIComponent(route.type) + '">Back to ' + esc(titleCase(definition.pluralTitle)) + '</a>'
|
|
4089
|
+
: "";
|
|
4090
|
+
main.innerHTML = '<div class="page"><section class="panel not-found"><p class="kicker">Not found</p><h2>' + esc(title) + '</h2><p>' + esc(message) + '</p><div class="page-actions">' + collectionLink + '<a class="button ' + (recordMissing ? "" : "primary") + '" href="#/">Program overview</a></div></section></div>';
|
|
4091
|
+
}
|
|
3640
4092
|
function applyMutationState(result) {
|
|
3641
|
-
if (
|
|
3642
|
-
|
|
4093
|
+
if (result?.state) {
|
|
4094
|
+
state = result.state;
|
|
4095
|
+
} else if (result?.stateRefresh) {
|
|
4096
|
+
applyFastMutationPatch(result);
|
|
4097
|
+
scheduleMutationStateRefresh();
|
|
4098
|
+
} else {
|
|
4099
|
+
throw new Error("The save response did not include the current workspace state.");
|
|
4100
|
+
}
|
|
3643
4101
|
scheduleRepositorySyncPoll(result.synchronization);
|
|
3644
4102
|
}
|
|
3645
4103
|
|
|
4104
|
+
function applyFastMutationPatch(result) {
|
|
4105
|
+
if (result.operation === "collection-review" && result.assessment?.resourceType) {
|
|
4106
|
+
state.collectionReviews[result.assessment.resourceType] = result.assessment;
|
|
4107
|
+
}
|
|
4108
|
+
for (const record of result.changes?.update || []) {
|
|
4109
|
+
const entry = state.resources.find(({ record: current }) => current.id === record.id);
|
|
4110
|
+
if (entry) entry.record = record;
|
|
4111
|
+
}
|
|
4112
|
+
for (const record of result.changes?.create || []) {
|
|
4113
|
+
if (!state.resources.some(({ record: current }) => current.id === record.id)) {
|
|
4114
|
+
state.resources.push({ record, content: {}, history: [], detailsLoaded: false });
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
if (result.synchronization) {
|
|
4118
|
+
state.repository = {
|
|
4119
|
+
...state.repository,
|
|
4120
|
+
status: result.synchronization.status,
|
|
4121
|
+
label: result.synchronization.status === "syncing" ? "Syncing" : state.repository.label,
|
|
4122
|
+
currentCommit: result.synchronization.commit || state.repository.currentCommit,
|
|
4123
|
+
writesAllowed: result.synchronization.status !== "syncing",
|
|
4124
|
+
backgroundSynchronization: result.synchronization.status === "syncing" ? result.synchronization : null
|
|
4125
|
+
};
|
|
4126
|
+
}
|
|
4127
|
+
state.readOnly = true;
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
function scheduleMutationStateRefresh(delay = 0) {
|
|
4131
|
+
if (mutationStateRefreshTimer || mutationStateRefreshInFlight) return;
|
|
4132
|
+
mutationStateRefreshTimer = setTimeout(refreshMutationState, delay);
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
async function refreshMutationState() {
|
|
4136
|
+
mutationStateRefreshTimer = null;
|
|
4137
|
+
if (mutationStateRefreshInFlight) return;
|
|
4138
|
+
mutationStateRefreshInFlight = true;
|
|
4139
|
+
let retry = false;
|
|
4140
|
+
try {
|
|
4141
|
+
const response = await fetch("/api/state");
|
|
4142
|
+
if (!response.ok) throw new Error(await responseMessage(response));
|
|
4143
|
+
state = await response.json();
|
|
4144
|
+
render();
|
|
4145
|
+
scheduleRepositorySyncPoll();
|
|
4146
|
+
} catch {
|
|
4147
|
+
retry = true;
|
|
4148
|
+
} finally {
|
|
4149
|
+
mutationStateRefreshInFlight = false;
|
|
4150
|
+
}
|
|
4151
|
+
if (retry) scheduleMutationStateRefresh(1_000);
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
function prefetchRepositoryForReview(status) {
|
|
4155
|
+
if (state.repository?.mode !== "trunk" || state.repository?.developmentOverride) {
|
|
4156
|
+
return Promise.resolve(null);
|
|
4157
|
+
}
|
|
4158
|
+
status.textContent = "Checking repository…";
|
|
4159
|
+
return fetch("/api/git/prefetch", { method: "POST" })
|
|
4160
|
+
.then(async (response) => {
|
|
4161
|
+
if (!response.ok) throw new Error(await responseMessage(response));
|
|
4162
|
+
const result = await response.json();
|
|
4163
|
+
status.textContent = result.status === "checked" ? "Repository checked" : "Ready to save";
|
|
4164
|
+
return result;
|
|
4165
|
+
})
|
|
4166
|
+
.catch(() => {
|
|
4167
|
+
status.textContent = "Repository will be checked when you save";
|
|
4168
|
+
return null;
|
|
4169
|
+
});
|
|
4170
|
+
}
|
|
4171
|
+
|
|
3646
4172
|
function scheduleRepositorySyncPoll(synchronization = state.repository?.backgroundSynchronization) {
|
|
3647
4173
|
const syncing = synchronization?.status === "syncing"
|
|
3648
4174
|
|| state.repository?.status === "syncing";
|
|
@@ -3717,6 +4243,30 @@ function showError(message) {
|
|
|
3717
4243
|
dialog.querySelectorAll("button").forEach((button) => button.addEventListener("click", () => dialog.close()));
|
|
3718
4244
|
dialog.addEventListener("close", () => dialog.remove());
|
|
3719
4245
|
}
|
|
4246
|
+
|
|
4247
|
+
function confirmAction({ kicker, title, message, confirmLabel = "Confirm", danger = false }) {
|
|
4248
|
+
const dialog = document.createElement("dialog");
|
|
4249
|
+
dialog.className = "alert-dialog confirmation-dialog";
|
|
4250
|
+
dialog.setAttribute("aria-labelledby", "confirmation-dialog-title");
|
|
4251
|
+
dialog.setAttribute("aria-describedby", "confirmation-dialog-message");
|
|
4252
|
+
dialog.innerHTML = '<form><div class="dialog-head"><div><p class="kicker">' + esc(kicker) + '</p><h2 id="confirmation-dialog-title">' + esc(title) + '</h2></div><button type="button" class="icon-button" data-confirm-dismiss aria-label="Close">×</button></div><p id="confirmation-dialog-message">' + esc(message) + '</p><div class="dialog-actions"><button type="button" class="button" data-confirm-dismiss>Cancel</button><button type="submit" class="button ' + (danger ? "danger-action" : "primary") + '">' + esc(confirmLabel) + '</button></div></form>';
|
|
4253
|
+
document.body.append(dialog);
|
|
4254
|
+
return new Promise((resolve) => {
|
|
4255
|
+
let confirmed = false;
|
|
4256
|
+
dialog.querySelector("form").addEventListener("submit", (event) => {
|
|
4257
|
+
event.preventDefault();
|
|
4258
|
+
confirmed = true;
|
|
4259
|
+
dialog.close();
|
|
4260
|
+
});
|
|
4261
|
+
dialog.querySelectorAll("[data-confirm-dismiss]").forEach((button) => button.addEventListener("click", () => dialog.close()));
|
|
4262
|
+
dialog.addEventListener("close", () => {
|
|
4263
|
+
dialog.remove();
|
|
4264
|
+
resolve(confirmed);
|
|
4265
|
+
}, { once: true });
|
|
4266
|
+
dialog.showModal();
|
|
4267
|
+
});
|
|
4268
|
+
}
|
|
4269
|
+
|
|
3720
4270
|
async function responseMessage(response) {
|
|
3721
4271
|
const source = await response.text();
|
|
3722
4272
|
try { return JSON.parse(source).error || source; } catch { return source; }
|
|
@@ -3725,7 +4275,7 @@ async function localFetch(url, options) {
|
|
|
3725
4275
|
const method = String(options?.method || "GET").toUpperCase();
|
|
3726
4276
|
const synchronizing = state?.repository?.mode === "trunk"
|
|
3727
4277
|
&& ["POST", "PUT", "DELETE"].includes(method)
|
|
3728
|
-
&&
|
|
4278
|
+
&& !["/api/evidence-packet", "/api/git/prefetch"].includes(url);
|
|
3729
4279
|
const chip = synchronizing ? document.querySelector(".repo-chip") : null;
|
|
3730
4280
|
const previousChip = chip?.innerHTML;
|
|
3731
4281
|
let repositoryRefreshed = false;
|
|
@@ -3770,7 +4320,7 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
|
|
|
3770
4320
|
.nav-stage>.nav-items>a.nav-direct{display:grid;grid-template-columns:minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;width:100%;padding:6px 6px 6px 7px;font-size:13.2px}
|
|
3771
4321
|
.nav-heading-row{display:grid;grid-template-columns:minmax(0,1fr) 24px;gap:2px;align-items:stretch}.nav-heading-row>.nav-heading{display:grid;grid-template-columns:24px minmax(0,1fr);gap:6px;align-items:center;width:100%;padding:7px 6px;border-radius:7px;color:#d5d9ed;text-align:left;text-transform:none;letter-spacing:0;text-decoration:none}.nav-heading-row>.nav-heading:hover,.nav-heading-row>.nav-heading.current{background:rgba(255,255,255,.1);color:#fff}.nav-toggle{display:grid;place-items:center;width:24px;height:auto;min-height:100%;padding:0;border:0;border-radius:6px;background:none;color:#aeb6d8;cursor:pointer}.nav-toggle:hover{background:rgba(255,255,255,.1);color:#fff}.nav-chevron{display:block;width:12px;height:12px;place-self:center;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;transition:transform .15s}.nav-subheading-row{display:grid;grid-template-columns:minmax(0,1fr) 22px;gap:2px;align-items:center;width:100%;padding:0;border:0;border-radius:6px;background:none;color:#919bc4;cursor:pointer}.nav-subheading-row:hover{background:rgba(255,255,255,.1);color:#fff}.nav-subheading-row>.nav-subheading{display:flex;align-items:center;min-width:0;padding:7px;color:inherit;text-align:left;text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780}.nav-group.open>.nav-heading-row .nav-chevron,.nav-group.open>.nav-subheading-row .nav-chevron{transform:rotate(90deg)}
|
|
3772
4322
|
.stage-overview-hero{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:28px;align-items:center;padding:26px 28px;background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.stage-overview-hero h2,.group-overview-head h2{font:500 37.2px Georgia,serif;margin:7px 0}.stage-overview-hero>div>p:not(.kicker),.group-overview-head>div>p:not(.kicker){max-width:760px;color:var(--muted);font-size:14.4px;line-height:1.55;margin:0}.stage-progress-card{padding:15px 17px;background:var(--paper);border:1px solid var(--line);border-radius:9px}.stage-progress-card>div:first-child{display:flex;align-items:center;justify-content:space-between;margin-bottom:11px}.stage-progress-card>div:first-child>strong{font:500 33.6px Georgia,serif}.stage-progress-card p{color:var(--muted);font-size:10.8px;line-height:1.45;margin:9px 0 0}.badge.neutral{background:#e5e8f2;color:#555e73}.stage-overview-layout{display:grid;grid-template-columns:320px minmax(0,1fr);gap:15px;margin-top:15px;align-items:start}.stage-plan ol,.group-plan ol{display:grid;gap:12px;padding-left:20px;margin:0}.stage-plan li,.group-plan li{padding-left:4px;color:var(--ink);font-size:13.2px;line-height:1.5}.stage-groups{min-width:0}.stage-groups>.section-head{margin:4px 0 13px}.stage-group-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-group-card{position:relative;display:block;min-height:138px;padding:18px 38px 17px 18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;text-decoration:none;box-shadow:0 2px 8px rgba(21,40,33,.025)}.stage-group-card:hover{border-color:var(--accent-light)}.stage-group-card h3{font-size:15.6px;margin:0 0 7px}.stage-group-card p{color:var(--muted);font-size:12px;line-height:1.5;margin:0}.stage-group-card small{display:block;color:var(--accent);font-size:9.6px;font-weight:700;margin-top:12px}.stage-group-arrow{position:absolute;right:16px;top:16px;color:var(--accent);font-size:24px}.group-overview-head{display:flex;justify-content:space-between;align-items:end;gap:25px;margin-bottom:15px}.stage-status-link{display:grid;grid-template-columns:auto auto;align-items:center;gap:4px 12px;min-width:155px;padding:12px 14px;background:var(--panel);border:1px solid var(--line);border-radius:9px;text-decoration:none}.stage-status-link>strong{font:500 28.8px Georgia,serif;text-align:right}.stage-status-link>small{grid-column:1/-1;color:var(--muted);font-size:9.6px;text-align:right}.relationship-note{display:grid;grid-template-columns:250px minmax(0,1fr);gap:20px;align-items:center;margin-bottom:15px;padding:17px 20px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.relationship-note h3{font-size:15.6px;margin:5px 0 0}.relationship-note>p{color:var(--muted);font-size:12px;line-height:1.55;margin:0}.relationship-note code{font-size:10.8px}.group-plan{margin-bottom:24px}.group-related-links{display:flex;align-items:center;gap:8px;margin-top:18px;padding-top:14px;border-top:1px solid var(--line)}.group-related-links>span{color:var(--muted);font-size:10.8px;margin-right:auto}.group-destinations>.section-head{margin-bottom:12px}.group-destination-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.group-destination-card{display:grid;grid-template-columns:minmax(0,1fr) 100px;gap:16px;min-height:150px;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;text-decoration:none;box-shadow:0 2px 8px rgba(21,40,33,.025)}.group-destination-card:hover{border-color:var(--accent-light)}.group-destination-card h3{font-size:15.6px;margin:5px 0 7px}.group-destination-card p:not(.kicker){color:var(--muted);font-size:10.8px;line-height:1.5;margin:0}.destination-rollup{align-self:center;text-align:right}.destination-rollup strong,.destination-rollup small{display:block}.destination-rollup strong{font:500 32.4px Georgia,serif}.destination-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:3px}
|
|
3773
|
-
.collection-review-panel{margin:16px 0 22px}.collection-review-panel.required{border-color:#d8bd78}.collection-review-panel.current{border-color:#b9dac6}.collection-review-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px}.collection-review-head h3{margin:4px 0 6px}.collection-review-head p:not(.kicker){max-width:900px;margin:0;color:var(--muted);font-size:11px;line-height:1.5}.collection-review-panel details{margin-top:13px;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-panel summary{cursor:pointer;font-size:11px;font-weight:750}.collection-review-panel ul,.collection-review-checks ul{margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.55}.collection-review-foot{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:13px}.collection-review-result{display:flex;align-items:baseline;gap:8px;margin:0}.collection-review-result strong{font-size:11px}.collection-review-result span{color:var(--muted);font-size:10.4px}.collection-review-checks{margin:13px 0;padding:11px 13px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-checks>strong{font-size:11px}.event-dialog-steps.collection-review-checks{margin:15px 0 0;padding:10px;border:0}.collection-review-dialog textarea{box-sizing:border-box;width:100%;resize:vertical}.resource-review-criteria{margin:16px 0 22px;padding:13px 15px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.resource-review-criteria>strong{font-size:11px}.resource-review-criteria ul{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px 28px;margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.5}.commit-dialog .resource-review-criteria{margin:12px 0;background:var(--surface-soft)}.record-workflow-action{display:grid;grid-template-columns:auto minmax(140px,1fr);gap:7px;align-items:start;min-width:240px;text-decoration:none}.record-workflow-action strong,.record-workflow-action small{display:block}.record-workflow-action strong{font-size:10.4px}.record-workflow-action small{margin-top:2px;color:var(--muted);font-size:9.2px;line-height:1.35}.record-workflow-clear{color:var(--muted);font-size:10px;white-space:nowrap}.workflow-guidance{margin:16px 0 22px}.workflow-guidance .panel-head{align-items:flex-start;margin-bottom:12px}.workflow-guidance .panel-head h3{margin:4px 0}.workflow-guidance .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:11px}.workflow-findings{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.workflow-findings>a,.workflow-findings>div{display:grid;grid-template-columns:auto minmax(0,1fr);gap:9px;align-items:start;padding:10px 11px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.workflow-findings strong,.workflow-findings small{display:block}.workflow-findings strong{font-size:11px}.workflow-findings small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.workflow-finding-status{min-width:62px;padding:3px 5px;border-radius:99px;background:var(--accent-soft);color:var(--accent);font-size:8.4px;font-weight:750;text-align:center;text-transform:uppercase;letter-spacing:.04em}.workflow-finding-status.overdue,.workflow-finding-status.blocked{background:#f5ded9;color:#8d352c}.workflow-finding-status.ready,.workflow-finding-status.due,.workflow-finding-status.open{background:#f7e9cf;color:#855717}.workflow-finding-status.complete{background:#ddefe5;color:#176143}.workflow-guidance-more{margin:10px 0 0;color:var(--muted);font-size:9.6px}.context-workflow{display:flex;align-items:center;justify-content:space-between;gap:24px;margin:-8px 0 18px;padding:17px 19px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.context-workflow h3{font-size:16px;margin:4px 0 5px}.context-workflow p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.5;margin:0;max-width:850px}.context-workflow .button{white-space:nowrap}.evidence-map{display:grid;gap:12px;margin-top:18px}.evidence-map-head{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px 22px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:11px}.evidence-map-head>div:first-child{max-width:760px}.evidence-map-head h2{font:500 24px Georgia,serif;margin:5px 0 7px}.evidence-map-head p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.55;margin:0}.evidence-map-actions{display:flex;gap:8px}.evidence-map-card{padding:18px 20px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.evidence-map-card.complete{border-color:#b9dac6}.evidence-map-card-head{display:flex;align-items:start;justify-content:space-between;gap:20px}.evidence-map-card-head h3{font-size:16px;margin:7px 0 0}.evidence-map-card-head>small{color:var(--muted);font-size:10px;text-align:right}.evidence-map-card>p{color:var(--muted);font-size:12px;line-height:1.55;margin:12px 0}.evidence-map-expectation{display:grid;grid-template-columns:120px minmax(0,1fr);gap:12px;padding:8px 0;border-top:1px solid var(--line);font-size:11px;line-height:1.5}.evidence-map-expectation strong{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.06em}.evidence-map-expectation code{background:var(--paper);border-radius:4px;padding:2px 5px}.evidence-map-links{display:grid;grid-template-columns:minmax(220px,1fr) minmax(280px,1.2fr);gap:20px;margin-top:13px;padding-top:13px;border-top:1px solid var(--line)}.evidence-map-links>div>small{display:block;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:7px}.evidence-map-references,.evidence-map-sources{display:flex;flex-wrap:wrap;gap:5px}.evidence-map-source{display:flex;align-items:center;gap:7px;padding:7px 9px;background:var(--paper);border:1px solid var(--line);border-radius:7px;font-size:11px;text-decoration:none}.evidence-map-source.complete{border-color:#b9dac6;background:#edf7f1}.evidence-map-source small{color:var(--muted);font-size:9px}.evidence-map-status{padding:9px 11px;background:var(--paper);border-radius:7px}.evidence-map-empty{padding:24px;background:var(--panel);border:1px solid var(--line);border-radius:10px}.evidence-map-empty h3{margin:6px 0}.evidence-map-empty p:not(.kicker){color:var(--muted);font-size:12px;margin:0 0 14px}.stage-pages{margin-top:24px}.stage-page-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-page-card{position:relative;display:flex;flex-direction:column;min-width:0;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025);transition:border-color .15s,box-shadow .15s}.stage-page-card:hover{border-color:var(--accent-light);box-shadow:0 5px 16px rgba(21,40,33,.07)}.stage-page-card.complete{border-color:#b9dac6}.stage-page-card-link{position:absolute;inset:0;z-index:1;border-radius:10px}.stage-page-card-link:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.stage-page-card-head{display:flex;align-items:center;justify-content:space-between;gap:18px}.stage-page-card-head h3{font-size:15.6px;line-height:1.35;margin:4px 0 0}.stage-page-card-head>div>small{display:block;color:var(--accent);font-size:9.6px;font-weight:700}.stage-page-card>p{color:var(--muted);font-size:12px;line-height:1.5;margin:13px 0 0}.stage-page-tasks{position:relative;z-index:2;display:grid;gap:6px;margin-top:13px}.stage-page-tasks>a{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px;align-items:start;padding:9px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.stage-page-tasks>a:hover{border-color:var(--accent-light)}.stage-page-tasks strong,.stage-page-tasks small{display:block}.stage-page-tasks strong{font-size:10.8px}.stage-page-tasks small{margin-top:2px;color:var(--muted);font-size:9.4px;line-height:1.35}.stage-page-tasks-more{color:var(--muted);font-size:9.4px}.stage-page-rollup{display:flex;flex:0 0 104px;flex-direction:column;justify-content:center;text-align:right}.stage-page-rollup strong,.stage-page-rollup small{display:block}.stage-page-rollup strong{font:500 24px Georgia,serif}.stage-page-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:2px}.stage-page-card-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto;padding-top:15px}.stage-page-completion-state{color:var(--muted);font-size:10.8px}.stage-page-completion-state.complete{color:#176143;font-weight:700}.stage-page-open{color:var(--accent);font-size:12px;font-weight:700}.work-queue-section{margin-top:28px}.work-queue-section>.section-head{align-items:end}
|
|
4323
|
+
.collection-review-panel{margin:16px 0 22px}.collection-review-panel.required{border-color:#d8bd78}.collection-review-panel.current{border-color:#b9dac6}.collection-review-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px}.collection-review-head h3{margin:4px 0 6px}.collection-review-head p:not(.kicker){max-width:900px;margin:0;color:var(--muted);font-size:11px;line-height:1.5}.collection-review-panel details{margin-top:13px;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-panel summary{cursor:pointer;font-size:11px;font-weight:750}.collection-review-panel ul,.collection-review-checks ul{margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.55}.collection-review-foot{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:13px}.collection-review-result{display:flex;align-items:baseline;gap:8px;margin:0}.collection-review-result strong{font-size:11px}.collection-review-result span{color:var(--muted);font-size:10.4px}.collection-review-checks{margin:13px 0;padding:11px 13px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-checks>strong{font-size:11px}.event-dialog-steps.collection-review-checks{margin:15px 0 0;padding:10px;border:0}.collection-review-dialog textarea{box-sizing:border-box;width:100%;resize:vertical}.review-save-status{margin-right:auto;color:var(--muted);font-size:11px}.resource-review-criteria{margin:16px 0 22px;padding:13px 15px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.resource-review-criteria>strong{font-size:11px}.resource-review-criteria ul{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px 28px;margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.5}.commit-dialog .resource-review-criteria{margin:12px 0;background:var(--surface-soft)}.record-workflow-action{display:grid;grid-template-columns:auto minmax(140px,1fr);gap:7px;align-items:start;min-width:240px;text-decoration:none}.record-workflow-action strong,.record-workflow-action small{display:block}.record-workflow-action strong{font-size:10.4px}.record-workflow-action small{margin-top:2px;color:var(--muted);font-size:9.2px;line-height:1.35}.record-workflow-clear{color:var(--muted);font-size:10px;white-space:nowrap}.workflow-guidance{margin:16px 0 22px}.workflow-guidance .panel-head{align-items:flex-start;margin-bottom:12px}.workflow-guidance .panel-head h3{margin:4px 0}.workflow-guidance .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:11px}.workflow-findings{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.workflow-findings>a,.workflow-findings>div{display:grid;grid-template-columns:auto minmax(0,1fr);gap:9px;align-items:start;padding:10px 11px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.workflow-findings strong,.workflow-findings small{display:block}.workflow-findings strong{font-size:11px}.workflow-findings small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.workflow-finding-status{min-width:62px;padding:3px 5px;border-radius:99px;background:var(--accent-soft);color:var(--accent);font-size:8.4px;font-weight:750;text-align:center;text-transform:uppercase;letter-spacing:.04em}.workflow-finding-status.overdue,.workflow-finding-status.blocked{background:#f5ded9;color:#8d352c}.workflow-finding-status.ready,.workflow-finding-status.due,.workflow-finding-status.open{background:#f7e9cf;color:#855717}.workflow-finding-status.complete{background:#ddefe5;color:#176143}.workflow-guidance-more{margin:10px 0 0;color:var(--muted);font-size:9.6px}.context-workflow{display:flex;align-items:center;justify-content:space-between;gap:24px;margin:-8px 0 18px;padding:17px 19px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.context-workflow h3{font-size:16px;margin:4px 0 5px}.context-workflow p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.5;margin:0;max-width:850px}.context-workflow .button{white-space:nowrap}.evidence-map{display:grid;gap:12px;margin-top:18px}.evidence-map-head{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px 22px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:11px}.evidence-map-head>div:first-child{max-width:760px}.evidence-map-head h2{font:500 24px Georgia,serif;margin:5px 0 7px}.evidence-map-head p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.55;margin:0}.evidence-map-actions{display:flex;gap:8px}.evidence-map-card{padding:18px 20px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.evidence-map-card.complete{border-color:#b9dac6}.evidence-map-card-head{display:flex;align-items:start;justify-content:space-between;gap:20px}.evidence-map-card-head h3{font-size:16px;margin:7px 0 0}.evidence-map-card-head>small{color:var(--muted);font-size:10px;text-align:right}.evidence-map-card>p{color:var(--muted);font-size:12px;line-height:1.55;margin:12px 0}.evidence-map-expectation{display:grid;grid-template-columns:120px minmax(0,1fr);gap:12px;padding:8px 0;border-top:1px solid var(--line);font-size:11px;line-height:1.5}.evidence-map-expectation strong{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.06em}.evidence-map-expectation code{background:var(--paper);border-radius:4px;padding:2px 5px}.evidence-map-links{display:grid;grid-template-columns:minmax(220px,1fr) minmax(280px,1.2fr);gap:20px;margin-top:13px;padding-top:13px;border-top:1px solid var(--line)}.evidence-map-links>div>small{display:block;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:7px}.evidence-map-references,.evidence-map-sources{display:flex;flex-wrap:wrap;gap:5px}.evidence-map-source{display:flex;align-items:center;gap:7px;padding:7px 9px;background:var(--paper);border:1px solid var(--line);border-radius:7px;font-size:11px;text-decoration:none}.evidence-map-source.complete{border-color:#b9dac6;background:#edf7f1}.evidence-map-source small{color:var(--muted);font-size:9px}.evidence-map-status{padding:9px 11px;background:var(--paper);border-radius:7px}.evidence-map-empty{padding:24px;background:var(--panel);border:1px solid var(--line);border-radius:10px}.evidence-map-empty h3{margin:6px 0}.evidence-map-empty p:not(.kicker){color:var(--muted);font-size:12px;margin:0 0 14px}.stage-pages{margin-top:24px}.stage-page-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-page-card{position:relative;display:flex;flex-direction:column;min-width:0;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025);transition:border-color .15s,box-shadow .15s}.stage-page-card:hover{border-color:var(--accent-light);box-shadow:0 5px 16px rgba(21,40,33,.07)}.stage-page-card.complete{border-color:#b9dac6}.stage-page-card-link{position:absolute;inset:0;z-index:1;border-radius:10px}.stage-page-card-link:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.stage-page-card-head{display:flex;align-items:center;justify-content:space-between;gap:18px}.stage-page-card-head h3{font-size:15.6px;line-height:1.35;margin:4px 0 0}.stage-page-card-head>div>small{display:block;color:var(--accent);font-size:9.6px;font-weight:700}.stage-page-card>p{color:var(--muted);font-size:12px;line-height:1.5;margin:13px 0 0}.stage-page-tasks{position:relative;z-index:2;display:grid;gap:6px;margin-top:13px}.stage-page-tasks>a{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px;align-items:start;padding:9px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.stage-page-tasks>a:hover{border-color:var(--accent-light)}.stage-page-tasks strong,.stage-page-tasks small{display:block}.stage-page-tasks strong{font-size:10.8px}.stage-page-tasks small{margin-top:2px;color:var(--muted);font-size:9.4px;line-height:1.35}.stage-page-tasks-more{color:var(--muted);font-size:9.4px}.stage-page-rollup{display:flex;flex:0 0 104px;flex-direction:column;justify-content:center;text-align:right}.stage-page-rollup strong,.stage-page-rollup small{display:block}.stage-page-rollup strong{font:500 24px Georgia,serif}.stage-page-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:2px}.stage-page-card-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto;padding-top:15px}.stage-page-completion-state{color:var(--muted);font-size:10.8px}.stage-page-completion-state.complete{color:#176143;font-weight:700}.stage-page-open{color:var(--accent);font-size:12px;font-weight:700}.work-queue-section{margin-top:28px}.work-queue-section>.section-head{align-items:end}
|
|
3774
4324
|
.workflow-findings>a:hover{border-color:var(--accent-light);box-shadow:0 3px 9px rgba(21,40,33,.05)}.workflow-findings>a:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.stage-page-card-head{align-items:flex-start}.stage-page-completion-state{flex:0 0 auto;max-width:150px;padding:5px 8px;border-radius:99px;background:#f7e9cf;color:#855717;font-size:9.6px;font-weight:750;line-height:1.25;text-align:right}.stage-page-completion-state.complete{background:#ddefe5;color:#176143}.obligation-card.workflow-target{outline:2px solid var(--focus);outline-offset:3px}.obligation-card-foot{flex-wrap:wrap}.obligation-card-foot .obligation-links{flex:1 1 120px}
|
|
3775
4325
|
.evidence-attachments .panel-head{align-items:flex-start}.evidence-attachments .panel-head h3{margin:0}.evidence-attachments .panel-head p{margin:4px 0 0;color:var(--muted);font-size:10px}.evidence-attachments ul{list-style:none;margin:0;padding:0}.evidence-attachments li{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 0;border-top:1px solid var(--line)}.evidence-attachments li:first-child{border-top:0}.evidence-attachments strong,.evidence-attachments small{display:block}.evidence-attachments strong{font-size:11px}.evidence-attachments small{margin-top:2px;color:var(--muted);font-size:9px;overflow-wrap:anywhere}.evidence-attachments .attachment-empty{display:block;color:var(--muted);font-size:10px;line-height:1.45}.danger-text{color:var(--red)}
|
|
3776
4326
|
.button{text-decoration:none}
|
|
@@ -3780,26 +4330,28 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
|
|
|
3780
4330
|
.overview-grid>.panel{padding:15px}.overview-grid .panel-head{margin-bottom:10px}.overview-grid .audit-progress{gap:7px 20px}.overview-grid .progress-number strong{font-size:31.2px}.overview-grid .audit-engagement{padding:8px 11px}
|
|
3781
4331
|
.nav-close,.nav-scrim{display:none}.pagination{display:flex;align-items:center;justify-content:center;gap:12px;margin-top:14px}.pagination[hidden]{display:none}.page-status{color:var(--muted);font-size:12px;min-width:150px;text-align:center}.button:disabled{cursor:not-allowed;opacity:.45}.search-pagination{padding-top:2px}
|
|
3782
4332
|
.list-tools{flex-wrap:wrap}.list-tools label{min-width:220px}.list-header-tools{flex:1;justify-content:flex-end;margin:0 0 0 28px}.list-header-tools label{flex:1 1 220px;max-width:360px}.list-header-tools select{max-width:190px}.list-header-tools .button{white-space:nowrap}
|
|
3783
|
-
.setup-banner{margin:14px 0;background:#eef1ff;border:1px solid #ccd4ff;border-radius:11px;padding:19px 22px;display:grid;grid-template-columns:1fr
|
|
4333
|
+
.setup-banner{margin:14px 0;background:#eef1ff;border:1px solid #ccd4ff;border-radius:11px;padding:19px 22px;display:grid;grid-template-columns:1fr auto;gap:25px;align-items:center}.setup-banner h3{margin:5px 0 6px;font-size:18px}.setup-banner p:not(.kicker){margin:0;color:var(--muted);font-size:13.2px;line-height:1.5}.setup-banner>.button{justify-self:end}.setup-draft-state{display:flex;align-items:center;justify-content:flex-end;gap:22px}.setup-draft-state span{min-width:100px}.setup-draft-state small,.setup-draft-state strong{display:block}.setup-draft-state small{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.06em}.setup-draft-state strong{margin-top:3px;font-size:12px}.due-list time.overdue{color:var(--red)}.content-label{display:flex;align-items:center;justify-content:space-between;gap:12px}.text-button{border:0;background:none;color:var(--accent);font-size:10.8px;text-transform:uppercase;letter-spacing:.06em;font-weight:750;cursor:pointer;white-space:nowrap}.tag{white-space:normal;overflow-wrap:anywhere;max-width:100%}.editor{max-height:calc(100vh - 30px);overflow:auto}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin:20px 0}.form-field>.field-label,.content-editor-field>span{display:flex;align-items:center;justify-content:space-between;gap:8px;color:#3e4557;font-size:12px;font-weight:700;margin-bottom:6px}.required-mark{font-size:9.6px;color:var(--accent);text-transform:uppercase;letter-spacing:.06em}.form-field input,.form-field select,.editor .form-field textarea{width:100%;height:auto;min-height:40px;border:1px solid var(--line);border-radius:7px;background:#fff;color:var(--ink);padding:9px 10px;font:14.4px/1.4 inherit}.editor .form-field textarea{height:82px}.form-field input[readonly]{background:#eef0f6;color:#5d6475}.form-field>small{display:block;color:#6a7181;font-size:10.8px;margin-top:5px}.checkbox-list{display:grid;gap:5px;max-height:145px;overflow:auto;border:1px solid var(--line);border-radius:7px;padding:7px}.checkbox-list label{display:flex;align-items:center;gap:8px;padding:5px;border-radius:5px}.checkbox-list input{width:16px;min-height:16px;padding:0;flex:0 0 auto}.checkbox-list label:hover{background:#f2f4fa}.checkbox-list span,.checkbox-list small{display:block;font-size:12px}.checkbox-list small{color:var(--muted);margin-top:2px}.choice-picker{border:1px solid var(--line);border-radius:7px;background:var(--field)}.choice-picker summary{min-height:40px;padding:10px;cursor:pointer;font-size:13.2px}.choice-picker .checkbox-list{max-height:220px;border:0;border-top:1px solid var(--line);border-radius:0}.structured-object-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;padding:10px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.structured-object-fields.nested{grid-column:1/-1;background:var(--panel)}.object-property{min-width:0}.object-property-label{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-bottom:4px;font-size:11px;font-weight:700}.string-map-editor,.string-map-items{display:grid;gap:7px}.string-map-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;gap:6px;align-items:center}.object-array-items{display:grid;gap:10px;margin-bottom:8px}.object-array-item{display:grid;gap:9px;border:1px solid var(--line);border-radius:8px;padding:10px}.object-array-item legend{padding:0 5px;color:var(--muted);font-size:11px;font-weight:750}.object-array-item>.structured-object-fields{padding:0;border:0;background:none}.missing-options{padding:11px;border:1px dashed #d7c8a9;background:#fbf5e9;color:#795b23;border-radius:7px;font-size:12px}.content-editor-field{display:block;margin:17px 0}.editor .content-editor-field textarea,.editor .markdown-source{height:260px;background:#10162b;color:#e8ebff;font:13.2px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.advanced-editor{border-top:1px solid var(--line);margin-top:18px;padding-top:13px}.advanced-editor summary{cursor:pointer;color:var(--accent);font-size:13.2px;font-weight:750}.advanced-editor p{font-size:12px;color:var(--muted)}.editor .advanced-editor>textarea{height:320px}.alert-dialog{width:min(520px,calc(100vw - 30px));border:0;border-radius:12px;padding:23px;box-shadow:0 25px 80px rgba(0,0,24,.28)}.alert-dialog>p{font-size:14.4px;line-height:1.55;color:var(--muted)}.metadata dd{overflow-wrap:anywhere}
|
|
3784
4334
|
.record-content-action{display:flex;justify-content:flex-start;margin-top:20px}.record-content-details{border-top:1px solid var(--line);margin-top:18px;padding-top:13px}.record-content-details summary{cursor:pointer;color:var(--accent);font-size:13.2px;font-weight:750}.record-content-details>p{color:var(--muted);font-size:12px}.record-content-editor>span small{color:var(--muted);font-size:10.8px;font-weight:500}
|
|
3785
4335
|
.program-setup{grid-template-columns:minmax(250px,.75fr) minmax(440px,1.4fr)}.setup-steps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.setup-steps a{display:grid;grid-template-columns:24px minmax(0,1fr);gap:9px;align-items:start;padding:10px;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--ink);text-decoration:none}.setup-steps a:hover{border-color:var(--accent-light)}.setup-steps a>span:first-child{display:grid;place-items:center;width:24px;height:24px;border-radius:50%;background:var(--accent-soft);color:var(--accent);font-size:12px}.setup-steps a.done>span:first-child{background:#dcefe4;color:#125733}.setup-steps strong,.setup-steps small{display:block}.setup-steps strong{font-size:12px}.setup-steps small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4;font-weight:500}
|
|
3786
|
-
.readiness-map{margin:14px 0;background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:20px 22px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.readiness-map-head{display:grid;grid-template-columns:minmax(220px,1fr) minmax(320px,420px);gap:28px;align-items:center;margin-bottom:17px}.readiness-map-head h3{font-size:18px;margin:5px 0 0}.readiness-progress-summary{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 14px;align-items:center}.readiness-progress-summary>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:3px
|
|
4336
|
+
.readiness-map{margin:14px 0;background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:20px 22px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.readiness-map-head{display:grid;grid-template-columns:minmax(220px,1fr) minmax(320px,420px);gap:28px;align-items:center;margin-bottom:17px}.readiness-map-head h3{font-size:18px;margin:5px 0 0}.readiness-progress-summary{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 14px;align-items:center}.readiness-progress-summary>div{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:3px 8px;align-items:baseline}.readiness-progress-summary>div>span{color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}.readiness-progress-summary>div>strong{font-size:9.6px;font-weight:700;line-height:1.2}.readiness-progress-summary>div>.badge{font-size:8.4px}.readiness-progress-summary .progress,.readiness-progress-summary small{grid-column:1/-1}.readiness-progress-summary small{color:var(--muted);font-size:9.6px}.readiness-progress-summary>.button{white-space:nowrap}.readiness-flow{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}.readiness-flow a{display:grid;grid-template-columns:23px minmax(0,1fr);column-gap:8px;align-content:start;min-width:0;padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);text-decoration:none}.readiness-flow a:hover{border-color:var(--accent-light);background:var(--accent-soft)}.readiness-flow a>span{grid-row:1/4;display:grid;place-items:center;width:23px;height:23px;border-radius:50%;background:var(--primary-gradient);color:#fff;font-size:9.6px;font-weight:800}.readiness-flow strong{font-size:12px;line-height:1.25}.readiness-flow small{grid-column:2;color:var(--muted);font-size:9.6px;line-height:1.4;margin-top:3px}.readiness-state{grid-column:2;justify-self:start;margin-top:8px;padding:3px 6px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:8.4px;line-height:1.2}.readiness-state.good{background:#dcefe4;color:#125733}.readiness-state.warn{background:#f6e8c9;color:#79500f}.readiness-state.bad{background:#f7dfdc;color:#873027}.audit-engagement{display:grid;grid-template-columns:minmax(210px,1fr) minmax(260px,1.25fr) auto;gap:20px;align-items:center;padding:14px 15px;border-radius:8px;background:var(--surface-soft)}.audit-engagement strong{font-size:13.2px}.audit-engagement p,.audit-engagement li{color:var(--muted);font-size:10.8px;line-height:1.5}.audit-engagement p{margin:5px 0 0}.audit-engagement ul{margin:0;padding-left:18px}.audit-engagement .button{white-space:nowrap;text-decoration:none}.resource-directory{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.resource-directory>section{min-width:0;padding:12px;border-radius:8px;background:var(--surface-soft)}.resource-directory h4{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.resource-directory a{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-top:1px solid var(--line);font-size:10.8px;text-decoration:none}.resource-directory a:first-of-type{border-top:0}.resource-directory a:hover span{color:var(--accent)}.resource-directory a strong{color:var(--muted);font-size:9.6px}.record-prose{max-width:790px}.record-prose section{padding:0 0 20px}.record-prose section+section{padding-top:20px;border-top:1px solid var(--line)}.record-prose h3{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.record-prose p{margin:0;font-size:16.8px;line-height:1.65;white-space:pre-wrap}.connections-panel .panel-head>span{display:grid;place-items:center;min-width:22px;height:22px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:9.6px}.connections{display:grid}.connections a{display:block;padding:9px 0;border-top:1px solid var(--line);text-decoration:none}.connections a:first-child{padding-top:0;border-top:0}.connections strong,.connections small{display:block}.connections strong{font-size:12px}.connections small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.connections a:hover strong{color:var(--accent)}.connections-more{margin:9px 0 0;color:var(--muted);font-size:9.6px;line-height:1.4}.external-source{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;color:var(--accent);text-decoration:none}.external-source span,.external-source strong,.external-source small{display:block}.external-source strong{font-size:12px;line-height:1.35}.external-source small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.35;overflow-wrap:anywhere}.external-source b{font-size:13.2px}.external-source:hover strong{text-decoration:underline}
|
|
3787
4337
|
.page-title-line{display:flex;align-items:center;gap:8px}.guide-trigger{display:grid;place-items:center;width:24px;height:24px;flex:0 0 auto;padding:0;border:1px solid var(--line);border-radius:50%;background:var(--panel);color:var(--muted);cursor:pointer}.guide-trigger svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round}.guide-trigger:hover{border-color:var(--accent-light);color:var(--accent)}.guide-trigger:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.page-guide{display:grid;grid-template-columns:1.05fr 1.25fr 1fr;gap:0;margin:0;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.resource-guide-popover{position:fixed;z-index:40;overflow:auto;box-shadow:0 18px 50px rgba(0,0,24,.24)}.resource-guide-popover[hidden]{display:none}.page-guide>div{padding:14px 16px;border-left:1px solid var(--line);min-width:0}.page-guide>div:first-child{border-left:0}.page-guide>div>span{display:block;color:var(--accent);text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780;margin-bottom:6px}.page-guide p{color:var(--muted);font-size:12px;line-height:1.5;margin:0}.page-guide>.guide-review{grid-column:1/-1;border-top:1px solid var(--line);border-left:0}.guide-review ul{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px 28px;margin:0;padding-left:18px;color:var(--muted);font-size:11px;line-height:1.45}.guide-links{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.guide-links a{color:var(--accent);background:var(--accent-soft);border-radius:99px;padding:4px 7px;text-decoration:none;font-size:9.6px;font-weight:700}
|
|
3788
4338
|
.operation-tracking{display:grid;gap:2px;min-width:0;text-decoration:none}.operation-tracking strong,.operation-tracking small{display:block;overflow-wrap:anywhere}.operation-tracking small{color:var(--muted);line-height:1.35}.operation-tracking.running strong{color:#176143}.operation-tracking.waiting strong,.operation-tracking.mixed strong{color:var(--amber)}.operation-tracking.paused strong{color:var(--red)}a.operation-tracking:hover strong{text-decoration:underline}
|
|
3789
4339
|
.page-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.repository-sync-status{min-height:18px;margin:7px 0 0;color:var(--muted);font-size:13.2px}.repository-sync-status.error{color:var(--red)}.repository-sync-alert{display:flex;align-items:flex-start;gap:9px;padding:10px 22px;border-bottom:1px solid #e9c888;background:#fff8e8;color:var(--ink);font-size:12px;line-height:1.45}.repository-sync-alert.syncing{border-color:var(--line);background:var(--surface-soft)}.repository-sync-alert .status-dot{flex:0 0 auto;margin-top:4px}.repository-sync-alert a{margin-left:auto;white-space:nowrap}.repository-state-banner,.repository-override{display:flex;align-items:flex-start;gap:13px;margin-bottom:14px}.repository-state-banner>.status-dot,.repository-override>.status-dot{margin-top:5px}.repository-state-banner h3{margin:3px 0 5px}.repository-state-banner p:last-child,.repository-override p{margin:0;color:var(--muted);line-height:1.5}.repository-override{padding:14px 17px;border:1px solid #e9c888;border-radius:9px;background:#fff8e8;font-size:13.2px}.repository-override strong{display:block;margin-bottom:4px}.onboarding-dialog{width:min(470px,calc(100vw - 30px));max-height:calc(100vh - 32px);margin:0;border:1px solid var(--line);border-radius:13px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 28px 90px rgba(0,0,24,.38);overflow:hidden}.onboarding-dialog[open]{display:flex;flex-direction:column}.onboarding-dialog::backdrop{background:transparent;backdrop-filter:none}.onboarding-shade{position:fixed;inset:0;z-index:60;pointer-events:none}.onboarding-shade span{position:absolute;background:rgba(0,0,24,.58)}.onboarding-progress{display:grid;flex:0 0 auto;grid-template-columns:repeat(var(--onboarding-step-count),1fr);gap:5px;padding:18px 24px 0}.onboarding-progress span{height:3px;border-radius:3px;background:var(--surface-muted)}.onboarding-progress span.active{background:var(--accent-light)}.onboarding-scroll{min-height:0;overflow-y:auto}.onboarding-head{padding:22px 25px 0}.onboarding-head h2{font-family:Georgia,serif;font-size:30px;font-weight:500;letter-spacing:-.015em;margin:8px 0 0}.onboarding-body{color:var(--muted);font-size:14.4px;line-height:1.6;margin:13px 25px 0}.onboarding-body+.onboarding-body{margin-top:8px}.onboarding-points{display:grid;gap:9px;margin:18px 25px 4px;padding-left:19px}.onboarding-points li{font-size:13.2px;line-height:1.5;padding-left:3px}.onboarding-sections{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 25px 4px}.onboarding-sections section{padding:13px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.onboarding-sections strong{font-size:13.2px}.onboarding-sections p{margin:6px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.onboarding-actions{flex:0 0 auto;flex-wrap:wrap;padding:12px 25px 23px;border-top:1px solid var(--line);background:var(--panel)}.onboarding-skip{margin-right:auto;color:var(--muted);text-transform:none;letter-spacing:0;font-size:13.2px}.onboarding-form{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:18px 25px 0}.onboarding-form label{display:block;min-width:0}.onboarding-form label.wide{grid-column:1/-1}.onboarding-form label>span{display:block;color:var(--ink);font-size:12px;font-weight:720;margin-bottom:6px}.onboarding-form input,.onboarding-form select,.onboarding-form textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.onboarding-form textarea{min-height:78px;resize:vertical}.onboarding-form small{display:block;color:var(--muted);font-size:10.8px;line-height:1.45;margin-top:5px}.onboarding-write-note{color:var(--muted);font-size:10.8px;line-height:1.5;margin:12px 25px 16px}.onboarding-scroll>.dialog-error{margin:8px 25px 0}.onboarding-focus{outline:4px solid var(--accent-light)!important;outline-offset:5px;scroll-margin-top:102px}
|
|
3790
4340
|
.onboarding-save-status{color:var(--muted);font-size:10.8px;line-height:1.35}.onboarding-save-status:empty{display:none}.onboarding-save-status:not(:empty){order:-1;flex-basis:100%;margin-bottom:4px}.onboarding-retry-sync{margin-top:9px}.detail-loading{color:var(--muted)}
|
|
3791
4341
|
.save-status{min-height:16px;color:var(--muted);font-size:10.8px;line-height:1.35}
|
|
3792
4342
|
.page-intro,.detail-head{align-items:center;margin-bottom:12px}.actions{align-items:center}.detail-head>div:first-child{min-width:0}.detail-head h2{margin:7px 0}.detail-head .header-breadcrumbs{margin:0;font-size:10.8px;line-height:normal;min-height:11px;align-items:center}.header-breadcrumbs span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60ch}
|
|
4343
|
+
.not-found{max-width:620px;padding:28px}.not-found h2{font-family:Georgia,serif;font-size:37.2px;font-weight:500;margin:7px 0}.not-found>p:not(.kicker){margin:0;color:var(--muted);font-size:15.6px}.not-found .page-actions{justify-content:flex-start;margin-top:22px}
|
|
4344
|
+
.button.danger-action{background:var(--red);border-color:var(--red);color:#fff}
|
|
3793
4345
|
@media(max-width:1200px){.readiness-flow{grid-template-columns:repeat(3,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr 1fr}.audit-engagement .button{grid-column:1/-1;justify-self:start}}
|
|
3794
4346
|
@media(max-width:1100px){.search{display:none}.topbar-status{margin-left:auto}.metrics{grid-template-columns:repeat(2,1fr)}.dashboard-grid,.organization-grid{grid-template-columns:repeat(2,1fr)}.catalog{grid-template-columns:repeat(3,1fr)}.span-2{grid-column:span 2}.resource-directory{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
3795
|
-
@media(max-width:760px){.shell{display:block}.sidebar{transform:translateX(-100%);transition:.2s;box-shadow:8px 0 30px rgba(0,0,0,.2)}.sidebar.shown{transform:translateX(0)}.workspace{min-width:0}.mobile-nav{display:block;border:0;background:none;font-size:24px}.topbar{height:72px;padding:0 16px}.topbar>div:first-of-type{min-width:0}.topbar-status{display:none}.search{display:flex;max-width:none}.search kbd,.topbar .eyebrow{display:none}.page{padding:20px 15px 60px}.hero{display:block;padding:23px}.hero-meta{margin-top:22px;flex-wrap:wrap}.metrics,.dashboard-grid,.organization-grid{grid-template-columns:1fr}.span-2{grid-column:auto}.catalog{grid-template-columns:repeat(2,1fr)}.detail-grid{grid-template-columns:1fr}.page-intro,.detail-head{display:block}.page-intro>.button,.actions{margin-top:15px}.page-intro>.list-header-tools{justify-content:flex-start;margin:15px 0 0}.list-header-tools label{max-width:none}.record-table{min-width:720px}.readiness-map{padding:17px}.readiness-map-head{grid-template-columns:1fr;gap:8px}.readiness-flow{grid-template-columns:repeat(2,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr}.audit-engagement .button{grid-column:auto}.resource-directory{grid-template-columns:1fr}}
|
|
4347
|
+
@media(max-width:760px){.shell{display:block}.sidebar{transform:translateX(-100%);transition:.2s;box-shadow:8px 0 30px rgba(0,0,0,.2)}.sidebar.shown{transform:translateX(0)}.workspace{min-width:0}.mobile-nav{display:block;border:0;background:none;font-size:24px}.topbar{height:72px;padding:0 16px}.topbar>div:first-of-type{flex:1;min-width:0}.topbar h1{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.topbar-status{display:none}.search{display:flex;flex:0 1 180px;min-width:110px;max-width:none}.search kbd,.topbar .eyebrow{display:none}.page{padding:20px 15px 60px}.hero{display:block;padding:23px}.hero-meta{margin-top:22px;flex-wrap:wrap}.metrics,.dashboard-grid,.organization-grid{grid-template-columns:1fr}.span-2{grid-column:auto}.catalog{grid-template-columns:repeat(2,1fr)}.detail-grid{grid-template-columns:1fr}.page-intro,.detail-head{display:block}.page-intro>.button,.actions{margin-top:15px}.page-intro>.list-header-tools{justify-content:flex-start;margin:15px 0 0}.list-header-tools label{max-width:none}.record-table{min-width:720px}.readiness-map{padding:17px}.readiness-map-head{grid-template-columns:1fr;gap:8px}.readiness-flow{grid-template-columns:repeat(2,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr}.audit-engagement .button{grid-column:auto}.resource-directory{grid-template-columns:1fr}}
|
|
3796
4348
|
@media(max-width:760px){.setup-banner,.page-guide,.stage-overview-hero,.relationship-note,.group-destination-card,.stage-page-grid,.evidence-map-expectation,.evidence-map-links,.external-evidence-list{grid-template-columns:1fr}.context-workflow,.evidence-map-head{align-items:stretch;flex-direction:column}.evidence-map-actions{flex-wrap:wrap}.page-guide>div{border-left:0;border-top:1px solid var(--line)}.page-guide>div:first-child{border-top:0}.group-overview-head{display:block}.stage-progress-card,.stage-status-link{margin-top:15px}.destination-rollup{text-align:left}.form-grid{grid-template-columns:1fr}.record-table{min-width:0}.record-table thead{display:none}.record-table,.record-table tbody,.record-table tr{display:block}.record-table tr{padding:8px 12px;border-bottom:1px solid var(--line)}.record-table tr:last-child{border-bottom:0}.record-table td:not([data-label]){display:block}.record-table td[data-label]{display:grid;grid-template-columns:105px minmax(0,1fr);gap:10px;border:0;padding:7px 0;align-items:start}.record-table td[data-label]::before{content:attr(data-label);color:#75817b;text-transform:uppercase;letter-spacing:.07em;font-size:9.6px;font-weight:700}.record-table td[data-primary-field]{display:block;padding:8px 0 10px}.record-table td[data-primary-field]::before{display:none}.content-label{align-items:flex-start}.editor form{padding:18px}.diagnostics>div{grid-template-columns:58px minmax(0,1fr)}.diagnostics p{grid-column:1/-1}.changes code{overflow-wrap:anywhere}.onboarding-dialog{max-height:56vh}}
|
|
3797
4349
|
@media(max-width:760px){.guide-review ul,.resource-review-criteria ul{grid-template-columns:1fr}.collection-review-head,.collection-review-foot{align-items:stretch;flex-direction:column}.record-workflow-action{min-width:0}}
|
|
3798
4350
|
@media(max-width:520px){.onboarding-form,.onboarding-sections,.setup-steps{grid-template-columns:1fr}.onboarding-form label.wide{grid-column:auto}.onboarding-actions{flex-wrap:wrap}.onboarding-skip{width:100%;order:3;margin:3px 0 0}.readiness-flow{grid-template-columns:1fr}.obligation-card-foot{align-items:flex-start;flex-direction:column}.obligation-action{align-self:flex-start}}
|
|
3799
4351
|
@media(min-width:761px){.detail-grid{grid-template-columns:minmax(270px,1fr) minmax(0,2fr)}.detail-grid aside{grid-column:1;grid-row:1}.detail-main{grid-column:2;grid-row:1}}
|
|
3800
4352
|
@media(min-width:761px){.detail-grid.detail-grid-structured{grid-template-columns:1fr}.detail-grid-structured aside{grid-column:1;grid-row:1;grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}.detail-grid-structured aside>.panel{align-self:start}}
|
|
3801
4353
|
@media(max-width:760px){.sidebar{visibility:hidden;transition:transform .2s,visibility 0s .2s}.sidebar.shown{visibility:visible;transition-delay:0s}.nav-close{display:grid;place-items:center;position:absolute;top:25px;right:18px;width:34px;height:34px;border:1px solid #5966a4;border-radius:50%;background:#11174a;color:#eef1ff;font-size:24px;cursor:pointer}.nav-scrim{display:block;position:fixed;inset:0;border:0;background:rgba(0,0,24,.38);opacity:0;pointer-events:none;transition:opacity .2s;z-index:15}.sidebar.shown+.nav-scrim{opacity:1;pointer-events:auto}.pagination{justify-content:space-between;gap:8px}.page-status{min-width:0}}
|
|
3802
|
-
@media(max-width:760px){.topbar{height:56px}.nav-close{font-size:0}.nav-close:before,.nav-close:after{content:"";position:absolute;width:13px;height:2px;border-radius:2px;background:currentColor;transform:rotate(45deg)}.nav-close:after{transform:rotate(-45deg)}}
|
|
4354
|
+
@media(max-width:760px){.topbar{height:56px}.topbar>div:first-of-type{display:none}.search{flex:1;min-width:0}.nav-close{font-size:0}.nav-close:before,.nav-close:after{content:"";position:absolute;width:13px;height:2px;border-radius:2px;background:currentColor;transform:rotate(45deg)}.nav-close:after{transform:rotate(-45deg)}}
|
|
3803
4355
|
|
|
3804
4356
|
.connection-group+.connection-group{margin-top:15px;padding-top:13px;border-top:1px solid var(--line)}.connection-group h4{margin:0 0 8px;color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.08em}
|
|
3805
4357
|
body,button,input,select,textarea,dialog{color:var(--ink)}
|
|
@@ -3874,6 +4426,8 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
|
|
|
3874
4426
|
.obligation-card-foot{display:flex;align-items:flex-end;justify-content:space-between;gap:9px;margin-top:10px}.obligation-card-foot .obligation-links{margin-top:0;min-width:0}.obligation-action{flex:0 0 auto;border:0;border-radius:6px;background:var(--accent-soft);color:var(--accent);padding:7px 9px;font-family:inherit;font-size:10.8px;font-weight:700;line-height:1;text-decoration:none;cursor:pointer}.obligation-action:hover{filter:brightness(1.08)}.obligation-action.blocked{background:var(--surface-muted);color:var(--muted)}.obligation-more{width:100%;margin-top:9px}.workflow-section{scroll-margin-top:92px}
|
|
3875
4427
|
.event-dialog label{display:block;margin-top:13px}.event-dialog label[hidden]{display:none}.event-dialog label>span{display:block;font-size:12px;font-weight:720;margin-bottom:6px}.event-dialog input,.event-dialog select,.event-dialog textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.event-dialog textarea{resize:vertical}.commit-dialog .form-grid label.full{grid-column:1/-1}.workflow-preview{margin-top:14px;padding:0 12px;border-radius:7px;background:var(--surface-soft)}.workflow-preview:not(:empty){padding-top:10px;padding-bottom:10px}.workflow-preview strong,.workflow-preview p{display:block;margin:0}.workflow-preview p{margin-top:5px;color:var(--muted);font-size:12px;line-height:1.5}.event-dialog-steps{display:grid;gap:6px;margin-top:15px;padding:10px;background:var(--surface-soft);border-radius:7px}.event-dialog-steps strong,.event-dialog-steps small{display:block}.event-dialog-steps strong{font-size:12px}.event-dialog-steps small{font-size:9.6px;color:var(--muted);margin-top:2px}
|
|
3876
4428
|
.applicability-dialog{width:min(980px,calc(100vw - 30px));max-height:calc(100vh - 32px);border:0;border-radius:12px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 25px 80px rgba(0,0,24,.28)}.applicability-dialog form{padding:23px}.applicability-dialog form>p{color:var(--muted);font-size:13.2px}.review-context label>span{display:block;font-size:12px;font-weight:720;margin-bottom:6px}.review-context input,.review-context select,.applicability-row input,.applicability-row select{width:100%;min-height:38px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:8px 9px;font-size:13.2px}.review-context label.full{grid-column:1/-1}.applicability-rows{display:grid;gap:7px;max-height:46vh;overflow:auto;margin-top:16px;padding-right:4px}.applicability-row{display:grid;grid-template-columns:minmax(210px,1fr) 180px minmax(240px,1.3fr);gap:9px;align-items:center;padding:9px;border:1px solid var(--line);border-radius:8px}.applicability-row strong,.applicability-row small{display:block}.applicability-row small{margin-top:3px;color:var(--muted);font-size:10.8px}
|
|
4429
|
+
.object-value-list{display:grid;gap:7px}.object-value{min-width:0;padding:7px 8px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.object-value-facts{display:flex;flex-wrap:wrap;gap:5px 10px}.object-value-facts>span{display:flex;align-items:baseline;gap:5px;min-width:0;font-size:11px}.object-value-facts b{color:var(--muted);font-size:8.8px;text-transform:uppercase;letter-spacing:.05em}.object-value>small{display:block;margin-top:6px;color:var(--muted);font-size:10px;line-height:1.45}.object-value-list.compact .object-value{padding:0;border:0;background:none}.object-value-list.compact .object-value-facts{display:grid;gap:5px}.object-value-list.compact .object-value-facts>span{display:grid;gap:2px;font-size:10px}
|
|
4430
|
+
.resource-review-criteria.compact{margin:12px 0;padding:10px 12px;background:var(--surface-soft)}.resource-review-criteria.compact summary{cursor:pointer;font-size:11px;font-weight:750}.resource-review-criteria.compact ul{margin-bottom:0}.evidence-map-more{justify-self:center;margin:2px 0 8px}
|
|
3877
4431
|
.packet-builder form{display:grid;grid-template-columns:repeat(3,minmax(0,1fr)) auto;gap:12px;align-items:end}.packet-builder label>span{display:block;font-size:10.8px;font-weight:720;margin-bottom:6px}.packet-builder input,.packet-builder select{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:13.2px}.packet-note,.packet-output>p{font-size:12px;color:var(--muted);margin:12px 0 0}.packet-output{margin:14px 0}.packet-output h3{overflow-wrap:anywhere}.packet-gaps{display:grid}.packet-gaps>div{display:grid;grid-template-columns:58px 1fr;gap:10px;border-top:1px solid var(--line);padding:10px 0}.packet-gaps>div:first-child{border-top:0}.packet-gaps p{font-size:12px;margin:0}.packet-list{display:grid}.packet-list a{display:block;text-decoration:none;border-top:1px solid var(--line);padding:9px 0}.packet-list a:first-child{border-top:0}.packet-list strong,.packet-list small{display:block}.packet-list strong{font-size:12px}.packet-list small{font-size:9.6px;color:var(--muted);margin-top:2px}
|
|
3878
4432
|
.packet-preflight{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin-bottom:12px}.packet-preflight a{display:flex;align-items:flex-start;gap:9px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel);text-decoration:none}.packet-preflight .status-dot{margin-top:4px}.packet-preflight small,.packet-preflight strong{display:block}.packet-preflight small{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.07em}.packet-preflight strong{margin-top:3px;font-size:12px}.audit-evidence-paths{margin-bottom:12px}.audit-evidence-paths .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:10.8px}.audit-evidence-path-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:12px}.audit-evidence-path-grid>a{display:block;padding:14px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);text-decoration:none}.audit-evidence-path-grid h4{margin:7px 0 5px;font-size:13.2px}.audit-evidence-path-grid p{margin:0;color:var(--muted);font-size:10.8px;line-height:1.55}
|
|
3879
4433
|
.audit-preparation{margin-bottom:12px}.audit-preparation .panel-head{align-items:flex-start}.audit-preparation .panel-head h3{margin:3px 0}.audit-preparation .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:10.8px}.preparation-progress{height:5px;margin:12px 0 0;border-radius:99px;background:var(--surface-muted);overflow:hidden}.preparation-progress span{display:block;height:100%;border-radius:inherit;background:var(--primary-gradient)}.audit-preparation-note{margin:9px 0 0;color:var(--muted);font-size:10.8px;line-height:1.5}.preparation-stages{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:12px}.preparation-stage{border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);overflow:hidden}.preparation-stage summary{display:flex;justify-content:space-between;gap:12px;align-items:center;padding:11px 12px;cursor:pointer;list-style:none}.preparation-stage summary::-webkit-details-marker{display:none}.preparation-stage summary span,.preparation-stage summary strong,.preparation-stage summary small{display:block}.preparation-stage summary strong{font-size:12px}.preparation-stage summary small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.preparation-stage summary b{flex:none;color:var(--muted);font-size:9.6px;font-weight:650}.preparation-items{border-top:1px solid var(--line);background:var(--panel)}.preparation-items>a,.preparation-items>div{display:grid;grid-template-columns:22px minmax(0,1fr);gap:9px;padding:10px 12px;border-top:1px solid var(--line);text-decoration:none}.preparation-items>:first-child{border-top:0}.preparation-items strong,.preparation-items small{display:block}.preparation-items strong{font-size:10.8px}.preparation-items small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.45}.preparation-status{display:grid;place-items:center;width:20px;height:20px;border-radius:50%;background:var(--surface-muted);color:var(--muted);font-size:10.8px;font-weight:800}.preparation-status.complete{background:#dcefe4;color:#125733}.preparation-status.action{background:#f7dfdc;color:#873027}.preparation-status.later{background:#f6e8c9;color:#79500f}.preparation-status.external,.preparation-status.info{background:var(--accent-soft);color:var(--accent)}.audit-preparation-error:empty{display:none}
|
|
@@ -3883,7 +4437,11 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
|
|
|
3883
4437
|
@media(max-width:900px){.overview-grid{grid-template-columns:1fr}.overview-grid>.audit-panel{grid-column:auto}}
|
|
3884
4438
|
@media(max-width:520px){.event-reminder-preview,.policy-event-list,.packet-builder form,.packet-preflight,.audit-evidence-path-grid{grid-template-columns:1fr}.policy-event-row:nth-child(n) .policy-event-tooltip{right:auto;left:0}.packet-metrics{grid-template-columns:1fr}.obligation-card-head{display:block}.obligation-card-head strong{display:block;text-align:left;margin-top:3px}}
|
|
3885
4439
|
@media(max-width:520px){.policy-event-feedback{grid-template-columns:auto minmax(0,1fr) auto}.policy-event-feedback>.button{grid-column:2}.policy-event-feedback>.icon-button{grid-column:3;grid-row:1}}
|
|
4440
|
+
@media(max-width:520px){.applicability-dialog form{padding:18px}.applicability-row{grid-template-columns:1fr}.applicability-rows{max-height:42vh}}
|
|
3886
4441
|
@media(max-width:760px){.workflow-findings{grid-template-columns:1fr}}
|
|
4442
|
+
.choice-tag{overflow-wrap:normal;font-size:10px}.form-field[data-kind="structured-object"],.string-map-property{grid-column:1/-1}.string-map-row .text-button{text-transform:none;letter-spacing:0;color:var(--muted);font-size:11px}
|
|
4443
|
+
@media(max-width:760px){.setup-banner>.button{justify-self:start}.setup-draft-state{justify-content:flex-start;flex-wrap:wrap;gap:12px 22px}.setup-draft-state .button{width:100%}}
|
|
4444
|
+
@media(max-width:760px){.string-map-row{grid-template-columns:1fr 1fr}.string-map-row .text-button{grid-column:1/-1;justify-self:start}}
|
|
3887
4445
|
|
|
3888
4446
|
@media(prefers-color-scheme:dark){
|
|
3889
4447
|
:root{--ink:#f4f5ff;--muted:#b8bfd3;--line:#343d5c;--paper:#000;--panel:#141a2e;--accent:#aab7ff;--accent-soft:#252e52;--accent-light:#9aabff;--focus:#bdc7ff;--amber:#ffd08a;--red:#ffaaa0;--surface-soft:#1b2238;--surface-muted:#252d48;--field:#11172a;--field-readonly:#1c2338;--shadow:0 12px 34px rgba(0,0,0,.3)}
|