@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/index.js +575 -273
- package/dist/tui/index.js +559 -258
- 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 { 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/
|
|
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 [
|
|
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
|
|
453
|
-
|
|
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
|
|
563
|
+
mode,
|
|
461
564
|
agents: detail.data.agents,
|
|
462
|
-
rules
|
|
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("
|
|
590
|
+
setView("rules");
|
|
479
591
|
}
|
|
480
592
|
return;
|
|
481
593
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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__ */
|
|
512
|
-
/* @__PURE__ */
|
|
513
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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
|
|
526
|
-
|
|
527
|
-
/* @__PURE__ */
|
|
528
|
-
/* @__PURE__ */
|
|
529
|
-
|
|
530
|
-
|
|
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__ */
|
|
533
|
-
/* @__PURE__ */
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
{
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
|
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
|
|
729
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
561
730
|
var MODES = ["off", "detect", "block"];
|
|
562
|
-
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
|
+
}
|
|
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 =
|
|
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
|
|
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 +
|
|
604
|
-
else if (key.downArrow) setFi((n) => (n + 1) %
|
|
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
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
/* @__PURE__ */
|
|
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__ */
|
|
622
|
-
dirty ? /* @__PURE__ */
|
|
623
|
-
status ? /* @__PURE__ */
|
|
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__ */
|
|
626
|
-
/* @__PURE__ */
|
|
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__ */
|
|
829
|
+
history.length ? /* @__PURE__ */ jsx4(
|
|
631
830
|
Table,
|
|
632
831
|
{
|
|
633
832
|
columns: [
|
|
634
|
-
{ header: "WHEN", width:
|
|
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,
|
|
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__ */
|
|
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__ */
|
|
678
|
-
/* @__PURE__ */
|
|
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
|
|
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
|
|
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
|
|
701
|
-
|
|
702
|
-
|
|
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
|
|
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(
|
|
717
|
-
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) {
|
|
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
|
-
|
|
724
|
-
} else if (input === "
|
|
725
|
-
const
|
|
726
|
-
|
|
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__ */
|
|
733
|
-
/* @__PURE__ */
|
|
734
|
-
/* @__PURE__ */
|
|
735
|
-
/* @__PURE__ */
|
|
736
|
-
/* @__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
|
|
737
935
|
] }),
|
|
738
|
-
/* @__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) => {
|
|
739
938
|
const on = enabled.has(p);
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
746
|
-
/* @__PURE__ */
|
|
747
|
-
dlp.custom.
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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__ */
|
|
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
|
|
760
|
-
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";
|
|
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__ */
|
|
771
|
-
/* @__PURE__ */
|
|
772
|
-
/* @__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: [
|
|
773
1002
|
s.total_calls,
|
|
774
1003
|
" "
|
|
775
1004
|
] }),
|
|
776
|
-
/* @__PURE__ */
|
|
777
|
-
/* @__PURE__ */
|
|
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__ */
|
|
782
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
794
|
-
/* @__PURE__ */
|
|
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__ */
|
|
799
|
-
/* @__PURE__ */
|
|
800
|
-
/* @__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) })
|
|
801
1030
|
] }),
|
|
802
|
-
/* @__PURE__ */
|
|
803
|
-
/* @__PURE__ */
|
|
804
|
-
/* @__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) })
|
|
805
1034
|
] }),
|
|
806
|
-
/* @__PURE__ */
|
|
807
|
-
/* @__PURE__ */
|
|
808
|
-
/* @__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) })
|
|
809
1038
|
] })
|
|
810
1039
|
] }),
|
|
811
|
-
/* @__PURE__ */
|
|
812
|
-
/* @__PURE__ */
|
|
813
|
-
/* @__PURE__ */
|
|
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
|
|
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
|
|
838
|
-
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"];
|
|
839
1070
|
function AuditPanel({ active: active2, focused }) {
|
|
840
|
-
const [
|
|
841
|
-
const
|
|
842
|
-
const
|
|
843
|
-
|
|
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 (
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
/* @__PURE__ */
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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__ */
|
|
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:
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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() {
|