filegrc 0.13.2 → 0.13.4
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/model/v10.json +12 -12
- package/package.json +1 -1
- package/src/audit-preparation.js +6 -0
- package/src/policy-library/data-retention-schedule-v2.md +2 -2
- package/src/policy-library.js +24 -9
- package/src/web.js +215 -87
- package/src/workflow.js +33 -17
package/model/v10.json
CHANGED
|
@@ -1436,9 +1436,9 @@
|
|
|
1436
1436
|
"policyBasis": "Policies rely on governed documents for detailed plans, schedules, procedures, charters, and reports. Approval accepts the intended values and exact content. Activation records that the linked requirements were implemented and the approved Document was put into use.",
|
|
1437
1437
|
"cadence": "Complete required program Documents in Step 2, obtain approval from a reviewer who is separate from the owner, implement their linked requirements in Step 3, then activate the unchanged approved revision. Follow the linked review Obligation after activation. Prepare Audit Documents in Step 5, then approve and activate each one there as separate revision-bound updates.",
|
|
1438
1438
|
"reviewPoints": [
|
|
1439
|
-
"
|
|
1440
|
-
"Replace every bracketed prompt with a reviewed fact before approval, activation,
|
|
1441
|
-
"
|
|
1439
|
+
"Keep companion Markdown to the policy, procedure, evidence, or narrative the Document records. Put FileGRC instructions and derived actions in the UI or guides unless FileGRC is the subject.",
|
|
1440
|
+
"Replace every bracketed prompt with a reviewed fact before approval, activation, signing, or delivery.",
|
|
1441
|
+
"Use ordinary business language; keep resource relationships in this record and its supporting records."
|
|
1442
1442
|
],
|
|
1443
1443
|
"sourceResourceIds": [
|
|
1444
1444
|
"policy-information-security",
|
|
@@ -1771,9 +1771,9 @@
|
|
|
1771
1771
|
"cadence": "Create an Evidence Artifact only for a real retained artifact or approved reference. Identify its source Component or source records, Controls, collection and verification facts, coverage, revision, and classification.",
|
|
1772
1772
|
"emptyState": "No Evidence Artifacts have been collected. Add one only when a real retained artifact or approved external reference exists.",
|
|
1773
1773
|
"reviewPoints": [
|
|
1774
|
-
"
|
|
1775
|
-
"For
|
|
1776
|
-
"
|
|
1774
|
+
"For a signed management representation, use artifactKind signed-record and artifactSubtype signed-management-representation.",
|
|
1775
|
+
"For that representation, record the signing time in businessEventAt; use collectedOn for when FileGRC received it.",
|
|
1776
|
+
"For a final CPA report, use artifactKind third-party-report and artifactSubtype soc2-report, then record its issuance time in sourceGeneratedAt."
|
|
1777
1777
|
],
|
|
1778
1778
|
"sourceResourceIds": [
|
|
1779
1779
|
"policy-information-security"
|
|
@@ -2865,9 +2865,9 @@
|
|
|
2865
2865
|
"policyBasis": "Controls translate approved Policies and applicable Requirements into owned procedures that management can operate and prove. FileGRC does not infer technical implementation from policy prose. Configuration facts belong in Controls, Components, Systems, Obligations, and Evidence.",
|
|
2866
2866
|
"cadence": "A Control may be implemented while its governing Policy, required program Document, or Training is approved but inactive. Before marking it implemented, record its owner, actual procedure in Record Markdown, bounded System scope, operation pattern, authoritative evidence-source Components, implementation date, and enabled calendar or event Obligations. Confirm each source Component is active, has the evidence role required by the Control family and current access owners, and includes repeatable retrieval instructions in Record Markdown. Enabled Obligations remain dormant until all of their governing content is active and effective.",
|
|
2867
2867
|
"reviewPoints": [
|
|
2868
|
-
"Confirm the Control is needed for the applicable Requirements,
|
|
2869
|
-
"
|
|
2870
|
-
"Use
|
|
2868
|
+
"Confirm the Control is needed for the applicable Requirements, Commitments, and service boundary.",
|
|
2869
|
+
"Describe the actual procedure. Put starter-selection and FileGRC record-entry instructions in the UI or guides, unless FileGRC is an operating Component or evidence source.",
|
|
2870
|
+
"Use not applicable, external, or zero population only when it is true for the current scope."
|
|
2871
2871
|
],
|
|
2872
2872
|
"sourceResourceIds": [
|
|
2873
2873
|
"policy-information-security"
|
|
@@ -4389,9 +4389,9 @@
|
|
|
4389
4389
|
"policyBasis": "The information security policy requires security training and added role-specific instruction when a person’s responsibilities or data access warrant it.",
|
|
4390
4390
|
"cadence": "Approve the exact training content in Step 2. During Step 3, finish the linked Controls and Obligations, then activate the unchanged approved revision. Assign training through Obligations at onboarding, annually, and after relevant material changes or incidents.",
|
|
4391
4391
|
"reviewPoints": [
|
|
4392
|
-
"
|
|
4393
|
-
"
|
|
4394
|
-
"
|
|
4392
|
+
"Keep workforce material focused on required behavior. Put FileGRC commands, record-entry instructions, and data-model terms in guides and assignments.",
|
|
4393
|
+
"Do not imply every user needs a paid product or that customer MFA applies without an approved requirement.",
|
|
4394
|
+
"Bind each completion to the exact revision the person reviewed."
|
|
4395
4395
|
],
|
|
4396
4396
|
"sourceResourceIds": [
|
|
4397
4397
|
"policy-information-security"
|
package/package.json
CHANGED
package/src/audit-preparation.js
CHANGED
|
@@ -1599,6 +1599,12 @@ function containsOpenPlaceholder(source) {
|
|
|
1599
1599
|
function managementDocumentContentIssues(source, definition, audit) {
|
|
1600
1600
|
if (!source) return ["Add the required Markdown content."];
|
|
1601
1601
|
if (containsOpenPlaceholder(source)) return ["Replace every bracketed preparation placeholder before approval."];
|
|
1602
|
+
if (definition.kind === "soc2-management-assertion" && audit) {
|
|
1603
|
+
const incompatibleKind = audit.auditKind === "soc-2-type-1" ? "Type 2" : "Type 1";
|
|
1604
|
+
if (new RegExp(`^#{1,6}\\s+${incompatibleKind} Assertion(?: Draft)?\\s*$`, "imu").test(source)) {
|
|
1605
|
+
return [`Remove the ${incompatibleKind} assertion from this ${audit.auditKind === "soc-2-type-1" ? "Type 1" : "Type 2"} engagement document.`];
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1602
1608
|
const words = source.match(/[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu) || [];
|
|
1603
1609
|
if (definition.minimumWords && words.length < definition.minimumWords) {
|
|
1604
1610
|
return [`Add substantive content; this document has ${words.length} words and the preparation check expects at least ${definition.minimumWords}.`];
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Use
|
|
4
4
|
|
|
5
|
-
This schedule records how long {{company_name}} keeps important record classes and what happens when each period ends.
|
|
5
|
+
This schedule records how long {{company_name}} keeps important record classes and what happens when each period ends.
|
|
6
6
|
|
|
7
7
|
Retention periods may come from law, contract, tax, audit, security, or a documented business need. Use the longest applicable period, but do not keep data indefinitely without a reason.
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ Retention periods may come from law, contract, tax, audit, security, or a docume
|
|
|
10
10
|
|
|
11
11
|
The structured Retention Schedule Items linked to this document are its schedule rows. Each approved item must name the covered Information Types and operational scope, owner, cutoff, period, disposition action, instructions, and authority. Planned items are review prompts and are not approved retention behavior.
|
|
12
12
|
|
|
13
|
-
Management must cover important information used by Systems, Components, and Vendors, including security logs, backups or alternate recovery copies, governance records, audit evidence, customer and service records, and incident records when those classes exist.
|
|
13
|
+
Management must cover important information used by Systems, Components, and Vendors, including security logs, backups or alternate recovery copies, governance records, audit evidence, customer and service records, and incident records when those classes exist. A proposed period or disposition action is not an approved organization value.
|
|
14
14
|
|
|
15
15
|
## Holds and exceptions
|
|
16
16
|
|
package/src/policy-library.js
CHANGED
|
@@ -55,9 +55,10 @@ const DOCUMENT_CONTENT_UPDATES = [
|
|
|
55
55
|
priorRevision: "45a408e8139bd57f42dda5ca5ae5c8cd4480b4e7bf08834f60058148a3a63475",
|
|
56
56
|
additionalPriorRevisions: new Set([
|
|
57
57
|
"d80b99ce53d1012cc169bbbc2afab8d0597bfbe9f30ac0812a8d5bbeb2ed9f90",
|
|
58
|
-
"dd11857ae7d881f176bd93947ef3031c33c75ee41e3c0435198fd60c67a94cf7"
|
|
58
|
+
"dd11857ae7d881f176bd93947ef3031c33c75ee41e3c0435198fd60c67a94cf7",
|
|
59
|
+
"4a48c15a4e20e4f29028cf2ff8597315eb51878814120125b5268356b923c9db"
|
|
59
60
|
]),
|
|
60
|
-
currentRevision: "
|
|
61
|
+
currentRevision: "1738d29786cd6a027c9a2bb339ce36ce3e15c858a55ec503f4e690e65273598b",
|
|
61
62
|
currentSourcePath: "./policy-library/data-retention-schedule-v2.md",
|
|
62
63
|
replacements: [
|
|
63
64
|
["FileGRC detects the bracketed prompts as approval blockers. Remove each prompt only after replacing it with a reviewed fact.", "Remove each bracketed prompt only after replacing it with a reviewed fact."],
|
|
@@ -73,9 +74,10 @@ const DOCUMENT_CONTENT_UPDATES = [
|
|
|
73
74
|
additionalPriorRevisions: new Set([
|
|
74
75
|
"51a19e22f3e0b31196371676297bfa843f96295d2f6c72e81a969ce7bafc36ae",
|
|
75
76
|
"5107f5c6015089a7c64e87692976aa48f727b8c03988f93f71e8f3e5084a05ac",
|
|
76
|
-
"970b97c4f594ff54fab554460f3cf44f80ee8079e996f4d5dc1427290f8aa714"
|
|
77
|
+
"970b97c4f594ff54fab554460f3cf44f80ee8079e996f4d5dc1427290f8aa714",
|
|
78
|
+
"ff8b5764ba3073d7ab6678c7da607850de5a82de4383cb54417cd74d82c05305"
|
|
77
79
|
]),
|
|
78
|
-
currentRevision: "
|
|
80
|
+
currentRevision: "72a6049f2eed25525417fc3eb6263c231e44193d3c5cbbf6675bb98459374d0c",
|
|
79
81
|
replacements: [
|
|
80
82
|
["## Reporting routes", "## Reporting channels"],
|
|
81
83
|
["The primary reporting route is {{security_contact_email}}.", "Use the current approved primary security Reporting Route. The Reporting Route record is the source of truth for its channel, destination, owner, priority, and effective period."],
|
|
@@ -90,7 +92,14 @@ const DOCUMENT_CONTENT_UPDATES = [
|
|
|
90
92
|
["- Restore-validation method and governed schedule", "- Restore-validation method and approved schedule"],
|
|
91
93
|
["- Dependencies, fallback paths, and validation steps", "- Dependencies, fallback paths, and validation steps\n- Any recovery targets required by an approved commitment, included Availability criterion, or risk decision"],
|
|
92
94
|
["[Confirm or replace before activation: The starter proposal for important production data is a daily backup, 30-day retention period, and annual restore validation. Record the approved choice for every important System in its Control, Component, System, Retention Schedule, and Obligation records.]", "[Confirm or replace before activation: The proposed starting point for important production data is a daily backup, 30-day retention period, and annual restore validation. Document the approved choice for every important System in its recovery procedures and the Data Retention Schedule.]"],
|
|
93
|
-
["on the approved governed schedule", "on the approved schedule"]
|
|
95
|
+
["on the approved governed schedule", "on the approved schedule"],
|
|
96
|
+
["[Complete before activation: Describe how workers can find the fallback reporting channel when the normal email, identity, or collaboration System is unavailable, compromised, or involved in the concern. Do not copy the destination here or include plaintext credentials, private keys, tokens, or recovery codes.]", "[Describe how workers find the fallback reporting channel when the normal email, identity, or collaboration System is unavailable, compromised, or involved in the concern. Do not copy the destination here or include plaintext credentials, private keys, tokens, or recovery codes.]"],
|
|
97
|
+
["The Policy Owner maintains this plan and ensures that the organization assigns these duties before activation:", "The Policy Owner maintains this plan and ensures that the organization assigns these duties:"],
|
|
98
|
+
["[Complete before activation: Record the emergency contact arrangement, its owner, alternate communication channel, protected storage location, and review schedule.]", "[Describe the emergency contact arrangement, its owner, alternate communication channel, protected storage location, and review schedule.]"],
|
|
99
|
+
["[Complete before activation: Identify every important System's recovery priority, dependencies, owner, backup or alternate recovery approach, and critical customer commitments. Record numeric recovery targets only when an approved commitment, included Availability criterion, or risk decision requires them.]", "[Identify every important System's recovery priority, dependencies, owner, backup or alternate recovery approach, and critical customer commitments. Record numeric recovery targets only when an approved commitment, included Availability criterion, or risk decision requires them.]"],
|
|
100
|
+
["[Confirm or replace before activation: The proposed starting point for important production data is a daily backup, 30-day retention period, and annual restore validation. Document the approved choice for every important System in its recovery procedures and the Data Retention Schedule.]", "[Document the management-approved recovery approach for every important System in its recovery procedures and the Data Retention Schedule. A daily backup, 30-day retention period, and annual restore validation are examples, not approved organization values.]"],
|
|
101
|
+
["[Complete before activation: Record the protected alternate location and access method responders will use when the primary identity, source-control, or collaboration Systems are unavailable. Confirm that authorized responders can retrieve the plan without exposing plaintext secrets or decryption keys.]", "[Describe the protected alternate location and access method responders use when the primary identity, source-control, or collaboration Systems are unavailable. Confirm that authorized responders can retrieve the plan without exposing plaintext secrets or decryption keys.]"],
|
|
102
|
+
["- Any recovery targets required by an approved commitment, included Availability criterion, or risk decision\n- Any recovery targets required by an approved commitment, included Availability criterion, or risk decision", "- Any recovery targets required by an approved commitment, included Availability criterion, or risk decision"],
|
|
94
103
|
],
|
|
95
104
|
summary: "Express incident and recovery requirements as a standalone plan, keep FileGRC record-entry guidance outside it, and make numeric recovery targets conditional."
|
|
96
105
|
},
|
|
@@ -98,9 +107,12 @@ const DOCUMENT_CONTENT_UPDATES = [
|
|
|
98
107
|
id: "document-soc2-management-representation",
|
|
99
108
|
path: "documents/document-soc2-management-representation.md",
|
|
100
109
|
priorRevision: "59e7e5929eeb24fa6bc23e1ed1553206901c33ba249b372ad0f42282ab7a64c7",
|
|
101
|
-
|
|
110
|
+
additionalPriorRevisions: new Set(["83de736025d07d8085fba93a212a98e7b3e951aded9894b933eab1e77c3a0d29"]),
|
|
111
|
+
currentRevision: "45fcfa1bfa280fe96c65220df48d55b2bd0f238c54a6dc18e8f2d9ccd38a7f96",
|
|
102
112
|
replacements: [
|
|
103
|
-
["Signed letter evidence record: [Evidence ID]", "Signed letter reference: [Approved storage location or evidence reference]"]
|
|
113
|
+
["Signed letter evidence record: [Evidence ID]", "Signed letter reference: [Approved storage location or evidence reference]"],
|
|
114
|
+
["> The service auditor normally supplies the required representation-letter wording near the end of fieldwork. Replace this preparation note with the agreed final letter, obtain the required management signature, and retain the signed fixed-format copy as linked evidence.", "> [Insert the final management representation agreed with the service auditor.]"],
|
|
115
|
+
["Before signing, reconcile the auditor's letter to:", "The signer confirms that the letter agrees with:"]
|
|
104
116
|
],
|
|
105
117
|
summary: "Keep the representation-letter template independent of FileGRC relationship fields."
|
|
106
118
|
},
|
|
@@ -121,11 +133,14 @@ const DOCUMENT_CONTENT_UPDATES = [
|
|
|
121
133
|
id: "document-soc2-system-description",
|
|
122
134
|
path: "documents/document-soc2-system-description.md",
|
|
123
135
|
priorRevision: "dc15bc2141be0d63a832eb6f7734bc7381e77582030534bcaf636138782f8e84",
|
|
124
|
-
|
|
136
|
+
additionalPriorRevisions: new Set(["929a7cd87ca78a2d9caa349cad0ae23583ad79184f26ba50c1694c62549aab9e"]),
|
|
137
|
+
currentRevision: "0e2e0252e0e0a5be821851de1d168fbcf22bfcca41d80836c657583ab38ba154",
|
|
125
138
|
replacements: [
|
|
126
139
|
["reconcile it to the filegrc records", "reconcile it to management's authoritative records"],
|
|
127
140
|
["Link the filegrc commitment records.", "Reconcile the summary to supporting commitment records."],
|
|
128
|
-
["Reference the selected criteria and control matrix generated by filegrc.", "Reference the selected criteria and management's control matrix."]
|
|
141
|
+
["Reference the selected criteria and control matrix generated by filegrc.", "Reference the selected criteria and management's control matrix."],
|
|
142
|
+
["> Draft preparation document. Complete every bracketed item, reconcile it to management's authoritative records, and have the service auditor review the final presentation.", "> [Describe the service as it existed for the stated reporting date or period.]"],
|
|
143
|
+
["[Summarize customer commitments, contractual security promises, internal objectives, and the system requirements needed to meet them. Reconcile the summary to supporting commitment records.]", "[Summarize customer commitments, contractual security promises, internal objectives, and the system requirements needed to meet them.]"]
|
|
129
144
|
],
|
|
130
145
|
summary: "Keep the SOC 2 System Description as a generic management deliverable."
|
|
131
146
|
}
|
package/src/web.js
CHANGED
|
@@ -43,7 +43,7 @@ export function renderIndex(state = null) {
|
|
|
43
43
|
</head>
|
|
44
44
|
<body>
|
|
45
45
|
<a class="skip-link" href="#main">Skip to content</a>
|
|
46
|
-
<div id="app"><div class="loading">Loading workspace…</div></div>
|
|
46
|
+
<div id="app"><div class="loading" role="status" aria-live="polite" aria-busy="true"><span class="spinner" aria-hidden="true"></span><span>Loading workspace…</span></div></div>
|
|
47
47
|
${snapshot}
|
|
48
48
|
<script src="./filegrc-app.js" defer></script>
|
|
49
49
|
</body>
|
|
@@ -98,6 +98,44 @@ let mutationStateRefreshRequested = false;
|
|
|
98
98
|
let programSelectionGeneration = 0;
|
|
99
99
|
let expiredStateRefresh = null;
|
|
100
100
|
|
|
101
|
+
function loadingIndicator(label) {
|
|
102
|
+
return '<span class="spinner" aria-hidden="true"></span><span>' + esc(label) + '</span>';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function setLoadingContent(element, label) {
|
|
106
|
+
if (!element) return;
|
|
107
|
+
element.classList.add("is-loading");
|
|
108
|
+
element.setAttribute("aria-busy", "true");
|
|
109
|
+
element.innerHTML = loadingIndicator(label);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function clearLoadingContent(element, label = "") {
|
|
113
|
+
if (!element) return;
|
|
114
|
+
element.classList.remove("is-loading");
|
|
115
|
+
element.removeAttribute("aria-busy");
|
|
116
|
+
element.textContent = label;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function setButtonBusy(button, busy, label = "") {
|
|
120
|
+
if (!button) return;
|
|
121
|
+
if (busy) {
|
|
122
|
+
if (!button.dataset.loadingIdle) {
|
|
123
|
+
button.dataset.loadingIdle = button.innerHTML;
|
|
124
|
+
button.dataset.loadingWasDisabled = String(button.disabled);
|
|
125
|
+
}
|
|
126
|
+
button.disabled = true;
|
|
127
|
+
setLoadingContent(button, label);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const idle = button.dataset.loadingIdle;
|
|
131
|
+
const wasDisabled = button.dataset.loadingWasDisabled === "true";
|
|
132
|
+
clearLoadingContent(button);
|
|
133
|
+
if (idle !== undefined) button.innerHTML = idle;
|
|
134
|
+
button.disabled = wasDisabled;
|
|
135
|
+
delete button.dataset.loadingIdle;
|
|
136
|
+
delete button.dataset.loadingWasDisabled;
|
|
137
|
+
}
|
|
138
|
+
|
|
101
139
|
start().catch((error) => {
|
|
102
140
|
root.innerHTML = '<main class="fatal"><h1>Could Not Load the Workspace</h1><pre></pre></main>';
|
|
103
141
|
root.querySelector("pre").textContent = error.stack || error.message;
|
|
@@ -188,7 +226,7 @@ function renderStateLoading(main, route, sections) {
|
|
|
188
226
|
const message = failed
|
|
189
227
|
? state.sectionErrors?.[failed] || "Reload the workspace and try again."
|
|
190
228
|
: "Records are ready. FileGRC is calculating the information needed for this page.";
|
|
191
|
-
main.innerHTML = '<div class="page"><section class="panel state-loading" role="status" aria-live="polite"><p class="kicker">' + (failed ? "Loading error" : "One moment") + '</p><h2>' + esc(title) + '</h2><p>' + esc(message) + '</p></section></div>';
|
|
229
|
+
main.innerHTML = '<div class="page"><section class="panel state-loading" role="' + (failed ? "alert" : "status") + '" aria-live="polite"' + (failed ? "" : ' aria-busy="true"') + '>' + (failed ? "" : '<span class="spinner spinner-large" aria-hidden="true"></span>') + '<div><p class="kicker">' + (failed ? "Loading error" : "One moment") + '</p><h2>' + esc(title) + '</h2><p>' + esc(message) + '</p></div></section></div>';
|
|
192
230
|
}
|
|
193
231
|
|
|
194
232
|
function loadStateForRoute() {
|
|
@@ -373,13 +411,17 @@ function topbar(route) {
|
|
|
373
411
|
const programSelect = programs.length > 1 && !state.readOnly
|
|
374
412
|
? '<label class="program-select"><span class="sr-only">Current Program</span><select data-program-select>' + programs.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === state.selectedProgramId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label>'
|
|
375
413
|
: "";
|
|
376
|
-
|
|
414
|
+
const validationMark = validationLoading && !repositoryError ? '<span class="spinner" aria-hidden="true"></span>' : '<span class="status-dot ' + validationTone + '"></span>';
|
|
415
|
+
const repositoryBusy = repositoryLoading && !repositoryError || state.repository?.status === "syncing";
|
|
416
|
+
const repositoryMark = repositoryBusy ? '<span class="spinner" aria-hidden="true"></span>' : '<span class="status-dot ' + repositoryTone + '"></span>';
|
|
417
|
+
return '<button class="mobile-nav" type="button" aria-label="Open navigation" aria-controls="sidebar-navigation" aria-expanded="false">☰</button><div><small class="eyebrow">' + esc(state.workspace.organizationName) + '</small><h1>' + esc(titleCase(title)) + '</h1></div><div class="topbar-status">' + programSelect + topbarProgramReadiness() + '<label class="search topbar-search"><span aria-hidden="true">⌕</span><input data-global-search type="search" placeholder="Search records" aria-label="Search records"><kbd>/</kbd></label><a class="validation-chip' + (validationLoading && !repositoryError ? ' is-loading' : '') + '" href="#/repository"' + (validationLoading && !repositoryError ? ' aria-busy="true"' : '') + '>' + validationMark + validationLabel + '</a><a class="repo-chip' + (repositoryBusy ? ' is-loading' : '') + '" href="#/repository"' + (repositoryBusy ? ' aria-busy="true"' : '') + '>' + repositoryMark + esc(repositoryLabel) + '</a></div>';
|
|
377
418
|
}
|
|
378
419
|
|
|
379
420
|
function topbarProgramReadiness() {
|
|
380
421
|
if (state.sections?.program !== "complete") {
|
|
381
|
-
const
|
|
382
|
-
|
|
422
|
+
const loading = state.sections?.program !== "error";
|
|
423
|
+
const label = loading ? '<span class="spinner" aria-hidden="true"></span><span class="sr-only">Loading</span>' : "View";
|
|
424
|
+
return '<a class="topbar-readiness' + (loading ? ' is-loading' : '') + '" href="#/"' + (loading ? ' aria-busy="true"' : '') + '><span class="topbar-readiness-copy"><span>Program readiness</span><strong>' + label + '</strong></span><span class="progress" aria-hidden="true"><span style="width:0%"></span></span></a>';
|
|
383
425
|
}
|
|
384
426
|
const progress = dashboardProgramReadiness(state.programReadiness);
|
|
385
427
|
const detail = progress.complete + " of " + progress.total + " readiness items complete";
|
|
@@ -394,7 +436,7 @@ function repositoryStatusTone(status) {
|
|
|
394
436
|
|
|
395
437
|
function repositorySyncAlert() {
|
|
396
438
|
if (state.repository?.status === "syncing") {
|
|
397
|
-
return '<div class="repository-sync-alert syncing" role="status" aria-live="polite"><span class="
|
|
439
|
+
return '<div class="repository-sync-alert syncing is-loading" role="status" aria-live="polite" aria-busy="true"><span class="spinner" aria-hidden="true"></span><span><strong>Saved and committed locally.</strong> Git push is continuing in the background. Other changes unlock when synchronization finishes.</span><a href="#/repository">View status</a></div>';
|
|
398
440
|
}
|
|
399
441
|
const message = state.repository?.backgroundSyncError;
|
|
400
442
|
if (!message) return "";
|
|
@@ -805,13 +847,13 @@ function openPolicyActivationDialog() {
|
|
|
805
847
|
}
|
|
806
848
|
|
|
807
849
|
function workflowGuidance(options = {}) {
|
|
808
|
-
const workflow = state.workflow;
|
|
850
|
+
const workflow = options.workflow || state.workflow;
|
|
809
851
|
if (!workflow) return "";
|
|
810
852
|
const matches = (item) => {
|
|
811
853
|
if (options.type === "audit" && options.id && item.auditId === options.id) return true;
|
|
812
854
|
if (options.stageId && item.stage !== options.stageId) return false;
|
|
813
|
-
if (options.type && item.subject?.type !== options.type
|
|
814
|
-
if (options.id && item.subject?.id !== options.id
|
|
855
|
+
if (options.type && item.subject?.type !== options.type) return false;
|
|
856
|
+
if (options.id && item.subject?.id !== options.id) return false;
|
|
815
857
|
return true;
|
|
816
858
|
};
|
|
817
859
|
const activeStates = new Set(["blocked", "due", "open", "overdue", "ready", "scheduled", "upcoming", "waiting-external"]);
|
|
@@ -838,9 +880,16 @@ function workflowGuidance(options = {}) {
|
|
|
838
880
|
const rows = visible.map(renderRow).join("");
|
|
839
881
|
const remaining = items.slice(visible.length);
|
|
840
882
|
const more = remaining.length
|
|
841
|
-
? '<details class="workflow-guidance-more"><summary>Show ' + remaining.length + ' more ' + pluralize("item", remaining.length) + '</summary><div class="workflow-findings workflow-findings-more">' + remaining.map(renderRow).join("") + '</div><p><code>filegrc workflow --json</code> returns
|
|
883
|
+
? '<details class="workflow-guidance-more"><summary>Show ' + remaining.length + ' more ' + pluralize("item", remaining.length) + '</summary><div class="workflow-findings workflow-findings-more">' + remaining.map(renderRow).join("") + '</div><p><code>filegrc workflow --json</code> returns this same checklist for CLI and agent use.</p></details>'
|
|
842
884
|
: "";
|
|
843
|
-
|
|
885
|
+
const completionRequirements = options.type && resourceReviewCriteria(options.type, { trigger: true });
|
|
886
|
+
return '<section class="workflow-guidance panel detail-support-panel detail-workflow-panel"><div class="panel-head"><div><p class="kicker">To-do</p><h3>' + esc(options.title || "Checklist") + '</h3></div><span class="badge ' + (blocking.length ? "warn" : "good") + '">' + (blocking.length ? "Needs work" : "Current") + '</span></div><p class="workflow-status">' + esc(status) + (completionRequirements ? '. ' + completionRequirements : "") + '</p><div class="workflow-findings">' + rows + '</div>' + more + '</section>';
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function recordCompletionState(record) {
|
|
890
|
+
const completeStatuses = new Set(["approved", "active", "complete", "completed", "closed", "done", "reconciled"]);
|
|
891
|
+
if (!completeStatuses.has(record.status)) return "";
|
|
892
|
+
return '<section class="record-completion-state panel detail-support-panel"><p class="kicker">Record status</p><h3>' + esc(properCase(record.status)) + '</h3><p>This record has no direct next step.</p></section>';
|
|
844
893
|
}
|
|
845
894
|
|
|
846
895
|
function collectionReviewPanel(type) {
|
|
@@ -857,14 +906,21 @@ function collectionReviewPanel(type) {
|
|
|
857
906
|
const reviewSummary = current
|
|
858
907
|
? '<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>'
|
|
859
908
|
: '<p class="collection-review-result"><strong>' + (needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Review again" : "Review required") + '</strong><span>' + esc(assessment.message) + '</span></p>';
|
|
909
|
+
const compactReviewSummary = current
|
|
910
|
+
? '<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(", ")) : "") + ".</span></p>"
|
|
911
|
+
: reviewSummary;
|
|
860
912
|
const action = state.readOnly
|
|
861
913
|
? ""
|
|
862
914
|
: needsFirstRecord
|
|
863
915
|
? '<a class="button primary" href="#/resources/' + encodeURIComponent(type) + '?new=1">Add first ' + esc(state.model.resources[type].title.toLowerCase()) + '</a>'
|
|
864
916
|
: '<button class="button ' + (current ? "" : "primary") + '" type="button" data-review-collection="' + esc(type) + '">' + (current ? "Review again" : "Review and confirm") + '</button>';
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
917
|
+
const status = current ? "Reviewed" : needsFirstRecord ? "Records required" : assessment.status === "stale" ? "Stale" : "Review required";
|
|
918
|
+
const details = '<details ' + (current ? 'class="collection-review-details"' : "open") + '><summary>' + (current ? "Show scope confirmation" : "What to review") + '</summary><div class="collection-review-detail-content"><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") + '">' + status + '</span></div><ul>' + configuration.reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul>' + (current ? reviewSummary : "") + '</div></details>';
|
|
919
|
+
const summary = current
|
|
920
|
+
? '<div class="collection-review-current-row"><div class="collection-review-complete-summary"><span class="kicker">Scope confirmation</span>' + compactReviewSummary + '</div>' + details + action + '</div>'
|
|
921
|
+
: details;
|
|
922
|
+
return '<section class="collection-review-panel panel ' + (current ? "current" : "required") + '">' + summary +
|
|
923
|
+
(current ? "" : '<div class="collection-review-foot">' + reviewSummary + action + '</div>') + '</section>';
|
|
868
924
|
}
|
|
869
925
|
|
|
870
926
|
function collectionNeedsFirstRecord(type) {
|
|
@@ -889,13 +945,31 @@ function collectionEmptyState(type, definition) {
|
|
|
889
945
|
: "No records exist. Use the scope confirmation above to record an allowed empty-collection conclusion, or add the records management identified.";
|
|
890
946
|
}
|
|
891
947
|
|
|
892
|
-
function resourceReviewCriteria(type,
|
|
948
|
+
function resourceReviewCriteria(type, options = {}) {
|
|
949
|
+
const collapsed = options === true || options.collapsed;
|
|
950
|
+
const trigger = typeof options === "object" && options.trigger;
|
|
951
|
+
const title = typeof options === "object" && options.title ? options.title : "Review criteria";
|
|
893
952
|
const reviewPoints = state.model.resources[type]?.guidance?.reviewPoints || [];
|
|
894
953
|
if (!reviewPoints.length) return "";
|
|
954
|
+
if (trigger) return '<button type="button" class="workflow-completion-requirements" data-completion-requirements="' + esc(type) + '">Completion requirements</button>';
|
|
895
955
|
const points = '<ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul>';
|
|
896
956
|
return collapsed
|
|
897
|
-
? '<details class="resource-review-criteria compact"><summary>
|
|
898
|
-
: '<section class="resource-review-criteria panel detail-support-panel detail-review-panel"><div class="panel-head"><h3>
|
|
957
|
+
? '<details class="resource-review-criteria compact"><summary>' + esc(title) + '</summary>' + points + '</details>'
|
|
958
|
+
: '<section class="resource-review-criteria panel detail-support-panel detail-review-panel"><div class="panel-head"><h3>' + esc(title) + '</h3></div>' + points + '</section>';
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function openCompletionRequirementsDialog(type) {
|
|
962
|
+
const definition = state.model.resources[type];
|
|
963
|
+
const reviewPoints = definition?.guidance?.reviewPoints || [];
|
|
964
|
+
if (!reviewPoints.length) return;
|
|
965
|
+
const dialog = document.createElement("dialog");
|
|
966
|
+
dialog.className = "alert-dialog completion-requirements-dialog";
|
|
967
|
+
dialog.innerHTML = '<div class="dialog-head"><div><p class="kicker">Completion requirements</p><h2>' + esc(titleCase(definition.title)) + '</h2></div><button type="button" class="icon-button" aria-label="Close">×</button></div><p>Check these before considering this record complete.</p><ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul><div class="dialog-actions"><button type="button" class="button primary">Close</button></div>';
|
|
968
|
+
document.body.append(dialog);
|
|
969
|
+
const close = () => dialog.close();
|
|
970
|
+
dialog.querySelectorAll("button").forEach((button) => button.addEventListener("click", close));
|
|
971
|
+
dialog.addEventListener("close", () => dialog.remove(), { once: true });
|
|
972
|
+
dialog.showModal();
|
|
899
973
|
}
|
|
900
974
|
|
|
901
975
|
function recordWorkflowItems(type, id) {
|
|
@@ -992,8 +1066,10 @@ function openCollectionReviewDialog(type) {
|
|
|
992
1066
|
expectedRevision: assessment.reviewRevision || undefined
|
|
993
1067
|
};
|
|
994
1068
|
form.querySelectorAll("button,input,select,textarea").forEach((control) => { control.disabled = true; });
|
|
1069
|
+
const submit = form.querySelector('button[type="submit"]');
|
|
1070
|
+
setButtonBusy(submit, true, "Saving…");
|
|
995
1071
|
try {
|
|
996
|
-
saveStatus
|
|
1072
|
+
setLoadingContent(saveStatus, "Validating and saving…");
|
|
997
1073
|
const prefetch = await repositoryPrefetch;
|
|
998
1074
|
if (prefetch?.error) throw prefetch.error;
|
|
999
1075
|
const response = await localFetch("/api/collection-review", {
|
|
@@ -1002,14 +1078,15 @@ function openCollectionReviewDialog(type) {
|
|
|
1002
1078
|
body: JSON.stringify({ ...payload, confirmed: true, prefetchToken: prefetch?.token })
|
|
1003
1079
|
});
|
|
1004
1080
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
1005
|
-
saveStatus
|
|
1081
|
+
setLoadingContent(saveStatus, "Saved locally. Refreshing page…");
|
|
1006
1082
|
applyMutationState(await response.json());
|
|
1007
1083
|
dialog.close();
|
|
1008
1084
|
render();
|
|
1009
1085
|
} catch (requestError) {
|
|
1010
|
-
saveStatus
|
|
1086
|
+
clearLoadingContent(saveStatus, "Not saved");
|
|
1011
1087
|
error.textContent = requestError.message;
|
|
1012
1088
|
} finally {
|
|
1089
|
+
setButtonBusy(submit, false);
|
|
1013
1090
|
form.querySelectorAll("button,input,select,textarea").forEach((control) => { control.disabled = false; });
|
|
1014
1091
|
}
|
|
1015
1092
|
});
|
|
@@ -1920,8 +1997,7 @@ async function openObligationRuleActivation(ruleId) {
|
|
|
1920
1997
|
const approvedByIds = [...form.querySelectorAll('input[name="approver"]:checked')].map(({ value }) => value);
|
|
1921
1998
|
if (!approvedByIds.length) return void (error.textContent = "Select at least one approver.");
|
|
1922
1999
|
const button = form.querySelector('button[type="submit"]');
|
|
1923
|
-
button
|
|
1924
|
-
button.textContent = "Activating…";
|
|
2000
|
+
setButtonBusy(button, true, "Activating…");
|
|
1925
2001
|
error.textContent = "";
|
|
1926
2002
|
try {
|
|
1927
2003
|
const saved = await localFetch("/api/obligation-rule-activations", {
|
|
@@ -1942,8 +2018,7 @@ async function openObligationRuleActivation(ruleId) {
|
|
|
1942
2018
|
render();
|
|
1943
2019
|
} catch (caught) {
|
|
1944
2020
|
error.textContent = caught.message;
|
|
1945
|
-
button
|
|
1946
|
-
button.textContent = "Activate schedule";
|
|
2021
|
+
setButtonBusy(button, false);
|
|
1947
2022
|
}
|
|
1948
2023
|
});
|
|
1949
2024
|
} catch (error) {
|
|
@@ -2079,6 +2154,8 @@ function openObligationEventDialog(trigger) {
|
|
|
2079
2154
|
const form = event.currentTarget;
|
|
2080
2155
|
if (!form.reportValidity()) return;
|
|
2081
2156
|
form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = true; });
|
|
2157
|
+
const submit = form.querySelector('button[type="submit"]');
|
|
2158
|
+
setButtonBusy(submit, true, "Adding tasks…");
|
|
2082
2159
|
try {
|
|
2083
2160
|
const response = await localFetch("/api/obligation-events", {
|
|
2084
2161
|
method: "POST",
|
|
@@ -2103,6 +2180,7 @@ function openObligationEventDialog(trigger) {
|
|
|
2103
2180
|
history.replaceState(null, "", "#/stage/run");
|
|
2104
2181
|
render();
|
|
2105
2182
|
} catch (error) {
|
|
2183
|
+
setButtonBusy(submit, false);
|
|
2106
2184
|
form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = false; });
|
|
2107
2185
|
form.querySelector(".dialog-error").textContent = error.message;
|
|
2108
2186
|
}
|
|
@@ -2135,6 +2213,8 @@ function openExternalReviewerGovernanceDialog() {
|
|
|
2135
2213
|
const error = dialog.querySelector(".dialog-error");
|
|
2136
2214
|
error.textContent = "";
|
|
2137
2215
|
form.querySelectorAll("button,input,textarea").forEach((control) => { control.disabled = true; });
|
|
2216
|
+
const submit = form.querySelector('[data-preview-governance]');
|
|
2217
|
+
setButtonBusy(submit, true, previewedPayload ? "Applying…" : "Preparing preview…");
|
|
2138
2218
|
try {
|
|
2139
2219
|
if (!previewedPayload) {
|
|
2140
2220
|
const response = await localFetch("/api/external-reviewer-governance/preview", {
|
|
@@ -2161,6 +2241,8 @@ function openExternalReviewerGovernanceDialog() {
|
|
|
2161
2241
|
} catch (requestError) {
|
|
2162
2242
|
error.textContent = requestError.message;
|
|
2163
2243
|
} finally {
|
|
2244
|
+
setButtonBusy(submit, false);
|
|
2245
|
+
submit.textContent = previewedPayload ? "Confirm and apply" : "Preview bundle";
|
|
2164
2246
|
form.querySelectorAll("button,input,textarea").forEach((control) => { control.disabled = false; });
|
|
2165
2247
|
}
|
|
2166
2248
|
});
|
|
@@ -2197,6 +2279,8 @@ function openNextAuditCycleDialog(prior) {
|
|
|
2197
2279
|
const error = dialog.querySelector(".dialog-error");
|
|
2198
2280
|
error.textContent = "";
|
|
2199
2281
|
form.querySelectorAll("button,input").forEach((control) => { control.disabled = true; });
|
|
2282
|
+
const submit = form.querySelector("[data-preview-cycle]");
|
|
2283
|
+
setButtonBusy(submit, true, previewedPayload ? "Creating…" : "Preparing preview…");
|
|
2200
2284
|
try {
|
|
2201
2285
|
if (!previewedPayload) {
|
|
2202
2286
|
const response = await localFetch("/api/audit-cycle/preview", {
|
|
@@ -2225,6 +2309,8 @@ function openNextAuditCycleDialog(prior) {
|
|
|
2225
2309
|
} catch (requestError) {
|
|
2226
2310
|
error.textContent = requestError.message;
|
|
2227
2311
|
} finally {
|
|
2312
|
+
setButtonBusy(submit, false);
|
|
2313
|
+
submit.textContent = previewedPayload ? "Confirm and create" : "Preview carry-forward";
|
|
2228
2314
|
form.querySelectorAll("button,input").forEach((control) => { control.disabled = false; });
|
|
2229
2315
|
}
|
|
2230
2316
|
});
|
|
@@ -2314,6 +2400,8 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
2314
2400
|
])
|
|
2315
2401
|
};
|
|
2316
2402
|
form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = true; });
|
|
2403
|
+
const submit = form.querySelector("[data-preview-review]");
|
|
2404
|
+
setButtonBusy(submit, true, previewedPayload ? "Saving…" : "Preparing preview…");
|
|
2317
2405
|
try {
|
|
2318
2406
|
if (!previewedPayload) {
|
|
2319
2407
|
const response = await localFetch("/api/applicability-review/preview", {
|
|
@@ -2327,7 +2415,7 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
2327
2415
|
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>';
|
|
2328
2416
|
dialog.querySelector("[data-preview-review]").textContent = "Confirm and save";
|
|
2329
2417
|
} else {
|
|
2330
|
-
saveStatus
|
|
2418
|
+
setLoadingContent(saveStatus, "Validating and saving…");
|
|
2331
2419
|
const prefetch = await repositoryPrefetch;
|
|
2332
2420
|
if (prefetch?.error) throw prefetch.error;
|
|
2333
2421
|
const response = await localFetch("/api/applicability-review", {
|
|
@@ -2336,14 +2424,17 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
2336
2424
|
body: JSON.stringify({ ...previewedPayload, confirmed: true, prefetchToken: prefetch?.token })
|
|
2337
2425
|
});
|
|
2338
2426
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
2339
|
-
saveStatus
|
|
2427
|
+
setLoadingContent(saveStatus, "Saved locally. Refreshing page…");
|
|
2340
2428
|
applyMutationState(await response.json());
|
|
2341
2429
|
dialog.close();
|
|
2342
2430
|
render();
|
|
2343
2431
|
}
|
|
2344
2432
|
} catch (requestError) {
|
|
2433
|
+
clearLoadingContent(saveStatus, "Not saved");
|
|
2345
2434
|
error.textContent = requestError.message;
|
|
2346
2435
|
} finally {
|
|
2436
|
+
setButtonBusy(submit, false);
|
|
2437
|
+
submit.textContent = previewedPayload ? "Confirm and save" : "Preview decisions";
|
|
2347
2438
|
form.querySelectorAll("button,input,select").forEach((control) => { control.disabled = false; });
|
|
2348
2439
|
}
|
|
2349
2440
|
});
|
|
@@ -2388,8 +2479,7 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
2388
2479
|
main.querySelector("#initialize-audit-work")?.addEventListener("click", async (event) => {
|
|
2389
2480
|
const button = event.currentTarget;
|
|
2390
2481
|
const error = main.querySelector(".audit-preparation-error");
|
|
2391
|
-
button
|
|
2392
|
-
button.textContent = "Initializing…";
|
|
2482
|
+
setButtonBusy(button, true, "Initializing…");
|
|
2393
2483
|
error.textContent = "";
|
|
2394
2484
|
try {
|
|
2395
2485
|
const response = await localFetch("/api/audit-preparation", {
|
|
@@ -2402,8 +2492,7 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
2402
2492
|
render();
|
|
2403
2493
|
} catch (caught) {
|
|
2404
2494
|
error.textContent = caught.message;
|
|
2405
|
-
button
|
|
2406
|
-
button.textContent = "Initialize audit work";
|
|
2495
|
+
setButtonBusy(button, false);
|
|
2407
2496
|
}
|
|
2408
2497
|
});
|
|
2409
2498
|
main.querySelector("#packet-form").addEventListener("submit", async (event) => {
|
|
@@ -2412,8 +2501,7 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
2412
2501
|
if (!form.reportValidity()) return;
|
|
2413
2502
|
const button = form.querySelector('button[type="submit"]');
|
|
2414
2503
|
const error = main.querySelector(".dialog-error");
|
|
2415
|
-
button
|
|
2416
|
-
button.textContent = "Generating…";
|
|
2504
|
+
setButtonBusy(button, true, "Generating…");
|
|
2417
2505
|
error.textContent = "";
|
|
2418
2506
|
try {
|
|
2419
2507
|
const response = await localFetch("/api/evidence-packet", {
|
|
@@ -2434,8 +2522,8 @@ function renderAuditPacket(main, params = new URLSearchParams()) {
|
|
|
2434
2522
|
} catch (caught) {
|
|
2435
2523
|
error.textContent = caught.message;
|
|
2436
2524
|
} finally {
|
|
2525
|
+
setButtonBusy(button, false);
|
|
2437
2526
|
button.disabled = state.readOnly;
|
|
2438
|
-
button.textContent = draft ? "Generate draft" : "Generate packet";
|
|
2439
2527
|
}
|
|
2440
2528
|
});
|
|
2441
2529
|
if (latestPacketResult && latestPacketState === state && latestPacketResult.packet.audit?.id === selected?.id) {
|
|
@@ -2707,7 +2795,7 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
|
|
|
2707
2795
|
const definition = state.model.resources[type];
|
|
2708
2796
|
if (!entry || !definition) return renderNotFound(main);
|
|
2709
2797
|
if (entry.detailsLoaded === false) {
|
|
2710
|
-
main.innerHTML = '<div class="page"><div class="detail-head"><div><div class="breadcrumbs header-breadcrumbs"><a href="#/resources/' + encodeURIComponent(type) + '">' + esc(titleCase(definition.pluralTitle)) + '</a><span>/</span><span>' + esc(entry.record.title) + '</span></div><h2>' + esc(titleCase(entry.record.title)) + '</h2></div></div><section class="panel detail-loading" role="status">Loading record
|
|
2798
|
+
main.innerHTML = '<div class="page"><div class="detail-head"><div><div class="breadcrumbs header-breadcrumbs"><a href="#/resources/' + encodeURIComponent(type) + '">' + esc(titleCase(definition.pluralTitle)) + '</a><span>/</span><span>' + esc(entry.record.title) + '</span></div><h2>' + esc(titleCase(entry.record.title)) + '</h2></div></div><section class="panel detail-loading is-loading" role="status" aria-live="polite" aria-busy="true">' + loadingIndicator("Loading record…") + '</section></div>';
|
|
2711
2799
|
loadResourceDetail(type, id);
|
|
2712
2800
|
return;
|
|
2713
2801
|
}
|
|
@@ -2779,12 +2867,13 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
|
|
|
2779
2867
|
? '<section class="panel detail-history-panel"><div class="panel-head"><h3>File History</h3></div><div class="history">' + entry.history.map((commit) => '<div><code>' + esc(commit.shortCommit) + '</code><span><strong>' + esc(commit.subject) + '</strong><small>' + esc(commit.author) + ' · ' + esc(formatLocalDateTime(commit.timestamp)) + '</small></span></div>').join("") + '</div></section>'
|
|
2780
2868
|
: "";
|
|
2781
2869
|
const participationPanel = entry.historyLoaded === false
|
|
2782
|
-
? '<section class="panel detail-support-panel" role="status"><div class="panel-head"><h3>Participation</h3></div><p class="muted">Loading participation and file history
|
|
2783
|
-
:
|
|
2870
|
+
? '<section class="panel detail-support-panel" role="status" aria-live="polite" aria-busy="true"><div class="panel-head"><h3>Participation</h3></div><p class="muted is-loading">' + loadingIndicator("Loading participation and file history…") + '</p></section>'
|
|
2871
|
+
: entry.historyError
|
|
2872
|
+
? '<section class="panel detail-support-panel" role="status" aria-live="polite"><div class="panel-head"><h3>Participation</h3></div><p class="muted">Participation and file history are unavailable. Reload the record to try again.</p></section>'
|
|
2873
|
+
: personParticipation(entry);
|
|
2784
2874
|
const supportPanels = renderDetailSupport({
|
|
2785
2875
|
hasRecordBody,
|
|
2786
|
-
workflowPanel: workflowGuidance({ type, id, title: "Next steps" }),
|
|
2787
|
-
reviewPanel: resourceReviewCriteria(type),
|
|
2876
|
+
workflowPanel: workflowGuidance({ type, id, title: "Next steps", workflow: entry.workflow }) || recordCompletionState(entry.record),
|
|
2788
2877
|
metadataPanel: '<section class="panel detail-support-panel detail-metadata-panel"><div class="panel-head"><h3>Record details</h3></div><dl class="metadata">' + sourceMetadata + visible.map(([name, value]) => '<div><dt>' + esc(fields[name]?.label || humanize(name)) + '</dt><dd>' + formatValue(name === "status" ? displayStatus(entry.record) : value, name, type) + '</dd></div>').join("") + '</dl></section>',
|
|
2789
2878
|
attachmentPanel,
|
|
2790
2879
|
participationPanel,
|
|
@@ -2792,6 +2881,7 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
|
|
|
2792
2881
|
historyPanel
|
|
2793
2882
|
});
|
|
2794
2883
|
main.innerHTML = '<div class="page"><div class="detail-head"><div><div class="breadcrumbs header-breadcrumbs"><a href="#/resources/' + encodeURIComponent(type) + '">' + esc(titleCase(definition.pluralTitle)) + '</a><span>/</span><span>' + esc(entry.record.title) + '</span></div><h2>' + esc(titleCase(entry.record.title)) + '</h2></div><div class="actions">' + reportingRouteSetActions + auditCycleAction + auditPopulationCorrectionAction + (type === "audit" ? '<a class="button primary" href="#/audit-packet?auditId=' + encodeURIComponent(entry.record.id) + '">Audit Evidence & Packet</a>' : "") + governanceActions + lifecycleActions + issueActions + addRecordContentAction + (!state.readOnly && !routeSetLocked ? '<button class="button" id="edit-resource">Edit</button>' + (!definition.singleton ? '<button class="button danger" id="delete-resource">Delete</button>' : "") : "") + '</div></div><div class="detail-grid ' + (hasRecordBody ? "" : "detail-grid-structured") + '">' + detailMain + supportPanels + '</div></div>';
|
|
2884
|
+
main.querySelectorAll("[data-completion-requirements]").forEach((button) => button.addEventListener("click", () => openCompletionRequirementsDialog(button.dataset.completionRequirements)));
|
|
2795
2885
|
main.querySelector("#edit-resource")?.addEventListener("click", () => openEditor(type, entry));
|
|
2796
2886
|
main.querySelector("[data-external-reviewer-governance]")?.addEventListener("click", openExternalReviewerGovernanceDialog);
|
|
2797
2887
|
main.querySelector("[data-next-audit-cycle]")?.addEventListener("click", () => openNextAuditCycleDialog(entry.record));
|
|
@@ -2929,11 +3019,11 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
|
|
|
2929
3019
|
}
|
|
2930
3020
|
}
|
|
2931
3021
|
|
|
2932
|
-
function renderDetailSupport({ hasRecordBody, workflowPanel,
|
|
2933
|
-
const panels = workflowPanel +
|
|
3022
|
+
function renderDetailSupport({ hasRecordBody, workflowPanel, metadataPanel, attachmentPanel, participationPanel, connectionsPanel, historyPanel }) {
|
|
3023
|
+
const panels = workflowPanel + metadataPanel + attachmentPanel + participationPanel + connectionsPanel + historyPanel;
|
|
2934
3024
|
if (hasRecordBody) return '<aside>' + panels + '</aside>';
|
|
2935
3025
|
const stacks = [
|
|
2936
|
-
{ name: "guidance", content: workflowPanel
|
|
3026
|
+
{ name: "guidance", content: workflowPanel },
|
|
2937
3027
|
{ name: "record", content: metadataPanel + attachmentPanel + historyPanel },
|
|
2938
3028
|
{ name: "relationships", content: participationPanel + connectionsPanel }
|
|
2939
3029
|
].filter(({ content }) => content);
|
|
@@ -2965,7 +3055,7 @@ async function loadResourceDetail(type, id) {
|
|
|
2965
3055
|
let token = state.stateToken;
|
|
2966
3056
|
let detail = null;
|
|
2967
3057
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2968
|
-
const tokenQuery = token ? "?token=" + encodeURIComponent(token) + "&history=false" : "?history=false";
|
|
3058
|
+
const tokenQuery = token ? "?token=" + encodeURIComponent(token) + "&history=false&workflow=true" : "?history=false&workflow=true";
|
|
2969
3059
|
const response = await localFetch("/api/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(id) + tokenQuery);
|
|
2970
3060
|
if (response.status === 409 && token) {
|
|
2971
3061
|
await refreshExpiredAppState(token);
|
|
@@ -3007,8 +3097,8 @@ async function loadResourceHistory(type, id) {
|
|
|
3007
3097
|
const key = (state.stateToken || "live") + "\0history\0" + type + "\0" + id;
|
|
3008
3098
|
if (resourceDetailRequests.has(key)) return resourceDetailRequests.get(key);
|
|
3009
3099
|
const request = (async () => {
|
|
3100
|
+
const token = state.stateToken;
|
|
3010
3101
|
try {
|
|
3011
|
-
const token = state.stateToken;
|
|
3012
3102
|
const tokenQuery = (token ? "?token=" + encodeURIComponent(token) + "&" : "?") + "history=only";
|
|
3013
3103
|
const response = await localFetch("/api/resource/" + encodeURIComponent(type) + "/" + encodeURIComponent(id) + tokenQuery);
|
|
3014
3104
|
if (response.status === 409 && token) {
|
|
@@ -3029,8 +3119,15 @@ async function loadResourceHistory(type, id) {
|
|
|
3029
3119
|
entry.historyLoaded = true;
|
|
3030
3120
|
const route = parseRoute();
|
|
3031
3121
|
if (route.name === "detail" && route.type === type && route.id === id) render();
|
|
3032
|
-
} catch {
|
|
3033
|
-
|
|
3122
|
+
} catch (error) {
|
|
3123
|
+
if (token && state.stateToken !== token) return;
|
|
3124
|
+
const entry = state.resources.find(({ record }) => record.type === type && record.id === id);
|
|
3125
|
+
if (entry) {
|
|
3126
|
+
entry.historyLoaded = true;
|
|
3127
|
+
entry.historyError = error.message;
|
|
3128
|
+
}
|
|
3129
|
+
const route = parseRoute();
|
|
3130
|
+
if (route.name === "detail" && route.type === type && route.id === id) render();
|
|
3034
3131
|
} finally {
|
|
3035
3132
|
for (const [requestKey, pending] of resourceDetailRequests) {
|
|
3036
3133
|
if (pending === request) resourceDetailRequests.delete(requestKey);
|
|
@@ -3423,8 +3520,8 @@ async function runRepositoryGitAction(action) {
|
|
|
3423
3520
|
status.textContent = "";
|
|
3424
3521
|
status.classList.remove("error");
|
|
3425
3522
|
}
|
|
3523
|
+
if (active) setButtonBusy(active, true, label);
|
|
3426
3524
|
buttons.forEach((button) => { button.disabled = true; });
|
|
3427
|
-
if (active) active.textContent = label;
|
|
3428
3525
|
try {
|
|
3429
3526
|
const response = await localFetch("/api/git/" + action, { method: "POST" });
|
|
3430
3527
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
@@ -3440,8 +3537,8 @@ async function runRepositoryGitAction(action) {
|
|
|
3440
3537
|
? "Synchronized " + result.shortCommit + " with " + result.upstream + "."
|
|
3441
3538
|
: "Pushed " + result.shortCommit + " to " + result.upstream + ".";
|
|
3442
3539
|
} catch (cause) {
|
|
3540
|
+
if (active) setButtonBusy(active, false);
|
|
3443
3541
|
buttons.forEach((button, index) => { button.disabled = disabled[index]; });
|
|
3444
|
-
if (active) active.textContent = action === "pull" ? "Check incoming commits" : action === "retry-sync" ? "Retry sync" : "Push";
|
|
3445
3542
|
if (status) {
|
|
3446
3543
|
status.textContent = cause.message;
|
|
3447
3544
|
status.classList.add("error");
|
|
@@ -3467,8 +3564,10 @@ function openCommitDialog() {
|
|
|
3467
3564
|
const form = event.currentTarget;
|
|
3468
3565
|
if (!form.reportValidity()) return;
|
|
3469
3566
|
const button = form.querySelector('button[type="submit"]');
|
|
3470
|
-
form.querySelectorAll("button,input")
|
|
3471
|
-
|
|
3567
|
+
const controls = [...form.querySelectorAll("button,input")];
|
|
3568
|
+
const disabled = controls.map((control) => control.disabled);
|
|
3569
|
+
setButtonBusy(button, true, busyLabel);
|
|
3570
|
+
controls.forEach((control) => { control.disabled = true; });
|
|
3472
3571
|
try {
|
|
3473
3572
|
const response = await localFetch("/api/commit", {
|
|
3474
3573
|
method: "POST",
|
|
@@ -3490,8 +3589,8 @@ function openCommitDialog() {
|
|
|
3490
3589
|
status.classList.toggle("error", !result.pushed && !result.pushSkipped);
|
|
3491
3590
|
}
|
|
3492
3591
|
} catch (error) {
|
|
3493
|
-
|
|
3494
|
-
|
|
3592
|
+
setButtonBusy(button, false);
|
|
3593
|
+
controls.forEach((control, index) => { control.disabled = disabled[index]; });
|
|
3495
3594
|
form.querySelector(".dialog-error").textContent = error.message;
|
|
3496
3595
|
}
|
|
3497
3596
|
});
|
|
@@ -4072,9 +4171,11 @@ function setOnboardingBusy(busy, label = "") {
|
|
|
4072
4171
|
onboardingBusy = busy;
|
|
4073
4172
|
onboardingDialog.querySelectorAll("button,input,select,textarea").forEach((control) => { control.disabled = busy; });
|
|
4074
4173
|
const next = onboardingDialog.querySelector('[data-onboarding="next"]');
|
|
4075
|
-
|
|
4174
|
+
const draft = onboardingDialog.querySelector('[data-onboarding="draft"]');
|
|
4076
4175
|
const skip = onboardingDialog.querySelector('[data-onboarding="skip"]');
|
|
4077
|
-
|
|
4176
|
+
const retry = onboardingDialog.querySelector(".onboarding-retry-sync");
|
|
4177
|
+
const active = label === "Saving draft…" ? draft : label === "Skipping…" ? skip : label === "Retrying sync…" ? retry : next;
|
|
4178
|
+
if (busy && active && label) setLoadingContent(active, label);
|
|
4078
4179
|
if (busy && onboardingStep === onboardingSteps().length - 1) {
|
|
4079
4180
|
onboardingStillWorkingTimer = setTimeout(() => {
|
|
4080
4181
|
const status = onboardingDialog?.querySelector(".onboarding-save-status");
|
|
@@ -4754,12 +4855,8 @@ function wireStructuredObjectEditors(dialog) {
|
|
|
4754
4855
|
dialog.querySelector("form").addEventListener("click", (event) => {
|
|
4755
4856
|
const bind = event.target.closest("[data-bind-review-revisions]");
|
|
4756
4857
|
if (bind) {
|
|
4757
|
-
const previousLabel = bind.textContent;
|
|
4758
4858
|
bindCurrentReviewRevisions(dialog, bind).catch((error) => {
|
|
4759
4859
|
dialog.querySelector(".dialog-error").textContent = error.message;
|
|
4760
|
-
}).finally(() => {
|
|
4761
|
-
bind.disabled = false;
|
|
4762
|
-
if (bind.textContent === "Binding…") bind.textContent = previousLabel;
|
|
4763
4860
|
});
|
|
4764
4861
|
return;
|
|
4765
4862
|
}
|
|
@@ -4835,23 +4932,28 @@ async function bindCurrentReviewRevisions(dialog, button) {
|
|
|
4835
4932
|
throw new Error("Select the retention schedule, Information Types, and operational scope before binding their current revisions.");
|
|
4836
4933
|
}
|
|
4837
4934
|
const ids = [...new Set(Object.values(selected).flat().concat(scheduleDocumentId, retentionUseReviewIds(dialog)).filter(Boolean))];
|
|
4838
|
-
button
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4935
|
+
setButtonBusy(button, true, "Binding…");
|
|
4936
|
+
let bound = false;
|
|
4937
|
+
try {
|
|
4938
|
+
const query = ids.map((id) => "id=" + encodeURIComponent(id)).join("&");
|
|
4939
|
+
const response = await localFetch("/api/review-revisions?" + query);
|
|
4940
|
+
if (!response.ok) throw new Error(await responseMessage(response));
|
|
4941
|
+
const { revisions } = await response.json();
|
|
4942
|
+
const editor = button.closest(".string-map-editor");
|
|
4943
|
+
const items = editor.querySelector(".string-map-items");
|
|
4944
|
+
items.replaceChildren();
|
|
4945
|
+
for (const [key, value] of Object.entries(revisions)) {
|
|
4946
|
+
items.append(editor.querySelector("template").content.cloneNode(true));
|
|
4947
|
+
const row = items.lastElementChild;
|
|
4948
|
+
row.querySelector("[data-map-key]").value = key;
|
|
4949
|
+
row.querySelector("[data-map-value]").value = value;
|
|
4950
|
+
}
|
|
4951
|
+
bound = true;
|
|
4952
|
+
dialog.querySelector(".dialog-error").textContent = "";
|
|
4953
|
+
} finally {
|
|
4954
|
+
setButtonBusy(button, false);
|
|
4955
|
+
if (bound) button.textContent = "Refresh current revisions";
|
|
4852
4956
|
}
|
|
4853
|
-
button.textContent = "Refresh current revisions";
|
|
4854
|
-
dialog.querySelector(".dialog-error").textContent = "";
|
|
4855
4957
|
}
|
|
4856
4958
|
|
|
4857
4959
|
function retentionUseReviewIds(dialog) {
|
|
@@ -5698,16 +5800,16 @@ function prefetchRepositoryForReview(status = null) {
|
|
|
5698
5800
|
if (state.repository?.mode !== "trunk" || state.repository?.developmentOverride) {
|
|
5699
5801
|
return Promise.resolve(null);
|
|
5700
5802
|
}
|
|
5701
|
-
if (status) status
|
|
5803
|
+
if (status) setLoadingContent(status, "Checking repository…");
|
|
5702
5804
|
return fetch("/api/git/prefetch", { method: "POST" })
|
|
5703
5805
|
.then(async (response) => {
|
|
5704
5806
|
if (!response.ok) throw new Error(await responseMessage(response));
|
|
5705
5807
|
const result = await response.json();
|
|
5706
|
-
if (status) status
|
|
5808
|
+
if (status) clearLoadingContent(status, result.status === "checked" ? "Repository checked" : "Ready to save");
|
|
5707
5809
|
return result;
|
|
5708
5810
|
})
|
|
5709
5811
|
.catch((error) => {
|
|
5710
|
-
if (status) status
|
|
5812
|
+
if (status) clearLoadingContent(status, "Repository check failed");
|
|
5711
5813
|
return null;
|
|
5712
5814
|
});
|
|
5713
5815
|
}
|
|
@@ -5756,7 +5858,10 @@ function setMutationBusy(dialog, busy, label, idleLabel) {
|
|
|
5756
5858
|
clearTimeout(dialog._stillWorkingTimer);
|
|
5757
5859
|
dialog._stillWorkingTimer = null;
|
|
5758
5860
|
dialog.dataset.mutationBusy = busy ? "true" : "false";
|
|
5861
|
+
const button = dialog.querySelector("#save-record,#save-content,#activate-policy") || dialog.querySelector('button[type="submit"]');
|
|
5862
|
+
if (busy && button) setButtonBusy(button, true, label);
|
|
5759
5863
|
dialog.querySelectorAll("button,input,select,textarea").forEach((control) => {
|
|
5864
|
+
if (control === button) return;
|
|
5760
5865
|
if (busy) {
|
|
5761
5866
|
control.dataset.mutationWasDisabled = control.disabled ? "true" : "false";
|
|
5762
5867
|
control.disabled = true;
|
|
@@ -5765,13 +5870,15 @@ function setMutationBusy(dialog, busy, label, idleLabel) {
|
|
|
5765
5870
|
delete control.dataset.mutationWasDisabled;
|
|
5766
5871
|
}
|
|
5767
5872
|
});
|
|
5768
|
-
|
|
5769
|
-
|
|
5873
|
+
if (!busy && button) {
|
|
5874
|
+
setButtonBusy(button, false);
|
|
5875
|
+
if (idleLabel) button.textContent = idleLabel;
|
|
5876
|
+
}
|
|
5770
5877
|
const status = dialog.querySelector(".save-status");
|
|
5771
|
-
if (status) status
|
|
5878
|
+
if (status) clearLoadingContent(status);
|
|
5772
5879
|
if (busy) {
|
|
5773
5880
|
dialog._stillWorkingTimer = setTimeout(() => {
|
|
5774
|
-
if (status) status
|
|
5881
|
+
if (status) setLoadingContent(status, "Still working. Recalculating readiness and repository state.");
|
|
5775
5882
|
}, 1_500);
|
|
5776
5883
|
}
|
|
5777
5884
|
}
|
|
@@ -5815,6 +5922,8 @@ async function responseMessage(response) {
|
|
|
5815
5922
|
try { return JSON.parse(source).error || source; } catch { return source; }
|
|
5816
5923
|
}
|
|
5817
5924
|
async function localFetch(url, options) {
|
|
5925
|
+
const requestProgramId = state?.selectedProgramId || null;
|
|
5926
|
+
const requestProgramGeneration = programSelectionGeneration;
|
|
5818
5927
|
const requestUrl = new URL(url, location.origin);
|
|
5819
5928
|
if (state?.selectedProgramId && !requestUrl.searchParams.has("programId")) {
|
|
5820
5929
|
requestUrl.searchParams.set("programId", state.selectedProgramId);
|
|
@@ -5837,17 +5946,24 @@ async function localFetch(url, options) {
|
|
|
5837
5946
|
const chip = synchronizing ? document.querySelector(".repo-chip") : null;
|
|
5838
5947
|
const previousChip = chip?.innerHTML;
|
|
5839
5948
|
let repositoryRefreshed = false;
|
|
5840
|
-
if (chip)
|
|
5949
|
+
if (chip) {
|
|
5950
|
+
chip.classList.add("is-loading");
|
|
5951
|
+
chip.setAttribute("aria-busy", "true");
|
|
5952
|
+
chip.innerHTML = loadingIndicator("Syncing");
|
|
5953
|
+
}
|
|
5841
5954
|
try {
|
|
5842
5955
|
const response = await fetch(scopedUrl, requestOptions);
|
|
5843
5956
|
if (synchronizing && !response.ok) {
|
|
5844
5957
|
try {
|
|
5845
|
-
const stateResponse = await fetch("/api/state?programId=" + encodeURIComponent(
|
|
5958
|
+
const stateResponse = await fetch("/api/state?programId=" + encodeURIComponent(requestProgramId || ""));
|
|
5846
5959
|
if (stateResponse.ok) {
|
|
5847
|
-
|
|
5960
|
+
const refreshedState = normalizeAppState(await stateResponse.json());
|
|
5961
|
+
if (programSelectionGeneration !== requestProgramGeneration || (state?.selectedProgramId || null) !== requestProgramId) return response;
|
|
5962
|
+
state = refreshedState;
|
|
5963
|
+
repositoryRefreshed = true;
|
|
5964
|
+
scheduleRepositorySyncPoll();
|
|
5848
5965
|
if (chip?.isConnected) {
|
|
5849
5966
|
chip.innerHTML = '<span class="status-dot ' + repositoryStatusTone(state.repository.status) + '"></span>' + esc(state.repository.label);
|
|
5850
|
-
repositoryRefreshed = true;
|
|
5851
5967
|
}
|
|
5852
5968
|
}
|
|
5853
5969
|
} catch {
|
|
@@ -5858,7 +5974,11 @@ async function localFetch(url, options) {
|
|
|
5858
5974
|
} catch {
|
|
5859
5975
|
throw new Error("The filegrc server is unavailable. Restart npm run serve, or pnpm dev in the monorepo, and try again.");
|
|
5860
5976
|
} finally {
|
|
5861
|
-
if (
|
|
5977
|
+
if (chip?.isConnected) {
|
|
5978
|
+
chip.classList.remove("is-loading");
|
|
5979
|
+
chip.removeAttribute("aria-busy");
|
|
5980
|
+
if (!repositoryRefreshed && previousChip) chip.innerHTML = previousChip;
|
|
5981
|
+
}
|
|
5862
5982
|
}
|
|
5863
5983
|
}
|
|
5864
5984
|
async function fetchJson(url, options) { const response = await localFetch(url, options); if (!response.ok) throw new Error(await responseMessage(response)); return response.json(); }
|
|
@@ -5867,9 +5987,11 @@ function esc(value) { return String(value ?? "").replace(/[&<>"']/g, (character)
|
|
|
5867
5987
|
|
|
5868
5988
|
export const APP_STYLES = String.raw`
|
|
5869
5989
|
:root{--ink:#151827;--muted:#5d6475;--line:#dfe3ef;--paper:#f6f7fb;--panel:#fff;--accent:#0000a5;--accent-soft:#eef1ff;--accent-light:#8aa1ff;--focus:#0000e0;--amber:#8a5200;--red:#a13a31;--sidebar:linear-gradient(135deg,#000070 0%,#000035 60%);--primary-gradient:linear-gradient(135deg,#000070 0%,#000035 60%);--surface-soft:#f2f4fa;--surface-muted:#eceff7;--field:#fff;--field-readonly:#eef0f6;--code-bg:#10162b;--code-ink:#e8ebff;--shadow:0 8px 28px rgba(0,0,53,.08);color-scheme:light dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--ink);background:var(--paper);font-synthesis:none}
|
|
5870
|
-
*{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--paper)}button,input,select,textarea{font:inherit}a{color:inherit}.skip-link{position:fixed;left:1rem;top:-4rem;z-index:100;padding:.7rem 1rem;background:#fff}.skip-link:focus{top:1rem}.loading,.fatal{padding:3rem}.shell{display:grid;grid-template-columns:248px 1fr;min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;width:248px;background:var(--sidebar);color:#eef1ff;padding:25px 18px 18px;overflow:auto;z-index:20}.brand{display:flex;align-items:center;gap:12px;text-decoration:none;margin:0 7px 27px}.brand .mark{display:block;width:39px;height:39px;border-radius:10px}.brand strong,.brand small{display:block}.brand strong{color:#fff;font-size:18px}.brand small{font-size:13.2px;color:#c5cae2;margin-top:2px}.nav-home,.nav-items a{display:flex;justify-content:space-between;align-items:center;text-decoration:none;border-radius:7px;padding:8px 10px;font-size:15.6px;color:#d5d9ed}.nav-home{margin-bottom:9px}.nav-home:hover,.nav-items a:hover,.nav-home.current,.nav-items a.current{background:#202066;color:#fff}.nav-heading{width:100%;border:0;background:none;color:#b4bbdc;text-transform:uppercase;letter-spacing:.11em;font-size:12px;font-weight:750;display:flex;align-items:center;justify-content:space-between;padding:13px 10px 5px;cursor:pointer}.chevron{display:grid;place-items:center;width:14px;height:22px;font-size:0;line-height:1;transform:none}.chevron:before{content:"";width:6px;height:6px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transform-origin:center;transition:transform .15s}.nav-items{display:none}.nav-group.open .nav-items{display:block}.nav-items small{font-size:12px;color:#b8bed7}.side-foot{position:sticky;bottom:-18px;margin:25px -18px -18px;padding:17px 25px;background:#000024;border-top:1px solid #34345f;color:#cbd0e5;font-size:13.2px;display:flex;align-items:center;gap:8px}.status-dot{width:8px;height:8px;border-radius:50%;background:#9aa39f;display:inline-block;flex:0 0 auto}.status-dot.good,.badge.good{background:#6abf8c}.status-dot.warn,.badge.warn{background:#e9a445}.status-dot.bad,.badge.bad{background:#dc6c5d}.status-dot.neutral{background:#9aabff}.workspace{grid-column:2;min-width:0}.topbar{height:86px;background:rgba(255,255,255,.88);backdrop-filter:blur(10px);border-bottom:1px solid var(--line);padding:0 32px;display:flex;align-items:center;gap:23px;position:sticky;top:0;z-index:10}.topbar>div:first-of-type{min-width:190px}.topbar h1{font-size:20.4px;line-height:1.1;margin:3px 0 0}.eyebrow,.kicker{color:var(--accent);text-transform:uppercase;letter-spacing:.12em;font-weight:760;font-size:10.8px;margin:0}.search{height:39px;max-width:240px;flex:1;margin-left:auto;display:flex;align-items:center;gap:9px;background:#f2f4fa;border:1px solid #dfe3ef;border-radius:8px;padding:0 10px;color:#5d6475}.search input{border:0;outline:0;background:none;min-width:0;flex:1;font-size:15.6px}.search kbd{background:#fff;border:1px solid #dfe3ef;border-radius:4px;padding:1px 5px;font-size:12px}.mobile-sidebar-search{display:none}.repo-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.mobile-nav{display:none}.page{padding:30px 34px 70px;max-width:1510px;margin:auto}.hero{color:#f8f9ff;background:linear-gradient(120deg,#000070,#000035);border-radius:13px;padding:28px 31px;display:flex;justify-content:space-between;align-items:end;min-height:158px;box-shadow:var(--shadow);position:relative;overflow:hidden}.hero:after{content:"";position:absolute;width:270px;height:270px;border:55px solid rgba(138,161,255,.1);border-radius:50%;right:-80px;top:-145px}.hero .kicker{color:#cbd3ff}.hero h2{font-family:Georgia,serif;font-weight:500;font-size:33.6px;margin:10px 0 8px;letter-spacing:-.02em}.hero p:not(.kicker){margin:0;color:#dde1f4;font-size:15.6px;max-width:650px}.hero-meta{display:flex;gap:15px;position:relative;z-index:1}.hero-meta span{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#e6e8f7;border-left:1px solid #6874ab;padding-left:15px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:14px 0}.metric{background:#fff;border:1px solid var(--line);border-radius:10px;padding:16px 18px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.metric-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);display:flex;align-items:center;gap:7px}.metric>strong{display:block;font-family:Georgia,serif;font-size:30px;font-weight:500;margin:8px 0 2px}.metric>small{font-size:12px;color:#697184}.dashboard-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.panel{background:#fff;border:1px solid var(--line);border-radius:11px;padding:21px;min-width:0;box-shadow:0 2px 8px rgba(21,40,33,.025)}.span-2{grid-column:span 2}.panel-head{display:flex;align-items:start;justify-content:space-between;gap:15px;margin-bottom:18px}.panel-head h3{font-size:16.8px;margin:4px 0 0}.panel-head>a{font-size:13.2px;color:var(--accent);font-weight:700}.audit-progress{display:grid;grid-template-columns:105px 1fr;gap:11px 20px;align-items:end}.progress-number strong{font-family:Georgia,serif;font-size:36px;font-weight:500;display:block}.progress-number span{font-size:12px;color:var(--muted)}.progress{height:9px;background:#eceff7;border-radius:9px;overflow:hidden}.progress span{display:block;height:100%;background:linear-gradient(90deg,#0000a5,var(--accent-light));border-radius:9px}.progress-meta{grid-column:2;display:flex;justify-content:space-between;font-size:10.8px;text-transform:uppercase;letter-spacing:.08em;color:#5d6475}.due-list{display:grid}.due-list a{display:grid;grid-template-columns:60px 1fr;text-decoration:none;border-top:1px solid #e8ebf3;padding:10px 0;align-items:center}.due-list a:first-child{border:0;padding-top:0}.due-list time{font-size:12px;color:var(--accent);font-weight:750}.due-list strong,.due-list small{display:block}.due-list strong{font-size:13.2px}.due-list small{font-size:10.8px;color:var(--muted);margin-top:3px}.resource-bars{display:grid;gap:11px}.resource-bars a{display:grid;grid-template-columns:105px 1fr 20px;gap:9px;align-items:center;text-decoration:none;font-size:12px}.resource-bars i{height:5px;background:#e8ebf3;border-radius:5px;overflow:hidden}.resource-bars b{display:block;height:100%;background:#6676dd;border-radius:5px}.resource-bars strong{text-align:right}.catalog{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}.catalog a{display:flex;justify-content:space-between;text-decoration:none;padding:9px 11px;background:#f2f4fa;border-radius:6px;font-size:12px}.catalog a:hover{background:var(--accent-soft)}.page-intro{display:flex;justify-content:space-between;align-items:end;margin-bottom:25px}.page-intro h2,.detail-head h2{font-family:Georgia,serif;font-size:37.2px;font-weight:500;margin:7px 0}.page-intro p:not(.kicker){color:var(--muted);max-width:700px;font-size:15.6px;margin:0}.button{border:1px solid #d0d5e3;background:#fff;border-radius:7px;padding:9px 13px;cursor:pointer;font-size:14.4px;font-weight:650}.button.primary{background:var(--accent);border-color:var(--accent);color:#fff}.button.danger{color:var(--red)}.list-tools{display:flex;align-items:center;gap:10px;margin-bottom:12px}.list-tools label{flex:1}.list-tools input,.list-tools select{width:100%;border:1px solid var(--line);border-radius:7px;background:#fff;padding:10px 12px;font-size:14.4px}.list-tools select{width:auto}.list-tools>span{color:var(--muted);font-size:12px}.record-table-wrap{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:auto}.record-table{width:100%;border-collapse:collapse;font-size:13.2px}.record-table th{background:#f2f4fa;text-align:left;text-transform:uppercase;letter-spacing:.08em;color:#75817b;font-size:10.8px;padding:11px 14px;border-bottom:1px solid var(--line)}.record-table td{padding:13px 14px;border-bottom:1px solid #e8ebf3;vertical-align:top}.record-table tr:last-child td{border-bottom:0}.record-table code{font-size:10.8px;color:#5d6475}.record-title{display:block;color:var(--ink);font-weight:700;text-decoration:none}.record-table td>small{display:block;color:#6a7181;margin-top:3px}.record-table td[data-label="Description"]{min-width:260px;max-width:520px;color:var(--muted);line-height:1.45}.badge,.tag,.type-pill{display:inline-block;border-radius:99px;background:#eceff7;padding:3px 7px;font-size:10.8px;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap}.tag{text-transform:none;margin:1px}.badge.status-active,.badge.status-approved,.badge.status-complete,.badge.status-passed,.badge.status-accepted{background:#ddefe5;color:#176143}.badge.status-open,.badge.status-high,.badge.status-critical,.badge.status-failed{background:#f5ded9;color:#8d352c}.badge.status-draft,.badge.status-planned,.badge.status-in-progress,.badge.status-medium{background:#f7e9cf;color:#855717}.breadcrumbs{display:flex;gap:8px;color:var(--muted);font-size:13.2px;margin-bottom:20px}.detail-head{display:flex;justify-content:space-between;align-items:end;margin-bottom:22px}.detail-head h2{margin-bottom:4px}.detail-head>div>code{font-size:12px;color:var(--muted)}.actions{display:flex;gap:7px}.detail-grid{display:grid;grid-template-columns:minmax(0,2fr) minmax(270px,1fr);gap:14px}.detail-grid aside{display:grid;gap:14px;align-content:start}.detail-main{padding:29px}.content-label{color:#75817b;text-transform:uppercase;letter-spacing:.08em;font-size:10.8px;border-bottom:1px solid var(--line);padding-bottom:13px;margin-bottom:23px}.markdown{max-width:790px}.markdown h1{font-family:Georgia,serif;font-size:34.8px;font-weight:500}.markdown h2{font-family:Georgia,serif;font-size:27.6px;font-weight:500;margin-top:1.8em}.markdown h3{font-size:18px;margin-top:1.7em}.markdown p,.markdown li{font-size:15.6px;line-height:1.65;color:#272c3b}.markdown code{background:#eef0f6;border-radius:3px;padding:1px 4px}.markdown pre{padding:15px;background:#10162b;color:#e8ebff;border-radius:7px;overflow:auto}.markdown blockquote{border-left:3px solid var(--accent-light);padding:4px 15px;color:var(--muted);margin-left:0}.table-wrap{overflow:auto}.markdown table{border-collapse:collapse;width:100%;font-size:13.2px}.markdown th,.markdown td{border:1px solid var(--line);padding:8px;text-align:left}.metadata{margin:0}.metadata>div{display:grid;grid-template-columns:105px 1fr;gap:10px;border-top:1px solid #e8ebf3;padding:10px 0}.metadata>div:first-child{border-top:0;padding-top:0}.metadata dt{font-size:10.8px;text-transform:uppercase;letter-spacing:.06em;color:#5d6475}.metadata dd{margin:0;font-size:13.2px;min-width:0}.compact-json{white-space:pre-wrap;font-size:10.8px}.git-panel>code{font-size:10.8px;word-break:break-all}.git-panel p{font-size:12px;color:var(--muted)}.relation{color:var(--accent);text-decoration:none}.history{display:grid}.history>div{display:grid;grid-template-columns:60px 1fr;gap:8px;padding:8px 0;border-top:1px solid #e8ebf3}.history>div:first-child{border-top:0}.history code{font-size:10.8px;color:var(--accent)}.history strong,.history small{display:block}.history strong{font-size:12px}.history small{font-size:10.8px;color:var(--muted);margin-top:2px}.empty{padding:25px;color:#697184;text-align:center;font-size:13.2px;background:#f4f5fa;border-radius:7px}.changes{padding-left:18px}.changes li{margin:8px 0}.diagnostics>div{display:grid;grid-template-columns:58px minmax(120px,180px) minmax(0,1fr);gap:10px;align-items:start;border-top:1px solid var(--line);padding:10px 0}.diagnostics p{margin:0;font-size:13.2px;overflow-wrap:anywhere}.diagnostics code{font-size:10.8px;overflow-wrap:anywhere}.editor,.search-results{width:min(760px,calc(100vw - 30px));border:0;border-radius:12px;padding:0;box-shadow:0 25px 80px rgba(0,0,24,.28)}dialog::backdrop{background:rgba(0,0,24,.55)}.editor form,.search-results{padding:23px}.dialog-head{display:flex;justify-content:space-between;align-items:start}.dialog-head h2{font-family:Georgia,serif;font-weight:500;margin:5px 0 0}.icon-button{border:0;background:#eceff7;width:32px;height:32px;border-radius:50%;font-size:26.4px;cursor:pointer}.editor form>p{font-size:13.2px;color:var(--muted)}.editor textarea{width:100%;height:440px;border:1px solid var(--line);border-radius:7px;background:#10162b;color:#e8ebff;padding:15px;font:13.2px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;tab-size:2}.dialog-actions{display:flex;justify-content:end;gap:8px;margin-top:14px}.dialog-error{color:var(--red);font-size:13.2px;min-height:18px;margin-top:7px}.result-list{display:grid;margin-top:17px;max-height:60vh;overflow:auto}.result-list a{display:block;text-decoration:none;padding:11px;border-top:1px solid var(--line)}.result-list strong,.result-list small{display:block}.result-list small{color:var(--muted);margin-top:3px}.muted{color:#737a8b}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
5990
|
+
*{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--paper)}button,input,select,textarea{font:inherit}a{color:inherit}@keyframes spinner-rotate{to{transform:rotate(360deg)}}.spinner{display:inline-block;flex:0 0 auto;width:1em;height:1em;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:spinner-rotate .7s linear infinite}.spinner-large{width:30px;height:30px;border-width:3px}.loading,.detail-loading,.muted.is-loading,.save-status.is-loading,.onboarding-save-status.is-loading,.button.is-loading{display:flex;align-items:center;gap:9px}.loading{min-height:100vh;justify-content:center;color:var(--muted)}.state-loading{display:flex;align-items:flex-start;gap:16px}.state-loading>.spinner{margin-top:5px;color:var(--accent)}.state-loading h2{margin:5px 0 8px}.state-loading p:last-child{margin:0;color:var(--muted)}.skip-link{position:fixed;left:1rem;top:-4rem;z-index:100;padding:.7rem 1rem;background:#fff}.skip-link:focus{top:1rem}.loading,.fatal{padding:3rem}.shell{display:grid;grid-template-columns:248px 1fr;min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;width:248px;background:var(--sidebar);color:#eef1ff;padding:25px 18px 18px;overflow:auto;z-index:20}.brand{display:flex;align-items:center;gap:12px;text-decoration:none;margin:0 7px 27px}.brand .mark{display:block;width:39px;height:39px;border-radius:10px}.brand strong,.brand small{display:block}.brand strong{color:#fff;font-size:18px}.brand small{font-size:13.2px;color:#c5cae2;margin-top:2px}.nav-home,.nav-items a{display:flex;justify-content:space-between;align-items:center;text-decoration:none;border-radius:7px;padding:8px 10px;font-size:15.6px;color:#d5d9ed}.nav-home{margin-bottom:9px}.nav-home:hover,.nav-items a:hover,.nav-home.current,.nav-items a.current{background:#202066;color:#fff}.nav-heading{width:100%;border:0;background:none;color:#b4bbdc;text-transform:uppercase;letter-spacing:.11em;font-size:12px;font-weight:750;display:flex;align-items:center;justify-content:space-between;padding:13px 10px 5px;cursor:pointer}.chevron{display:grid;place-items:center;width:14px;height:22px;font-size:0;line-height:1;transform:none}.chevron:before{content:"";width:6px;height:6px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transform-origin:center;transition:transform .15s}.nav-items{display:none}.nav-group.open .nav-items{display:block}.nav-items small{font-size:12px;color:#b8bed7}.side-foot{position:sticky;bottom:-18px;margin:25px -18px -18px;padding:17px 25px;background:#000024;border-top:1px solid #34345f;color:#cbd0e5;font-size:13.2px;display:flex;align-items:center;gap:8px}.status-dot{width:8px;height:8px;border-radius:50%;background:#9aa39f;display:inline-block;flex:0 0 auto}.status-dot.good,.badge.good{background:#6abf8c}.status-dot.warn,.badge.warn{background:#e9a445}.status-dot.bad,.badge.bad{background:#dc6c5d}.status-dot.neutral{background:#9aabff}.workspace{grid-column:2;min-width:0}.topbar{height:86px;background:rgba(255,255,255,.88);backdrop-filter:blur(10px);border-bottom:1px solid var(--line);padding:0 32px;display:flex;align-items:center;gap:23px;position:sticky;top:0;z-index:10}.topbar>div:first-of-type{min-width:190px}.topbar h1{font-size:20.4px;line-height:1.1;margin:3px 0 0}.eyebrow,.kicker{color:var(--accent);text-transform:uppercase;letter-spacing:.12em;font-weight:760;font-size:10.8px;margin:0}.search{height:39px;max-width:240px;flex:1;margin-left:auto;display:flex;align-items:center;gap:9px;background:#f2f4fa;border:1px solid #dfe3ef;border-radius:8px;padding:0 10px;color:#5d6475}.search input{border:0;outline:0;background:none;min-width:0;flex:1;font-size:15.6px}.search kbd{background:#fff;border:1px solid #dfe3ef;border-radius:4px;padding:1px 5px;font-size:12px}.mobile-sidebar-search{display:none}.repo-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.mobile-nav{display:none}.page{padding:30px 34px 70px;max-width:1510px;margin:auto}.hero{color:#f8f9ff;background:linear-gradient(120deg,#000070,#000035);border-radius:13px;padding:28px 31px;display:flex;justify-content:space-between;align-items:end;min-height:158px;box-shadow:var(--shadow);position:relative;overflow:hidden}.hero:after{content:"";position:absolute;width:270px;height:270px;border:55px solid rgba(138,161,255,.1);border-radius:50%;right:-80px;top:-145px}.hero .kicker{color:#cbd3ff}.hero h2{font-family:Georgia,serif;font-weight:500;font-size:33.6px;margin:10px 0 8px;letter-spacing:-.02em}.hero p:not(.kicker){margin:0;color:#dde1f4;font-size:15.6px;max-width:650px}.hero-meta{display:flex;gap:15px;position:relative;z-index:1}.hero-meta span{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#e6e8f7;border-left:1px solid #6874ab;padding-left:15px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:14px 0}.metric{background:#fff;border:1px solid var(--line);border-radius:10px;padding:16px 18px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.metric-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);display:flex;align-items:center;gap:7px}.metric>strong{display:block;font-family:Georgia,serif;font-size:30px;font-weight:500;margin:8px 0 2px}.metric>small{font-size:12px;color:#697184}.dashboard-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.panel{background:#fff;border:1px solid var(--line);border-radius:11px;padding:21px;min-width:0;box-shadow:0 2px 8px rgba(21,40,33,.025)}.span-2{grid-column:span 2}.panel-head{display:flex;align-items:start;justify-content:space-between;gap:15px;margin-bottom:18px}.panel-head h3{font-size:16.8px;margin:4px 0 0}.panel-head>a{font-size:13.2px;color:var(--accent);font-weight:700}.audit-progress{display:grid;grid-template-columns:105px 1fr;gap:11px 20px;align-items:end}.progress-number strong{font-family:Georgia,serif;font-size:36px;font-weight:500;display:block}.progress-number span{font-size:12px;color:var(--muted)}.progress{height:9px;background:#eceff7;border-radius:9px;overflow:hidden}.progress span{display:block;height:100%;background:linear-gradient(90deg,#0000a5,var(--accent-light));border-radius:9px}.progress-meta{grid-column:2;display:flex;justify-content:space-between;font-size:10.8px;text-transform:uppercase;letter-spacing:.08em;color:#5d6475}.due-list{display:grid}.due-list a{display:grid;grid-template-columns:60px 1fr;text-decoration:none;border-top:1px solid #e8ebf3;padding:10px 0;align-items:center}.due-list a:first-child{border:0;padding-top:0}.due-list time{font-size:12px;color:var(--accent);font-weight:750}.due-list strong,.due-list small{display:block}.due-list strong{font-size:13.2px}.due-list small{font-size:10.8px;color:var(--muted);margin-top:3px}.resource-bars{display:grid;gap:11px}.resource-bars a{display:grid;grid-template-columns:105px 1fr 20px;gap:9px;align-items:center;text-decoration:none;font-size:12px}.resource-bars i{height:5px;background:#e8ebf3;border-radius:5px;overflow:hidden}.resource-bars b{display:block;height:100%;background:#6676dd;border-radius:5px}.resource-bars strong{text-align:right}.catalog{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}.catalog a{display:flex;justify-content:space-between;text-decoration:none;padding:9px 11px;background:#f2f4fa;border-radius:6px;font-size:12px}.catalog a:hover{background:var(--accent-soft)}.page-intro{display:flex;justify-content:space-between;align-items:end;margin-bottom:25px}.page-intro h2,.detail-head h2{font-family:Georgia,serif;font-size:37.2px;font-weight:500;margin:7px 0}.page-intro p:not(.kicker){color:var(--muted);max-width:700px;font-size:15.6px;margin:0}.button{border:1px solid #d0d5e3;background:#fff;border-radius:7px;padding:9px 13px;cursor:pointer;font-size:14.4px;font-weight:650}.button.primary{background:var(--accent);border-color:var(--accent);color:#fff}.button.danger{color:var(--red)}.list-tools{display:flex;align-items:center;gap:10px;margin-bottom:12px}.list-tools label{flex:1}.list-tools input,.list-tools select{width:100%;border:1px solid var(--line);border-radius:7px;background:#fff;padding:10px 12px;font-size:14.4px}.list-tools select{width:auto}.list-tools>span{color:var(--muted);font-size:12px}.record-table-wrap{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:auto}.record-table{width:100%;border-collapse:collapse;font-size:13.2px}.record-table th{background:#f2f4fa;text-align:left;text-transform:uppercase;letter-spacing:.08em;color:#75817b;font-size:10.8px;padding:11px 14px;border-bottom:1px solid var(--line)}.record-table td{padding:13px 14px;border-bottom:1px solid #e8ebf3;vertical-align:top}.record-table tr:last-child td{border-bottom:0}.record-table code{font-size:10.8px;color:#5d6475}.record-title{display:block;color:var(--ink);font-weight:700;text-decoration:none}.record-table td>small{display:block;color:#6a7181;margin-top:3px}.record-table td[data-label="Description"]{min-width:260px;max-width:520px;color:var(--muted);line-height:1.45}.badge,.tag,.type-pill{display:inline-block;border-radius:99px;background:#eceff7;padding:3px 7px;font-size:10.8px;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap}.tag{text-transform:none;margin:1px}.badge.status-active,.badge.status-approved,.badge.status-complete,.badge.status-passed,.badge.status-accepted{background:#ddefe5;color:#176143}.badge.status-open,.badge.status-high,.badge.status-critical,.badge.status-failed{background:#f5ded9;color:#8d352c}.badge.status-draft,.badge.status-planned,.badge.status-in-progress,.badge.status-medium{background:#f7e9cf;color:#855717}.breadcrumbs{display:flex;gap:8px;color:var(--muted);font-size:13.2px;margin-bottom:20px}.detail-head{display:flex;justify-content:space-between;align-items:end;margin-bottom:22px}.detail-head h2{margin-bottom:4px}.detail-head>div>code{font-size:12px;color:var(--muted)}.actions{display:flex;gap:7px}.detail-grid{display:grid;grid-template-columns:minmax(0,2fr) minmax(270px,1fr);gap:14px}.detail-grid aside{display:grid;gap:14px;align-content:start}.detail-main{padding:29px}.content-label{color:#75817b;text-transform:uppercase;letter-spacing:.08em;font-size:10.8px;border-bottom:1px solid var(--line);padding-bottom:13px;margin-bottom:23px}.markdown{max-width:790px}.markdown h1{font-family:Georgia,serif;font-size:34.8px;font-weight:500}.markdown h2{font-family:Georgia,serif;font-size:27.6px;font-weight:500;margin-top:1.8em}.markdown h3{font-size:18px;margin-top:1.7em}.markdown p,.markdown li{font-size:15.6px;line-height:1.65;color:#272c3b}.markdown code{background:#eef0f6;border-radius:3px;padding:1px 4px}.markdown pre{padding:15px;background:#10162b;color:#e8ebff;border-radius:7px;overflow:auto}.markdown blockquote{border-left:3px solid var(--accent-light);padding:4px 15px;color:var(--muted);margin-left:0}.table-wrap{overflow:auto}.markdown table{border-collapse:collapse;width:100%;font-size:13.2px}.markdown th,.markdown td{border:1px solid var(--line);padding:8px;text-align:left}.metadata{margin:0}.metadata>div{display:grid;grid-template-columns:105px 1fr;gap:10px;border-top:1px solid #e8ebf3;padding:10px 0}.metadata>div:first-child{border-top:0;padding-top:0}.metadata dt{font-size:10.8px;text-transform:uppercase;letter-spacing:.06em;color:#5d6475}.metadata dd{margin:0;font-size:13.2px;min-width:0}.compact-json{white-space:pre-wrap;font-size:10.8px}.git-panel>code{font-size:10.8px;word-break:break-all}.git-panel p{font-size:12px;color:var(--muted)}.relation{color:var(--accent);text-decoration:none}.history{display:grid}.history>div{display:grid;grid-template-columns:60px 1fr;gap:8px;padding:8px 0;border-top:1px solid #e8ebf3}.history>div:first-child{border-top:0}.history code{font-size:10.8px;color:var(--accent)}.history strong,.history small{display:block}.history strong{font-size:12px}.history small{font-size:10.8px;color:var(--muted);margin-top:2px}.empty{padding:25px;color:#697184;text-align:center;font-size:13.2px;background:#f4f5fa;border-radius:7px}.changes{padding-left:18px}.changes li{margin:8px 0}.diagnostics>div{display:grid;grid-template-columns:58px minmax(120px,180px) minmax(0,1fr);gap:10px;align-items:start;border-top:1px solid var(--line);padding:10px 0}.diagnostics p{margin:0;font-size:13.2px;overflow-wrap:anywhere}.diagnostics code{font-size:10.8px;overflow-wrap:anywhere}.editor,.search-results{width:min(760px,calc(100vw - 30px));border:0;border-radius:12px;padding:0;box-shadow:0 25px 80px rgba(0,0,24,.28)}dialog::backdrop{background:rgba(0,0,24,.55)}.editor form,.search-results{padding:23px}.dialog-head{display:flex;justify-content:space-between;align-items:start}.dialog-head h2{font-family:Georgia,serif;font-weight:500;margin:5px 0 0}.icon-button{border:0;background:#eceff7;width:32px;height:32px;border-radius:50%;font-size:26.4px;cursor:pointer}.editor form>p{font-size:13.2px;color:var(--muted)}.editor textarea{width:100%;height:440px;border:1px solid var(--line);border-radius:7px;background:#10162b;color:#e8ebff;padding:15px;font:13.2px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;tab-size:2px}.dialog-actions{display:flex;justify-content:end;gap:8px;margin-top:14px}.dialog-error{color:var(--red);font-size:13.2px;min-height:18px;margin-top:7px}.result-list{display:grid;margin-top:17px;max-height:60vh;overflow:auto}.result-list a{display:block;text-decoration:none;padding:11px;border-top:1px solid var(--line)}.result-list strong,.result-list small{display:block}.result-list small{color:var(--muted);margin-top:3px}.muted{color:#737a8b}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
5871
5991
|
.topbar-readiness{display:grid;grid-template-columns:minmax(0,1fr);gap:3px;flex:0 1 220px;min-width:155px;padding:5px 8px;border:1px solid transparent;border-radius:8px;color:var(--ink);text-decoration:none}.topbar-readiness:hover{border-color:var(--accent-light);background:var(--accent-soft)}.topbar-readiness-copy{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px;align-items:center}.topbar-readiness-copy>span{overflow:hidden;color:var(--muted);font-size:9.6px;font-weight:700;text-overflow:ellipsis;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap}.topbar-readiness-copy>strong{font-size:10.8px;line-height:1}.topbar-readiness>.progress{width:100%;height:5px}.topbar-status{display:flex;flex:1 1 auto;min-width:0;align-items:center;justify-content:flex-end;gap:8px;margin-left:auto}.program-select select{max-width:180px;border:1px solid var(--line);border-radius:8px;padding:9px 28px 9px 10px;background:var(--surface);color:var(--ink);font:inherit}.topbar-status .topbar-search{flex:0 1 240px;width:240px;min-width:140px;margin-left:0}.repo-chip,.validation-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);font-size:13.2px;white-space:nowrap;text-decoration:none}.repo-chip:hover,.validation-chip:hover{color:var(--ink);border-color:var(--accent-light)}
|
|
5872
|
-
.metadata>div{grid-template-columns:minmax(140px,1fr) minmax(0,2.5fr)}.metadata dt{min-width:0;overflow-wrap:anywhere}
|
|
5992
|
+
.metadata>div{grid-template-columns:minmax(140px,1fr) minmax(0,2.5fr)}.metadata dt{min-width:0;overflow-wrap:anywhere}.editor textarea{tab-size:2}
|
|
5993
|
+
.is-loading>.spinner,.state-loading>.spinner{align-self:center;margin-top:0}.state-loading,.repository-sync-alert.is-loading{align-items:center}.topbar-readiness.is-loading .topbar-readiness-copy>strong{display:flex;align-items:center;justify-content:center}
|
|
5994
|
+
@media(prefers-reduced-motion:reduce){.spinner{animation:none}}
|
|
5873
5995
|
.audit-progress-empty{padding:11px 13px;border-radius:8px;background:var(--surface-soft)}.audit-progress-empty strong,.audit-progress-empty span{display:block}.audit-progress-empty strong{font-size:13.2px}.audit-progress-empty span{margin-top:4px;color:var(--muted);font-size:10.8px}
|
|
5874
5996
|
.icon-button{position:relative;display:grid;place-items:center;padding:0;color:var(--ink);font-size:0}.icon-button:before,.icon-button:after{content:"";position:absolute;width:13px;height:2px;border-radius:2px;background:currentColor;transform:rotate(45deg)}.icon-button:after{transform:rotate(-45deg)}
|
|
5875
5997
|
html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(0,1fr);height:100vh;min-height:0}.sidebar{display:flex;flex-direction:column;height:100vh;overflow:hidden;overscroll-behavior:contain}.workspace{height:100vh;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}
|
|
@@ -5881,6 +6003,9 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
|
|
|
5881
6003
|
.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}
|
|
5882
6004
|
.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}.workflow-guidance-more>summary{cursor:pointer;font-weight:750}.workflow-findings-more{margin-top:9px}.workflow-guidance-more>p{margin:8px 0 0}.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}.policy-lifecycle-note{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(260px,1fr);gap:24px;margin-top:18px}.policy-lifecycle-note h3{margin:5px 0 8px}.policy-lifecycle-note p:not(.kicker),.policy-library-proposals p{color:var(--muted);font-size:11px;line-height:1.55}.policy-library-proposals{display:grid;gap:8px}.policy-library-proposals article{padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.policy-library-proposals p{margin:5px 0 8px}.policy-activation{display:grid;gap:12px;margin-top:22px}.policy-activation-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.policy-activation-card{padding:17px 18px;border:1px solid #d8bd78;border-radius:10px;background:var(--panel)}.policy-activation-card.active-and-operating{border-color:#b9dac6}.policy-activation-card h3{margin:7px 0 0}.policy-activation-gaps{display:grid;gap:9px;margin-top:13px}.policy-activation-gaps>div{padding-top:8px;border-top:1px solid var(--line)}.policy-activation-gaps small{display:block;margin-bottom:6px;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.05em}.policy-activation-warning{margin:11px 0 0;padding:9px 10px;border-radius:7px;background:#f7e9cf;color:#6d4917;font-size:10.4px;line-height:1.45}.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}
|
|
5883
6005
|
.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}
|
|
6006
|
+
.collection-review-panel.current details{margin-top:8px}.collection-review-detail-content{margin-top:10px}.collection-review-complete-summary{display:flex;align-items:baseline;gap:10px}.collection-review-complete-summary>.kicker{margin:0;white-space:nowrap}.collection-review-panel.current .collection-review-foot{justify-content:flex-end;margin-top:8px}
|
|
6007
|
+
.collection-review-current-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.collection-review-panel.current .collection-review-details{flex:0 0 auto;margin:0;padding:0;border:0;background:transparent}.collection-review-panel.current .collection-review-details>summary{color:var(--accent);white-space:nowrap}.collection-review-panel.current .collection-review-details[open]{flex-basis:100%;order:3;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-panel.current .collection-review-details[open]>summary{margin-bottom:10px}.collection-review-panel.current .collection-review-details[open] .collection-review-detail-content{margin-top:0}.collection-review-panel.current .button{margin-left:auto}
|
|
6008
|
+
@media(max-width:520px){.collection-review-complete-summary{flex:1 1 180px;min-width:0;flex-wrap:wrap}.collection-review-complete-summary .collection-review-result{min-width:0;flex-wrap:wrap}.collection-review-complete-summary .collection-review-result span{overflow-wrap:anywhere}.collection-review-panel.current .button{margin-left:0}}
|
|
5884
6009
|
.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)}
|
|
5885
6010
|
.policy-lifecycle-note.single{grid-template-columns:1fr}
|
|
5886
6011
|
.policy-content-table{margin-top:22px}.policy-content-table>.section-head{align-items:end}.policy-content-table>.section-head h2{font:500 27.6px Georgia,serif;margin:5px 0 6px}.policy-content-table>.section-head p:not(.kicker){max-width:720px;margin:0;color:var(--muted);font-size:12px;line-height:1.5}.policy-content-tools{display:flex;align-items:center;justify-content:flex-end;gap:9px;flex-wrap:wrap}.policy-content-tools input[type="search"]{min-width:210px;border:1px solid var(--line);border-radius:7px;background:var(--field);padding:9px 11px}.history-toggle{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:11px}.policy-content-add{position:relative}.policy-content-add>summary{list-style:none}.policy-content-add>summary::-webkit-details-marker{display:none}.policy-content-add>div{position:absolute;right:0;z-index:5;display:grid;min-width:150px;margin-top:6px;padding:5px;border:1px solid var(--line);border-radius:8px;background:var(--panel);box-shadow:var(--shadow)}.policy-content-add button{border:0;border-radius:5px;background:transparent;padding:8px 10px;color:var(--ink);text-align:left;cursor:pointer}.policy-content-add button:hover{background:var(--accent-soft)}.policy-content-type span,.policy-content-approval span{display:block}.policy-content-type small,.policy-content-approval small{display:block;margin-top:3px;color:var(--muted);font-size:9.6px}.policy-content-count{margin:9px 2px 0;color:var(--muted);font-size:10.8px;text-align:right}
|
|
@@ -6052,6 +6177,9 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
|
|
|
6052
6177
|
.record-table td:has(.relation){min-width:150px}
|
|
6053
6178
|
.record-table .relation{border-radius:7px;line-height:1.35;overflow-wrap:anywhere;word-break:normal}
|
|
6054
6179
|
@media(max-width:760px){.record-table td:has(.relation){min-width:0}.record-table td[data-label]>.relation{grid-column:2}}
|
|
6180
|
+
@media(max-width:760px){.diagnostics>div{grid-template-columns:max-content minmax(0,1fr)}}
|
|
6181
|
+
.workflow-guidance .workflow-status{margin:4px 0 12px;color:var(--muted);font-size:10px}.workflow-completion-requirements{display:inline;appearance:none;-webkit-appearance:none;white-space:nowrap;padding:0;border:0;background:none;color:var(--accent);font:inherit;font-weight:750;cursor:pointer;text-decoration:underline;text-underline-offset:2px}.workflow-completion-requirements:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.completion-requirements-dialog ul{margin:14px 0 0;padding-left:20px;color:var(--muted);font-size:12px;line-height:1.55}
|
|
6182
|
+
.record-completion-state{background:var(--surface-soft)}.record-completion-state h3{margin:4px 0 5px;font-size:16.8px}.record-completion-state p:last-child{margin:0;color:var(--muted);font-size:11px;line-height:1.45}
|
|
6055
6183
|
`;
|
|
6056
6184
|
|
|
6057
6185
|
function safeJson(value) {
|
package/src/workflow.js
CHANGED
|
@@ -217,18 +217,34 @@ export function buildWorkflowDelta(before, after) {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
export function workflowForResource(workflow, type, id) {
|
|
220
|
-
if (!workflow) return {
|
|
220
|
+
if (!workflow) return {
|
|
221
|
+
findings: [],
|
|
222
|
+
workItems: [],
|
|
223
|
+
related: { findings: [], workItems: [], recommended: null },
|
|
224
|
+
recommended: null
|
|
225
|
+
};
|
|
221
226
|
const matches = (item) => item.subject?.type === type && item.subject?.id === id;
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
227
|
+
const related = (item) => (
|
|
228
|
+
item.source?.type === type
|
|
229
|
+
&& item.source?.id === id
|
|
230
|
+
&& !matches(item)
|
|
231
|
+
);
|
|
232
|
+
const directFindings = workflow.findings.filter(matches);
|
|
233
|
+
const directWorkItems = workflow.workItems.filter(matches);
|
|
234
|
+
const relatedFindings = workflow.findings.filter(related);
|
|
235
|
+
const relatedWorkItems = workflow.workItems.filter(related);
|
|
236
|
+
const findings = [...directFindings, ...relatedFindings];
|
|
237
|
+
const workItems = [...directWorkItems, ...relatedWorkItems];
|
|
227
238
|
return {
|
|
228
239
|
contractVersion: workflow.contractVersion,
|
|
229
240
|
assessments: workflow.assessments,
|
|
230
241
|
findings,
|
|
231
242
|
workItems,
|
|
243
|
+
related: {
|
|
244
|
+
findings: relatedFindings,
|
|
245
|
+
workItems: relatedWorkItems,
|
|
246
|
+
recommended: [...relatedFindings, ...relatedWorkItems].sort(compareRecommended)[0] || null
|
|
247
|
+
},
|
|
232
248
|
recommended: [...findings, ...workItems].sort(compareRecommended)[0] || null
|
|
233
249
|
};
|
|
234
250
|
}
|
|
@@ -430,7 +446,7 @@ function recordIncompleteReason(record, loaded, program) {
|
|
|
430
446
|
return {
|
|
431
447
|
state: "ready",
|
|
432
448
|
requiredness: "required",
|
|
433
|
-
message: "Review this criterion against the current service scope
|
|
449
|
+
message: "Review this criterion against the current service scope, then record the applicability decision."
|
|
434
450
|
};
|
|
435
451
|
}
|
|
436
452
|
if (record.type === "appointment" && record.status === "planned") {
|
|
@@ -440,16 +456,16 @@ function recordIncompleteReason(record, loaded, program) {
|
|
|
440
456
|
return {
|
|
441
457
|
state: "ready",
|
|
442
458
|
requiredness: appointmentRequiredness(record, loaded.model),
|
|
443
|
-
message: "Assign a holder, confirm the authority
|
|
459
|
+
message: "Assign a holder, confirm the authority and independence needed, then activate this Appointment on its actual start date."
|
|
444
460
|
};
|
|
445
461
|
}
|
|
446
462
|
const messages = {
|
|
447
|
-
draft: "Complete the record, its relationships, and required Markdown before
|
|
448
|
-
planned: "
|
|
449
|
-
proposed: "Review the proposed work, owner, schedule, and completion
|
|
463
|
+
draft: "Complete the record, its relationships, and required Markdown before review.",
|
|
464
|
+
planned: "Check this planned record against the actual program, then complete its finalization checks.",
|
|
465
|
+
proposed: "Review the proposed work, owner, schedule, and completion requirements before activation.",
|
|
450
466
|
"in-review": "Complete independent review and bind the approval to the exact content revision.",
|
|
451
467
|
open: "Complete or formally dispose of this open work with the required proof.",
|
|
452
|
-
"in-progress": "Finish the work, record
|
|
468
|
+
"in-progress": "Finish the work, record the result, and link the completion proof.",
|
|
453
469
|
blocked: "Resolve the recorded blockers before completing this work.",
|
|
454
470
|
"partially-implemented": "Finish the remaining control design, operation, source, and scheduling work."
|
|
455
471
|
};
|
|
@@ -517,7 +533,7 @@ function finalizationFields(record, model, resources, program) {
|
|
|
517
533
|
fields.push({
|
|
518
534
|
field: "applicabilityReview",
|
|
519
535
|
requiredness: "required",
|
|
520
|
-
message: "Record or refresh the
|
|
536
|
+
message: "Record or refresh the applicability decision, rationale, reviewer, and date. FileGRC records the current material scope automatically."
|
|
521
537
|
});
|
|
522
538
|
}
|
|
523
539
|
if (record.type === "policy" && model.resources.policy?.fields?.programRole && !record.programRole) {
|
|
@@ -570,7 +586,7 @@ function finalizationFields(record, model, resources, program) {
|
|
|
570
586
|
fields.push({
|
|
571
587
|
field,
|
|
572
588
|
requiredness: "conditional",
|
|
573
|
-
message: `
|
|
589
|
+
message: `Add ${fieldLabel(field)} before treating this ${fieldLabel(record.type).toLowerCase()} as complete.`
|
|
574
590
|
});
|
|
575
591
|
}
|
|
576
592
|
return fields;
|
|
@@ -676,9 +692,9 @@ async function sourceCoverageFindings(loaded, program) {
|
|
|
676
692
|
? "The source family has a reviewed authoritative path, valid coverage dates, retrieval ownership, and reconciliation method."
|
|
677
693
|
: record
|
|
678
694
|
? record.status === "active"
|
|
679
|
-
? "
|
|
680
|
-
: "Finish the
|
|
681
|
-
: "Create a source-
|
|
695
|
+
? "Add missing source details and link a passed retrieval test after setting a candidate period."
|
|
696
|
+
: "Finish the source decision, retrieval method, retention, validity dates, and pre-period dry run after setting a candidate period."
|
|
697
|
+
: "Create a source-coverage record and choose FileGRC, an external authoritative System, zero population, or not applicable.",
|
|
682
698
|
subject: { type: "source-coverage", ...(record ? { id: record.id } : {}) },
|
|
683
699
|
dependencies: [],
|
|
684
700
|
actions: record
|