@wcag-checkr/ci 1.0.0-rc.366 → 1.0.0-rc.367
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/dist/assets/{ErrorBoundary-BH1wbHb3.js → ErrorBoundary-Ck0XMUiQ.js} +22 -22
- package/dist/assets/{copy-ai-fixer-prompt-FO0N3IMf.js → copy-ai-fixer-prompt-CKp52Td1.js} +2 -2
- package/dist/assets/{devtools-panel-BAHfgRsc.js → devtools-panel-Dw0aFhIc.js} +1 -1
- package/dist/assets/{parallel-tab-flow-C4bjrDqL.js → parallel-tab-flow-BuZZIdRb.js} +1 -1
- package/dist/assets/{scheduled-audit-runner-XCDyFhY6.js → scheduled-audit-runner-9R-aGzwO.js} +137 -137
- package/dist/assets/{service-worker.ts-57FwiS_-.js → service-worker.ts-CyL2aQvI.js} +2 -2
- package/dist/assets/{side-panel-Dl247dJv.js → side-panel-CuEUwRBD.js} +3 -3
- package/dist/assets/{site-report-renderer-CNWZjbOd.js → site-report-renderer-46_JyAkB.js} +1 -1
- package/dist/devtools/panel.html +3 -3
- package/dist/manifest.json +3 -4
- package/dist/service-worker-loader.js +1 -1
- package/dist/side-panel/side-panel.html +4 -4
- package/package.json +1 -1
- package/dist/side-panel/App.tsx +0 -308
- package/dist/side-panel/README.md +0 -57
- package/dist/side-panel/audit-launcher.test.ts +0 -56
- package/dist/side-panel/audit-launcher.ts +0 -98
- package/dist/side-panel/azure-devops-issue.test.ts +0 -68
- package/dist/side-panel/azure-devops-issue.ts +0 -89
- package/dist/side-panel/format-component-id.test.ts +0 -89
- package/dist/side-panel/format-component-id.ts +0 -40
- package/dist/side-panel/github-issue.test.ts +0 -102
- package/dist/side-panel/github-issue.ts +0 -66
- package/dist/side-panel/gitlab-issue.test.ts +0 -53
- package/dist/side-panel/gitlab-issue.ts +0 -78
- package/dist/side-panel/jira-issue.ts +0 -64
- package/dist/side-panel/main.tsx +0 -86
- package/dist/side-panel/store.ts +0 -435
- package/dist/side-panel/styles.css +0 -55
- package/dist/side-panel/use-audit-view-model.ts +0 -116
- package/dist/side-panel/wire-messaging.test.ts +0 -234
- package/dist/side-panel/wire-messaging.ts +0 -521
|
@@ -1,521 +0,0 @@
|
|
|
1
|
-
// Subscribe Zustand store to messaging events. Open keepalive port to SW.
|
|
2
|
-
// Also handles last-audit persistence so closing/reopening the side panel preserves results.
|
|
3
|
-
|
|
4
|
-
import { on, request } from '../shared/messaging';
|
|
5
|
-
import { useStore } from './store';
|
|
6
|
-
import { announcer } from '../shared/announcer';
|
|
7
|
-
import { getAuditTargetTabId } from '../shared/active-tab';
|
|
8
|
-
import type { BaselineListResponse, SettingsResponse, TierGetResponse, ExportResponse } from '../shared/messages';
|
|
9
|
-
import type { UserMode } from './store';
|
|
10
|
-
import type { AuditResult, DeltaResult } from '../types/audit';
|
|
11
|
-
import {
|
|
12
|
-
getAutoExportSettings,
|
|
13
|
-
recordLastAutoExport,
|
|
14
|
-
} from '../shared/auto-export-settings';
|
|
15
|
-
import { triggerDownload, bundleFilename } from '../shared/download-helper';
|
|
16
|
-
import { isFeatureAllowed } from '../shared/tier-rules';
|
|
17
|
-
import { isLockoutInEffectForTier } from '../shared/headless-build-lockdown';
|
|
18
|
-
import {
|
|
19
|
-
getCachedRubricConfig,
|
|
20
|
-
setResolvedRubrics,
|
|
21
|
-
freshness as rubricFreshness,
|
|
22
|
-
} from '../shared/rubric-config-cache';
|
|
23
|
-
|
|
24
|
-
const LAST_AUDIT_KEY = 'sidePanel:lastAudit';
|
|
25
|
-
|
|
26
|
-
type PersistedAudit = {
|
|
27
|
-
results: AuditResult[];
|
|
28
|
-
delta: DeltaResult | null;
|
|
29
|
-
componentId: string | null;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export function wireMessaging(): () => void {
|
|
33
|
-
const offs: Array<() => void> = [];
|
|
34
|
-
|
|
35
|
-
offs.push(
|
|
36
|
-
on('AUDIT_PROGRESS_EVENT', (msg) => {
|
|
37
|
-
const s = useStore.getState();
|
|
38
|
-
const wasRunning = s.status === 'running';
|
|
39
|
-
const inCrawl = s.siteCrawlStatus === 'running';
|
|
40
|
-
s.setProgress({
|
|
41
|
-
current: msg.current,
|
|
42
|
-
total: msg.total,
|
|
43
|
-
currentState: msg.currentState,
|
|
44
|
-
});
|
|
45
|
-
// rc.288 — During a crawl, AUDIT_PROGRESS_EVENT fires per page
|
|
46
|
-
// per state so the side panel can show inner matrix progress
|
|
47
|
-
// underneath the crawl's page counter. Don't flip s.status to
|
|
48
|
-
// 'running' in that case — the crawl's own siteCrawlStatus is
|
|
49
|
-
// the lifecycle signal, and we'd leave status='running' dangling
|
|
50
|
-
// after the crawl ends (AUDIT_COMPLETE is suppressed by
|
|
51
|
-
// asSubroutine). For genuine single-page runs, status='running'
|
|
52
|
-
// is still the right signal.
|
|
53
|
-
if (!inCrawl) {
|
|
54
|
-
s.setStatus('running');
|
|
55
|
-
if (!wasRunning) {
|
|
56
|
-
announcer.polite(`Audit running, scanning ${msg.total} state${msg.total === 1 ? '' : 's'}.`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
})
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
offs.push(
|
|
63
|
-
on('AUDIT_COMPLETE_EVENT', (msg) => {
|
|
64
|
-
useStore.getState().setResults(msg.results, msg.delta, msg.componentId);
|
|
65
|
-
const violationCount = msg.results.reduce((acc, r) => acc + r.violations.length, 0);
|
|
66
|
-
const newCount = msg.delta?.newCount ?? 0;
|
|
67
|
-
const summary = msg.delta
|
|
68
|
-
? `Audit complete. ${newCount} new violation${newCount === 1 ? '' : 's'} versus baseline.`
|
|
69
|
-
: `Audit complete. ${violationCount} violation${violationCount === 1 ? '' : 's'} found across ${msg.results.length} state${msg.results.length === 1 ? '' : 's'}.`;
|
|
70
|
-
announcer.polite(summary);
|
|
71
|
-
// rc.159 — Auto-navigate to Findings on audit completion. The user
|
|
72
|
-
// most likely wants to see the results, not stay on the Dashboard
|
|
73
|
-
// / specialized tool they kicked the audit off from. Exception: if
|
|
74
|
-
// they're mid-flow on Compliance (multi-step report builder),
|
|
75
|
-
// Guided (interactive workflow session), or Crawl (multi-page scan
|
|
76
|
-
// in progress), don't yank them out of context.
|
|
77
|
-
const currentView = useStore.getState().view;
|
|
78
|
-
const stickyViews: Array<typeof currentView> = ['compliance', 'guided', 'crawl'];
|
|
79
|
-
if (!stickyViews.includes(currentView)) {
|
|
80
|
-
useStore.getState().setView('report');
|
|
81
|
-
useStore.getState().setFindingsLens('overview');
|
|
82
|
-
}
|
|
83
|
-
void persistLastAudit({
|
|
84
|
-
results: msg.results,
|
|
85
|
-
delta: msg.delta,
|
|
86
|
-
componentId: msg.componentId,
|
|
87
|
-
});
|
|
88
|
-
// rc.201 — Auto-export the audit report if the user has the
|
|
89
|
-
// "auto-export after each audit" setting on AND their tier
|
|
90
|
-
// allows it. Fires AFTER setResults so the rest of the UI has
|
|
91
|
-
// updated. Errors are best-effort + silent — the user already
|
|
92
|
-
// has the in-app result; failing to write a file is logged but
|
|
93
|
-
// doesn't disrupt the audit flow.
|
|
94
|
-
void maybeAutoExport(msg.results, msg.delta, msg.componentId);
|
|
95
|
-
})
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
offs.push(
|
|
99
|
-
on('AUDIT_FAILED_EVENT', (msg) => {
|
|
100
|
-
// ErrorBanner has role="alert" (implicit assertive live region) so the SR
|
|
101
|
-
// announcement fires when the banner mounts. No need for an extra
|
|
102
|
-
// announcer.assertive — that would double-speak.
|
|
103
|
-
useStore.getState().setError(msg.error.message);
|
|
104
|
-
})
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
offs.push(
|
|
108
|
-
on('AI_AUGMENTATION_PROGRESS_EVENT', (msg) => {
|
|
109
|
-
const prev = useStore.getState().aiProgress;
|
|
110
|
-
useStore.getState().setAiProgress({
|
|
111
|
-
currentCheckLabel: msg.currentCheckLabel,
|
|
112
|
-
current: msg.current,
|
|
113
|
-
total: msg.total,
|
|
114
|
-
candidatesDone: msg.candidatesDone,
|
|
115
|
-
candidatesTotal: msg.candidatesTotal,
|
|
116
|
-
});
|
|
117
|
-
// Announce only on the first AI check so SR users hear the phase
|
|
118
|
-
// transition once, not 8 times. Sub-progress events fire often;
|
|
119
|
-
// don't speak those.
|
|
120
|
-
if (!prev) {
|
|
121
|
-
announcer.polite(`AI augmentation running ${msg.total} check${msg.total === 1 ? '' : 's'}.`);
|
|
122
|
-
}
|
|
123
|
-
})
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
offs.push(
|
|
127
|
-
on('AI_AUGMENTATION_FAILED_EVENT', (msg) => {
|
|
128
|
-
useStore.getState().setAiFailure({
|
|
129
|
-
severity: msg.severity,
|
|
130
|
-
reason: msg.reason,
|
|
131
|
-
checksAttempted: msg.checksAttempted,
|
|
132
|
-
checksSucceeded: msg.checksSucceeded,
|
|
133
|
-
checksErrored: msg.checksErrored,
|
|
134
|
-
errorDetails: msg.errorDetails,
|
|
135
|
-
});
|
|
136
|
-
// The banner has role="alert" — the SR announcement fires on mount.
|
|
137
|
-
// Don't also call announcer.assertive (would double-speak).
|
|
138
|
-
})
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
offs.push(
|
|
142
|
-
on('SCORECARD_UPDATED_EVENT', () => {
|
|
143
|
-
void refreshBaselineList();
|
|
144
|
-
})
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
// The license tier can change when the user activates a new token in another context;
|
|
148
|
-
// refresh on broadcast so the tier badge is current.
|
|
149
|
-
offs.push(
|
|
150
|
-
on('LICENSE_CHANGED_EVENT', () => {
|
|
151
|
-
void refreshLicenseTier();
|
|
152
|
-
})
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
// ─── Site crawl events ─────────────────────────────────────────────────
|
|
156
|
-
offs.push(
|
|
157
|
-
on('SITE_CRAWL_PROGRESS_EVENT', (msg) => {
|
|
158
|
-
const s = useStore.getState();
|
|
159
|
-
s.setSiteCrawlStatus('running');
|
|
160
|
-
s.setSiteCrawlProgress({
|
|
161
|
-
current: msg.current,
|
|
162
|
-
total: msg.total,
|
|
163
|
-
url: msg.url,
|
|
164
|
-
lastViolations: msg.violations,
|
|
165
|
-
});
|
|
166
|
-
if (msg.status === 'auditing' && msg.current === 1) {
|
|
167
|
-
announcer.polite(`Site crawl started, scanning up to ${msg.total} pages.`);
|
|
168
|
-
}
|
|
169
|
-
})
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
offs.push(
|
|
173
|
-
on('SITE_CRAWL_COMPLETE_EVENT', (msg) => {
|
|
174
|
-
const s = useStore.getState();
|
|
175
|
-
s.setSiteCrawlStatus('complete');
|
|
176
|
-
s.setSiteCrawlReport(msg.report);
|
|
177
|
-
s.setSiteCrawlProgress(null);
|
|
178
|
-
announcer.polite(
|
|
179
|
-
`Site crawl complete. Grade ${msg.report.siteGrade}, ${msg.report.totalUniqueViolations} unique violation${msg.report.totalUniqueViolations === 1 ? '' : 's'}.`
|
|
180
|
-
);
|
|
181
|
-
// rc.236 — Pre-warm resolutions cache for every crawled URL so
|
|
182
|
-
// that when the user clicks a page in the do-the-work report
|
|
183
|
-
// and drills into its Findings, the AI-resolved-fails count is
|
|
184
|
-
// correct on FIRST render (no two-render flicker like rc.235
|
|
185
|
-
// fixed for the now-removed aggregate view). Pre-rc.236 we also
|
|
186
|
-
// flattened all crawled results into the main `results` store
|
|
187
|
-
// to feed an aggregate "View site-wide Findings" button — that
|
|
188
|
-
// button is gone (the Crawl panel itself is the report now),
|
|
189
|
-
// and the aggregate-Findings view's broken math was what
|
|
190
|
-
// confused users. Drill-in setResults still happens, just only
|
|
191
|
-
// when the user clicks a specific page.
|
|
192
|
-
void (async () => {
|
|
193
|
-
try {
|
|
194
|
-
const { loadSiteCrawlPerUrlResults } = await import('../shared/site-crawl-storage');
|
|
195
|
-
const { getResolutionsForPage } = await import('../shared/incomplete-resolutions');
|
|
196
|
-
const perUrl = await loadSiteCrawlPerUrlResults();
|
|
197
|
-
if (!perUrl || perUrl.length === 0) return;
|
|
198
|
-
const setResolutionsForUrl = useStore.getState().setResolutionsForUrl;
|
|
199
|
-
await Promise.all(
|
|
200
|
-
perUrl.map(async (p) => {
|
|
201
|
-
try {
|
|
202
|
-
const r = await getResolutionsForPage(p.url);
|
|
203
|
-
setResolutionsForUrl(p.url, r);
|
|
204
|
-
} catch {
|
|
205
|
-
// best-effort
|
|
206
|
-
}
|
|
207
|
-
}),
|
|
208
|
-
);
|
|
209
|
-
} catch (err) {
|
|
210
|
-
// eslint-disable-next-line no-console
|
|
211
|
-
console.warn('[site-crawl] failed to pre-warm resolutions cache', err);
|
|
212
|
-
}
|
|
213
|
-
})();
|
|
214
|
-
})
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
offs.push(
|
|
218
|
-
on('SITE_CRAWL_FAILED_EVENT', (msg) => {
|
|
219
|
-
const s = useStore.getState();
|
|
220
|
-
s.setSiteCrawlStatus('failed');
|
|
221
|
-
s.setSiteCrawlError(msg.error.message);
|
|
222
|
-
s.setSiteCrawlProgress(null);
|
|
223
|
-
announcer.assertive(`Site crawl failed: ${msg.error.message}`);
|
|
224
|
-
})
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
// ─── Crawl discovery (review-before-audit) events ──────────────────────
|
|
228
|
-
offs.push(
|
|
229
|
-
on('CRAWL_DISCOVERY_PROGRESS_EVENT', (msg) => {
|
|
230
|
-
const s = useStore.getState();
|
|
231
|
-
if (s.crawlDiscoveryStatus !== 'discovering') s.setCrawlDiscoveryStatus('discovering');
|
|
232
|
-
s.setCrawlDiscoveryProgress(msg.found, msg.url);
|
|
233
|
-
if (msg.found === 1) announcer.polite('Discovering pages to review.');
|
|
234
|
-
})
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
offs.push(
|
|
238
|
-
on('CRAWL_DISCOVERY_COMPLETE_EVENT', (msg) => {
|
|
239
|
-
const s = useStore.getState();
|
|
240
|
-
s.setCrawlDiscoveryUrls(msg.urls);
|
|
241
|
-
s.setCrawlDiscoveryStatus('reviewing');
|
|
242
|
-
announcer.polite(
|
|
243
|
-
`Discovery complete. ${msg.urls.length} page${msg.urls.length === 1 ? '' : 's'} found — review and choose which to audit.`
|
|
244
|
-
);
|
|
245
|
-
})
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
offs.push(
|
|
249
|
-
on('CRAWL_DISCOVERY_FAILED_EVENT', (msg) => {
|
|
250
|
-
const s = useStore.getState();
|
|
251
|
-
s.resetCrawlDiscovery();
|
|
252
|
-
s.setSiteCrawlStatus('failed');
|
|
253
|
-
s.setSiteCrawlError(msg.error.message);
|
|
254
|
-
announcer.assertive(`Page discovery failed: ${msg.error.message}`);
|
|
255
|
-
})
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
return () => offs.forEach((off) => off());
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** rc.201 — Auto-export the just-completed audit if the user opted in.
|
|
262
|
-
* Silent + best-effort. Errors are logged but never thrown — the audit
|
|
263
|
-
* itself completed successfully even if file-system write fails. */
|
|
264
|
-
async function maybeAutoExport(
|
|
265
|
-
results: AuditResult[],
|
|
266
|
-
delta: DeltaResult | null | undefined,
|
|
267
|
-
componentId: string | null | undefined,
|
|
268
|
-
): Promise<void> {
|
|
269
|
-
if (results.length === 0) return;
|
|
270
|
-
try {
|
|
271
|
-
const settings = await getAutoExportSettings();
|
|
272
|
-
if (!settings.enabled) return;
|
|
273
|
-
// Tier-gate. Free tier won't get the toggle exposed post-launch;
|
|
274
|
-
// even if a tier change happens between toggle-on and audit-complete,
|
|
275
|
-
// the gate re-checks here.
|
|
276
|
-
const tier = useStore.getState().tier;
|
|
277
|
-
if (isLockoutInEffectForTier(tier)) return;
|
|
278
|
-
if (!isFeatureAllowed(tier, 'autoExportAuditReports')) return;
|
|
279
|
-
|
|
280
|
-
// rc.221 — Loop every enabled format. Each completed audit produces
|
|
281
|
-
// each requested artifact (defense-bundle for legal + ai-prompt for
|
|
282
|
-
// dev handoff + html-print for sharing — pick the set that matches
|
|
283
|
-
// the workflow).
|
|
284
|
-
const url = results[0]?.pageUrl ?? results[0]?.scope ?? '';
|
|
285
|
-
const exported: string[] = [];
|
|
286
|
-
// rc.228 — Snapshot wall-clock now so all formats in this auto-
|
|
287
|
-
// export round see the same value.
|
|
288
|
-
const wallClockMs = useStore.getState().lastAuditWallClockMs ?? undefined;
|
|
289
|
-
for (const format of settings.formats) {
|
|
290
|
-
try {
|
|
291
|
-
const res = await request<ExportResponse>({
|
|
292
|
-
type: 'EXPORT_REQUEST',
|
|
293
|
-
format,
|
|
294
|
-
results,
|
|
295
|
-
delta: delta ?? undefined,
|
|
296
|
-
wallClockMs,
|
|
297
|
-
});
|
|
298
|
-
// EXPORT_REQUEST returns a lockout error string when the SW-side
|
|
299
|
-
// gate refuses. Treat any very-short body as "refused."
|
|
300
|
-
if (typeof res.content !== 'string' || res.content.length < 200) continue;
|
|
301
|
-
const filename = bundleFilename(format, componentId ?? null);
|
|
302
|
-
triggerDownload(res.content, filename);
|
|
303
|
-
if (url) {
|
|
304
|
-
await recordLastAutoExport({
|
|
305
|
-
url,
|
|
306
|
-
exportedAt: new Date().toISOString(),
|
|
307
|
-
format,
|
|
308
|
-
filename,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
exported.push(filename);
|
|
312
|
-
} catch (formatErr) {
|
|
313
|
-
console.warn(`[auto-export] format ${format} failed:`, formatErr);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
if (exported.length > 0) {
|
|
317
|
-
announcer.polite(`Audit auto-exported ${exported.length} file${exported.length === 1 ? '' : 's'}.`);
|
|
318
|
-
}
|
|
319
|
-
} catch (err) {
|
|
320
|
-
// Silent failure — surface in console for debugging but don't
|
|
321
|
-
// interrupt the user's audit-review flow with a modal.
|
|
322
|
-
console.warn('[auto-export] failed:', err);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
async function persistLastAudit(payload: PersistedAudit): Promise<void> {
|
|
327
|
-
// Strip screenshotDataUrl from each result before persisting. With a full
|
|
328
|
-
// state matrix (up to 144 states), 144 base64 JPEGs at ~50-200KB each blow
|
|
329
|
-
// chrome.storage.local's 10MB MV3 quota — manifestly observed in panel
|
|
330
|
-
// unhandled rejections with "Resource::kQuotaBytes quota exceeded".
|
|
331
|
-
//
|
|
332
|
-
// Trade-off: screenshots remain in-memory for the current panel session
|
|
333
|
-
// (export bundles run in this session still have them). After a panel
|
|
334
|
-
// reopen / SW restart, violations + delta + componentId are restored but
|
|
335
|
-
// screenshots aren't — the user would need to re-audit to regenerate them.
|
|
336
|
-
const stripped: PersistedAudit = {
|
|
337
|
-
...payload,
|
|
338
|
-
results: payload.results.map(({ screenshotDataUrl: _drop, ...rest }) => rest),
|
|
339
|
-
};
|
|
340
|
-
try {
|
|
341
|
-
await chrome.storage.local.set({ [LAST_AUDIT_KEY]: stripped });
|
|
342
|
-
} catch (err) {
|
|
343
|
-
// Even stripped, a very-large violation set could still exceed quota.
|
|
344
|
-
// Better to leave the in-memory results intact than crash the panel on
|
|
345
|
-
// a non-essential persistence step.
|
|
346
|
-
console.warn('[wire-messaging] persistLastAudit failed; results stay in-memory only', err);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
export async function loadLastAudit(): Promise<void> {
|
|
351
|
-
const r = await chrome.storage.local.get(LAST_AUDIT_KEY);
|
|
352
|
-
const last = r[LAST_AUDIT_KEY] as PersistedAudit | undefined;
|
|
353
|
-
if (!last) return;
|
|
354
|
-
useStore.setState({
|
|
355
|
-
results: last.results,
|
|
356
|
-
delta: last.delta,
|
|
357
|
-
componentId: last.componentId,
|
|
358
|
-
status: 'complete',
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
export async function clearLastAudit(): Promise<void> {
|
|
363
|
-
await chrome.storage.local.remove(LAST_AUDIT_KEY);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/** Wipe the SINGLE-PAGE slice only — store results + the persisted
|
|
367
|
-
* last-audit cache. Reused by the navigation auto-clear path
|
|
368
|
-
* (clearIfTargetUrlChanged), which must NOT touch a crawl: navigating the
|
|
369
|
-
* audited tab away should drop the stale single-page audit but leave a
|
|
370
|
-
* separate site-crawl alone. User-triggered "Clear results" buttons want a
|
|
371
|
-
* COMPLETE wipe instead — use clearAllResults() for those. */
|
|
372
|
-
export async function clearAudit(): Promise<void> {
|
|
373
|
-
useStore.getState().clearResults();
|
|
374
|
-
await clearLastAudit();
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/** rc.360 — Full user-triggered wipe: the single-page slice AND the shared
|
|
378
|
-
* crawl pool (in-memory siteCrawlReport, the persisted `siteCrawlReport`
|
|
379
|
-
* key, and the per-URL IDB store). Before the single-data-pool work
|
|
380
|
-
* (rc.351/353/354) only the single-page slice rendered a grade, so a bare
|
|
381
|
-
* clearAudit() was enough. Now the v1 LastRunBanner and owner-mode
|
|
382
|
-
* CrawlResultGrade both derive a grade from the crawl pool — so unless
|
|
383
|
-
* Clear wipes that pool too, the grade reappears the instant after Clear is
|
|
384
|
-
* clicked. Every "Clear results"/"Clear" button across all three UIs routes
|
|
385
|
-
* through here so the wipe is uniform regardless of which UI triggers it. */
|
|
386
|
-
export async function clearAllResults(): Promise<void> {
|
|
387
|
-
const store = useStore.getState();
|
|
388
|
-
store.clearResults();
|
|
389
|
-
store.setSiteCrawlReport(null); // in-memory; also fires a persist-null
|
|
390
|
-
store.setSiteCrawlError(null);
|
|
391
|
-
store.setSiteCrawlStatus('idle');
|
|
392
|
-
await clearLastAudit();
|
|
393
|
-
// Await the persisted wipe (removes the storage key AND deletes the
|
|
394
|
-
// per-URL IDB record) so a quick panel close/reopen right after Clear
|
|
395
|
-
// cannot rehydrate the old grade from a half-finished fire-and-forget.
|
|
396
|
-
const { saveSiteCrawlReport } = await import('../shared/site-crawl-storage');
|
|
397
|
-
await saveSiteCrawlReport(null);
|
|
398
|
-
// rc.361 — Also invalidate the audit-cache (flows.ts fast-path). Without
|
|
399
|
-
// this, re-auditing a page whose DOM fingerprint is unchanged replays the
|
|
400
|
-
// cached results in ~10ms (broadcasts AUDIT_COMPLETE, skips the whole state
|
|
401
|
-
// matrix) — so right after Clear the user gets the SAME grade back
|
|
402
|
-
// "instantly instead of actually running the check." Clear means start
|
|
403
|
-
// fresh, so the next audit must actually run. (Cliff's ".org instant grade":
|
|
404
|
-
// .org was cached from constant re-auditing; a freshly-visited site wasn't,
|
|
405
|
-
// which is why it only happened on .org.)
|
|
406
|
-
const { clearAllCachedAudits } = await import('../shared/audit-cache');
|
|
407
|
-
await clearAllCachedAudits();
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/** Strip the URL fragment so anchor-only changes (#section) don't read as
|
|
411
|
-
* navigation. Path + querystring + host all still count. */
|
|
412
|
-
function normalizeUrl(u: string): string {
|
|
413
|
-
try {
|
|
414
|
-
const url = new URL(u);
|
|
415
|
-
url.hash = '';
|
|
416
|
-
return url.toString();
|
|
417
|
-
} catch {
|
|
418
|
-
return u;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/** If the currently-loaded audit's pageUrl no longer matches the audit target
|
|
423
|
-
* tab's URL, clear the audit. Called on side-panel mount AND whenever the
|
|
424
|
-
* target tab navigates or the active tab changes (see App.tsx). */
|
|
425
|
-
export async function clearIfTargetUrlChanged(): Promise<void> {
|
|
426
|
-
const { results } = useStore.getState();
|
|
427
|
-
if (results.length === 0) return;
|
|
428
|
-
const audited = results[0]?.pageUrl;
|
|
429
|
-
if (!audited) return; // legacy result without pageUrl — leave alone
|
|
430
|
-
|
|
431
|
-
const tabId = await getAuditTargetTabId();
|
|
432
|
-
if (!tabId) {
|
|
433
|
-
// No usable target tab anymore (e.g. only chrome:// pages open) — treat
|
|
434
|
-
// as a navigation away from the audited page.
|
|
435
|
-
await clearAudit();
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
try {
|
|
439
|
-
const tab = await chrome.tabs.get(tabId);
|
|
440
|
-
if (normalizeUrl(tab.url ?? '') !== normalizeUrl(audited)) {
|
|
441
|
-
await clearAudit();
|
|
442
|
-
}
|
|
443
|
-
} catch {
|
|
444
|
-
// tab gone — also stale
|
|
445
|
-
await clearAudit();
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
export async function refreshBaselineList(): Promise<void> {
|
|
450
|
-
const res = await request<BaselineListResponse>({ type: 'BASELINE_LIST' });
|
|
451
|
-
useStore.getState().setBaselineList(res.items);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
export async function refreshLicenseTier(opts?: { forceRefresh?: boolean }): Promise<void> {
|
|
455
|
-
const res = await request<TierGetResponse>({
|
|
456
|
-
type: 'TIER_GET',
|
|
457
|
-
forceRefresh: opts?.forceRefresh === true,
|
|
458
|
-
});
|
|
459
|
-
useStore.getState().setTier(res.tier, {
|
|
460
|
-
trialDaysRemaining: res.trialDaysRemaining,
|
|
461
|
-
seatsUsed: res.seatsUsed,
|
|
462
|
-
seatsTotal: res.seatsTotal,
|
|
463
|
-
planCode: res.planCode,
|
|
464
|
-
licenseDaysRemaining: res.licenseDaysRemaining,
|
|
465
|
-
pastDue: res.pastDue,
|
|
466
|
-
});
|
|
467
|
-
// Fold the in-app messages refresh into the same wake-up so the bell
|
|
468
|
-
// stays in sync with the license-validate cadence. Failures are silent
|
|
469
|
-
// — the bell just stays in its last-known state.
|
|
470
|
-
void refreshInAppMessages();
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
export async function refreshInAppMessages(): Promise<void> {
|
|
474
|
-
// Dynamic import keeps the messages-client out of the initial chunk —
|
|
475
|
-
// bell content is only ever painted after the panel mounts.
|
|
476
|
-
const { fetchMessages } = await import('../modules/messages-client');
|
|
477
|
-
const mode = useStore.getState().userMode;
|
|
478
|
-
const res = await fetchMessages(mode);
|
|
479
|
-
if (!res) return;
|
|
480
|
-
useStore.getState().setMessages(res.messages, res.unreadCount, res.criticalUnacked);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/** Load the user's persona setting. rc.333 — 'power' added as a
|
|
484
|
-
* first-class persona; first-launch defaults to 'owner' (no
|
|
485
|
-
* wizard). The Root component in main.tsx handles the storage-
|
|
486
|
-
* to-mode mapping; this hydrates the side-panel store from the
|
|
487
|
-
* same persisted value. */
|
|
488
|
-
/** rc.337 — Warm the side-panel's in-memory rubric snapshot from
|
|
489
|
-
* chrome.storage.local. The SW polls the server hourly and writes
|
|
490
|
-
* to storage; this reads the latest cached config so synchronous
|
|
491
|
-
* consumers (computeLawfareRisk in OwnerView, etc.) see the
|
|
492
|
-
* server-fetched curated lists rather than the bundled fallback.
|
|
493
|
-
* Also subscribes to storage changes so server-pushed updates
|
|
494
|
-
* propagate live without panel reload. */
|
|
495
|
-
export async function warmAndWatchRubrics(): Promise<void> {
|
|
496
|
-
async function apply(): Promise<void> {
|
|
497
|
-
const entry = await getCachedRubricConfig();
|
|
498
|
-
if (!entry || rubricFreshness(entry) === 'expired') {
|
|
499
|
-
setResolvedRubrics(null);
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
setResolvedRubrics(entry.config);
|
|
503
|
-
}
|
|
504
|
-
await apply();
|
|
505
|
-
chrome.storage.onChanged.addListener((changes, area) => {
|
|
506
|
-
if (area !== 'local' || !('rubricConfigCache' in changes)) return;
|
|
507
|
-
void apply();
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
export async function refreshUserMode(): Promise<void> {
|
|
512
|
-
const res = await request<SettingsResponse>({ type: 'SETTINGS_GET', key: 'userMode' });
|
|
513
|
-
const v = res.data;
|
|
514
|
-
const mode: UserMode | null =
|
|
515
|
-
v === 'owner' || v === 'power' || v === 'dev' ? v : null;
|
|
516
|
-
useStore.getState().setUserMode(mode);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
export function startKeepalive(): chrome.runtime.Port {
|
|
520
|
-
return chrome.runtime.connect({ name: 'audit-keepalive' });
|
|
521
|
-
}
|