@usex/mikrotik-mcp 4.11.0 → 4.12.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/cli.js +26 -3
- package/dist/ui/observability.html +1 -1
- package/package.json +1 -1
- package/schemas/tool-catalog.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -112,6 +112,7 @@ import {
|
|
|
112
112
|
import { existsSync as existsSync2 } from "fs";
|
|
113
113
|
|
|
114
114
|
// src/observability/dashboard.ts
|
|
115
|
+
import { spawn } from "child_process";
|
|
115
116
|
import { readFileSync as readFileSync2 } from "fs";
|
|
116
117
|
import { homedir, networkInterfaces } from "os";
|
|
117
118
|
import { dirname as dirname4, join as join2 } from "path";
|
|
@@ -1473,6 +1474,24 @@ var JSON_HEADERS3 = { "content-type": "application/json; charset=utf-8" };
|
|
|
1473
1474
|
function json3(body, status = 200) {
|
|
1474
1475
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS3 });
|
|
1475
1476
|
}
|
|
1477
|
+
function restartProcess() {
|
|
1478
|
+
let ok = false;
|
|
1479
|
+
try {
|
|
1480
|
+
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
1481
|
+
cwd: process.cwd(),
|
|
1482
|
+
env: { ...process.env, MIKROTIK_RESTART_DELAY_MS: "1500" },
|
|
1483
|
+
detached: true,
|
|
1484
|
+
stdio: "inherit"
|
|
1485
|
+
});
|
|
1486
|
+
child.unref();
|
|
1487
|
+
ok = true;
|
|
1488
|
+
logger.warn("Dashboard requested a restart \u2014 relaunched a fresh server process; this one is exiting.");
|
|
1489
|
+
} catch (e) {
|
|
1490
|
+
logger.error(`Restart relaunch failed: ${e instanceof Error ? e.message : String(e)}. Exiting anyway.`);
|
|
1491
|
+
}
|
|
1492
|
+
setTimeout(() => process.exit(0), 500);
|
|
1493
|
+
return ok;
|
|
1494
|
+
}
|
|
1476
1495
|
function dashboardHtml() {
|
|
1477
1496
|
try {
|
|
1478
1497
|
return readFileSync2(join2(UI_DIST_DIR, "observability.html"), "utf8");
|
|
@@ -1774,12 +1793,11 @@ async function configRoutes(req, url, admin) {
|
|
|
1774
1793
|
if (p === "/api/reload" && req.method === "POST") {
|
|
1775
1794
|
const body = await readJson(req);
|
|
1776
1795
|
if (body?.hard === true) {
|
|
1777
|
-
|
|
1778
|
-
setTimeout(() => process.exit(0), 300);
|
|
1796
|
+
const relaunched = restartProcess();
|
|
1779
1797
|
return json3({
|
|
1780
1798
|
ok: true,
|
|
1781
1799
|
mode: "restart",
|
|
1782
|
-
note: "
|
|
1800
|
+
note: relaunched ? "Restarting now \u2014 a fresh server process was launched and will bind the same port(s) in ~1.5s. Reconnect shortly." : "Could not self-relaunch; the process is exiting. It only comes back if a supervisor respawns it."
|
|
1783
1801
|
});
|
|
1784
1802
|
}
|
|
1785
1803
|
try {
|
|
@@ -3259,6 +3277,11 @@ async function main() {
|
|
|
3259
3277
|
if (command === "auth-check") {
|
|
3260
3278
|
process.exit(await authCheck());
|
|
3261
3279
|
}
|
|
3280
|
+
const restartDelayMs = Number.parseInt(process.env.MIKROTIK_RESTART_DELAY_MS ?? "", 10);
|
|
3281
|
+
if (Number.isFinite(restartDelayMs) && restartDelayMs > 0) {
|
|
3282
|
+
logger.info(`Restart: waiting ${restartDelayMs}ms for the previous process to release its ports\u2026`);
|
|
3283
|
+
await new Promise((resolve) => setTimeout(resolve, restartDelayMs));
|
|
3284
|
+
}
|
|
3262
3285
|
const cfg = loadConfig();
|
|
3263
3286
|
setConfig(cfg);
|
|
3264
3287
|
await printBanner();
|
|
@@ -151,7 +151,7 @@ ${n.map(([e,n])=>{let r=n.theme?.[t]??n.color;return r?` --color-${e}: ${r};`:n
|
|
|
151
151
|
<oli>`),t=t.replace(/((?:<oli>.*<\/oli>\n?)+)/g,e=>`<ol>${e.trimEnd().replace(/<\/?oli>/g,e=>e.replace(`oli`,`li`))}</ol>`),t=t.replace(RegExp(`^${H9}[-*+] (.+)$`,`gm`),`<li>$1</li>`),t=t.replace(/<\/li>\n\n+<li>/g,`</li>
|
|
152
152
|
<li>`),t=t.replace(/((?:<li>.*<\/li>\n?)+)/g,e=>`<ul>${e.trimEnd()}</ul>`),t=t.replace(/<li>\[x\] (.+?)<\/li>/gi,`<li class="wn-task done"><span class="wn-check">✓</span> $1</li>`),t=t.replace(/<li>\[ \] (.+?)<\/li>/g,`<li class="wn-task"><span class="wn-check">☐</span> $1</li>`);let i=/^<\/?(h[2-5]|ul|ol|li|pre|blockquote|hr|div|table|img)/;return t=t.split(`
|
|
153
153
|
`).map(e=>{let t=e.trim();return t?i.test(t)?t:`<p>${t}</p>`:``}).join(`
|
|
154
|
-
`),t=t.replace(/%%CB(\d+)%%/g,(e,t)=>n[Number(t)]),t=t.replace(/%%IC(\d+)%%/g,(e,t)=>r[Number(t)]),t}var U9=`mt-whats-new-dismissed`,W9=`mt-whats-new-seen-version`;function qee(e){try{return localStorage.getItem(U9)===e}catch{return!1}}function Jee(e){try{localStorage.setItem(U9,e)}catch{}}function Yee(e){try{let t=localStorage.getItem(W9);return localStorage.setItem(W9,e),t!==null&&t!==e}catch{return!1}}var Xee=`text-[13px] leading-[1.7] text-muted-foreground [&_h2]:mt-[18px] [&_h2]:mb-1.5 [&_h2]:text-[17px] [&_h2]:font-bold [&_h2]:text-foreground [&_h3]:mt-[18px] [&_h3]:mb-1.5 [&_h3]:text-[15px] [&_h3]:font-bold [&_h3]:text-foreground [&_h4]:mt-[18px] [&_h4]:mb-1.5 [&_h4]:text-[14px] [&_h4]:font-bold [&_h4]:text-foreground [&_h5]:mt-[18px] [&_h5]:mb-1.5 [&_h5]:text-[14px] [&_h5]:font-bold [&_h5]:text-foreground [&_p]:my-[5px] [&_ul]:my-[5px] [&_ul]:list-disc [&_ul]:pl-[18px] [&_ol]:my-[5px] [&_ol]:list-decimal [&_ol]:pl-[22px] [&_li]:my-[3px] [&_li]:marker:text-brand [&_img]:my-1.5 [&_img]:max-w-full [&_img]:rounded-md [&_img]:border [&_img]:border-border [&_code]:rounded [&_code]:border [&_code]:border-border [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs [&_code]:text-brand [&_pre]:my-2 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:border [&_pre]:border-border [&_pre]:bg-background [&_pre]:p-3 [&_pre_code]:border-0 [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-muted-foreground [&_a]:text-brand [&_a]:underline [&_a]:underline-offset-2 [&_strong]:font-semibold [&_strong]:text-foreground [&_blockquote]:my-2 [&_blockquote]:border-l-[3px] [&_blockquote]:border-brand/40 [&_blockquote]:px-3.5 [&_blockquote]:py-1.5 [&_blockquote]:italic [&_hr]:my-3.5 [&_hr]:border-0 [&_hr]:border-t [&_hr]:border-border [&_del]:text-muted-foreground [&_del]:opacity-60 [&_table]:w-full [&_table]:border-collapse [&_table]:font-mono [&_table]:text-xs [&_th]:border-b [&_th]:border-border [&_th]:bg-muted [&_th]:px-3 [&_th]:py-[7px] [&_th]:text-left [&_th]:text-[11px] [&_th]:font-semibold [&_th]:tracking-wide [&_th]:text-foreground [&_th]:uppercase [&_td]:border-b [&_td]:border-border [&_td]:px-3 [&_td]:py-[7px] [&_td]:text-muted-foreground [&_.wn-table-wrap]:my-2.5 [&_.wn-table-wrap]:overflow-x-auto [&_.wn-table-wrap]:rounded-md [&_.wn-table-wrap]:border [&_.wn-table-wrap]:border-border [&_.wn-task]:-ml-[18px] [&_.wn-task]:list-none [&_.wn-check]:inline-block [&_.wn-check]:w-4 [&_.wn-task.done_.wn-check]:text-success`,Zee=600*1e3;function Qee(e){let[t,n]=(0,v.useState)(null),[r,i]=(0,v.useState)(!1);(0,v.useEffect)(()=>{if(!e)return;let t=Yee(e),r=null,a=()=>{mc(`/api/releases/latest`).then(e=>{n(e);let a=r===null,o=r!==null&&e.version!==r;if(r=e.version,a&&t){setTimeout(()=>i(!0),1200);return}e.isNewer&&!qee(e.version)&&(a||o)&&setTimeout(()=>i(!0),a?1200:0)}).catch(()=>{})};a();let o=setInterval(a,Zee);return()=>clearInterval(o)},[e]);let a=(0,v.useCallback)(()=>{t?.isNewer&&Jee(t.version),i(!1)},[t]),o=(0,v.useCallback)(()=>i(!0),[]);return{release:t,showModal:r,showIndicator:t!==null&&t.isNewer,openModal:o,dismissRelease:a}}function $ee({release:e,onDismiss:t}){let n=new Date(e.publishedAt).toLocaleDateString(`en-US`,{month:`short`,day:`numeric`,year:`numeric`}),r={__html:Kee(e.body)};return(0,K.jsx)(Ree,{open:!0,onOpenChange:e=>{e||t()},children:(0,K.jsxs)(Vee,{className:`max-h-[calc(100vh-80px)] gap-0 overflow-y-auto p-0 sm:max-w-[520px]`,children:[(0,K.jsxs)(Hee,{className:`items-center gap-4 px-8 pt-10 pb-6 text-center`,children:[(0,K.jsx)(`span`,{className:`inline-block rounded-full border border-brand/30 bg-brand/10 px-3.5 py-1 font-mono text-[10px] font-bold tracking-[0.2em] text-brand uppercase`,children:e.isNewer?`NEW RELEASE`:`RELEASE NOTES`}),(0,K.jsxs)(Wee,{className:`text-[44px] leading-none font-extrabold tracking-tight text-brand`,children:[`v`,e.version]}),(0,K.jsxs)(Gee,{className:`font-mono text-xs text-muted-foreground`,children:[`Released `,n,e.isNewer&&(0,K.jsxs)(K.Fragment,{children:[` · you’re on v`,e.currentVersion]})]})]}),(0,K.jsx)(`div`,{className:Y(`px-8 py-5`,Xee),dangerouslySetInnerHTML:r}),(0,K.jsxs)(Uee,{className:`flex-row items-center justify-between border-t border-border px-8 pt-3.5 pb-6`,children:[(0,K.jsxs)(`a`,{href:e.url,target:`_blank`,rel:`noopener noreferrer`,className:`inline-flex items-center gap-1.5 font-mono text-xs text-muted-foreground hover:text-foreground`,children:[`View on GitHub`,(0,K.jsx)(Rc,{className:`size-3.5`})]}),(0,K.jsx)(X,{type:`default`,size:`sm`,onClick:t,children:`Got it`})]})]})})}zi.registerPlugin(G);function G9(e,t,n){let r=URL.createObjectURL(new Blob([t],{type:`${n};charset=utf-8`})),i=document.createElement(`a`);i.href=r,i.download=e,document.body.append(i),i.click(),i.remove(),URL.revokeObjectURL(r)}var K9=[{id:`overview`,label:`Overview`,sub:`Calls, latency & risk at a glance`},{id:`devices`,label:`Devices`,sub:`Connectivity radar & system health`},{id:`clients`,label:`Clients`,sub:`Connected LAN devices — usage, block/allow, pin IP`},{id:`aaa`,label:`RADIUS & UM`,sub:`RADIUS client & User Manager RADIUS server`},{id:`topology`,label:`Topology`,sub:`Layer-2 neighbours via MNDP / CDP / LLDP`},{id:`packets`,label:`Packets`,sub:`Live TZSP capture & decode`},{id:`snapshots`,label:`Snapshots`,sub:`Config history & time-travel diff`},{id:`drift`,label:`Drift Guard`,sub:`Golden config baselines & live drift detection`},{id:`plan`,label:`Change Plan`,sub:`Dry-run intended RouterOS commands`},{id:`s3`,label:`S3 Backups`,sub:`List, download & delete S3 backup objects`},{id:`backups`,label:`Backups`,sub:`Local config vault — create, restore, manage`},{id:`modules`,label:`Modules`,sub:`Enable/disable tool modules — curate the surface`},{id:`config`,label:`Config`,sub:`Effective configuration & safe editor`},{id:`memory`,label:`Memory`,sub:`Knowledge graph — entities, relations & observations`},{id:`feed`,label:`Live Feed`,sub:`Every tool call, in real time`}],q9=new Set(K9.map(e=>e.id)),J9=`mt-view`;function Y9(){try{let e=location.hash.replace(/^#\/?/,``);return q9.has(e)?e:null}catch{return null}}function ete(){let e=Y9();if(e)return e;try{let e=localStorage.getItem(J9);if(e&&q9.has(e))return e}catch{}return`overview`}var X9=[`var(--foreground)`,`var(--muted-foreground)`],Z9={overview:X9,devices:X9,clients:X9,aaa:X9,topology:X9,packets:X9,snapshots:X9,drift:X9,plan:X9,s3:X9,backups:X9,modules:X9,config:X9,memory:X9,feed:X9},tte={overview:{what:`A live pulse of all MCP tool activity: total calls, error rate, p50/p95 latency, the busiest tools, and a risk breakdown — over a time window you choose.`,tips:[`Change the time window (top-right) to zoom from the last 5 minutes out to 24 hours.`,`The risk donut splits calls by annotation: read · write · destructive · dangerous.`,`A rising error line usually points at one device or one tool — jump to Live Feed to see which.`]},devices:{what:`Every configured router with its live reachability (SSH or MAC-Telnet), latency, identity, and system health — CPU, memory and disk — refreshed continuously.`,tips:[`Each device gets a stable colour so you can track it across the connectivity radar.`,`Health (CPU/Mem/Disk) is probed periodically; MAC-Telnet devices are probed on a slower cadence.`,`Latency tiers are colour-coded green → amber → red; a grey node is currently unreachable.`,`The SSH Connection Pool panel shows persistent connections: green = idle/ready, blue = busy with inflight channels, red = reconnecting.`]},clients:{what:`The LAN devices connected to a router — merged from its DHCP leases and ARP table — with live Download/Upload charts, and one-click controls to block/allow a device, pin (reserve) its IP, change that IP, or relabel it.`,tips:[`Pick the router (top-right) to inspect its connected devices; filter by IP, MAC or name.`,`Click a device to open its live ↓/↑ traffic chart — needs a simple queue targeting its IP.`,`Block/allow is enforced by MAC, so it survives the device changing IP; “Pin IP” makes its lease static.`]},aaa:{what:"Full management of the router's RADIUS client (`/radius`) and the built-in User Manager RADIUS server (`/user-manager`): RADIUS servers + incoming CoA, and User Manager users, service profiles, rate/quota limitations, NAS clients, profile assignments, accounting sessions, and global settings.",tips:[`Pick the router (top-right), then switch tabs across RADIUS, Users, Profiles, Limitations, NAS, Assignments, Sessions and Settings.`,`Every tab is full CRUD: add, edit, enable/disable and remove — secrets are write-only and shown redacted.`,`The Usage & Heatmap tab shows each user's 3-month download/upload and a GitHub-style connection heatmap, persisted locally.`,`If a device lacks the user-manager package, the User Manager tabs explain how to install it; RADIUS-client tabs still work.`]},topology:{what:`A Layer-2 map of neighbours each router discovers via MNDP / CDP / LLDP — the physical adjacency of your network, drawn live.`,tips:[`Solid nodes are configured devices; faint nodes are discovered-but-unmanaged neighbours.`,`Use “Add to config →” on an unmanaged neighbour to pre-fill it in the Config editor.`,`Drag to pan; the layout settles automatically as new neighbours arrive.`]},packets:{what:`Live packet capture streamed from a router over TZSP — decode headers in real time without leaving the dashboard.`,tips:[`Pick a device and start the capture; packets decode as they arrive.`,`Stop the capture when done — it frees the router-side sniffer.`,`Great for debugging a protocol issue alongside the Live Feed of tool calls.`]},snapshots:{what:`Point-in-time captures of a device’s full configuration (/export), stored locally so you can diff any two and see exactly what changed.`,tips:[`Capture a snapshot before a risky change, then diff after to audit the delta.`,`The diff is line-level: green added, red removed.`,`Snapshots are device config exports — for the dashboard’s OWN config history see the Config page.`]},drift:{what:`Detect when router configs drift from their golden baseline — with change attribution and one-click reconciliation.`,tips:[`Set a baseline via MCP tools (config_set_baseline) or the Baseline Manager below.`,`Click a device card to run a live drift check against its golden config.`,`Promote accepted changes as the new baseline, or reconcile to roll back.`]},plan:{what:`Dry-run the exact RouterOS commands a change would run before it touches a device — a change plan you can review and trust.`,tips:[`Paste or build intended commands to see them validated and ordered.`,`Nothing is sent to the device from here — it’s a preview.`,`Pair with Safe Mode (auto-revert) when you do apply for real.`]},s3:{what:`Browse, download, and delete backup objects in your configured S3-compatible bucket — your off-box archive of device backups and exports.`,tips:[`Filter by key prefix to find a device’s backups quickly.`,`Download fetches the object through a presigned URL; delete is permanent.`,`For host-side .rsc backups instead, use the Backups page.`]},backups:{what:`A local config vault on the MCP server: capture a device’s /export as a timestamped .rsc file, then download, upload, rename, restore (via Safe Mode), or delete it.`,tips:[`Restore offers a dry-run (applies then rolls back) before you commit for real.`,`Edit the vault path inline in the header — it’s saved to your config.`,`Filenames are stamped in the device’s local 24-hour clock.`]},modules:{what:"Every tool module in the catalog with a live on/off switch. Toggling one writes your config file's `tools` block (disabledModules / enabledModules) and applies it immediately, so you can curate exactly which scopes the MCP server exposes.",tips:[`MCP clients search-rank tools and degrade past ~100; trim the surface below ~150–200 tools so every remaining tool is reliably findable.`,`Disable adds the module to tools.disabledModules; enable removes it (or adds it to an active allow-list) — your config file is updated live on each toggle.`,`Changes need an MCP client reconnect (or a server restart) to actually shrink/grow the visible tool list.`]},config:{what:`View the effective configuration, edit it safely with schema-aware validation and auto-rollback, browse a full field guide, and travel through config version history.`,tips:[`Every successful apply is auto-saved to the version timeline — restore any point in time.`,`Save a named checkpoint before a big change for an easy, labelled rollback.`,`The Field Guide documents every config option, its type, and default — straight from the schema.`]},memory:{what:`A persistent knowledge graph the AI builds across sessions — entities (routers, subnets, users), relations between them, and free-text observations.`,tips:[`Entities are created by the AI via MCP tools — you can browse and delete them here.`,`The graph visualization shows entities as circles and relations as directed edges; click a node to inspect it.`,`Change the database path to switch between different knowledge bases.`]},feed:{what:`Every tool call as it happens — tool, device, risk, duration, and success/error — with full request/response detail on click.`,tips:[`Filter by status to isolate failures, or by tool/device to follow one thread.`,`Click any row to open the full (secret-redacted) request and response.`,`Pause the stream when you want to inspect without rows shifting under you.`]}};function Q9({icon:e,title:t,sub:n,body:r,className:i}){return(0,K.jsxs)(`div`,{className:Y(`text-muted-foreground flex flex-col items-center gap-2 rounded-lg border border-dashed px-6 py-14 text-center`,i),children:[e,(0,K.jsx)(`p`,{className:`text-foreground text-sm font-medium`,children:t}),n!=null&&(0,K.jsx)(`p`,{className:`max-w-md text-xs`,children:n}),r]})}function nte({view:e}){let t=tte[e];return(0,K.jsxs)(`div`,{className:`bg-card reveal flex gap-3 rounded-lg border p-4`,role:`region`,"aria-label":`Page help`,children:[(0,K.jsx)(Nc,{className:`text-brand mt-0.5 size-4 shrink-0`,"aria-hidden":`true`}),(0,K.jsxs)(`div`,{className:`flex min-w-0 flex-col gap-2`,children:[(0,K.jsx)(`p`,{className:`text-sm`,children:t.what}),(0,K.jsx)(`ul`,{className:`text-muted-foreground list-disc space-y-1 pl-4 text-xs`,children:t.tips.map((e,t)=>(0,K.jsx)(`li`,{children:e},t))})]})]})}function rte(){let[e,t]=(0,v.useState)(`idle`),[n,r]=(0,v.useState)(``),[i,a]=(0,v.useState)(!1);async function o(e){t(`working`),r(e?`Restarting…`:`Reloading…`);try{let n=await hc(`/api/reload`,{hard:e});n?.ok?(t(`ok`),r(e?`Restarting — reconnect shortly`:`Reloaded · ${n.count??`?`} device${n.count===1?``:`s`}`)):(t(`err`),r(n?.error??`Reload failed`))}catch{t(`err`),r(`Reload failed (server unreachable)`)}a(!1),setTimeout(()=>t(`idle`),5e3)}function s(){if(!i){a(!0),setTimeout(()=>a(!1),4e3);return}o(!0)}return(0,K.jsxs)(`div`,{className:`flex flex-col gap-1`,children:[(0,K.jsxs)(`button`,{type:`button`,onClick:()=>void o(!1),disabled:e===`working`,title:`Re-read the config from disk and apply it live (no downtime)`,className:Y(`flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-[12px] font-medium transition-colors`,`text-muted-foreground hover:bg-accent/50 hover:text-foreground disabled:opacity-60`),children:[(0,K.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,"aria-hidden":`true`,className:`size-3.5`,children:(0,K.jsxs)(`g`,{stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,K.jsx)(`path`,{d:`M21 12a9 9 0 1 1-2.64-6.36`}),(0,K.jsx)(`path`,{d:`M21 3v6h-6`})]})}),(0,K.jsx)(`span`,{children:e===`working`?`Reloading…`:`Reload Server`})]}),e!==`idle`&&(0,K.jsx)(`small`,{className:Y(`text-[11px]`,e===`err`?`text-destructive`:`text-muted-foreground`),children:n}),(0,K.jsx)(`button`,{type:`button`,onClick:s,disabled:e===`working`,title:`Fully restart the server process (needs a supervisor to respawn; drops the connection)`,className:Y(`self-start text-[10px] underline decoration-dotted underline-offset-2 disabled:opacity-60`,i?`text-destructive font-medium`:`text-muted-foreground hover:text-foreground`),children:i?`click again to confirm restart`:`restart process…`})]})}function ite({name:e}){return(0,K.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.7`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,children:{overview:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`rect`,{x:`3`,y:`3`,width:`8`,height:`8`,rx:`1.6`}),(0,K.jsx)(`rect`,{x:`13`,y:`3`,width:`8`,height:`5`,rx:`1.6`}),(0,K.jsx)(`rect`,{x:`13`,y:`10`,width:`8`,height:`11`,rx:`1.6`}),(0,K.jsx)(`rect`,{x:`3`,y:`13`,width:`8`,height:`8`,rx:`1.6`})]}),devices:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`rect`,{x:`3`,y:`4`,width:`18`,height:`7`,rx:`2`}),(0,K.jsx)(`rect`,{x:`3`,y:`13`,width:`18`,height:`7`,rx:`2`}),(0,K.jsx)(`path`,{d:`M7 7.5h.01M7 16.5h.01`})]}),clients:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`circle`,{cx:`9`,cy:`8`,r:`3`}),(0,K.jsx)(`path`,{d:`M3.5 19a5.5 5.5 0 0 1 11 0`}),(0,K.jsx)(`path`,{d:`M16 7.5a2.5 2.5 0 0 1 0 5M17.5 19a4.5 4.5 0 0 0-2-3.6`})]}),aaa:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`path`,{d:`M12 3 4 6v5c0 4.4 3.2 7.6 8 9 4.8-1.4 8-4.6 8-9V6l-8-3Z`}),(0,K.jsx)(`path`,{d:`M9.5 11.5 11 13l3.5-3.5`})]}),topology:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`circle`,{cx:`12`,cy:`5`,r:`2.4`}),(0,K.jsx)(`circle`,{cx:`5`,cy:`19`,r:`2.4`}),(0,K.jsx)(`circle`,{cx:`19`,cy:`19`,r:`2.4`}),(0,K.jsx)(`path`,{d:`M12 7.4 6.4 16.6M12 7.4 17.6 16.6`})]}),packets:(0,K.jsx)(`path`,{d:`M3 12h4l2-7 4 14 2-7h6`}),snapshots:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`path`,{d:`M12 3 3 7.5 12 12 21 7.5 12 3Z`}),(0,K.jsx)(`path`,{d:`M3 12 12 16.5 21 12`}),(0,K.jsx)(`path`,{d:`M3 16.5 12 21 21 16.5`})]}),drift:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`path`,{d:`M12 22c5.5 0 10-4.5 10-10S17.5 2 12 2 2 6.5 2 12s4.5 10 10 10Z`}),(0,K.jsx)(`path`,{d:`M9 12l2 2 4-4`}),(0,K.jsx)(`path`,{d:`M12 6v2M12 16v2M6 12h2M16 12h2`})]}),plan:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`circle`,{cx:`6`,cy:`6`,r:`2.3`}),(0,K.jsx)(`circle`,{cx:`6`,cy:`18`,r:`2.3`}),(0,K.jsx)(`circle`,{cx:`18`,cy:`8`,r:`2.3`}),(0,K.jsx)(`path`,{d:`M6 8.3v7.4M8.3 6H13a3 3 0 0 1 3 3v0`})]}),s3:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`ellipse`,{cx:`12`,cy:`6`,rx:`7`,ry:`2.6`}),(0,K.jsx)(`path`,{d:`M5 6v12c0 1.5 3.1 2.6 7 2.6s7-1.1 7-2.6V6`}),(0,K.jsx)(`path`,{d:`M5 12c0 1.5 3.1 2.6 7 2.6s7-1.1 7-2.6`})]}),backups:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`path`,{d:`M3 6.5 5 3.5h14l2 3`}),(0,K.jsx)(`rect`,{x:`3`,y:`6.5`,width:`18`,height:`14`,rx:`2`}),(0,K.jsx)(`path`,{d:`M9.5 12h5`})]}),modules:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`rect`,{x:`3`,y:`5`,width:`18`,height:`6`,rx:`3`}),(0,K.jsx)(`circle`,{cx:`8`,cy:`8`,r:`1.5`}),(0,K.jsx)(`rect`,{x:`3`,y:`13`,width:`18`,height:`6`,rx:`3`}),(0,K.jsx)(`circle`,{cx:`16`,cy:`16`,r:`1.5`})]}),config:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`path`,{d:`M4 7h8M16 7h4M4 17h4M12 17h8`}),(0,K.jsx)(`circle`,{cx:`14`,cy:`7`,r:`2.2`}),(0,K.jsx)(`circle`,{cx:`10`,cy:`17`,r:`2.2`})]}),memory:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`circle`,{cx:`12`,cy:`6`,r:`2`}),(0,K.jsx)(`circle`,{cx:`6`,cy:`14`,r:`2`}),(0,K.jsx)(`circle`,{cx:`18`,cy:`14`,r:`2`}),(0,K.jsx)(`circle`,{cx:`12`,cy:`20`,r:`2`}),(0,K.jsx)(`path`,{d:`M12 8v-0.5M12 8 7.2 12.5M12 8 16.8 12.5M6 16 10.5 18.5M18 16 13.5 18.5`})]}),feed:(0,K.jsx)(`path`,{d:`M4 6h16M4 12h16M4 18h10`})}[e]})}function ate(){let[e,t]=(0,v.useState)(null),[n,r]=(0,v.useState)(null),[i,a]=(0,v.useState)(null),[o,s]=(0,v.useState)(null),c=Qee(n?.version),[l,u]=(0,v.useState)(null),[d,f]=(0,v.useState)(null),[p,m]=(0,v.useState)(!1),[h,g]=(0,v.useState)(null),[_,y]=(0,v.useState)([]),[b,x]=(0,v.useState)(36e5),S=Number(d?.dashboard?.feedLimit)||200,C=(0,v.useCallback)(async e=>{if(!d)return;let t={...d.dashboard,feedLimit:e};await hc(`/api/config`,{config:{...d,dashboard:t},rollbackMs:0}).catch(()=>{}),await mc(`/api/config`).then(f).catch(()=>{})},[d]),[w,T]=(0,v.useState)(!1),[E,D]=(0,v.useState)(`off`),[O,k]=(0,v.useState)(null),[A,j]=(0,v.useState)(()=>new Set),[M,N]=(0,v.useState)({}),[P,F]=(0,v.useState)({tool:``,risk:``,device:``,status:``,q:``}),I=(0,v.useRef)(w);I.current=w;let L=(0,v.useRef)(null);l9(L);let[R,ee]=(0,v.useState)(ete),te=(0,v.useCallback)(e=>{ee(e);try{Y9()!==e&&(location.hash=e),localStorage.setItem(J9,e)}catch{}},[]);(0,v.useEffect)(()=>{if(Y9()!==R)try{location.hash=R}catch{}let e=()=>{let e=Y9();e&&te(e)};return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]);let[z,B]=(0,v.useState)(()=>{try{let e=localStorage.getItem(`mt-help-open`);return new Set(e?JSON.parse(e):[])}catch{return new Set}}),V=e=>B(t=>{let n=new Set(t);n.has(e)?n.delete(e):n.add(e);try{localStorage.setItem(`mt-help-open`,JSON.stringify([...n]))}catch{}return n}),[ne,re]=(0,v.useState)(``),[ie,ae]=(0,v.useState)(`all`),oe=(0,v.useMemo)(()=>{let e=i?.devices??[];return{online:e.filter(e=>e.status.reachable===!0).length,offline:e.filter(e=>e.status.reachable===!1).length,total:e.length}},[i]),se=(0,v.useMemo)(()=>{let e=i?.devices??[],t=ne.trim().toLowerCase();return e.filter(e=>!(ie===`online`&&e.status.reachable!==!0||ie===`offline`&&e.status.reachable!==!1||t&&!e.name.toLowerCase().includes(t)&&!(e.address??e.host??``).toLowerCase().includes(t)))},[i,ne,ie]);c9((0,v.useCallback)(e=>{if(I.current)return;y(t=>[e,...t].slice(0,yb));let t=e.device;t&&N(e=>({...e,[t]:(e[t]??0)+1}))},[]),(0,v.useCallback)(e=>D(e),[])),(0,v.useEffect)(()=>{mc(`/api/events?limit=${yb}`).then(e=>y(e.events)).catch(()=>{})},[]);let H=(0,v.useCallback)(()=>{mc(`/api/stats?window=${b}&buckets=60`).then(t).catch(()=>{})},[b]);(0,v.useEffect)(()=>{H();let e=()=>{H(),mc(`/api/meta`).then(r).catch(()=>{}),mc(`/api/devices`).then(a).catch(()=>{}),mc(`/api/ssh-pool`).then(s).catch(()=>{}),mc(`/api/topology`).then(u).catch(()=>{})};e();let t=setInterval(()=>{I.current||e()},4e3);return()=>clearInterval(t)},[H]),(0,v.useEffect)(()=>{let e=()=>void mc(`/api/config`).then(f).catch(()=>{});e();let t=setInterval(e,3e4);return()=>clearInterval(t)},[]);let ce=(0,v.useCallback)((e,t)=>{hc(`/api/devices/toggle`,{device:e,disabled:t}).then(e=>{a(e)})},[]);(0,v.useEffect)(()=>{let e=e=>{e.key===`Escape`&&k(null)};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[]);let le=(0,v.useMemo)(()=>{let e=P.q.trim().toLowerCase();return _.filter(t=>!(P.tool&&t.tool!==P.tool||P.risk&&t.risk!==P.risk||P.device&&t.device!==P.device||P.status===`ok`&&t.isError||P.status===`error`&&!t.isError||e&&!t.tool.toLowerCase().includes(e)&&!t.input.toLowerCase().includes(e)&&!t.output.toLowerCase().includes(e)&&!(t.error??``).toLowerCase().includes(e))).sort((e,t)=>t.ts-e.ts)},[_,P]),ue=!!(P.tool||P.risk||P.device||P.status||P.q),de=(0,v.useMemo)(()=>_.filter(e=>e.isError),[_]),fe=(0,v.useMemo)(()=>({ok:_.length-de.length,error:de.length}),[_,de]),pe=(0,v.useCallback)(async e=>{try{k(await mc(`/api/event/${encodeURIComponent(e.id)}`))}catch{k(e)}},[]),me=(0,v.useMemo)(()=>le.slice(0,S),[le,S]),he=(0,v.useMemo)(()=>me.map(e=>e.id),[me]),ge=he.length>0&&he.every(e=>A.has(e)),_e=!ge&&he.some(e=>A.has(e)),ve=(0,v.useCallback)(e=>{j(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},[]),ye=(0,v.useCallback)(()=>{j(e=>{let t=new Set(e);if(he.length>0&&he.every(e=>t.has(e)))for(let e of he)t.delete(e);else for(let e of he)t.add(e);return t})},[he]),[be,xe]=(0,v.useState)(!1),Se=(0,v.useCallback)(async()=>{let e=[...A];if(e.length!==0)try{await _c({ids:e});let t=new Set(e);y(e=>e.filter(e=>!t.has(e.id))),j(new Set)}catch{}finally{xe(!1)}},[A]),Ce=e=>{if(e===`json`){G9(`mcp-events.json`,JSON.stringify(le,null,2),`application/json`);return}let t=[`ts`,`tool`,`risk`,`device`,`durationMs`,`isError`,`error`,`reason`],n=e=>/[",\n]/.test(e)?`"${e.replace(/"/g,`""`)}"`:e,r=le.map(e=>[new Date(e.ts).toISOString(),e.tool,e.risk,e.device??``,String(e.durationMs),String(e.isError),e.error??``,e.reason??``].map(n).join(`,`)).join(`
|
|
154
|
+
`),t=t.replace(/%%CB(\d+)%%/g,(e,t)=>n[Number(t)]),t=t.replace(/%%IC(\d+)%%/g,(e,t)=>r[Number(t)]),t}var U9=`mt-whats-new-dismissed`,W9=`mt-whats-new-seen-version`;function qee(e){try{return localStorage.getItem(U9)===e}catch{return!1}}function Jee(e){try{localStorage.setItem(U9,e)}catch{}}function Yee(e){try{let t=localStorage.getItem(W9);return localStorage.setItem(W9,e),t!==null&&t!==e}catch{return!1}}var Xee=`text-[13px] leading-[1.7] text-muted-foreground [&_h2]:mt-[18px] [&_h2]:mb-1.5 [&_h2]:text-[17px] [&_h2]:font-bold [&_h2]:text-foreground [&_h3]:mt-[18px] [&_h3]:mb-1.5 [&_h3]:text-[15px] [&_h3]:font-bold [&_h3]:text-foreground [&_h4]:mt-[18px] [&_h4]:mb-1.5 [&_h4]:text-[14px] [&_h4]:font-bold [&_h4]:text-foreground [&_h5]:mt-[18px] [&_h5]:mb-1.5 [&_h5]:text-[14px] [&_h5]:font-bold [&_h5]:text-foreground [&_p]:my-[5px] [&_ul]:my-[5px] [&_ul]:list-disc [&_ul]:pl-[18px] [&_ol]:my-[5px] [&_ol]:list-decimal [&_ol]:pl-[22px] [&_li]:my-[3px] [&_li]:marker:text-brand [&_img]:my-1.5 [&_img]:max-w-full [&_img]:rounded-md [&_img]:border [&_img]:border-border [&_code]:rounded [&_code]:border [&_code]:border-border [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs [&_code]:text-brand [&_pre]:my-2 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:border [&_pre]:border-border [&_pre]:bg-background [&_pre]:p-3 [&_pre_code]:border-0 [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-muted-foreground [&_a]:text-brand [&_a]:underline [&_a]:underline-offset-2 [&_strong]:font-semibold [&_strong]:text-foreground [&_blockquote]:my-2 [&_blockquote]:border-l-[3px] [&_blockquote]:border-brand/40 [&_blockquote]:px-3.5 [&_blockquote]:py-1.5 [&_blockquote]:italic [&_hr]:my-3.5 [&_hr]:border-0 [&_hr]:border-t [&_hr]:border-border [&_del]:text-muted-foreground [&_del]:opacity-60 [&_table]:w-full [&_table]:border-collapse [&_table]:font-mono [&_table]:text-xs [&_th]:border-b [&_th]:border-border [&_th]:bg-muted [&_th]:px-3 [&_th]:py-[7px] [&_th]:text-left [&_th]:text-[11px] [&_th]:font-semibold [&_th]:tracking-wide [&_th]:text-foreground [&_th]:uppercase [&_td]:border-b [&_td]:border-border [&_td]:px-3 [&_td]:py-[7px] [&_td]:text-muted-foreground [&_.wn-table-wrap]:my-2.5 [&_.wn-table-wrap]:overflow-x-auto [&_.wn-table-wrap]:rounded-md [&_.wn-table-wrap]:border [&_.wn-table-wrap]:border-border [&_.wn-task]:-ml-[18px] [&_.wn-task]:list-none [&_.wn-check]:inline-block [&_.wn-check]:w-4 [&_.wn-task.done_.wn-check]:text-success`,Zee=600*1e3;function Qee(e){let[t,n]=(0,v.useState)(null),[r,i]=(0,v.useState)(!1);(0,v.useEffect)(()=>{if(!e)return;let t=Yee(e),r=null,a=()=>{mc(`/api/releases/latest`).then(e=>{n(e);let a=r===null,o=r!==null&&e.version!==r;if(r=e.version,a&&t){setTimeout(()=>i(!0),1200);return}e.isNewer&&!qee(e.version)&&(a||o)&&setTimeout(()=>i(!0),a?1200:0)}).catch(()=>{})};a();let o=setInterval(a,Zee);return()=>clearInterval(o)},[e]);let a=(0,v.useCallback)(()=>{t?.isNewer&&Jee(t.version),i(!1)},[t]),o=(0,v.useCallback)(()=>i(!0),[]);return{release:t,showModal:r,showIndicator:t!==null&&t.isNewer,openModal:o,dismissRelease:a}}function $ee({release:e,onDismiss:t}){let n=new Date(e.publishedAt).toLocaleDateString(`en-US`,{month:`short`,day:`numeric`,year:`numeric`}),r={__html:Kee(e.body)};return(0,K.jsx)(Ree,{open:!0,onOpenChange:e=>{e||t()},children:(0,K.jsxs)(Vee,{className:`max-h-[calc(100vh-80px)] gap-0 overflow-y-auto p-0 sm:max-w-[520px]`,children:[(0,K.jsxs)(Hee,{className:`items-center gap-4 px-8 pt-10 pb-6 text-center`,children:[(0,K.jsx)(`span`,{className:`inline-block rounded-full border border-brand/30 bg-brand/10 px-3.5 py-1 font-mono text-[10px] font-bold tracking-[0.2em] text-brand uppercase`,children:e.isNewer?`NEW RELEASE`:`RELEASE NOTES`}),(0,K.jsxs)(Wee,{className:`text-[44px] leading-none font-extrabold tracking-tight text-brand`,children:[`v`,e.version]}),(0,K.jsxs)(Gee,{className:`font-mono text-xs text-muted-foreground`,children:[`Released `,n,e.isNewer&&(0,K.jsxs)(K.Fragment,{children:[` · you’re on v`,e.currentVersion]})]})]}),(0,K.jsx)(`div`,{className:Y(`px-8 py-5`,Xee),dangerouslySetInnerHTML:r}),(0,K.jsxs)(Uee,{className:`flex-row items-center justify-between border-t border-border px-8 pt-3.5 pb-6`,children:[(0,K.jsxs)(`a`,{href:e.url,target:`_blank`,rel:`noopener noreferrer`,className:`inline-flex items-center gap-1.5 font-mono text-xs text-muted-foreground hover:text-foreground`,children:[`View on GitHub`,(0,K.jsx)(Rc,{className:`size-3.5`})]}),(0,K.jsx)(X,{type:`default`,size:`sm`,onClick:t,children:`Got it`})]})]})})}zi.registerPlugin(G);function G9(e,t,n){let r=URL.createObjectURL(new Blob([t],{type:`${n};charset=utf-8`})),i=document.createElement(`a`);i.href=r,i.download=e,document.body.append(i),i.click(),i.remove(),URL.revokeObjectURL(r)}var K9=[{id:`overview`,label:`Overview`,sub:`Calls, latency & risk at a glance`},{id:`devices`,label:`Devices`,sub:`Connectivity radar & system health`},{id:`clients`,label:`Clients`,sub:`Connected LAN devices — usage, block/allow, pin IP`},{id:`aaa`,label:`RADIUS & UM`,sub:`RADIUS client & User Manager RADIUS server`},{id:`topology`,label:`Topology`,sub:`Layer-2 neighbours via MNDP / CDP / LLDP`},{id:`packets`,label:`Packets`,sub:`Live TZSP capture & decode`},{id:`snapshots`,label:`Snapshots`,sub:`Config history & time-travel diff`},{id:`drift`,label:`Drift Guard`,sub:`Golden config baselines & live drift detection`},{id:`plan`,label:`Change Plan`,sub:`Dry-run intended RouterOS commands`},{id:`s3`,label:`S3 Backups`,sub:`List, download & delete S3 backup objects`},{id:`backups`,label:`Backups`,sub:`Local config vault — create, restore, manage`},{id:`modules`,label:`Modules`,sub:`Enable/disable tool modules — curate the surface`},{id:`config`,label:`Config`,sub:`Effective configuration & safe editor`},{id:`memory`,label:`Memory`,sub:`Knowledge graph — entities, relations & observations`},{id:`feed`,label:`Live Feed`,sub:`Every tool call, in real time`}],q9=new Set(K9.map(e=>e.id)),J9=`mt-view`;function Y9(){try{let e=location.hash.replace(/^#\/?/,``);return q9.has(e)?e:null}catch{return null}}function ete(){let e=Y9();if(e)return e;try{let e=localStorage.getItem(J9);if(e&&q9.has(e))return e}catch{}return`overview`}var X9=[`var(--foreground)`,`var(--muted-foreground)`],Z9={overview:X9,devices:X9,clients:X9,aaa:X9,topology:X9,packets:X9,snapshots:X9,drift:X9,plan:X9,s3:X9,backups:X9,modules:X9,config:X9,memory:X9,feed:X9},tte={overview:{what:`A live pulse of all MCP tool activity: total calls, error rate, p50/p95 latency, the busiest tools, and a risk breakdown — over a time window you choose.`,tips:[`Change the time window (top-right) to zoom from the last 5 minutes out to 24 hours.`,`The risk donut splits calls by annotation: read · write · destructive · dangerous.`,`A rising error line usually points at one device or one tool — jump to Live Feed to see which.`]},devices:{what:`Every configured router with its live reachability (SSH or MAC-Telnet), latency, identity, and system health — CPU, memory and disk — refreshed continuously.`,tips:[`Each device gets a stable colour so you can track it across the connectivity radar.`,`Health (CPU/Mem/Disk) is probed periodically; MAC-Telnet devices are probed on a slower cadence.`,`Latency tiers are colour-coded green → amber → red; a grey node is currently unreachable.`,`The SSH Connection Pool panel shows persistent connections: green = idle/ready, blue = busy with inflight channels, red = reconnecting.`]},clients:{what:`The LAN devices connected to a router — merged from its DHCP leases and ARP table — with live Download/Upload charts, and one-click controls to block/allow a device, pin (reserve) its IP, change that IP, or relabel it.`,tips:[`Pick the router (top-right) to inspect its connected devices; filter by IP, MAC or name.`,`Click a device to open its live ↓/↑ traffic chart — needs a simple queue targeting its IP.`,`Block/allow is enforced by MAC, so it survives the device changing IP; “Pin IP” makes its lease static.`]},aaa:{what:"Full management of the router's RADIUS client (`/radius`) and the built-in User Manager RADIUS server (`/user-manager`): RADIUS servers + incoming CoA, and User Manager users, service profiles, rate/quota limitations, NAS clients, profile assignments, accounting sessions, and global settings.",tips:[`Pick the router (top-right), then switch tabs across RADIUS, Users, Profiles, Limitations, NAS, Assignments, Sessions and Settings.`,`Every tab is full CRUD: add, edit, enable/disable and remove — secrets are write-only and shown redacted.`,`The Usage & Heatmap tab shows each user's 3-month download/upload and a GitHub-style connection heatmap, persisted locally.`,`If a device lacks the user-manager package, the User Manager tabs explain how to install it; RADIUS-client tabs still work.`]},topology:{what:`A Layer-2 map of neighbours each router discovers via MNDP / CDP / LLDP — the physical adjacency of your network, drawn live.`,tips:[`Solid nodes are configured devices; faint nodes are discovered-but-unmanaged neighbours.`,`Use “Add to config →” on an unmanaged neighbour to pre-fill it in the Config editor.`,`Drag to pan; the layout settles automatically as new neighbours arrive.`]},packets:{what:`Live packet capture streamed from a router over TZSP — decode headers in real time without leaving the dashboard.`,tips:[`Pick a device and start the capture; packets decode as they arrive.`,`Stop the capture when done — it frees the router-side sniffer.`,`Great for debugging a protocol issue alongside the Live Feed of tool calls.`]},snapshots:{what:`Point-in-time captures of a device’s full configuration (/export), stored locally so you can diff any two and see exactly what changed.`,tips:[`Capture a snapshot before a risky change, then diff after to audit the delta.`,`The diff is line-level: green added, red removed.`,`Snapshots are device config exports — for the dashboard’s OWN config history see the Config page.`]},drift:{what:`Detect when router configs drift from their golden baseline — with change attribution and one-click reconciliation.`,tips:[`Set a baseline via MCP tools (config_set_baseline) or the Baseline Manager below.`,`Click a device card to run a live drift check against its golden config.`,`Promote accepted changes as the new baseline, or reconcile to roll back.`]},plan:{what:`Dry-run the exact RouterOS commands a change would run before it touches a device — a change plan you can review and trust.`,tips:[`Paste or build intended commands to see them validated and ordered.`,`Nothing is sent to the device from here — it’s a preview.`,`Pair with Safe Mode (auto-revert) when you do apply for real.`]},s3:{what:`Browse, download, and delete backup objects in your configured S3-compatible bucket — your off-box archive of device backups and exports.`,tips:[`Filter by key prefix to find a device’s backups quickly.`,`Download fetches the object through a presigned URL; delete is permanent.`,`For host-side .rsc backups instead, use the Backups page.`]},backups:{what:`A local config vault on the MCP server: capture a device’s /export as a timestamped .rsc file, then download, upload, rename, restore (via Safe Mode), or delete it.`,tips:[`Restore offers a dry-run (applies then rolls back) before you commit for real.`,`Edit the vault path inline in the header — it’s saved to your config.`,`Filenames are stamped in the device’s local 24-hour clock.`]},modules:{what:"Every tool module in the catalog with a live on/off switch. Toggling one writes your config file's `tools` block (disabledModules / enabledModules) and applies it immediately, so you can curate exactly which scopes the MCP server exposes.",tips:[`MCP clients search-rank tools and degrade past ~100; trim the surface below ~150–200 tools so every remaining tool is reliably findable.`,`Disable adds the module to tools.disabledModules; enable removes it (or adds it to an active allow-list) — your config file is updated live on each toggle.`,`Changes need an MCP client reconnect (or a server restart) to actually shrink/grow the visible tool list.`]},config:{what:`View the effective configuration, edit it safely with schema-aware validation and auto-rollback, browse a full field guide, and travel through config version history.`,tips:[`Every successful apply is auto-saved to the version timeline — restore any point in time.`,`Save a named checkpoint before a big change for an easy, labelled rollback.`,`The Field Guide documents every config option, its type, and default — straight from the schema.`]},memory:{what:`A persistent knowledge graph the AI builds across sessions — entities (routers, subnets, users), relations between them, and free-text observations.`,tips:[`Entities are created by the AI via MCP tools — you can browse and delete them here.`,`The graph visualization shows entities as circles and relations as directed edges; click a node to inspect it.`,`Change the database path to switch between different knowledge bases.`]},feed:{what:`Every tool call as it happens — tool, device, risk, duration, and success/error — with full request/response detail on click.`,tips:[`Filter by status to isolate failures, or by tool/device to follow one thread.`,`Click any row to open the full (secret-redacted) request and response.`,`Pause the stream when you want to inspect without rows shifting under you.`]}};function Q9({icon:e,title:t,sub:n,body:r,className:i}){return(0,K.jsxs)(`div`,{className:Y(`text-muted-foreground flex flex-col items-center gap-2 rounded-lg border border-dashed px-6 py-14 text-center`,i),children:[e,(0,K.jsx)(`p`,{className:`text-foreground text-sm font-medium`,children:t}),n!=null&&(0,K.jsx)(`p`,{className:`max-w-md text-xs`,children:n}),r]})}function nte({view:e}){let t=tte[e];return(0,K.jsxs)(`div`,{className:`bg-card reveal flex gap-3 rounded-lg border p-4`,role:`region`,"aria-label":`Page help`,children:[(0,K.jsx)(Nc,{className:`text-brand mt-0.5 size-4 shrink-0`,"aria-hidden":`true`}),(0,K.jsxs)(`div`,{className:`flex min-w-0 flex-col gap-2`,children:[(0,K.jsx)(`p`,{className:`text-sm`,children:t.what}),(0,K.jsx)(`ul`,{className:`text-muted-foreground list-disc space-y-1 pl-4 text-xs`,children:t.tips.map((e,t)=>(0,K.jsx)(`li`,{children:e},t))})]})]})}function rte(){let[e,t]=(0,v.useState)(`idle`),[n,r]=(0,v.useState)(``),[i,a]=(0,v.useState)(!1);async function o(e){t(`working`),r(e?`Restarting…`:`Reloading…`);try{let n=await hc(`/api/reload`,{hard:e});n?.ok?(t(`ok`),r(e?`Restarting — reconnect shortly`:`Reloaded · ${n.count??`?`} device${n.count===1?``:`s`}`)):(t(`err`),r(n?.error??`Reload failed`))}catch{t(`err`),r(`Reload failed (server unreachable)`)}a(!1),setTimeout(()=>t(`idle`),5e3)}function s(){if(!i){a(!0),setTimeout(()=>a(!1),4e3);return}o(!0)}return(0,K.jsxs)(`div`,{className:`flex flex-col gap-1`,children:[(0,K.jsxs)(`button`,{type:`button`,onClick:()=>void o(!1),disabled:e===`working`,title:`Re-read the config from disk and apply it live (no downtime)`,className:Y(`flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-[12px] font-medium transition-colors`,`text-muted-foreground hover:bg-accent/50 hover:text-foreground disabled:opacity-60`),children:[(0,K.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,"aria-hidden":`true`,className:`size-3.5`,children:(0,K.jsxs)(`g`,{stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,K.jsx)(`path`,{d:`M21 12a9 9 0 1 1-2.64-6.36`}),(0,K.jsx)(`path`,{d:`M21 3v6h-6`})]})}),(0,K.jsx)(`span`,{children:e===`working`?`Reloading…`:`Reload Server`})]}),e!==`idle`&&(0,K.jsx)(`small`,{className:Y(`text-[11px]`,e===`err`?`text-destructive`:`text-muted-foreground`),children:n}),(0,K.jsx)(`button`,{type:`button`,onClick:s,disabled:e===`working`,title:`Fully restart the server process — it relaunches itself and rebinds in ~1.5s (drops the connection briefly)`,className:Y(`self-start text-[10px] underline decoration-dotted underline-offset-2 disabled:opacity-60`,i?`text-destructive font-medium`:`text-muted-foreground hover:text-foreground`),children:i?`click again to confirm restart`:`restart process…`})]})}function ite({name:e}){return(0,K.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.7`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,children:{overview:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`rect`,{x:`3`,y:`3`,width:`8`,height:`8`,rx:`1.6`}),(0,K.jsx)(`rect`,{x:`13`,y:`3`,width:`8`,height:`5`,rx:`1.6`}),(0,K.jsx)(`rect`,{x:`13`,y:`10`,width:`8`,height:`11`,rx:`1.6`}),(0,K.jsx)(`rect`,{x:`3`,y:`13`,width:`8`,height:`8`,rx:`1.6`})]}),devices:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`rect`,{x:`3`,y:`4`,width:`18`,height:`7`,rx:`2`}),(0,K.jsx)(`rect`,{x:`3`,y:`13`,width:`18`,height:`7`,rx:`2`}),(0,K.jsx)(`path`,{d:`M7 7.5h.01M7 16.5h.01`})]}),clients:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`circle`,{cx:`9`,cy:`8`,r:`3`}),(0,K.jsx)(`path`,{d:`M3.5 19a5.5 5.5 0 0 1 11 0`}),(0,K.jsx)(`path`,{d:`M16 7.5a2.5 2.5 0 0 1 0 5M17.5 19a4.5 4.5 0 0 0-2-3.6`})]}),aaa:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`path`,{d:`M12 3 4 6v5c0 4.4 3.2 7.6 8 9 4.8-1.4 8-4.6 8-9V6l-8-3Z`}),(0,K.jsx)(`path`,{d:`M9.5 11.5 11 13l3.5-3.5`})]}),topology:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`circle`,{cx:`12`,cy:`5`,r:`2.4`}),(0,K.jsx)(`circle`,{cx:`5`,cy:`19`,r:`2.4`}),(0,K.jsx)(`circle`,{cx:`19`,cy:`19`,r:`2.4`}),(0,K.jsx)(`path`,{d:`M12 7.4 6.4 16.6M12 7.4 17.6 16.6`})]}),packets:(0,K.jsx)(`path`,{d:`M3 12h4l2-7 4 14 2-7h6`}),snapshots:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`path`,{d:`M12 3 3 7.5 12 12 21 7.5 12 3Z`}),(0,K.jsx)(`path`,{d:`M3 12 12 16.5 21 12`}),(0,K.jsx)(`path`,{d:`M3 16.5 12 21 21 16.5`})]}),drift:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`path`,{d:`M12 22c5.5 0 10-4.5 10-10S17.5 2 12 2 2 6.5 2 12s4.5 10 10 10Z`}),(0,K.jsx)(`path`,{d:`M9 12l2 2 4-4`}),(0,K.jsx)(`path`,{d:`M12 6v2M12 16v2M6 12h2M16 12h2`})]}),plan:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`circle`,{cx:`6`,cy:`6`,r:`2.3`}),(0,K.jsx)(`circle`,{cx:`6`,cy:`18`,r:`2.3`}),(0,K.jsx)(`circle`,{cx:`18`,cy:`8`,r:`2.3`}),(0,K.jsx)(`path`,{d:`M6 8.3v7.4M8.3 6H13a3 3 0 0 1 3 3v0`})]}),s3:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`ellipse`,{cx:`12`,cy:`6`,rx:`7`,ry:`2.6`}),(0,K.jsx)(`path`,{d:`M5 6v12c0 1.5 3.1 2.6 7 2.6s7-1.1 7-2.6V6`}),(0,K.jsx)(`path`,{d:`M5 12c0 1.5 3.1 2.6 7 2.6s7-1.1 7-2.6`})]}),backups:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`path`,{d:`M3 6.5 5 3.5h14l2 3`}),(0,K.jsx)(`rect`,{x:`3`,y:`6.5`,width:`18`,height:`14`,rx:`2`}),(0,K.jsx)(`path`,{d:`M9.5 12h5`})]}),modules:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`rect`,{x:`3`,y:`5`,width:`18`,height:`6`,rx:`3`}),(0,K.jsx)(`circle`,{cx:`8`,cy:`8`,r:`1.5`}),(0,K.jsx)(`rect`,{x:`3`,y:`13`,width:`18`,height:`6`,rx:`3`}),(0,K.jsx)(`circle`,{cx:`16`,cy:`16`,r:`1.5`})]}),config:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`path`,{d:`M4 7h8M16 7h4M4 17h4M12 17h8`}),(0,K.jsx)(`circle`,{cx:`14`,cy:`7`,r:`2.2`}),(0,K.jsx)(`circle`,{cx:`10`,cy:`17`,r:`2.2`})]}),memory:(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(`circle`,{cx:`12`,cy:`6`,r:`2`}),(0,K.jsx)(`circle`,{cx:`6`,cy:`14`,r:`2`}),(0,K.jsx)(`circle`,{cx:`18`,cy:`14`,r:`2`}),(0,K.jsx)(`circle`,{cx:`12`,cy:`20`,r:`2`}),(0,K.jsx)(`path`,{d:`M12 8v-0.5M12 8 7.2 12.5M12 8 16.8 12.5M6 16 10.5 18.5M18 16 13.5 18.5`})]}),feed:(0,K.jsx)(`path`,{d:`M4 6h16M4 12h16M4 18h10`})}[e]})}function ate(){let[e,t]=(0,v.useState)(null),[n,r]=(0,v.useState)(null),[i,a]=(0,v.useState)(null),[o,s]=(0,v.useState)(null),c=Qee(n?.version),[l,u]=(0,v.useState)(null),[d,f]=(0,v.useState)(null),[p,m]=(0,v.useState)(!1),[h,g]=(0,v.useState)(null),[_,y]=(0,v.useState)([]),[b,x]=(0,v.useState)(36e5),S=Number(d?.dashboard?.feedLimit)||200,C=(0,v.useCallback)(async e=>{if(!d)return;let t={...d.dashboard,feedLimit:e};await hc(`/api/config`,{config:{...d,dashboard:t},rollbackMs:0}).catch(()=>{}),await mc(`/api/config`).then(f).catch(()=>{})},[d]),[w,T]=(0,v.useState)(!1),[E,D]=(0,v.useState)(`off`),[O,k]=(0,v.useState)(null),[A,j]=(0,v.useState)(()=>new Set),[M,N]=(0,v.useState)({}),[P,F]=(0,v.useState)({tool:``,risk:``,device:``,status:``,q:``}),I=(0,v.useRef)(w);I.current=w;let L=(0,v.useRef)(null);l9(L);let[R,ee]=(0,v.useState)(ete),te=(0,v.useCallback)(e=>{ee(e);try{Y9()!==e&&(location.hash=e),localStorage.setItem(J9,e)}catch{}},[]);(0,v.useEffect)(()=>{if(Y9()!==R)try{location.hash=R}catch{}let e=()=>{let e=Y9();e&&te(e)};return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]);let[z,B]=(0,v.useState)(()=>{try{let e=localStorage.getItem(`mt-help-open`);return new Set(e?JSON.parse(e):[])}catch{return new Set}}),V=e=>B(t=>{let n=new Set(t);n.has(e)?n.delete(e):n.add(e);try{localStorage.setItem(`mt-help-open`,JSON.stringify([...n]))}catch{}return n}),[ne,re]=(0,v.useState)(``),[ie,ae]=(0,v.useState)(`all`),oe=(0,v.useMemo)(()=>{let e=i?.devices??[];return{online:e.filter(e=>e.status.reachable===!0).length,offline:e.filter(e=>e.status.reachable===!1).length,total:e.length}},[i]),se=(0,v.useMemo)(()=>{let e=i?.devices??[],t=ne.trim().toLowerCase();return e.filter(e=>!(ie===`online`&&e.status.reachable!==!0||ie===`offline`&&e.status.reachable!==!1||t&&!e.name.toLowerCase().includes(t)&&!(e.address??e.host??``).toLowerCase().includes(t)))},[i,ne,ie]);c9((0,v.useCallback)(e=>{if(I.current)return;y(t=>[e,...t].slice(0,yb));let t=e.device;t&&N(e=>({...e,[t]:(e[t]??0)+1}))},[]),(0,v.useCallback)(e=>D(e),[])),(0,v.useEffect)(()=>{mc(`/api/events?limit=${yb}`).then(e=>y(e.events)).catch(()=>{})},[]);let H=(0,v.useCallback)(()=>{mc(`/api/stats?window=${b}&buckets=60`).then(t).catch(()=>{})},[b]);(0,v.useEffect)(()=>{H();let e=()=>{H(),mc(`/api/meta`).then(r).catch(()=>{}),mc(`/api/devices`).then(a).catch(()=>{}),mc(`/api/ssh-pool`).then(s).catch(()=>{}),mc(`/api/topology`).then(u).catch(()=>{})};e();let t=setInterval(()=>{I.current||e()},4e3);return()=>clearInterval(t)},[H]),(0,v.useEffect)(()=>{let e=()=>void mc(`/api/config`).then(f).catch(()=>{});e();let t=setInterval(e,3e4);return()=>clearInterval(t)},[]);let ce=(0,v.useCallback)((e,t)=>{hc(`/api/devices/toggle`,{device:e,disabled:t}).then(e=>{a(e)})},[]);(0,v.useEffect)(()=>{let e=e=>{e.key===`Escape`&&k(null)};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[]);let le=(0,v.useMemo)(()=>{let e=P.q.trim().toLowerCase();return _.filter(t=>!(P.tool&&t.tool!==P.tool||P.risk&&t.risk!==P.risk||P.device&&t.device!==P.device||P.status===`ok`&&t.isError||P.status===`error`&&!t.isError||e&&!t.tool.toLowerCase().includes(e)&&!t.input.toLowerCase().includes(e)&&!t.output.toLowerCase().includes(e)&&!(t.error??``).toLowerCase().includes(e))).sort((e,t)=>t.ts-e.ts)},[_,P]),ue=!!(P.tool||P.risk||P.device||P.status||P.q),de=(0,v.useMemo)(()=>_.filter(e=>e.isError),[_]),fe=(0,v.useMemo)(()=>({ok:_.length-de.length,error:de.length}),[_,de]),pe=(0,v.useCallback)(async e=>{try{k(await mc(`/api/event/${encodeURIComponent(e.id)}`))}catch{k(e)}},[]),me=(0,v.useMemo)(()=>le.slice(0,S),[le,S]),he=(0,v.useMemo)(()=>me.map(e=>e.id),[me]),ge=he.length>0&&he.every(e=>A.has(e)),_e=!ge&&he.some(e=>A.has(e)),ve=(0,v.useCallback)(e=>{j(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},[]),ye=(0,v.useCallback)(()=>{j(e=>{let t=new Set(e);if(he.length>0&&he.every(e=>t.has(e)))for(let e of he)t.delete(e);else for(let e of he)t.add(e);return t})},[he]),[be,xe]=(0,v.useState)(!1),Se=(0,v.useCallback)(async()=>{let e=[...A];if(e.length!==0)try{await _c({ids:e});let t=new Set(e);y(e=>e.filter(e=>!t.has(e.id))),j(new Set)}catch{}finally{xe(!1)}},[A]),Ce=e=>{if(e===`json`){G9(`mcp-events.json`,JSON.stringify(le,null,2),`application/json`);return}let t=[`ts`,`tool`,`risk`,`device`,`durationMs`,`isError`,`error`,`reason`],n=e=>/[",\n]/.test(e)?`"${e.replace(/"/g,`""`)}"`:e,r=le.map(e=>[new Date(e.ts).toISOString(),e.tool,e.risk,e.device??``,String(e.durationMs),String(e.isError),e.error??``,e.reason??``].map(n).join(`,`)).join(`
|
|
155
155
|
`);G9(`mcp-events.csv`,`${t.join(`,`)}\n${r}\n`,`text/csv`)},we=e?e.errorRate>=.2?`text-destructive`:e.errorRate>=.05?`text-warning`:``:``,Te=d?.mcp??{},Ee=d?.dashboard??{},De=d?.ssh??{},Oe=(e,t,n)=>(0,K.jsx)(Gb,{size:`sm`,"aria-label":t,value:P[e],onValueChange:t=>F(n=>({...n,[e]:t})),options:[{value:``,label:t},...n.map(e=>({value:e,label:e}))]}),ke=K9.find(e=>e.id===R)??K9[0];return(0,K.jsxs)(`div`,{className:`bg-background text-foreground grid min-h-screen grid-cols-[240px_1fr]`,ref:L,"data-view":R,style:{"--page-accent":Z9[R][0],"--page-accent-2":Z9[R][1]},children:[(0,K.jsxs)(`aside`,{className:`bg-card sticky top-0 flex h-screen flex-col gap-4 border-r p-4`,children:[(0,K.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,K.jsx)(`div`,{className:`bg-foreground text-background grid size-9 shrink-0 place-items-center rounded-md`,children:(0,K.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,"aria-hidden":`true`,className:`size-5`,children:(0,K.jsxs)(`g`,{stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,K.jsx)(`path`,{d:`M12 12 L4 5 M12 12 L20 5 M12 12 L12 20`}),(0,K.jsx)(`circle`,{cx:`12`,cy:`12`,r:`3`,fill:`currentColor`,stroke:`none`}),(0,K.jsx)(`circle`,{cx:`4`,cy:`5`,r:`1.9`,fill:`currentColor`,stroke:`none`}),(0,K.jsx)(`circle`,{cx:`20`,cy:`5`,r:`1.9`,fill:`currentColor`,stroke:`none`}),(0,K.jsx)(`circle`,{cx:`12`,cy:`20`,r:`1.9`,fill:`currentColor`,stroke:`none`})]})})}),(0,K.jsxs)(`div`,{className:`flex min-w-0 flex-col leading-tight`,children:[(0,K.jsx)(`b`,{className:`truncate text-sm`,children:`MikroTik MCP`}),(0,K.jsxs)(`small`,{className:Y(`text-muted-foreground inline-flex items-center gap-1 text-[11px]`,c.release&&`hover:text-foreground cursor-pointer`),title:c.release?`View release notes`:void 0,onClick:c.release?()=>c.openModal():void 0,children:[n?.version?`v${n.version}`:`Observability`,c.showIndicator&&(0,K.jsx)(`span`,{className:`bg-brand inline-block size-1.5 rounded-full`,title:`v${c.release?.version} available`})]})]}),(0,K.jsx)(`span`,{className:`flex-1`}),(0,K.jsx)(lb,{})]}),(0,K.jsx)(`nav`,{className:`flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto`,children:K9.map(e=>(0,K.jsxs)(`button`,{type:`button`,className:Y(`flex items-center gap-2.5 rounded-md px-2.5 py-2 text-left text-[13px] transition-colors`,`[&_svg]:size-4 [&_svg]:shrink-0`,R===e.id?`bg-accent text-accent-foreground font-medium`:`text-muted-foreground hover:bg-accent/50 hover:text-foreground`),onClick:()=>te(e.id),children:[(0,K.jsx)(ite,{name:e.id}),(0,K.jsx)(`span`,{className:`truncate`,children:e.label}),e.id===`feed`&&_.length>0&&(0,K.jsx)(`span`,{className:`bg-brand text-brand-foreground animate-in zoom-in-50 ml-auto rounded-full px-1.5 py-0.5 text-[10px] tabular-nums`,children:_.length>999?`999+`:_.length},_.length)]},e.id))}),(0,K.jsxs)(`div`,{className:`flex flex-col gap-1.5 border-t pt-3`,children:[(0,K.jsx)(rte,{}),(0,K.jsxs)(`span`,{className:`inline-flex items-center gap-2 text-[11px]`,title:`Live transport: WebSocket (preferred) or SSE fallback`,children:[(0,K.jsx)(zb,{type:E===`off`?`secondary`:`success`,pulse:E!==`off`}),E===`off`?`offline`:`live · ${E}`]}),(0,K.jsx)(`small`,{className:`text-muted-foreground text-[11px]`,children:n?`${pb(n.total)} events · ${n.transport}`:`connecting…`})]})]}),(0,K.jsxs)(`main`,{className:`flex min-w-0 flex-col gap-5 p-6`,"data-view":R,children:[(0,K.jsxs)(`header`,{className:`reveal flex flex-wrap items-center gap-3`,children:[(0,K.jsxs)(`div`,{className:`flex min-w-0 flex-col`,children:[(0,K.jsx)(`h1`,{className:`truncate text-xl font-semibold`,children:ke.label}),(0,K.jsx)(`small`,{className:`text-muted-foreground text-xs`,children:ke.sub})]}),(0,K.jsx)(`span`,{className:`flex-1`}),R===`overview`&&(0,K.jsx)(Gb,{size:`sm`,"aria-label":`Stats time window`,value:String(b),onValueChange:e=>x(Number(e)),options:vb.map(([e,t])=>({value:String(t),label:`window: ${e}`}))}),(0,K.jsx)(X,{size:`sm`,ghost:!0,type:z.has(R)?`accent`:`default`,onClick:()=>V(R),"aria-expanded":z.has(R),title:`About this page`,icon:(0,K.jsx)(Nc,{}),children:`Help`})]}),z.has(R)&&(0,K.jsx)(nte,{view:R}),R===`overview`&&(0,K.jsxs)(`section`,{className:`grid content-start gap-[18px]`,children:[(0,K.jsx)(`div`,{className:`reveal grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-3`,children:e?(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(nb,{k:`Calls (window)`,v:pb(e.total)}),(0,K.jsx)(nb,{k:`Calls / min`,v:e.callsPerMin.toFixed(1)}),(0,K.jsx)(nb,{k:`Error rate`,v:`${(e.errorRate*100).toFixed(1)}%`,sub:`${e.errors} err`,cls:we}),(0,K.jsx)(nb,{k:`Avg latency`,v:ub(e.latency.avg)}),(0,K.jsx)(nb,{k:`p95 latency`,v:ub(e.latency.p95)}),(0,K.jsx)(nb,{k:`p99 latency`,v:ub(e.latency.p99)}),(0,K.jsx)(nb,{k:`Distinct tools`,v:pb(e.distinctTools)}),(0,K.jsx)(nb,{k:`Output volume`,v:db(e.outputBytes)})]}):(0,K.jsx)(nb,{k:`Loading…`,v:`—`})}),(0,K.jsxs)(`div`,{className:`reveal grid grid-cols-1 gap-4 xl:grid-cols-3`,children:[(0,K.jsx)(tb,{title:`Calls over time`,className:`xl:col-span-2`,children:e?(0,K.jsx)(y5,{series:e.series}):(0,K.jsx)(`div`,{className:`text-muted-foreground text-[11px]`,children:`no data`})}),e&&(0,K.jsxs)(K.Fragment,{children:[(0,K.jsx)(tb,{title:`By risk`,children:(0,K.jsx)(b5,{segments:Object.keys(e.byRisk).map(t=>({label:t,value:e.byRisk[t],color:gb[t]}))})}),(0,K.jsx)(tb,{title:`Top tools`,className:`xl:col-span-2`,children:(0,K.jsx)(rb,{rows:e.byTool.map(e=>({label:e.tool,value:e.count,sub:`${e.count}× · ${ub(e.p95Ms)} p95${e.errors?` · ${e.errors} err`:``}`,color:e.errors?`var(--destructive)`:void 0}))})}),(0,K.jsx)(tb,{title:`Status`,children:(0,K.jsx)(b5,{centerLabel:`calls`,segments:[{label:`ok`,value:fe.ok,color:`var(--muted-foreground)`},{label:`error`,value:fe.error,color:`var(--destructive)`}]})}),(0,K.jsx)(tb,{title:`By device`,children:e.byDevice.length?(0,K.jsx)(rb,{rows:e.byDevice.map(e=>({label:e.device,value:e.count}))}):(0,K.jsx)(`div`,{className:`text-muted-foreground text-[11px]`,children:`single device`})}),(0,K.jsx)(tb,{title:`Recent errors`,children:de.length?(0,K.jsx)(`div`,{className:`flex flex-col gap-2`,children:de.slice(0,8).map(e=>(0,K.jsxs)(`div`,{className:`hover:bg-accent/50 grid cursor-pointer grid-cols-[auto_1fr] items-center gap-3 rounded-md px-1 py-0.5`,onClick:()=>void pe(e),children:[(0,K.jsx)(`span`,{className:`text-muted-foreground text-[11px]`,children:fb(e.ts)}),(0,K.jsxs)(`span`,{className:`text-destructive min-w-0 truncate text-xs`,title:e.error??e.output,children:[e.tool,`: `,e.error??e.output??`error`]})]},e.id))}):(0,K.jsx)(`div`,{className:`text-muted-foreground text-[11px]`,children:`no errors 🎉`})})]})]})]}),R===`devices`&&(i&&i.devices.length>0?(0,K.jsxs)(`section`,{className:`grid content-start gap-[18px]`,children:[(0,K.jsxs)(`div`,{className:`reveal flex flex-wrap items-center gap-2`,children:[(0,K.jsx)(Vb,{className:`min-w-[180px] flex-1`,type:`search`,placeholder:`Search devices by name or address…`,value:ne,onChange:e=>re(e.target.value)}),(0,K.jsx)(`div`,{className:`bg-muted flex gap-0.5 rounded-md p-0.5`,children:[`all`,`online`,`offline`].map(e=>(0,K.jsx)(`button`,{type:`button`,className:Y(`rounded-sm px-2.5 py-1 text-xs transition-colors`,ie===e?`bg-background text-foreground shadow-xs`:`text-muted-foreground hover:text-foreground`),onClick:()=>ae(e),children:e===`all`?`All ${oe.total}`:e===`online`?`Online ${oe.online}`:`Offline ${oe.offline}`},e))}),(0,K.jsxs)(`span`,{className:`text-muted-foreground text-[11px]`,children:[se.length,`/`,oe.total,` shown`]})]}),(0,K.jsxs)(`details`,{className:`bg-card reveal rounded-lg border p-4`,open:oe.total<=8,children:[(0,K.jsxs)(`summary`,{className:`cursor-pointer text-sm font-medium`,children:[`Connectivity radar`,(0,K.jsxs)(`span`,{className:`text-muted-foreground text-[11px]`,children:[` `,`· `,oe.online,` online · `,oe.offline,` offline ·`,` `,oe.total,` total`]})]}),(0,K.jsx)(Z7,{payload:i,pulses:M})]}),(0,K.jsx)(Lee,{devices:se,poolPayload:o}),se.length===0?(0,K.jsx)(Q9,{className:`reveal`,icon:(0,K.jsx)(Qc,{className:`size-6`}),title:`No devices match`,sub:`Try a different search or status filter.`}):(0,K.jsx)(`div`,{className:`reveal grid grid-cols-[repeat(auto-fill,minmax(20rem,1fr))] gap-4`,children:se.map(e=>(0,K.jsx)(Q7,{d:e,onToggle:ce},e.name))}),se.length>0&&(0,K.jsx)(tb,{title:`Device system health`,className:`reveal`,extra:(0,K.jsx)(`span`,{className:`text-muted-foreground text-[11px]`,children:`CPU · memory · disk · latency · live probe`}),children:(0,K.jsx)(`div`,{className:`grid grid-cols-[repeat(auto-fill,minmax(22rem,1fr))] gap-3`,children:se.map(e=>(0,K.jsx)(s9,{d:e},e.name))})})]}):(0,K.jsx)(Q9,{icon:(0,K.jsx)(Wc,{className:`size-6`}),title:`No devices configured`,body:(0,K.jsx)(Pb,{type:`secondary`,label:`Tip`,children:`Add a device to your config to see connectivity and system health here.`})})),R===`clients`&&(0,K.jsx)(I5,{}),R===`aaa`&&(0,K.jsx)(Px,{}),R===`topology`&&(l&&l.nodes.length>0?(0,K.jsx)(`section`,{className:`reveal grid content-start gap-[18px]`,children:(0,K.jsx)(Mee,{topo:l,onOnboard:(e,t)=>{g({name:e,body:t}),m(!0),te(`config`)}})}):(0,K.jsx)(Q9,{icon:(0,K.jsx)(Yc,{className:`size-6`}),title:`No neighbours discovered yet`,sub:`Layer-2 neighbours (MNDP / CDP / LLDP) appear here as the device reports them.`})),R===`packets`&&(0,K.jsx)(`section`,{className:`grid content-start gap-[18px]`,children:(0,K.jsx)(tb,{title:`Packet capture`,className:`reveal`,extra:(0,K.jsx)(`span`,{className:`text-muted-foreground text-[11px]`,children:`live TZSP decode · /tool sniffer streaming`}),children:(0,K.jsx)(Eee,{})})}),R===`snapshots`&&(0,K.jsx)(Oee,{}),R===`drift`&&(0,K.jsx)(y9,{}),R===`plan`&&(0,K.jsx)(Rx,{}),R===`s3`&&(0,K.jsx)(Dee,{}),R===`backups`&&(0,K.jsx)(qb,{}),R===`modules`&&(0,K.jsx)(M9,{}),R===`config`&&(d?(0,K.jsxs)(`section`,{className:`grid content-start gap-[18px]`,children:[(0,K.jsx)(tb,{title:`Configuration`,className:`reveal`,extra:(0,K.jsx)(X,{size:`sm`,ghost:!0,onClick:()=>m(e=>!e),title:`Edit the config JSON with autocomplete, validation and safe-apply`,icon:p?void 0:(0,K.jsx)(Kc,{}),children:p?`View`:`Edit config`}),children:p?(0,K.jsx)(R7,{initial:h?{...d,devices:{...d.devices,[h.name]:h.body}}:d,onClose:()=>{m(!1),g(null)},onReload:()=>{g(null),mc(`/api/config`).then(f).catch(()=>{})}},h?`seed-${h.name}`:`config`):(0,K.jsxs)(K.Fragment,{children:[(0,K.jsxs)(`div`,{className:`text-muted-foreground mb-2.5 flex flex-wrap gap-x-4 gap-y-1 text-[11px]`,children:[(0,K.jsxs)(`span`,{children:[`transport: `,mb(Te.transport)]}),(0,K.jsxs)(`span`,{children:[`read-only: `,d.readOnly?`yes`:`no`]}),(0,K.jsxs)(`span`,{children:[`dashboard: `,mb(Ee.host),`:`,mb(Ee.port)]}),(0,K.jsxs)(`span`,{children:[`capture: `,Ee.captureBody?`on`:`off`]}),(0,K.jsxs)(`span`,{children:[`s3: `,d.s3?`configured`:`off`]}),(0,K.jsxs)(`span`,{children:[`ssh pool: `,De.keepAlive===!1?`off`:`on`]})]}),(0,K.jsxs)(`details`,{children:[(0,K.jsx)(`summary`,{className:`cursor-pointer text-sm`,children:`Full effective configuration (secrets redacted)`}),(0,K.jsx)(G5,{value:d,maxHeight:340})]})]})}),(0,K.jsx)(tb,{title:`Dashboard preferences`,className:`reveal`,extra:(0,K.jsxs)(`span`,{className:`text-muted-foreground text-[11px]`,children:[`saved to `,(0,K.jsx)(`code`,{children:`dashboard.feedLimit`}),` in the config`]}),children:(0,K.jsxs)(`div`,{className:`flex flex-wrap items-center gap-3`,children:[(0,K.jsxs)(`div`,{className:`flex min-w-0 flex-col`,children:[(0,K.jsx)(zy,{className:`text-[12.5px]`,children:`Live Feed rows`}),(0,K.jsxs)(`span`,{className:`text-muted-foreground text-[11px]`,children:[`How many matching calls the feed table renders. Up to `,pb(1e4),` are kept in memory regardless; this only bounds what is drawn.`]})]}),(0,K.jsx)(`span`,{className:`flex-1`}),(0,K.jsx)(Gb,{size:`sm`,"aria-label":`Live Feed rows`,value:String(S),onValueChange:e=>void C(Number(e)),options:ab.map(e=>({value:String(e),label:`${pb(e)} rows${e===200?` (default)`:``}`}))})]})}),(0,K.jsx)(tb,{title:`Version history`,className:`reveal`,extra:(0,K.jsx)(`span`,{className:`text-muted-foreground text-[11px]`,children:`point-in-time snapshots · diff & restore`}),children:(0,K.jsx)($5,{onRestored:()=>void mc(`/api/config`).then(f).catch(()=>{})})}),(0,K.jsx)(tb,{title:`Field guide`,className:`reveal`,extra:(0,K.jsx)(`span`,{className:`text-muted-foreground text-[11px]`,children:`every config option, documented from the schema`}),children:(0,K.jsx)(t7,{})})]}):(0,K.jsx)(Q9,{icon:(0,K.jsx)(Bb,{}),title:`Loading configuration…`})),R===`memory`&&(0,K.jsx)(k9,{}),R===`feed`&&(0,K.jsxs)(tb,{className:`reveal`,title:`Live tool calls`,extra:(0,K.jsxs)(`span`,{className:`text-muted-foreground text-[11px]`,children:[pb(me.length),` shown`,le.length>me.length&&` of ${pb(le.length)} matching`,` ·`,` `,pb(_.length),` buffered`]}),children:[(0,K.jsxs)(`div`,{className:`mb-3 flex flex-wrap items-center gap-2`,children:[(0,K.jsx)(Vb,{className:`min-w-[180px] flex-1`,type:`search`,placeholder:`Search tool / input / output / error…`,value:P.q,onChange:e=>F(t=>({...t,q:e.target.value}))}),Oe(`tool`,`all tools`,n?.tools??[]),Oe(`risk`,`all risk`,[`READ`,`WRITE`,`WRITE_IDEMPOTENT`,`DESTRUCTIVE`,`DANGEROUS`]),Oe(`device`,`all devices`,n?.devices??[]),Oe(`status`,`all status`,[`ok`,`error`]),(0,K.jsx)(X,{size:`sm`,ghost:!0,type:w?`accent`:`default`,onClick:()=>T(e=>!e),icon:w?(0,K.jsx)(qc,{}):(0,K.jsx)(Gc,{}),children:w?`Resume`:`Pause`}),(0,K.jsx)(X,{size:`sm`,ghost:!0,onClick:()=>Ce(`csv`),children:`CSV`}),(0,K.jsx)(X,{size:`sm`,ghost:!0,onClick:()=>Ce(`json`),children:`JSON`}),(0,K.jsx)(X,{size:`sm`,ghost:!0,onClick:()=>F({tool:``,risk:``,device:``,status:``,q:``}),children:`Clear`}),be&&A.size>0?(0,K.jsxs)(K.Fragment,{children:[(0,K.jsxs)(X,{size:`sm`,type:`error`,onClick:()=>void Se(),icon:(0,K.jsx)(kc,{}),children:[`Confirm delete (`,A.size,`)`]}),(0,K.jsx)(X,{size:`sm`,ghost:!0,onClick:()=>xe(!1),children:`Cancel`})]}):(0,K.jsxs)(X,{size:`sm`,ghost:!0,disabled:A.size===0,onClick:()=>xe(!0),title:`Delete the selected rows`,icon:(0,K.jsx)(il,{}),children:[`Delete`,A.size>0?` (${A.size})`:``]})]}),le.length===0?ue?(0,K.jsx)(Q9,{icon:(0,K.jsx)(Qc,{className:`size-6`}),title:`No calls match your filters`,sub:`${_.length} call${_.length===1?``:`s`} buffered — try widening the search or the risk / device / status filters.`,body:(0,K.jsx)(X,{size:`sm`,ghost:!0,className:`mt-2`,onClick:()=>F({tool:``,risk:``,device:``,status:``,q:``}),children:`Clear filters`})}):(0,K.jsx)(Q9,{icon:(0,K.jsx)(zb,{type:E===`off`?`secondary`:`success`,pulse:E!==`off`,className:`size-3`}),title:E===`off`?`Not connected`:`Listening for tool calls…`,sub:E===`off`?`The live stream is offline — it will reconnect automatically.`:`Tool calls the LLM makes against this server stream in here in real time.`}):(0,K.jsx)(`div`,{className:`max-h-[60vh] overflow-auto`,children:(0,K.jsxs)(By,{children:[(0,K.jsx)(Vy,{children:(0,K.jsxs)(Uy,{children:[(0,K.jsx)(Wy,{className:`w-7`,children:(0,K.jsx)(Ry,{"aria-label":`Select all shown rows`,checked:ge?!0:_e?`indeterminate`:!1,onCheckedChange:ye})}),(0,K.jsx)(Wy,{children:`time`}),(0,K.jsx)(Wy,{children:`tool`}),(0,K.jsx)(Wy,{children:`risk`}),(0,K.jsx)(Wy,{children:`device`}),(0,K.jsx)(Wy,{className:`text-right`,children:`dur`}),(0,K.jsx)(Wy,{children:`status`}),(0,K.jsx)(Wy,{children:`output`})]})}),(0,K.jsx)(Hy,{children:me.map(e=>(0,K.jsxs)(Uy,{"data-state":A.has(e.id)?`selected`:void 0,className:Y(`cursor-pointer`,e.isError&&`bg-destructive/8`),onClick:()=>void pe(e),children:[(0,K.jsx)(Gy,{onClick:e=>e.stopPropagation(),children:(0,K.jsx)(Ry,{"aria-label":`Select row`,checked:A.has(e.id),onCheckedChange:()=>ve(e.id)})}),(0,K.jsx)(Gy,{className:`tabular-nums`,children:fb(e.ts)}),(0,K.jsx)(Gy,{children:e.tool}),(0,K.jsx)(Gy,{children:(0,K.jsx)(`span`,{className:Y(`rounded-full border px-1.5 py-0.5 text-[10px]`,_b[e.risk]),children:e.risk.replace(`WRITE_IDEMPOTENT`,`WRITE·I`)})}),(0,K.jsx)(Gy,{children:e.device??`—`}),(0,K.jsx)(Gy,{className:`text-right tabular-nums`,children:ub(e.durationMs)}),(0,K.jsx)(Gy,{children:(0,K.jsx)(`span`,{className:e.isError?`text-destructive`:`text-muted-foreground`,children:e.isError?`error`:`ok`})}),(0,K.jsx)(Gy,{className:`text-muted-foreground max-w-[24rem] truncate`,children:e.isError?e.error??`error`:e.reason?(0,K.jsx)(`span`,{className:`italic`,children:e.reason}):e.output||`—`})]},e.id))})]})})]})]}),c.showModal&&c.release&&(0,K.jsx)($ee,{release:c.release,onDismiss:c.dismissRelease}),O&&(0,K.jsx)(a9,{event:O,onClose:()=>k(null)})]})}var $9=document.getElementById(`root`);$9&&(0,y.createRoot)($9).render((0,K.jsx)(ate,{}));
|
|
156
156
|
</script>
|
|
157
157
|
<style>
|
package/package.json
CHANGED