@everscribe/components-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1933 @@
1
+ import { useState, useEffect, useRef, useMemo, useCallback, useSyncExternalStore } from 'react';
2
+ import { fetchTokenViaOpts, ALL_COLUMNS, EmbedError, exportEvents, parseClaims, createDistinctValuesStore, EMPTY_DISTINCT_VALUES, createEventsStore, COLUMN_LABELS, RELATIVE_TIME_TICK_MS, hasParseableDiff, renderDiff, generateNLPFilters, formatTimeCell } from '@everscribe/components-core';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+ import { createPortal } from 'react-dom';
5
+
6
+ // src/AuditTrail.tsx
7
+ function useNLP(opts) {
8
+ const [state, setState] = useState({ phase: "idle" });
9
+ const ctrlRef = useRef(null);
10
+ const submit = useCallback(
11
+ async (query) => {
12
+ const trimmed = query.trim();
13
+ if (!trimmed) return;
14
+ if (!opts.token) {
15
+ setState({ phase: "error", query: trimmed, error: "not_configured" });
16
+ return;
17
+ }
18
+ ctrlRef.current?.abort();
19
+ const ctrl = new AbortController();
20
+ ctrlRef.current = ctrl;
21
+ setState({ phase: "loading" });
22
+ try {
23
+ const result = await generateNLPFilters({
24
+ apiBase: opts.apiBase,
25
+ token: opts.token,
26
+ query: trimmed,
27
+ signal: ctrl.signal
28
+ });
29
+ if (ctrl.signal.aborted) return;
30
+ setState({ phase: "ready", query: trimmed, result });
31
+ } catch (err) {
32
+ if (err instanceof DOMException && err.name === "AbortError") return;
33
+ if (err instanceof EmbedError && err.kind === "unauthorized") {
34
+ const refreshed = await fetchTokenViaOpts({
35
+ tokenEndpoint: opts.tokenEndpoint,
36
+ onTokenExpired: opts.onTokenExpired
37
+ });
38
+ if (refreshed && !ctrl.signal.aborted) {
39
+ try {
40
+ const retry = await generateNLPFilters({
41
+ apiBase: opts.apiBase,
42
+ token: refreshed,
43
+ query: trimmed,
44
+ signal: ctrl.signal
45
+ });
46
+ setState({ phase: "ready", query: trimmed, result: retry });
47
+ return;
48
+ } catch (retryErr) {
49
+ setState({
50
+ phase: "error",
51
+ query: trimmed,
52
+ error: classifyNLPError(retryErr)
53
+ });
54
+ return;
55
+ }
56
+ }
57
+ }
58
+ setState({ phase: "error", query: trimmed, error: classifyNLPError(err) });
59
+ }
60
+ },
61
+ [opts.apiBase, opts.token, opts.tokenEndpoint, opts.onTokenExpired]
62
+ );
63
+ const reset = useCallback(() => {
64
+ ctrlRef.current?.abort();
65
+ setState({ phase: "idle" });
66
+ }, []);
67
+ return { state, submit, reset };
68
+ }
69
+ function classifyNLPError(err) {
70
+ if (!(err instanceof EmbedError)) return "unknown";
71
+ if (err.status === 503) {
72
+ if (err.message?.includes("busy")) return "provider_busy";
73
+ return "not_configured";
74
+ }
75
+ if (err.status === 429) return "rate_limited";
76
+ if (err.status === 403) return "not_allowed";
77
+ if (err.kind === "bad_request") return "bad_request";
78
+ return "unknown";
79
+ }
80
+ var TIME_PRESETS = [
81
+ { key: "24h", label: "24h" },
82
+ { key: "7d", label: "7d" },
83
+ { key: "30d", label: "30d" },
84
+ { key: "custom", label: "Custom" },
85
+ { key: "all", label: "All" }
86
+ ];
87
+ function pickInitialTab(v, claims) {
88
+ if (v.nlpQ && claims?.allow_nlp) return "ai";
89
+ if (v.q && claims?.allow_dsl_input) return "query";
90
+ if (hasAnyColumnFilter(v)) return "filters";
91
+ if (claims?.allow_nlp) return "ai";
92
+ return "filters";
93
+ }
94
+ function FiltersPanel({
95
+ value,
96
+ onChange,
97
+ distinct,
98
+ claims,
99
+ apiBase,
100
+ token,
101
+ tokenEndpoint,
102
+ onTokenExpired,
103
+ onApplied
104
+ }) {
105
+ const allowNLP = !!claims?.allow_nlp;
106
+ const allowDSL = !!claims?.allow_dsl_input;
107
+ const [activeTab, setActiveTab] = useState(
108
+ () => pickInitialTab(value, claims)
109
+ );
110
+ return /* @__PURE__ */ jsxs("div", { className: "audit-trail-filter-panel", children: [
111
+ /* @__PURE__ */ jsxs("div", { className: "audit-trail-filter-modes", role: "tablist", "aria-label": "Filter mode", children: [
112
+ allowNLP && /* @__PURE__ */ jsx(
113
+ "button",
114
+ {
115
+ type: "button",
116
+ role: "tab",
117
+ "aria-selected": activeTab === "ai",
118
+ className: tabClass(activeTab === "ai"),
119
+ onClick: () => setActiveTab("ai"),
120
+ children: "Prompt"
121
+ }
122
+ ),
123
+ /* @__PURE__ */ jsx(
124
+ "button",
125
+ {
126
+ type: "button",
127
+ role: "tab",
128
+ "aria-selected": activeTab === "filters",
129
+ className: tabClass(activeTab === "filters"),
130
+ onClick: () => setActiveTab("filters"),
131
+ children: "Filters"
132
+ }
133
+ ),
134
+ allowDSL && /* @__PURE__ */ jsx(
135
+ "button",
136
+ {
137
+ type: "button",
138
+ role: "tab",
139
+ "aria-selected": activeTab === "query",
140
+ className: tabClass(activeTab === "query"),
141
+ onClick: () => setActiveTab("query"),
142
+ children: "Query"
143
+ }
144
+ )
145
+ ] }),
146
+ activeTab === "ai" && allowNLP && /* @__PURE__ */ jsx(
147
+ AITabPanel,
148
+ {
149
+ value,
150
+ onChange,
151
+ apiBase,
152
+ token,
153
+ tokenEndpoint,
154
+ onTokenExpired
155
+ }
156
+ ),
157
+ activeTab === "filters" && /* @__PURE__ */ jsx(
158
+ FiltersTabPanel,
159
+ {
160
+ value,
161
+ onChange,
162
+ distinct,
163
+ onApplied
164
+ }
165
+ ),
166
+ activeTab === "query" && allowDSL && /* @__PURE__ */ jsx(QueryTabPanel, { value, onChange })
167
+ ] });
168
+ }
169
+ function tabClass(active) {
170
+ return active ? "audit-trail-filter-mode audit-trail-filter-mode-selected" : "audit-trail-filter-mode";
171
+ }
172
+ function AITabPanel({
173
+ value,
174
+ onChange,
175
+ apiBase,
176
+ token,
177
+ tokenEndpoint,
178
+ onTokenExpired
179
+ }) {
180
+ const [draft, setDraft] = useState(value.nlpQ ?? "");
181
+ const nlp = useNLP({ apiBase, token, tokenEndpoint, onTokenExpired });
182
+ const lastAppliedRef = useRef(null);
183
+ useEffect(() => {
184
+ if (nlp.state.phase !== "ready") return;
185
+ const sig = nlp.state.query + "|" + (nlp.state.result.dsl ?? "");
186
+ if (lastAppliedRef.current === sig) return;
187
+ lastAppliedRef.current = sig;
188
+ onChange({
189
+ ...value,
190
+ q: nlp.state.result.dsl || void 0,
191
+ nlpQ: nlp.state.query,
192
+ nlpExplanation: nlp.state.result.explanation,
193
+ nlpUnsupported: nlp.state.result.unsupported
194
+ });
195
+ }, [nlp.state, onChange, value]);
196
+ const isBusy = nlp.state.phase === "loading";
197
+ const handleSubmit = useCallback(
198
+ (e) => {
199
+ e.preventDefault();
200
+ void nlp.submit(draft);
201
+ },
202
+ [draft, nlp]
203
+ );
204
+ return /* @__PURE__ */ jsxs("form", { className: "audit-trail-nlp-form", onSubmit: handleSubmit, children: [
205
+ /* @__PURE__ */ jsxs("label", { className: "audit-trail-nlp-label", htmlFor: "audit-trail-nlp-input", children: [
206
+ "Ask in plain English",
207
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-badge audit-trail-badge-info", title: "Beta", children: "Beta" })
208
+ ] }),
209
+ /* @__PURE__ */ jsx(
210
+ "input",
211
+ {
212
+ id: "audit-trail-nlp-input",
213
+ type: "text",
214
+ className: "audit-trail-nlp-input",
215
+ placeholder: "e.g. failed logins last 24 hours",
216
+ value: draft,
217
+ onChange: (e) => setDraft(e.target.value),
218
+ maxLength: 500,
219
+ "aria-label": "Natural-language filter query",
220
+ disabled: isBusy
221
+ }
222
+ ),
223
+ value.q && value.nlpQ && /* @__PURE__ */ jsx("div", { className: "audit-trail-nlp-banner", children: /* @__PURE__ */ jsxs("p", { children: [
224
+ /* @__PURE__ */ jsx("strong", { children: "Translated to:" }),
225
+ " ",
226
+ /* @__PURE__ */ jsx("code", { className: "audit-trail-nlp-translated", children: value.q })
227
+ ] }) }),
228
+ nlp.state.phase === "error" && /* @__PURE__ */ jsx("div", { className: "audit-trail-error", children: nlpErrorMessage(nlp.state.error) }),
229
+ value.nlpUnsupported && value.nlpUnsupported.length > 0 && /* @__PURE__ */ jsx("div", { className: "audit-trail-error audit-trail-nlp-unsupported", children: /* @__PURE__ */ jsx("ul", { children: value.nlpUnsupported.map((u, i) => /* @__PURE__ */ jsxs("li", { children: [
230
+ "Couldn't apply: ",
231
+ u
232
+ ] }, i)) }) }),
233
+ /* @__PURE__ */ jsx("div", { className: "audit-trail-filter-actions", children: /* @__PURE__ */ jsx(
234
+ "button",
235
+ {
236
+ type: "submit",
237
+ className: "audit-trail-button audit-trail-button-secondary",
238
+ disabled: !draft.trim() || isBusy,
239
+ children: isBusy ? /* @__PURE__ */ jsxs(Fragment, { children: [
240
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-button-spinner" }),
241
+ " Translating\u2026"
242
+ ] }) : "Search"
243
+ }
244
+ ) })
245
+ ] });
246
+ }
247
+ function nlpErrorMessage(reason) {
248
+ switch (reason) {
249
+ case "not_configured":
250
+ return "Natural-language filtering isn't configured on this server.";
251
+ case "not_allowed":
252
+ return "This embed token does not permit natural-language queries.";
253
+ case "rate_limited":
254
+ return "You've hit the per-hour limit. Try again in a few minutes.";
255
+ case "provider_busy":
256
+ return "The natural-language service is busy right now. Try again in a moment.";
257
+ case "bad_request":
258
+ return "That query couldn't be processed. Try rephrasing.";
259
+ default:
260
+ return "Couldn't process that query. Try simpler terms.";
261
+ }
262
+ }
263
+ function FiltersTabPanel({ value, onChange, distinct, onApplied }) {
264
+ const [draftSince, setDraftSince] = useState(value.since ?? "");
265
+ const [draftBefore, setDraftBefore] = useState(value.before ?? "");
266
+ const [draftAction, setDraftAction] = useState(value.action ?? "");
267
+ const [draftActorType, setDraftActorType] = useState(value.actorType ?? "");
268
+ const [draftTenantID, setDraftTenantID] = useState(value.tenantId ?? "");
269
+ const [draftTargetType, setDraftTargetType] = useState(value.targetType ?? "");
270
+ const [draftResultStatus, setDraftResultStatus] = useState(value.resultStatus ?? "");
271
+ const [draftActor, setDraftActor] = useState(value.actor ?? "");
272
+ const [draftTargetID, setDraftTargetID] = useState(value.targetId ?? "");
273
+ const [draftOriginIP, setDraftOriginIP] = useState(value.originIP ?? "");
274
+ useEffect(() => {
275
+ setDraftSince(value.since ?? "");
276
+ setDraftBefore(value.before ?? "");
277
+ setDraftAction(value.action ?? "");
278
+ setDraftActorType(value.actorType ?? "");
279
+ setDraftTenantID(value.tenantId ?? "");
280
+ setDraftTargetType(value.targetType ?? "");
281
+ setDraftResultStatus(value.resultStatus ?? "");
282
+ setDraftActor(value.actor ?? "");
283
+ setDraftTargetID(value.targetId ?? "");
284
+ setDraftOriginIP(value.originIP ?? "");
285
+ }, [
286
+ value.since,
287
+ value.before,
288
+ value.action,
289
+ value.actorType,
290
+ value.tenantId,
291
+ value.targetType,
292
+ value.resultStatus,
293
+ value.actor,
294
+ value.targetId,
295
+ value.originIP
296
+ ]);
297
+ const norm = (s) => s === "" ? void 0 : s;
298
+ const dirty = norm(draftAction) !== value.action || norm(draftActorType) !== value.actorType || norm(draftTenantID) !== value.tenantId || norm(draftTargetType) !== value.targetType || norm(draftResultStatus) !== value.resultStatus || norm(draftActor) !== value.actor || norm(draftTargetID) !== value.targetId || norm(draftOriginIP) !== value.originIP || value.range === "custom" && (norm(draftSince) !== value.since || norm(draftBefore) !== value.before);
299
+ const handleApply = () => {
300
+ onChange({
301
+ ...value,
302
+ action: norm(draftAction),
303
+ actorType: norm(draftActorType),
304
+ tenantId: norm(draftTenantID),
305
+ targetType: norm(draftTargetType),
306
+ resultStatus: norm(draftResultStatus),
307
+ actor: norm(draftActor),
308
+ targetId: norm(draftTargetID),
309
+ originIP: norm(draftOriginIP),
310
+ since: value.range === "custom" ? norm(draftSince) : value.since,
311
+ before: value.range === "custom" ? norm(draftBefore) : value.before,
312
+ q: void 0,
313
+ nlpQ: void 0,
314
+ nlpExplanation: void 0,
315
+ nlpUnsupported: void 0
316
+ });
317
+ onApplied?.();
318
+ };
319
+ const handlePresetClick = (range) => {
320
+ if (range === "custom") {
321
+ onChange({ ...value, range });
322
+ return;
323
+ }
324
+ onChange({ ...value, range, since: void 0, before: void 0 });
325
+ };
326
+ return /* @__PURE__ */ jsxs("div", { className: "audit-trail-filter-tab-panel", children: [
327
+ /* @__PURE__ */ jsx(
328
+ "div",
329
+ {
330
+ className: "audit-trail-filter-tabs",
331
+ role: "tablist",
332
+ "aria-label": "Time range",
333
+ children: TIME_PRESETS.map((p) => /* @__PURE__ */ jsx(
334
+ "button",
335
+ {
336
+ type: "button",
337
+ role: "tab",
338
+ "aria-selected": value.range === p.key,
339
+ className: value.range === p.key ? "audit-trail-filter-tab audit-trail-filter-tab-selected" : "audit-trail-filter-tab",
340
+ onClick: () => handlePresetClick(p.key),
341
+ children: p.label
342
+ },
343
+ p.key
344
+ ))
345
+ }
346
+ ),
347
+ value.range === "custom" && /* @__PURE__ */ jsxs("div", { className: "audit-trail-filter-row", children: [
348
+ /* @__PURE__ */ jsxs("label", { className: "audit-trail-filter-field", children: [
349
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-filter-field-label", children: "From" }),
350
+ /* @__PURE__ */ jsx(
351
+ "input",
352
+ {
353
+ type: "datetime-local",
354
+ className: "audit-trail-filter-input",
355
+ value: draftSince,
356
+ onChange: (e) => setDraftSince(e.target.value)
357
+ }
358
+ )
359
+ ] }),
360
+ /* @__PURE__ */ jsxs("label", { className: "audit-trail-filter-field", children: [
361
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-filter-field-label", children: "To" }),
362
+ /* @__PURE__ */ jsx(
363
+ "input",
364
+ {
365
+ type: "datetime-local",
366
+ className: "audit-trail-filter-input",
367
+ value: draftBefore,
368
+ onChange: (e) => setDraftBefore(e.target.value)
369
+ }
370
+ )
371
+ ] })
372
+ ] }),
373
+ /* @__PURE__ */ jsxs("div", { className: "audit-trail-filter-row", children: [
374
+ /* @__PURE__ */ jsxs(
375
+ "select",
376
+ {
377
+ className: "audit-trail-filter-select",
378
+ "aria-label": "Action",
379
+ value: draftAction,
380
+ onChange: (e) => setDraftAction(e.target.value),
381
+ children: [
382
+ /* @__PURE__ */ jsx("option", { value: "", children: "All actions" }),
383
+ distinct.actions.map((a) => /* @__PURE__ */ jsx("option", { value: a, children: a }, a))
384
+ ]
385
+ }
386
+ ),
387
+ /* @__PURE__ */ jsxs(
388
+ "select",
389
+ {
390
+ className: "audit-trail-filter-select",
391
+ "aria-label": "Actor type",
392
+ value: draftActorType,
393
+ onChange: (e) => setDraftActorType(e.target.value),
394
+ children: [
395
+ /* @__PURE__ */ jsx("option", { value: "", children: "All actor types" }),
396
+ distinct.actorTypes.map((t) => /* @__PURE__ */ jsx("option", { value: t, children: t }, t))
397
+ ]
398
+ }
399
+ ),
400
+ /* @__PURE__ */ jsxs(
401
+ "select",
402
+ {
403
+ className: "audit-trail-filter-select",
404
+ "aria-label": "Target type",
405
+ value: draftTargetType,
406
+ onChange: (e) => setDraftTargetType(e.target.value),
407
+ children: [
408
+ /* @__PURE__ */ jsx("option", { value: "", children: "All target types" }),
409
+ distinct.targetTypes.map((t) => /* @__PURE__ */ jsx("option", { value: t, children: t }, t))
410
+ ]
411
+ }
412
+ ),
413
+ distinct.tenants.length > 0 && /* @__PURE__ */ jsxs(
414
+ "select",
415
+ {
416
+ className: "audit-trail-filter-select",
417
+ "aria-label": "Tenant",
418
+ value: draftTenantID,
419
+ onChange: (e) => setDraftTenantID(e.target.value),
420
+ children: [
421
+ /* @__PURE__ */ jsx("option", { value: "", children: "All tenants" }),
422
+ distinct.tenants.map((t) => /* @__PURE__ */ jsx("option", { value: t, children: t }, t))
423
+ ]
424
+ }
425
+ ),
426
+ distinct.resultStatuses.length > 0 && /* @__PURE__ */ jsxs(
427
+ "select",
428
+ {
429
+ className: "audit-trail-filter-select",
430
+ "aria-label": "Result",
431
+ value: draftResultStatus,
432
+ onChange: (e) => setDraftResultStatus(e.target.value),
433
+ children: [
434
+ /* @__PURE__ */ jsx("option", { value: "", children: "All results" }),
435
+ distinct.resultStatuses.map((s) => /* @__PURE__ */ jsx("option", { value: s, children: s }, s))
436
+ ]
437
+ }
438
+ )
439
+ ] }),
440
+ /* @__PURE__ */ jsx("div", { className: "audit-trail-filter-row", children: /* @__PURE__ */ jsx(
441
+ "input",
442
+ {
443
+ type: "text",
444
+ className: "audit-trail-filter-input audit-trail-filter-actor-input",
445
+ placeholder: "Actor (id, name, email)",
446
+ value: draftActor,
447
+ onChange: (e) => setDraftActor(e.target.value)
448
+ }
449
+ ) }),
450
+ /* @__PURE__ */ jsx("div", { className: "audit-trail-filter-row", children: /* @__PURE__ */ jsx(
451
+ "input",
452
+ {
453
+ type: "text",
454
+ className: "audit-trail-filter-input audit-trail-filter-wide-input",
455
+ placeholder: "Target ID",
456
+ value: draftTargetID,
457
+ onChange: (e) => setDraftTargetID(e.target.value)
458
+ }
459
+ ) }),
460
+ /* @__PURE__ */ jsx("div", { className: "audit-trail-filter-row", children: /* @__PURE__ */ jsx(
461
+ "input",
462
+ {
463
+ type: "text",
464
+ className: "audit-trail-filter-input audit-trail-filter-wide-input",
465
+ placeholder: "Origin IP",
466
+ value: draftOriginIP,
467
+ onChange: (e) => setDraftOriginIP(e.target.value)
468
+ }
469
+ ) }),
470
+ /* @__PURE__ */ jsx("div", { className: "audit-trail-filter-actions", children: /* @__PURE__ */ jsx(
471
+ "button",
472
+ {
473
+ type: "button",
474
+ className: "audit-trail-button audit-trail-button-secondary",
475
+ disabled: !dirty,
476
+ onClick: handleApply,
477
+ children: "Add filters"
478
+ }
479
+ ) }),
480
+ /* @__PURE__ */ jsx(
481
+ MetadataFilterSection,
482
+ {
483
+ value,
484
+ onChange,
485
+ distinct,
486
+ onApplied
487
+ }
488
+ )
489
+ ] });
490
+ }
491
+ function MetadataFilterSection({
492
+ value,
493
+ onChange,
494
+ distinct,
495
+ onApplied
496
+ }) {
497
+ const [variant, setVariant] = useState("metadata");
498
+ const [mdKey, setMdKey] = useState("");
499
+ const [mdOp, setMdOp] = useState("eq");
500
+ const [mdValue, setMdValue] = useState("");
501
+ const [mdValue2, setMdValue2] = useState("");
502
+ const [cfField, setCfField] = useState("");
503
+ const [cbField, setCbField] = useState("");
504
+ const [cbValue, setCbValue] = useState("");
505
+ const [caField, setCaField] = useState("");
506
+ const [caValue, setCaValue] = useState("");
507
+ const compiled = compileMetaClause({
508
+ variant,
509
+ mdKey,
510
+ mdOp,
511
+ mdValue,
512
+ mdValue2,
513
+ cfField,
514
+ cbField,
515
+ cbValue,
516
+ caField,
517
+ caValue
518
+ });
519
+ const submittable = compiled.dsl !== null;
520
+ const showError = !submittable && compiled.error && /Both bounds/.test(compiled.error);
521
+ const handleAdd = () => {
522
+ if (!compiled.dsl) return;
523
+ onChange({
524
+ ...value,
525
+ q: compiled.dsl,
526
+ nlpQ: void 0,
527
+ nlpExplanation: void 0,
528
+ nlpUnsupported: void 0
529
+ });
530
+ onApplied?.();
531
+ };
532
+ return /* @__PURE__ */ jsxs("div", { className: "audit-trail-add-filter-inline", children: [
533
+ /* @__PURE__ */ jsx("h3", { className: "audit-trail-add-filter-heading", children: "Metadata and Changed Fields" }),
534
+ /* @__PURE__ */ jsxs("label", { className: "audit-trail-add-filter-row", children: [
535
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-add-filter-label", children: "Filter type" }),
536
+ /* @__PURE__ */ jsxs(
537
+ "select",
538
+ {
539
+ value: variant,
540
+ onChange: (e) => setVariant(e.target.value),
541
+ children: [
542
+ /* @__PURE__ */ jsx("option", { value: "metadata", children: "Metadata key/value" }),
543
+ /* @__PURE__ */ jsx("option", { value: "change-field", children: "Change: a field changed" }),
544
+ /* @__PURE__ */ jsx("option", { value: "change-before", children: "Change: previous value (before)" }),
545
+ /* @__PURE__ */ jsx("option", { value: "change-after", children: "Change: new value (after)" })
546
+ ]
547
+ }
548
+ )
549
+ ] }),
550
+ variant === "metadata" && /* @__PURE__ */ jsxs(Fragment, { children: [
551
+ /* @__PURE__ */ jsxs("label", { className: "audit-trail-add-filter-row", children: [
552
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-add-filter-label", children: "Key" }),
553
+ /* @__PURE__ */ jsx(
554
+ "input",
555
+ {
556
+ type: "text",
557
+ list: "audit-trail-mdk-key-list",
558
+ placeholder: "e.g. environment",
559
+ value: mdKey,
560
+ onChange: (e) => setMdKey(e.target.value)
561
+ }
562
+ ),
563
+ /* @__PURE__ */ jsx("datalist", { id: "audit-trail-mdk-key-list", children: distinct.metadataKeys.map((k) => /* @__PURE__ */ jsxs("option", { value: k.key, children: [
564
+ k.observed_type,
565
+ " \xB7 ",
566
+ k.event_count
567
+ ] }, k.key)) })
568
+ ] }),
569
+ /* @__PURE__ */ jsxs("label", { className: "audit-trail-add-filter-row", children: [
570
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-add-filter-label", children: "Operator" }),
571
+ /* @__PURE__ */ jsxs(
572
+ "select",
573
+ {
574
+ value: mdOp,
575
+ onChange: (e) => setMdOp(e.target.value),
576
+ children: [
577
+ /* @__PURE__ */ jsx("option", { value: "eq", children: "equals" }),
578
+ /* @__PURE__ */ jsx("option", { value: "neq", children: "not equal" }),
579
+ /* @__PURE__ */ jsx("option", { value: "contains", children: "contains" }),
580
+ /* @__PURE__ */ jsx("option", { value: "gt", children: "greater than" }),
581
+ /* @__PURE__ */ jsx("option", { value: "gte", children: "greater or equal" }),
582
+ /* @__PURE__ */ jsx("option", { value: "lt", children: "less than" }),
583
+ /* @__PURE__ */ jsx("option", { value: "lte", children: "less or equal" }),
584
+ /* @__PURE__ */ jsx("option", { value: "between", children: "between" })
585
+ ]
586
+ }
587
+ )
588
+ ] }),
589
+ /* @__PURE__ */ jsxs("label", { className: "audit-trail-add-filter-row", children: [
590
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-add-filter-label", children: "Value" }),
591
+ /* @__PURE__ */ jsx(
592
+ "input",
593
+ {
594
+ type: "text",
595
+ placeholder: "e.g. prod",
596
+ value: mdValue,
597
+ onChange: (e) => setMdValue(e.target.value)
598
+ }
599
+ )
600
+ ] }),
601
+ mdOp === "between" && /* @__PURE__ */ jsxs("label", { className: "audit-trail-add-filter-row", children: [
602
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-add-filter-label", children: "Upper bound" }),
603
+ /* @__PURE__ */ jsx(
604
+ "input",
605
+ {
606
+ type: "text",
607
+ placeholder: "e.g. 1000",
608
+ value: mdValue2,
609
+ onChange: (e) => setMdValue2(e.target.value)
610
+ }
611
+ )
612
+ ] })
613
+ ] }),
614
+ variant === "change-field" && /* @__PURE__ */ jsxs("label", { className: "audit-trail-add-filter-row", children: [
615
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-add-filter-label", children: "Field" }),
616
+ /* @__PURE__ */ jsx(
617
+ "input",
618
+ {
619
+ type: "text",
620
+ list: "audit-trail-cf-field-list",
621
+ placeholder: "e.g. email",
622
+ value: cfField,
623
+ onChange: (e) => setCfField(e.target.value)
624
+ }
625
+ ),
626
+ /* @__PURE__ */ jsx("datalist", { id: "audit-trail-cf-field-list", children: distinct.changeFields.map((c) => /* @__PURE__ */ jsxs("option", { value: c.field, children: [
627
+ c.event_count,
628
+ " events"
629
+ ] }, c.field)) })
630
+ ] }),
631
+ variant === "change-before" && /* @__PURE__ */ jsxs(Fragment, { children: [
632
+ /* @__PURE__ */ jsxs("label", { className: "audit-trail-add-filter-row", children: [
633
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-add-filter-label", children: "Field" }),
634
+ /* @__PURE__ */ jsx(
635
+ "input",
636
+ {
637
+ type: "text",
638
+ list: "audit-trail-cf-field-list",
639
+ placeholder: "e.g. role",
640
+ value: cbField,
641
+ onChange: (e) => setCbField(e.target.value)
642
+ }
643
+ )
644
+ ] }),
645
+ /* @__PURE__ */ jsxs("label", { className: "audit-trail-add-filter-row", children: [
646
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-add-filter-label", children: "Previous value" }),
647
+ /* @__PURE__ */ jsx(
648
+ "input",
649
+ {
650
+ type: "text",
651
+ placeholder: "e.g. user",
652
+ value: cbValue,
653
+ onChange: (e) => setCbValue(e.target.value)
654
+ }
655
+ )
656
+ ] })
657
+ ] }),
658
+ variant === "change-after" && /* @__PURE__ */ jsxs(Fragment, { children: [
659
+ /* @__PURE__ */ jsxs("label", { className: "audit-trail-add-filter-row", children: [
660
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-add-filter-label", children: "Field" }),
661
+ /* @__PURE__ */ jsx(
662
+ "input",
663
+ {
664
+ type: "text",
665
+ list: "audit-trail-cf-field-list",
666
+ placeholder: "e.g. role",
667
+ value: caField,
668
+ onChange: (e) => setCaField(e.target.value)
669
+ }
670
+ )
671
+ ] }),
672
+ /* @__PURE__ */ jsxs("label", { className: "audit-trail-add-filter-row", children: [
673
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-add-filter-label", children: "New value" }),
674
+ /* @__PURE__ */ jsx(
675
+ "input",
676
+ {
677
+ type: "text",
678
+ placeholder: "e.g. admin",
679
+ value: caValue,
680
+ onChange: (e) => setCaValue(e.target.value)
681
+ }
682
+ )
683
+ ] })
684
+ ] }),
685
+ showError && /* @__PURE__ */ jsx("p", { className: "audit-trail-add-filter-error", children: compiled.error }),
686
+ /* @__PURE__ */ jsx("div", { className: "audit-trail-filter-actions audit-trail-add-filter-actions", children: /* @__PURE__ */ jsx(
687
+ "button",
688
+ {
689
+ type: "button",
690
+ className: "audit-trail-button audit-trail-button-secondary",
691
+ disabled: !submittable,
692
+ onClick: handleAdd,
693
+ children: "Add filter"
694
+ }
695
+ ) })
696
+ ] });
697
+ }
698
+ function compileMetaClause(i) {
699
+ switch (i.variant) {
700
+ case "metadata": {
701
+ const key = i.mdKey.trim();
702
+ if (!key) return { dsl: null, error: "Key is required" };
703
+ const path = "metadata." + quoteKeyIfNeeded(key);
704
+ const v1 = i.mdValue;
705
+ const v2 = i.mdValue2;
706
+ switch (i.mdOp) {
707
+ case "eq":
708
+ return { dsl: `${path}:${quoteIfNeeded(v1)}` };
709
+ case "neq":
710
+ return { dsl: `${path}:!${quoteIfNeeded(v1)}` };
711
+ case "contains":
712
+ return { dsl: `${path}:~${quoteIfNeeded(v1)}` };
713
+ case "gt":
714
+ return { dsl: `${path}:>${quoteIfNeeded(v1)}` };
715
+ case "gte":
716
+ return { dsl: `${path}:>=${quoteIfNeeded(v1)}` };
717
+ case "lt":
718
+ return { dsl: `${path}:<${quoteIfNeeded(v1)}` };
719
+ case "lte":
720
+ return { dsl: `${path}:<=${quoteIfNeeded(v1)}` };
721
+ case "between":
722
+ if (v1 === "" || v2 === "") {
723
+ return { dsl: null, error: "Both bounds required for between" };
724
+ }
725
+ return {
726
+ dsl: `${path}:[${quoteIfNeeded(v1)} TO ${quoteIfNeeded(v2)}]`
727
+ };
728
+ }
729
+ return { dsl: null, error: "Unknown operator" };
730
+ }
731
+ case "change-field": {
732
+ const f = i.cfField.trim();
733
+ if (!f) return { dsl: null, error: "Field is required" };
734
+ return { dsl: `change.field:${quoteIfNeeded(f)}` };
735
+ }
736
+ case "change-before": {
737
+ const f = i.cbField.trim();
738
+ if (!f) return { dsl: null, error: "Field is required" };
739
+ return {
740
+ dsl: `change.${quoteKeyIfNeeded(f)}.before:${quoteIfNeeded(i.cbValue)}`
741
+ };
742
+ }
743
+ case "change-after": {
744
+ const f = i.caField.trim();
745
+ if (!f) return { dsl: null, error: "Field is required" };
746
+ return {
747
+ dsl: `change.${quoteKeyIfNeeded(f)}.after:${quoteIfNeeded(i.caValue)}`
748
+ };
749
+ }
750
+ }
751
+ }
752
+ function quoteIfNeeded(raw) {
753
+ if (raw === "") return '""';
754
+ if (/[\s"\(\)\[\]]/.test(raw)) {
755
+ return '"' + raw.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
756
+ }
757
+ return raw;
758
+ }
759
+ function quoteKeyIfNeeded(raw) {
760
+ if (/^[A-Za-z_][A-Za-z0-9_-]*$/.test(raw)) return raw;
761
+ return '"' + raw + '"';
762
+ }
763
+ function QueryTabPanel({ value, onChange }) {
764
+ const [draft, setDraft] = useState(value.q ?? "");
765
+ useEffect(() => {
766
+ setDraft(value.q ?? "");
767
+ }, [value.q]);
768
+ const handleSubmit = (e) => {
769
+ e.preventDefault();
770
+ const next = draft.trim() || void 0;
771
+ if (next === value.q) return;
772
+ onChange({
773
+ ...value,
774
+ q: next,
775
+ nlpQ: void 0,
776
+ nlpExplanation: void 0,
777
+ nlpUnsupported: void 0
778
+ });
779
+ };
780
+ return /* @__PURE__ */ jsxs("form", { className: "audit-trail-query-form", onSubmit: handleSubmit, children: [
781
+ /* @__PURE__ */ jsxs("label", { className: "audit-trail-query-label", htmlFor: "audit-trail-query-input", children: [
782
+ "Query",
783
+ " ",
784
+ /* @__PURE__ */ jsx("a", { href: "https://everscribe.io/docs", target: "_blank", rel: "noopener noreferrer", children: "(see docs)" })
785
+ ] }),
786
+ /* @__PURE__ */ jsx(
787
+ "input",
788
+ {
789
+ id: "audit-trail-query-input",
790
+ type: "text",
791
+ className: "audit-trail-query-input",
792
+ placeholder: "e.g. action:user.login AND result.status:!ok",
793
+ value: draft,
794
+ onChange: (e) => setDraft(e.target.value),
795
+ "aria-label": "Advanced query (DSL)",
796
+ spellCheck: false
797
+ }
798
+ ),
799
+ /* @__PURE__ */ jsx("div", { className: "audit-trail-filter-actions", children: /* @__PURE__ */ jsx(
800
+ "button",
801
+ {
802
+ type: "submit",
803
+ className: "audit-trail-button audit-trail-button-secondary",
804
+ disabled: (draft.trim() || "") === (value.q ?? ""),
805
+ children: "Search"
806
+ }
807
+ ) })
808
+ ] });
809
+ }
810
+ function hasAnyColumnFilter(v) {
811
+ return !!(v.action || v.actor || v.actorType || v.tenantId || v.targetType || v.targetId || v.resultStatus || v.originIP);
812
+ }
813
+ function countActiveColumnFilters(v) {
814
+ let n = 0;
815
+ if (v.action) n++;
816
+ if (v.actor) n++;
817
+ if (v.actorType) n++;
818
+ if (v.tenantId) n++;
819
+ if (v.targetType) n++;
820
+ if (v.targetId) n++;
821
+ if (v.resultStatus) n++;
822
+ if (v.originIP) n++;
823
+ return n;
824
+ }
825
+ function parseQClauses(q) {
826
+ const trimmed = q.trim();
827
+ if (!trimmed) return [];
828
+ if (/[()]/.test(trimmed)) return [{ label: trimmed, raw: trimmed }];
829
+ return trimmed.split(/\s+AND\s+/i).map((s) => s.trim()).filter((s) => s.length > 0).map((s) => ({ label: s, raw: s }));
830
+ }
831
+ function removeClauseFromQ(q, raw) {
832
+ const remaining = parseQClauses(q).filter((c) => c.raw !== raw);
833
+ if (remaining.length === 0) return void 0;
834
+ return remaining.map((c) => c.raw).join(" AND ");
835
+ }
836
+ function ActiveFilterChips({ value, onChange }) {
837
+ const chips = [];
838
+ const colChip = (field, label, raw) => {
839
+ if (!raw) return;
840
+ chips.push({
841
+ label: `${label}: ${raw}`,
842
+ onRemove: () => onChange({ ...value, [field]: void 0 })
843
+ });
844
+ };
845
+ colChip("action", "Action", value.action);
846
+ colChip("actor", "Actor", value.actor);
847
+ colChip("actorType", "Actor type", value.actorType);
848
+ colChip("tenantId", "Tenant", value.tenantId);
849
+ colChip("targetType", "Target type", value.targetType);
850
+ colChip("targetId", "Target ID", value.targetId);
851
+ colChip("resultStatus", "Result", value.resultStatus);
852
+ colChip("originIP", "Origin IP", value.originIP);
853
+ if (value.q) {
854
+ for (const clause of parseQClauses(value.q)) {
855
+ chips.push({
856
+ label: clause.label,
857
+ onRemove: () => {
858
+ const nextQ = removeClauseFromQ(value.q, clause.raw);
859
+ onChange({
860
+ ...value,
861
+ q: nextQ,
862
+ // If this was the last DSL clause and the q came from an
863
+ // NLP translation, clear the echo fields too - there's
864
+ // nothing left to attribute to "you asked".
865
+ nlpQ: nextQ ? value.nlpQ : void 0,
866
+ nlpExplanation: nextQ ? value.nlpExplanation : void 0,
867
+ nlpUnsupported: nextQ ? value.nlpUnsupported : void 0
868
+ });
869
+ }
870
+ });
871
+ }
872
+ }
873
+ if (chips.length === 0) return null;
874
+ const clearAll = () => onChange({
875
+ range: value.range,
876
+ since: value.since,
877
+ before: value.before
878
+ });
879
+ return /* @__PURE__ */ jsxs("div", { className: "audit-trail-filter-chips-row", children: [
880
+ /* @__PURE__ */ jsx("div", { className: "audit-trail-filter-chips", children: chips.map((c, i) => /* @__PURE__ */ jsxs(
881
+ "button",
882
+ {
883
+ type: "button",
884
+ className: "audit-trail-filter-chip",
885
+ onClick: c.onRemove,
886
+ title: "Remove this filter",
887
+ "aria-label": `Remove filter: ${c.label}`,
888
+ children: [
889
+ /* @__PURE__ */ jsx("span", { children: c.label }),
890
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "audit-trail-filter-chip-x", children: "\xD7" })
891
+ ]
892
+ },
893
+ i
894
+ )) }),
895
+ /* @__PURE__ */ jsx(
896
+ "button",
897
+ {
898
+ type: "button",
899
+ className: "audit-trail-button audit-trail-button-text",
900
+ onClick: clearAll,
901
+ children: "Clear all"
902
+ }
903
+ )
904
+ ] });
905
+ }
906
+ function ColumnPicker({ available, visible, onToggle }) {
907
+ const visibleCount = available.reduce((n, c) => visible.has(c) ? n + 1 : n, 0);
908
+ return /* @__PURE__ */ jsxs("details", { className: "audit-trail-picker", children: [
909
+ /* @__PURE__ */ jsxs("summary", { className: "audit-trail-picker-summary", children: [
910
+ "Columns (",
911
+ visibleCount,
912
+ "/",
913
+ available.length,
914
+ ")"
915
+ ] }),
916
+ /* @__PURE__ */ jsx("div", { className: "audit-trail-picker-content", children: available.map((col) => /* @__PURE__ */ jsxs("label", { className: "audit-trail-picker-item", children: [
917
+ /* @__PURE__ */ jsx(
918
+ "input",
919
+ {
920
+ type: "checkbox",
921
+ checked: visible.has(col),
922
+ onChange: () => onToggle(col)
923
+ }
924
+ ),
925
+ /* @__PURE__ */ jsx("span", { children: COLUMN_LABELS[col] ?? col })
926
+ ] }, col)) })
927
+ ] });
928
+ }
929
+ function EventDetail({ event, onClose, theme = "light" }) {
930
+ const [mounted, setMounted] = useState(false);
931
+ const [tab, setTab] = useState("raw");
932
+ const [copied, setCopied] = useState(false);
933
+ const closeBtnRef = useRef(null);
934
+ const showDiff = hasParseableDiff(event.change);
935
+ const diff = useMemo(
936
+ () => showDiff ? renderDiff(event.change) : { lines: [] },
937
+ [event.change, showDiff]
938
+ );
939
+ const rawJson = useMemo(() => JSON.stringify(event, null, 2), [event]);
940
+ const highlightedJson = useMemo(() => highlightJSON(rawJson), [rawJson]);
941
+ const metadataRows = useMemo(() => buildMetadataRows(event.metadata), [event.metadata]);
942
+ const showMetadata = metadataRows.length > 0;
943
+ useEffect(() => setMounted(true), []);
944
+ useEffect(() => {
945
+ if (!mounted) return;
946
+ closeBtnRef.current?.focus();
947
+ const onKey = (e) => {
948
+ if (e.key === "Escape") onClose();
949
+ };
950
+ document.addEventListener("keydown", onKey);
951
+ return () => document.removeEventListener("keydown", onKey);
952
+ }, [mounted, onClose]);
953
+ const handleCopy = async () => {
954
+ try {
955
+ await navigator.clipboard.writeText(rawJson);
956
+ setCopied(true);
957
+ setTimeout(() => setCopied(false), 1500);
958
+ } catch {
959
+ }
960
+ };
961
+ if (!mounted) return null;
962
+ return createPortal(
963
+ /* @__PURE__ */ jsx("div", { className: `audit-trail-portal audit-trail-theme-${theme}`, children: /* @__PURE__ */ jsx("div", { className: "audit-trail-inspect-backdrop", onMouseDown: onClose, children: /* @__PURE__ */ jsxs(
964
+ "div",
965
+ {
966
+ className: "audit-trail-inspect-modal",
967
+ role: "dialog",
968
+ "aria-modal": "true",
969
+ "aria-labelledby": "audit-trail-inspect-title",
970
+ onMouseDown: (e) => e.stopPropagation(),
971
+ children: [
972
+ /* @__PURE__ */ jsx(
973
+ "button",
974
+ {
975
+ type: "button",
976
+ ref: closeBtnRef,
977
+ className: "audit-trail-inspect-close",
978
+ "aria-label": "Close",
979
+ onClick: onClose,
980
+ children: "\xD7"
981
+ }
982
+ ),
983
+ /* @__PURE__ */ jsx("h2", { id: "audit-trail-inspect-title", className: "audit-trail-inspect-title", children: "Inspect Event" }),
984
+ /* @__PURE__ */ jsxs("p", { className: "audit-trail-inspect-subtitle", children: [
985
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-inspect-subtitle-line", children: /* @__PURE__ */ jsx("code", { children: event.action || "-" }) }),
986
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-inspect-subtitle-line", children: formatHeaderTimestamp(event.occurred_at) }),
987
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-inspect-subtitle-line", children: /* @__PURE__ */ jsx("code", { children: event.id }) })
988
+ ] }),
989
+ (showDiff || showMetadata) && /* @__PURE__ */ jsxs("div", { className: "audit-trail-inspect-tabs", role: "tablist", "aria-label": "View", children: [
990
+ /* @__PURE__ */ jsx(
991
+ "button",
992
+ {
993
+ type: "button",
994
+ role: "tab",
995
+ "aria-selected": tab === "raw",
996
+ className: inspectTabClass(tab === "raw"),
997
+ onClick: () => setTab("raw"),
998
+ children: "Raw"
999
+ }
1000
+ ),
1001
+ showDiff && /* @__PURE__ */ jsx(
1002
+ "button",
1003
+ {
1004
+ type: "button",
1005
+ role: "tab",
1006
+ "aria-selected": tab === "diff",
1007
+ className: inspectTabClass(tab === "diff"),
1008
+ onClick: () => setTab("diff"),
1009
+ children: "Diff"
1010
+ }
1011
+ ),
1012
+ showMetadata && /* @__PURE__ */ jsx(
1013
+ "button",
1014
+ {
1015
+ type: "button",
1016
+ role: "tab",
1017
+ "aria-selected": tab === "metadata",
1018
+ className: inspectTabClass(tab === "metadata"),
1019
+ onClick: () => setTab("metadata"),
1020
+ children: "Metadata"
1021
+ }
1022
+ )
1023
+ ] }),
1024
+ tab === "raw" && /* @__PURE__ */ jsx("div", { className: "audit-trail-inspect-panel", children: /* @__PURE__ */ jsxs("div", { className: "audit-trail-code-block-wrap", children: [
1025
+ /* @__PURE__ */ jsx("pre", { className: "audit-trail-code-block", children: /* @__PURE__ */ jsx("code", { dangerouslySetInnerHTML: { __html: highlightedJson } }) }),
1026
+ /* @__PURE__ */ jsx(
1027
+ "button",
1028
+ {
1029
+ type: "button",
1030
+ className: "audit-trail-copy-button",
1031
+ onClick: handleCopy,
1032
+ "aria-label": copied ? "Copied" : "Copy to clipboard",
1033
+ title: copied ? "Copied" : "Copy to clipboard",
1034
+ children: copied ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx(CopyIcon, {})
1035
+ }
1036
+ )
1037
+ ] }) }),
1038
+ tab === "diff" && showDiff && /* @__PURE__ */ jsx("div", { className: "audit-trail-inspect-panel", children: /* @__PURE__ */ jsx(DiffTable, { lines: diff.lines }) }),
1039
+ tab === "metadata" && showMetadata && /* @__PURE__ */ jsx("div", { className: "audit-trail-inspect-panel", children: /* @__PURE__ */ jsx(MetadataTable, { rows: metadataRows }) })
1040
+ ]
1041
+ }
1042
+ ) }) }),
1043
+ document.body
1044
+ );
1045
+ }
1046
+ function DiffTable({ lines }) {
1047
+ return /* @__PURE__ */ jsx("div", { className: "audit-trail-diff-wrap", children: /* @__PURE__ */ jsxs("table", { className: "audit-trail-diff-table", children: [
1048
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1049
+ /* @__PURE__ */ jsx("th", { children: "Before" }),
1050
+ /* @__PURE__ */ jsx("th", { children: "After" })
1051
+ ] }) }),
1052
+ /* @__PURE__ */ jsx("tbody", { children: lines.map((line, idx) => /* @__PURE__ */ jsxs("tr", { children: [
1053
+ /* @__PURE__ */ jsx(
1054
+ "td",
1055
+ {
1056
+ className: `audit-trail-diff-cell audit-trail-diff-before-${line.beforeKind || "blank"}`,
1057
+ children: /* @__PURE__ */ jsx("pre", { children: line.before })
1058
+ }
1059
+ ),
1060
+ /* @__PURE__ */ jsx(
1061
+ "td",
1062
+ {
1063
+ className: `audit-trail-diff-cell audit-trail-diff-after-${line.afterKind || "blank"}`,
1064
+ children: /* @__PURE__ */ jsx("pre", { children: line.after })
1065
+ }
1066
+ )
1067
+ ] }, idx)) })
1068
+ ] }) });
1069
+ }
1070
+ function CopyIcon() {
1071
+ return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
1072
+ /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
1073
+ /* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
1074
+ ] });
1075
+ }
1076
+ function CheckIcon() {
1077
+ return /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) });
1078
+ }
1079
+ function formatHeaderTimestamp(rfc3339) {
1080
+ if (!rfc3339) return "-";
1081
+ const d = new Date(rfc3339);
1082
+ if (Number.isNaN(d.getTime())) return rfc3339;
1083
+ const month = MONTHS[d.getUTCMonth()] ?? "";
1084
+ const day = d.getUTCDate();
1085
+ const year = d.getUTCFullYear();
1086
+ const hh = String(d.getUTCHours()).padStart(2, "0");
1087
+ const mm = String(d.getUTCMinutes()).padStart(2, "0");
1088
+ const ss = String(d.getUTCSeconds()).padStart(2, "0");
1089
+ const ms = String(d.getUTCMilliseconds()).padStart(3, "0");
1090
+ return `${month} ${day}, ${year} ${hh}:${mm}:${ss}.${ms} UTC`;
1091
+ }
1092
+ var MONTHS = [
1093
+ "Jan",
1094
+ "Feb",
1095
+ "Mar",
1096
+ "Apr",
1097
+ "May",
1098
+ "Jun",
1099
+ "Jul",
1100
+ "Aug",
1101
+ "Sep",
1102
+ "Oct",
1103
+ "Nov",
1104
+ "Dec"
1105
+ ];
1106
+ function inspectTabClass(active) {
1107
+ return active ? "audit-trail-inspect-tab audit-trail-inspect-tab-active" : "audit-trail-inspect-tab";
1108
+ }
1109
+ function highlightJSON(json) {
1110
+ const safe = escapeHTML(json);
1111
+ return safe.replace(
1112
+ /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
1113
+ (match) => {
1114
+ let cls = "audit-trail-json-num";
1115
+ if (match.startsWith('"')) {
1116
+ cls = /:$/.test(match) ? "audit-trail-json-key" : "audit-trail-json-str";
1117
+ } else if (/true|false/.test(match)) {
1118
+ cls = "audit-trail-json-bool";
1119
+ } else if (/null/.test(match)) {
1120
+ cls = "audit-trail-json-null";
1121
+ }
1122
+ return `<span class="${cls}">${match}</span>`;
1123
+ }
1124
+ );
1125
+ }
1126
+ function escapeHTML(s) {
1127
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1128
+ }
1129
+ function buildMetadataRows(metadata) {
1130
+ if (!metadata) return [];
1131
+ const keys = Object.keys(metadata);
1132
+ if (keys.length === 0) return [];
1133
+ keys.sort();
1134
+ return keys.map((k) => {
1135
+ const raw = metadata[k];
1136
+ return { key: k, value: renderMetadataValue(raw), type: classifyValue(raw) };
1137
+ });
1138
+ }
1139
+ function renderMetadataValue(v) {
1140
+ if (v === null || v === void 0) return "";
1141
+ if (typeof v === "string") return v;
1142
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
1143
+ try {
1144
+ return JSON.stringify(v);
1145
+ } catch {
1146
+ return String(v);
1147
+ }
1148
+ }
1149
+ function classifyValue(v) {
1150
+ if (v === null) return "null";
1151
+ if (Array.isArray(v)) return "array";
1152
+ if (typeof v === "object") return "object";
1153
+ return typeof v;
1154
+ }
1155
+ function MetadataTable({ rows }) {
1156
+ return /* @__PURE__ */ jsxs("table", { className: "audit-trail-metadata-kv-table", children: [
1157
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1158
+ /* @__PURE__ */ jsx("th", { className: "audit-trail-md-col-key", children: "Key" }),
1159
+ /* @__PURE__ */ jsx("th", { className: "audit-trail-md-col-type", children: "Type" }),
1160
+ /* @__PURE__ */ jsx("th", { className: "audit-trail-md-col-value", children: "Value" })
1161
+ ] }) }),
1162
+ /* @__PURE__ */ jsx("tbody", { children: rows.map((r) => /* @__PURE__ */ jsxs("tr", { children: [
1163
+ /* @__PURE__ */ jsx("td", { className: "audit-trail-md-col-key", children: /* @__PURE__ */ jsx("code", { children: r.key }) }),
1164
+ /* @__PURE__ */ jsx("td", { className: "audit-trail-md-col-type", children: /* @__PURE__ */ jsx("span", { className: "audit-trail-muted", children: r.type }) }),
1165
+ /* @__PURE__ */ jsx("td", { className: "audit-trail-md-col-value", children: /* @__PURE__ */ jsx("code", { children: r.value }) })
1166
+ ] }, r.key)) })
1167
+ ] });
1168
+ }
1169
+ function useRelativeTimeTick(intervalMs = RELATIVE_TIME_TICK_MS) {
1170
+ const [now, setNow] = useState(() => Date.now());
1171
+ useEffect(() => {
1172
+ const id = setInterval(() => setNow(Date.now()), intervalMs);
1173
+ return () => clearInterval(id);
1174
+ }, [intervalMs]);
1175
+ return now;
1176
+ }
1177
+ function EventRow({ event, columns, now, onClick }) {
1178
+ const interactive = Boolean(onClick);
1179
+ return /* @__PURE__ */ jsx(
1180
+ "tr",
1181
+ {
1182
+ className: "audit-trail-row",
1183
+ onClick: interactive ? () => onClick(event) : void 0,
1184
+ tabIndex: interactive ? 0 : void 0,
1185
+ onKeyDown: interactive ? (e) => {
1186
+ if (e.key === "Enter" || e.key === " ") {
1187
+ e.preventDefault();
1188
+ onClick(event);
1189
+ }
1190
+ } : void 0,
1191
+ children: columns.map((col) => /* @__PURE__ */ jsx(
1192
+ "td",
1193
+ {
1194
+ className: `audit-trail-cell audit-trail-cell-${col}`,
1195
+ "data-label": COLUMN_LABELS[col] ?? col,
1196
+ children: renderCell(col, event, now)
1197
+ },
1198
+ col
1199
+ ))
1200
+ }
1201
+ );
1202
+ }
1203
+ function renderCell(column, event, now) {
1204
+ switch (column) {
1205
+ case "result":
1206
+ return renderResult(event.result);
1207
+ case "origin":
1208
+ return renderOrigin(event.origin);
1209
+ case "change":
1210
+ case "metadata":
1211
+ return renderPresence(getField(event, column));
1212
+ default: {
1213
+ const value = getField(event, column);
1214
+ if (value == null) return null;
1215
+ if (column === "occurred_at" && typeof value === "string") {
1216
+ return /* @__PURE__ */ jsx("time", { dateTime: value, title: value, children: formatTimeCell(value, now) });
1217
+ }
1218
+ if (typeof value === "string") return value;
1219
+ if (typeof value === "object") return summarizeObject(value);
1220
+ return String(value);
1221
+ }
1222
+ }
1223
+ }
1224
+ function getField(event, key) {
1225
+ return event[key];
1226
+ }
1227
+ function renderResult(value) {
1228
+ if (value == null || typeof value !== "object") return "-";
1229
+ const obj = value;
1230
+ const status = typeof obj.status === "string" ? obj.status : "";
1231
+ if (!status) return "-";
1232
+ return /* @__PURE__ */ jsx("span", { className: `audit-trail-status audit-trail-status-${cssToken(status)}`, children: status });
1233
+ }
1234
+ function renderOrigin(value) {
1235
+ if (value == null || typeof value !== "object") return "-";
1236
+ const obj = value;
1237
+ for (const key of ["ip", "hostname", "host"]) {
1238
+ const v = obj[key];
1239
+ if (typeof v === "string" && v.length > 0) return v;
1240
+ }
1241
+ return summarizeObject(obj);
1242
+ }
1243
+ function renderPresence(value) {
1244
+ if (value == null) return "-";
1245
+ if (typeof value === "object") {
1246
+ const keys = Object.keys(value);
1247
+ if (keys.length === 0) return "-";
1248
+ return /* @__PURE__ */ jsx("span", { className: "audit-trail-cell-presence", children: "View" });
1249
+ }
1250
+ if (typeof value === "string" && value.length > 0) {
1251
+ return /* @__PURE__ */ jsx("span", { className: "audit-trail-cell-presence", children: "View" });
1252
+ }
1253
+ return "-";
1254
+ }
1255
+ function summarizeObject(value) {
1256
+ const obj = value;
1257
+ for (const key of ["name", "email", "id", "type"]) {
1258
+ const v = obj[key];
1259
+ if (typeof v === "string" && v.length > 0) return v;
1260
+ }
1261
+ const keys = Object.keys(obj);
1262
+ if (keys.length === 0) return "-";
1263
+ return `{${keys.length} ${keys.length === 1 ? "field" : "fields"}}`;
1264
+ }
1265
+ function cssToken(s) {
1266
+ return s.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
1267
+ }
1268
+ function EventTable({ events, visibleColumns, onRowClick }) {
1269
+ const now = useRelativeTimeTick();
1270
+ return /* @__PURE__ */ jsx("div", { className: "audit-trail-table-wrap", children: /* @__PURE__ */ jsxs("table", { className: "audit-trail-table", children: [
1271
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: visibleColumns.map((col) => /* @__PURE__ */ jsx("th", { scope: "col", className: `audit-trail-th audit-trail-th-${col}`, children: COLUMN_LABELS[col] ?? col }, col)) }) }),
1272
+ /* @__PURE__ */ jsx("tbody", { children: events.map((event) => /* @__PURE__ */ jsx(
1273
+ EventRow,
1274
+ {
1275
+ event,
1276
+ columns: visibleColumns,
1277
+ now,
1278
+ onClick: onRowClick
1279
+ },
1280
+ event.id
1281
+ )) })
1282
+ ] }) });
1283
+ }
1284
+ function ExportModal({ open, onClose, onDownload }) {
1285
+ const [format, setFormat] = useState(null);
1286
+ const [status, setStatus] = useState("idle");
1287
+ const [errMsg, setErrMsg] = useState(null);
1288
+ useEffect(() => {
1289
+ if (!open) {
1290
+ setFormat(null);
1291
+ setStatus("idle");
1292
+ setErrMsg(null);
1293
+ }
1294
+ }, [open]);
1295
+ useEffect(() => {
1296
+ if (!open) return;
1297
+ const onKey = (e) => {
1298
+ if (e.key === "Escape" && status !== "downloading") onClose();
1299
+ };
1300
+ document.addEventListener("keydown", onKey);
1301
+ return () => document.removeEventListener("keydown", onKey);
1302
+ }, [open, status, onClose]);
1303
+ if (!open) return null;
1304
+ const handleDownload = async () => {
1305
+ if (!format) return;
1306
+ setStatus("downloading");
1307
+ setErrMsg(null);
1308
+ try {
1309
+ await onDownload(format);
1310
+ onClose();
1311
+ } catch (err) {
1312
+ setStatus("error");
1313
+ setErrMsg(err instanceof Error ? err.message : "Export failed.");
1314
+ }
1315
+ };
1316
+ const onBackdropClick = (e) => {
1317
+ if (e.target !== e.currentTarget) return;
1318
+ if (status === "downloading") return;
1319
+ onClose();
1320
+ };
1321
+ return /* @__PURE__ */ jsx(
1322
+ "div",
1323
+ {
1324
+ className: "audit-trail-export-backdrop",
1325
+ role: "presentation",
1326
+ onClick: onBackdropClick,
1327
+ children: /* @__PURE__ */ jsxs(
1328
+ "div",
1329
+ {
1330
+ className: "audit-trail-export-modal",
1331
+ role: "dialog",
1332
+ "aria-modal": "true",
1333
+ "aria-labelledby": "audit-trail-export-title",
1334
+ children: [
1335
+ /* @__PURE__ */ jsx("h2", { id: "audit-trail-export-title", className: "audit-trail-export-title", children: "Export events" }),
1336
+ /* @__PURE__ */ jsx("p", { className: "audit-trail-export-blurb", children: "The current filters and time range are applied. Capped at 100,000 rows - narrow the filters or time window if you hit it." }),
1337
+ /* @__PURE__ */ jsxs("div", { className: "audit-trail-export-options", children: [
1338
+ /* @__PURE__ */ jsx(
1339
+ FormatCard,
1340
+ {
1341
+ format: "csv",
1342
+ selected: format === "csv",
1343
+ onSelect: () => setFormat("csv"),
1344
+ title: "CSV",
1345
+ body: "Spreadsheet-friendly. Common fields are columns; nested fields (origin, metadata, change) are JSON in single columns."
1346
+ }
1347
+ ),
1348
+ /* @__PURE__ */ jsx(
1349
+ FormatCard,
1350
+ {
1351
+ format: "json",
1352
+ selected: format === "json",
1353
+ onSelect: () => setFormat("json"),
1354
+ title: "JSON",
1355
+ body: "Full event objects preserving nested structure. Same shape the API returns."
1356
+ }
1357
+ )
1358
+ ] }),
1359
+ status === "error" && errMsg && /* @__PURE__ */ jsx("div", { className: "audit-trail-export-error", role: "alert", children: errMsg }),
1360
+ /* @__PURE__ */ jsxs("div", { className: "audit-trail-export-actions", children: [
1361
+ /* @__PURE__ */ jsx(
1362
+ "button",
1363
+ {
1364
+ type: "button",
1365
+ className: "audit-trail-button",
1366
+ onClick: handleDownload,
1367
+ disabled: !format || status === "downloading",
1368
+ children: status === "downloading" ? "Downloading\u2026" : "Download"
1369
+ }
1370
+ ),
1371
+ /* @__PURE__ */ jsx(
1372
+ "button",
1373
+ {
1374
+ type: "button",
1375
+ className: "audit-trail-button audit-trail-button-secondary",
1376
+ onClick: onClose,
1377
+ disabled: status === "downloading",
1378
+ children: "Cancel"
1379
+ }
1380
+ )
1381
+ ] })
1382
+ ]
1383
+ }
1384
+ )
1385
+ }
1386
+ );
1387
+ }
1388
+ function FormatCard({ format, selected, onSelect, title, body }) {
1389
+ return /* @__PURE__ */ jsxs(
1390
+ "button",
1391
+ {
1392
+ type: "button",
1393
+ role: "radio",
1394
+ "aria-checked": selected,
1395
+ className: selected ? "audit-trail-export-card audit-trail-export-card-selected" : "audit-trail-export-card",
1396
+ onClick: onSelect,
1397
+ "data-format": format,
1398
+ children: [
1399
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-export-card-title", children: title }),
1400
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-export-card-body", children: body })
1401
+ ]
1402
+ }
1403
+ );
1404
+ }
1405
+ function FiltersToggle({ open, onOpenChange, activeCount }) {
1406
+ return /* @__PURE__ */ jsxs(
1407
+ "button",
1408
+ {
1409
+ type: "button",
1410
+ className: "audit-trail-filter-toggle",
1411
+ "aria-expanded": open,
1412
+ onClick: () => onOpenChange(!open),
1413
+ children: [
1414
+ "Search",
1415
+ activeCount > 0 && /* @__PURE__ */ jsx("span", { className: "audit-trail-filter-toggle-badge", children: activeCount }),
1416
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-filter-toggle-caret", "aria-hidden": "true", children: open ? "\u25B4" : "\u25BE" })
1417
+ ]
1418
+ }
1419
+ );
1420
+ }
1421
+ function LiveIndicator({ active }) {
1422
+ if (!active) return null;
1423
+ return /* @__PURE__ */ jsxs("span", { className: "audit-trail-live", "aria-label": "Live updates", title: "Live updates", children: [
1424
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-live-dot", "aria-hidden": "true" }),
1425
+ /* @__PURE__ */ jsx("span", { children: "Live" })
1426
+ ] });
1427
+ }
1428
+ function useClaims(token) {
1429
+ return useMemo(() => parseClaims(token), [token]);
1430
+ }
1431
+ var NOOP_UNSUB = () => {
1432
+ };
1433
+ function useDistinctValues(opts) {
1434
+ const [store, setStore] = useState(null);
1435
+ useEffect(() => {
1436
+ if (!opts.token) {
1437
+ setStore(null);
1438
+ return;
1439
+ }
1440
+ const s = createDistinctValuesStore({
1441
+ apiBase: opts.apiBase,
1442
+ token: opts.token,
1443
+ tokenEndpoint: opts.tokenEndpoint,
1444
+ onTokenExpired: opts.onTokenExpired
1445
+ });
1446
+ setStore(s);
1447
+ return () => {
1448
+ s.dispose();
1449
+ };
1450
+ }, [opts.token, opts.apiBase]);
1451
+ const subscribe = useMemo(
1452
+ () => (listener) => store?.subscribe(listener) ?? NOOP_UNSUB,
1453
+ [store]
1454
+ );
1455
+ const getSnapshot = () => store?.getSnapshot() ?? EMPTY_DISTINCT_VALUES;
1456
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
1457
+ }
1458
+ var EMPTY_STATE = {
1459
+ events: [],
1460
+ nextCursor: null,
1461
+ status: "loading",
1462
+ error: null
1463
+ };
1464
+ var NOOP_UNSUB2 = () => {
1465
+ };
1466
+ function useEvents(opts) {
1467
+ const [store, setStore] = useState(null);
1468
+ useEffect(() => {
1469
+ if (!opts.token) {
1470
+ setStore(null);
1471
+ return;
1472
+ }
1473
+ const s = createEventsStore({
1474
+ apiBase: opts.apiBase,
1475
+ token: opts.token,
1476
+ pageSize: opts.pageSize,
1477
+ pollInterval: opts.pollInterval,
1478
+ tokenEndpoint: opts.tokenEndpoint,
1479
+ onTokenExpired: opts.onTokenExpired,
1480
+ onError: opts.onError,
1481
+ since: opts.since,
1482
+ before: opts.before,
1483
+ action: opts.action,
1484
+ actor: opts.actor,
1485
+ actorType: opts.actorType,
1486
+ tenantId: opts.tenantId,
1487
+ targetType: opts.targetType,
1488
+ targetId: opts.targetId,
1489
+ resultStatus: opts.resultStatus,
1490
+ originIP: opts.originIP,
1491
+ q: opts.q
1492
+ });
1493
+ setStore(s);
1494
+ return () => {
1495
+ s.dispose();
1496
+ };
1497
+ }, [
1498
+ opts.token,
1499
+ opts.apiBase,
1500
+ opts.pageSize,
1501
+ opts.pollInterval,
1502
+ opts.since,
1503
+ opts.before,
1504
+ opts.action,
1505
+ opts.actor,
1506
+ opts.actorType,
1507
+ opts.tenantId,
1508
+ opts.targetType,
1509
+ opts.targetId,
1510
+ opts.resultStatus,
1511
+ opts.originIP,
1512
+ opts.q
1513
+ ]);
1514
+ const subscribe = useMemo(
1515
+ () => (listener) => store?.subscribe(listener) ?? NOOP_UNSUB2,
1516
+ [store]
1517
+ );
1518
+ const getSnapshot = () => store?.getSnapshot() ?? EMPTY_STATE;
1519
+ const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
1520
+ return {
1521
+ events: state.events,
1522
+ hasMore: state.nextCursor !== null,
1523
+ status: state.status,
1524
+ error: state.error,
1525
+ loadMore: () => store?.loadMore(),
1526
+ refresh: () => store?.refresh()
1527
+ };
1528
+ }
1529
+ var DEFAULT_API_BASE = "https://api.everscribe.io/v1/embed";
1530
+ var DEFAULT_PAGE_SIZE = 25;
1531
+ var DEFAULT_POLL_INTERVAL_MS = 5e3;
1532
+ var DEFAULT_VISIBLE_COLUMNS = [
1533
+ "occurred_at",
1534
+ "action",
1535
+ "actor",
1536
+ "target",
1537
+ "tenant_id",
1538
+ "result"
1539
+ ];
1540
+ function AuditTrail(props) {
1541
+ const [bootstrap, setBootstrap] = useState(
1542
+ () => props.token ? { phase: "ready", token: props.token } : { phase: "idle" }
1543
+ );
1544
+ const [retryCount, setRetryCount] = useState(0);
1545
+ useEffect(() => {
1546
+ if (props.token) setBootstrap({ phase: "ready", token: props.token });
1547
+ }, [props.token]);
1548
+ const propsRef = useRef(props);
1549
+ propsRef.current = props;
1550
+ useEffect(() => {
1551
+ if (propsRef.current.token) return;
1552
+ if (!propsRef.current.tokenEndpoint && !propsRef.current.onTokenExpired) {
1553
+ setBootstrap({ phase: "error", reason: "config" });
1554
+ return;
1555
+ }
1556
+ setBootstrap({ phase: "loading" });
1557
+ let cancelled = false;
1558
+ void (async () => {
1559
+ const t = await fetchTokenViaOpts({
1560
+ tokenEndpoint: propsRef.current.tokenEndpoint,
1561
+ onTokenExpired: propsRef.current.onTokenExpired
1562
+ });
1563
+ if (cancelled) return;
1564
+ if (!t) {
1565
+ setBootstrap({ phase: "error", reason: "fetch" });
1566
+ return;
1567
+ }
1568
+ setBootstrap({ phase: "ready", token: t });
1569
+ })();
1570
+ return () => {
1571
+ cancelled = true;
1572
+ };
1573
+ }, [props.token, retryCount]);
1574
+ const activeToken = bootstrap.phase === "ready" ? bootstrap.token : null;
1575
+ const claims = useClaims(activeToken);
1576
+ const [selected, setSelected] = useState(null);
1577
+ const [filters, setFilters] = useState(() => ({
1578
+ range: props.defaultTimeRange ?? "all"
1579
+ }));
1580
+ const [filtersOpen, setFiltersOpen] = useState(false);
1581
+ const [exportOpen, setExportOpen] = useState(false);
1582
+ const pollIntervalMs = props.pollInterval ?? DEFAULT_POLL_INTERVAL_MS;
1583
+ const availableColumns = useMemo(() => {
1584
+ if (claims?.columns && claims.columns.length > 0) return claims.columns;
1585
+ return ALL_COLUMNS;
1586
+ }, [claims]);
1587
+ const [visibleSet, setVisibleSet] = useState(() => {
1588
+ if (claims?.columns && claims.columns.length > 0) return new Set(claims.columns);
1589
+ return new Set(DEFAULT_VISIBLE_COLUMNS);
1590
+ });
1591
+ const restoredKeyRef = useRef(null);
1592
+ const claimsKey = claims?.sub ? `${claims.sub}:${claims.tenant_id || "_"}` : null;
1593
+ useEffect(() => {
1594
+ if (!claimsKey) return;
1595
+ if (restoredKeyRef.current === claimsKey) return;
1596
+ restoredKeyRef.current = claimsKey;
1597
+ try {
1598
+ const raw = globalThis.localStorage?.getItem(`audit-trail:cols:${claimsKey}`);
1599
+ if (raw) {
1600
+ const hidden = JSON.parse(raw);
1601
+ if (Array.isArray(hidden)) {
1602
+ setVisibleSet((prev) => {
1603
+ const next = new Set(prev);
1604
+ for (const c of hidden) {
1605
+ if (typeof c === "string") next.delete(c);
1606
+ }
1607
+ return next;
1608
+ });
1609
+ }
1610
+ }
1611
+ } catch {
1612
+ }
1613
+ try {
1614
+ const raw = globalThis.localStorage?.getItem(`audit-trail:filters:${claimsKey}`);
1615
+ if (raw) {
1616
+ const parsed = JSON.parse(raw);
1617
+ if (parsed && typeof parsed === "object") {
1618
+ const range = parsed.range;
1619
+ if (range === "24h" || range === "7d" || range === "30d" || range === "custom" || range === "all") {
1620
+ setFilters(parsed);
1621
+ }
1622
+ }
1623
+ }
1624
+ } catch {
1625
+ }
1626
+ }, [claimsKey]);
1627
+ useEffect(() => {
1628
+ if (!claimsKey || restoredKeyRef.current !== claimsKey) return;
1629
+ try {
1630
+ const hidden = availableColumns.filter((c) => !visibleSet.has(c));
1631
+ globalThis.localStorage?.setItem(`audit-trail:cols:${claimsKey}`, JSON.stringify(hidden));
1632
+ } catch {
1633
+ }
1634
+ }, [visibleSet, claimsKey, availableColumns]);
1635
+ useEffect(() => {
1636
+ if (!claimsKey || restoredKeyRef.current !== claimsKey) return;
1637
+ try {
1638
+ globalThis.localStorage?.setItem(`audit-trail:filters:${claimsKey}`, JSON.stringify(filters));
1639
+ } catch {
1640
+ }
1641
+ }, [filters, claimsKey]);
1642
+ const visibleColumns = useMemo(
1643
+ () => availableColumns.filter((c) => visibleSet.has(c)),
1644
+ [availableColumns, visibleSet]
1645
+ );
1646
+ const toggleColumn = (col) => {
1647
+ setVisibleSet((prev) => {
1648
+ const next = new Set(prev);
1649
+ if (next.has(col)) next.delete(col);
1650
+ else next.add(col);
1651
+ return next;
1652
+ });
1653
+ };
1654
+ const { since: filterSince, before: filterBefore } = useMemo(
1655
+ () => resolveTimeBounds(filters),
1656
+ [filters]
1657
+ );
1658
+ const livePollActive = pollIntervalMs > 0 && !filterBefore;
1659
+ const distinct = useDistinctValues({
1660
+ apiBase: props.apiBase ?? DEFAULT_API_BASE,
1661
+ token: activeToken,
1662
+ tokenEndpoint: props.tokenEndpoint,
1663
+ onTokenExpired: props.onTokenExpired
1664
+ });
1665
+ const { events, hasMore, status, error, loadMore, refresh } = useEvents({
1666
+ apiBase: props.apiBase ?? DEFAULT_API_BASE,
1667
+ token: activeToken,
1668
+ pageSize: props.pageSize ?? DEFAULT_PAGE_SIZE,
1669
+ pollInterval: pollIntervalMs,
1670
+ tokenEndpoint: props.tokenEndpoint,
1671
+ onTokenExpired: props.onTokenExpired,
1672
+ onError: props.onError,
1673
+ since: filterSince,
1674
+ before: filterBefore,
1675
+ action: filters.action,
1676
+ actor: filters.actor,
1677
+ actorType: filters.actorType,
1678
+ tenantId: filters.tenantId,
1679
+ targetType: filters.targetType,
1680
+ targetId: filters.targetId,
1681
+ resultStatus: filters.resultStatus,
1682
+ originIP: filters.originIP,
1683
+ q: filters.q
1684
+ });
1685
+ const handleExportDownload = useCallback(
1686
+ async (format) => {
1687
+ if (!activeToken) throw new Error("Not authenticated.");
1688
+ const apiBase = props.apiBase ?? DEFAULT_API_BASE;
1689
+ const params = {
1690
+ format,
1691
+ since: filterSince,
1692
+ before: filterBefore,
1693
+ action: filters.action,
1694
+ actor: filters.actor,
1695
+ actorType: filters.actorType,
1696
+ tenantId: filters.tenantId,
1697
+ targetType: filters.targetType,
1698
+ targetId: filters.targetId,
1699
+ resultStatus: filters.resultStatus,
1700
+ originIP: filters.originIP,
1701
+ q: filters.q
1702
+ };
1703
+ const run = (token) => exportEvents({ apiBase, token, params });
1704
+ let result;
1705
+ try {
1706
+ result = await run(activeToken);
1707
+ } catch (err) {
1708
+ if (err instanceof EmbedError && err.kind === "unauthorized") {
1709
+ const refreshed = await fetchTokenViaOpts({
1710
+ tokenEndpoint: props.tokenEndpoint,
1711
+ onTokenExpired: props.onTokenExpired
1712
+ });
1713
+ if (!refreshed) throw new Error("Authentication failed.");
1714
+ result = await run(refreshed);
1715
+ } else if (err instanceof EmbedError) {
1716
+ throw new Error(exportErrorMessage(err));
1717
+ } else {
1718
+ throw err;
1719
+ }
1720
+ }
1721
+ triggerBrowserDownload(result.blob, result.filename);
1722
+ },
1723
+ [
1724
+ activeToken,
1725
+ props.apiBase,
1726
+ props.tokenEndpoint,
1727
+ props.onTokenExpired,
1728
+ filterSince,
1729
+ filterBefore,
1730
+ filters.action,
1731
+ filters.actor,
1732
+ filters.actorType,
1733
+ filters.tenantId,
1734
+ filters.targetType,
1735
+ filters.targetId,
1736
+ filters.resultStatus,
1737
+ filters.originIP,
1738
+ filters.q
1739
+ ]
1740
+ );
1741
+ const rootClassName = [
1742
+ "audit-trail-root",
1743
+ `audit-trail-theme-${props.theme ?? "light"}`,
1744
+ props.className
1745
+ ].filter(Boolean).join(" ");
1746
+ if (bootstrap.phase === "error" && bootstrap.reason === "config") {
1747
+ return /* @__PURE__ */ jsx("div", { className: rootClassName, style: props.style, children: /* @__PURE__ */ jsxs("div", { className: "audit-trail-state audit-trail-state-error", children: [
1748
+ "Configure ",
1749
+ /* @__PURE__ */ jsx("code", { children: "token" }),
1750
+ ", ",
1751
+ /* @__PURE__ */ jsx("code", { children: "tokenEndpoint" }),
1752
+ ", or",
1753
+ " ",
1754
+ /* @__PURE__ */ jsx("code", { children: "onTokenExpired" }),
1755
+ "."
1756
+ ] }) });
1757
+ }
1758
+ if (bootstrap.phase === "error" && bootstrap.reason === "fetch") {
1759
+ return /* @__PURE__ */ jsx("div", { className: rootClassName, style: props.style, children: /* @__PURE__ */ jsxs("div", { className: "audit-trail-state audit-trail-state-error", children: [
1760
+ /* @__PURE__ */ jsx("span", { children: "Couldn\u2019t fetch token." }),
1761
+ /* @__PURE__ */ jsx(
1762
+ "button",
1763
+ {
1764
+ type: "button",
1765
+ className: "audit-trail-button",
1766
+ onClick: () => setRetryCount((c) => c + 1),
1767
+ children: "Retry"
1768
+ }
1769
+ )
1770
+ ] }) });
1771
+ }
1772
+ if (bootstrap.phase !== "ready") {
1773
+ return /* @__PURE__ */ jsx("div", { className: rootClassName, style: props.style, children: /* @__PURE__ */ jsx("div", { className: "audit-trail-state audit-trail-state-loading", children: "Loading\u2026" }) });
1774
+ }
1775
+ if (claims === null) {
1776
+ return /* @__PURE__ */ jsx("div", { className: rootClassName, style: props.style, children: /* @__PURE__ */ jsx("div", { className: "audit-trail-state audit-trail-state-error", children: "Invalid token." }) });
1777
+ }
1778
+ return /* @__PURE__ */ jsxs("div", { className: rootClassName, style: props.style, children: [
1779
+ /* @__PURE__ */ jsxs("div", { className: "audit-trail-toolbar", children: [
1780
+ /* @__PURE__ */ jsx(LiveIndicator, { active: livePollActive }),
1781
+ /* @__PURE__ */ jsx("span", { className: "audit-trail-toolbar-spacer" }),
1782
+ /* @__PURE__ */ jsx(
1783
+ FiltersToggle,
1784
+ {
1785
+ open: filtersOpen,
1786
+ onOpenChange: setFiltersOpen,
1787
+ activeCount: countActiveColumnFilters(filters) + (filters.q ? parseQClauses(filters.q).length : 0)
1788
+ }
1789
+ ),
1790
+ /* @__PURE__ */ jsx(
1791
+ "button",
1792
+ {
1793
+ type: "button",
1794
+ className: "audit-trail-button",
1795
+ onClick: () => setExportOpen(true),
1796
+ children: "Export"
1797
+ }
1798
+ ),
1799
+ /* @__PURE__ */ jsx(
1800
+ ColumnPicker,
1801
+ {
1802
+ available: availableColumns,
1803
+ visible: visibleSet,
1804
+ onToggle: toggleColumn
1805
+ }
1806
+ )
1807
+ ] }),
1808
+ /* @__PURE__ */ jsx(ActiveFilterChips, { value: filters, onChange: setFilters }),
1809
+ filtersOpen && /* @__PURE__ */ jsx(
1810
+ FiltersPanel,
1811
+ {
1812
+ value: filters,
1813
+ onChange: setFilters,
1814
+ distinct,
1815
+ claims,
1816
+ apiBase: props.apiBase ?? DEFAULT_API_BASE,
1817
+ token: activeToken,
1818
+ tokenEndpoint: props.tokenEndpoint,
1819
+ onTokenExpired: props.onTokenExpired,
1820
+ onApplied: () => setFiltersOpen(false)
1821
+ }
1822
+ ),
1823
+ status === "loading" && events.length === 0 && /* @__PURE__ */ jsx("div", { className: "audit-trail-state audit-trail-state-loading", children: "Loading\u2026" }),
1824
+ status === "expired" && /* @__PURE__ */ jsx("div", { className: "audit-trail-state audit-trail-state-error", children: "Session expired." }),
1825
+ status === "error" && error && /* @__PURE__ */ jsxs("div", { className: "audit-trail-state audit-trail-state-error", children: [
1826
+ /* @__PURE__ */ jsx("span", { children: errorMessage(error) }),
1827
+ /* @__PURE__ */ jsx("button", { type: "button", className: "audit-trail-button", onClick: refresh, children: "Retry" })
1828
+ ] }),
1829
+ status === "ok" && events.length === 0 && /* @__PURE__ */ jsx("div", { className: "audit-trail-state audit-trail-state-empty", children: "No events match." }),
1830
+ events.length > 0 && visibleColumns.length === 0 && /* @__PURE__ */ jsx("div", { className: "audit-trail-state audit-trail-state-empty", children: "No columns selected." }),
1831
+ events.length > 0 && visibleColumns.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
1832
+ /* @__PURE__ */ jsx(
1833
+ EventTable,
1834
+ {
1835
+ events,
1836
+ visibleColumns,
1837
+ onRowClick: setSelected
1838
+ }
1839
+ ),
1840
+ hasMore && /* @__PURE__ */ jsx("button", { type: "button", className: "audit-trail-button audit-trail-load-more", onClick: loadMore, children: "Load more" })
1841
+ ] }),
1842
+ /* @__PURE__ */ jsx(
1843
+ ExportModal,
1844
+ {
1845
+ open: exportOpen,
1846
+ onClose: () => setExportOpen(false),
1847
+ onDownload: handleExportDownload
1848
+ }
1849
+ ),
1850
+ selected && /* @__PURE__ */ jsx(
1851
+ EventDetail,
1852
+ {
1853
+ event: selected,
1854
+ onClose: () => setSelected(null),
1855
+ theme: props.theme ?? "light"
1856
+ },
1857
+ selected.id
1858
+ )
1859
+ ] });
1860
+ }
1861
+ function resolveTimeBounds(filters) {
1862
+ switch (filters.range) {
1863
+ case "24h":
1864
+ return { since: relativeIso(24 * 60 * 60 * 1e3) };
1865
+ case "7d":
1866
+ return { since: relativeIso(7 * 24 * 60 * 60 * 1e3) };
1867
+ case "30d":
1868
+ return { since: relativeIso(30 * 24 * 60 * 60 * 1e3) };
1869
+ case "custom":
1870
+ return {
1871
+ since: localToIso(filters.since),
1872
+ before: localToIso(filters.before)
1873
+ };
1874
+ case "all":
1875
+ default:
1876
+ return {};
1877
+ }
1878
+ }
1879
+ function relativeIso(ms) {
1880
+ return new Date(Date.now() - ms).toISOString();
1881
+ }
1882
+ function localToIso(value) {
1883
+ if (!value) return void 0;
1884
+ const d = new Date(value);
1885
+ if (Number.isNaN(d.getTime())) return void 0;
1886
+ return d.toISOString();
1887
+ }
1888
+ function triggerBrowserDownload(blob, filename) {
1889
+ const url = URL.createObjectURL(blob);
1890
+ const a = document.createElement("a");
1891
+ a.href = url;
1892
+ a.download = filename;
1893
+ document.body.appendChild(a);
1894
+ a.click();
1895
+ document.body.removeChild(a);
1896
+ setTimeout(() => URL.revokeObjectURL(url), 0);
1897
+ }
1898
+ function exportErrorMessage(err) {
1899
+ switch (err.kind) {
1900
+ case "rate_limited":
1901
+ return "Too many requests. Try again in a moment.";
1902
+ case "bad_request":
1903
+ return err.message || "Bad request.";
1904
+ case "server":
1905
+ return "Server error. Please try again.";
1906
+ case "network":
1907
+ return "Network error. Check your connection.";
1908
+ case "not_found":
1909
+ return "Not found.";
1910
+ case "unauthorized":
1911
+ return "Authentication failed.";
1912
+ }
1913
+ }
1914
+ function errorMessage(err) {
1915
+ switch (err.kind) {
1916
+ case "unauthorized":
1917
+ return "Authentication failed.";
1918
+ case "not_found":
1919
+ return "Not found.";
1920
+ case "rate_limited":
1921
+ return "Too many requests. Please slow down.";
1922
+ case "bad_request":
1923
+ return err.message || "Bad request.";
1924
+ case "server":
1925
+ return "Server error. Please try again.";
1926
+ case "network":
1927
+ return "Network error. Check your connection.";
1928
+ }
1929
+ }
1930
+
1931
+ export { AuditTrail };
1932
+ //# sourceMappingURL=index.js.map
1933
+ //# sourceMappingURL=index.js.map