@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.
Files changed (32) hide show
  1. package/dist/assets/{ErrorBoundary-BH1wbHb3.js → ErrorBoundary-Ck0XMUiQ.js} +22 -22
  2. package/dist/assets/{copy-ai-fixer-prompt-FO0N3IMf.js → copy-ai-fixer-prompt-CKp52Td1.js} +2 -2
  3. package/dist/assets/{devtools-panel-BAHfgRsc.js → devtools-panel-Dw0aFhIc.js} +1 -1
  4. package/dist/assets/{parallel-tab-flow-C4bjrDqL.js → parallel-tab-flow-BuZZIdRb.js} +1 -1
  5. package/dist/assets/{scheduled-audit-runner-XCDyFhY6.js → scheduled-audit-runner-9R-aGzwO.js} +137 -137
  6. package/dist/assets/{service-worker.ts-57FwiS_-.js → service-worker.ts-CyL2aQvI.js} +2 -2
  7. package/dist/assets/{side-panel-Dl247dJv.js → side-panel-CuEUwRBD.js} +3 -3
  8. package/dist/assets/{site-report-renderer-CNWZjbOd.js → site-report-renderer-46_JyAkB.js} +1 -1
  9. package/dist/devtools/panel.html +3 -3
  10. package/dist/manifest.json +3 -4
  11. package/dist/service-worker-loader.js +1 -1
  12. package/dist/side-panel/side-panel.html +4 -4
  13. package/package.json +1 -1
  14. package/dist/side-panel/App.tsx +0 -308
  15. package/dist/side-panel/README.md +0 -57
  16. package/dist/side-panel/audit-launcher.test.ts +0 -56
  17. package/dist/side-panel/audit-launcher.ts +0 -98
  18. package/dist/side-panel/azure-devops-issue.test.ts +0 -68
  19. package/dist/side-panel/azure-devops-issue.ts +0 -89
  20. package/dist/side-panel/format-component-id.test.ts +0 -89
  21. package/dist/side-panel/format-component-id.ts +0 -40
  22. package/dist/side-panel/github-issue.test.ts +0 -102
  23. package/dist/side-panel/github-issue.ts +0 -66
  24. package/dist/side-panel/gitlab-issue.test.ts +0 -53
  25. package/dist/side-panel/gitlab-issue.ts +0 -78
  26. package/dist/side-panel/jira-issue.ts +0 -64
  27. package/dist/side-panel/main.tsx +0 -86
  28. package/dist/side-panel/store.ts +0 -435
  29. package/dist/side-panel/styles.css +0 -55
  30. package/dist/side-panel/use-audit-view-model.ts +0 -116
  31. package/dist/side-panel/wire-messaging.test.ts +0 -234
  32. 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
- }