@solongate/proxy 0.61.0 → 0.62.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/tui/index.js CHANGED
@@ -124,9 +124,8 @@ function KeyHints({ hints }) {
124
124
  ] }, i)) });
125
125
  }
126
126
 
127
- // src/tui/panels/Policies.tsx
128
- import { Box as Box2, Text as Text2, useInput } from "ink";
129
- import { useEffect as useEffect2, useState as useState2 } from "react";
127
+ // src/tui/panels/Live.tsx
128
+ import { Box as Box2, Text as Text2 } from "ink";
130
129
 
131
130
  // src/api-client/client.ts
132
131
  import { readFileSync, existsSync } from "fs";
@@ -429,16 +428,113 @@ function usePoll(reload, intervalMs, enabled = true) {
429
428
  }, [reload, intervalMs, enabled]);
430
429
  }
431
430
 
432
- // src/tui/panels/Policies.tsx
431
+ // src/tui/panels/Live.tsx
433
432
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
433
+ function Tile({ label, value, color }) {
434
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginRight: 1, minWidth: 14, children: [
435
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: label }),
436
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color, children: value })
437
+ ] });
438
+ }
439
+ function LivePanel({ active: active2 }) {
440
+ const stats = useLoader(() => api.stats.get());
441
+ const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
442
+ const feed = useLoader(() => api.audit.list({ limit: 12 }));
443
+ usePoll(() => {
444
+ stats.reload();
445
+ ts.reload();
446
+ feed.reload();
447
+ }, 2e3, active2);
448
+ const s = stats.data;
449
+ const points = ts.data?.timeseries ?? [];
450
+ const entries = feed.data?.entries ?? [];
451
+ const denyRate = s && s.total_calls > 0 ? Math.round(s.denied / s.total_calls * 100) : 0;
452
+ return /* @__PURE__ */ jsx2(DataView, { loading: stats.loading && !s, error: stats.error, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
453
+ /* @__PURE__ */ jsxs2(Box2, { children: [
454
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "\u25CF" }),
455
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " LIVE \xB7 refreshing every 2s" })
456
+ ] }),
457
+ s ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
458
+ /* @__PURE__ */ jsx2(Tile, { label: "total calls", value: String(s.total_calls) }),
459
+ /* @__PURE__ */ jsx2(Tile, { label: "allowed", value: String(s.allowed), color: theme.ok }),
460
+ /* @__PURE__ */ jsx2(Tile, { label: "denied", value: String(s.denied), color: theme.bad }),
461
+ /* @__PURE__ */ jsx2(Tile, { label: "deny rate", value: denyRate + "%", color: denyRate > 10 ? theme.bad : void 0 }),
462
+ /* @__PURE__ */ jsx2(Tile, { label: "policies", value: String(s.active_policies), color: theme.accent })
463
+ ] }) : null,
464
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
465
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "Activity \xB7 24h" }),
466
+ /* @__PURE__ */ jsxs2(Box2, { children: [
467
+ /* @__PURE__ */ jsx2(Text2, { children: "calls " }),
468
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sparkline(points.map((p) => p.total), 60) })
469
+ ] }),
470
+ /* @__PURE__ */ jsxs2(Box2, { children: [
471
+ /* @__PURE__ */ jsx2(Text2, { children: "denied " }),
472
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 60) })
473
+ ] })
474
+ ] }),
475
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
476
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "Live feed" }),
477
+ entries.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " waiting for tool calls\u2026" }) : entries.slice(0, 10).map((e) => /* @__PURE__ */ jsxs2(Box2, { children: [
478
+ /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), children: e.decision.padEnd(8) }),
479
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool_name, 16).padEnd(17) }),
480
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate(e.agent_name ?? "\u2014", 14).padEnd(15) }),
481
+ e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP " }) : /* @__PURE__ */ jsx2(Text2, { children: " " }),
482
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
483
+ ago(e.created_at),
484
+ " ago"
485
+ ] })
486
+ ] }, e.id))
487
+ ] })
488
+ ] }) });
489
+ }
490
+
491
+ // src/tui/panels/Policies.tsx
492
+ import { Box as Box3, Text as Text3, useInput } from "ink";
493
+ import TextInput from "ink-text-input";
494
+ import { useEffect as useEffect2, useState as useState2 } from "react";
495
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
496
+ var constr = (r, g, side) => (r[g]?.[side] ?? []).join(", ");
497
+ var setConstr = (r, g, side, val) => {
498
+ const arr = val.split(",").map((s) => s.trim()).filter(Boolean);
499
+ const prev = r[g] ?? {};
500
+ return { ...r, [g]: { ...prev, [side]: arr } };
501
+ };
502
+ var FIELDS = [
503
+ { label: "Effect", kind: "effect", get: (r) => r.effect },
504
+ { label: "Enabled", kind: "enabled", get: (r) => r.enabled ? "yes" : "no" },
505
+ { label: "Priority", kind: "priority", get: (r) => String(r.priority) },
506
+ { label: "Tool pattern", kind: "text", get: (r) => r.toolPattern, set: (r, v) => ({ ...r, toolPattern: v || "*" }) },
507
+ { label: "Description", kind: "text", get: (r) => r.description ?? "", set: (r, v) => ({ ...r, description: v }) },
508
+ { label: "Cmd allow", kind: "text", get: (r) => constr(r, "commandConstraints", "allowed"), set: (r, v) => setConstr(r, "commandConstraints", "allowed", v) },
509
+ { label: "Cmd deny", kind: "text", get: (r) => constr(r, "commandConstraints", "denied"), set: (r, v) => setConstr(r, "commandConstraints", "denied", v) },
510
+ { label: "Path allow", kind: "text", get: (r) => constr(r, "pathConstraints", "allowed"), set: (r, v) => setConstr(r, "pathConstraints", "allowed", v) },
511
+ { label: "Path deny", kind: "text", get: (r) => constr(r, "pathConstraints", "denied"), set: (r, v) => setConstr(r, "pathConstraints", "denied", v) },
512
+ { label: "File allow", kind: "text", get: (r) => constr(r, "filenameConstraints", "allowed"), set: (r, v) => setConstr(r, "filenameConstraints", "allowed", v) },
513
+ { label: "File deny", kind: "text", get: (r) => constr(r, "filenameConstraints", "denied"), set: (r, v) => setConstr(r, "filenameConstraints", "denied", v) },
514
+ { label: "URL allow", kind: "text", get: (r) => constr(r, "urlConstraints", "allowed"), set: (r, v) => setConstr(r, "urlConstraints", "allowed", v) },
515
+ { label: "URL deny", kind: "text", get: (r) => constr(r, "urlConstraints", "denied"), set: (r, v) => setConstr(r, "urlConstraints", "denied", v) }
516
+ ];
517
+ function ruleSummary(r) {
518
+ const bits = [];
519
+ for (const [g, tag] of [["commandConstraints", "cmd"], ["pathConstraints", "path"], ["filenameConstraints", "file"], ["urlConstraints", "url"]]) {
520
+ const c2 = r[g];
521
+ const n = (c2?.allowed?.length ?? 0) + (c2?.denied?.length ?? 0);
522
+ if (n) bits.push(`${tag}:${n}`);
523
+ }
524
+ return bits.join(" ");
525
+ }
434
526
  function PoliciesPanel({ focused }) {
435
527
  const list3 = useLoader(() => api.policies.list());
436
528
  const policies = list3.data?.policies ?? [];
437
529
  const [view, setView] = useState2("list");
438
530
  const [pi, setPi] = useState2(0);
439
531
  const [ri, setRi] = useState2(0);
440
- const [rules, setRules] = useState2(null);
532
+ const [fi, setFi] = useState2(0);
533
+ const [rules, setRules] = useState2([]);
441
534
  const [mode, setMode] = useState2("denylist");
535
+ const [dirty, setDirty] = useState2(false);
536
+ const [editing, setEditing] = useState2(false);
537
+ const [editVal, setEditVal] = useState2("");
442
538
  const [status, setStatus] = useState2(null);
443
539
  const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
444
540
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
@@ -446,28 +542,44 @@ function PoliciesPanel({ focused }) {
446
542
  if (detail.data) {
447
543
  setRules(detail.data.rules);
448
544
  setMode(detail.data.mode ?? "denylist");
545
+ setDirty(false);
449
546
  setRi(0);
450
547
  }
451
548
  }, [detail.data]);
452
- const persist = async (nextRules, nextMode) => {
453
- if (!detail.data || !selected) return;
549
+ const mutate = (nextRules, nextMode = mode) => {
550
+ setRules(nextRules);
551
+ setMode(nextMode);
552
+ setDirty(true);
553
+ setStatus(null);
554
+ };
555
+ const save = async () => {
556
+ if (!selected || !detail.data) return;
454
557
  setStatus("Saving\u2026");
455
558
  try {
456
559
  const res = await api.policies.update(selected.id, {
457
560
  id: selected.id,
458
561
  name: detail.data.name,
459
562
  description: detail.data.description,
460
- mode: nextMode,
563
+ mode,
461
564
  agents: detail.data.agents,
462
- rules: nextRules
565
+ rules
463
566
  });
464
567
  setRules(res.rules);
568
+ setDirty(false);
465
569
  setStatus("\u2713 Saved v" + res._version);
466
570
  list3.reload();
467
571
  } catch (e) {
468
572
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
469
573
  }
470
574
  };
575
+ const discard = () => {
576
+ if (detail.data) {
577
+ setRules(detail.data.rules);
578
+ setMode(detail.data.mode ?? "denylist");
579
+ }
580
+ setDirty(false);
581
+ setStatus("discarded");
582
+ };
471
583
  useInput(
472
584
  (input, key) => {
473
585
  if (view === "list") {
@@ -475,44 +587,61 @@ function PoliciesPanel({ focused }) {
475
587
  else if (key.downArrow) setPi((n) => Math.min(policies.length - 1, n + 1));
476
588
  else if (key.return || key.rightArrow) {
477
589
  setStatus(null);
478
- setView("detail");
590
+ setView("rules");
479
591
  }
480
592
  return;
481
593
  }
482
- const rs2 = rules ?? [];
483
- if (key.leftArrow) {
484
- setView("list");
485
- setStatus(null);
486
- } else if (key.upArrow) setRi((n) => Math.max(0, n - 1));
487
- else if (key.downArrow) setRi((n) => Math.min(rs2.length - 1, n + 1));
488
- else if (input === " " || input === "e") {
489
- if (!rs2[ri]) return;
490
- const next = rs2.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r);
491
- setRules(next);
492
- void persist(next, mode);
493
- } else if (input === "d") {
494
- const target = rs2[ri];
495
- if (!target || !selected) return;
496
- setStatus("Deleting\u2026");
497
- void api.policies.revokeRule(selected.id, target.id).then(() => {
498
- setStatus("\u2713 Deleted");
499
- detail.reload();
500
- list3.reload();
501
- }).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
502
- } else if (input === "m") {
503
- const nm = mode === "denylist" ? "whitelist" : "denylist";
504
- setMode(nm);
505
- void persist(rs2, nm);
594
+ if (view === "rules") {
595
+ if (key.leftArrow) return setView("list"), void setStatus(null);
596
+ if (key.upArrow) setRi((n) => Math.max(0, n - 1));
597
+ else if (key.downArrow) setRi((n) => Math.min(rules.length - 1, n + 1));
598
+ else if (key.return || key.rightArrow) {
599
+ if (rules[ri]) {
600
+ setFi(0);
601
+ setView("rule");
602
+ }
603
+ } else if (input === " ") {
604
+ if (rules[ri]) mutate(rules.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r));
605
+ } else if (input === "e") {
606
+ if (rules[ri]) mutate(rules.map((r, i) => i === ri ? { ...r, effect: r.effect === "ALLOW" ? "DENY" : "ALLOW" } : r));
607
+ } else if (input === "d") {
608
+ if (rules[ri]) {
609
+ const next = rules.filter((_, i) => i !== ri);
610
+ setRi((n) => Math.max(0, Math.min(n, next.length - 1)));
611
+ mutate(next);
612
+ }
613
+ } else if (input === "m") {
614
+ mutate(rules, mode === "denylist" ? "whitelist" : "denylist");
615
+ } else if (input === "s") void save();
616
+ else if (input === "x") discard();
617
+ return;
506
618
  }
619
+ const rule2 = rules[ri];
620
+ if (!rule2) return setView("rules");
621
+ const field = FIELDS[fi];
622
+ if (key.leftArrow) setView("rules");
623
+ else if (key.upArrow) setFi((n) => Math.max(0, n - 1));
624
+ else if (key.downArrow) setFi((n) => Math.min(FIELDS.length - 1, n + 1));
625
+ else if (field.kind === "effect" && (input === " " || key.rightArrow)) {
626
+ mutate(rules.map((r, i) => i === ri ? { ...r, effect: r.effect === "ALLOW" ? "DENY" : "ALLOW" } : r));
627
+ } else if (field.kind === "enabled" && (input === " " || key.rightArrow)) {
628
+ mutate(rules.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r));
629
+ } else if (field.kind === "priority" && (key.rightArrow || key.leftArrow)) {
630
+ const d = key.rightArrow ? 1 : -1;
631
+ mutate(rules.map((r, i) => i === ri ? { ...r, priority: Math.max(0, r.priority + d) } : r));
632
+ } else if (field.kind === "text" && key.return) {
633
+ setEditVal(field.get(rule2));
634
+ setEditing(true);
635
+ } else if (input === "s") void save();
507
636
  },
508
- { isActive: focused }
637
+ { isActive: focused && !editing }
509
638
  );
510
639
  if (view === "list") {
511
- return /* @__PURE__ */ jsx2(DataView, { loading: list3.loading && !list3.data, error: list3.error, empty: !!list3.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
512
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: focused ? "\u2191\u2193 select \xB7 enter open" : "press \u2192 to browse" }),
513
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === pi ? theme.accentBright : void 0, bold: i === pi, children: [
640
+ return /* @__PURE__ */ jsx3(DataView, { loading: list3.loading && !list3.data, error: list3.error, empty: !!list3.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
641
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 select \xB7 enter open" : "press \u2192 to browse" }),
642
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs3(Text3, { color: i === pi ? theme.accentBright : void 0, bold: i === pi, children: [
514
643
  (i === pi ? "\u25B8 " : " ") + truncate(p.name, 26).padEnd(27),
515
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
644
+ /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
516
645
  p.mode.padEnd(10),
517
646
  " ",
518
647
  p.rules.length,
@@ -522,44 +651,94 @@ function PoliciesPanel({ focused }) {
522
651
  ] }, p.id)) })
523
652
  ] }) });
524
653
  }
525
- const rs = rules ?? [];
526
- return /* @__PURE__ */ jsx2(DataView, { loading: detail.loading && !rules, error: detail.error, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
527
- /* @__PURE__ */ jsxs2(Box2, { children: [
528
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.accentBright, children: truncate(selected?.name ?? "", 30) }),
529
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " mode: " }),
530
- /* @__PURE__ */ jsx2(Text2, { color: mode === "whitelist" ? theme.ok : void 0, children: mode })
654
+ const dirtyTag = dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null;
655
+ if (view === "rules") {
656
+ return /* @__PURE__ */ jsx3(DataView, { loading: detail.loading && !detail.data, error: detail.error, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
657
+ /* @__PURE__ */ jsxs3(Box3, { children: [
658
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: truncate(selected?.name ?? "", 28) }),
659
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " mode: " }),
660
+ /* @__PURE__ */ jsx3(Text3, { color: mode === "whitelist" ? theme.warn : void 0, children: mode }),
661
+ dirtyTag
662
+ ] }),
663
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 rule \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 d delete \xB7 m mode \xB7 s save \xB7 \u2190 back" }),
664
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
665
+ Table,
666
+ {
667
+ columns: [
668
+ { header: "", width: 2 },
669
+ { header: "ON", width: 2 },
670
+ { header: "EFFECT", width: 7 },
671
+ { header: "PRIO", width: 4 },
672
+ { header: "TOOL", width: 20 },
673
+ { header: "CONSTRAINTS", width: 22 }
674
+ ],
675
+ rows: rules.map((r, i) => [
676
+ { value: i === ri ? "\u25B8" : "", color: theme.accentBright },
677
+ { value: r.enabled ? "\u25CF" : "\u25CB", color: r.enabled ? theme.ok : theme.dim },
678
+ { value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad, dim: !r.enabled },
679
+ { value: String(r.priority), dim: true },
680
+ { value: truncate(r.toolPattern, 20), color: theme.accent, dim: !r.enabled },
681
+ { value: ruleSummary(r) || truncate(r.description || "\u2014", 22), dim: true }
682
+ ])
683
+ }
684
+ ) }),
685
+ rules.length === 0 ? /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "(no rules)" }) : null,
686
+ status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
687
+ ] }) });
688
+ }
689
+ const rule = rules[ri];
690
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
691
+ /* @__PURE__ */ jsxs3(Box3, { children: [
692
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: "Rule " }),
693
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: truncate(rule?.id ?? "", 26) }),
694
+ dirtyTag
531
695
  ] }),
532
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2191\u2193 rule \xB7 space toggle \xB7 d delete \xB7 m mode \xB7 \u2190 back" }),
533
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(
534
- Table,
535
- {
536
- columns: [
537
- { header: "", width: 2 },
538
- { header: "EFFECT", width: 7 },
539
- { header: "PRIO", width: 4 },
540
- { header: "TOOL", width: 20 },
541
- { header: "DESCRIPTION", width: 34 }
542
- ],
543
- rows: rs.map((r, i) => [
544
- { value: i === ri ? "\u25B8" : "", color: theme.accentBright },
545
- { value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad, dim: !r.enabled },
546
- { value: String(r.priority), dim: true },
547
- { value: (r.enabled ? "" : "\u2298 ") + truncate(r.toolPattern, 18), color: theme.accent, dim: !r.enabled },
548
- { value: truncate(r.description || "\u2014", 34), dim: !r.enabled }
549
- ])
550
- }
551
- ) }),
552
- rs.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "(no rules)" }) : null,
553
- status ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
554
- ] }) });
696
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 field \xB7 enter edit text \xB7 space/\u2192 toggle \xB7 s save \xB7 \u2190 back" }),
697
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: FIELDS.map((f, i) => {
698
+ const val = rule ? f.get(rule) : "";
699
+ const active2 = i === fi;
700
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
701
+ /* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + f.label.padEnd(13) }),
702
+ active2 && editing && f.kind === "text" ? /* @__PURE__ */ jsx3(
703
+ TextInput,
704
+ {
705
+ value: editVal,
706
+ onChange: setEditVal,
707
+ onSubmit: (v) => {
708
+ if (rule && f.set) mutate(rules.map((r, idx) => idx === ri ? f.set(r, v) : r));
709
+ setEditing(false);
710
+ }
711
+ }
712
+ ) : /* @__PURE__ */ jsx3(
713
+ Text3,
714
+ {
715
+ color: f.kind === "effect" ? val === "ALLOW" ? theme.ok : theme.bad : void 0,
716
+ dimColor: !val,
717
+ children: val || "\u2014"
718
+ }
719
+ )
720
+ ] }, f.label);
721
+ }) }),
722
+ status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
723
+ ] });
555
724
  }
556
725
 
557
726
  // src/tui/panels/RateLimit.tsx
558
- import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
727
+ import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
559
728
  import { useEffect as useEffect3, useState as useState3 } from "react";
560
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
729
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
561
730
  var MODES = ["off", "detect", "block"];
562
- var FIELDS = ["mode", "perMinute", "perHour", "perDay"];
731
+ var FIELDS2 = ["mode", "perMinute", "perHour", "perDay"];
732
+ var BLOCKS2 = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
733
+ function ColoredSpark({ values, limit, width = 60 }) {
734
+ const v = values.length > width ? values.slice(values.length - width) : values;
735
+ const max = Math.max(1, ...v);
736
+ return /* @__PURE__ */ jsx4(Text4, { children: v.map((n, i) => {
737
+ const ch = BLOCKS2[Math.min(BLOCKS2.length - 1, Math.round(n / max * (BLOCKS2.length - 1)))];
738
+ const over = limit > 0 && n > limit;
739
+ return /* @__PURE__ */ jsx4(Text4, { color: over ? theme.bad : theme.accent, children: ch }, i);
740
+ }) });
741
+ }
563
742
  function RateLimitPanel({ focused }) {
564
743
  const layersQ = useLoader(() => api.settings.getSecurityLayers());
565
744
  const historyQ = useLoader(() => api.settings.getRateLimitHistory());
@@ -573,7 +752,7 @@ function RateLimitPanel({ focused }) {
573
752
  }, [layersQ.data, draft]);
574
753
  const adjust = (dir, step) => {
575
754
  if (!draft) return;
576
- const field = FIELDS[fi];
755
+ const field = FIELDS2[fi];
577
756
  if (field === "mode") {
578
757
  const idx = (MODES.indexOf(draft.mode) + dir + MODES.length) % MODES.length;
579
758
  setDraft({ ...draft, mode: MODES[idx] });
@@ -587,8 +766,7 @@ function RateLimitPanel({ focused }) {
587
766
  if (!draft || !layersQ.data) return;
588
767
  setStatus("Saving\u2026");
589
768
  try {
590
- const next = { ...layersQ.data.layers, rateLimit: draft };
591
- const res = await api.settings.setSecurityLayers(next);
769
+ const res = await api.settings.setSecurityLayers({ ...layersQ.data.layers, rateLimit: draft });
592
770
  setDraft({ ...res.layers.rateLimit });
593
771
  setDirty(false);
594
772
  setStatus("\u2713 Saved");
@@ -600,8 +778,8 @@ function RateLimitPanel({ focused }) {
600
778
  useInput2(
601
779
  (input, key) => {
602
780
  const step = key.shift ? 10 : 1;
603
- if (key.upArrow) setFi((n) => (n - 1 + FIELDS.length) % FIELDS.length);
604
- else if (key.downArrow) setFi((n) => (n + 1) % FIELDS.length);
781
+ if (key.upArrow) setFi((n) => (n - 1 + FIELDS2.length) % FIELDS2.length);
782
+ else if (key.downArrow) setFi((n) => (n + 1) % FIELDS2.length);
605
783
  else if (key.leftArrow) adjust(-1, step);
606
784
  else if (key.rightArrow) adjust(1, step);
607
785
  else if (input === "s") void save();
@@ -609,155 +787,206 @@ function RateLimitPanel({ focused }) {
609
787
  { isActive: focused }
610
788
  );
611
789
  const history = (historyQ.data?.history ?? []).slice().sort((a, b) => b.ts - a.ts);
612
- const bursts = (insightsQ.data?.["anomalies"] ?? []).slice(0, 6);
613
- return /* @__PURE__ */ jsx3(DataView, { loading: layersQ.loading && !draft, error: layersQ.error, children: draft ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
614
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 field \xB7 \u2190\u2192 \xB11 \xB7 shift+\u2190\u2192 \xB110 \xB7 s save" : "press \u2192 to edit" }),
615
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
616
- /* @__PURE__ */ jsx3(FieldRow, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx3(Text3, { color: modeColor(draft.mode), children: draft.mode }) }),
617
- /* @__PURE__ */ jsx3(FieldRow, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perMinute }) }),
618
- /* @__PURE__ */ jsx3(FieldRow, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
619
- /* @__PURE__ */ jsx3(FieldRow, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
790
+ const activity = insightsQ.data?.["activity"] ?? {};
791
+ const minuteSeries = (activity.minute ?? []).map((b) => b.count);
792
+ const hourSeries = (activity.hour ?? []).map((b) => b.count);
793
+ const bursts = minuteSeries.filter((c2) => draft && draft.perMinute > 0 && c2 > draft.perMinute).length;
794
+ return /* @__PURE__ */ jsx4(DataView, { loading: layersQ.loading && !draft, error: layersQ.error, children: draft ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
795
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: focused ? "\u2191\u2193 field \xB7 \u2190\u2192 \xB11 \xB7 shift+\u2190\u2192 \xB110 \xB7 s save" : "press \u2192 to edit" }),
796
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
797
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx4(Text4, { color: modeColor(draft.mode), children: draft.mode }) }),
798
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perMinute }) }),
799
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
800
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
620
801
  ] }),
621
- dirty || status ? /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
622
- dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: "\u25CF unsaved \u2014 press s to apply " }) : null,
623
- status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
802
+ dirty || status ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
803
+ dirty ? /* @__PURE__ */ jsx4(Text4, { color: theme.warn, children: "\u25CF unsaved \u2014 press s to apply " }) : null,
804
+ status ? /* @__PURE__ */ jsx4(Text4, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
624
805
  ] }) : null,
625
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
626
- /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
806
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
807
+ /* @__PURE__ */ jsxs4(Box4, { children: [
808
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "Activity " }),
809
+ bursts > 0 ? /* @__PURE__ */ jsxs4(Text4, { color: theme.bad, children: [
810
+ bursts,
811
+ " burst min"
812
+ ] }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no bursts" }),
813
+ insightsQ.loading && !insightsQ.data ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: " \u2026" }) : null
814
+ ] }),
815
+ /* @__PURE__ */ jsxs4(Box4, { children: [
816
+ /* @__PURE__ */ jsx4(Text4, { children: "/min " }),
817
+ minuteSeries.length ? /* @__PURE__ */ jsx4(ColoredSpark, { values: minuteSeries, limit: draft.perMinute }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no data" })
818
+ ] }),
819
+ /* @__PURE__ */ jsxs4(Box4, { children: [
820
+ /* @__PURE__ */ jsx4(Text4, { children: "/hour " }),
821
+ hourSeries.length ? /* @__PURE__ */ jsx4(ColoredSpark, { values: hourSeries, limit: draft.perHour, width: 24 }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no data" })
822
+ ] })
823
+ ] }),
824
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
825
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
627
826
  "Change history ",
628
827
  history.length ? `(${history.length})` : ""
629
828
  ] }),
630
- history.length ? /* @__PURE__ */ jsx3(
829
+ history.length ? /* @__PURE__ */ jsx4(
631
830
  Table,
632
831
  {
633
832
  columns: [
634
- { header: "WHEN", width: 8 },
833
+ { header: "WHEN", width: 10 },
635
834
  { header: "/MIN", width: 6 },
636
835
  { header: "/HOUR", width: 7 },
637
836
  { header: "/DAY", width: 6 }
638
837
  ],
639
- rows: history.slice(0, 5).map((h, i) => [
838
+ rows: history.slice(0, 4).map((h, i) => [
640
839
  { value: ago(h.ts) + " ago", dim: true, bold: i === 0 },
641
840
  { value: String(h.minute), bold: i === 0 },
642
841
  { value: h.hour === 0 ? "off" : String(h.hour), dim: h.hour === 0 },
643
842
  { value: h.day === 0 ? "off" : String(h.day), dim: h.day === 0 }
644
843
  ])
645
844
  }
646
- ) : /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " no changes" })
647
- ] }),
648
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
649
- /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
650
- "Recent bursts ",
651
- insightsQ.loading && !insightsQ.data ? "\u2026" : `(${bursts.length})`
652
- ] }),
653
- bursts.length ? /* @__PURE__ */ jsx3(
654
- Table,
655
- {
656
- columns: [
657
- { header: "RESULT", width: 9 },
658
- { header: "AGENT", width: 16 },
659
- { header: "CALLS/LIMIT", width: 12 },
660
- { header: "WHEN", width: 8 }
661
- ],
662
- rows: bursts.map((b) => [
663
- { value: b.blocked ? "blocked" : "BYPASSED", color: b.blocked ? theme.accent : theme.bad, bold: !b.blocked },
664
- { value: (b.agent || "\u2014").slice(0, 16), dim: true },
665
- { value: `${b.count}/${b.limit}` },
666
- { value: ago(b.minute) + " ago", dim: true }
667
- ])
668
- }
669
- ) : /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
670
- " ",
671
- insightsQ.error ? insightsQ.error : "none in 7d"
672
- ] })
845
+ ) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: " no changes" })
673
846
  ] })
674
847
  ] }) : null });
675
848
  }
676
849
  function FieldRow({ label, active: active2, children }) {
677
- return /* @__PURE__ */ jsxs3(Box3, { children: [
678
- /* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + label.padEnd(12) }),
850
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
851
+ /* @__PURE__ */ jsx4(Text4, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + label.padEnd(12) }),
679
852
  children
680
853
  ] });
681
854
  }
682
855
 
683
856
  // src/tui/panels/Dlp.tsx
684
- import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
857
+ import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
858
+ import TextInput2 from "ink-text-input";
685
859
  import { useEffect as useEffect4, useState as useState4 } from "react";
686
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
860
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
687
861
  var MODES2 = ["off", "detect", "block"];
688
862
  function DlpPanel({ focused }) {
689
863
  const q = useLoader(() => api.settings.getSecurityLayers());
690
864
  const [dlp, setDlp] = useState4(null);
691
865
  const [available, setAvailable] = useState4([]);
692
866
  const [sel, setSel] = useState4(0);
867
+ const [dirty, setDirty] = useState4(false);
693
868
  const [status, setStatus] = useState4(null);
869
+ const [adding, setAdding] = useState4(null);
870
+ const [newName, setNewName] = useState4("");
871
+ const [newRe, setNewRe] = useState4("");
694
872
  useEffect4(() => {
695
873
  if (q.data && !dlp) {
696
- setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns] });
874
+ setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
697
875
  setAvailable(q.data.availablePatterns);
698
876
  }
699
877
  }, [q.data, dlp]);
700
- const persist = async (nextDlp) => {
701
- if (!q.data) return;
702
- setDlp(nextDlp);
878
+ const mutate = (next) => {
879
+ setDlp(next);
880
+ setDirty(true);
881
+ setStatus(null);
882
+ };
883
+ const save = async () => {
884
+ if (!dlp || !q.data) return;
703
885
  setStatus("Saving\u2026");
704
886
  try {
705
- const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp: nextDlp });
706
- setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns] });
887
+ const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp });
888
+ setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns], custom: [...res.layers.dlp.custom] });
889
+ setDirty(false);
707
890
  setStatus("\u2713 Saved");
708
891
  } catch (e) {
709
892
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
710
893
  }
711
894
  };
895
+ const discard = () => {
896
+ if (q.data) setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
897
+ setDirty(false);
898
+ setStatus("discarded");
899
+ };
900
+ const customCount = dlp?.custom.length ?? 0;
901
+ const total = available.length + customCount;
712
902
  useInput3(
713
903
  (input, key) => {
714
904
  if (!dlp) return;
715
905
  if (key.upArrow) setSel((n) => Math.max(0, n - 1));
716
- else if (key.downArrow) setSel((n) => Math.min(available.length - 1, n + 1));
717
- else if (input === " " || key.return) {
906
+ else if (key.downArrow) setSel((n) => Math.min(total - 1, n + 1));
907
+ else if (input === "m") {
908
+ const idx = (MODES2.indexOf(dlp.mode) + 1) % MODES2.length;
909
+ mutate({ ...dlp, mode: MODES2[idx] });
910
+ } else if (input === "a") {
911
+ setNewName("");
912
+ setNewRe("");
913
+ setAdding("name");
914
+ } else if (input === " " && sel < available.length) {
718
915
  const p = available[sel];
719
- if (!p) return;
720
916
  const set = new Set(dlp.patterns);
721
917
  if (set.has(p)) set.delete(p);
722
918
  else set.add(p);
723
- void persist({ ...dlp, patterns: [...set] });
724
- } else if (input === "m") {
725
- const idx = (MODES2.indexOf(dlp.mode) + 1) % MODES2.length;
726
- void persist({ ...dlp, mode: MODES2[idx] });
727
- }
919
+ mutate({ ...dlp, patterns: [...set] });
920
+ } else if (input === "d" && sel >= available.length) {
921
+ const ci = sel - available.length;
922
+ mutate({ ...dlp, custom: dlp.custom.filter((_, i) => i !== ci) });
923
+ setSel((n) => Math.max(0, n - 1));
924
+ } else if (input === "s") void save();
925
+ else if (input === "x") discard();
728
926
  },
729
- { isActive: focused }
927
+ { isActive: focused && !adding }
730
928
  );
731
929
  const enabled = new Set(dlp?.patterns ?? []);
732
- return /* @__PURE__ */ jsx4(DataView, { loading: q.loading && !dlp, error: q.error, children: dlp ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
733
- /* @__PURE__ */ jsxs4(Box4, { children: [
734
- /* @__PURE__ */ jsx4(Text4, { children: "mode: " }),
735
- /* @__PURE__ */ jsx4(Text4, { color: modeColor(dlp.mode), children: dlp.mode }),
736
- /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: focused ? " \u2191\u2193 move \xB7 space toggle \xB7 m mode" : " press \u2192 to edit" })
930
+ return /* @__PURE__ */ jsx5(DataView, { loading: q.loading && !dlp, error: q.error, children: dlp ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
931
+ /* @__PURE__ */ jsxs5(Box5, { children: [
932
+ /* @__PURE__ */ jsx5(Text5, { children: "mode: " }),
933
+ /* @__PURE__ */ jsx5(Text5, { color: modeColor(dlp.mode), children: dlp.mode }),
934
+ dirty ? /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null
737
935
  ] }),
738
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
936
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: focused ? "\u2191\u2193 move \xB7 space toggle \xB7 m mode \xB7 a add \xB7 d remove \xB7 s save" : "press \u2192 to edit" }),
937
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
739
938
  const on = enabled.has(p);
740
- return /* @__PURE__ */ jsxs4(Text4, { color: focused && i === sel ? theme.accentBright : void 0, bold: focused && i === sel, children: [
741
- (focused && i === sel ? "\u25B8 " : " ") + (on ? "\u25CF " : "\u25CB "),
742
- /* @__PURE__ */ jsx4(Text4, { color: on ? theme.ok : theme.dim, children: p })
939
+ const active2 = focused && sel === i;
940
+ return /* @__PURE__ */ jsxs5(Text5, { color: active2 ? theme.accentBright : void 0, bold: active2, children: [
941
+ (active2 ? "\u25B8 " : " ") + (on ? "\u25CF " : "\u25CB "),
942
+ /* @__PURE__ */ jsx5(Text5, { color: on ? theme.ok : theme.dim, children: p })
743
943
  ] }, p);
744
944
  }) }),
745
- dlp.custom.length ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
746
- /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "Custom:" }),
747
- dlp.custom.map((c2) => /* @__PURE__ */ jsxs4(Text4, { children: [
748
- " ",
749
- /* @__PURE__ */ jsx4(Text4, { color: theme.accent, children: c2.name }),
750
- " ",
751
- /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: c2.re })
752
- ] }, c2.name))
945
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
946
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "Custom patterns" }),
947
+ dlp.custom.length === 0 ? /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " (none \u2014 press a to add)" }) : null,
948
+ dlp.custom.map((c2, i) => {
949
+ const active2 = focused && sel === available.length + i;
950
+ return /* @__PURE__ */ jsxs5(Text5, { color: active2 ? theme.accentBright : void 0, bold: active2, children: [
951
+ active2 ? "\u25B8 " : " ",
952
+ /* @__PURE__ */ jsx5(Text5, { color: theme.accent, children: c2.name }),
953
+ " ",
954
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: c2.re })
955
+ ] }, c2.name + i);
956
+ })
957
+ ] }),
958
+ adding ? /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
959
+ /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: adding === "name" ? "New pattern name: " : `Regex for "${newName}": ` }),
960
+ adding === "name" ? /* @__PURE__ */ jsx5(
961
+ TextInput2,
962
+ {
963
+ value: newName,
964
+ onChange: setNewName,
965
+ onSubmit: (v) => {
966
+ if (!v.trim()) return setAdding(null);
967
+ setNewName(v.trim());
968
+ setAdding("re");
969
+ }
970
+ }
971
+ ) : /* @__PURE__ */ jsx5(
972
+ TextInput2,
973
+ {
974
+ value: newRe,
975
+ onChange: setNewRe,
976
+ onSubmit: (v) => {
977
+ if (v.trim() && dlp) mutate({ ...dlp, custom: [...dlp.custom, { name: newName, re: v.trim() }] });
978
+ setAdding(null);
979
+ }
980
+ }
981
+ )
753
982
  ] }) : null,
754
- status ? /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
983
+ status ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
755
984
  ] }) : null });
756
985
  }
757
986
 
758
987
  // src/tui/panels/Stats.tsx
759
- import { Box as Box5, Text as Text5 } from "ink";
760
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
988
+ import { Box as Box6, Text as Text6 } from "ink";
989
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
761
990
  function StatsPanel({ active: active2 }) {
762
991
  const stats = useLoader(() => api.stats.get());
763
992
  const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
@@ -767,50 +996,50 @@ function StatsPanel({ active: active2 }) {
767
996
  }, 5e3, active2);
768
997
  const s = stats.data;
769
998
  const points = ts.data?.timeseries ?? [];
770
- return /* @__PURE__ */ jsx5(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
771
- /* @__PURE__ */ jsxs5(Box5, { children: [
772
- /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
999
+ return /* @__PURE__ */ jsx6(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1000
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1001
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
773
1002
  s.total_calls,
774
1003
  " "
775
1004
  ] }),
776
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "calls " }),
777
- /* @__PURE__ */ jsxs5(Text5, { color: theme.ok, children: [
1005
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: "calls " }),
1006
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.ok, children: [
778
1007
  s.allowed,
779
1008
  " allowed"
780
1009
  ] }),
781
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " " }),
782
- /* @__PURE__ */ jsxs5(Text5, { color: theme.bad, children: [
1010
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: " " }),
1011
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.bad, children: [
783
1012
  s.denied,
784
1013
  " denied"
785
1014
  ] })
786
1015
  ] }),
787
- /* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
1016
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
788
1017
  s.active_policies,
789
1018
  " policies \xB7 ",
790
1019
  s.registered_tools,
791
1020
  " tools"
792
1021
  ] }),
793
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
794
- /* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
1022
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
1023
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
795
1024
  "Last 24h ",
796
1025
  ts.data ? `(per ${ts.data.granularity})` : ""
797
1026
  ] }),
798
- /* @__PURE__ */ jsxs5(Box5, { children: [
799
- /* @__PURE__ */ jsx5(Text5, { children: "total " }),
800
- /* @__PURE__ */ jsx5(Text5, { color: theme.accent, children: sparkline(points.map((p) => p.total), 64) })
1027
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1028
+ /* @__PURE__ */ jsx6(Text6, { children: "total " }),
1029
+ /* @__PURE__ */ jsx6(Text6, { color: theme.accent, children: sparkline(points.map((p) => p.total), 64) })
801
1030
  ] }),
802
- /* @__PURE__ */ jsxs5(Box5, { children: [
803
- /* @__PURE__ */ jsx5(Text5, { children: "allowed " }),
804
- /* @__PURE__ */ jsx5(Text5, { color: theme.ok, children: sparkline(points.map((p) => p.allowed), 64) })
1031
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1032
+ /* @__PURE__ */ jsx6(Text6, { children: "allowed " }),
1033
+ /* @__PURE__ */ jsx6(Text6, { color: theme.ok, children: sparkline(points.map((p) => p.allowed), 64) })
805
1034
  ] }),
806
- /* @__PURE__ */ jsxs5(Box5, { children: [
807
- /* @__PURE__ */ jsx5(Text5, { children: "denied " }),
808
- /* @__PURE__ */ jsx5(Text5, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 64) })
1035
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1036
+ /* @__PURE__ */ jsx6(Text6, { children: "denied " }),
1037
+ /* @__PURE__ */ jsx6(Text6, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 64) })
809
1038
  ] })
810
1039
  ] }),
811
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
812
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "Recent" }),
813
- /* @__PURE__ */ jsx5(
1040
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
1041
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: "Recent" }),
1042
+ /* @__PURE__ */ jsx6(
814
1043
  Table,
815
1044
  {
816
1045
  columns: [
@@ -832,111 +1061,183 @@ function StatsPanel({ active: active2 }) {
832
1061
  }
833
1062
 
834
1063
  // src/tui/panels/Audit.tsx
835
- import { Box as Box6, Text as Text6, useInput as useInput4 } from "ink";
1064
+ import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
1065
+ import TextInput3 from "ink-text-input";
836
1066
  import { useState as useState5 } from "react";
837
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
838
- var FILTERS = [void 0, "DENY", "ALLOW"];
1067
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1068
+ var DECISIONS = [void 0, "DENY", "ALLOW"];
1069
+ var SIGNALS = [void 0, "dlp", "ratelimit"];
839
1070
  function AuditPanel({ active: active2, focused }) {
840
- const [fi, setFi] = useState5(0);
841
- const filter = FILTERS[fi];
842
- const audit = useLoader(() => api.audit.list({ filter, limit: 14 }), [fi]);
843
- usePoll(audit.reload, 5e3, active2);
1071
+ const [di, setDi] = useState5(0);
1072
+ const [gi, setGi] = useState5(0);
1073
+ const [tool, setTool] = useState5("");
1074
+ const [agent, setAgent] = useState5("");
1075
+ const [search, setSearch] = useState5("");
1076
+ const [sel, setSel] = useState5(0);
1077
+ const [view, setView] = useState5("list");
1078
+ const [editing, setEditing] = useState5(null);
1079
+ const query = {
1080
+ filter: DECISIONS[di],
1081
+ signal: SIGNALS[gi],
1082
+ tool: tool || void 0,
1083
+ agent_name: agent || void 0,
1084
+ search: search || void 0,
1085
+ limit: 100
1086
+ };
1087
+ const auditQ = useLoader(() => api.audit.list(query), [di, gi, tool, agent, search]);
1088
+ usePoll(auditQ.reload, 6e3, active2 && view === "list" && !editing);
1089
+ const entries = auditQ.data?.entries ?? [];
1090
+ const current = entries[Math.min(sel, Math.max(0, entries.length - 1))];
1091
+ const sessionQ = useLoader(
1092
+ () => view === "detail" && current?.session_id ? api.audit.list({ session_id: current.session_id, limit: 40 }) : Promise.resolve(null),
1093
+ [view, current?.session_id]
1094
+ );
844
1095
  useInput4(
845
- (input) => {
846
- if (input === "f") setFi((n) => (n + 1) % FILTERS.length);
1096
+ (input, key) => {
1097
+ if (view === "detail") {
1098
+ if (key.leftArrow || key.escape) setView("list");
1099
+ return;
1100
+ }
1101
+ if (key.upArrow) setSel((n) => Math.max(0, n - 1));
1102
+ else if (key.downArrow) setSel((n) => Math.min(entries.length - 1, n + 1));
1103
+ else if (key.return || key.rightArrow) {
1104
+ if (current) setView("detail");
1105
+ } else if (input === "f") setDi((n) => (n + 1) % DECISIONS.length);
1106
+ else if (input === "g") setGi((n) => (n + 1) % SIGNALS.length);
1107
+ else if (input === "t") setEditing("tool");
1108
+ else if (input === "n") setEditing("agent");
1109
+ else if (input === "/") setEditing("search");
1110
+ else if (input === "c") {
1111
+ setDi(0);
1112
+ setGi(0);
1113
+ setTool("");
1114
+ setAgent("");
1115
+ setSearch("");
1116
+ }
847
1117
  },
848
- { isActive: focused }
1118
+ { isActive: focused && !editing }
849
1119
  );
850
- const entries = audit.data?.entries ?? [];
851
- return /* @__PURE__ */ jsx6(DataView, { loading: audit.loading && !audit.data, error: audit.error, empty: !!audit.data && entries.length === 0, emptyText: "No audit entries.", children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
852
- /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
853
- audit.data?.total ?? 0,
854
- " entries \xB7 filter: ",
855
- /* @__PURE__ */ jsx6(Text6, { color: theme.accent, children: filter ?? "all" }),
856
- " (press f)"
1120
+ if (view === "detail" && current) {
1121
+ const sessionCalls = sessionQ.data?.entries ?? [];
1122
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1123
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1124
+ /* @__PURE__ */ jsx7(Text7, { color: decisionColor(current.decision), bold: true, children: current.decision }),
1125
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: " " + current.tool_name }),
1126
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " + current.permission + " \xB7 " + current.trust_level })
1127
+ ] }),
1128
+ /* @__PURE__ */ jsx7(Detail, { label: "reason", value: current.reason ?? "\u2014" }),
1129
+ /* @__PURE__ */ jsx7(Detail, { label: "rule", value: current.matched_rule_id ?? "\u2014" }),
1130
+ /* @__PURE__ */ jsx7(Detail, { label: "agent", value: current.agent_name ?? "\u2014" }),
1131
+ /* @__PURE__ */ jsx7(Detail, { label: "session", value: current.session_id ?? "\u2014" }),
1132
+ /* @__PURE__ */ jsx7(Detail, { label: "dlp", value: current.dlp_matches?.length ? current.dlp_matches.join(", ") : "none", color: current.dlp_matches?.length ? theme.bad : void 0 }),
1133
+ /* @__PURE__ */ jsx7(Detail, { label: "when", value: new Date(current.created_at).toLocaleString() }),
1134
+ current.arguments_summary ? /* @__PURE__ */ jsx7(Detail, { label: "args", value: truncate(JSON.stringify(current.arguments_summary), 60) }) : null,
1135
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
1136
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
1137
+ "Session ",
1138
+ current.session_id ? `\xB7 ${sessionCalls.length} calls` : "(no session id)"
1139
+ ] }),
1140
+ current.session_id ? /* @__PURE__ */ jsx7(
1141
+ Table,
1142
+ {
1143
+ columns: [
1144
+ { header: "DECISION", width: 9 },
1145
+ { header: "TOOL", width: 20 },
1146
+ { header: "REASON", width: 26 },
1147
+ { header: "WHEN", width: 6 }
1148
+ ],
1149
+ rows: sessionCalls.slice(0, 12).map((e) => [
1150
+ { value: e.decision, color: decisionColor(e.decision) },
1151
+ { value: truncate(e.tool_name, 20), color: theme.accent },
1152
+ { value: truncate(e.reason ?? "\u2014", 26) },
1153
+ { value: ago(e.created_at), dim: true }
1154
+ ])
1155
+ }
1156
+ ) : null
1157
+ ] }),
1158
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2190 back to list" })
1159
+ ] });
1160
+ }
1161
+ const chip = (label, val, on) => /* @__PURE__ */ jsxs7(Text7, { children: [
1162
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
1163
+ label,
1164
+ ":"
1165
+ ] }),
1166
+ /* @__PURE__ */ jsx7(Text7, { color: on ? theme.accentBright : theme.dim, children: val }),
1167
+ /* @__PURE__ */ jsx7(Text7, { children: " " })
1168
+ ] });
1169
+ return /* @__PURE__ */ jsx7(DataView, { loading: auditQ.loading && !auditQ.data, error: auditQ.error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1170
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1171
+ chip("dec", DECISIONS[di] ?? "all", di !== 0),
1172
+ chip("sig", SIGNALS[gi] ?? "all", gi !== 0),
1173
+ chip("tool", tool || "\xB7", !!tool),
1174
+ chip("agent", agent || "\xB7", !!agent),
1175
+ chip("search", search || "\xB7", !!search)
857
1176
  ] }),
858
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(
1177
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: focused ? "\u2191\u2193 \xB7 enter detail \xB7 f dec \xB7 g sig \xB7 t tool \xB7 n agent \xB7 / search \xB7 c clear" : "press \u2192 to browse" }),
1178
+ editing ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
1179
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
1180
+ editing,
1181
+ ": "
1182
+ ] }),
1183
+ /* @__PURE__ */ jsx7(
1184
+ TextInput3,
1185
+ {
1186
+ value: editing === "tool" ? tool : editing === "agent" ? agent : search,
1187
+ onChange: editing === "tool" ? setTool : editing === "agent" ? setAgent : setSearch,
1188
+ onSubmit: () => setEditing(null)
1189
+ }
1190
+ )
1191
+ ] }) : null,
1192
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
1193
+ auditQ.data?.total ?? 0,
1194
+ " matched \xB7 showing ",
1195
+ Math.min(entries.length, 12)
1196
+ ] }) }),
1197
+ /* @__PURE__ */ jsx7(
859
1198
  Table,
860
1199
  {
861
1200
  columns: [
1201
+ { header: "", width: 2 },
862
1202
  { header: "DECISION", width: 9 },
863
1203
  { header: "TOOL", width: 18 },
864
1204
  { header: "AGENT", width: 14 },
865
- { header: "REASON", width: 26 },
1205
+ { header: "REASON", width: 24 },
866
1206
  { header: "DLP", width: 4 },
867
1207
  { header: "WHEN", width: 6 }
868
1208
  ],
869
- rows: entries.map((e) => [
870
- { value: e.decision, color: decisionColor(e.decision) },
871
- { value: truncate(e.tool_name, 18), color: theme.accent },
872
- { value: truncate(e.agent_name ?? "\u2014", 14), dim: true },
873
- { value: truncate(e.reason ?? "\u2014", 26) },
874
- { value: e.dlp_matches?.length ? String(e.dlp_matches.length) : "0", color: e.dlp_matches?.length ? theme.bad : void 0, dim: !e.dlp_matches?.length },
875
- { value: ago(e.created_at), dim: true }
876
- ])
1209
+ rows: entries.slice(0, 12).map((e, i) => rowFor(e, i === sel && focused))
877
1210
  }
878
- ) })
1211
+ )
879
1212
  ] }) });
880
1213
  }
881
-
882
- // src/tui/panels/Agents.tsx
883
- import { Box as Box7, Text as Text7 } from "ink";
884
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
885
- function AgentsPanel({ active: active2 }) {
886
- const agents = useLoader(() => api.agents.live({ limit: 14 }));
887
- usePoll(agents.reload, 4e3, active2);
888
- const data = agents.data;
889
- const rows = data?.agents ?? [];
890
- return /* @__PURE__ */ jsx7(DataView, { loading: agents.loading && !data, error: agents.error, empty: !!data && rows.length === 0, emptyText: "No agent sessions.", children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
891
- data ? /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
892
- /* @__PURE__ */ jsxs7(Text7, { color: theme.ok, children: [
893
- data.counts.active,
894
- " active"
895
- ] }),
896
- " \xB7 ",
897
- /* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
898
- data.counts.idle,
899
- " idle"
900
- ] }),
901
- " \xB7 ",
902
- data.counts.deactivated,
903
- " off"
904
- ] }) : null,
905
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
906
- Table,
907
- {
908
- columns: [
909
- { header: "STATUS", width: 7 },
910
- { header: "AGENT", width: 20 },
911
- { header: "CALLS", width: 6 },
912
- { header: "DENY", width: 5 },
913
- { header: "DLP", width: 4 },
914
- { header: "TRUST", width: 6 },
915
- { header: "CHARACTER", width: 20 }
916
- ],
917
- rows: rows.map((a) => [
918
- { value: a.status, color: modeColor(a.status) },
919
- { value: truncate(a.agent_name ?? a.agent_id ?? a.session_id, 20), color: theme.accent },
920
- { value: String(a.total_calls) },
921
- { value: String(a.denied_calls), color: a.denied_calls ? theme.bad : void 0, dim: !a.denied_calls },
922
- { value: String(a.dlp_events), color: a.dlp_events ? theme.bad : void 0, dim: !a.dlp_events },
923
- { value: `${a.trust_score}` },
924
- { value: truncate(a.character || "\u2014", 20), dim: true }
925
- ])
926
- }
927
- ) })
928
- ] }) });
1214
+ function rowFor(e, active2) {
1215
+ return [
1216
+ { value: active2 ? "\u25B8" : "", color: theme.accentBright },
1217
+ { value: e.decision, color: decisionColor(e.decision) },
1218
+ { value: truncate(e.tool_name, 18), color: theme.accent },
1219
+ { value: truncate(e.agent_name ?? "\u2014", 14), dim: true },
1220
+ { value: truncate(e.reason ?? "\u2014", 24) },
1221
+ { value: e.dlp_matches?.length ? String(e.dlp_matches.length) : "0", color: e.dlp_matches?.length ? theme.bad : void 0, dim: !e.dlp_matches?.length },
1222
+ { value: ago(e.created_at), dim: true }
1223
+ ];
1224
+ }
1225
+ function Detail({ label, value, color }) {
1226
+ return /* @__PURE__ */ jsxs7(Box7, { children: [
1227
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: label.padEnd(9) }),
1228
+ /* @__PURE__ */ jsx7(Text7, { color, children: value })
1229
+ ] });
929
1230
  }
930
1231
 
931
1232
  // src/tui/App.tsx
932
1233
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
933
1234
  var SECTIONS = [
1235
+ { label: "Live", Panel: LivePanel },
934
1236
  { label: "Policies", Panel: PoliciesPanel },
935
1237
  { label: "Rate Limit", Panel: RateLimitPanel },
936
1238
  { label: "DLP", Panel: DlpPanel },
937
1239
  { label: "Stats", Panel: StatsPanel },
938
- { label: "Audit", Panel: AuditPanel },
939
- { label: "Agents", Panel: AgentsPanel }
1240
+ { label: "Audit", Panel: AuditPanel }
940
1241
  ];
941
1242
  var BANNER_HEX = ["#1432A0", "#2850BE", "#3C6ED7", "#5A8CE6", "#82AAF0", "#AAC8FA"];
942
1243
  function Banner() {