@solongate/proxy 0.60.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/index.js +643 -223
- package/dist/tui/index.js +626 -207
- package/dist/tui/panels/Live.d.ts +4 -0
- package/package.json +1 -1
- package/dist/tui/panels/Agents.d.ts +0 -3
package/dist/tui/index.js
CHANGED
|
@@ -124,9 +124,8 @@ function KeyHints({ hints }) {
|
|
|
124
124
|
] }, i)) });
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
// src/tui/panels/
|
|
128
|
-
import { Box as Box2, Text as Text2
|
|
129
|
-
import { 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,83 +428,335 @@ function usePoll(reload, intervalMs, enabled = true) {
|
|
|
429
428
|
}, [reload, intervalMs, enabled]);
|
|
430
429
|
}
|
|
431
430
|
|
|
432
|
-
// src/tui/panels/
|
|
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
|
-
const [sel, setSel] = useState2(0);
|
|
436
527
|
const list3 = useLoader(() => api.policies.list());
|
|
437
528
|
const policies = list3.data?.policies ?? [];
|
|
438
|
-
const
|
|
529
|
+
const [view, setView] = useState2("list");
|
|
530
|
+
const [pi, setPi] = useState2(0);
|
|
531
|
+
const [ri, setRi] = useState2(0);
|
|
532
|
+
const [fi, setFi] = useState2(0);
|
|
533
|
+
const [rules, setRules] = useState2([]);
|
|
534
|
+
const [mode, setMode] = useState2("denylist");
|
|
535
|
+
const [dirty, setDirty] = useState2(false);
|
|
536
|
+
const [editing, setEditing] = useState2(false);
|
|
537
|
+
const [editVal, setEditVal] = useState2("");
|
|
538
|
+
const [status, setStatus] = useState2(null);
|
|
539
|
+
const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
|
|
439
540
|
const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
|
|
541
|
+
useEffect2(() => {
|
|
542
|
+
if (detail.data) {
|
|
543
|
+
setRules(detail.data.rules);
|
|
544
|
+
setMode(detail.data.mode ?? "denylist");
|
|
545
|
+
setDirty(false);
|
|
546
|
+
setRi(0);
|
|
547
|
+
}
|
|
548
|
+
}, [detail.data]);
|
|
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;
|
|
557
|
+
setStatus("Saving\u2026");
|
|
558
|
+
try {
|
|
559
|
+
const res = await api.policies.update(selected.id, {
|
|
560
|
+
id: selected.id,
|
|
561
|
+
name: detail.data.name,
|
|
562
|
+
description: detail.data.description,
|
|
563
|
+
mode,
|
|
564
|
+
agents: detail.data.agents,
|
|
565
|
+
rules
|
|
566
|
+
});
|
|
567
|
+
setRules(res.rules);
|
|
568
|
+
setDirty(false);
|
|
569
|
+
setStatus("\u2713 Saved v" + res._version);
|
|
570
|
+
list3.reload();
|
|
571
|
+
} catch (e) {
|
|
572
|
+
setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
|
|
573
|
+
}
|
|
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
|
+
};
|
|
440
583
|
useInput(
|
|
441
|
-
(
|
|
442
|
-
if (
|
|
443
|
-
|
|
584
|
+
(input, key) => {
|
|
585
|
+
if (view === "list") {
|
|
586
|
+
if (key.upArrow) setPi((n) => Math.max(0, n - 1));
|
|
587
|
+
else if (key.downArrow) setPi((n) => Math.min(policies.length - 1, n + 1));
|
|
588
|
+
else if (key.return || key.rightArrow) {
|
|
589
|
+
setStatus(null);
|
|
590
|
+
setView("rules");
|
|
591
|
+
}
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
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;
|
|
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();
|
|
444
636
|
},
|
|
445
|
-
{ isActive: focused }
|
|
637
|
+
{ isActive: focused && !editing }
|
|
446
638
|
);
|
|
447
|
-
|
|
448
|
-
/* @__PURE__ */
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
639
|
+
if (view === "list") {
|
|
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: [
|
|
643
|
+
(i === pi ? "\u25B8 " : " ") + truncate(p.name, 26).padEnd(27),
|
|
644
|
+
/* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
|
|
645
|
+
p.mode.padEnd(10),
|
|
646
|
+
" ",
|
|
647
|
+
p.rules.length,
|
|
648
|
+
" rules \xB7 v",
|
|
649
|
+
p.version
|
|
650
|
+
] })
|
|
651
|
+
] }, p.id)) })
|
|
652
|
+
] }) });
|
|
653
|
+
}
|
|
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
|
|
463
662
|
] }),
|
|
464
|
-
/* @__PURE__ */
|
|
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(
|
|
465
665
|
Table,
|
|
466
666
|
{
|
|
467
667
|
columns: [
|
|
668
|
+
{ header: "", width: 2 },
|
|
669
|
+
{ header: "ON", width: 2 },
|
|
468
670
|
{ header: "EFFECT", width: 7 },
|
|
469
671
|
{ header: "PRIO", width: 4 },
|
|
470
|
-
{ header: "TOOL", width:
|
|
471
|
-
{ header: "
|
|
672
|
+
{ header: "TOOL", width: 20 },
|
|
673
|
+
{ header: "CONSTRAINTS", width: 22 }
|
|
472
674
|
],
|
|
473
|
-
rows:
|
|
474
|
-
{ value:
|
|
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 },
|
|
475
679
|
{ value: String(r.priority), dim: true },
|
|
476
|
-
{ value: truncate(r.toolPattern,
|
|
477
|
-
{ value: truncate(r.description || "\u2014",
|
|
680
|
+
{ value: truncate(r.toolPattern, 20), color: theme.accent, dim: !r.enabled },
|
|
681
|
+
{ value: ruleSummary(r) || truncate(r.description || "\u2014", 22), dim: true }
|
|
478
682
|
])
|
|
479
683
|
}
|
|
480
|
-
)
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
695
|
+
] }),
|
|
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
|
+
] });
|
|
483
724
|
}
|
|
484
725
|
|
|
485
726
|
// src/tui/panels/RateLimit.tsx
|
|
486
|
-
import { Box as
|
|
487
|
-
import { useEffect as
|
|
488
|
-
import { jsx as
|
|
727
|
+
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
728
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
729
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
489
730
|
var MODES = ["off", "detect", "block"];
|
|
490
|
-
var
|
|
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
|
+
}
|
|
491
742
|
function RateLimitPanel({ focused }) {
|
|
492
743
|
const layersQ = useLoader(() => api.settings.getSecurityLayers());
|
|
493
744
|
const historyQ = useLoader(() => api.settings.getRateLimitHistory());
|
|
745
|
+
const insightsQ = useLoader(() => api.stats.securityInsights(7));
|
|
494
746
|
const [draft, setDraft] = useState3(null);
|
|
495
747
|
const [dirty, setDirty] = useState3(false);
|
|
496
748
|
const [fi, setFi] = useState3(0);
|
|
497
749
|
const [status, setStatus] = useState3(null);
|
|
498
|
-
|
|
750
|
+
useEffect3(() => {
|
|
499
751
|
if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
|
|
500
752
|
}, [layersQ.data, draft]);
|
|
501
|
-
const adjust = (dir) => {
|
|
753
|
+
const adjust = (dir, step) => {
|
|
502
754
|
if (!draft) return;
|
|
503
|
-
const field =
|
|
755
|
+
const field = FIELDS2[fi];
|
|
504
756
|
if (field === "mode") {
|
|
505
757
|
const idx = (MODES.indexOf(draft.mode) + dir + MODES.length) % MODES.length;
|
|
506
758
|
setDraft({ ...draft, mode: MODES[idx] });
|
|
507
759
|
} else {
|
|
508
|
-
const step = 10;
|
|
509
760
|
setDraft({ ...draft, [field]: Math.max(0, draft[field] + dir * step) });
|
|
510
761
|
}
|
|
511
762
|
setDirty(true);
|
|
@@ -515,8 +766,7 @@ function RateLimitPanel({ focused }) {
|
|
|
515
766
|
if (!draft || !layersQ.data) return;
|
|
516
767
|
setStatus("Saving\u2026");
|
|
517
768
|
try {
|
|
518
|
-
const
|
|
519
|
-
const res = await api.settings.setSecurityLayers(next);
|
|
769
|
+
const res = await api.settings.setSecurityLayers({ ...layersQ.data.layers, rateLimit: draft });
|
|
520
770
|
setDraft({ ...res.layers.rateLimit });
|
|
521
771
|
setDirty(false);
|
|
522
772
|
setStatus("\u2713 Saved");
|
|
@@ -527,119 +777,216 @@ function RateLimitPanel({ focused }) {
|
|
|
527
777
|
};
|
|
528
778
|
useInput2(
|
|
529
779
|
(input, key) => {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
else if (key.
|
|
533
|
-
else if (key.
|
|
780
|
+
const step = key.shift ? 10 : 1;
|
|
781
|
+
if (key.upArrow) setFi((n) => (n - 1 + FIELDS2.length) % FIELDS2.length);
|
|
782
|
+
else if (key.downArrow) setFi((n) => (n + 1) % FIELDS2.length);
|
|
783
|
+
else if (key.leftArrow) adjust(-1, step);
|
|
784
|
+
else if (key.rightArrow) adjust(1, step);
|
|
534
785
|
else if (input === "s") void save();
|
|
535
786
|
},
|
|
536
787
|
{ isActive: focused }
|
|
537
788
|
);
|
|
538
|
-
const history = historyQ.data?.history ?? [];
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
789
|
+
const history = (historyQ.data?.history ?? []).slice().sort((a, b) => b.ts - a.ts);
|
|
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 }) })
|
|
546
801
|
] }),
|
|
547
|
-
|
|
548
|
-
/* @__PURE__ */
|
|
549
|
-
/* @__PURE__ */
|
|
550
|
-
/* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " " + history.length + " changes" })
|
|
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
|
|
551
805
|
] }) : null,
|
|
552
|
-
/* @__PURE__ */
|
|
553
|
-
|
|
554
|
-
|
|
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: [
|
|
826
|
+
"Change history ",
|
|
827
|
+
history.length ? `(${history.length})` : ""
|
|
828
|
+
] }),
|
|
829
|
+
history.length ? /* @__PURE__ */ jsx4(
|
|
830
|
+
Table,
|
|
831
|
+
{
|
|
832
|
+
columns: [
|
|
833
|
+
{ header: "WHEN", width: 10 },
|
|
834
|
+
{ header: "/MIN", width: 6 },
|
|
835
|
+
{ header: "/HOUR", width: 7 },
|
|
836
|
+
{ header: "/DAY", width: 6 }
|
|
837
|
+
],
|
|
838
|
+
rows: history.slice(0, 4).map((h, i) => [
|
|
839
|
+
{ value: ago(h.ts) + " ago", dim: true, bold: i === 0 },
|
|
840
|
+
{ value: String(h.minute), bold: i === 0 },
|
|
841
|
+
{ value: h.hour === 0 ? "off" : String(h.hour), dim: h.hour === 0 },
|
|
842
|
+
{ value: h.day === 0 ? "off" : String(h.day), dim: h.day === 0 }
|
|
843
|
+
])
|
|
844
|
+
}
|
|
845
|
+
) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: " no changes" })
|
|
555
846
|
] })
|
|
556
847
|
] }) : null });
|
|
557
848
|
}
|
|
558
|
-
function
|
|
559
|
-
return /* @__PURE__ */
|
|
560
|
-
/* @__PURE__ */
|
|
849
|
+
function FieldRow({ label, active: active2, children }) {
|
|
850
|
+
return /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
851
|
+
/* @__PURE__ */ jsx4(Text4, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + label.padEnd(12) }),
|
|
561
852
|
children
|
|
562
853
|
] });
|
|
563
854
|
}
|
|
564
855
|
|
|
565
856
|
// src/tui/panels/Dlp.tsx
|
|
566
|
-
import { Box as
|
|
567
|
-
import
|
|
568
|
-
import {
|
|
857
|
+
import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
|
|
858
|
+
import TextInput2 from "ink-text-input";
|
|
859
|
+
import { useEffect as useEffect4, useState as useState4 } from "react";
|
|
860
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
569
861
|
var MODES2 = ["off", "detect", "block"];
|
|
570
862
|
function DlpPanel({ focused }) {
|
|
571
863
|
const q = useLoader(() => api.settings.getSecurityLayers());
|
|
572
864
|
const [dlp, setDlp] = useState4(null);
|
|
573
865
|
const [available, setAvailable] = useState4([]);
|
|
574
866
|
const [sel, setSel] = useState4(0);
|
|
867
|
+
const [dirty, setDirty] = useState4(false);
|
|
575
868
|
const [status, setStatus] = useState4(null);
|
|
576
|
-
|
|
869
|
+
const [adding, setAdding] = useState4(null);
|
|
870
|
+
const [newName, setNewName] = useState4("");
|
|
871
|
+
const [newRe, setNewRe] = useState4("");
|
|
872
|
+
useEffect4(() => {
|
|
577
873
|
if (q.data && !dlp) {
|
|
578
|
-
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] });
|
|
579
875
|
setAvailable(q.data.availablePatterns);
|
|
580
876
|
}
|
|
581
877
|
}, [q.data, dlp]);
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
878
|
+
const mutate = (next) => {
|
|
879
|
+
setDlp(next);
|
|
880
|
+
setDirty(true);
|
|
881
|
+
setStatus(null);
|
|
882
|
+
};
|
|
883
|
+
const save = async () => {
|
|
884
|
+
if (!dlp || !q.data) return;
|
|
585
885
|
setStatus("Saving\u2026");
|
|
586
886
|
try {
|
|
587
|
-
const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp
|
|
588
|
-
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);
|
|
589
890
|
setStatus("\u2713 Saved");
|
|
590
891
|
} catch (e) {
|
|
591
892
|
setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
|
|
592
893
|
}
|
|
593
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;
|
|
594
902
|
useInput3(
|
|
595
903
|
(input, key) => {
|
|
596
904
|
if (!dlp) return;
|
|
597
905
|
if (key.upArrow) setSel((n) => Math.max(0, n - 1));
|
|
598
|
-
else if (key.downArrow) setSel((n) => Math.min(
|
|
599
|
-
else if (input === "
|
|
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) {
|
|
600
915
|
const p = available[sel];
|
|
601
|
-
if (!p) return;
|
|
602
916
|
const set = new Set(dlp.patterns);
|
|
603
917
|
if (set.has(p)) set.delete(p);
|
|
604
918
|
else set.add(p);
|
|
605
|
-
|
|
606
|
-
} else if (input === "
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
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();
|
|
610
926
|
},
|
|
611
|
-
{ isActive: focused }
|
|
927
|
+
{ isActive: focused && !adding }
|
|
612
928
|
);
|
|
613
929
|
const enabled = new Set(dlp?.patterns ?? []);
|
|
614
|
-
return /* @__PURE__ */
|
|
615
|
-
/* @__PURE__ */
|
|
616
|
-
/* @__PURE__ */
|
|
617
|
-
/* @__PURE__ */
|
|
618
|
-
/* @__PURE__ */
|
|
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
|
|
619
935
|
] }),
|
|
620
|
-
/* @__PURE__ */
|
|
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) => {
|
|
621
938
|
const on = enabled.has(p);
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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 })
|
|
625
943
|
] }, p);
|
|
626
944
|
}) }),
|
|
627
|
-
|
|
628
|
-
/* @__PURE__ */
|
|
629
|
-
dlp.custom.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
+
)
|
|
635
982
|
] }) : null,
|
|
636
|
-
status ? /* @__PURE__ */
|
|
983
|
+
status ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
|
|
637
984
|
] }) : null });
|
|
638
985
|
}
|
|
639
986
|
|
|
640
987
|
// src/tui/panels/Stats.tsx
|
|
641
|
-
import { Box as
|
|
642
|
-
import { jsx as
|
|
988
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
989
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
643
990
|
function StatsPanel({ active: active2 }) {
|
|
644
991
|
const stats = useLoader(() => api.stats.get());
|
|
645
992
|
const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
|
|
@@ -649,50 +996,50 @@ function StatsPanel({ active: active2 }) {
|
|
|
649
996
|
}, 5e3, active2);
|
|
650
997
|
const s = stats.data;
|
|
651
998
|
const points = ts.data?.timeseries ?? [];
|
|
652
|
-
return /* @__PURE__ */
|
|
653
|
-
/* @__PURE__ */
|
|
654
|
-
/* @__PURE__ */
|
|
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: [
|
|
655
1002
|
s.total_calls,
|
|
656
1003
|
" "
|
|
657
1004
|
] }),
|
|
658
|
-
/* @__PURE__ */
|
|
659
|
-
/* @__PURE__ */
|
|
1005
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: "calls " }),
|
|
1006
|
+
/* @__PURE__ */ jsxs6(Text6, { color: theme.ok, children: [
|
|
660
1007
|
s.allowed,
|
|
661
1008
|
" allowed"
|
|
662
1009
|
] }),
|
|
663
|
-
/* @__PURE__ */
|
|
664
|
-
/* @__PURE__ */
|
|
1010
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: " " }),
|
|
1011
|
+
/* @__PURE__ */ jsxs6(Text6, { color: theme.bad, children: [
|
|
665
1012
|
s.denied,
|
|
666
1013
|
" denied"
|
|
667
1014
|
] })
|
|
668
1015
|
] }),
|
|
669
|
-
/* @__PURE__ */
|
|
1016
|
+
/* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
|
|
670
1017
|
s.active_policies,
|
|
671
1018
|
" policies \xB7 ",
|
|
672
1019
|
s.registered_tools,
|
|
673
1020
|
" tools"
|
|
674
1021
|
] }),
|
|
675
|
-
/* @__PURE__ */
|
|
676
|
-
/* @__PURE__ */
|
|
1022
|
+
/* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
1023
|
+
/* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
|
|
677
1024
|
"Last 24h ",
|
|
678
1025
|
ts.data ? `(per ${ts.data.granularity})` : ""
|
|
679
1026
|
] }),
|
|
680
|
-
/* @__PURE__ */
|
|
681
|
-
/* @__PURE__ */
|
|
682
|
-
/* @__PURE__ */
|
|
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) })
|
|
683
1030
|
] }),
|
|
684
|
-
/* @__PURE__ */
|
|
685
|
-
/* @__PURE__ */
|
|
686
|
-
/* @__PURE__ */
|
|
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) })
|
|
687
1034
|
] }),
|
|
688
|
-
/* @__PURE__ */
|
|
689
|
-
/* @__PURE__ */
|
|
690
|
-
/* @__PURE__ */
|
|
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) })
|
|
691
1038
|
] })
|
|
692
1039
|
] }),
|
|
693
|
-
/* @__PURE__ */
|
|
694
|
-
/* @__PURE__ */
|
|
695
|
-
/* @__PURE__ */
|
|
1040
|
+
/* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
1041
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: "Recent" }),
|
|
1042
|
+
/* @__PURE__ */ jsx6(
|
|
696
1043
|
Table,
|
|
697
1044
|
{
|
|
698
1045
|
columns: [
|
|
@@ -714,111 +1061,183 @@ function StatsPanel({ active: active2 }) {
|
|
|
714
1061
|
}
|
|
715
1062
|
|
|
716
1063
|
// src/tui/panels/Audit.tsx
|
|
717
|
-
import { Box as
|
|
1064
|
+
import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
|
|
1065
|
+
import TextInput3 from "ink-text-input";
|
|
718
1066
|
import { useState as useState5 } from "react";
|
|
719
|
-
import { jsx as
|
|
720
|
-
var
|
|
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"];
|
|
721
1070
|
function AuditPanel({ active: active2, focused }) {
|
|
722
|
-
const [
|
|
723
|
-
const
|
|
724
|
-
const
|
|
725
|
-
|
|
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
|
+
);
|
|
726
1095
|
useInput4(
|
|
727
|
-
(input) => {
|
|
728
|
-
if (
|
|
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
|
+
}
|
|
729
1117
|
},
|
|
730
|
-
{ isActive: focused }
|
|
1118
|
+
{ isActive: focused && !editing }
|
|
731
1119
|
);
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
/* @__PURE__ */
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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)
|
|
739
1176
|
] }),
|
|
740
|
-
/* @__PURE__ */
|
|
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(
|
|
741
1198
|
Table,
|
|
742
1199
|
{
|
|
743
1200
|
columns: [
|
|
1201
|
+
{ header: "", width: 2 },
|
|
744
1202
|
{ header: "DECISION", width: 9 },
|
|
745
1203
|
{ header: "TOOL", width: 18 },
|
|
746
1204
|
{ header: "AGENT", width: 14 },
|
|
747
|
-
{ header: "REASON", width:
|
|
1205
|
+
{ header: "REASON", width: 24 },
|
|
748
1206
|
{ header: "DLP", width: 4 },
|
|
749
1207
|
{ header: "WHEN", width: 6 }
|
|
750
1208
|
],
|
|
751
|
-
rows: entries.map((e) =>
|
|
752
|
-
{ value: e.decision, color: decisionColor(e.decision) },
|
|
753
|
-
{ value: truncate(e.tool_name, 18), color: theme.accent },
|
|
754
|
-
{ value: truncate(e.agent_name ?? "\u2014", 14), dim: true },
|
|
755
|
-
{ value: truncate(e.reason ?? "\u2014", 26) },
|
|
756
|
-
{ 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 },
|
|
757
|
-
{ value: ago(e.created_at), dim: true }
|
|
758
|
-
])
|
|
1209
|
+
rows: entries.slice(0, 12).map((e, i) => rowFor(e, i === sel && focused))
|
|
759
1210
|
}
|
|
760
|
-
)
|
|
1211
|
+
)
|
|
761
1212
|
] }) });
|
|
762
1213
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
/* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
|
|
780
|
-
data.counts.idle,
|
|
781
|
-
" idle"
|
|
782
|
-
] }),
|
|
783
|
-
" \xB7 ",
|
|
784
|
-
data.counts.deactivated,
|
|
785
|
-
" off"
|
|
786
|
-
] }) : null,
|
|
787
|
-
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
|
|
788
|
-
Table,
|
|
789
|
-
{
|
|
790
|
-
columns: [
|
|
791
|
-
{ header: "STATUS", width: 7 },
|
|
792
|
-
{ header: "AGENT", width: 20 },
|
|
793
|
-
{ header: "CALLS", width: 6 },
|
|
794
|
-
{ header: "DENY", width: 5 },
|
|
795
|
-
{ header: "DLP", width: 4 },
|
|
796
|
-
{ header: "TRUST", width: 6 },
|
|
797
|
-
{ header: "CHARACTER", width: 20 }
|
|
798
|
-
],
|
|
799
|
-
rows: rows.map((a) => [
|
|
800
|
-
{ value: a.status, color: modeColor(a.status) },
|
|
801
|
-
{ value: truncate(a.agent_name ?? a.agent_id ?? a.session_id, 20), color: theme.accent },
|
|
802
|
-
{ value: String(a.total_calls) },
|
|
803
|
-
{ value: String(a.denied_calls), color: a.denied_calls ? theme.bad : void 0, dim: !a.denied_calls },
|
|
804
|
-
{ value: String(a.dlp_events), color: a.dlp_events ? theme.bad : void 0, dim: !a.dlp_events },
|
|
805
|
-
{ value: `${a.trust_score}` },
|
|
806
|
-
{ value: truncate(a.character || "\u2014", 20), dim: true }
|
|
807
|
-
])
|
|
808
|
-
}
|
|
809
|
-
) })
|
|
810
|
-
] }) });
|
|
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
|
+
] });
|
|
811
1230
|
}
|
|
812
1231
|
|
|
813
1232
|
// src/tui/App.tsx
|
|
814
1233
|
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
815
1234
|
var SECTIONS = [
|
|
1235
|
+
{ label: "Live", Panel: LivePanel },
|
|
816
1236
|
{ label: "Policies", Panel: PoliciesPanel },
|
|
817
1237
|
{ label: "Rate Limit", Panel: RateLimitPanel },
|
|
818
1238
|
{ label: "DLP", Panel: DlpPanel },
|
|
819
1239
|
{ label: "Stats", Panel: StatsPanel },
|
|
820
|
-
{ label: "Audit", Panel: AuditPanel }
|
|
821
|
-
{ label: "Agents", Panel: AgentsPanel }
|
|
1240
|
+
{ label: "Audit", Panel: AuditPanel }
|
|
822
1241
|
];
|
|
823
1242
|
var BANNER_HEX = ["#1432A0", "#2850BE", "#3C6ED7", "#5A8CE6", "#82AAF0", "#AAC8FA"];
|
|
824
1243
|
function Banner() {
|