@solongate/proxy 0.81.55 → 0.81.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui/index.js CHANGED
@@ -283,6 +283,20 @@ function saveAccount(acc) {
283
283
  } catch {
284
284
  }
285
285
  }
286
+ function removeAccount(apiKey) {
287
+ try {
288
+ const list5 = (() => {
289
+ try {
290
+ const raw = JSON.parse(readFileSync3(accountsFile(), "utf-8"));
291
+ return Array.isArray(raw) ? raw.filter((a) => a && a.apiKey) : [];
292
+ } catch {
293
+ return [];
294
+ }
295
+ })();
296
+ writeFileSync2(accountsFile(), JSON.stringify(list5.filter((a) => a.apiKey !== apiKey), null, 2));
297
+ } catch {
298
+ }
299
+ }
286
300
  var viewOverride = null;
287
301
  function setViewCredentials(creds) {
288
302
  viewOverride = creds;
@@ -550,11 +564,13 @@ __export(settings_exports, {
550
564
  getSecurityLayers: () => getSecurityLayers,
551
565
  getSelfProtection: () => getSelfProtection,
552
566
  getWebhooks: () => getWebhooks,
567
+ sendTestAlert: () => sendTestAlert,
553
568
  sendTestWebhook: () => sendTestWebhook,
554
569
  setAlertEnabled: () => setAlertEnabled,
555
570
  setLocalLogs: () => setLocalLogs,
556
571
  setSecurityLayers: () => setSecurityLayers,
557
572
  setSelfProtection: () => setSelfProtection,
573
+ updateAlert: () => updateAlert,
558
574
  updateWebhook: () => updateWebhook
559
575
  });
560
576
  function getSecurityLayers() {
@@ -596,6 +612,12 @@ function deleteAlert(id) {
596
612
  function setAlertEnabled(id, enabled) {
597
613
  return request("PATCH", "/settings/denial-alerts", { query: { id }, body: { enabled } });
598
614
  }
615
+ function updateAlert(id, patch) {
616
+ return request("PATCH", "/settings/denial-alerts", { query: { id }, body: patch });
617
+ }
618
+ function sendTestAlert(id) {
619
+ return request("POST", "/settings/denial-alerts/send-test", { body: { id } });
620
+ }
599
621
  function getWebhooks() {
600
622
  return request("GET", "/settings/denial-webhook");
601
623
  }
@@ -3175,8 +3197,20 @@ function stopLogsServerDaemon() {
3175
3197
  import { Fragment as Fragment5, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
3176
3198
  var EVENTS = ["denials", "allowed", "all"];
3177
3199
  var SPIN2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3178
- var ALERT_BASE = { name: "dataroom alert", threshold: 5, windowSeconds: 60, signal: "any" };
3200
+ var SIGNALS2 = ["any", "deny", "dlp", "ratelimit"];
3201
+ var SIGNAL_LABEL = { any: "any signal", deny: "denials", dlp: "dlp secrets", ratelimit: "rate-limit" };
3202
+ var WINDOWS = [30, 60, 300, 3600, 86400];
3203
+ var winLabel = (s) => s < 60 ? `${s}s` : s < 3600 ? `${s / 60}m` : s < 86400 ? `${s / 3600}h` : `${s / 86400}d`;
3204
+ var THRESH_MIN = 5;
3205
+ var THRESH_MAX = 100;
3206
+ var nearestWindowIdx = (s) => {
3207
+ let best = 0;
3208
+ for (let i = 1; i < WINDOWS.length; i++) if (Math.abs(WINDOWS[i] - s) < Math.abs(WINDOWS[best] - s)) best = i;
3209
+ return best;
3210
+ };
3211
+ var EDITOR_FIELDS = 4;
3179
3212
  var acctLabel = (a) => a.email || a.user || "account \u2026" + a.apiKey.slice(-4);
3213
+ var alertChannel = (r) => r.emails && r.emails.length ? "email" : r.telegram && r.telegram.length ? "telegram" : null;
3180
3214
  function SettingsPanel({
3181
3215
  active: active2,
3182
3216
  focused,
@@ -3191,6 +3225,7 @@ function SettingsPanel({
3191
3225
  const [confirmDel, setConfirmDel] = useState7(null);
3192
3226
  const [msg, setMsg] = useState7(null);
3193
3227
  const [busy, setBusy] = useState7(false);
3228
+ const [editor, setEditor] = useState7(null);
3194
3229
  const [accounts, setAccounts] = useState7(() => listAccounts());
3195
3230
  const [login, setLogin] = useState7(null);
3196
3231
  const [tick, setTick] = useState7(0);
@@ -3210,22 +3245,30 @@ function SettingsPanel({
3210
3245
  const localQ = useLoader(() => listAccounts().length ? api.settings.getLocalLogs() : Promise.resolve(null));
3211
3246
  const whQ = useLoader(() => listAccounts().length ? api.settings.getWebhooks() : Promise.resolve(null));
3212
3247
  const alertQ = useLoader(() => listAccounts().length ? api.settings.getAlerts() : Promise.resolve(null));
3248
+ const guardQ = useLoader(() => listAccounts().length ? api.settings.getGuardStatus() : Promise.resolve(null));
3249
+ const selfQ = useLoader(() => listAccounts().length ? api.settings.getSelfProtection() : Promise.resolve(null));
3213
3250
  const local = localQ.data;
3251
+ const guard = guardQ.data;
3252
+ const selfProt = selfQ.data;
3214
3253
  const [hidden, setHidden] = useState7(/* @__PURE__ */ new Set());
3215
3254
  const webhooks = (whQ.data?.webhooks ?? []).filter((w) => !hidden.has("wh:" + w.id));
3216
3255
  const alerts = (alertQ.data?.rules ?? []).filter((r) => !hidden.has("alert:" + r.id));
3256
+ const hasEmailAlert = alerts.some((r) => alertChannel(r) === "email");
3257
+ const hasTgAlert = alerts.some((r) => alertChannel(r) === "telegram");
3217
3258
  const rowsAll = [
3218
3259
  ...accounts.map((acc) => ({ kind: "acct", acc })),
3219
3260
  { kind: "acct-add" },
3220
3261
  ...locked ? [] : [
3262
+ { kind: "guard" },
3263
+ { kind: "self" },
3221
3264
  { kind: "ll-enabled" },
3222
3265
  { kind: "ll-path" },
3223
3266
  { kind: "ll-server" },
3224
3267
  ...webhooks.map((wh) => ({ kind: "wh", wh })),
3225
3268
  { kind: "wh-add" },
3226
3269
  ...alerts.map((rule) => ({ kind: "alert", rule })),
3227
- { kind: "alert-add-email" },
3228
- { kind: "alert-add-tg" }
3270
+ ...hasEmailAlert ? [] : [{ kind: "alert-add-email" }],
3271
+ ...hasTgAlert ? [] : [{ kind: "alert-add-tg" }]
3229
3272
  ]
3230
3273
  ];
3231
3274
  const selClamped = Math.min(sel, rowsAll.length - 1);
@@ -3236,6 +3279,8 @@ function SettingsPanel({
3236
3279
  localQ.reload();
3237
3280
  whQ.reload();
3238
3281
  alertQ.reload();
3282
+ guardQ.reload();
3283
+ selfQ.reload();
3239
3284
  };
3240
3285
  const run = (label, fn, reload) => {
3241
3286
  if (busy) return;
@@ -3251,19 +3296,19 @@ function SettingsPanel({
3251
3296
  abort.current = false;
3252
3297
  setLogin({ phase: "starting" });
3253
3298
  void (async () => {
3254
- let start2;
3299
+ let start;
3255
3300
  try {
3256
- start2 = await startDeviceLogin(DEFAULT_API_URL);
3301
+ start = await startDeviceLogin(DEFAULT_API_URL);
3257
3302
  } catch (e) {
3258
3303
  setLogin({ phase: "error", msg: "could not reach SolonGate: " + (e instanceof Error ? e.message : String(e)) });
3259
3304
  return;
3260
3305
  }
3261
- setLogin({ phase: "waiting", url: start2.verifyUrl });
3262
- openBrowser(start2.verifyUrl);
3263
- while (Date.now() < start2.expiresAt && !abort.current) {
3264
- await new Promise((r) => setTimeout(r, start2.intervalMs));
3306
+ setLogin({ phase: "waiting", url: start.verifyUrl });
3307
+ openBrowser(start.verifyUrl);
3308
+ while (Date.now() < start.expiresAt && !abort.current) {
3309
+ await new Promise((r) => setTimeout(r, start.intervalMs));
3265
3310
  if (abort.current) return;
3266
- const res = await pollDeviceLogin(DEFAULT_API_URL, start2.deviceCode);
3311
+ const res = await pollDeviceLogin(DEFAULT_API_URL, start.deviceCode);
3267
3312
  if (res.status === "approved" && res.apiKey) {
3268
3313
  saveAccount({ apiKey: res.apiKey, apiUrl: DEFAULT_API_URL, project: res.project, user: res.user, email: res.email });
3269
3314
  setLogin({ phase: "done", msg: `\u2713 added ${res.email || res.user || res.project || "account"}` });
@@ -3280,6 +3325,57 @@ function SettingsPanel({
3280
3325
  if (!abort.current) setLogin({ phase: "error", msg: "timed out \u2014 press n to retry" });
3281
3326
  })();
3282
3327
  };
3328
+ const openAlertEditor = (channel, rule) => {
3329
+ setEditor({
3330
+ id: rule?.id,
3331
+ channel,
3332
+ target: (channel === "email" ? rule?.emails?.[0] : rule?.telegram?.[0]) ?? "",
3333
+ signal: rule?.signal ?? "any",
3334
+ threshold: rule?.threshold ?? THRESH_MIN,
3335
+ windowSeconds: rule?.windowSeconds ?? 300,
3336
+ field: rule ? 1 : 0
3337
+ // new rule starts on the target; existing on signal
3338
+ });
3339
+ if (!rule) {
3340
+ setInput("");
3341
+ setEditing("alert-target");
3342
+ }
3343
+ };
3344
+ const normTarget = (channel, raw) => {
3345
+ const v = raw.trim();
3346
+ if (channel === "email") {
3347
+ const email = v.replace(/^mailto:/i, "").replace(/^["'<]+|["'>]+$/g, "").replace(/[.,;:]+$/, "").replace(/\s+/g, "");
3348
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return { value: email, error: "that does not look like an e-mail address" };
3349
+ return { value: email };
3350
+ }
3351
+ const tg = v.replace(/^@/, "").replace(/^(chat[\s_-]*)?id[:=\s]*/i, "").replace(/\s+/g, "");
3352
+ if (!/^-?\d{5,}$/.test(tg)) return { value: tg, error: "a telegram chat id is a number \u2014 get yours from @userinfobot" };
3353
+ return { value: tg };
3354
+ };
3355
+ const saveEditor = () => {
3356
+ if (!editor) return;
3357
+ const t = normTarget(editor.channel, editor.target);
3358
+ if (t.error) {
3359
+ setMsg({ text: "\u2717 " + t.error, level: "bad" });
3360
+ return;
3361
+ }
3362
+ const channels = editor.channel === "email" ? { emails: [t.value], telegram: [] } : { telegram: [t.value], emails: [] };
3363
+ const body = { signal: editor.signal, threshold: editor.threshold, windowSeconds: editor.windowSeconds, ...channels };
3364
+ const id = editor.id;
3365
+ setEditor(null);
3366
+ if (id) run("alert updated", () => api.settings.updateAlert(id, body), alertQ.reload);
3367
+ else run(`${editor.channel} alert added`, () => api.settings.createAlert({ name: "dataroom alert", ...body }), alertQ.reload);
3368
+ };
3369
+ const testAlert = (rule) => {
3370
+ run(
3371
+ "alert test sent \u2014 check your " + (alertChannel(rule) ?? "channel"),
3372
+ () => api.settings.sendTestAlert(rule.id).then((r) => {
3373
+ if (!r.delivered.telegram && !r.delivered.email) throw new Error("no channel on this rule");
3374
+ }),
3375
+ () => {
3376
+ }
3377
+ );
3378
+ };
3283
3379
  const activate = (r) => {
3284
3380
  if (r.kind === "acct") {
3285
3381
  setViewCredentials({ apiKey: r.acc.apiKey, apiUrl: r.acc.apiUrl });
@@ -3287,6 +3383,9 @@ function SettingsPanel({
3287
3383
  setMsg({ text: `viewing ${acctLabel(r.acc)}`, level: "ok" });
3288
3384
  } else if (r.kind === "acct-add") {
3289
3385
  beginLogin();
3386
+ } else if (r.kind === "self") {
3387
+ if (!selfProt) return;
3388
+ run(selfProt.enabled ? "self-protection disabled" : "self-protection enabled", () => api.settings.setSelfProtection(!selfProt.enabled), selfQ.reload);
3290
3389
  } else if (r.kind === "ll-enabled") {
3291
3390
  if (!local) return;
3292
3391
  if (!local.enabled && !local.path.trim()) {
@@ -3309,13 +3408,12 @@ function SettingsPanel({
3309
3408
  setInput("");
3310
3409
  setEditing("wh-url");
3311
3410
  } else if (r.kind === "alert") {
3312
- run(r.rule.enabled ? "alert disabled" : "alert enabled", () => api.settings.setAlertEnabled(r.rule.id, !r.rule.enabled), alertQ.reload);
3411
+ const ch = alertChannel(r.rule);
3412
+ if (ch) openAlertEditor(ch, r.rule);
3313
3413
  } else if (r.kind === "alert-add-email") {
3314
- setInput("");
3315
- setEditing("alert-email");
3316
- } else {
3317
- setInput("");
3318
- setEditing("alert-tg");
3414
+ openAlertEditor("email");
3415
+ } else if (r.kind === "alert-add-tg") {
3416
+ openAlertEditor("telegram");
3319
3417
  }
3320
3418
  };
3321
3419
  const submitInput = () => {
@@ -3333,22 +3431,8 @@ function SettingsPanel({
3333
3431
  return;
3334
3432
  }
3335
3433
  run("webhook added", () => api.settings.createWebhook({ url, events: "denials" }), whQ.reload);
3336
- } else if (which === "alert-email") {
3337
- if (!v) return;
3338
- const email = v.replace(/^mailto:/i, "").replace(/^["'<]+|["'>]+$/g, "").replace(/[.,;:]+$/, "").replace(/\s+/g, "").trim();
3339
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
3340
- setMsg({ text: "\u2717 that does not look like an e-mail address", level: "bad" });
3341
- return;
3342
- }
3343
- run("email alert added", () => api.settings.createAlert({ ...ALERT_BASE, emails: [email] }), alertQ.reload);
3344
- } else if (which === "alert-tg") {
3345
- if (!v) return;
3346
- const tg = v.replace(/^@/, "").replace(/^(chat[\s_-]*)?id[:=\s]*/i, "").replace(/\s+/g, "").trim();
3347
- if (!/^-?\d{5,}$/.test(tg)) {
3348
- setMsg({ text: "\u2717 a telegram chat id is a number \u2014 get yours from @userinfobot", level: "bad" });
3349
- return;
3350
- }
3351
- run("telegram alert added", () => api.settings.createAlert({ ...ALERT_BASE, telegram: [tg] }), alertQ.reload);
3434
+ } else if (which === "alert-target" && editor) {
3435
+ setEditor({ ...editor, target: v, field: 1 });
3352
3436
  }
3353
3437
  };
3354
3438
  useInput6(
@@ -3360,6 +3444,35 @@ function SettingsPanel({
3360
3444
  }
3361
3445
  return;
3362
3446
  }
3447
+ if (editor) {
3448
+ if (key.escape) {
3449
+ setEditor(null);
3450
+ return;
3451
+ }
3452
+ if (inp === "e" || key.return && editor.field === 0) {
3453
+ setInput(editor.target);
3454
+ setEditing("alert-target");
3455
+ return;
3456
+ }
3457
+ if (inp === "t" && editor.id) {
3458
+ const rule = alerts.find((a) => a.id === editor.id);
3459
+ if (rule) testAlert(rule);
3460
+ return;
3461
+ }
3462
+ if (key.return) {
3463
+ saveEditor();
3464
+ return;
3465
+ }
3466
+ if (key.upArrow) setEditor({ ...editor, field: (editor.field + EDITOR_FIELDS - 1) % EDITOR_FIELDS });
3467
+ else if (key.downArrow) setEditor({ ...editor, field: (editor.field + 1) % EDITOR_FIELDS });
3468
+ else if (key.leftArrow || key.rightArrow) {
3469
+ const d = key.rightArrow ? 1 : -1;
3470
+ if (editor.field === 1) setEditor({ ...editor, signal: SIGNALS2[(SIGNALS2.indexOf(editor.signal) + d + SIGNALS2.length) % SIGNALS2.length] });
3471
+ else if (editor.field === 2) setEditor({ ...editor, threshold: Math.max(THRESH_MIN, Math.min(THRESH_MAX, editor.threshold + d * 5)) });
3472
+ else if (editor.field === 3) setEditor({ ...editor, windowSeconds: WINDOWS[Math.max(0, Math.min(WINDOWS.length - 1, nearestWindowIdx(editor.windowSeconds) + d))] });
3473
+ }
3474
+ return;
3475
+ }
3363
3476
  setMsg(null);
3364
3477
  if (key.upArrow) {
3365
3478
  setSel((n) => Math.max(0, n - 1));
@@ -3373,28 +3486,41 @@ function SettingsPanel({
3373
3486
  const ok = setActiveAccount({ apiKey: cur.acc.apiKey, apiUrl: cur.acc.apiUrl });
3374
3487
  setMsg(ok ? { text: `\u2713 ${acctLabel(cur.acc)} is now the ACTIVE key (guard + logging)`, level: "ok" } : { text: "\u2717 could not set active", level: "bad" });
3375
3488
  refreshAccounts();
3376
- } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "ll-enabled" || cur.kind === "ll-server") activate(cur);
3489
+ } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "self" || cur.kind === "ll-enabled" || cur.kind === "ll-server") {
3490
+ if (cur.kind === "alert") run(cur.rule.enabled ? "alert disabled" : "alert enabled", () => api.settings.setAlertEnabled(cur.rule.id, !cur.rule.enabled), alertQ.reload);
3491
+ else activate(cur);
3492
+ }
3493
+ } else if (inp === "x" && cur.kind === "acct") {
3494
+ const k = "acct:" + cur.acc.apiKey;
3495
+ if (confirmDel !== k) {
3496
+ setConfirmDel(k);
3497
+ setMsg({ text: `press x again to remove ${acctLabel(cur.acc)} from this device (the cloud account is untouched)`, level: "bad" });
3498
+ return;
3499
+ }
3500
+ setConfirmDel(null);
3501
+ removeAccount(cur.acc.apiKey);
3502
+ setMsg({ text: `\u2713 removed ${acctLabel(cur.acc)} from this device`, level: "ok" });
3503
+ refreshAccounts();
3377
3504
  } else if (inp === "t" && cur.kind === "wh") {
3378
- run(
3379
- "test event sent \u2014 check your endpoint",
3380
- () => api.settings.sendTestWebhook(cur.wh.id).then((r) => {
3381
- if (!r.delivered) throw new Error("endpoint rejected the test (non-2xx response)");
3382
- }),
3383
- whQ.reload
3384
- );
3385
- } else if (inp === "n") beginLogin();
3505
+ run("webhook test sent \u2014 check your endpoint", () => api.settings.sendTestWebhook(cur.wh.id).then((r) => {
3506
+ if (!r.delivered) throw new Error("endpoint rejected the test (non-2xx)");
3507
+ }), whQ.reload);
3508
+ } else if (inp === "t" && cur.kind === "alert") {
3509
+ testAlert(cur.rule);
3510
+ } else if (inp === "e" && cur.kind === "ll-path") activate(cur);
3386
3511
  else if (inp === "e" && cur.kind === "wh") {
3387
3512
  const next = EVENTS[(EVENTS.indexOf(cur.wh.events) + 1) % EVENTS.length];
3388
3513
  run(`webhook events \u2192 ${next}`, () => api.settings.updateWebhook(cur.wh.id, { events: next }), whQ.reload);
3389
- } else if (inp === "e" && cur.kind === "ll-path") activate(cur);
3514
+ } else if (inp === "e" && cur.kind === "alert") {
3515
+ const ch = alertChannel(cur.rule);
3516
+ if (ch) openAlertEditor(ch, cur.rule);
3517
+ } else if (inp === "n") beginLogin();
3390
3518
  else if (inp === "a") {
3391
3519
  if (cur.kind === "wh" || cur.kind === "wh-add") {
3392
3520
  setInput("");
3393
3521
  setEditing("wh-url");
3394
- } else if (cur.kind === "alert" || cur.kind === "alert-add-email" || cur.kind === "alert-add-tg") {
3395
- setInput("");
3396
- setEditing("alert-email");
3397
- }
3522
+ } else if (cur.kind === "alert-add-email") openAlertEditor("email");
3523
+ else if (cur.kind === "alert-add-tg") openAlertEditor("telegram");
3398
3524
  } else if (inp === "d" && (cur.kind === "wh" || cur.kind === "alert")) {
3399
3525
  const k = keyOf(cur);
3400
3526
  if (confirmDel !== k) {
@@ -3434,123 +3560,186 @@ function SettingsPanel({
3434
3560
  ] })
3435
3561
  ] });
3436
3562
  }
3437
- const loading = !locked && (localQ.loading && !localQ.data || whQ.loading && !whQ.data || alertQ.loading && !alertQ.data);
3438
- const error = locked ? null : localQ.error ?? whQ.error ?? alertQ.error;
3439
- const mark = (r) => keyOf(r) === keyOf(cur) && focused ? "\u25B8 " : " ";
3440
- const onOff = (on) => on ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: "on " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "off" });
3441
- const chanOf = (rule) => [...rule.emails ?? [], ...(rule.telegram ?? []).map((t) => "tg " + t)].join(", ") || "\u2014";
3442
- const listBudget = Math.max(4, rows - 10);
3443
- const start = Math.min(Math.max(0, selClamped - Math.floor(listBudget / 2)), Math.max(0, rowsAll.length - listBudget));
3444
- const inWindow = (r) => {
3445
- const i = rowsAll.findIndex((x) => keyOf(x) === keyOf(r));
3446
- return i >= start && i < start + listBudget;
3447
- };
3448
- return /* @__PURE__ */ jsx7(DataView, { loading, error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
3449
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: focused ? "\u2191\u2193 move \xB7 enter/m act \xB7 t test webhook \xB7 n add account \xB7 e events \xB7 d delete \xB7 r refresh" : "press \u2192 to configure" }),
3450
- editing ? /* @__PURE__ */ jsxs7(Box7, { children: [
3451
- /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: editing === "path" ? "local log path: " : editing === "wh-url" ? "webhook url: " : editing === "alert-email" ? "e-mail address: " : "telegram chat id (from @userinfobot): " }),
3452
- /* @__PURE__ */ jsx7(TextInput5, { value: input, onChange: setInput, onSubmit: submitInput })
3453
- ] }) : msg ? /* @__PURE__ */ jsx7(Text7, { color: msg.level === "bad" ? theme.bad : theme.ok, children: truncate(msg.text, cols) }) : login?.msg ? /* @__PURE__ */ jsx7(Text7, { color: login.phase === "error" ? theme.bad : theme.ok, children: login.msg }) : /* @__PURE__ */ jsx7(Text7, { children: " " }),
3454
- /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3563
+ if (editor) {
3564
+ const fieldRow = (idx, label, value, hint) => /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3565
+ /* @__PURE__ */ jsx7(Text7, { color: editor.field === idx ? theme.accentBright : theme.dim, children: editor.field === idx ? "\u25B8 " : " " }),
3566
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: label.padEnd(11) }),
3567
+ value,
3568
+ editor.field === idx && hint ? /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " + hint }) : null
3569
+ ] });
3570
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
3455
3571
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
3456
- "ACCOUNTS ",
3457
- /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
3458
- "\u2014 logged in on this device (",
3459
- accounts.length,
3460
- ") \xB7 \u25CF viewing = shown here \xB7 ACTIVE = key the guard uses"
3461
- ] })
3572
+ editor.id ? "Edit" : "New",
3573
+ " ",
3574
+ editor.channel,
3575
+ " alert"
3462
3576
  ] }),
3463
- accounts.length === 0 ? /* @__PURE__ */ jsxs7(Text7, { children: [
3464
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " no accounts yet \u2014 " }),
3465
- /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: "n" }),
3466
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " (or enter) logs in, pairs this device and unlocks the dataroom" })
3467
- ] }) : null,
3468
- accounts.filter((acc) => inWindow({ kind: "acct", acc })).map((a) => {
3577
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
3578
+ "\u2191\u2193 field \xB7 \u2190\u2192 change \xB7 e edit target \xB7 enter save",
3579
+ editor.id ? " \xB7 t test" : "",
3580
+ " \xB7 esc cancel"
3581
+ ] }),
3582
+ editing === "alert-target" ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
3583
+ /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: editor.channel === "email" ? "e-mail address: " : "telegram chat id (from @userinfobot): " }),
3584
+ /* @__PURE__ */ jsx7(TextInput5, { value: input, onChange: setInput, onSubmit: submitInput })
3585
+ ] }) : msg ? /* @__PURE__ */ jsx7(Text7, { color: msg.level === "bad" ? theme.bad : theme.ok, children: truncate(msg.text, cols) }) : /* @__PURE__ */ jsx7(Text7, { children: " " }),
3586
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3587
+ fieldRow(0, "target", editor.target ? /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate(editor.target, cols - 16) }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "empty \u2014 press e" }), "press e to edit"),
3588
+ fieldRow(1, "signal", /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: SIGNAL_LABEL[editor.signal] }), "\u2190\u2192 change"),
3589
+ fieldRow(2, "threshold", /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: `\u2265 ${editor.threshold}` }), "\u2190\u2192 \xB15"),
3590
+ fieldRow(3, "window", /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: winLabel(editor.windowSeconds) }), "\u2190\u2192 change")
3591
+ ] }),
3592
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
3593
+ "fires when ",
3594
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: SIGNAL_LABEL[editor.signal] }),
3595
+ " reach ",
3596
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: editor.threshold }),
3597
+ " within",
3598
+ " ",
3599
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: winLabel(editor.windowSeconds) })
3600
+ ] }) })
3601
+ ] });
3602
+ }
3603
+ const loading = !locked && (localQ.loading && !localQ.data || whQ.loading && !whQ.data || alertQ.loading && !alertQ.data || guardQ.loading && !guardQ.data || selfQ.loading && !selfQ.data);
3604
+ const error = locked ? null : localQ.error ?? whQ.error ?? alertQ.error ?? guardQ.error ?? selfQ.error;
3605
+ const onOff = (on) => on ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: "on " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "off" });
3606
+ const chanOf = (rule) => [...rule.emails ?? [], ...(rule.telegram ?? []).map((t) => "tg " + t)].join(", ") || "\u2014";
3607
+ const isCur = (r) => keyOf(r) === keyOf(cur) && focused;
3608
+ const cursor = (r) => /* @__PURE__ */ jsx7(Text7, { color: isCur(r) ? theme.accentBright : theme.dim, children: isCur(r) ? "\u25B8 " : " " });
3609
+ const rowLine = (r) => {
3610
+ switch (r.kind) {
3611
+ case "acct": {
3612
+ const a = r.acc;
3469
3613
  const isView = viewApiKey ? a.apiKey === viewApiKey : accounts[0]?.apiKey === a.apiKey;
3470
3614
  const isActive = isActiveAccount(a.apiKey);
3471
3615
  return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3472
- /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "acct:" + a.apiKey && focused ? theme.accentBright : theme.dim, children: mark({ kind: "acct", acc: a }) }),
3616
+ cursor(r),
3473
3617
  /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate(acctLabel(a), 30).padEnd(31) }),
3474
3618
  isView ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: "\u25CF viewing " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " }),
3475
3619
  isActive ? /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: "ACTIVE " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " }),
3476
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: a.project ? truncate(a.project, 24) : "" })
3477
- ] }, a.apiKey);
3478
- }),
3479
- inWindow({ kind: "acct-add" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
3480
- /* @__PURE__ */ jsx7(Text7, { color: cur.kind === "acct-add" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "acct-add" }) }),
3481
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "+ add account (enter \u2014 device login in the browser)" })
3482
- ] }) : null
3483
- ] }),
3484
- locked ? null : /* @__PURE__ */ jsxs7(Fragment5, { children: [
3485
- inWindow({ kind: "ll-enabled" }) || inWindow({ kind: "ll-path" }) || inWindow({ kind: "ll-server" }) ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3486
- /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
3487
- "LOCAL LOGS ",
3488
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2014 hooks mirror every decision to a file on the agent machine" })
3489
- ] }),
3490
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3491
- /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-enabled" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-enabled" }) }),
3492
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "enabled ".padEnd(10) }),
3620
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: a.project ? truncate(a.project, 20) : "" })
3621
+ ] });
3622
+ }
3623
+ case "acct-add":
3624
+ return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3625
+ cursor(r),
3626
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "+ add account (enter \u2014 device login in the browser)" })
3627
+ ] });
3628
+ case "guard":
3629
+ return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3630
+ cursor(r),
3631
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "guard".padEnd(11) }),
3632
+ guard ? /* @__PURE__ */ jsxs7(Fragment5, { children: [
3633
+ /* @__PURE__ */ jsx7(Text7, { color: guard.up_to_date ? theme.ok : theme.warn, children: `v${guard.installed ?? "?"}` }),
3634
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: guard.up_to_date ? " (latest)" : ` \u2192 v${guard.latest} available` }),
3635
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: ` \xB7 ${guard.device_count} device${guard.device_count === 1 ? "" : "s"}${guard.outdated_count ? ` \xB7 ${guard.outdated_count} outdated` : ""}` })
3636
+ ] }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "loading\u2026" })
3637
+ ] });
3638
+ case "self":
3639
+ return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3640
+ cursor(r),
3641
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "self-prot".padEnd(11) }),
3642
+ selfProt ? onOff(selfProt.enabled) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2026" }),
3643
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " blocks agents editing SolonGate\u2019s own hooks/config \xB7 enter toggles" })
3644
+ ] });
3645
+ case "ll-enabled":
3646
+ return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3647
+ cursor(r),
3648
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "enabled ".padEnd(11) }),
3493
3649
  onOff(!!local?.enabled),
3494
3650
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " enter toggles" })
3495
- ] }),
3496
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3497
- /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-path" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-path" }) }),
3498
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "path ".padEnd(10) }),
3651
+ ] });
3652
+ case "ll-path":
3653
+ return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3654
+ cursor(r),
3655
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "path ".padEnd(11) }),
3499
3656
  local?.path ? /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate(local.path, cols - 14) }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "not set \u2014 enter to edit" })
3500
- ] }),
3501
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3502
- /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-server" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-server" }) }),
3503
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "dashboard".padEnd(10) }),
3657
+ ] });
3658
+ case "ll-server":
3659
+ return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3660
+ cursor(r),
3661
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "dashboard".padEnd(11) }),
3504
3662
  srv.running ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: `running 127.0.0.1:${srv.port}` }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "stopped" }),
3505
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: srv.running ? " serves local logs to the dashboard \xB7 survives closing the dataroom \xB7 enter stops" : " enter starts the local-logs link for the dashboard" })
3506
- ] })
3507
- ] }) : null,
3508
- /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3509
- /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
3510
- "WEBHOOKS ",
3511
- /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
3512
- "\u2014 POST every matching event to your endpoint (",
3513
- webhooks.length,
3514
- ")"
3515
- ] })
3516
- ] }),
3517
- webhooks.filter((wh) => inWindow({ kind: "wh", wh })).map((wh) => /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3518
- /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "wh:" + wh.id && focused ? theme.accentBright : theme.dim, children: mark({ kind: "wh", wh }) }),
3519
- onOff(wh.enabled),
3520
- /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: (" " + wh.events).padEnd(9) }),
3521
- /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate(wh.url, cols - 16) })
3522
- ] }, wh.id)),
3523
- inWindow({ kind: "wh-add" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
3524
- /* @__PURE__ */ jsx7(Text7, { color: cur.kind === "wh-add" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "wh-add" }) }),
3663
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: srv.running ? " survives closing the dataroom \xB7 enter stops" : " enter starts the dashboard local-logs link" })
3664
+ ] });
3665
+ case "wh":
3666
+ return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3667
+ cursor(r),
3668
+ onOff(r.wh.enabled),
3669
+ /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: (" " + r.wh.events).padEnd(9) }),
3670
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate(r.wh.url, cols - 16) })
3671
+ ] });
3672
+ case "wh-add":
3673
+ return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3674
+ cursor(r),
3525
3675
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "+ add webhook (enter)" })
3526
- ] }) : null
3527
- ] }),
3528
- /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3529
- /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
3530
- "ALERTS ",
3531
- /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
3532
- "\u2014 notify on a signal spike (",
3533
- alerts.length,
3534
- ")"
3535
- ] })
3536
- ] }),
3537
- alerts.filter((rule) => inWindow({ kind: "alert", rule })).map((rule) => /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3538
- /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "alert:" + rule.id && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert", rule }) }),
3539
- onOff(rule.enabled),
3540
- /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: (" " + rule.signal).padEnd(11) }),
3541
- /* @__PURE__ */ jsx7(Text7, { children: `\u2265${rule.threshold}/${rule.windowSeconds}s ` }),
3542
- /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate(chanOf(rule), cols - 26) })
3543
- ] }, rule.id)),
3544
- inWindow({ kind: "alert-add-email" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
3545
- /* @__PURE__ */ jsx7(Text7, { color: cur.kind === "alert-add-email" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert-add-email" }) }),
3676
+ ] });
3677
+ case "alert":
3678
+ return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3679
+ cursor(r),
3680
+ onOff(r.rule.enabled),
3681
+ /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: (" " + SIGNAL_LABEL[r.rule.signal]).padEnd(13) }),
3682
+ /* @__PURE__ */ jsx7(Text7, { children: `\u2265${r.rule.threshold}/${winLabel(r.rule.windowSeconds)} `.padEnd(11) }),
3683
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate(chanOf(r.rule), cols - 34) })
3684
+ ] });
3685
+ case "alert-add-email":
3686
+ return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3687
+ cursor(r),
3546
3688
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "+ add email alert (enter)" })
3547
- ] }) : null,
3548
- inWindow({ kind: "alert-add-tg" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
3549
- /* @__PURE__ */ jsx7(Text7, { color: cur.kind === "alert-add-tg" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert-add-tg" }) }),
3550
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "+ add telegram alert (enter \u2014 numeric chat id, get it from @userinfobot)" })
3551
- ] }) : null
3552
- ] })
3553
- ] })
3689
+ ] });
3690
+ case "alert-add-tg":
3691
+ return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3692
+ cursor(r),
3693
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "+ add telegram alert (enter \u2014 numeric chat id from @userinfobot)" })
3694
+ ] });
3695
+ }
3696
+ };
3697
+ const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" ? "PROTECTION" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
3698
+ const SECTION_DESC = {
3699
+ ACCOUNTS: `on this device (${accounts.length}) \xB7 \u25CF viewing \xB7 ACTIVE = guard key \xB7 x removes`,
3700
+ PROTECTION: "guard hook version + self-protection toggle",
3701
+ "LOCAL LOGS": "mirror every decision to a file + dashboard link",
3702
+ WEBHOOKS: `POST events to a URL (${webhooks.length}) \xB7 t tests`,
3703
+ ALERTS: `one email + one telegram (${alerts.length}) \xB7 enter edits \xB7 t tests`
3704
+ };
3705
+ const lineEls = [];
3706
+ const lineKey = [];
3707
+ let lastSec = "";
3708
+ rowsAll.forEach((r) => {
3709
+ const sec = sectionOf(r);
3710
+ if (sec !== lastSec) {
3711
+ lastSec = sec;
3712
+ lineEls.push(
3713
+ /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3714
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: theme.accentBright, children: sec }),
3715
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2014 " + SECTION_DESC[sec] })
3716
+ ] }, "h:" + sec)
3717
+ );
3718
+ lineKey.push("");
3719
+ }
3720
+ lineEls.push(/* @__PURE__ */ jsx7(Box7, { children: rowLine(r) }, keyOf(r)));
3721
+ lineKey.push(keyOf(r));
3722
+ });
3723
+ if (hasEmailAlert && hasTgAlert) {
3724
+ lineEls.push(
3725
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " both channels in use \u2014 remove one (d) to change it" }, "both-note")
3726
+ );
3727
+ lineKey.push("");
3728
+ }
3729
+ const budget = Math.max(4, rows - 2);
3730
+ const selLine = Math.max(0, lineKey.indexOf(keyOf(cur)));
3731
+ const maxStart = Math.max(0, lineEls.length - budget);
3732
+ const startL = Math.min(Math.max(0, selLine - Math.floor(budget / 2)), maxStart);
3733
+ const win = lineEls.slice(startL, startL + budget);
3734
+ const moreAbove = startL;
3735
+ const moreBelow = Math.max(0, lineEls.length - (startL + budget));
3736
+ return /* @__PURE__ */ jsx7(DataView, { loading, error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
3737
+ /* @__PURE__ */ jsx7(Text7, { wrap: "truncate", color: theme.dim, children: focused ? `\u2191\u2193 move \xB7 enter act \xB7 m toggle \xB7 t test \xB7 e edit \xB7 x remove \xB7 d delete \xB7 n add${moreAbove ? ` \xB7 \u25B2${moreAbove}` : ""}${moreBelow ? ` \xB7 \u25BC${moreBelow}` : ""}` : "press \u2192 to configure" }),
3738
+ editing ? /* @__PURE__ */ jsxs7(Box7, { children: [
3739
+ /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: editing === "path" ? "local log path: " : "webhook url: " }),
3740
+ /* @__PURE__ */ jsx7(TextInput5, { value: input, onChange: setInput, onSubmit: submitInput })
3741
+ ] }) : msg ? /* @__PURE__ */ jsx7(Text7, { wrap: "truncate", color: msg.level === "bad" ? theme.bad : theme.ok, children: truncate(msg.text, cols) }) : login?.msg ? /* @__PURE__ */ jsx7(Text7, { wrap: "truncate", color: login.phase === "error" ? theme.bad : theme.ok, children: login.msg }) : /* @__PURE__ */ jsx7(Text7, { children: " " }),
3742
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", height: budget, overflow: "hidden", children: win })
3554
3743
  ] }) });
3555
3744
  }
3556
3745
 
@@ -3796,24 +3985,29 @@ function App() {
3796
3985
  }
3797
3986
  const Panel = current.Panel;
3798
3987
  const panelBody = current.label === "Settings" ? /* @__PURE__ */ jsx8(SettingsPanel, { active: true, focused: focus === "panel", viewApiKey: viewKey, onView: viewAccount }) : /* @__PURE__ */ jsx8(Panel, { active: focus === "panel" || current.label !== "Live", focused: focus === "panel" });
3799
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width: cols, height: rows, paddingX: 1, paddingTop: 1, children: [
3800
- /* @__PURE__ */ jsx8(Banner, { cols }),
3801
- /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
3802
- /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "account: " }),
3803
- /* @__PURE__ */ jsx8(Text8, { color: locked ? theme.warn : theme.accentBright, bold: true, children: acctLabel2 }),
3804
- locked ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: " \xB7 log in from Settings to unlock the dataroom" }) : accounts.length > 1 ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: ` (${acctIdx + 1}/${accounts.length} \xB7 a switch \xB7 Settings to manage)` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: " \xB7 Settings to add another" }),
3805
- update2.kind === "updating" ? /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` \u2191 updating to v${update2.version}\u2026` }) : update2.kind === "updated" ? /* @__PURE__ */ jsx8(Text8, { color: theme.ok, bold: true, children: ` \u2191 v${update2.version} installed \u2014 restart (q, then solongate) to apply` }) : update2.kind === "manual" ? /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` \u2191 v${update2.version} available \u2014 npm i -g @solongate/proxy@latest` }) : null
3806
- ] }),
3807
- /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexGrow: 1, children: [
3808
- /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => {
3809
- const isCur = i === effectiveSection;
3810
- const disabled = locked && s.label !== "Settings";
3811
- return /* @__PURE__ */ jsx8(Text8, { color: isCur ? theme.accentBright : disabled ? theme.dim : void 0, bold: isCur, dimColor: disabled, children: (isCur ? "\u25B8 " : disabled ? "\u2298 " : " ") + s.label }, s.label);
3812
- }) }),
3813
- /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, borderStyle: "round", borderColor: focus === "panel" ? theme.accent : "gray", paddingX: 1, paddingY: 0, children: panelBody })
3814
- ] }, viewNonce),
3815
- /* @__PURE__ */ jsx8(Box8, { children: locked ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2192/enter", "open Settings"], ["n", "log in"], ["q", "quit"]] }) : focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ...accounts.length > 1 ? [["a", "account"]] : [], ["?", "help"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2190/esc", "back"], ["\u2191\u2193", "in-panel"], ["space/s", "act"]] }) })
3816
- ] });
3988
+ return (
3989
+ // height rows-1 (not rows): once total output reaches stdout.rows ink stops
3990
+ // diffing and clearTerminal-repaints every frame, which slides the banner.
3991
+ // overflow hidden makes it a hard guarantee even if a panel over-renders.
3992
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width: cols, height: rows - 1, paddingX: 1, paddingTop: 1, overflow: "hidden", children: [
3993
+ /* @__PURE__ */ jsx8(Banner, { cols }),
3994
+ /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
3995
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "account: " }),
3996
+ /* @__PURE__ */ jsx8(Text8, { color: locked ? theme.warn : theme.accentBright, bold: true, children: acctLabel2 }),
3997
+ locked ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: " \xB7 log in from Settings to unlock the dataroom" }) : accounts.length > 1 ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: ` (${acctIdx + 1}/${accounts.length} \xB7 a switch \xB7 Settings to manage)` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: " \xB7 Settings to add another" }),
3998
+ update2.kind === "updating" ? /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` \u2191 updating to v${update2.version}\u2026` }) : update2.kind === "updated" ? /* @__PURE__ */ jsx8(Text8, { color: theme.ok, bold: true, children: ` \u2191 v${update2.version} installed \u2014 restart (q, then solongate) to apply` }) : update2.kind === "manual" ? /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` \u2191 v${update2.version} available \u2014 npm i -g @solongate/proxy@latest` }) : null
3999
+ ] }),
4000
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexGrow: 1, children: [
4001
+ /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => {
4002
+ const isCur = i === effectiveSection;
4003
+ const disabled = locked && s.label !== "Settings";
4004
+ return /* @__PURE__ */ jsx8(Text8, { color: isCur ? theme.accentBright : disabled ? theme.dim : void 0, bold: isCur, dimColor: disabled, children: (isCur ? "\u25B8 " : disabled ? "\u2298 " : " ") + s.label }, s.label);
4005
+ }) }),
4006
+ /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, borderStyle: "round", borderColor: focus === "panel" ? theme.accent : "gray", paddingX: 1, paddingY: 0, overflow: "hidden", children: panelBody })
4007
+ ] }, viewNonce),
4008
+ /* @__PURE__ */ jsx8(Box8, { children: locked ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2192/enter", "open Settings"], ["n", "log in"], ["q", "quit"]] }) : focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ...accounts.length > 1 ? [["a", "account"]] : [], ["?", "help"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2190/esc", "back"], ["\u2191\u2193", "in-panel"], ["space/s", "act"]] }) })
4009
+ ] })
4010
+ );
3817
4011
  }
3818
4012
  var HELP = [
3819
4013
  ["Global", [["\u2191\u2193", "move between sections"], ["\u2192 / enter", "open a section"], ["\u2190 / esc", "back to the menu"], ["?", "this help"], ["q", "quit"]]],