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