@everscribe/components-element 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,2253 @@
1
+ import { fetchTokenViaOpts, parseClaims, createEventsStore, createDistinctValuesStore, RELATIVE_TIME_TICK_MS, formatTimeCell, EmbedError, ALL_COLUMNS, generateNLPFilters, COLUMN_LABELS, exportEvents, hasParseableDiff, renderDiff } from '@everscribe/components-core';
2
+
3
+ // src/AuditTrailElement.ts
4
+
5
+ // src/dom.ts
6
+ function h(tag, attrs, ...children) {
7
+ const el = document.createElement(tag);
8
+ if (attrs) {
9
+ for (const [k, v] of Object.entries(attrs)) {
10
+ if (v == null || v === false) continue;
11
+ if (k === "class") el.className = String(v);
12
+ else if (k === "html") el.innerHTML = String(v);
13
+ else el.setAttribute(k, String(v));
14
+ }
15
+ }
16
+ for (const c of children.flat()) {
17
+ if (c == null || c === false) continue;
18
+ el.appendChild(typeof c === "object" ? c : document.createTextNode(String(c)));
19
+ }
20
+ return el;
21
+ }
22
+ var COPY_ICON_SVG = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
23
+ var CHECK_ICON_SVG = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"></polyline></svg>';
24
+ function openEventDetail(opts) {
25
+ const { event, theme, onClose } = opts;
26
+ let tab = "raw";
27
+ let copied = false;
28
+ const showDiff = hasParseableDiff(event.change);
29
+ const diff = showDiff ? renderDiff(event.change) : { lines: [] };
30
+ const rawJson = JSON.stringify(event, null, 2);
31
+ const highlightedJson = highlightJSON(rawJson);
32
+ const metadataRows = buildMetadataRows(event.metadata);
33
+ const showMetadata = metadataRows.length > 0;
34
+ const portal = h("div", { class: `audit-trail-portal audit-trail-theme-${theme}` });
35
+ const closeBtn = h(
36
+ "button",
37
+ {
38
+ type: "button",
39
+ class: "audit-trail-inspect-close",
40
+ "aria-label": "Close"
41
+ },
42
+ "\xD7"
43
+ );
44
+ const panelContainer = h("div");
45
+ const renderPanel = () => {
46
+ panelContainer.replaceChildren(buildPanel());
47
+ };
48
+ const buildPanel = () => {
49
+ if (tab === "diff" && showDiff) {
50
+ return h(
51
+ "div",
52
+ { class: "audit-trail-inspect-panel" },
53
+ renderDiffTable(diff.lines)
54
+ );
55
+ }
56
+ if (tab === "metadata" && showMetadata) {
57
+ return h(
58
+ "div",
59
+ { class: "audit-trail-inspect-panel" },
60
+ renderMetadataTable(metadataRows)
61
+ );
62
+ }
63
+ const codeBlock = h(
64
+ "pre",
65
+ { class: "audit-trail-code-block" },
66
+ h("code", { html: highlightedJson })
67
+ );
68
+ const copyBtn = h(
69
+ "button",
70
+ {
71
+ type: "button",
72
+ class: "audit-trail-copy-button",
73
+ "aria-label": copied ? "Copied" : "Copy to clipboard",
74
+ title: copied ? "Copied" : "Copy to clipboard",
75
+ html: copied ? CHECK_ICON_SVG : COPY_ICON_SVG
76
+ }
77
+ );
78
+ copyBtn.addEventListener("click", () => {
79
+ void (async () => {
80
+ try {
81
+ await navigator.clipboard.writeText(rawJson);
82
+ copied = true;
83
+ renderPanel();
84
+ setTimeout(() => {
85
+ copied = false;
86
+ if (!disposed && tab === "raw") renderPanel();
87
+ }, 1500);
88
+ } catch {
89
+ }
90
+ })();
91
+ });
92
+ return h(
93
+ "div",
94
+ { class: "audit-trail-inspect-panel" },
95
+ h(
96
+ "div",
97
+ { class: "audit-trail-code-block-wrap" },
98
+ codeBlock,
99
+ copyBtn
100
+ )
101
+ );
102
+ };
103
+ const subtitle = h(
104
+ "p",
105
+ { class: "audit-trail-inspect-subtitle" },
106
+ h(
107
+ "span",
108
+ { class: "audit-trail-inspect-subtitle-line" },
109
+ h("code", null, event.action || "-")
110
+ ),
111
+ h(
112
+ "span",
113
+ { class: "audit-trail-inspect-subtitle-line" },
114
+ formatHeaderTimestamp(event.occurred_at)
115
+ ),
116
+ h(
117
+ "span",
118
+ { class: "audit-trail-inspect-subtitle-line" },
119
+ h("code", null, event.id)
120
+ )
121
+ );
122
+ const tabs = showDiff || showMetadata ? renderTabs(tab, showDiff, showMetadata, (next) => {
123
+ tab = next;
124
+ setSelectedTab(tabsEl, next);
125
+ renderPanel();
126
+ }) : null;
127
+ const tabsEl = tabs;
128
+ const modalChildren = [
129
+ closeBtn,
130
+ h(
131
+ "h2",
132
+ { id: "audit-trail-inspect-title", class: "audit-trail-inspect-title" },
133
+ "Inspect Event"
134
+ ),
135
+ subtitle
136
+ ];
137
+ if (tabs) modalChildren.push(tabs);
138
+ modalChildren.push(panelContainer);
139
+ const modal = h(
140
+ "div",
141
+ {
142
+ class: "audit-trail-inspect-modal",
143
+ role: "dialog",
144
+ "aria-modal": "true",
145
+ "aria-labelledby": "audit-trail-inspect-title"
146
+ },
147
+ ...modalChildren
148
+ );
149
+ modal.addEventListener("mousedown", (e) => e.stopPropagation());
150
+ const backdrop = h("div", { class: "audit-trail-inspect-backdrop" }, modal);
151
+ portal.appendChild(backdrop);
152
+ let disposed = false;
153
+ const dispose = () => {
154
+ if (disposed) return;
155
+ disposed = true;
156
+ document.removeEventListener("keydown", onKey);
157
+ portal.remove();
158
+ };
159
+ const userClose = () => {
160
+ if (disposed) return;
161
+ dispose();
162
+ onClose();
163
+ };
164
+ const onKey = (e) => {
165
+ if (e.key === "Escape") userClose();
166
+ };
167
+ closeBtn.addEventListener("click", userClose);
168
+ backdrop.addEventListener("mousedown", userClose);
169
+ document.addEventListener("keydown", onKey);
170
+ document.body.appendChild(portal);
171
+ renderPanel();
172
+ queueMicrotask(() => {
173
+ if (!disposed) closeBtn.focus();
174
+ });
175
+ return dispose;
176
+ }
177
+ function renderTabs(initial, showDiff, showMetadata, onSelect) {
178
+ const make = (key, label) => {
179
+ const btn = h(
180
+ "button",
181
+ {
182
+ type: "button",
183
+ role: "tab",
184
+ class: initial === key ? "audit-trail-inspect-tab audit-trail-inspect-tab-active" : "audit-trail-inspect-tab",
185
+ "aria-selected": initial === key ? "true" : "false",
186
+ "data-tab": key
187
+ },
188
+ label
189
+ );
190
+ btn.addEventListener("click", () => onSelect(key));
191
+ return btn;
192
+ };
193
+ const tabs = [make("raw", "Raw")];
194
+ if (showDiff) tabs.push(make("diff", "Diff"));
195
+ if (showMetadata) tabs.push(make("metadata", "Metadata"));
196
+ return h(
197
+ "div",
198
+ { class: "audit-trail-inspect-tabs", role: "tablist", "aria-label": "View" },
199
+ ...tabs
200
+ );
201
+ }
202
+ function setSelectedTab(tabsEl, tab) {
203
+ if (!tabsEl) return;
204
+ for (const child of Array.from(tabsEl.children)) {
205
+ if (!(child instanceof HTMLElement)) continue;
206
+ const isMatch = child.dataset.tab === tab;
207
+ child.className = isMatch ? "audit-trail-inspect-tab audit-trail-inspect-tab-active" : "audit-trail-inspect-tab";
208
+ child.setAttribute("aria-selected", isMatch ? "true" : "false");
209
+ }
210
+ }
211
+ function renderDiffTable(lines) {
212
+ const rows = lines.map(
213
+ (line) => h(
214
+ "tr",
215
+ null,
216
+ h(
217
+ "td",
218
+ {
219
+ class: `audit-trail-diff-cell audit-trail-diff-before-${line.beforeKind || "blank"}`
220
+ },
221
+ h("pre", null, line.before)
222
+ ),
223
+ h(
224
+ "td",
225
+ {
226
+ class: `audit-trail-diff-cell audit-trail-diff-after-${line.afterKind || "blank"}`
227
+ },
228
+ h("pre", null, line.after)
229
+ )
230
+ )
231
+ );
232
+ return h(
233
+ "div",
234
+ { class: "audit-trail-diff-wrap" },
235
+ h(
236
+ "table",
237
+ { class: "audit-trail-diff-table" },
238
+ h(
239
+ "thead",
240
+ null,
241
+ h("tr", null, h("th", null, "Before"), h("th", null, "After"))
242
+ ),
243
+ h("tbody", null, ...rows)
244
+ )
245
+ );
246
+ }
247
+ function formatHeaderTimestamp(rfc3339) {
248
+ if (!rfc3339) return "-";
249
+ const d = new Date(rfc3339);
250
+ if (Number.isNaN(d.getTime())) return rfc3339;
251
+ const month = MONTHS[d.getUTCMonth()] ?? "";
252
+ const day = d.getUTCDate();
253
+ const year = d.getUTCFullYear();
254
+ const hh = String(d.getUTCHours()).padStart(2, "0");
255
+ const mm = String(d.getUTCMinutes()).padStart(2, "0");
256
+ const ss = String(d.getUTCSeconds()).padStart(2, "0");
257
+ const ms = String(d.getUTCMilliseconds()).padStart(3, "0");
258
+ return `${month} ${day}, ${year} ${hh}:${mm}:${ss}.${ms} UTC`;
259
+ }
260
+ var MONTHS = [
261
+ "Jan",
262
+ "Feb",
263
+ "Mar",
264
+ "Apr",
265
+ "May",
266
+ "Jun",
267
+ "Jul",
268
+ "Aug",
269
+ "Sep",
270
+ "Oct",
271
+ "Nov",
272
+ "Dec"
273
+ ];
274
+ function highlightJSON(json) {
275
+ const safe = escapeHTML(json);
276
+ return safe.replace(
277
+ /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
278
+ (match) => {
279
+ let cls = "audit-trail-json-num";
280
+ if (match.startsWith('"')) {
281
+ cls = /:$/.test(match) ? "audit-trail-json-key" : "audit-trail-json-str";
282
+ } else if (/true|false/.test(match)) {
283
+ cls = "audit-trail-json-bool";
284
+ } else if (/null/.test(match)) {
285
+ cls = "audit-trail-json-null";
286
+ }
287
+ return `<span class="${cls}">${match}</span>`;
288
+ }
289
+ );
290
+ }
291
+ function escapeHTML(s) {
292
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
293
+ }
294
+ function buildMetadataRows(metadata) {
295
+ if (!metadata) return [];
296
+ const keys = Object.keys(metadata);
297
+ if (keys.length === 0) return [];
298
+ keys.sort();
299
+ return keys.map((k) => {
300
+ const raw = metadata[k];
301
+ return { key: k, value: renderMetadataValue(raw), type: classifyValue(raw) };
302
+ });
303
+ }
304
+ function renderMetadataValue(v) {
305
+ if (v === null || v === void 0) return "";
306
+ if (typeof v === "string") return v;
307
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
308
+ try {
309
+ return JSON.stringify(v);
310
+ } catch {
311
+ return String(v);
312
+ }
313
+ }
314
+ function classifyValue(v) {
315
+ if (v === null) return "null";
316
+ if (Array.isArray(v)) return "array";
317
+ if (typeof v === "object") return "object";
318
+ return typeof v;
319
+ }
320
+ function renderMetadataTable(rows) {
321
+ return h(
322
+ "table",
323
+ { class: "audit-trail-metadata-kv-table" },
324
+ h(
325
+ "thead",
326
+ null,
327
+ h(
328
+ "tr",
329
+ null,
330
+ h("th", { class: "audit-trail-md-col-key" }, "Key"),
331
+ h("th", { class: "audit-trail-md-col-type" }, "Type"),
332
+ h("th", { class: "audit-trail-md-col-value" }, "Value")
333
+ )
334
+ ),
335
+ h(
336
+ "tbody",
337
+ null,
338
+ ...rows.map(
339
+ (r) => h(
340
+ "tr",
341
+ null,
342
+ h("td", { class: "audit-trail-md-col-key" }, h("code", null, r.key)),
343
+ h(
344
+ "td",
345
+ { class: "audit-trail-md-col-type" },
346
+ h("span", { class: "audit-trail-muted" }, r.type)
347
+ ),
348
+ h("td", { class: "audit-trail-md-col-value" }, h("code", null, r.value))
349
+ )
350
+ )
351
+ )
352
+ );
353
+ }
354
+
355
+ // src/exportModal.ts
356
+ function openExportModal(opts) {
357
+ const { theme, onDownload, onClose } = opts;
358
+ let format = null;
359
+ let status = "idle";
360
+ let errMsg = null;
361
+ const csvCard = renderFormatCard(
362
+ "csv",
363
+ "CSV",
364
+ "Spreadsheet-friendly. Common fields are columns; nested fields (origin, metadata, change) are JSON in single columns.",
365
+ () => selectFormat("csv")
366
+ );
367
+ const jsonCard = renderFormatCard(
368
+ "json",
369
+ "JSON",
370
+ "Full event objects preserving nested structure. Same shape the API returns.",
371
+ () => selectFormat("json")
372
+ );
373
+ const errorSlot = h("div", { class: "audit-trail-export-error-slot" });
374
+ const downloadBtn = h(
375
+ "button",
376
+ { type: "button", class: "audit-trail-button" },
377
+ "Download"
378
+ );
379
+ downloadBtn.disabled = true;
380
+ const cancelBtn = h(
381
+ "button",
382
+ {
383
+ type: "button",
384
+ class: "audit-trail-button audit-trail-button-secondary"
385
+ },
386
+ "Cancel"
387
+ );
388
+ const modal = h(
389
+ "div",
390
+ {
391
+ class: "audit-trail-export-modal",
392
+ role: "dialog",
393
+ "aria-modal": "true",
394
+ "aria-labelledby": "audit-trail-export-title"
395
+ },
396
+ h(
397
+ "h2",
398
+ { id: "audit-trail-export-title", class: "audit-trail-export-title" },
399
+ "Export events"
400
+ ),
401
+ h(
402
+ "p",
403
+ { class: "audit-trail-export-blurb" },
404
+ "The current filters and time range are applied. Capped at 100,000 rows - narrow the filters or time window if you hit it."
405
+ ),
406
+ h("div", { class: "audit-trail-export-options" }, csvCard, jsonCard),
407
+ errorSlot,
408
+ h("div", { class: "audit-trail-export-actions" }, downloadBtn, cancelBtn)
409
+ );
410
+ const backdrop = h(
411
+ "div",
412
+ { class: "audit-trail-export-backdrop", role: "presentation" },
413
+ modal
414
+ );
415
+ const portal = h(
416
+ "div",
417
+ { class: `audit-trail-portal audit-trail-theme-${theme}` },
418
+ backdrop
419
+ );
420
+ const selectFormat = (fmt) => {
421
+ format = fmt;
422
+ setSelected(csvCard, fmt === "csv");
423
+ setSelected(jsonCard, fmt === "json");
424
+ refreshButtons();
425
+ };
426
+ const refreshButtons = () => {
427
+ downloadBtn.disabled = !format || status === "downloading";
428
+ downloadBtn.textContent = status === "downloading" ? "Downloading\u2026" : "Download";
429
+ cancelBtn.disabled = status === "downloading";
430
+ if (status === "error" && errMsg) {
431
+ errorSlot.replaceChildren(
432
+ h(
433
+ "div",
434
+ { class: "audit-trail-export-error", role: "alert" },
435
+ errMsg
436
+ )
437
+ );
438
+ } else {
439
+ errorSlot.replaceChildren();
440
+ }
441
+ };
442
+ let disposed = false;
443
+ const dispose = () => {
444
+ if (disposed) return;
445
+ disposed = true;
446
+ document.removeEventListener("keydown", onKey);
447
+ portal.remove();
448
+ };
449
+ const userClose = () => {
450
+ if (disposed) return;
451
+ if (status === "downloading") return;
452
+ dispose();
453
+ onClose();
454
+ };
455
+ const onKey = (e) => {
456
+ if (e.key === "Escape") userClose();
457
+ };
458
+ cancelBtn.addEventListener("click", userClose);
459
+ backdrop.addEventListener("click", (e) => {
460
+ if (e.target !== backdrop) return;
461
+ userClose();
462
+ });
463
+ document.addEventListener("keydown", onKey);
464
+ downloadBtn.addEventListener("click", () => {
465
+ if (!format || status === "downloading") return;
466
+ status = "downloading";
467
+ errMsg = null;
468
+ refreshButtons();
469
+ void (async () => {
470
+ try {
471
+ await onDownload(format);
472
+ if (disposed) return;
473
+ dispose();
474
+ } catch (err) {
475
+ if (disposed) return;
476
+ status = "error";
477
+ errMsg = err instanceof Error ? err.message : "Export failed.";
478
+ refreshButtons();
479
+ }
480
+ })();
481
+ });
482
+ document.body.appendChild(portal);
483
+ return dispose;
484
+ }
485
+ function renderFormatCard(format, title, body, onSelect) {
486
+ const btn = h(
487
+ "button",
488
+ {
489
+ type: "button",
490
+ role: "radio",
491
+ "aria-checked": "false",
492
+ class: "audit-trail-export-card",
493
+ "data-format": format
494
+ },
495
+ h("span", { class: "audit-trail-export-card-title" }, title),
496
+ h("span", { class: "audit-trail-export-card-body" }, body)
497
+ );
498
+ btn.addEventListener("click", onSelect);
499
+ return btn;
500
+ }
501
+ function setSelected(card, selected) {
502
+ card.className = selected ? "audit-trail-export-card audit-trail-export-card-selected" : "audit-trail-export-card";
503
+ card.setAttribute("aria-checked", selected ? "true" : "false");
504
+ }
505
+
506
+ // src/filters.ts
507
+ var TIME_PRESETS = [
508
+ { key: "24h", label: "24h" },
509
+ { key: "7d", label: "7d" },
510
+ { key: "30d", label: "30d" },
511
+ { key: "custom", label: "Custom" },
512
+ { key: "all", label: "All" }
513
+ ];
514
+ function pickInitialTab(v, claims) {
515
+ if (v.nlpQ && claims?.allow_nlp) return "ai";
516
+ if (v.q && claims?.allow_dsl_input) return "query";
517
+ if (hasAnyColumnFilter(v)) return "filters";
518
+ if (claims?.allow_nlp) return "ai";
519
+ return "filters";
520
+ }
521
+ function renderFiltersPanel(opts) {
522
+ const { claims, activeTab } = opts;
523
+ const allowNLP = !!claims?.allow_nlp;
524
+ const allowDSL = !!claims?.allow_dsl_input;
525
+ const tabBar = renderTabBar({ allowNLP, allowDSL, activeTab, onTabChange: opts.onTabChange });
526
+ let body;
527
+ if (activeTab === "ai" && allowNLP) {
528
+ body = renderAITabPanel(opts);
529
+ } else if (activeTab === "query" && allowDSL) {
530
+ body = renderQueryTabPanel(opts);
531
+ } else {
532
+ body = renderFiltersTabPanel(opts);
533
+ }
534
+ return h("div", { class: "audit-trail-filter-panel" }, tabBar, body);
535
+ }
536
+ function renderTabBar(opts) {
537
+ const tabs = [];
538
+ const make = (key, label) => {
539
+ const active = opts.activeTab === key;
540
+ const btn = h(
541
+ "button",
542
+ {
543
+ type: "button",
544
+ role: "tab",
545
+ "aria-selected": active ? "true" : "false",
546
+ class: active ? "audit-trail-filter-mode audit-trail-filter-mode-selected" : "audit-trail-filter-mode"
547
+ },
548
+ label
549
+ );
550
+ btn.addEventListener("click", () => opts.onTabChange(key));
551
+ return btn;
552
+ };
553
+ if (opts.allowNLP) tabs.push(make("ai", "Prompt"));
554
+ tabs.push(make("filters", "Filters"));
555
+ if (opts.allowDSL) tabs.push(make("query", "Query"));
556
+ return h(
557
+ "div",
558
+ { class: "audit-trail-filter-modes", role: "tablist", "aria-label": "Filter mode" },
559
+ ...tabs
560
+ );
561
+ }
562
+ function renderAITabPanel(opts) {
563
+ const { value, nlpState, onNLPSubmit } = opts;
564
+ const input = h("input", {
565
+ type: "text",
566
+ id: "audit-trail-nlp-input",
567
+ class: "audit-trail-nlp-input",
568
+ placeholder: "e.g. failed logins last 24 hours",
569
+ maxlength: 500,
570
+ "aria-label": "Natural-language filter query",
571
+ value: value.nlpQ ?? ""
572
+ });
573
+ input.value = value.nlpQ ?? "";
574
+ if (nlpState.phase === "loading") input.disabled = true;
575
+ const submit = () => {
576
+ const q = input.value.trim();
577
+ if (!q) return;
578
+ onNLPSubmit(q);
579
+ };
580
+ const label = h(
581
+ "label",
582
+ { class: "audit-trail-nlp-label", for: "audit-trail-nlp-input" },
583
+ "Ask in plain English",
584
+ h(
585
+ "span",
586
+ {
587
+ class: "audit-trail-badge audit-trail-badge-info",
588
+ title: "Beta"
589
+ },
590
+ "Beta"
591
+ )
592
+ );
593
+ const children = [label, input];
594
+ if (value.q && value.nlpQ) {
595
+ children.push(
596
+ h(
597
+ "div",
598
+ { class: "audit-trail-nlp-banner" },
599
+ h(
600
+ "p",
601
+ null,
602
+ h("strong", null, "Translated to:"),
603
+ " ",
604
+ h("code", { class: "audit-trail-nlp-translated" }, value.q)
605
+ )
606
+ )
607
+ );
608
+ }
609
+ if (nlpState.phase === "error") {
610
+ children.push(
611
+ h("div", { class: "audit-trail-error" }, nlpErrorMessage(nlpState.reason))
612
+ );
613
+ }
614
+ if (value.nlpUnsupported && value.nlpUnsupported.length > 0) {
615
+ children.push(renderUnsupportedList(value.nlpUnsupported));
616
+ }
617
+ const btn = h(
618
+ "button",
619
+ {
620
+ type: "button",
621
+ class: "audit-trail-button audit-trail-button-secondary"
622
+ }
623
+ );
624
+ if (nlpState.phase === "loading") {
625
+ btn.disabled = true;
626
+ btn.appendChild(h("span", { class: "audit-trail-button-spinner" }));
627
+ btn.appendChild(document.createTextNode(" Translating\u2026"));
628
+ } else {
629
+ btn.textContent = "Search";
630
+ }
631
+ const updateBtn = () => {
632
+ if (nlpState.phase === "loading") return;
633
+ btn.disabled = input.value.trim() === "";
634
+ };
635
+ input.addEventListener("input", updateBtn);
636
+ updateBtn();
637
+ btn.addEventListener("click", submit);
638
+ input.addEventListener("keydown", (e) => {
639
+ if (e.key === "Enter") {
640
+ e.preventDefault();
641
+ submit();
642
+ }
643
+ });
644
+ children.push(h("div", { class: "audit-trail-filter-actions" }, btn));
645
+ return h("form", { class: "audit-trail-nlp-form" }, ...children);
646
+ }
647
+ function nlpErrorMessage(reason) {
648
+ switch (reason) {
649
+ case "not_configured":
650
+ return "Natural-language filtering isn't configured on this server.";
651
+ case "not_allowed":
652
+ return "This embed token does not permit natural-language queries.";
653
+ case "rate_limited":
654
+ return "You've hit the per-hour limit. Try again in a few minutes.";
655
+ case "provider_busy":
656
+ return "The natural-language service is busy right now. Try again in a moment.";
657
+ case "bad_request":
658
+ return "That query couldn't be processed. Try rephrasing.";
659
+ default:
660
+ return "Couldn't process that query. Try simpler terms.";
661
+ }
662
+ }
663
+ function renderUnsupportedList(items) {
664
+ const ul = h(
665
+ "ul",
666
+ null,
667
+ ...items.map((u) => h("li", null, `Couldn't apply: ${u}`))
668
+ );
669
+ return h(
670
+ "div",
671
+ { class: "audit-trail-error audit-trail-nlp-unsupported" },
672
+ ul
673
+ );
674
+ }
675
+ function renderFiltersTabPanel(opts) {
676
+ const { value, distinct, onChange, onApplied } = opts;
677
+ let actionSelect;
678
+ let actorTypeSelect;
679
+ let targetTypeSelect;
680
+ let tenantSelect = null;
681
+ let resultSelect = null;
682
+ let actorInput;
683
+ let targetIDInput;
684
+ let originIPInput;
685
+ let sinceInput = null;
686
+ let beforeInput = null;
687
+ const timePresets = h(
688
+ "div",
689
+ {
690
+ class: "audit-trail-filter-tabs",
691
+ role: "tablist",
692
+ "aria-label": "Time range"
693
+ },
694
+ ...TIME_PRESETS.map((p) => {
695
+ const selected = value.range === p.key;
696
+ const btn = h(
697
+ "button",
698
+ {
699
+ type: "button",
700
+ role: "tab",
701
+ "aria-selected": selected ? "true" : "false",
702
+ class: selected ? "audit-trail-filter-tab audit-trail-filter-tab-selected" : "audit-trail-filter-tab"
703
+ },
704
+ p.label
705
+ );
706
+ btn.addEventListener("click", () => {
707
+ if (p.key === "custom") {
708
+ onChange({ ...value, range: p.key });
709
+ } else {
710
+ onChange({ ...value, range: p.key, since: void 0, before: void 0 });
711
+ }
712
+ });
713
+ return btn;
714
+ })
715
+ );
716
+ let customRow = null;
717
+ if (value.range === "custom") {
718
+ sinceInput = buildDateInput(value.since ?? "");
719
+ beforeInput = buildDateInput(value.before ?? "");
720
+ customRow = h(
721
+ "div",
722
+ { class: "audit-trail-filter-row" },
723
+ h(
724
+ "label",
725
+ { class: "audit-trail-filter-field" },
726
+ h("span", { class: "audit-trail-filter-field-label" }, "From"),
727
+ sinceInput
728
+ ),
729
+ h(
730
+ "label",
731
+ { class: "audit-trail-filter-field" },
732
+ h("span", { class: "audit-trail-filter-field-label" }, "To"),
733
+ beforeInput
734
+ )
735
+ );
736
+ }
737
+ actionSelect = buildSelect("Action", "All actions", value.action ?? "", distinct.actions);
738
+ actorTypeSelect = buildSelect(
739
+ "Actor type",
740
+ "All actor types",
741
+ value.actorType ?? "",
742
+ distinct.actorTypes
743
+ );
744
+ targetTypeSelect = buildSelect(
745
+ "Target type",
746
+ "All target types",
747
+ value.targetType ?? "",
748
+ distinct.targetTypes
749
+ );
750
+ const dropdownChildren = [actionSelect, actorTypeSelect, targetTypeSelect];
751
+ if (distinct.tenants.length > 0) {
752
+ tenantSelect = buildSelect(
753
+ "Tenant",
754
+ "All tenants",
755
+ value.tenantId ?? "",
756
+ distinct.tenants
757
+ );
758
+ dropdownChildren.push(tenantSelect);
759
+ }
760
+ if (distinct.resultStatuses.length > 0) {
761
+ resultSelect = buildSelect(
762
+ "Result",
763
+ "All results",
764
+ value.resultStatus ?? "",
765
+ distinct.resultStatuses
766
+ );
767
+ dropdownChildren.push(resultSelect);
768
+ }
769
+ const dropdownRow = h(
770
+ "div",
771
+ { class: "audit-trail-filter-row" },
772
+ ...dropdownChildren
773
+ );
774
+ actorInput = buildTextInput(
775
+ value.actor ?? "",
776
+ "Actor (id, name, email)",
777
+ "audit-trail-filter-input audit-trail-filter-actor-input"
778
+ );
779
+ targetIDInput = buildTextInput(
780
+ value.targetId ?? "",
781
+ "Target ID",
782
+ "audit-trail-filter-input audit-trail-filter-wide-input"
783
+ );
784
+ originIPInput = buildTextInput(
785
+ value.originIP ?? "",
786
+ "Origin IP",
787
+ "audit-trail-filter-input audit-trail-filter-wide-input"
788
+ );
789
+ const textRowActor = h("div", { class: "audit-trail-filter-row" }, actorInput);
790
+ const textRowTarget = h("div", { class: "audit-trail-filter-row" }, targetIDInput);
791
+ const textRowOrigin = h("div", { class: "audit-trail-filter-row" }, originIPInput);
792
+ const addBtn = h(
793
+ "button",
794
+ {
795
+ type: "button",
796
+ class: "audit-trail-button audit-trail-button-secondary"
797
+ },
798
+ "Add filters"
799
+ );
800
+ const norm = (s) => s === "" ? void 0 : s;
801
+ const computeDirty = () => {
802
+ if (norm(actionSelect.value) !== value.action) return true;
803
+ if (norm(actorTypeSelect.value) !== value.actorType) return true;
804
+ if (norm(targetTypeSelect.value) !== value.targetType) return true;
805
+ if (tenantSelect && norm(tenantSelect.value) !== value.tenantId) return true;
806
+ if (resultSelect && norm(resultSelect.value) !== value.resultStatus) return true;
807
+ if (norm(actorInput.value) !== value.actor) return true;
808
+ if (norm(targetIDInput.value) !== value.targetId) return true;
809
+ if (norm(originIPInput.value) !== value.originIP) return true;
810
+ if (value.range === "custom") {
811
+ if (norm(sinceInput?.value ?? "") !== value.since) return true;
812
+ if (norm(beforeInput?.value ?? "") !== value.before) return true;
813
+ }
814
+ return false;
815
+ };
816
+ const refreshDirty = () => {
817
+ addBtn.disabled = !computeDirty();
818
+ };
819
+ refreshDirty();
820
+ for (const el of [
821
+ actionSelect,
822
+ actorTypeSelect,
823
+ targetTypeSelect,
824
+ tenantSelect,
825
+ resultSelect
826
+ ].filter(Boolean)) {
827
+ el.addEventListener("change", refreshDirty);
828
+ }
829
+ for (const el of [actorInput, targetIDInput, originIPInput].filter(
830
+ Boolean
831
+ )) {
832
+ el.addEventListener("input", refreshDirty);
833
+ }
834
+ if (sinceInput) sinceInput.addEventListener("input", refreshDirty);
835
+ if (beforeInput) beforeInput.addEventListener("input", refreshDirty);
836
+ addBtn.addEventListener("click", () => {
837
+ onChange({
838
+ ...value,
839
+ action: norm(actionSelect.value),
840
+ actorType: norm(actorTypeSelect.value),
841
+ targetType: norm(targetTypeSelect.value),
842
+ tenantId: tenantSelect ? norm(tenantSelect.value) : value.tenantId,
843
+ resultStatus: resultSelect ? norm(resultSelect.value) : value.resultStatus,
844
+ actor: norm(actorInput.value),
845
+ targetId: norm(targetIDInput.value),
846
+ originIP: norm(originIPInput.value),
847
+ since: value.range === "custom" ? norm(sinceInput?.value ?? "") : value.since,
848
+ before: value.range === "custom" ? norm(beforeInput?.value ?? "") : value.before,
849
+ q: void 0,
850
+ nlpQ: void 0,
851
+ nlpExplanation: void 0,
852
+ nlpUnsupported: void 0
853
+ });
854
+ onApplied?.();
855
+ });
856
+ const addBtnRow = h("div", { class: "audit-trail-filter-actions" }, addBtn);
857
+ const metaSection = renderMetadataFilterSection(value, distinct, onChange, onApplied);
858
+ const children = [timePresets];
859
+ if (customRow) children.push(customRow);
860
+ children.push(
861
+ dropdownRow,
862
+ textRowActor,
863
+ textRowTarget,
864
+ textRowOrigin,
865
+ addBtnRow,
866
+ metaSection
867
+ );
868
+ return h("div", { class: "audit-trail-filter-tab-panel" }, ...children);
869
+ }
870
+ function buildSelect(ariaLabel, emptyLabel, selected, options) {
871
+ const sel = h("select", {
872
+ class: "audit-trail-filter-select",
873
+ "aria-label": ariaLabel
874
+ });
875
+ const empty = h("option", { value: "" }, emptyLabel);
876
+ if (selected === "") empty.selected = true;
877
+ sel.appendChild(empty);
878
+ for (const opt of options) {
879
+ const o = h("option", { value: opt }, opt);
880
+ if (opt === selected) o.selected = true;
881
+ sel.appendChild(o);
882
+ }
883
+ return sel;
884
+ }
885
+ function buildTextInput(value, placeholder, className) {
886
+ const input = h("input", {
887
+ type: "text",
888
+ class: className,
889
+ placeholder,
890
+ value
891
+ });
892
+ input.value = value;
893
+ return input;
894
+ }
895
+ function buildDateInput(value) {
896
+ const input = h("input", {
897
+ type: "datetime-local",
898
+ class: "audit-trail-filter-input",
899
+ value
900
+ });
901
+ input.value = value;
902
+ return input;
903
+ }
904
+ function renderMetadataFilterSection(value, distinct, onChange, onApplied) {
905
+ let variant = "metadata";
906
+ const typeSelect = h("select", null);
907
+ for (const [val, label] of [
908
+ ["metadata", "Metadata key/value"],
909
+ ["change-field", "Change: a field changed"],
910
+ ["change-before", "Change: previous value (before)"],
911
+ ["change-after", "Change: new value (after)"]
912
+ ]) {
913
+ const o = h("option", { value: val }, label);
914
+ typeSelect.appendChild(o);
915
+ }
916
+ typeSelect.value = variant;
917
+ const mdKey = h("input", {
918
+ type: "text",
919
+ placeholder: "e.g. environment",
920
+ list: "audit-trail-mdk-key-list"
921
+ });
922
+ const mdKeyList = h("datalist", { id: "audit-trail-mdk-key-list" });
923
+ for (const k of distinct.metadataKeys) {
924
+ mdKeyList.appendChild(
925
+ h("option", { value: k.key }, `${k.observed_type} \xB7 ${k.event_count}`)
926
+ );
927
+ }
928
+ const mdOp = h("select", null);
929
+ for (const [val, label] of [
930
+ ["eq", "equals"],
931
+ ["neq", "not equal"],
932
+ ["contains", "contains"],
933
+ ["gt", "greater than"],
934
+ ["gte", "greater or equal"],
935
+ ["lt", "less than"],
936
+ ["lte", "less or equal"],
937
+ ["between", "between"]
938
+ ]) {
939
+ mdOp.appendChild(h("option", { value: val }, label));
940
+ }
941
+ const mdValue = h("input", {
942
+ type: "text",
943
+ placeholder: "e.g. prod"
944
+ });
945
+ const mdValue2 = h("input", {
946
+ type: "text",
947
+ placeholder: "e.g. 1000"
948
+ });
949
+ const cfField = h("input", {
950
+ type: "text",
951
+ placeholder: "e.g. email",
952
+ list: "audit-trail-cf-field-list"
953
+ });
954
+ const cfFieldList = h("datalist", { id: "audit-trail-cf-field-list" });
955
+ for (const c of distinct.changeFields) {
956
+ cfFieldList.appendChild(h("option", { value: c.field }, `${c.event_count} events`));
957
+ }
958
+ const cbField = h("input", {
959
+ type: "text",
960
+ placeholder: "e.g. role",
961
+ list: "audit-trail-cf-field-list"
962
+ });
963
+ const cbValue = h("input", {
964
+ type: "text",
965
+ placeholder: "e.g. user"
966
+ });
967
+ const caField = h("input", {
968
+ type: "text",
969
+ placeholder: "e.g. role",
970
+ list: "audit-trail-cf-field-list"
971
+ });
972
+ const caValue = h("input", {
973
+ type: "text",
974
+ placeholder: "e.g. admin"
975
+ });
976
+ const metadataVariant = h(
977
+ "div",
978
+ { "data-variant": "metadata" },
979
+ addFilterRow("Key", mdKey, mdKeyList),
980
+ addFilterRow("Operator", mdOp),
981
+ addFilterRow("Value", mdValue),
982
+ addFilterRow("Upper bound", mdValue2)
983
+ );
984
+ const changeFieldVariant = h(
985
+ "div",
986
+ { "data-variant": "change-field" },
987
+ addFilterRow("Field", cfField, cfFieldList)
988
+ );
989
+ const changeBeforeVariant = h(
990
+ "div",
991
+ { "data-variant": "change-before" },
992
+ addFilterRow("Field", cbField),
993
+ addFilterRow("Previous value", cbValue)
994
+ );
995
+ const changeAfterVariant = h(
996
+ "div",
997
+ { "data-variant": "change-after" },
998
+ addFilterRow("Field", caField),
999
+ addFilterRow("New value", caValue)
1000
+ );
1001
+ const variants = [
1002
+ metadataVariant,
1003
+ changeFieldVariant,
1004
+ changeBeforeVariant,
1005
+ changeAfterVariant
1006
+ ];
1007
+ const errorEl = h("p", { class: "audit-trail-add-filter-error" }, "");
1008
+ errorEl.style.display = "none";
1009
+ const addBtn = h(
1010
+ "button",
1011
+ {
1012
+ type: "button",
1013
+ class: "audit-trail-button audit-trail-button-secondary"
1014
+ },
1015
+ "Add filter"
1016
+ );
1017
+ const mdValue2Row = metadataVariant.children[3];
1018
+ mdValue2Row.style.display = "none";
1019
+ const refresh = () => {
1020
+ const compiled = compileMetaClause({
1021
+ variant,
1022
+ mdKey: mdKey.value,
1023
+ mdOp: mdOp.value,
1024
+ mdValue: mdValue.value,
1025
+ mdValue2: mdValue2.value,
1026
+ cfField: cfField.value,
1027
+ cbField: cbField.value,
1028
+ cbValue: cbValue.value,
1029
+ caField: caField.value,
1030
+ caValue: caValue.value
1031
+ });
1032
+ addBtn.disabled = compiled.dsl === null;
1033
+ if (compiled.error && /Both bounds/.test(compiled.error)) {
1034
+ errorEl.textContent = compiled.error;
1035
+ errorEl.style.display = "";
1036
+ } else {
1037
+ errorEl.textContent = "";
1038
+ errorEl.style.display = "none";
1039
+ }
1040
+ };
1041
+ const showVariant = (v) => {
1042
+ variant = v;
1043
+ for (const el of variants) {
1044
+ el.style.display = el.dataset.variant === v ? "" : "none";
1045
+ }
1046
+ refresh();
1047
+ };
1048
+ showVariant("metadata");
1049
+ typeSelect.addEventListener("change", () => showVariant(typeSelect.value));
1050
+ mdOp.addEventListener("change", () => {
1051
+ mdValue2Row.style.display = mdOp.value === "between" ? "" : "none";
1052
+ refresh();
1053
+ });
1054
+ for (const el of [
1055
+ mdKey,
1056
+ mdValue,
1057
+ mdValue2,
1058
+ cfField,
1059
+ cbField,
1060
+ cbValue,
1061
+ caField,
1062
+ caValue
1063
+ ]) {
1064
+ el.addEventListener("input", refresh);
1065
+ }
1066
+ addBtn.addEventListener("click", () => {
1067
+ const compiled = compileMetaClause({
1068
+ variant,
1069
+ mdKey: mdKey.value,
1070
+ mdOp: mdOp.value,
1071
+ mdValue: mdValue.value,
1072
+ mdValue2: mdValue2.value,
1073
+ cfField: cfField.value,
1074
+ cbField: cbField.value,
1075
+ cbValue: cbValue.value,
1076
+ caField: caField.value,
1077
+ caValue: caValue.value
1078
+ });
1079
+ if (!compiled.dsl) return;
1080
+ onChange({
1081
+ ...value,
1082
+ q: compiled.dsl,
1083
+ nlpQ: void 0,
1084
+ nlpExplanation: void 0,
1085
+ nlpUnsupported: void 0
1086
+ });
1087
+ onApplied?.();
1088
+ });
1089
+ refresh();
1090
+ return h(
1091
+ "div",
1092
+ { class: "audit-trail-add-filter-inline" },
1093
+ h("h3", { class: "audit-trail-add-filter-heading" }, "Metadata and Changed Fields"),
1094
+ addFilterRow("Filter type", typeSelect),
1095
+ metadataVariant,
1096
+ changeFieldVariant,
1097
+ changeBeforeVariant,
1098
+ changeAfterVariant,
1099
+ errorEl,
1100
+ h(
1101
+ "div",
1102
+ { class: "audit-trail-filter-actions audit-trail-add-filter-actions" },
1103
+ addBtn
1104
+ )
1105
+ );
1106
+ }
1107
+ function addFilterRow(label, ...children) {
1108
+ return h(
1109
+ "label",
1110
+ { class: "audit-trail-add-filter-row" },
1111
+ h("span", { class: "audit-trail-add-filter-label" }, label),
1112
+ ...children
1113
+ );
1114
+ }
1115
+ function compileMetaClause(i) {
1116
+ switch (i.variant) {
1117
+ case "metadata": {
1118
+ const key = i.mdKey.trim();
1119
+ if (!key) return { dsl: null, error: "Key is required" };
1120
+ const path = "metadata." + quoteKeyIfNeeded(key);
1121
+ const v1 = i.mdValue;
1122
+ const v2 = i.mdValue2;
1123
+ switch (i.mdOp) {
1124
+ case "eq":
1125
+ return { dsl: `${path}:${quoteIfNeeded(v1)}` };
1126
+ case "neq":
1127
+ return { dsl: `${path}:!${quoteIfNeeded(v1)}` };
1128
+ case "contains":
1129
+ return { dsl: `${path}:~${quoteIfNeeded(v1)}` };
1130
+ case "gt":
1131
+ return { dsl: `${path}:>${quoteIfNeeded(v1)}` };
1132
+ case "gte":
1133
+ return { dsl: `${path}:>=${quoteIfNeeded(v1)}` };
1134
+ case "lt":
1135
+ return { dsl: `${path}:<${quoteIfNeeded(v1)}` };
1136
+ case "lte":
1137
+ return { dsl: `${path}:<=${quoteIfNeeded(v1)}` };
1138
+ case "between":
1139
+ if (v1 === "" || v2 === "") {
1140
+ return { dsl: null, error: "Both bounds required for between" };
1141
+ }
1142
+ return {
1143
+ dsl: `${path}:[${quoteIfNeeded(v1)} TO ${quoteIfNeeded(v2)}]`
1144
+ };
1145
+ }
1146
+ return { dsl: null, error: "Unknown operator" };
1147
+ }
1148
+ case "change-field": {
1149
+ const f = i.cfField.trim();
1150
+ if (!f) return { dsl: null, error: "Field is required" };
1151
+ return { dsl: `change.field:${quoteIfNeeded(f)}` };
1152
+ }
1153
+ case "change-before": {
1154
+ const f = i.cbField.trim();
1155
+ if (!f) return { dsl: null, error: "Field is required" };
1156
+ return {
1157
+ dsl: `change.${quoteKeyIfNeeded(f)}.before:${quoteIfNeeded(i.cbValue)}`
1158
+ };
1159
+ }
1160
+ case "change-after": {
1161
+ const f = i.caField.trim();
1162
+ if (!f) return { dsl: null, error: "Field is required" };
1163
+ return {
1164
+ dsl: `change.${quoteKeyIfNeeded(f)}.after:${quoteIfNeeded(i.caValue)}`
1165
+ };
1166
+ }
1167
+ }
1168
+ }
1169
+ function quoteIfNeeded(raw) {
1170
+ if (raw === "") return '""';
1171
+ if (/[\s"\(\)\[\]]/.test(raw)) {
1172
+ return '"' + raw.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
1173
+ }
1174
+ return raw;
1175
+ }
1176
+ function quoteKeyIfNeeded(raw) {
1177
+ if (/^[A-Za-z_][A-Za-z0-9_-]*$/.test(raw)) return raw;
1178
+ return '"' + raw + '"';
1179
+ }
1180
+ function renderQueryTabPanel(opts) {
1181
+ const { value, onChange } = opts;
1182
+ const input = h("input", {
1183
+ type: "text",
1184
+ id: "audit-trail-query-input",
1185
+ class: "audit-trail-query-input",
1186
+ placeholder: "e.g. action:user.login AND result.status:!ok",
1187
+ "aria-label": "Advanced query (DSL)",
1188
+ spellcheck: "false",
1189
+ value: value.q ?? ""
1190
+ });
1191
+ input.value = value.q ?? "";
1192
+ const label = h(
1193
+ "label",
1194
+ { class: "audit-trail-query-label", for: "audit-trail-query-input" },
1195
+ "Query ",
1196
+ h(
1197
+ "a",
1198
+ {
1199
+ href: "https://everscribe.io/docs",
1200
+ target: "_blank",
1201
+ rel: "noopener noreferrer"
1202
+ },
1203
+ "(see docs)"
1204
+ )
1205
+ );
1206
+ const btn = h(
1207
+ "button",
1208
+ {
1209
+ type: "button",
1210
+ class: "audit-trail-button audit-trail-button-secondary"
1211
+ },
1212
+ "Search"
1213
+ );
1214
+ const refresh = () => {
1215
+ btn.disabled = input.value.trim() === (value.q ?? "");
1216
+ };
1217
+ refresh();
1218
+ input.addEventListener("input", refresh);
1219
+ const submit = () => {
1220
+ const next = input.value.trim() || void 0;
1221
+ if (next === value.q) return;
1222
+ onChange({
1223
+ ...value,
1224
+ q: next,
1225
+ nlpQ: void 0,
1226
+ nlpExplanation: void 0,
1227
+ nlpUnsupported: void 0
1228
+ });
1229
+ };
1230
+ btn.addEventListener("click", submit);
1231
+ input.addEventListener("keydown", (e) => {
1232
+ if (e.key === "Enter") {
1233
+ e.preventDefault();
1234
+ submit();
1235
+ }
1236
+ });
1237
+ return h(
1238
+ "form",
1239
+ { class: "audit-trail-query-form" },
1240
+ label,
1241
+ input,
1242
+ h("div", { class: "audit-trail-filter-actions" }, btn)
1243
+ );
1244
+ }
1245
+ function renderFilterChips(opts) {
1246
+ const { value, onChange } = opts;
1247
+ const chips = [];
1248
+ const colChip = (field, label, raw) => {
1249
+ if (!raw) return;
1250
+ chips.push({
1251
+ label: `${label}: ${raw}`,
1252
+ remove: () => onChange({ ...value, [field]: void 0 })
1253
+ });
1254
+ };
1255
+ colChip("action", "Action", value.action);
1256
+ colChip("actor", "Actor", value.actor);
1257
+ colChip("actorType", "Actor type", value.actorType);
1258
+ colChip("tenantId", "Tenant", value.tenantId);
1259
+ colChip("targetType", "Target type", value.targetType);
1260
+ colChip("targetId", "Target ID", value.targetId);
1261
+ colChip("resultStatus", "Result", value.resultStatus);
1262
+ colChip("originIP", "Origin IP", value.originIP);
1263
+ if (value.q) {
1264
+ for (const clause of parseQClauses(value.q)) {
1265
+ chips.push({
1266
+ label: clause.label,
1267
+ remove: () => {
1268
+ const nextQ = removeClauseFromQ(value.q, clause.raw);
1269
+ onChange({
1270
+ ...value,
1271
+ q: nextQ,
1272
+ nlpQ: nextQ ? value.nlpQ : void 0,
1273
+ nlpExplanation: nextQ ? value.nlpExplanation : void 0,
1274
+ nlpUnsupported: nextQ ? value.nlpUnsupported : void 0
1275
+ });
1276
+ }
1277
+ });
1278
+ }
1279
+ }
1280
+ if (chips.length === 0) return null;
1281
+ const chipsRow = h(
1282
+ "div",
1283
+ { class: "audit-trail-filter-chips" },
1284
+ ...chips.map((c) => {
1285
+ const btn = h(
1286
+ "button",
1287
+ {
1288
+ type: "button",
1289
+ class: "audit-trail-filter-chip",
1290
+ title: "Remove this filter",
1291
+ "aria-label": `Remove filter: ${c.label}`
1292
+ },
1293
+ h("span", null, c.label),
1294
+ h("span", { "aria-hidden": "true", class: "audit-trail-filter-chip-x" }, "\xD7")
1295
+ );
1296
+ btn.addEventListener("click", c.remove);
1297
+ return btn;
1298
+ })
1299
+ );
1300
+ const clearAllBtn = h(
1301
+ "button",
1302
+ {
1303
+ type: "button",
1304
+ class: "audit-trail-button audit-trail-button-text"
1305
+ },
1306
+ "Clear all"
1307
+ );
1308
+ clearAllBtn.addEventListener("click", () => {
1309
+ onChange({
1310
+ range: value.range,
1311
+ since: value.since,
1312
+ before: value.before
1313
+ });
1314
+ });
1315
+ return h(
1316
+ "div",
1317
+ { class: "audit-trail-filter-chips-row" },
1318
+ chipsRow,
1319
+ clearAllBtn
1320
+ );
1321
+ }
1322
+ function hasAnyColumnFilter(v) {
1323
+ return !!(v.action || v.actor || v.actorType || v.tenantId || v.targetType || v.targetId || v.resultStatus || v.originIP);
1324
+ }
1325
+ function countActiveColumnFilters(v) {
1326
+ let n = 0;
1327
+ if (v.action) n++;
1328
+ if (v.actor) n++;
1329
+ if (v.actorType) n++;
1330
+ if (v.tenantId) n++;
1331
+ if (v.targetType) n++;
1332
+ if (v.targetId) n++;
1333
+ if (v.resultStatus) n++;
1334
+ if (v.originIP) n++;
1335
+ return n;
1336
+ }
1337
+ function parseQClauses(q) {
1338
+ const trimmed = q.trim();
1339
+ if (!trimmed) return [];
1340
+ if (/[()]/.test(trimmed)) return [{ label: trimmed, raw: trimmed }];
1341
+ return trimmed.split(/\s+AND\s+/i).map((s) => s.trim()).filter((s) => s.length > 0).map((s) => ({ label: s, raw: s }));
1342
+ }
1343
+ function removeClauseFromQ(q, raw) {
1344
+ const remaining = parseQClauses(q).filter((c) => c.raw !== raw);
1345
+ if (remaining.length === 0) return void 0;
1346
+ return remaining.map((c) => c.raw).join(" AND ");
1347
+ }
1348
+ function resolveTimeBounds(filters) {
1349
+ switch (filters.range) {
1350
+ case "24h":
1351
+ return { since: relativeIso(24 * 60 * 60 * 1e3) };
1352
+ case "7d":
1353
+ return { since: relativeIso(7 * 24 * 60 * 60 * 1e3) };
1354
+ case "30d":
1355
+ return { since: relativeIso(30 * 24 * 60 * 60 * 1e3) };
1356
+ case "custom":
1357
+ return {
1358
+ since: localToIso(filters.since),
1359
+ before: localToIso(filters.before)
1360
+ };
1361
+ case "all":
1362
+ default:
1363
+ return {};
1364
+ }
1365
+ }
1366
+ function relativeIso(ms) {
1367
+ return new Date(Date.now() - ms).toISOString();
1368
+ }
1369
+ function localToIso(value) {
1370
+ if (!value) return void 0;
1371
+ const d = new Date(value);
1372
+ if (Number.isNaN(d.getTime())) return void 0;
1373
+ return d.toISOString();
1374
+ }
1375
+ function renderTable(events, columns, onRowClick) {
1376
+ const headerCells = columns.map(
1377
+ (col) => h(
1378
+ "th",
1379
+ { scope: "col", class: `audit-trail-th audit-trail-th-${col}` },
1380
+ COLUMN_LABELS[col] ?? col
1381
+ )
1382
+ );
1383
+ const rows = events.map((event) => renderRow(event, columns, onRowClick));
1384
+ return h(
1385
+ "div",
1386
+ { class: "audit-trail-table-wrap" },
1387
+ h(
1388
+ "table",
1389
+ { class: "audit-trail-table" },
1390
+ h("thead", null, h("tr", null, ...headerCells)),
1391
+ h("tbody", null, ...rows)
1392
+ )
1393
+ );
1394
+ }
1395
+ function renderRow(event, columns, onClick) {
1396
+ const interactive = Boolean(onClick);
1397
+ const cells = columns.map(
1398
+ (col) => h(
1399
+ "td",
1400
+ {
1401
+ class: `audit-trail-cell audit-trail-cell-${col}`,
1402
+ "data-label": COLUMN_LABELS[col] ?? col
1403
+ },
1404
+ renderCell(col, event)
1405
+ )
1406
+ );
1407
+ const tr = h(
1408
+ "tr",
1409
+ {
1410
+ class: "audit-trail-row",
1411
+ tabindex: interactive ? "0" : void 0
1412
+ },
1413
+ ...cells
1414
+ );
1415
+ if (interactive && onClick) {
1416
+ tr.addEventListener("click", () => onClick(event));
1417
+ tr.addEventListener("keydown", (e) => {
1418
+ if (e.key === "Enter" || e.key === " ") {
1419
+ e.preventDefault();
1420
+ onClick(event);
1421
+ }
1422
+ });
1423
+ }
1424
+ return tr;
1425
+ }
1426
+ function renderCell(column, event) {
1427
+ switch (column) {
1428
+ case "result":
1429
+ return renderResult(event.result);
1430
+ case "origin":
1431
+ return renderOrigin(event.origin);
1432
+ case "change":
1433
+ case "metadata":
1434
+ return renderPresence(getField(event, column));
1435
+ default: {
1436
+ const value = getField(event, column);
1437
+ if (value == null) return null;
1438
+ if (column === "occurred_at" && typeof value === "string") {
1439
+ return h(
1440
+ "time",
1441
+ {
1442
+ datetime: value,
1443
+ title: value,
1444
+ // Marker for AuditTrailElement's 30s tick: each render uses
1445
+ // Date.now(), and the timer queries by this attribute to
1446
+ // refresh the relative half without rebuilding the table.
1447
+ "data-occurred-at": value
1448
+ },
1449
+ formatTimeCell(value, Date.now())
1450
+ );
1451
+ }
1452
+ if (typeof value === "string") return value;
1453
+ if (typeof value === "object") return summarizeObject(value);
1454
+ return String(value);
1455
+ }
1456
+ }
1457
+ }
1458
+ function getField(event, key) {
1459
+ return event[key];
1460
+ }
1461
+ function renderResult(value) {
1462
+ if (value == null || typeof value !== "object") return "-";
1463
+ const obj = value;
1464
+ const status = typeof obj.status === "string" ? obj.status : "";
1465
+ if (!status) return "-";
1466
+ return h(
1467
+ "span",
1468
+ { class: `audit-trail-status audit-trail-status-${cssToken(status)}` },
1469
+ status
1470
+ );
1471
+ }
1472
+ function renderOrigin(value) {
1473
+ if (value == null || typeof value !== "object") return "-";
1474
+ const obj = value;
1475
+ for (const key of ["ip", "hostname", "host"]) {
1476
+ const v = obj[key];
1477
+ if (typeof v === "string" && v.length > 0) return v;
1478
+ }
1479
+ return summarizeObject(obj);
1480
+ }
1481
+ function renderPresence(value) {
1482
+ if (value == null) return "-";
1483
+ if (typeof value === "object") {
1484
+ const keys = Object.keys(value);
1485
+ if (keys.length === 0) return "-";
1486
+ return h("span", { class: "audit-trail-cell-presence" }, "View");
1487
+ }
1488
+ if (typeof value === "string" && value.length > 0) {
1489
+ return h("span", { class: "audit-trail-cell-presence" }, "View");
1490
+ }
1491
+ return "-";
1492
+ }
1493
+ function summarizeObject(value) {
1494
+ const obj = value;
1495
+ for (const key of ["name", "email", "id", "type"]) {
1496
+ const v = obj[key];
1497
+ if (typeof v === "string" && v.length > 0) return v;
1498
+ }
1499
+ const keys = Object.keys(obj);
1500
+ if (keys.length === 0) return "-";
1501
+ return `{${keys.length} ${keys.length === 1 ? "field" : "fields"}}`;
1502
+ }
1503
+ function cssToken(s) {
1504
+ return s.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
1505
+ }
1506
+
1507
+ // src/AuditTrailElement.ts
1508
+ var DEFAULT_API_BASE = "https://api.everscribe.io/v1/embed";
1509
+ var DEFAULT_PAGE_SIZE = 25;
1510
+ var DEFAULT_POLL_INTERVAL_MS = 5e3;
1511
+ var DEFAULT_VISIBLE_COLUMNS = [
1512
+ "occurred_at",
1513
+ "action",
1514
+ "actor",
1515
+ "target",
1516
+ "tenant_id",
1517
+ "result"
1518
+ ];
1519
+ var OBSERVED = [
1520
+ "token",
1521
+ "token-endpoint",
1522
+ "api-base",
1523
+ "page-size",
1524
+ "poll-interval",
1525
+ "theme",
1526
+ "default-time-range"
1527
+ ];
1528
+ var AuditTrailElement = class extends HTMLElement {
1529
+ constructor() {
1530
+ super(...arguments);
1531
+ this.bootstrap = { phase: "idle" };
1532
+ this.bootstrapAbort = null;
1533
+ this.eventsStore = null;
1534
+ this.distinctStore = null;
1535
+ this.eventsUnsub = null;
1536
+ this.distinctUnsub = null;
1537
+ // Latest snapshots cached so render() doesn't have to peek into stores.
1538
+ this.eventsState = null;
1539
+ this.distinctValues = {
1540
+ actions: [],
1541
+ actorTypes: [],
1542
+ targetTypes: [],
1543
+ tenants: [],
1544
+ resultStatuses: [],
1545
+ metadataKeys: [],
1546
+ changeFields: []
1547
+ };
1548
+ // UI state.
1549
+ this.filters = { range: "all" };
1550
+ this.filtersOpen = false;
1551
+ this.activeTab = "filters";
1552
+ this.nlpState = { phase: "idle" };
1553
+ this.nlpAbort = null;
1554
+ this.visibleSet = new Set(DEFAULT_VISIBLE_COLUMNS);
1555
+ // Slot tracking for partial re-renders. bodySlot wraps the table /
1556
+ // state messages so events-store subscriptions can update only the
1557
+ // table region without touching the toolbar (which would clobber
1558
+ // open <details> picker state) or filter inputs (focus + drafts).
1559
+ this.bodySlot = null;
1560
+ // Modal cleanup hooks. Set when a modal is open; null otherwise.
1561
+ this.detailDispose = null;
1562
+ this.exportDispose = null;
1563
+ // 30s timer that refreshes the relative half of each row's
1564
+ // "absolute (N ago)" timestamp without rebuilding the table.
1565
+ this.relativeTimeTimer = null;
1566
+ }
1567
+ static get observedAttributes() {
1568
+ return OBSERVED;
1569
+ }
1570
+ connectedCallback() {
1571
+ this.classList.add("audit-trail-root");
1572
+ this.classList.add(`audit-trail-theme-${this.themeAttr()}`);
1573
+ this.filters = { range: this.defaultTimeRangeAttr() };
1574
+ this.start();
1575
+ this.startRelativeTimeTimer();
1576
+ }
1577
+ disconnectedCallback() {
1578
+ this.cleanup();
1579
+ this.stopRelativeTimeTimer();
1580
+ }
1581
+ attributeChangedCallback(name, oldVal, newVal) {
1582
+ if (oldVal === newVal) return;
1583
+ if (!this.isConnected) return;
1584
+ if (name === "theme") {
1585
+ const oldClass = `audit-trail-theme-${oldVal === "dark" ? "dark" : "light"}`;
1586
+ const newClass = `audit-trail-theme-${newVal === "dark" ? "dark" : "light"}`;
1587
+ this.classList.remove(oldClass);
1588
+ this.classList.add(newClass);
1589
+ return;
1590
+ }
1591
+ if (name === "token" || name === "token-endpoint") {
1592
+ this.start();
1593
+ return;
1594
+ }
1595
+ if (name === "default-time-range") {
1596
+ return;
1597
+ }
1598
+ if (this.bootstrap.phase === "ready") this.restartEventsStore();
1599
+ }
1600
+ // Public method, mirrors the React component's refresh hatch. Triggers
1601
+ // a full re-bootstrap (fresh token fetch + fresh stores).
1602
+ refresh() {
1603
+ this.start();
1604
+ }
1605
+ // ---- attributes ----
1606
+ themeAttr() {
1607
+ return this.getAttribute("theme") === "dark" ? "dark" : "light";
1608
+ }
1609
+ apiBaseAttr() {
1610
+ return this.getAttribute("api-base") || DEFAULT_API_BASE;
1611
+ }
1612
+ pageSizeAttr() {
1613
+ const n = Number.parseInt(this.getAttribute("page-size") ?? "", 10);
1614
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_PAGE_SIZE;
1615
+ }
1616
+ pollIntervalAttr() {
1617
+ const v = this.getAttribute("poll-interval");
1618
+ if (v === null) return DEFAULT_POLL_INTERVAL_MS;
1619
+ const n = Number.parseInt(v, 10);
1620
+ return Number.isFinite(n) ? n : DEFAULT_POLL_INTERVAL_MS;
1621
+ }
1622
+ defaultTimeRangeAttr() {
1623
+ const v = this.getAttribute("default-time-range");
1624
+ if (v === "24h" || v === "7d" || v === "30d" || v === "all") return v;
1625
+ return "all";
1626
+ }
1627
+ // storageKey returns a localStorage key namespaced by the current
1628
+ // token's project (sub) and tenant. Returns null when the element
1629
+ // isn't ready or the token has no usable sub - callers treat null
1630
+ // as "skip persistence."
1631
+ storageKey(suffix) {
1632
+ if (this.bootstrap.phase !== "ready") return null;
1633
+ const sub = this.bootstrap.claims?.sub;
1634
+ if (!sub) return null;
1635
+ const tenant = this.bootstrap.claims?.tenant_id || "_";
1636
+ return `audit-trail:${suffix}:${sub}:${tenant}`;
1637
+ }
1638
+ // ---- bootstrap ----
1639
+ start() {
1640
+ this.disposeModals();
1641
+ this.cleanupStores();
1642
+ this.bootstrapAbort?.abort();
1643
+ this.bootstrapAbort = null;
1644
+ const token = this.getAttribute("token");
1645
+ if (token) {
1646
+ this.setReady(token);
1647
+ return;
1648
+ }
1649
+ if (!this.getAttribute("token-endpoint") && !this.onTokenExpired) {
1650
+ this.bootstrap = { phase: "error", reason: "config" };
1651
+ this.render();
1652
+ return;
1653
+ }
1654
+ this.bootstrap = { phase: "loading" };
1655
+ this.render();
1656
+ const ctrl = new AbortController();
1657
+ this.bootstrapAbort = ctrl;
1658
+ void (async () => {
1659
+ const t = await fetchTokenViaOpts({
1660
+ tokenEndpoint: this.getAttribute("token-endpoint") ?? void 0,
1661
+ onTokenExpired: this.onTokenExpired
1662
+ });
1663
+ if (ctrl.signal.aborted) return;
1664
+ if (!t) {
1665
+ this.bootstrap = { phase: "error", reason: "fetch" };
1666
+ this.render();
1667
+ return;
1668
+ }
1669
+ this.setReady(t);
1670
+ })();
1671
+ }
1672
+ setReady(token) {
1673
+ const claims = parseClaims(token);
1674
+ this.bootstrap = { phase: "ready", token, claims };
1675
+ if (claims?.columns && claims.columns.length > 0) {
1676
+ this.visibleSet = new Set(claims.columns);
1677
+ } else {
1678
+ this.visibleSet = new Set(DEFAULT_VISIBLE_COLUMNS);
1679
+ }
1680
+ this.restoreHiddenColumns();
1681
+ this.restoreFilters();
1682
+ this.activeTab = pickInitialTab(this.filters, claims);
1683
+ this.startEventsStore(token);
1684
+ this.startDistinctStore(token);
1685
+ this.render();
1686
+ }
1687
+ startEventsStore(token) {
1688
+ this.eventsUnsub?.();
1689
+ this.eventsStore?.dispose();
1690
+ const { since, before } = resolveTimeBounds(this.filters);
1691
+ const apiBase = this.apiBaseAttr();
1692
+ const tokenEndpoint = this.getAttribute("token-endpoint") ?? void 0;
1693
+ const events = createEventsStore({
1694
+ apiBase,
1695
+ token,
1696
+ pageSize: this.pageSizeAttr(),
1697
+ pollInterval: this.pollIntervalAttr(),
1698
+ tokenEndpoint,
1699
+ onTokenExpired: this.onTokenExpired,
1700
+ onError: (err) => this.dispatchError(err),
1701
+ since,
1702
+ before,
1703
+ action: this.filters.action,
1704
+ actor: this.filters.actor,
1705
+ actorType: this.filters.actorType,
1706
+ tenantId: this.filters.tenantId,
1707
+ targetType: this.filters.targetType,
1708
+ targetId: this.filters.targetId,
1709
+ resultStatus: this.filters.resultStatus,
1710
+ originIP: this.filters.originIP,
1711
+ q: this.filters.q
1712
+ });
1713
+ this.eventsStore = events;
1714
+ this.eventsState = events.getSnapshot();
1715
+ this.eventsUnsub = events.subscribe(() => {
1716
+ this.eventsState = events.getSnapshot();
1717
+ this.renderBody();
1718
+ });
1719
+ }
1720
+ startDistinctStore(token) {
1721
+ this.distinctUnsub?.();
1722
+ this.distinctStore?.dispose();
1723
+ const apiBase = this.apiBaseAttr();
1724
+ const tokenEndpoint = this.getAttribute("token-endpoint") ?? void 0;
1725
+ const distinct = createDistinctValuesStore({
1726
+ apiBase,
1727
+ token,
1728
+ tokenEndpoint,
1729
+ onTokenExpired: this.onTokenExpired
1730
+ });
1731
+ this.distinctStore = distinct;
1732
+ this.distinctValues = distinct.getSnapshot();
1733
+ this.distinctUnsub = distinct.subscribe(() => {
1734
+ this.distinctValues = distinct.getSnapshot();
1735
+ this.render();
1736
+ });
1737
+ }
1738
+ restartEventsStore() {
1739
+ if (this.bootstrap.phase !== "ready") return;
1740
+ this.startEventsStore(this.bootstrap.token);
1741
+ }
1742
+ cleanupStores() {
1743
+ this.eventsUnsub?.();
1744
+ this.distinctUnsub?.();
1745
+ this.eventsStore?.dispose();
1746
+ this.distinctStore?.dispose();
1747
+ this.eventsUnsub = null;
1748
+ this.distinctUnsub = null;
1749
+ this.eventsStore = null;
1750
+ this.distinctStore = null;
1751
+ this.eventsState = null;
1752
+ this.distinctValues = {
1753
+ actions: [],
1754
+ actorTypes: [],
1755
+ targetTypes: [],
1756
+ tenants: [],
1757
+ resultStatuses: [],
1758
+ metadataKeys: [],
1759
+ changeFields: []
1760
+ };
1761
+ }
1762
+ cleanup() {
1763
+ this.bootstrapAbort?.abort();
1764
+ this.bootstrapAbort = null;
1765
+ this.nlpAbort?.abort();
1766
+ this.nlpAbort = null;
1767
+ this.disposeModals();
1768
+ this.cleanupStores();
1769
+ }
1770
+ startRelativeTimeTimer() {
1771
+ if (this.relativeTimeTimer !== null) return;
1772
+ this.relativeTimeTimer = setInterval(
1773
+ () => this.tickRelativeTimes(),
1774
+ RELATIVE_TIME_TICK_MS
1775
+ );
1776
+ }
1777
+ stopRelativeTimeTimer() {
1778
+ if (this.relativeTimeTimer === null) return;
1779
+ clearInterval(this.relativeTimeTimer);
1780
+ this.relativeTimeTimer = null;
1781
+ }
1782
+ tickRelativeTimes() {
1783
+ const cells = this.querySelectorAll("time[data-occurred-at]");
1784
+ if (cells.length === 0) return;
1785
+ const now = Date.now();
1786
+ cells.forEach((c) => {
1787
+ const iso = c.getAttribute("data-occurred-at");
1788
+ if (!iso) return;
1789
+ c.textContent = formatTimeCell(iso, now);
1790
+ });
1791
+ }
1792
+ disposeModals() {
1793
+ this.detailDispose?.();
1794
+ this.detailDispose = null;
1795
+ this.exportDispose?.();
1796
+ this.exportDispose = null;
1797
+ }
1798
+ openDetail(event) {
1799
+ this.detailDispose?.();
1800
+ this.detailDispose = openEventDetail({
1801
+ event,
1802
+ theme: this.themeAttr(),
1803
+ onClose: () => {
1804
+ this.detailDispose = null;
1805
+ }
1806
+ });
1807
+ }
1808
+ openExport() {
1809
+ this.exportDispose?.();
1810
+ this.exportDispose = openExportModal({
1811
+ theme: this.themeAttr(),
1812
+ onDownload: (format) => this.runExport(format),
1813
+ onClose: () => {
1814
+ this.exportDispose = null;
1815
+ }
1816
+ });
1817
+ }
1818
+ async runExport(format) {
1819
+ if (this.bootstrap.phase !== "ready") throw new Error("Not authenticated.");
1820
+ const apiBase = this.apiBaseAttr();
1821
+ const tokenEndpoint = this.getAttribute("token-endpoint") ?? void 0;
1822
+ const { since, before } = resolveTimeBounds(this.filters);
1823
+ const params = {
1824
+ format,
1825
+ since,
1826
+ before,
1827
+ action: this.filters.action,
1828
+ actor: this.filters.actor,
1829
+ actorType: this.filters.actorType,
1830
+ tenantId: this.filters.tenantId,
1831
+ targetType: this.filters.targetType,
1832
+ targetId: this.filters.targetId,
1833
+ resultStatus: this.filters.resultStatus,
1834
+ originIP: this.filters.originIP,
1835
+ q: this.filters.q
1836
+ };
1837
+ const run = (token) => exportEvents({ apiBase, token, params });
1838
+ let result;
1839
+ try {
1840
+ result = await run(this.bootstrap.token);
1841
+ } catch (err) {
1842
+ if (err instanceof EmbedError && err.kind === "unauthorized") {
1843
+ const refreshed = await fetchTokenViaOpts({
1844
+ tokenEndpoint,
1845
+ onTokenExpired: this.onTokenExpired
1846
+ });
1847
+ if (!refreshed) throw new Error("Authentication failed.");
1848
+ result = await run(refreshed);
1849
+ } else if (err instanceof EmbedError) {
1850
+ throw new Error(exportErrorMessage(err));
1851
+ } else {
1852
+ throw err;
1853
+ }
1854
+ }
1855
+ triggerBrowserDownload(result.blob, result.filename);
1856
+ }
1857
+ dispatchError(err) {
1858
+ this.dispatchEvent(
1859
+ new CustomEvent("audit-trail-error", {
1860
+ detail: { error: err },
1861
+ bubbles: true
1862
+ })
1863
+ );
1864
+ }
1865
+ // ---- columns ----
1866
+ get availableColumns() {
1867
+ if (this.bootstrap.phase !== "ready") return ALL_COLUMNS;
1868
+ const cols = this.bootstrap.claims?.columns;
1869
+ if (cols && cols.length > 0) return cols;
1870
+ return ALL_COLUMNS;
1871
+ }
1872
+ get visibleColumns() {
1873
+ return this.availableColumns.filter((c) => this.visibleSet.has(c));
1874
+ }
1875
+ toggleColumn(col) {
1876
+ if (this.visibleSet.has(col)) this.visibleSet.delete(col);
1877
+ else this.visibleSet.add(col);
1878
+ this.persistHiddenColumns();
1879
+ this.render();
1880
+ }
1881
+ restoreHiddenColumns() {
1882
+ const key = this.storageKey("cols");
1883
+ if (!key) return;
1884
+ try {
1885
+ const raw = globalThis.localStorage?.getItem(key);
1886
+ if (!raw) return;
1887
+ const hidden = JSON.parse(raw);
1888
+ if (!Array.isArray(hidden)) return;
1889
+ for (const c of hidden) {
1890
+ if (typeof c === "string") this.visibleSet.delete(c);
1891
+ }
1892
+ } catch {
1893
+ }
1894
+ }
1895
+ persistHiddenColumns() {
1896
+ const key = this.storageKey("cols");
1897
+ if (!key) return;
1898
+ try {
1899
+ const hidden = this.availableColumns.filter((c) => !this.visibleSet.has(c));
1900
+ globalThis.localStorage?.setItem(key, JSON.stringify(hidden));
1901
+ } catch {
1902
+ }
1903
+ }
1904
+ restoreFilters() {
1905
+ const key = this.storageKey("filters");
1906
+ if (!key) return;
1907
+ try {
1908
+ const raw = globalThis.localStorage?.getItem(key);
1909
+ if (!raw) return;
1910
+ const parsed = JSON.parse(raw);
1911
+ if (!parsed || typeof parsed !== "object") return;
1912
+ const range = parsed.range;
1913
+ if (range !== "24h" && range !== "7d" && range !== "30d" && range !== "custom" && range !== "all") return;
1914
+ this.filters = parsed;
1915
+ } catch {
1916
+ }
1917
+ }
1918
+ persistFilters() {
1919
+ const key = this.storageKey("filters");
1920
+ if (!key) return;
1921
+ try {
1922
+ globalThis.localStorage?.setItem(key, JSON.stringify(this.filters));
1923
+ } catch {
1924
+ }
1925
+ }
1926
+ // ---- filters ----
1927
+ setFilters(next) {
1928
+ this.filters = next;
1929
+ this.persistFilters();
1930
+ this.render();
1931
+ this.restartEventsStore();
1932
+ }
1933
+ setActiveTab(next) {
1934
+ if (this.activeTab === next) return;
1935
+ this.activeTab = next;
1936
+ this.render();
1937
+ }
1938
+ // submitNLP fires the LLM round-trip via the embed NLP endpoint.
1939
+ // Updates nlpState immediately for the spinner, then folds the
1940
+ // result into filters on success or surfaces an error on failure.
1941
+ submitNLP(query) {
1942
+ if (this.bootstrap.phase !== "ready") return;
1943
+ const trimmed = query.trim();
1944
+ if (!trimmed) return;
1945
+ const apiBase = this.apiBaseAttr();
1946
+ const token = this.bootstrap.token;
1947
+ this.nlpAbort?.abort();
1948
+ const ctrl = new AbortController();
1949
+ this.nlpAbort = ctrl;
1950
+ this.nlpState = { phase: "loading" };
1951
+ this.render();
1952
+ void (async () => {
1953
+ try {
1954
+ const result = await generateNLPFilters({
1955
+ apiBase,
1956
+ token,
1957
+ query: trimmed,
1958
+ signal: ctrl.signal
1959
+ });
1960
+ if (ctrl.signal.aborted) return;
1961
+ this.nlpState = { phase: "idle" };
1962
+ this.filters = {
1963
+ ...this.filters,
1964
+ q: result.dsl || void 0,
1965
+ nlpQ: trimmed,
1966
+ nlpExplanation: result.explanation,
1967
+ nlpUnsupported: result.unsupported
1968
+ };
1969
+ this.persistFilters();
1970
+ this.render();
1971
+ this.restartEventsStore();
1972
+ } catch (err) {
1973
+ if (err instanceof DOMException && err.name === "AbortError") return;
1974
+ if (err instanceof EmbedError && err.kind === "unauthorized") {
1975
+ const refreshed = await fetchTokenViaOpts({
1976
+ tokenEndpoint: this.getAttribute("token-endpoint") ?? void 0,
1977
+ onTokenExpired: this.onTokenExpired
1978
+ });
1979
+ if (refreshed && !ctrl.signal.aborted) {
1980
+ try {
1981
+ const retry = await generateNLPFilters({
1982
+ apiBase,
1983
+ token: refreshed,
1984
+ query: trimmed,
1985
+ signal: ctrl.signal
1986
+ });
1987
+ this.nlpState = { phase: "idle" };
1988
+ this.filters = {
1989
+ ...this.filters,
1990
+ q: retry.dsl || void 0,
1991
+ nlpQ: trimmed,
1992
+ nlpExplanation: retry.explanation,
1993
+ nlpUnsupported: retry.unsupported
1994
+ };
1995
+ this.persistFilters();
1996
+ this.render();
1997
+ this.restartEventsStore();
1998
+ return;
1999
+ } catch (retryErr) {
2000
+ this.nlpState = {
2001
+ phase: "error",
2002
+ reason: classifyNLPError(retryErr)
2003
+ };
2004
+ this.render();
2005
+ return;
2006
+ }
2007
+ }
2008
+ }
2009
+ this.nlpState = { phase: "error", reason: classifyNLPError(err) };
2010
+ this.render();
2011
+ }
2012
+ })();
2013
+ }
2014
+ // ---- rendering ----
2015
+ render() {
2016
+ this.bodySlot = null;
2017
+ this.replaceChildren(...this.renderChildren());
2018
+ }
2019
+ renderBody() {
2020
+ if (!this.bodySlot) return;
2021
+ this.bodySlot.replaceChildren(...this.renderBodyChildren());
2022
+ }
2023
+ renderChildren() {
2024
+ if (this.bootstrap.phase === "error" && this.bootstrap.reason === "config") {
2025
+ return [
2026
+ this.stateElement(
2027
+ "error",
2028
+ h(
2029
+ "span",
2030
+ null,
2031
+ "Configure ",
2032
+ h("code", null, "token"),
2033
+ ", ",
2034
+ h("code", null, "token-endpoint"),
2035
+ ", or set the ",
2036
+ h("code", null, "onTokenExpired"),
2037
+ " property."
2038
+ )
2039
+ )
2040
+ ];
2041
+ }
2042
+ if (this.bootstrap.phase === "error" && this.bootstrap.reason === "fetch") {
2043
+ const retry = h("button", { type: "button", class: "audit-trail-button" }, "Retry");
2044
+ retry.addEventListener("click", () => this.start());
2045
+ return [this.stateElement("error", h("span", null, "Couldn\u2019t fetch token."), retry)];
2046
+ }
2047
+ if (this.bootstrap.phase !== "ready") {
2048
+ return [this.stateElement("loading", "Loading\u2026")];
2049
+ }
2050
+ if (this.bootstrap.claims === null) {
2051
+ return [this.stateElement("error", "Invalid token.")];
2052
+ }
2053
+ const out = [this.renderToolbar()];
2054
+ const chips = renderFilterChips({
2055
+ value: this.filters,
2056
+ onChange: (next) => this.setFilters(next)
2057
+ });
2058
+ if (chips) out.push(chips);
2059
+ if (this.filtersOpen) {
2060
+ out.push(
2061
+ renderFiltersPanel({
2062
+ value: this.filters,
2063
+ distinct: this.distinctValues,
2064
+ claims: this.bootstrap.claims,
2065
+ activeTab: this.activeTab,
2066
+ nlpState: this.nlpState,
2067
+ onTabChange: (next) => this.setActiveTab(next),
2068
+ onChange: (next) => this.setFilters(next),
2069
+ onNLPSubmit: (q) => this.submitNLP(q),
2070
+ onApplied: () => {
2071
+ this.filtersOpen = false;
2072
+ this.render();
2073
+ }
2074
+ })
2075
+ );
2076
+ }
2077
+ const body = h("div", { class: "audit-trail-body" });
2078
+ body.replaceChildren(...this.renderBodyChildren());
2079
+ this.bodySlot = body;
2080
+ out.push(body);
2081
+ return out;
2082
+ }
2083
+ renderToolbar() {
2084
+ const livePollActive = this.pollIntervalAttr() > 0 && !resolveTimeBounds(this.filters).before;
2085
+ const live = livePollActive ? this.renderLiveIndicator() : null;
2086
+ const spacer = h("span", { class: "audit-trail-toolbar-spacer" });
2087
+ const filtersToggle = this.renderFiltersToggle();
2088
+ const exportBtn = h(
2089
+ "button",
2090
+ { type: "button", class: "audit-trail-button" },
2091
+ "Export"
2092
+ );
2093
+ exportBtn.addEventListener("click", () => this.openExport());
2094
+ const picker = this.renderColumnPicker();
2095
+ const children = [];
2096
+ if (live) children.push(live);
2097
+ children.push(spacer, filtersToggle, exportBtn, picker);
2098
+ return h("div", { class: "audit-trail-toolbar" }, ...children);
2099
+ }
2100
+ renderLiveIndicator() {
2101
+ return h(
2102
+ "span",
2103
+ {
2104
+ class: "audit-trail-live",
2105
+ "aria-label": "Live updates",
2106
+ title: "Live updates"
2107
+ },
2108
+ h("span", { class: "audit-trail-live-dot", "aria-hidden": "true" }),
2109
+ h("span", null, "Live")
2110
+ );
2111
+ }
2112
+ renderFiltersToggle() {
2113
+ const activeCount = countActiveColumnFilters(this.filters) + (this.filters.q ? parseQClauses(this.filters.q).length : 0);
2114
+ const children = [document.createTextNode("Search")];
2115
+ if (activeCount > 0) {
2116
+ children.push(
2117
+ h("span", { class: "audit-trail-filter-toggle-badge" }, String(activeCount))
2118
+ );
2119
+ }
2120
+ children.push(
2121
+ h(
2122
+ "span",
2123
+ { class: "audit-trail-filter-toggle-caret", "aria-hidden": "true" },
2124
+ this.filtersOpen ? "\u25B4" : "\u25BE"
2125
+ )
2126
+ );
2127
+ const btn = h(
2128
+ "button",
2129
+ {
2130
+ type: "button",
2131
+ class: "audit-trail-filter-toggle",
2132
+ "aria-expanded": this.filtersOpen ? "true" : "false"
2133
+ },
2134
+ ...children
2135
+ );
2136
+ btn.addEventListener("click", () => {
2137
+ this.filtersOpen = !this.filtersOpen;
2138
+ this.render();
2139
+ });
2140
+ return btn;
2141
+ }
2142
+ renderColumnPicker() {
2143
+ const available = this.availableColumns;
2144
+ const visibleCount = available.reduce(
2145
+ (n, c) => this.visibleSet.has(c) ? n + 1 : n,
2146
+ 0
2147
+ );
2148
+ const items = available.map((col) => {
2149
+ const checkbox = h("input", { type: "checkbox" });
2150
+ checkbox.checked = this.visibleSet.has(col);
2151
+ checkbox.addEventListener("change", () => this.toggleColumn(col));
2152
+ return h(
2153
+ "label",
2154
+ { class: "audit-trail-picker-item" },
2155
+ checkbox,
2156
+ h("span", null, COLUMN_LABELS[col] ?? col)
2157
+ );
2158
+ });
2159
+ return h(
2160
+ "details",
2161
+ { class: "audit-trail-picker" },
2162
+ h(
2163
+ "summary",
2164
+ { class: "audit-trail-picker-summary" },
2165
+ `Columns (${visibleCount}/${available.length})`
2166
+ ),
2167
+ h("div", { class: "audit-trail-picker-content" }, ...items)
2168
+ );
2169
+ }
2170
+ renderBodyChildren() {
2171
+ const events = this.eventsState?.events ?? [];
2172
+ const status = this.eventsState?.status ?? "loading";
2173
+ const hasMore = (this.eventsState?.nextCursor ?? null) !== null;
2174
+ const cols = this.visibleColumns;
2175
+ if (status === "loading" && events.length === 0) {
2176
+ return [this.stateElement("loading", "Loading\u2026")];
2177
+ }
2178
+ if (status === "expired") {
2179
+ return [this.stateElement("error", "Session expired.")];
2180
+ }
2181
+ if (status === "error") {
2182
+ const retry = h("button", { type: "button", class: "audit-trail-button" }, "Retry");
2183
+ retry.addEventListener("click", () => this.eventsStore?.refresh());
2184
+ return [this.stateElement("error", "Could not load events.", retry)];
2185
+ }
2186
+ if (events.length === 0) {
2187
+ return [this.stateElement("empty", "No events match.")];
2188
+ }
2189
+ if (cols.length === 0) {
2190
+ return [this.stateElement("empty", "No columns selected.")];
2191
+ }
2192
+ const out = [renderTable(events, cols, (event) => this.openDetail(event))];
2193
+ if (hasMore) {
2194
+ const more = h(
2195
+ "button",
2196
+ { type: "button", class: "audit-trail-button audit-trail-load-more" },
2197
+ "Load more"
2198
+ );
2199
+ more.addEventListener("click", () => this.eventsStore?.loadMore());
2200
+ out.push(more);
2201
+ }
2202
+ return out;
2203
+ }
2204
+ stateElement(kind, ...children) {
2205
+ return h("div", { class: `audit-trail-state audit-trail-state-${kind}` }, ...children);
2206
+ }
2207
+ };
2208
+ function triggerBrowserDownload(blob, filename) {
2209
+ const url = URL.createObjectURL(blob);
2210
+ const a = document.createElement("a");
2211
+ a.href = url;
2212
+ a.download = filename;
2213
+ document.body.appendChild(a);
2214
+ a.click();
2215
+ document.body.removeChild(a);
2216
+ setTimeout(() => URL.revokeObjectURL(url), 0);
2217
+ }
2218
+ function classifyNLPError(err) {
2219
+ if (!(err instanceof EmbedError)) return "unknown";
2220
+ if (err.status === 503) {
2221
+ if (err.message?.includes("busy")) return "provider_busy";
2222
+ return "not_configured";
2223
+ }
2224
+ if (err.status === 429) return "rate_limited";
2225
+ if (err.status === 403) return "not_allowed";
2226
+ if (err.kind === "bad_request") return "bad_request";
2227
+ return "unknown";
2228
+ }
2229
+ function exportErrorMessage(err) {
2230
+ switch (err.kind) {
2231
+ case "rate_limited":
2232
+ return "Too many requests. Try again in a moment.";
2233
+ case "bad_request":
2234
+ return err.message || "Bad request.";
2235
+ case "server":
2236
+ return "Server error. Please try again.";
2237
+ case "network":
2238
+ return "Network error. Check your connection.";
2239
+ case "not_found":
2240
+ return "Not found.";
2241
+ case "unauthorized":
2242
+ return "Authentication failed.";
2243
+ }
2244
+ }
2245
+
2246
+ // src/index.ts
2247
+ if (typeof customElements !== "undefined" && !customElements.get("audit-trail")) {
2248
+ customElements.define("audit-trail", AuditTrailElement);
2249
+ }
2250
+
2251
+ export { AuditTrailElement };
2252
+ //# sourceMappingURL=index.js.map
2253
+ //# sourceMappingURL=index.js.map