filegrc 0.3.2 → 0.3.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/README.md +2 -2
- package/model/v1.json +8 -5
- package/package.json +1 -1
- package/src/build.js +3 -1
- package/src/cli.js +19 -7
- package/src/favicon.js +3 -108
- package/src/favicon.png +0 -0
- package/src/git-name.js +16 -0
- package/src/git.js +499 -1
- package/src/index.js +4 -1
- package/src/logo-mark-white.png +0 -0
- package/src/mutation.js +4 -1
- package/src/server.js +89 -20
- package/src/startup.js +5 -0
- package/src/state.js +7 -2
- package/src/validate.js +2 -0
- package/src/web.js +85 -17
package/src/server.js
CHANGED
|
@@ -5,9 +5,17 @@ import { getResourceDefinition } from "../model/index.js";
|
|
|
5
5
|
import { prepareAuditWorkspace } from "./audit-preparation.js";
|
|
6
6
|
import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
|
|
7
7
|
import { ensureEvidenceTestDrafts } from "./evidence-tests.js";
|
|
8
|
-
import { FAVICON_PNG } from "./favicon.js";
|
|
8
|
+
import { FAVICON_PNG, LOGO_MARK_PNG } from "./favicon.js";
|
|
9
9
|
import { createResource, deleteResource, updateContent, updateResource } from "./files.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
commitAndPushWorkspace,
|
|
12
|
+
getBrowserRepositoryState,
|
|
13
|
+
getFileHistory,
|
|
14
|
+
pullWorkspace,
|
|
15
|
+
pushWorkspace,
|
|
16
|
+
retryBrowserSync,
|
|
17
|
+
runBrowserMutation
|
|
18
|
+
} from "./git.js";
|
|
11
19
|
import { completeObligationOccurrence, createObligationEvent, planObligations } from "./obligations.js";
|
|
12
20
|
import { isWithin, relativeToWorkspace, resolveWorkspacePath } from "./paths.js";
|
|
13
21
|
import { createAppState } from "./state.js";
|
|
@@ -26,7 +34,9 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
26
34
|
return json(response, 403, { error: "Cross-origin writes are not allowed." });
|
|
27
35
|
}
|
|
28
36
|
if (request.method === "GET" && url.pathname === "/api/state") {
|
|
29
|
-
return json(response, 200, await createAppState(input
|
|
37
|
+
return json(response, 200, await createAppState(input, {
|
|
38
|
+
allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites
|
|
39
|
+
}));
|
|
30
40
|
}
|
|
31
41
|
if (request.method === "GET" && url.pathname === "/api/history") {
|
|
32
42
|
const path = url.searchParams.get("path");
|
|
@@ -44,17 +54,22 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
44
54
|
}));
|
|
45
55
|
}
|
|
46
56
|
if (request.method === "POST" && url.pathname === "/api/obligation-events") {
|
|
47
|
-
|
|
57
|
+
const payload = await readJson(request);
|
|
58
|
+
return json(response, 201, await browserMutation(input, options, {
|
|
59
|
+
message: (result) => `Create policy event: ${result.event?.title || payload.title || payload.eventType}`
|
|
60
|
+
}, () => createObligationEvent(input, payload)));
|
|
48
61
|
}
|
|
49
62
|
if (request.method === "POST" && url.pathname === "/api/obligation-completions") {
|
|
50
63
|
const payload = await readJson(request);
|
|
51
64
|
if (!safeSegment(payload.obligationId)) return json(response, 400, { error: "A safe obligation ID is required." });
|
|
52
|
-
const result = await
|
|
65
|
+
const result = await browserMutation(input, options, {
|
|
66
|
+
message: () => `Complete ${resourceTypeLabel(payload.record?.type)}: ${payload.record?.title || payload.obligationId}`
|
|
67
|
+
}, () => completeObligationOccurrence(input, {
|
|
53
68
|
obligationId: payload.obligationId,
|
|
54
69
|
record: payload.record,
|
|
55
70
|
content: payload.content,
|
|
56
71
|
expectedRevision: payload.revision
|
|
57
|
-
});
|
|
72
|
+
}));
|
|
58
73
|
return json(response, 201, result);
|
|
59
74
|
}
|
|
60
75
|
if (request.method === "GET" && url.pathname === "/api/evidence-packet") {
|
|
@@ -82,34 +97,54 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
82
97
|
});
|
|
83
98
|
}
|
|
84
99
|
if (request.method === "POST" && url.pathname === "/api/audit-preparation") {
|
|
85
|
-
|
|
100
|
+
const payload = await readJson(request);
|
|
101
|
+
return json(response, 201, await browserMutation(input, options, {
|
|
102
|
+
message: () => `Prepare audit: ${payload.auditId || "engagement"}`
|
|
103
|
+
}, () => prepareAuditWorkspace(input, payload)));
|
|
86
104
|
}
|
|
87
105
|
if (request.method === "POST" && url.pathname === "/api/evidence-test-drafts") {
|
|
88
|
-
return json(response, 201, await
|
|
106
|
+
return json(response, 201, await browserMutation(input, options, {
|
|
107
|
+
message: "Create evidence collection test drafts"
|
|
108
|
+
}, () => ensureEvidenceTestDrafts(input)));
|
|
89
109
|
}
|
|
90
110
|
if (request.method === "POST" && url.pathname === "/api/setup") {
|
|
91
|
-
|
|
111
|
+
const payload = await readJson(request);
|
|
112
|
+
return json(response, 200, await browserMutation(input, options, {
|
|
113
|
+
message: (result) => `${payload.draft === true ? "Save onboarding draft" : "Complete onboarding"} for ${result.workspace.organizationName}`
|
|
114
|
+
}, () => setupWorkspace(input, payload)));
|
|
92
115
|
}
|
|
93
116
|
if (request.method === "POST" && url.pathname === "/api/resources") {
|
|
94
117
|
const payload = await readJson(request);
|
|
95
118
|
const record = payload.record ?? payload;
|
|
96
|
-
const result = await
|
|
97
|
-
|
|
119
|
+
const result = await browserMutation(input, options, {
|
|
120
|
+
message: () => `Create ${resourceTypeLabel(record.type)}: ${record.title || record.id}`
|
|
121
|
+
}, () => createResource(input, record, { content: payload.record ? payload.content : undefined }));
|
|
122
|
+
return json(response, 201, { record: result.record, synchronization: result.synchronization });
|
|
98
123
|
}
|
|
99
124
|
if (request.method === "POST" && url.pathname === "/api/commit") {
|
|
125
|
+
await requireManualBrowserGit(input, options);
|
|
100
126
|
const payload = await readJson(request);
|
|
101
127
|
return json(response, 201, await commitAndPushWorkspace(input, payload.message));
|
|
102
128
|
}
|
|
103
129
|
if (request.method === "POST" && url.pathname === "/api/git/pull") {
|
|
130
|
+
await requireManualBrowserGit(input, options);
|
|
104
131
|
return json(response, 200, await pullWorkspace(input));
|
|
105
132
|
}
|
|
106
133
|
if (request.method === "POST" && url.pathname === "/api/git/push") {
|
|
134
|
+
await requireManualBrowserGit(input, options);
|
|
107
135
|
return json(response, 200, await pushWorkspace(input));
|
|
108
136
|
}
|
|
137
|
+
if (request.method === "POST" && url.pathname === "/api/git/retry-sync") {
|
|
138
|
+
return json(response, 200, await retryBrowserSync(input, {
|
|
139
|
+
allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
109
142
|
if (request.method === "PUT" && url.pathname === "/api/content") {
|
|
110
143
|
const payload = await readJson(request);
|
|
111
|
-
const result = await
|
|
112
|
-
|
|
144
|
+
const result = await browserMutation(input, options, {
|
|
145
|
+
message: () => `Update content: ${payload.path}`
|
|
146
|
+
}, () => updateContent(input, payload.path, payload.source, { expectedRevision: payload.revision }));
|
|
147
|
+
return json(response, 200, { path: result.dataRelativePath, synchronization: result.synchronization });
|
|
113
148
|
}
|
|
114
149
|
const match = /^\/api\/resource\/([^/]+)\/([^/]+)$/.exec(url.pathname);
|
|
115
150
|
if (match) {
|
|
@@ -124,19 +159,30 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
124
159
|
if (request.method === "PUT") {
|
|
125
160
|
const payload = await readJson(request);
|
|
126
161
|
const record = payload.record ?? payload;
|
|
127
|
-
const result = await
|
|
162
|
+
const result = await browserMutation(input, options, {
|
|
163
|
+
message: () => `Update ${resourceTypeLabel(type)}: ${record.title || id}`
|
|
164
|
+
}, () => updateResource(input, type, id, record, {
|
|
128
165
|
content: payload.record ? payload.content : undefined,
|
|
129
166
|
expectedRevision: payload.revision,
|
|
130
167
|
expectedContentRevisions: payload.contentRevisions
|
|
131
|
-
});
|
|
132
|
-
return json(response, 200, { record: result.record });
|
|
168
|
+
}));
|
|
169
|
+
return json(response, 200, { record: result.record, synchronization: result.synchronization });
|
|
133
170
|
}
|
|
134
171
|
if (request.method === "DELETE") {
|
|
135
|
-
const result = await
|
|
136
|
-
|
|
172
|
+
const result = await browserMutation(input, options, {
|
|
173
|
+
message: () => `Delete ${resourceTypeLabel(type)}: ${id}`
|
|
174
|
+
}, () => deleteResource(input, type, id, { expectedRevision: url.searchParams.get("revision") }));
|
|
175
|
+
return json(response, 200, {
|
|
176
|
+
deleted: true,
|
|
177
|
+
type,
|
|
178
|
+
id,
|
|
179
|
+
deletedContent: result.deletedContent,
|
|
180
|
+
synchronization: result.synchronization
|
|
181
|
+
});
|
|
137
182
|
}
|
|
138
183
|
}
|
|
139
184
|
if (request.method === "GET" && url.pathname === "/favicon.png") return text(response, 200, FAVICON_PNG, "image/png");
|
|
185
|
+
if (request.method === "GET" && url.pathname === "/logo-mark-white.png") return text(response, 200, LOGO_MARK_PNG, "image/png");
|
|
140
186
|
if (request.method === "GET" && url.pathname === "/filegrc-app.js") return text(response, 200, APP_SCRIPT, "text/javascript; charset=utf-8");
|
|
141
187
|
if (request.method === "GET" && url.pathname === "/filegrc.css") return text(response, 200, APP_STYLES, "text/css; charset=utf-8");
|
|
142
188
|
if (request.method === "GET" && url.pathname.startsWith("/packet/")) {
|
|
@@ -184,7 +230,10 @@ export async function serveWorkspace(input = process.cwd(), options = {}) {
|
|
|
184
230
|
}
|
|
185
231
|
const loaded = await loadWorkspace(input);
|
|
186
232
|
getResourceDefinition(loaded.model, "workspace");
|
|
187
|
-
const server = createFilegrcServer(loaded.root, {
|
|
233
|
+
const server = createFilegrcServer(loaded.root, {
|
|
234
|
+
allowedHosts: [host],
|
|
235
|
+
allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites === true
|
|
236
|
+
});
|
|
188
237
|
await new Promise((resolve, reject) => {
|
|
189
238
|
server.once("error", reject);
|
|
190
239
|
server.listen(port, host, resolve);
|
|
@@ -197,6 +246,26 @@ export async function serveWorkspace(input = process.cwd(), options = {}) {
|
|
|
197
246
|
};
|
|
198
247
|
}
|
|
199
248
|
|
|
249
|
+
function browserMutation(input, options, mutationOptions, task) {
|
|
250
|
+
return runBrowserMutation(input, {
|
|
251
|
+
...mutationOptions,
|
|
252
|
+
allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites === true
|
|
253
|
+
}, task);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function requireManualBrowserGit(input, options) {
|
|
257
|
+
const repository = await getBrowserRepositoryState(input, {
|
|
258
|
+
allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites
|
|
259
|
+
});
|
|
260
|
+
if (repository.mode === "trunk") {
|
|
261
|
+
throw new Error("Browser commit, pull, and push controls are disabled in trunk mode. Saved changes synchronize automatically.");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function resourceTypeLabel(value) {
|
|
266
|
+
return String(value || "record").replaceAll("-", " ");
|
|
267
|
+
}
|
|
268
|
+
|
|
200
269
|
async function readJson(request) {
|
|
201
270
|
const chunks = [];
|
|
202
271
|
let size = 0;
|
|
@@ -295,7 +364,7 @@ function statusFor(error) {
|
|
|
295
364
|
if (/exceeds 2 MB/i.test(error.message)) return 413;
|
|
296
365
|
if (/changed after you opened|source changed|revision changed/i.test(error.message)) return 409;
|
|
297
366
|
if (/already exists|target file already exists/i.test(error.message)) return 409;
|
|
298
|
-
if (/Git could not
|
|
367
|
+
if (/Git could not|upstream branch|multiple remotes|no Git remote|configured repository remote|safe Git name|check out a branch|before trying to (?:pull|push)|authoritative branch|not synchronized|not synced|diverged|waiting to be pushed|Retry sync|outside this FileGRC workspace|worktree has uncommitted changes|development write override|browser commit, pull, and push/i.test(error.message)) return 409;
|
|
299
368
|
if (/not found|ENOENT/i.test(error.message)) return 404;
|
|
300
369
|
if (/invalid|required|unsafe|match|workspace|singleton|commit message|no changes|git history|git user|unknown resource type|must use|must be|content path|data path|path leaves|valid .*date|not found|no active obligations|end date|through date|already exists|EEXIST/i.test(error.message)) return 400;
|
|
301
370
|
return 500;
|
package/src/startup.js
ADDED
package/src/state.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { assessAuditPreparation } from "./audit-preparation.js";
|
|
4
|
-
import { getGitSummary, getWorkspaceHistories } from "./git.js";
|
|
4
|
+
import { getBrowserRepositoryState, getGitSummary, getWorkspaceHistories } from "./git.js";
|
|
5
5
|
import { renderMarkdown } from "./markdown.js";
|
|
6
6
|
import { planObligations } from "./obligations.js";
|
|
7
7
|
import { resolveDataPath } from "./paths.js";
|
|
@@ -42,6 +42,10 @@ export async function createAppState(input = process.cwd(), options = {}) {
|
|
|
42
42
|
|
|
43
43
|
const git = getGitSummary(loaded.root);
|
|
44
44
|
delete git.root;
|
|
45
|
+
const repository = await getBrowserRepositoryState(loaded.root, {
|
|
46
|
+
readOnly: options.readOnly,
|
|
47
|
+
allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites
|
|
48
|
+
});
|
|
45
49
|
const workspace = loaded.workspace ?? {
|
|
46
50
|
schemaVersion: 1,
|
|
47
51
|
dataModelVersion: loaded.model.modelVersion,
|
|
@@ -70,7 +74,8 @@ export async function createAppState(input = process.cwd(), options = {}) {
|
|
|
70
74
|
));
|
|
71
75
|
return {
|
|
72
76
|
generatedAt,
|
|
73
|
-
readOnly: Boolean(options.readOnly),
|
|
77
|
+
readOnly: Boolean(options.readOnly || (repository.mode === "trunk" && !repository.writesAllowed)),
|
|
78
|
+
repository,
|
|
74
79
|
workspace,
|
|
75
80
|
model: loaded.model,
|
|
76
81
|
resources: entries,
|
package/src/validate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
import { getResourceDefinition } from "../model/index.js";
|
|
3
|
+
import { isSafeGitName } from "./git-name.js";
|
|
3
4
|
import { isCanonicalDataPath, resolveDataPath } from "./paths.js";
|
|
4
5
|
import { parseCalendarDate, validCalendarRecurrence } from "./recurrence.js";
|
|
5
6
|
import { obligationIsRunning } from "./program-lifecycle.js";
|
|
@@ -476,6 +477,7 @@ function validateValue(name, value, field, model, path, diagnostics) {
|
|
|
476
477
|
}
|
|
477
478
|
if (field.format === "email" && !EMAIL_PATTERN.test(value)) fail("must be an email address.");
|
|
478
479
|
if (field.format === "timezone" && !isTimezone(value)) fail("must be an IANA time zone.");
|
|
480
|
+
if (field.format === "git-name" && !isSafeGitName(value)) fail("must be a safe Git name.");
|
|
479
481
|
}
|
|
480
482
|
|
|
481
483
|
function validateNumericRange(value, field, fail) {
|
package/src/web.js
CHANGED
|
@@ -174,7 +174,7 @@ function buildNavigation(route) {
|
|
|
174
174
|
const organizationCurrent = route.name === "organization" || route.name === "repository" || ["workspace", "renderer-settings"].includes(route.type);
|
|
175
175
|
const organizationName = state.workspace.organizationName || "Organization";
|
|
176
176
|
const initial = organizationName.trim().charAt(0).toUpperCase() || "O";
|
|
177
|
-
return '<aside class="sidebar" id="sidebar-navigation"><button class="nav-close" type="button" aria-label="Close navigation">×</button><a href="#/" class="brand"' + (route.name === "home" ? ' aria-current="page"' : "") + '><img class="mark" src="./
|
|
177
|
+
return '<aside class="sidebar" id="sidebar-navigation"><button class="nav-close" type="button" aria-label="Close navigation">×</button><a href="#/" class="brand"' + (route.name === "home" ? ' aria-current="page"' : "") + '><img class="mark" src="./logo-mark-white.png" alt="" width="39" height="39"><span><strong>filegrc</strong><small>SOC 2 workspace</small></span></a><nav class="sidebar-nav">' + stages + '</nav><div class="sidebar-footer"><a class="organization-nav ' + (organizationCurrent ? "current" : "") + '" href="#/organization"><span class="organization-mark">' + esc(initial) + '</span><span><strong>' + esc(organizationName) + '</strong><small>Organization</small></span><span class="organization-arrow">›</span></a></div></aside><button class="nav-scrim" type="button" aria-label="Close navigation"></button>';
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
function readinessStageForRoute(route) {
|
|
@@ -217,7 +217,19 @@ function topbar(route) {
|
|
|
217
217
|
: route.name === "audit-packet"
|
|
218
218
|
? "Audit Readiness"
|
|
219
219
|
: state.model.resources[route.type]?.pluralTitle || "filegrc";
|
|
220
|
-
|
|
220
|
+
const repositoryLabel = state.repository?.mode === "trunk"
|
|
221
|
+
? state.repository.label
|
|
222
|
+
: state.git.available ? ((state.git.branch || "detached") + " · " + state.git.shortCommit) : "Git unavailable";
|
|
223
|
+
const repositoryTone = state.repository?.mode === "trunk"
|
|
224
|
+
? repositoryStatusTone(state.repository.status)
|
|
225
|
+
: state.git.clean ? "good" : "warn";
|
|
226
|
+
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><label class="search"><span aria-hidden="true">⌕</span><input id="global-search" type="search" placeholder="Search records" aria-label="Search records"><kbd>/</kbd></label><div class="topbar-status"><a class="validation-chip" href="#/repository"><span class="status-dot ' + (state.validation.ok ? "good" : "bad") + '"></span>' + (state.validation.ok ? "Data valid" : state.validation.counts.errors + " validation errors") + '</a><a class="repo-chip" href="#/repository"><span class="status-dot ' + repositoryTone + '"></span>' + esc(repositoryLabel) + '</a></div>';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function repositoryStatusTone(status) {
|
|
230
|
+
if (status === "synced") return "good";
|
|
231
|
+
if (status === "syncing") return "neutral";
|
|
232
|
+
return "warn";
|
|
221
233
|
}
|
|
222
234
|
|
|
223
235
|
function renderHome(main) {
|
|
@@ -1132,6 +1144,7 @@ function renderOrganization(main) {
|
|
|
1132
1144
|
}
|
|
1133
1145
|
|
|
1134
1146
|
function renderRepository(main) {
|
|
1147
|
+
if (state.repository?.mode === "trunk") return renderTrunkRepository(main);
|
|
1135
1148
|
const settings = rendererSettingsEntry();
|
|
1136
1149
|
const settingsLink = settings ? '<a class="button" href="#/resource/renderer-settings/' + encodeURIComponent(settings.record.id) + '">Renderer settings</a>' : "";
|
|
1137
1150
|
const onboardingButton = settings && !state.readOnly ? '<button class="button" type="button" id="start-onboarding">Run onboarding</button>' : "";
|
|
@@ -1177,12 +1190,41 @@ function renderRepository(main) {
|
|
|
1177
1190
|
main.querySelector("#start-onboarding")?.addEventListener("click", requestOnboarding);
|
|
1178
1191
|
}
|
|
1179
1192
|
|
|
1193
|
+
function renderTrunkRepository(main) {
|
|
1194
|
+
const repository = state.repository;
|
|
1195
|
+
const settings = rendererSettingsEntry();
|
|
1196
|
+
const settingsLink = settings ? '<a class="button" href="#/resource/renderer-settings/' + encodeURIComponent(settings.record.id) + '">Renderer settings</a>' : "";
|
|
1197
|
+
const onboardingButton = settings && repository.writesAllowed
|
|
1198
|
+
? '<button class="button" type="button" id="start-onboarding">Run onboarding</button>'
|
|
1199
|
+
: "";
|
|
1200
|
+
const retryButton = repository.retrySafe
|
|
1201
|
+
? '<button class="button primary" type="button" data-git-action="retry-sync">Retry sync</button>'
|
|
1202
|
+
: "";
|
|
1203
|
+
const pending = repository.pendingCommitsFilegrcOnly === false
|
|
1204
|
+
? empty("Ahead commits include files outside this FileGRC workspace. Reconcile them with Git.")
|
|
1205
|
+
: repository.pendingCommits?.length
|
|
1206
|
+
? '<ul class="changes">' + repository.pendingCommits.map((commit) => '<li><code>' + esc(commit.shortCommit) + '</code> ' + esc(commit.subject) + '</li>').join("") + '</ul>'
|
|
1207
|
+
: empty("No FileGRC commits are waiting to be pushed.");
|
|
1208
|
+
const lastSync = repository.lastSuccessfulSynchronization
|
|
1209
|
+
? formatLocalDateTime(repository.lastSuccessfulSynchronization)
|
|
1210
|
+
: "No successful sync recorded by this server";
|
|
1211
|
+
const override = repository.developmentOverride
|
|
1212
|
+
? '<div class="repository-override"><span class="status-dot warn"></span><div><strong>Development write override active</strong><p>Browser writes stay local. FileGRC will not fetch, commit, or push while this server uses <code>--allow-non-authoritative-writes</code>.</p></div></div>'
|
|
1213
|
+
: "";
|
|
1214
|
+
const validationBody = state.validation.diagnostics.length
|
|
1215
|
+
? '<div class="diagnostics">' + state.validation.diagnostics.map((item) => '<div><span class="badge ' + item.severity + '">' + esc(properCase(item.severity)) + '</span><code>' + esc(item.path) + '</code><p>' + esc(item.message) + '</p></div>').join("") + '</div>'
|
|
1216
|
+
: empty("No validation problems.");
|
|
1217
|
+
main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">Audit trail</p><h2>Repository State</h2><p>Browser saves use one authoritative branch. Record status represents draft, proposal, approval, and retirement; Git branches do not.</p><p class="repository-sync-status" role="status" aria-live="polite"></p></div><div class="page-actions">' + retryButton + onboardingButton + settingsLink + '<a class="button" href="#/resource/workspace/workspace">Workspace settings</a></div></div>' + override + '<section class="panel repository-state-banner"><span class="status-dot ' + repositoryStatusTone(repository.status) + '"></span><div><p class="kicker">Repository status</p><h3>' + esc(repository.label) + '</h3><p>' + esc(repository.message) + '</p></div></section><div class="dashboard-grid"><section class="panel"><div class="panel-head"><h3>Configured Repository</h3></div><dl class="metadata"><div><dt>Branch</dt><dd>' + esc(repository.authoritativeBranch) + '</dd></div><div><dt>Remote</dt><dd>' + esc(repository.remote) + '</dd></div><div><dt>Checkout</dt><dd>' + esc(state.git.branch || (state.git.available ? "Detached HEAD" : "Unavailable")) + '</dd></div><div><dt>Upstream</dt><dd>' + esc(repository.upstream || "Not configured") + '</dd></div></dl></section><section class="panel"><div class="panel-head"><h3>Synchronization</h3></div><dl class="metadata"><div><dt>Current commit</dt><dd><code>' + esc(repository.currentCommit || "Unavailable") + '</code></dd></div><div><dt>Upstream commit</dt><dd><code>' + esc(repository.upstreamCommit || "Unavailable") + '</code></dd></div><div><dt>Ahead</dt><dd>' + esc(repository.ahead ?? "Unknown") + '</dd></div><div><dt>Behind</dt><dd>' + esc(repository.behind ?? "Unknown") + '</dd></div><div><dt>Last sync</dt><dd>' + esc(lastSync) + '</dd></div></dl></section><section class="panel"><div class="panel-head"><h3>Safety Checks</h3></div><dl class="metadata"><div><dt>Whole worktree</dt><dd>' + (repository.wholeWorktreeClean === null ? "Unavailable" : repository.wholeWorktreeClean ? "Clean" : "Has changes") + '</dd></div><div><dt>Git operation</dt><dd>' + esc(repository.operationInProgress || "None") + '</dd></div><div><dt>Pending scope</dt><dd>' + (repository.pendingCommitsFilegrcOnly === false ? "Includes external files" : repository.pendingCommits?.length ? "FileGRC only" : "None") + '</dd></div></dl></section><section class="panel span-2"><div class="panel-head"><h3>Pending FileGRC-only Commits</h3></div>' + pending + '</section><section class="panel span-2"><div class="panel-head"><h3>Validation</h3><span class="badge ' + (state.validation.ok ? "good" : "bad") + '">' + (state.validation.ok ? "Passing" : "Needs attention") + '</span></div>' + validationBody + '</section></div></div>';
|
|
1218
|
+
main.querySelectorAll("[data-git-action]").forEach((button) => button.addEventListener("click", () => runRepositoryGitAction(button.dataset.gitAction)));
|
|
1219
|
+
main.querySelector("#start-onboarding")?.addEventListener("click", requestOnboarding);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1180
1222
|
async function runRepositoryGitAction(action) {
|
|
1181
1223
|
const buttons = [...document.querySelectorAll("[data-git-action]")];
|
|
1182
1224
|
const disabled = buttons.map((button) => button.disabled);
|
|
1183
1225
|
const active = buttons.find((button) => button.dataset.gitAction === action);
|
|
1184
1226
|
const status = document.querySelector(".repository-sync-status");
|
|
1185
|
-
const label = action === "pull" ? "Pulling…" : "Pushing…";
|
|
1227
|
+
const label = action === "pull" ? "Pulling…" : action === "retry-sync" ? "Syncing…" : "Pushing…";
|
|
1186
1228
|
if (status) {
|
|
1187
1229
|
status.textContent = "";
|
|
1188
1230
|
status.classList.remove("error");
|
|
@@ -1200,10 +1242,12 @@ async function runRepositoryGitAction(action) {
|
|
|
1200
1242
|
? result.updated
|
|
1201
1243
|
? "Pulled " + result.upstream + " with rebase at " + result.shortCommit + "."
|
|
1202
1244
|
: result.branch + " is current with " + result.upstream + "."
|
|
1203
|
-
:
|
|
1245
|
+
: action === "retry-sync"
|
|
1246
|
+
? "Synchronized " + result.shortCommit + " with " + result.upstream + "."
|
|
1247
|
+
: "Pushed " + result.shortCommit + " to " + result.upstream + ".";
|
|
1204
1248
|
} catch (cause) {
|
|
1205
1249
|
buttons.forEach((button, index) => { button.disabled = disabled[index]; });
|
|
1206
|
-
if (active) active.textContent = action === "pull" ? "Pull with rebase" : "Push";
|
|
1250
|
+
if (active) active.textContent = action === "pull" ? "Pull with rebase" : action === "retry-sync" ? "Retry sync" : "Push";
|
|
1207
1251
|
if (status) {
|
|
1208
1252
|
status.textContent = cause.message;
|
|
1209
1253
|
status.classList.add("error");
|
|
@@ -1413,8 +1457,9 @@ function onboardingSteps() {
|
|
|
1413
1457
|
points: [
|
|
1414
1458
|
"Use the UI, an editor, the CLI, or an agent; every path changes the same files.",
|
|
1415
1459
|
"JSON holds structured records. Markdown holds policies, plans, minutes, and other long-form work.",
|
|
1416
|
-
"
|
|
1417
|
-
"
|
|
1460
|
+
"In trunk mode, each browser save fast-forwards, validates, creates one focused commit, and pushes it.",
|
|
1461
|
+
"Record status represents approval. Draft, proposed, approved, and retired records all stay on the authoritative branch.",
|
|
1462
|
+
"Agents and terminal users continue to manage Git explicitly.",
|
|
1418
1463
|
"The dashboard derives program status from the current repository state."
|
|
1419
1464
|
]
|
|
1420
1465
|
};
|
|
@@ -1562,14 +1607,12 @@ function onboardingSetupForm() {
|
|
|
1562
1607
|
currentSystem ? "Updates system " + currentSystem.title + "." : "Creates a new in-scope system.",
|
|
1563
1608
|
"Records a management program goal without creating an audit engagement."
|
|
1564
1609
|
].filter(Boolean).join(" ");
|
|
1565
|
-
const gitStatus = state.
|
|
1566
|
-
? '<div class="onboarding-git-status warning"><span class="status-dot
|
|
1567
|
-
: state.git.available && state.git.
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
: '<div class="onboarding-git-status warning"><span class="status-dot warn"></span><span><strong>Git setup needed</strong><small>Saving still works. Run <code>git init</code> at the workspace root before your first compliance commit.</small></span></div>';
|
|
1572
|
-
return '<p class="onboarding-body">' + esc(onboardingSteps().at(-1).body) + '</p>' + gitStatus + '<form id="onboarding-setup" class="onboarding-form"><label class="wide"><span>Service name</span><input name="serviceName" required maxlength="200" value="' + esc(onboardingDraft.serviceName) + '" placeholder="Customer-facing application"></label><label class="wide"><span>Scope description</span><textarea name="scope" required maxlength="2000" placeholder="What the service does and which production boundary is in scope">' + esc(onboardingDraft.scope) + '</textarea></label><label><span>Accountable owner</span><select name="ownerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === onboardingDraft.ownerId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Business criticality</span><select name="criticality" required>' + ["low", "medium", "high", "critical"].map((value) => '<option value="' + value + '" ' + (value === onboardingDraft.criticality ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Highest data classification</span><select name="dataClassification" required>' + classifications.map((value) => '<option value="' + esc(value) + '" ' + (value === onboardingDraft.dataClassification ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Internet exposed</span><select name="internetExposed" required><option value="true" ' + (onboardingDraft.internetExposed === "true" ? "selected" : "") + '>Yes</option><option value="false" ' + (onboardingDraft.internetExposed === "false" ? "selected" : "") + '>No</option></select></label><label class="wide"><span>Program goal</span><select name="programGoal" required><option value="none" ' + (onboardingDraft.programGoal === "none" ? "selected" : "") + '>No Assurance Goal Yet</option><option value="readiness" ' + (onboardingDraft.programGoal === "readiness" ? "selected" : "") + '>Program Readiness</option><option value="type-1" ' + (onboardingDraft.programGoal === "type-1" ? "selected" : "") + '>SOC 2 Type 1</option><option value="type-2" ' + (onboardingDraft.programGoal === "type-2" ? "selected" : "") + '>SOC 2 Type 2</option></select><small>This records management intent only. It does not create an engagement or establish the formal report period.</small></label></form><p class="onboarding-write-note">' + esc(existing) + ' Save draft marks the service Planned and In scope. It is selected for scope review, but it is not approved or active. Saving writes JSON files but does not commit them. Complete the remaining Step 1 pages next.</p>';
|
|
1610
|
+
const gitStatus = state.repository?.mode === "trunk"
|
|
1611
|
+
? '<div class="onboarding-git-status ' + (state.repository.status === "synced" ? "" : "warning") + '"><span class="status-dot ' + repositoryStatusTone(state.repository.status) + '"></span><span><strong>' + esc(state.repository.label) + '</strong><small>' + esc(state.repository.status === "synced" ? "Completing onboarding will save its related workspace, system, and renderer changes in one commit and push it." : state.repository.message) + '</small></span></div>'
|
|
1612
|
+
: state.git.available && state.git.branch
|
|
1613
|
+
? '<div class="onboarding-git-status"><span class="status-dot good"></span><span><strong>Manual repository mode</strong><small>Setup changes will stay local until you commit and synchronize them.</small></span></div>'
|
|
1614
|
+
: '<div class="onboarding-git-status warning"><span class="status-dot warn"></span><span><strong>Git setup needed</strong><small>Manual-mode writes still work, but Git history is unavailable until the repository is configured.</small></span></div>';
|
|
1615
|
+
return '<p class="onboarding-body">' + esc(onboardingSteps().at(-1).body) + '</p>' + gitStatus + '<form id="onboarding-setup" class="onboarding-form"><label class="wide"><span>Service name</span><input name="serviceName" required maxlength="200" value="' + esc(onboardingDraft.serviceName) + '" placeholder="Customer-facing application"></label><label class="wide"><span>Scope description</span><textarea name="scope" required maxlength="2000" placeholder="What the service does and which production boundary is in scope">' + esc(onboardingDraft.scope) + '</textarea></label><label><span>Accountable owner</span><select name="ownerId" required><option value="">Select</option>' + people.map(({ record }) => '<option value="' + esc(record.id) + '" ' + (record.id === onboardingDraft.ownerId ? "selected" : "") + '>' + esc(record.title) + '</option>').join("") + '</select></label><label><span>Business criticality</span><select name="criticality" required>' + ["low", "medium", "high", "critical"].map((value) => '<option value="' + value + '" ' + (value === onboardingDraft.criticality ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Highest data classification</span><select name="dataClassification" required>' + classifications.map((value) => '<option value="' + esc(value) + '" ' + (value === onboardingDraft.dataClassification ? "selected" : "") + '>' + esc(properCase(value)) + '</option>').join("") + '</select></label><label><span>Internet exposed</span><select name="internetExposed" required><option value="true" ' + (onboardingDraft.internetExposed === "true" ? "selected" : "") + '>Yes</option><option value="false" ' + (onboardingDraft.internetExposed === "false" ? "selected" : "") + '>No</option></select></label><label class="wide"><span>Program goal</span><select name="programGoal" required><option value="none" ' + (onboardingDraft.programGoal === "none" ? "selected" : "") + '>No Assurance Goal Yet</option><option value="readiness" ' + (onboardingDraft.programGoal === "readiness" ? "selected" : "") + '>Program Readiness</option><option value="type-1" ' + (onboardingDraft.programGoal === "type-1" ? "selected" : "") + '>SOC 2 Type 1</option><option value="type-2" ' + (onboardingDraft.programGoal === "type-2" ? "selected" : "") + '>SOC 2 Type 2</option></select><small>This records management intent only. It does not create an engagement or establish the formal report period.</small></label></form><p class="onboarding-write-note">' + esc(existing) + ' Save draft marks the service Planned and In scope. It is selected for scope review, but it is not approved or active. ' + (state.repository?.mode === "trunk" ? "The browser saves and synchronizes the related files together." : "Manual mode leaves the files for you to commit.") + ' Complete the remaining Step 1 pages next.</p>';
|
|
1573
1616
|
}
|
|
1574
1617
|
|
|
1575
1618
|
function captureOnboardingForm() {
|
|
@@ -2562,10 +2605,35 @@ async function responseMessage(response) {
|
|
|
2562
2605
|
try { return JSON.parse(source).error || source; } catch { return source; }
|
|
2563
2606
|
}
|
|
2564
2607
|
async function localFetch(url, options) {
|
|
2608
|
+
const method = String(options?.method || "GET").toUpperCase();
|
|
2609
|
+
const synchronizing = state?.repository?.mode === "trunk"
|
|
2610
|
+
&& ["POST", "PUT", "DELETE"].includes(method)
|
|
2611
|
+
&& url !== "/api/evidence-packet";
|
|
2612
|
+
const chip = synchronizing ? document.querySelector(".repo-chip") : null;
|
|
2613
|
+
const previousChip = chip?.innerHTML;
|
|
2614
|
+
let repositoryRefreshed = false;
|
|
2615
|
+
if (chip) chip.innerHTML = '<span class="status-dot neutral"></span>Syncing';
|
|
2565
2616
|
try {
|
|
2566
|
-
|
|
2617
|
+
const response = await fetch(url, options);
|
|
2618
|
+
if (synchronizing && !response.ok) {
|
|
2619
|
+
try {
|
|
2620
|
+
const stateResponse = await fetch("/api/state");
|
|
2621
|
+
if (stateResponse.ok) {
|
|
2622
|
+
state = await stateResponse.json();
|
|
2623
|
+
if (chip?.isConnected) {
|
|
2624
|
+
chip.innerHTML = '<span class="status-dot ' + repositoryStatusTone(state.repository.status) + '"></span>' + esc(state.repository.label);
|
|
2625
|
+
repositoryRefreshed = true;
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
} catch {
|
|
2629
|
+
// The original response contains the useful mutation error.
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
return response;
|
|
2567
2633
|
} catch {
|
|
2568
2634
|
throw new Error("The filegrc server is unavailable. Restart npm run serve, or pnpm dev in the monorepo, and try again.");
|
|
2635
|
+
} finally {
|
|
2636
|
+
if (!repositoryRefreshed && chip?.isConnected && previousChip) chip.innerHTML = previousChip;
|
|
2569
2637
|
}
|
|
2570
2638
|
}
|
|
2571
2639
|
async function fetchJson(url, options) { const response = await localFetch(url, options); if (!response.ok) throw new Error(await responseMessage(response)); return response.json(); }
|
|
@@ -2598,7 +2666,7 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
|
|
|
2598
2666
|
.readiness-map{margin:14px 0;background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:20px 22px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.readiness-map-head{display:grid;grid-template-columns:minmax(220px,1fr) minmax(320px,420px);gap:28px;align-items:center;margin-bottom:17px}.readiness-map-head h3{font-size:18px;margin:5px 0 0}.readiness-progress-summary{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 14px;align-items:center}.readiness-progress-summary>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:3px 12px;align-items:baseline}.readiness-progress-summary>div>span{color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}.readiness-progress-summary>div>strong{font-size:9.6px;font-weight:700;line-height:1.2}.readiness-progress-summary .progress,.readiness-progress-summary small{grid-column:1/-1}.readiness-progress-summary small{color:var(--muted);font-size:9.6px}.readiness-progress-summary>.button{white-space:nowrap}.readiness-flow{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}.readiness-flow a{display:grid;grid-template-columns:23px minmax(0,1fr);column-gap:8px;align-content:start;min-width:0;padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);text-decoration:none}.readiness-flow a:hover{border-color:var(--accent-light);background:var(--accent-soft)}.readiness-flow a>span{grid-row:1/4;display:grid;place-items:center;width:23px;height:23px;border-radius:50%;background:var(--primary-gradient);color:#fff;font-size:9.6px;font-weight:800}.readiness-flow strong{font-size:12px;line-height:1.25}.readiness-flow small{grid-column:2;color:var(--muted);font-size:9.6px;line-height:1.4;margin-top:3px}.readiness-state{grid-column:2;justify-self:start;margin-top:8px;padding:3px 6px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:8.4px;line-height:1.2}.readiness-state.good{background:#dcefe4;color:#125733}.readiness-state.warn{background:#f6e8c9;color:#79500f}.readiness-state.bad{background:#f7dfdc;color:#873027}.audit-engagement{display:grid;grid-template-columns:minmax(210px,1fr) minmax(260px,1.25fr) auto;gap:20px;align-items:center;padding:14px 15px;border-radius:8px;background:var(--surface-soft)}.audit-engagement strong{font-size:13.2px}.audit-engagement p,.audit-engagement li{color:var(--muted);font-size:10.8px;line-height:1.5}.audit-engagement p{margin:5px 0 0}.audit-engagement ul{margin:0;padding-left:18px}.audit-engagement .button{white-space:nowrap;text-decoration:none}.resource-directory{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.resource-directory>section{min-width:0;padding:12px;border-radius:8px;background:var(--surface-soft)}.resource-directory h4{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.resource-directory a{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-top:1px solid var(--line);font-size:10.8px;text-decoration:none}.resource-directory a:first-of-type{border-top:0}.resource-directory a:hover span{color:var(--accent)}.resource-directory a strong{color:var(--muted);font-size:9.6px}.record-prose{max-width:790px}.record-prose section{padding:0 0 20px}.record-prose section+section{padding-top:20px;border-top:1px solid var(--line)}.record-prose h3{margin:0 0 7px;color:var(--muted);font-size:10.8px;text-transform:uppercase;letter-spacing:.08em}.record-prose p{margin:0;font-size:16.8px;line-height:1.65;white-space:pre-wrap}.connections-panel .panel-head>span{display:grid;place-items:center;min-width:22px;height:22px;border-radius:99px;background:var(--surface-muted);color:var(--muted);font-size:9.6px}.connections{display:grid}.connections a{display:block;padding:9px 0;border-top:1px solid var(--line);text-decoration:none}.connections a:first-child{padding-top:0;border-top:0}.connections strong,.connections small{display:block}.connections strong{font-size:12px}.connections small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.connections a:hover strong{color:var(--accent)}.connections-more{margin:9px 0 0;color:var(--muted);font-size:9.6px;line-height:1.4}.external-source{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;color:var(--accent);text-decoration:none}.external-source span,.external-source strong,.external-source small{display:block}.external-source strong{font-size:12px;line-height:1.35}.external-source small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.35;overflow-wrap:anywhere}.external-source b{font-size:13.2px}.external-source:hover strong{text-decoration:underline}
|
|
2599
2667
|
.page-title-line{display:flex;align-items:center;gap:8px}.guide-trigger{display:grid;place-items:center;width:24px;height:24px;flex:0 0 auto;padding:0;border:1px solid var(--line);border-radius:50%;background:var(--panel);color:var(--muted);cursor:pointer}.guide-trigger svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round}.guide-trigger:hover{border-color:var(--accent-light);color:var(--accent)}.guide-trigger:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.page-guide{display:grid;grid-template-columns:1.05fr 1.25fr 1fr;gap:0;margin:0;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.resource-guide-popover{position:fixed;z-index:40;overflow:auto;box-shadow:0 18px 50px rgba(0,0,24,.24)}.resource-guide-popover[hidden]{display:none}.page-guide>div{padding:14px 16px;border-left:1px solid var(--line);min-width:0}.page-guide>div:first-child{border-left:0}.page-guide>div>span{display:block;color:var(--accent);text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780;margin-bottom:6px}.page-guide p{color:var(--muted);font-size:12px;line-height:1.5;margin:0}.guide-links{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.guide-links a{color:var(--accent);background:var(--accent-soft);border-radius:99px;padding:4px 7px;text-decoration:none;font-size:9.6px;font-weight:700}
|
|
2600
2668
|
.operation-tracking{display:grid;gap:2px;min-width:0;text-decoration:none}.operation-tracking strong,.operation-tracking small{display:block;overflow-wrap:anywhere}.operation-tracking small{color:var(--muted);line-height:1.35}.operation-tracking.running strong{color:#176143}.operation-tracking.waiting strong,.operation-tracking.mixed strong{color:var(--amber)}.operation-tracking.paused strong{color:var(--red)}a.operation-tracking:hover strong{text-decoration:underline}
|
|
2601
|
-
.page-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.repository-sync-status{min-height:18px;margin:7px 0 0;color:var(--muted);font-size:13.2px}.repository-sync-status.error{color:var(--red)}.onboarding-dialog{width:min(470px,calc(100vw - 30px));max-height:calc(100vh - 32px);margin:0;border:1px solid var(--line);border-radius:13px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 28px 90px rgba(0,0,24,.38);overflow:hidden}.onboarding-dialog[open]{display:flex;flex-direction:column}.onboarding-dialog::backdrop{background:transparent;backdrop-filter:none}.onboarding-shade{position:fixed;inset:0;z-index:60;pointer-events:none}.onboarding-shade span{position:absolute;background:rgba(0,0,24,.58)}.onboarding-progress{display:grid;flex:0 0 auto;grid-template-columns:repeat(var(--onboarding-step-count),1fr);gap:5px;padding:18px 24px 0}.onboarding-progress span{height:3px;border-radius:3px;background:var(--surface-muted)}.onboarding-progress span.active{background:var(--accent-light)}.onboarding-scroll{min-height:0;overflow-y:auto}.onboarding-head{padding:22px 25px 0}.onboarding-head h2{font-family:Georgia,serif;font-size:30px;font-weight:500;letter-spacing:-.015em;margin:8px 0 0}.onboarding-body{color:var(--muted);font-size:14.4px;line-height:1.6;margin:13px 25px 0}.onboarding-body+.onboarding-body{margin-top:8px}.onboarding-points{display:grid;gap:9px;margin:18px 25px 4px;padding-left:19px}.onboarding-points li{font-size:13.2px;line-height:1.5;padding-left:3px}.onboarding-sections{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 25px 4px}.onboarding-sections section{padding:13px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.onboarding-sections strong{font-size:13.2px}.onboarding-sections p{margin:6px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.onboarding-actions{flex:0 0 auto;padding:12px 25px 23px;border-top:1px solid var(--line);background:var(--panel)}.onboarding-skip{margin-right:auto;color:var(--muted);text-transform:none;letter-spacing:0;font-size:13.2px}.onboarding-form{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:18px 25px 0}.onboarding-form label{display:block;min-width:0}.onboarding-form label.wide{grid-column:1/-1}.onboarding-form label>span{display:block;color:var(--ink);font-size:12px;font-weight:720;margin-bottom:6px}.onboarding-form input,.onboarding-form select,.onboarding-form textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.onboarding-form textarea{min-height:78px;resize:vertical}.onboarding-form small{display:block;color:var(--muted);font-size:10.8px;line-height:1.45;margin-top:5px}.onboarding-write-note{color:var(--muted);font-size:10.8px;line-height:1.5;margin:12px 25px 16px}.onboarding-scroll>.dialog-error{margin:8px 25px 0}.onboarding-focus{outline:4px solid var(--accent-light)!important;outline-offset:5px;scroll-margin-top:102px}
|
|
2669
|
+
.page-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.repository-sync-status{min-height:18px;margin:7px 0 0;color:var(--muted);font-size:13.2px}.repository-sync-status.error{color:var(--red)}.repository-state-banner,.repository-override{display:flex;align-items:flex-start;gap:13px;margin-bottom:14px}.repository-state-banner>.status-dot,.repository-override>.status-dot{margin-top:5px}.repository-state-banner h3{margin:3px 0 5px}.repository-state-banner p:last-child,.repository-override p{margin:0;color:var(--muted);line-height:1.5}.repository-override{padding:14px 17px;border:1px solid #e9c888;border-radius:9px;background:#fff8e8;font-size:13.2px}.repository-override strong{display:block;margin-bottom:4px}.onboarding-dialog{width:min(470px,calc(100vw - 30px));max-height:calc(100vh - 32px);margin:0;border:1px solid var(--line);border-radius:13px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 28px 90px rgba(0,0,24,.38);overflow:hidden}.onboarding-dialog[open]{display:flex;flex-direction:column}.onboarding-dialog::backdrop{background:transparent;backdrop-filter:none}.onboarding-shade{position:fixed;inset:0;z-index:60;pointer-events:none}.onboarding-shade span{position:absolute;background:rgba(0,0,24,.58)}.onboarding-progress{display:grid;flex:0 0 auto;grid-template-columns:repeat(var(--onboarding-step-count),1fr);gap:5px;padding:18px 24px 0}.onboarding-progress span{height:3px;border-radius:3px;background:var(--surface-muted)}.onboarding-progress span.active{background:var(--accent-light)}.onboarding-scroll{min-height:0;overflow-y:auto}.onboarding-head{padding:22px 25px 0}.onboarding-head h2{font-family:Georgia,serif;font-size:30px;font-weight:500;letter-spacing:-.015em;margin:8px 0 0}.onboarding-body{color:var(--muted);font-size:14.4px;line-height:1.6;margin:13px 25px 0}.onboarding-body+.onboarding-body{margin-top:8px}.onboarding-points{display:grid;gap:9px;margin:18px 25px 4px;padding-left:19px}.onboarding-points li{font-size:13.2px;line-height:1.5;padding-left:3px}.onboarding-sections{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 25px 4px}.onboarding-sections section{padding:13px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.onboarding-sections strong{font-size:13.2px}.onboarding-sections p{margin:6px 0 0;color:var(--muted);font-size:12px;line-height:1.5}.onboarding-actions{flex:0 0 auto;padding:12px 25px 23px;border-top:1px solid var(--line);background:var(--panel)}.onboarding-skip{margin-right:auto;color:var(--muted);text-transform:none;letter-spacing:0;font-size:13.2px}.onboarding-form{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:18px 25px 0}.onboarding-form label{display:block;min-width:0}.onboarding-form label.wide{grid-column:1/-1}.onboarding-form label>span{display:block;color:var(--ink);font-size:12px;font-weight:720;margin-bottom:6px}.onboarding-form input,.onboarding-form select,.onboarding-form textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.onboarding-form textarea{min-height:78px;resize:vertical}.onboarding-form small{display:block;color:var(--muted);font-size:10.8px;line-height:1.45;margin-top:5px}.onboarding-write-note{color:var(--muted);font-size:10.8px;line-height:1.5;margin:12px 25px 16px}.onboarding-scroll>.dialog-error{margin:8px 25px 0}.onboarding-focus{outline:4px solid var(--accent-light)!important;outline-offset:5px;scroll-margin-top:102px}
|
|
2602
2670
|
.page-intro,.detail-head{align-items:center;margin-bottom:12px}.actions{align-items:center}.detail-head>div:first-child{min-width:0}.detail-head h2{margin:7px 0}.detail-head .header-breadcrumbs{margin:0;font-size:10.8px;line-height:normal;min-height:11px;align-items:center}.header-breadcrumbs span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60ch}
|
|
2603
2671
|
@media(max-width:1200px){.readiness-flow{grid-template-columns:repeat(3,minmax(0,1fr))}.audit-engagement{grid-template-columns:1fr 1fr}.audit-engagement .button{grid-column:1/-1;justify-self:start}}
|
|
2604
2672
|
@media(max-width:1100px){.search{display:none}.topbar-status{margin-left:auto}.metrics{grid-template-columns:repeat(2,1fr)}.dashboard-grid,.organization-grid{grid-template-columns:repeat(2,1fr)}.catalog{grid-template-columns:repeat(3,1fr)}.span-2{grid-column:span 2}.resource-directory{grid-template-columns:repeat(2,minmax(0,1fr))}}
|