@usex/mikrotik-mcp 3.3.0 → 3.4.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.
@@ -24,11 +24,11 @@ Error generating stack: `+e.message+`
24
24
  `).map((e,t)=>(0,$.jsx)(`div`,{className:e.startsWith(`+`)?`d-add`:e.startsWith(`-`)?`d-del`:e.startsWith(`@@`)?`d-hunk`:``,children:e||` `},t))})}),o&&(0,$.jsx)(uc,{title:`Snapshot · ${o.label??o.id}`,className:`reveal`,extra:(0,$.jsx)(`button`,{className:`btn`,onClick:()=>s(null),children:`✕ Close`}),children:(0,$.jsx)(`pre`,{className:`body`,style:{maxHeight:460},children:o.body||`(empty)`})})]})}var qc={add:`+`,set:`~`,remove:`−`,enable:`▲`,disable:`▽`,move:`⇅`};function Jc(){let[e,t]=(0,l.useState)(``),[n,r]=(0,l.useState)(null),[i,a]=(0,l.useState)(null),[o,s]=(0,l.useState)(!1),c=async()=>{s(!0),a(null);let t=await sc(`/api/plan`,{script:e}).catch(()=>({error:`request failed`}));if(s(!1),t.error||!t.plan){a(t.error??`no plan produced`),r(null);return}r({plan:t.plan,text:t.text??``})},u=n?.plan.grade??``,d=u===`critical`||u===`high`;return(0,$.jsxs)(`section`,{className:`view`,children:[(0,$.jsxs)(uc,{title:`Change plan — dry-run`,className:`reveal`,extra:(0,$.jsx)(`span`,{className:`muted`,children:`terraform-style preview · pure analysis, never touches a device`}),children:[(0,$.jsx)(`textarea`,{className:`plan-input`,spellCheck:!1,placeholder:`Paste intended RouterOS commands, one per line, e.g.
25
25
  /ip firewall filter add chain=input action=drop in-interface=WAN
26
26
  /ip address add address=10.0.0.1/24 interface=ether1
27
- /ip address remove [find address=192.168.88.1/24]`,value:e,onChange:e=>t(e.target.value)}),(0,$.jsxs)(`div`,{className:`toolbar`,style:{marginTop:10},children:[(0,$.jsx)(`button`,{className:`btn is-active`,onClick:()=>void c(),disabled:o||!e.trim(),children:o?`Planning…`:`▸ Plan`}),i&&(0,$.jsx)(`span`,{className:`cfg-err`,children:i})]})]}),n&&(0,$.jsxs)(uc,{title:`Plan`,className:`reveal`,extra:(0,$.jsxs)(`span`,{className:`cfg-status ${d?`is-bad`:`is-ok`}`,children:[`risk `,n.plan.riskScore,` · `,u]}),children:[(0,$.jsxs)(`div`,{className:`legend`,style:{marginTop:0},children:[(0,$.jsxs)(`span`,{children:[`+`,n.plan.counts.add,` to add`]}),(0,$.jsxs)(`span`,{children:[`~`,n.plan.counts.modify,` to modify`]}),(0,$.jsxs)(`span`,{children:[`-`,n.plan.counts.remove,` to remove`]}),n.plan.reordered&&(0,$.jsx)(`span`,{children:`· reordered into a safe sequence`})]}),n.plan.warnings.length>0&&(0,$.jsx)(`div`,{className:`cfg-errors`,style:{margin:`10px 0`},children:n.plan.warnings.map((e,t)=>(0,$.jsxs)(`div`,{className:`cfg-err`,style:{color:`var(--mt-warn)`},children:[`⚠ `,e]},t))}),(0,$.jsx)(`div`,{className:`plan-steps`,children:n.plan.steps.map(e=>(0,$.jsxs)(`div`,{className:`plan-step risk-${e.risk}`,children:[(0,$.jsx)(`span`,{className:`plan-op`,children:qc[e.op]??`•`}),(0,$.jsx)(`code`,{children:e.command}),e.lockoutRisk&&(0,$.jsx)(`span`,{className:`plan-lock`,children:`⚠ lock-out`}),(0,$.jsx)(`span`,{className:`plan-risk`,children:e.risk})]},e.index))})]})]})}function Yc(){let[e,t]=(0,l.useState)(null),[n,r]=(0,l.useState)(null),[i,a]=(0,l.useState)(null),[o,s]=(0,l.useState)(null),c=(0,l.useCallback)(()=>{ac(`/api/s3/list`).then(t).catch(()=>t({configured:!1,objects:[]}))},[]);(0,l.useEffect)(()=>c(),[c]);let u=e=>{ac(`/api/s3/presign?key=${encodeURIComponent(e)}`).then(e=>{e.url?window.open(e.url,`_blank`,`noopener`):s(`could not generate download link`)}).catch(()=>s(`could not generate download link`))},d=async e=>{a(e);let t=await sc(`/api/s3/delete`,{key:e}).catch(()=>({error:`request failed`}));a(null),r(null),t.ok?(s(`Deleted ${e}`),c()):s(t.error??`delete failed`)};return e?e.configured?(0,$.jsx)(`section`,{className:`view`,children:(0,$.jsxs)(uc,{title:`S3 backups`,className:`reveal`,extra:(0,$.jsxs)($.Fragment,{children:[(0,$.jsxs)(`span`,{className:`muted`,children:[e.target,` · `,e.objects.length,` object`,e.objects.length===1?``:`s`,e.truncated?` (truncated)`:``]}),(0,$.jsx)(`button`,{className:`btn`,onClick:c,style:{marginLeft:10},children:`↻ Refresh`})]}),children:[o&&(0,$.jsx)(`div`,{className:`cfg-msg`,children:o}),e.objects.length===0?(0,$.jsxs)(`div`,{className:`muted`,style:{padding:12},children:[`No objects in the bucket. Upload one with the `,(0,$.jsx)(`code`,{children:`upload_backup_to_s3`}),` tool.`]}):(0,$.jsx)(`div`,{className:`feedwrap`,children:(0,$.jsxs)(`table`,{className:`feed`,children:[(0,$.jsx)(`thead`,{children:(0,$.jsxs)(`tr`,{children:[(0,$.jsx)(`th`,{children:`key`}),(0,$.jsx)(`th`,{className:`num`,children:`size`}),(0,$.jsx)(`th`,{children:`modified`}),(0,$.jsx)(`th`,{style:{width:200},children:`actions`})]})}),(0,$.jsx)(`tbody`,{children:e.objects.map(e=>(0,$.jsxs)(`tr`,{children:[(0,$.jsx)(`td`,{children:e.key}),(0,$.jsx)(`td`,{className:`num`,children:Qs(e.size)}),(0,$.jsx)(`td`,{children:e.lastModified?new Date(e.lastModified).toLocaleString():`—`}),(0,$.jsxs)(`td`,{onClick:e=>e.stopPropagation(),children:[(0,$.jsx)(`button`,{className:`btn`,onClick:()=>u(e.key),children:`⤓ Download`}),` `,n===e.key?(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`button`,{className:`btn btn-danger`,disabled:i===e.key,onClick:()=>void d(e.key),children:`✓ Confirm`}),` `,(0,$.jsx)(`button`,{className:`btn`,onClick:()=>r(null),children:`Cancel`})]}):(0,$.jsx)(`button`,{className:`btn`,onClick:()=>r(e.key),children:`🗑 Delete`})]})]},e.key))})]})})]})}):(0,$.jsxs)(`div`,{className:`feed-empty`,children:[(0,$.jsx)(`div`,{className:`feed-empty__icon`,children:`☁️`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:`S3 is not configured`}),(0,$.jsxs)(`p`,{className:`feed-empty__sub`,children:[`Add an `,(0,$.jsx)(`code`,{children:`s3`}),` block (bucket + credentials) to your config to manage backup objects here.`]})]}):(0,$.jsx)(`div`,{className:`muted`,children:`loading S3 objects…`})}function Xc({event:e,onClose:t}){let n=e=>void navigator.clipboard?.writeText(e).catch(()=>{});return(0,$.jsx)(`div`,{className:`overlay`,onClick:e=>e.target===e.currentTarget&&t(),children:(0,$.jsxs)(`div`,{className:`sheet`,children:[(0,$.jsxs)(`div`,{className:`sheet__hd`,children:[(0,$.jsx)(`span`,{className:`risk risk-${e.risk}`,children:e.risk}),(0,$.jsx)(`h3`,{children:e.tool}),(0,$.jsx)(`span`,{style:{flex:1}}),(0,$.jsx)(`button`,{className:`btn`,onClick:t,children:`✕ Close`})]}),(0,$.jsxs)(`div`,{className:`kv__body`,children:[(0,$.jsx)(`div`,{className:`kv__k`,children:`title`}),(0,$.jsx)(`div`,{className:`kv__v`,children:e.title}),(0,$.jsx)(`div`,{className:`kv__k`,children:`time`}),(0,$.jsx)(`div`,{className:`kv__v`,children:new Date(e.ts).toLocaleString()}),(0,$.jsx)(`div`,{className:`kv__k`,children:`device`}),(0,$.jsx)(`div`,{className:`kv__v`,children:e.device??`—`}),(0,$.jsx)(`div`,{className:`kv__k`,children:`transport`}),(0,$.jsx)(`div`,{className:`kv__v`,children:e.transport??`—`}),(0,$.jsx)(`div`,{className:`kv__k`,children:`duration`}),(0,$.jsx)(`div`,{className:`kv__v`,children:Zs(e.durationMs)}),(0,$.jsx)(`div`,{className:`kv__k`,children:`status`}),(0,$.jsx)(`div`,{className:`kv__v`,children:(0,$.jsx)(`span`,{className:e.isError?`status-err`:`status-ok`,children:e.isError?`error`:`ok`})}),(0,$.jsx)(`div`,{className:`kv__k`,children:`output size`}),(0,$.jsxs)(`div`,{className:`kv__v`,children:[Qs(e.outputBytes),e.truncated?` (truncated)`:``]}),(0,$.jsx)(`div`,{className:`kv__k`,children:`structured`}),(0,$.jsx)(`div`,{className:`kv__v`,children:e.hasStructured?`yes (renders an MCP App view)`:`no`})]}),e.error&&(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`h2`,{className:`muted`,children:`ERROR`}),(0,$.jsx)(`pre`,{className:`body`,style:{color:`var(--mt-bad)`},children:e.error})]}),(0,$.jsxs)(`div`,{className:`sheet__hd`,children:[(0,$.jsx)(`h2`,{className:`muted`,style:{margin:0},children:`INPUT`}),(0,$.jsx)(`span`,{style:{flex:1}}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>n(e.input),children:`Copy`})]}),e.input?(0,$.jsx)(Gc,{value:e.input}):(0,$.jsx)(`pre`,{className:`body`,children:`—`}),(0,$.jsxs)(`div`,{className:`sheet__hd`,children:[(0,$.jsx)(`h2`,{className:`muted`,style:{margin:0},children:`OUTPUT`}),(0,$.jsx)(`span`,{style:{flex:1}}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>n(e.output),children:`Copy`})]}),(0,$.jsx)(`pre`,{className:`body`,children:e.output||`—`})]})})}function Zc(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 Qc=[{id:`overview`,label:`Overview`,sub:`Calls, latency & risk at a glance`},{id:`devices`,label:`Devices`,sub:`Connectivity radar & system health`},{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:`plan`,label:`Change Plan`,sub:`Dry-run intended RouterOS commands`},{id:`s3`,label:`S3 Backups`,sub:`List, download & delete S3 backup objects`},{id:`config`,label:`Config`,sub:`Effective configuration & safe editor`},{id:`feed`,label:`Live Feed`,sub:`Every tool call, in real time`}];function $c({name:e}){return(0,$.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.7`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,children:{overview:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`rect`,{x:`3`,y:`3`,width:`8`,height:`8`,rx:`1.6`}),(0,$.jsx)(`rect`,{x:`13`,y:`3`,width:`8`,height:`5`,rx:`1.6`}),(0,$.jsx)(`rect`,{x:`13`,y:`10`,width:`8`,height:`11`,rx:`1.6`}),(0,$.jsx)(`rect`,{x:`3`,y:`13`,width:`8`,height:`8`,rx:`1.6`})]}),devices:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`rect`,{x:`3`,y:`4`,width:`18`,height:`7`,rx:`2`}),(0,$.jsx)(`rect`,{x:`3`,y:`13`,width:`18`,height:`7`,rx:`2`}),(0,$.jsx)(`path`,{d:`M7 7.5h.01M7 16.5h.01`})]}),topology:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`circle`,{cx:`12`,cy:`5`,r:`2.4`}),(0,$.jsx)(`circle`,{cx:`5`,cy:`19`,r:`2.4`}),(0,$.jsx)(`circle`,{cx:`19`,cy:`19`,r:`2.4`}),(0,$.jsx)(`path`,{d:`M12 7.4 6.4 16.6M12 7.4 17.6 16.6`})]}),packets:(0,$.jsx)(`path`,{d:`M3 12h4l2-7 4 14 2-7h6`}),snapshots:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`path`,{d:`M12 3 3 7.5 12 12 21 7.5 12 3Z`}),(0,$.jsx)(`path`,{d:`M3 12 12 16.5 21 12`}),(0,$.jsx)(`path`,{d:`M3 16.5 12 21 21 16.5`})]}),plan:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`circle`,{cx:`6`,cy:`6`,r:`2.3`}),(0,$.jsx)(`circle`,{cx:`6`,cy:`18`,r:`2.3`}),(0,$.jsx)(`circle`,{cx:`18`,cy:`8`,r:`2.3`}),(0,$.jsx)(`path`,{d:`M6 8.3v7.4M8.3 6H13a3 3 0 0 1 3 3v0`})]}),s3:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`ellipse`,{cx:`12`,cy:`6`,rx:`7`,ry:`2.6`}),(0,$.jsx)(`path`,{d:`M5 6v12c0 1.5 3.1 2.6 7 2.6s7-1.1 7-2.6V6`}),(0,$.jsx)(`path`,{d:`M5 12c0 1.5 3.1 2.6 7 2.6s7-1.1 7-2.6`})]}),config:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`path`,{d:`M4 7h8M16 7h4M4 17h4M12 17h8`}),(0,$.jsx)(`circle`,{cx:`14`,cy:`7`,r:`2.2`}),(0,$.jsx)(`circle`,{cx:`10`,cy:`17`,r:`2.2`})]}),feed:(0,$.jsx)(`path`,{d:`M4 6h16M4 12h16M4 18h10`})}[e]})}function el(){let[e,t]=(0,l.useState)(null),[n,r]=(0,l.useState)(null),[i,a]=(0,l.useState)(null),[o,s]=(0,l.useState)(null),[c,u]=(0,l.useState)(null),[d,f]=(0,l.useState)(!1),[p,m]=(0,l.useState)(null),[h,g]=(0,l.useState)([]),[_,v]=(0,l.useState)(36e5),[y,b]=(0,l.useState)(!1),[x,S]=(0,l.useState)(`off`),[C,w]=(0,l.useState)(null),[T,E]=(0,l.useState)(()=>new Set),[D,O]=(0,l.useState)({}),[k,A]=(0,l.useState)({tool:``,risk:``,device:``,status:``,q:``}),j=(0,l.useRef)(y);j.current=y;let M=(0,l.useRef)(null);Mc(M);let[N,P]=(0,l.useState)(`overview`),[F,I]=(0,l.useState)(``),[L,ee]=(0,l.useState)(`all`),te=(0,l.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]),ne=(0,l.useMemo)(()=>{let e=i?.devices??[],t=F.trim().toLowerCase();return e.filter(e=>!(L===`online`&&e.status.reachable!==!0||L===`offline`&&e.status.reachable!==!1||t&&!e.name.toLowerCase().includes(t)&&!(e.address??e.host??``).toLowerCase().includes(t)))},[i,F,L]);jc((0,l.useCallback)(e=>{if(j.current)return;g(t=>[e,...t].slice(0,lc));let t=e.device;t&&O(e=>({...e,[t]:(e[t]??0)+1}))},[]),(0,l.useCallback)(e=>S(e),[])),(0,l.useEffect)(()=>{ac(`/api/events?limit=200`).then(e=>g(e.events)).catch(()=>{})},[]);let R=(0,l.useCallback)(()=>{ac(`/api/stats?window=${_}&buckets=60`).then(t).catch(()=>{})},[_]);(0,l.useEffect)(()=>{R();let e=()=>{R(),ac(`/api/meta`).then(r).catch(()=>{}),ac(`/api/devices`).then(a).catch(()=>{}),ac(`/api/topology`).then(s).catch(()=>{})};e();let t=setInterval(()=>{j.current||e()},4e3);return()=>clearInterval(t)},[R]),(0,l.useEffect)(()=>{let e=()=>void ac(`/api/config`).then(u).catch(()=>{});e();let t=setInterval(e,3e4);return()=>clearInterval(t)},[]),(0,l.useEffect)(()=>{let e=e=>{e.key===`Escape`&&w(null)};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[]);let z=(0,l.useMemo)(()=>{let e=k.q.trim().toLowerCase();return h.filter(t=>!(k.tool&&t.tool!==k.tool||k.risk&&t.risk!==k.risk||k.device&&t.device!==k.device||k.status===`ok`&&t.isError||k.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)},[h,k]),B=!!(k.tool||k.risk||k.device||k.status||k.q),re=(0,l.useMemo)(()=>h.filter(e=>e.isError),[h]),V=(0,l.useMemo)(()=>({ok:h.length-re.length,error:re.length}),[h,re]),ie=(0,l.useCallback)(async e=>{try{w(await ac(`/api/event/${encodeURIComponent(e.id)}`))}catch{w(e)}},[]),ae=(0,l.useMemo)(()=>z.slice(0,200),[z]),oe=(0,l.useMemo)(()=>ae.map(e=>e.id),[ae]),se=oe.length>0&&oe.every(e=>T.has(e)),H=!se&&oe.some(e=>T.has(e)),ce=(0,l.useCallback)(e=>{E(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},[]),le=(0,l.useCallback)(()=>{E(e=>{let t=new Set(e);if(oe.length>0&&oe.every(e=>t.has(e)))for(let e of oe)t.delete(e);else for(let e of oe)t.add(e);return t})},[oe]),[ue,de]=(0,l.useState)(!1),fe=(0,l.useCallback)(async()=>{let e=[...T];if(e.length!==0)try{await oc({ids:e});let t=new Set(e);g(e=>e.filter(e=>!t.has(e.id))),E(new Set)}catch{}finally{de(!1)}},[T]),pe=e=>{if(e===`json`){Zc(`mcp-events.json`,JSON.stringify(z,null,2),`application/json`);return}let t=[`ts`,`tool`,`risk`,`device`,`durationMs`,`isError`,`error`],n=e=>/[",\n]/.test(e)?`"${e.replace(/"/g,`""`)}"`:e,r=z.map(e=>[new Date(e.ts).toISOString(),e.tool,e.risk,e.device??``,String(e.durationMs),String(e.isError),e.error??``].map(n).join(`,`)).join(`
28
- `);Zc(`mcp-events.csv`,`${t.join(`,`)}\n${r}\n`,`text/csv`)},me=e?e.errorRate>=.2?`is-bad`:e.errorRate>=.05?`is-warn`:`is-good`:``,he=c?.mcp??{},ge=c?.dashboard??{},U=(e,t,n)=>(0,$.jsxs)(`select`,{className:`btn`,value:k[e],onChange:t=>A(n=>({...n,[e]:t.target.value})),children:[(0,$.jsx)(`option`,{value:``,children:t}),n.map(e=>(0,$.jsx)(`option`,{value:e,children:e},e))]}),_e=Qc.find(e=>e.id===N)??Qc[0];return(0,$.jsxs)(`div`,{className:`shell`,ref:M,children:[(0,$.jsxs)(`aside`,{className:`nav`,children:[(0,$.jsxs)(`div`,{className:`nav__brand`,children:[(0,$.jsx)(`div`,{className:`nav__mark`,children:(0,$.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,"aria-hidden":`true`,children:(0,$.jsxs)(`g`,{stroke:`#18181b`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,$.jsx)(`path`,{d:`M12 12 L4 5 M12 12 L20 5 M12 12 L12 20`}),(0,$.jsx)(`circle`,{cx:`12`,cy:`12`,r:`3`,fill:`#18181b`,stroke:`none`}),(0,$.jsx)(`circle`,{cx:`4`,cy:`5`,r:`1.9`,fill:`#18181b`,stroke:`none`}),(0,$.jsx)(`circle`,{cx:`20`,cy:`5`,r:`1.9`,fill:`#18181b`,stroke:`none`}),(0,$.jsx)(`circle`,{cx:`12`,cy:`20`,r:`1.9`,fill:`#18181b`,stroke:`none`})]})})}),(0,$.jsxs)(`div`,{className:`nav__brandtext`,children:[(0,$.jsx)(`b`,{children:`MikroTik MCP`}),(0,$.jsx)(`small`,{children:`Observability`})]})]}),(0,$.jsx)(`nav`,{className:`nav__items`,children:Qc.map(e=>(0,$.jsxs)(`button`,{className:`nav__item${N===e.id?` is-active`:``}`,onClick:()=>P(e.id),children:[(0,$.jsx)($c,{name:e.id}),(0,$.jsx)(`span`,{children:e.label}),e.id===`feed`&&h.length>0&&(0,$.jsx)(`span`,{className:`nav__badge`,children:h.length>999?`999+`:h.length})]},e.id))}),(0,$.jsxs)(`div`,{className:`nav__foot`,children:[(0,$.jsxs)(`span`,{className:`hero__live${x===`off`?``:` is-on`}`,title:`Live transport: WebSocket (preferred) or SSE fallback`,children:[(0,$.jsx)(`span`,{className:`dot`}),x===`off`?`offline`:`live · ${x}`]}),(0,$.jsx)(`small`,{className:`muted`,children:n?`${ec(n.total)} events · ${n.transport}`:`connecting…`})]})]}),(0,$.jsxs)(`main`,{className:`main`,children:[(0,$.jsxs)(`header`,{className:`topline reveal`,children:[(0,$.jsxs)(`div`,{className:`topline__txt`,children:[(0,$.jsx)(`h1`,{children:_e.label}),(0,$.jsx)(`small`,{children:_e.sub})]}),(0,$.jsx)(`span`,{className:`topline__spacer`}),N===`overview`&&(0,$.jsx)(`select`,{className:`btn`,value:_,onChange:e=>v(Number(e.target.value)),title:`Stats time window`,children:cc.map(([e,t])=>(0,$.jsxs)(`option`,{value:t,children:[`window: `,e]},t))})]}),N===`overview`&&(0,$.jsxs)(`section`,{className:`view`,children:[(0,$.jsx)(`div`,{className:`cards reveal`,children:e?(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(dc,{k:`Calls (window)`,v:ec(e.total)}),(0,$.jsx)(dc,{k:`Calls / min`,v:e.callsPerMin.toFixed(1)}),(0,$.jsx)(dc,{k:`Error rate`,v:`${(e.errorRate*100).toFixed(1)}%`,sub:`${e.errors} err`,cls:me}),(0,$.jsx)(dc,{k:`Avg latency`,v:Zs(e.latency.avg)}),(0,$.jsx)(dc,{k:`p95 latency`,v:Zs(e.latency.p95)}),(0,$.jsx)(dc,{k:`p99 latency`,v:Zs(e.latency.p99)}),(0,$.jsx)(dc,{k:`Distinct tools`,v:ec(e.distinctTools)}),(0,$.jsx)(dc,{k:`Output volume`,v:Qs(e.outputBytes)})]}):(0,$.jsxs)(`div`,{className:`stat`,children:[(0,$.jsx)(`p`,{className:`k`,children:`Loading…`}),(0,$.jsx)(`div`,{className:`v`,children:`—`})]})}),(0,$.jsxs)(`div`,{className:`bento reveal`,children:[(0,$.jsxs)(uc,{title:`Calls over time`,className:`b-series`,children:[e?(0,$.jsx)(fc,{series:e.series}):(0,$.jsx)(`div`,{className:`muted`,children:`no data`}),(0,$.jsxs)(`div`,{className:`legend`,children:[(0,$.jsxs)(`span`,{children:[(0,$.jsx)(`i`,{style:{background:`#d4d4d8`}}),`ok`]}),(0,$.jsxs)(`span`,{children:[(0,$.jsx)(`i`,{style:{background:`#f87171`}}),`error`]})]})]}),e&&(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(uc,{title:`By risk`,className:`b-risk`,children:(0,$.jsx)(pc,{segments:Object.keys(e.byRisk).map(t=>({label:t,value:e.byRisk[t],color:nc[t]}))})}),(0,$.jsx)(uc,{title:`Top tools`,className:`b-tools`,children:(0,$.jsx)(mc,{rows:e.byTool.map(e=>({label:e.tool,value:e.count,sub:`${e.count}× · ${Zs(e.p95Ms)} p95${e.errors?` · ${e.errors} err`:``}`,color:e.errors?`#f87171`:void 0}))})}),(0,$.jsx)(uc,{title:`Status`,className:`b-status`,children:(0,$.jsx)(pc,{segments:[{label:`ok`,value:V.ok,color:`#d4d4d8`},{label:`error`,value:V.error,color:`#f87171`}]})}),(0,$.jsx)(uc,{title:`By device`,className:`b-device`,children:e.byDevice.length?(0,$.jsx)(mc,{rows:e.byDevice.map(e=>({label:e.device,value:e.count}))}):(0,$.jsx)(`div`,{className:`muted`,children:`single device`})}),(0,$.jsx)(uc,{title:`Recent errors`,className:`b-errors`,children:re.length?(0,$.jsx)(`div`,{className:`hbar`,children:re.slice(0,8).map(e=>(0,$.jsxs)(`div`,{className:`hbar__row conn-errrow`,style:{gridTemplateColumns:`auto 1fr`},onClick:()=>void ie(e),children:[(0,$.jsx)(`span`,{className:`muted`,children:$s(e.ts)}),(0,$.jsxs)(`span`,{style:{color:`var(--mt-bad)`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,minWidth:0},title:e.error??e.output,children:[e.tool,`: `,e.error??e.output??`error`]})]},e.id))}):(0,$.jsx)(`div`,{className:`muted`,children:`no errors 🎉`})})]})]})]}),N===`devices`&&(i&&i.devices.length>0?(0,$.jsxs)(`section`,{className:`view`,children:[(0,$.jsxs)(`div`,{className:`dev-toolbar reveal`,children:[(0,$.jsx)(`input`,{className:`search`,type:`search`,placeholder:`Search devices by name or address…`,value:F,onChange:e=>I(e.target.value),style:{flex:1,minWidth:180}}),(0,$.jsx)(`div`,{className:`dev-filters`,children:[`all`,`online`,`offline`].map(e=>(0,$.jsx)(`button`,{className:`dev-fbtn${L===e?` is-active`:``}`,onClick:()=>ee(e),children:e===`all`?`All ${te.total}`:e===`online`?`Online ${te.online}`:`Offline ${te.offline}`},e))}),(0,$.jsxs)(`span`,{className:`muted`,children:[ne.length,`/`,te.total,` shown`]})]}),(0,$.jsxs)(`details`,{className:`dev-collapse reveal`,open:te.total<=8,children:[(0,$.jsxs)(`summary`,{children:[`Connectivity radar`,(0,$.jsxs)(`span`,{className:`muted`,children:[` `,`· `,te.online,` online · `,te.offline,` offline ·`,` `,te.total,` total`]})]}),(0,$.jsx)(Tc,{payload:i,pulses:D})]}),ne.length===0?(0,$.jsxs)(`div`,{className:`feed-empty reveal`,children:[(0,$.jsx)(`div`,{className:`feed-empty__icon`,children:`🔍`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:`No devices match`}),(0,$.jsx)(`p`,{className:`feed-empty__sub`,children:`Try a different search or status filter.`})]}):(0,$.jsx)(`div`,{className:`dev-grid-wide reveal`,children:ne.map(e=>(0,$.jsx)(Ec,{d:e},e.name))}),ne.length>0&&(0,$.jsx)(uc,{title:`Device system health`,className:`reveal`,extra:(0,$.jsx)(`span`,{className:`muted`,children:`CPU · memory · disk · latency · live probe`}),children:(0,$.jsx)(`div`,{className:`health-grid`,children:ne.map(e=>(0,$.jsx)(Ac,{d:e},e.name))})})]}):(0,$.jsxs)(`div`,{className:`feed-empty`,children:[(0,$.jsx)(`div`,{className:`feed-empty__icon`,children:`🖧`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:`No devices configured`}),(0,$.jsx)(`p`,{className:`feed-empty__sub`,children:`Add a device to your config to see connectivity and system health here.`})]})),N===`topology`&&(o&&o.nodes.length>0?(0,$.jsx)(`section`,{className:`view`,children:(0,$.jsx)(uc,{title:`Network topology`,className:`reveal`,extra:(0,$.jsx)(`span`,{className:`muted`,children:`Layer-2 neighbours via MNDP/CDP/LLDP · click a neighbour to onboard it`}),children:(0,$.jsx)(Rc,{topo:o,onOnboard:(e,t)=>{m({name:e,body:t}),f(!0),P(`config`)}})})}):(0,$.jsxs)(`div`,{className:`feed-empty`,children:[(0,$.jsx)(`div`,{className:`feed-empty__icon`,children:`🛰️`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:`No neighbours discovered yet`}),(0,$.jsx)(`p`,{className:`feed-empty__sub`,children:`Layer-2 neighbours (MNDP / CDP / LLDP) appear here as the device reports them.`})]})),N===`packets`&&(0,$.jsx)(`section`,{className:`view`,children:(0,$.jsx)(uc,{title:`Packet capture`,className:`reveal`,extra:(0,$.jsx)(`span`,{className:`muted`,children:`live TZSP decode · /tool sniffer streaming`}),children:(0,$.jsx)(Wc,{})})}),N===`snapshots`&&(0,$.jsx)(Kc,{}),N===`plan`&&(0,$.jsx)(Jc,{}),N===`s3`&&(0,$.jsx)(Yc,{}),N===`config`&&(c?(0,$.jsx)(`section`,{className:`view`,children:(0,$.jsx)(uc,{title:`Configuration`,className:`reveal`,extra:(0,$.jsx)(`button`,{className:`btn`,onClick:()=>f(e=>!e),title:`Edit the config JSON with autocomplete, validation and safe-apply`,children:d?`View`:`✎ Edit config`}),children:d?(0,$.jsx)(Ic,{initial:p?{...c,devices:{...c.devices,[p.name]:p.body}}:c,onClose:()=>{f(!1),m(null)},onReload:()=>{m(null),ac(`/api/config`).then(u).catch(()=>{})}},p?`seed-${p.name}`:`config`):(0,$.jsxs)($.Fragment,{children:[(0,$.jsxs)(`div`,{className:`legend`,style:{margin:`0 0 10px`},children:[(0,$.jsxs)(`span`,{children:[`transport: `,tc(he.transport)]}),(0,$.jsxs)(`span`,{children:[`read-only: `,c.readOnly?`yes`:`no`]}),(0,$.jsxs)(`span`,{children:[`dashboard: `,tc(ge.host),`:`,tc(ge.port)]}),(0,$.jsxs)(`span`,{children:[`capture: `,ge.captureBody?`on`:`off`]}),(0,$.jsxs)(`span`,{children:[`s3: `,c.s3?`configured`:`off`]})]}),(0,$.jsxs)(`details`,{className:`cfg`,children:[(0,$.jsx)(`summary`,{children:`Full effective configuration (secrets redacted)`}),(0,$.jsx)(Gc,{value:c,maxHeight:340})]})]})})}):(0,$.jsxs)(`div`,{className:`feed-empty`,children:[(0,$.jsx)(`div`,{className:`feed-empty__icon`,children:`⚙️`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:`Loading configuration…`})]})),N===`feed`&&(0,$.jsxs)(`div`,{className:`panel reveal`,children:[(0,$.jsxs)(`div`,{className:`sheet__hd`,style:{marginBottom:12},children:[(0,$.jsx)(`h2`,{style:{margin:0},children:`Live tool calls`}),(0,$.jsx)(`span`,{style:{flex:1}}),(0,$.jsxs)(`span`,{className:`muted`,children:[z.length,` shown · `,h.length,` buffered`]})]}),(0,$.jsxs)(`div`,{className:`toolbar`,style:{marginBottom:12},children:[(0,$.jsx)(`div`,{className:`grow`,style:{flex:1,minWidth:180},children:(0,$.jsx)(`input`,{className:`search`,type:`search`,placeholder:`Search tool / input / output / error…`,value:k.q,onChange:e=>A(t=>({...t,q:e.target.value}))})}),U(`tool`,`all tools`,n?.tools??[]),U(`risk`,`all risk`,[`READ`,`WRITE`,`WRITE_IDEMPOTENT`,`DESTRUCTIVE`,`DANGEROUS`]),U(`device`,`all devices`,n?.devices??[]),U(`status`,`all status`,[`ok`,`error`]),(0,$.jsx)(`button`,{className:`btn${y?` is-active`:``}`,onClick:()=>b(e=>!e),children:y?`▶ Resume`:`⏸ Pause`}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>pe(`csv`),children:`CSV`}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>pe(`json`),children:`JSON`}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>A({tool:``,risk:``,device:``,status:``,q:``}),children:`Clear`}),ue&&T.size>0?(0,$.jsxs)($.Fragment,{children:[(0,$.jsxs)(`button`,{className:`btn btn-danger`,onClick:()=>void fe(),children:[`✓ Confirm delete (`,T.size,`)`]}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>de(!1),children:`Cancel`})]}):(0,$.jsxs)(`button`,{className:`btn`,disabled:T.size===0,onClick:()=>de(!0),title:`Delete the selected rows`,children:[`🗑 Delete`,T.size>0?` (${T.size})`:``]})]}),z.length===0?B?(0,$.jsxs)(`div`,{className:`feed-empty`,children:[(0,$.jsx)(`div`,{className:`feed-empty__icon`,children:`🔍`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:`No calls match your filters`}),(0,$.jsxs)(`p`,{className:`feed-empty__sub`,children:[h.length,` call`,h.length===1?``:`s`,` buffered — try widening the search or the risk / device / status filters.`]}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>A({tool:``,risk:``,device:``,status:``,q:``}),children:`Clear filters`})]}):(0,$.jsxs)(`div`,{className:`feed-empty`,children:[(0,$.jsx)(`div`,{className:`feed-empty__pulse${x===`off`?``:` is-on`}`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:x===`off`?`Not connected`:`Listening for tool calls…`}),(0,$.jsx)(`p`,{className:`feed-empty__sub`,children:x===`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,$.jsx)(`div`,{className:`feedwrap`,children:(0,$.jsxs)(`table`,{className:`feed`,children:[(0,$.jsx)(`thead`,{children:(0,$.jsxs)(`tr`,{children:[(0,$.jsx)(`th`,{style:{width:28},children:(0,$.jsx)(`input`,{type:`checkbox`,"aria-label":`Select all shown rows`,checked:se,ref:e=>{e&&(e.indeterminate=H)},onChange:le})}),(0,$.jsx)(`th`,{children:`time`}),(0,$.jsx)(`th`,{children:`tool`}),(0,$.jsx)(`th`,{children:`risk`}),(0,$.jsx)(`th`,{children:`device`}),(0,$.jsx)(`th`,{className:`num`,children:`dur`}),(0,$.jsx)(`th`,{children:`status`}),(0,$.jsx)(`th`,{children:`output`})]})}),(0,$.jsx)(`tbody`,{children:ae.map(e=>(0,$.jsxs)(`tr`,{className:`${e.isError?`is-err`:``}${T.has(e.id)?` is-selected`:``}`.trim()||void 0,onClick:()=>void ie(e),children:[(0,$.jsx)(`td`,{onClick:e=>e.stopPropagation(),children:(0,$.jsx)(`input`,{type:`checkbox`,"aria-label":`Select row`,checked:T.has(e.id),onChange:()=>ce(e.id)})}),(0,$.jsx)(`td`,{children:$s(e.ts)}),(0,$.jsx)(`td`,{children:e.tool}),(0,$.jsx)(`td`,{children:(0,$.jsx)(`span`,{className:`risk risk-${e.risk}`,children:e.risk.replace(`WRITE_IDEMPOTENT`,`WRITE·I`)})}),(0,$.jsx)(`td`,{children:e.device??`—`}),(0,$.jsx)(`td`,{className:`num`,children:Zs(e.durationMs)}),(0,$.jsx)(`td`,{children:(0,$.jsx)(`span`,{className:e.isError?`status-err`:`status-ok`,children:e.isError?`error`:`ok`})}),(0,$.jsx)(`td`,{className:`preview`,children:e.isError?e.error??`error`:e.output||`—`})]},e.id))})]})})]})]}),C&&(0,$.jsx)(Xc,{event:C,onClose:()=>w(null)})]})}var tl=document.getElementById(`root`);tl&&(0,u.createRoot)(tl).render((0,$.jsx)(el,{}));
27
+ /ip address remove [find address=192.168.88.1/24]`,value:e,onChange:e=>t(e.target.value)}),(0,$.jsxs)(`div`,{className:`toolbar`,style:{marginTop:10},children:[(0,$.jsx)(`button`,{className:`btn is-active`,onClick:()=>void c(),disabled:o||!e.trim(),children:o?`Planning…`:`▸ Plan`}),i&&(0,$.jsx)(`span`,{className:`cfg-err`,children:i})]})]}),n&&(0,$.jsxs)(uc,{title:`Plan`,className:`reveal`,extra:(0,$.jsxs)(`span`,{className:`cfg-status ${d?`is-bad`:`is-ok`}`,children:[`risk `,n.plan.riskScore,` · `,u]}),children:[(0,$.jsxs)(`div`,{className:`legend`,style:{marginTop:0},children:[(0,$.jsxs)(`span`,{children:[`+`,n.plan.counts.add,` to add`]}),(0,$.jsxs)(`span`,{children:[`~`,n.plan.counts.modify,` to modify`]}),(0,$.jsxs)(`span`,{children:[`-`,n.plan.counts.remove,` to remove`]}),n.plan.reordered&&(0,$.jsx)(`span`,{children:`· reordered into a safe sequence`})]}),n.plan.warnings.length>0&&(0,$.jsx)(`div`,{className:`cfg-errors`,style:{margin:`10px 0`},children:n.plan.warnings.map((e,t)=>(0,$.jsxs)(`div`,{className:`cfg-err`,style:{color:`var(--mt-warn)`},children:[`⚠ `,e]},t))}),(0,$.jsx)(`div`,{className:`plan-steps`,children:n.plan.steps.map(e=>(0,$.jsxs)(`div`,{className:`plan-step risk-${e.risk}`,children:[(0,$.jsx)(`span`,{className:`plan-op`,children:qc[e.op]??`•`}),(0,$.jsx)(`code`,{children:e.command}),e.lockoutRisk&&(0,$.jsx)(`span`,{className:`plan-lock`,children:`⚠ lock-out`}),(0,$.jsx)(`span`,{className:`plan-risk`,children:e.risk})]},e.index))})]})]})}function Yc(){let[e,t]=(0,l.useState)(null),[n,r]=(0,l.useState)(null),[i,a]=(0,l.useState)(null),[o,s]=(0,l.useState)(null),c=(0,l.useCallback)(()=>{ac(`/api/s3/list`).then(t).catch(()=>t({configured:!1,objects:[]}))},[]);(0,l.useEffect)(()=>c(),[c]);let u=e=>{ac(`/api/s3/presign?key=${encodeURIComponent(e)}`).then(e=>{e.url?window.open(e.url,`_blank`,`noopener`):s(`could not generate download link`)}).catch(()=>s(`could not generate download link`))},d=async e=>{a(e);let t=await sc(`/api/s3/delete`,{key:e}).catch(()=>({error:`request failed`}));a(null),r(null),t.ok?(s(`Deleted ${e}`),c()):s(t.error??`delete failed`)};return e?e.configured?(0,$.jsx)(`section`,{className:`view`,children:(0,$.jsxs)(uc,{title:`S3 backups`,className:`reveal`,extra:(0,$.jsxs)($.Fragment,{children:[(0,$.jsxs)(`span`,{className:`muted`,children:[e.target,` · `,e.objects.length,` object`,e.objects.length===1?``:`s`,e.truncated?` (truncated)`:``]}),(0,$.jsx)(`button`,{className:`btn`,onClick:c,style:{marginLeft:10},children:`↻ Refresh`})]}),children:[o&&(0,$.jsx)(`div`,{className:`cfg-msg`,children:o}),e.objects.length===0?(0,$.jsxs)(`div`,{className:`muted`,style:{padding:12},children:[`No objects in the bucket. Upload one with the `,(0,$.jsx)(`code`,{children:`upload_backup_to_s3`}),` tool.`]}):(0,$.jsx)(`div`,{className:`feedwrap`,children:(0,$.jsxs)(`table`,{className:`feed`,children:[(0,$.jsx)(`thead`,{children:(0,$.jsxs)(`tr`,{children:[(0,$.jsx)(`th`,{children:`key`}),(0,$.jsx)(`th`,{className:`num`,children:`size`}),(0,$.jsx)(`th`,{children:`modified`}),(0,$.jsx)(`th`,{style:{width:200},children:`actions`})]})}),(0,$.jsx)(`tbody`,{children:e.objects.map(e=>(0,$.jsxs)(`tr`,{children:[(0,$.jsx)(`td`,{children:e.key}),(0,$.jsx)(`td`,{className:`num`,children:Qs(e.size)}),(0,$.jsx)(`td`,{children:e.lastModified?new Date(e.lastModified).toLocaleString():`—`}),(0,$.jsxs)(`td`,{onClick:e=>e.stopPropagation(),children:[(0,$.jsx)(`button`,{className:`btn`,onClick:()=>u(e.key),children:`⤓ Download`}),` `,n===e.key?(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`button`,{className:`btn btn-danger`,disabled:i===e.key,onClick:()=>void d(e.key),children:`✓ Confirm`}),` `,(0,$.jsx)(`button`,{className:`btn`,onClick:()=>r(null),children:`Cancel`})]}):(0,$.jsx)(`button`,{className:`btn`,onClick:()=>r(e.key),children:`🗑 Delete`})]})]},e.key))})]})})]})}):(0,$.jsxs)(`div`,{className:`feed-empty`,children:[(0,$.jsx)(`div`,{className:`feed-empty__icon`,children:`☁️`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:`S3 is not configured`}),(0,$.jsxs)(`p`,{className:`feed-empty__sub`,children:[`Add an `,(0,$.jsx)(`code`,{children:`s3`}),` block (bucket + credentials) to your config to manage backup objects here.`]})]}):(0,$.jsx)(`div`,{className:`muted`,children:`loading S3 objects…`})}function Xc(){let[e,t]=(0,l.useState)(null),[n,r]=(0,l.useState)(null),[i,a]=(0,l.useState)(!1),[o,s]=(0,l.useState)(null),[c,u]=(0,l.useState)(null),[d,f]=(0,l.useState)(null),[p,m]=(0,l.useState)(``),[h,g]=(0,l.useState)(null),[_,v]=(0,l.useState)(null),y=(0,l.useRef)(null),b=(0,l.useCallback)(()=>{ac(`/api/backups`).then(e=>{t(e),m(t=>t||e.devices[0]||``)}).catch(()=>t({dir:``,devices:[],backups:[]}))},[]);(0,l.useEffect)(()=>b(),[b]);let x=(e,t)=>sc(e,t).catch(()=>({error:`request failed`})),S=async()=>{a(!0),r(`Capturing /export…`);let e=await sc(`/api/backups/create`,{device:p||void 0}).catch(()=>({error:`request failed`}));a(!1),r(e.ok?`Created ${e.name} (${e.bytes} bytes)`:`Create failed: ${e.error}`),e.ok&&b()},C=async e=>{let t=await e.text(),n=await x(`/api/backups/upload`,{name:e.name,content:t});r(n.ok?`Uploaded ${n.name}`:`Upload failed: ${n.error}`),n.ok&&b()},w=async e=>{s(null);let t=await x(`/api/backups/delete`,{name:e});r(t.ok?`Deleted ${e}`:`Delete failed: ${t.error}`),t.ok&&b()},T=async()=>{if(!d)return;let{name:e,value:t}=d;if(f(null),!t||t===e)return;let n=await x(`/api/backups/rename`,{name:e,new_name:t});r(n.ok?`Renamed to ${n.name}`:`Rename failed: ${n.error}`),n.ok&&b()},E=async e=>{if(!c)return;a(!0),r(e?`Restoring in Safe Mode…`:`Dry-run restore in Safe Mode…`);let t=await sc(`/api/backups/restore`,{name:c,device:p||void 0,confirm:e}).catch(()=>({ok:!1,message:`request failed`}));a(!1),r(t.ok?t.committed?`Restored ${c} → ${p}: ${t.message}`:`Dry-run OK (${t.applied} cmds, rolled back). Click “Restore (commit)” to apply for real.`:`Restore failed: ${t.message}`),t.ok&&e&&u(null)},D=e=>{ac(`/api/backups/get?name=${encodeURIComponent(e)}`).then(g).catch(()=>{})},O=async()=>{if(_===null)return;let t=_.trim();if(!t||t===e?.dir){v(null);return}a(!0);let n=await sc(`/api/backups/dir`,{dir:t}).catch(()=>({error:`request failed`}));a(!1),v(null),n.ok?r(n.persisted?`Backup path saved → ${n.dir}`:n.warning??`Path applied → ${n.dir}`):r(`Path change failed: ${n.error}`),n.ok&&b()};return e?(0,$.jsxs)(`section`,{className:`view`,children:[(0,$.jsxs)(uc,{title:`Local backup vault`,className:`reveal`,extra:_===null?(0,$.jsxs)(`span`,{className:`muted backup-path`,children:[(0,$.jsx)(`span`,{title:e.dir,children:e.dir}),(0,$.jsx)(`button`,{className:`linkish`,title:`Change backup directory`,onClick:()=>v(e.dir),children:`✎ edit path`})]}):(0,$.jsxs)(`span`,{className:`backup-path`,children:[(0,$.jsx)(`input`,{className:`backup-path-input`,value:_,autoFocus:!0,spellCheck:!1,placeholder:`~/.mikrotik-mcp/backups`,onChange:e=>v(e.target.value),onKeyDown:e=>{e.key===`Enter`&&O(),e.key===`Escape`&&v(null)}}),(0,$.jsx)(`button`,{className:`topo-btn cfg-save`,onClick:()=>void O(),disabled:i,children:`Save path`}),(0,$.jsx)(`button`,{className:`topo-btn`,onClick:()=>v(null),children:`Cancel`})]}),children:[n&&(0,$.jsx)(`div`,{className:`cfg-msg`,children:n}),(0,$.jsxs)(`div`,{className:`toolbar`,style:{marginBottom:12},children:[(0,$.jsxs)(`select`,{className:`btn`,value:p,onChange:e=>m(e.target.value),children:[e.devices.length===0&&(0,$.jsx)(`option`,{value:``,children:`no devices`}),e.devices.map(e=>(0,$.jsx)(`option`,{value:e,children:e},e))]}),(0,$.jsx)(`button`,{className:`btn is-active`,onClick:()=>void S(),disabled:i||!p,children:`⤓ Create backup`}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>y.current?.click(),children:`⤒ Upload .rsc`}),(0,$.jsx)(`input`,{ref:y,type:`file`,accept:`.rsc,text/plain`,style:{display:`none`},onChange:e=>{let t=e.target.files?.[0];t&&C(t),e.currentTarget.value=``}}),(0,$.jsx)(`button`,{className:`btn`,onClick:b,children:`↻ Refresh`})]}),c&&(0,$.jsxs)(`div`,{className:`cfg-banner`,style:{marginBottom:12},children:[(0,$.jsxs)(`strong`,{children:[`Restore `,c]}),` onto`,` `,(0,$.jsx)(`select`,{className:`cfg-select`,value:p,onChange:e=>m(e.target.value),children:e.devices.map(e=>(0,$.jsx)(`option`,{value:e,children:e},e))}),` `,`via Safe Mode (auto-reverts on lock-out).`,(0,$.jsx)(`span`,{style:{flex:1}}),(0,$.jsx)(`button`,{className:`topo-btn`,onClick:()=>void E(!1),disabled:i,children:`Dry-run`}),(0,$.jsx)(`button`,{className:`topo-btn cfg-save`,onClick:()=>void E(!0),disabled:i,children:`Restore (commit)`}),(0,$.jsx)(`button`,{className:`topo-btn`,onClick:()=>u(null),children:`Cancel`})]}),e.backups.length===0?(0,$.jsx)(`div`,{className:`muted`,style:{padding:12},children:"No backups yet. Click “Create backup” to capture this device’s config, or upload a `.rsc`."}):(0,$.jsx)(`div`,{className:`feedwrap`,children:(0,$.jsxs)(`table`,{className:`feed`,children:[(0,$.jsx)(`thead`,{children:(0,$.jsxs)(`tr`,{children:[(0,$.jsx)(`th`,{children:`name`}),(0,$.jsx)(`th`,{children:`device`}),(0,$.jsx)(`th`,{className:`num`,children:`size`}),(0,$.jsx)(`th`,{children:`captured`}),(0,$.jsx)(`th`,{style:{width:280},children:`actions`})]})}),(0,$.jsx)(`tbody`,{children:e.backups.map(e=>(0,$.jsxs)(`tr`,{className:h?.name===e.name?`is-selected`:void 0,children:[(0,$.jsx)(`td`,{children:e.name}),(0,$.jsx)(`td`,{children:e.device??`—`}),(0,$.jsx)(`td`,{className:`num`,children:Qs(e.bytes)}),(0,$.jsx)(`td`,{children:new Date(e.modified).toLocaleString()}),(0,$.jsxs)(`td`,{onClick:e=>e.stopPropagation(),children:[(0,$.jsx)(`button`,{className:`btn`,onClick:()=>D(e.name),children:`view`}),` `,(0,$.jsx)(`a`,{className:`btn`,href:ic(`/api/backups/raw?name=${encodeURIComponent(e.name)}`),download:e.name,children:`⤓`}),` `,d?.name===e.name?(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`input`,{className:`search`,style:{display:`inline-block`,width:150},value:d.value,autoFocus:!0,onChange:t=>f({name:e.name,value:t.target.value}),onKeyDown:e=>e.key===`Enter`&&void T()}),` `,(0,$.jsx)(`button`,{className:`btn cfg-save`,onClick:()=>void T(),children:`✓`}),` `,(0,$.jsx)(`button`,{className:`btn`,onClick:()=>f(null),children:`✕`}),` `]}):(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`button`,{className:`btn`,onClick:()=>f({name:e.name,value:e.name}),children:`rename`}),` `,(0,$.jsx)(`button`,{className:`btn cfg-save`,onClick:()=>u(e.name),children:`restore`}),` `]}),o===e.name?(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`button`,{className:`btn btn-danger`,onClick:()=>void w(e.name),children:`✓`}),` `,(0,$.jsx)(`button`,{className:`btn`,onClick:()=>s(null),children:`✕`})]}):(0,$.jsx)(`button`,{className:`btn`,onClick:()=>s(e.name),children:`🗑`})]})]},e.name))})]})})]}),h&&(0,$.jsx)(uc,{title:`Backup · ${h.name}`,className:`reveal`,extra:(0,$.jsx)(`button`,{className:`btn`,onClick:()=>g(null),children:`✕ Close`}),children:(0,$.jsx)(`pre`,{className:`body`,style:{maxHeight:460},children:h.content||`(empty)`})})]}):(0,$.jsx)(`div`,{className:`muted`,children:`loading backups…`})}function Zc({event:e,onClose:t}){let n=e=>void navigator.clipboard?.writeText(e).catch(()=>{});return(0,$.jsx)(`div`,{className:`overlay`,onClick:e=>e.target===e.currentTarget&&t(),children:(0,$.jsxs)(`div`,{className:`sheet`,children:[(0,$.jsxs)(`div`,{className:`sheet__hd`,children:[(0,$.jsx)(`span`,{className:`risk risk-${e.risk}`,children:e.risk}),(0,$.jsx)(`h3`,{children:e.tool}),(0,$.jsx)(`span`,{style:{flex:1}}),(0,$.jsx)(`button`,{className:`btn`,onClick:t,children:`✕ Close`})]}),(0,$.jsxs)(`div`,{className:`kv__body`,children:[(0,$.jsx)(`div`,{className:`kv__k`,children:`title`}),(0,$.jsx)(`div`,{className:`kv__v`,children:e.title}),(0,$.jsx)(`div`,{className:`kv__k`,children:`time`}),(0,$.jsx)(`div`,{className:`kv__v`,children:new Date(e.ts).toLocaleString()}),(0,$.jsx)(`div`,{className:`kv__k`,children:`device`}),(0,$.jsx)(`div`,{className:`kv__v`,children:e.device??`—`}),(0,$.jsx)(`div`,{className:`kv__k`,children:`transport`}),(0,$.jsx)(`div`,{className:`kv__v`,children:e.transport??`—`}),(0,$.jsx)(`div`,{className:`kv__k`,children:`duration`}),(0,$.jsx)(`div`,{className:`kv__v`,children:Zs(e.durationMs)}),(0,$.jsx)(`div`,{className:`kv__k`,children:`status`}),(0,$.jsx)(`div`,{className:`kv__v`,children:(0,$.jsx)(`span`,{className:e.isError?`status-err`:`status-ok`,children:e.isError?`error`:`ok`})}),(0,$.jsx)(`div`,{className:`kv__k`,children:`output size`}),(0,$.jsxs)(`div`,{className:`kv__v`,children:[Qs(e.outputBytes),e.truncated?` (truncated)`:``]}),(0,$.jsx)(`div`,{className:`kv__k`,children:`structured`}),(0,$.jsx)(`div`,{className:`kv__v`,children:e.hasStructured?`yes (renders an MCP App view)`:`no`})]}),e.error&&(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`h2`,{className:`muted`,children:`ERROR`}),(0,$.jsx)(`pre`,{className:`body`,style:{color:`var(--mt-bad)`},children:e.error})]}),(0,$.jsxs)(`div`,{className:`sheet__hd`,children:[(0,$.jsx)(`h2`,{className:`muted`,style:{margin:0},children:`INPUT`}),(0,$.jsx)(`span`,{style:{flex:1}}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>n(e.input),children:`Copy`})]}),e.input?(0,$.jsx)(Gc,{value:e.input}):(0,$.jsx)(`pre`,{className:`body`,children:`—`}),(0,$.jsxs)(`div`,{className:`sheet__hd`,children:[(0,$.jsx)(`h2`,{className:`muted`,style:{margin:0},children:`OUTPUT`}),(0,$.jsx)(`span`,{style:{flex:1}}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>n(e.output),children:`Copy`})]}),(0,$.jsx)(`pre`,{className:`body`,children:e.output||`—`})]})})}function Qc(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 $c=[{id:`overview`,label:`Overview`,sub:`Calls, latency & risk at a glance`},{id:`devices`,label:`Devices`,sub:`Connectivity radar & system health`},{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:`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:`config`,label:`Config`,sub:`Effective configuration & safe editor`},{id:`feed`,label:`Live Feed`,sub:`Every tool call, in real time`}];function el({name:e}){return(0,$.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.7`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,children:{overview:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`rect`,{x:`3`,y:`3`,width:`8`,height:`8`,rx:`1.6`}),(0,$.jsx)(`rect`,{x:`13`,y:`3`,width:`8`,height:`5`,rx:`1.6`}),(0,$.jsx)(`rect`,{x:`13`,y:`10`,width:`8`,height:`11`,rx:`1.6`}),(0,$.jsx)(`rect`,{x:`3`,y:`13`,width:`8`,height:`8`,rx:`1.6`})]}),devices:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`rect`,{x:`3`,y:`4`,width:`18`,height:`7`,rx:`2`}),(0,$.jsx)(`rect`,{x:`3`,y:`13`,width:`18`,height:`7`,rx:`2`}),(0,$.jsx)(`path`,{d:`M7 7.5h.01M7 16.5h.01`})]}),topology:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`circle`,{cx:`12`,cy:`5`,r:`2.4`}),(0,$.jsx)(`circle`,{cx:`5`,cy:`19`,r:`2.4`}),(0,$.jsx)(`circle`,{cx:`19`,cy:`19`,r:`2.4`}),(0,$.jsx)(`path`,{d:`M12 7.4 6.4 16.6M12 7.4 17.6 16.6`})]}),packets:(0,$.jsx)(`path`,{d:`M3 12h4l2-7 4 14 2-7h6`}),snapshots:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`path`,{d:`M12 3 3 7.5 12 12 21 7.5 12 3Z`}),(0,$.jsx)(`path`,{d:`M3 12 12 16.5 21 12`}),(0,$.jsx)(`path`,{d:`M3 16.5 12 21 21 16.5`})]}),plan:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`circle`,{cx:`6`,cy:`6`,r:`2.3`}),(0,$.jsx)(`circle`,{cx:`6`,cy:`18`,r:`2.3`}),(0,$.jsx)(`circle`,{cx:`18`,cy:`8`,r:`2.3`}),(0,$.jsx)(`path`,{d:`M6 8.3v7.4M8.3 6H13a3 3 0 0 1 3 3v0`})]}),s3:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`ellipse`,{cx:`12`,cy:`6`,rx:`7`,ry:`2.6`}),(0,$.jsx)(`path`,{d:`M5 6v12c0 1.5 3.1 2.6 7 2.6s7-1.1 7-2.6V6`}),(0,$.jsx)(`path`,{d:`M5 12c0 1.5 3.1 2.6 7 2.6s7-1.1 7-2.6`})]}),backups:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`path`,{d:`M3 6.5 5 3.5h14l2 3`}),(0,$.jsx)(`rect`,{x:`3`,y:`6.5`,width:`18`,height:`14`,rx:`2`}),(0,$.jsx)(`path`,{d:`M9.5 12h5`})]}),config:(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(`path`,{d:`M4 7h8M16 7h4M4 17h4M12 17h8`}),(0,$.jsx)(`circle`,{cx:`14`,cy:`7`,r:`2.2`}),(0,$.jsx)(`circle`,{cx:`10`,cy:`17`,r:`2.2`})]}),feed:(0,$.jsx)(`path`,{d:`M4 6h16M4 12h16M4 18h10`})}[e]})}function tl(){let[e,t]=(0,l.useState)(null),[n,r]=(0,l.useState)(null),[i,a]=(0,l.useState)(null),[o,s]=(0,l.useState)(null),[c,u]=(0,l.useState)(null),[d,f]=(0,l.useState)(!1),[p,m]=(0,l.useState)(null),[h,g]=(0,l.useState)([]),[_,v]=(0,l.useState)(36e5),[y,b]=(0,l.useState)(!1),[x,S]=(0,l.useState)(`off`),[C,w]=(0,l.useState)(null),[T,E]=(0,l.useState)(()=>new Set),[D,O]=(0,l.useState)({}),[k,A]=(0,l.useState)({tool:``,risk:``,device:``,status:``,q:``}),j=(0,l.useRef)(y);j.current=y;let M=(0,l.useRef)(null);Mc(M);let[N,P]=(0,l.useState)(`overview`),[F,I]=(0,l.useState)(``),[L,ee]=(0,l.useState)(`all`),te=(0,l.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]),ne=(0,l.useMemo)(()=>{let e=i?.devices??[],t=F.trim().toLowerCase();return e.filter(e=>!(L===`online`&&e.status.reachable!==!0||L===`offline`&&e.status.reachable!==!1||t&&!e.name.toLowerCase().includes(t)&&!(e.address??e.host??``).toLowerCase().includes(t)))},[i,F,L]);jc((0,l.useCallback)(e=>{if(j.current)return;g(t=>[e,...t].slice(0,lc));let t=e.device;t&&O(e=>({...e,[t]:(e[t]??0)+1}))},[]),(0,l.useCallback)(e=>S(e),[])),(0,l.useEffect)(()=>{ac(`/api/events?limit=200`).then(e=>g(e.events)).catch(()=>{})},[]);let R=(0,l.useCallback)(()=>{ac(`/api/stats?window=${_}&buckets=60`).then(t).catch(()=>{})},[_]);(0,l.useEffect)(()=>{R();let e=()=>{R(),ac(`/api/meta`).then(r).catch(()=>{}),ac(`/api/devices`).then(a).catch(()=>{}),ac(`/api/topology`).then(s).catch(()=>{})};e();let t=setInterval(()=>{j.current||e()},4e3);return()=>clearInterval(t)},[R]),(0,l.useEffect)(()=>{let e=()=>void ac(`/api/config`).then(u).catch(()=>{});e();let t=setInterval(e,3e4);return()=>clearInterval(t)},[]),(0,l.useEffect)(()=>{let e=e=>{e.key===`Escape`&&w(null)};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[]);let z=(0,l.useMemo)(()=>{let e=k.q.trim().toLowerCase();return h.filter(t=>!(k.tool&&t.tool!==k.tool||k.risk&&t.risk!==k.risk||k.device&&t.device!==k.device||k.status===`ok`&&t.isError||k.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)},[h,k]),B=!!(k.tool||k.risk||k.device||k.status||k.q),re=(0,l.useMemo)(()=>h.filter(e=>e.isError),[h]),V=(0,l.useMemo)(()=>({ok:h.length-re.length,error:re.length}),[h,re]),ie=(0,l.useCallback)(async e=>{try{w(await ac(`/api/event/${encodeURIComponent(e.id)}`))}catch{w(e)}},[]),ae=(0,l.useMemo)(()=>z.slice(0,200),[z]),oe=(0,l.useMemo)(()=>ae.map(e=>e.id),[ae]),se=oe.length>0&&oe.every(e=>T.has(e)),H=!se&&oe.some(e=>T.has(e)),ce=(0,l.useCallback)(e=>{E(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},[]),le=(0,l.useCallback)(()=>{E(e=>{let t=new Set(e);if(oe.length>0&&oe.every(e=>t.has(e)))for(let e of oe)t.delete(e);else for(let e of oe)t.add(e);return t})},[oe]),[ue,de]=(0,l.useState)(!1),fe=(0,l.useCallback)(async()=>{let e=[...T];if(e.length!==0)try{await oc({ids:e});let t=new Set(e);g(e=>e.filter(e=>!t.has(e.id))),E(new Set)}catch{}finally{de(!1)}},[T]),pe=e=>{if(e===`json`){Qc(`mcp-events.json`,JSON.stringify(z,null,2),`application/json`);return}let t=[`ts`,`tool`,`risk`,`device`,`durationMs`,`isError`,`error`],n=e=>/[",\n]/.test(e)?`"${e.replace(/"/g,`""`)}"`:e,r=z.map(e=>[new Date(e.ts).toISOString(),e.tool,e.risk,e.device??``,String(e.durationMs),String(e.isError),e.error??``].map(n).join(`,`)).join(`
28
+ `);Qc(`mcp-events.csv`,`${t.join(`,`)}\n${r}\n`,`text/csv`)},me=e?e.errorRate>=.2?`is-bad`:e.errorRate>=.05?`is-warn`:`is-good`:``,he=c?.mcp??{},ge=c?.dashboard??{},U=(e,t,n)=>(0,$.jsxs)(`select`,{className:`btn`,value:k[e],onChange:t=>A(n=>({...n,[e]:t.target.value})),children:[(0,$.jsx)(`option`,{value:``,children:t}),n.map(e=>(0,$.jsx)(`option`,{value:e,children:e},e))]}),_e=$c.find(e=>e.id===N)??$c[0];return(0,$.jsxs)(`div`,{className:`shell`,ref:M,children:[(0,$.jsxs)(`aside`,{className:`nav`,children:[(0,$.jsxs)(`div`,{className:`nav__brand`,children:[(0,$.jsx)(`div`,{className:`nav__mark`,children:(0,$.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,"aria-hidden":`true`,children:(0,$.jsxs)(`g`,{stroke:`#18181b`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,$.jsx)(`path`,{d:`M12 12 L4 5 M12 12 L20 5 M12 12 L12 20`}),(0,$.jsx)(`circle`,{cx:`12`,cy:`12`,r:`3`,fill:`#18181b`,stroke:`none`}),(0,$.jsx)(`circle`,{cx:`4`,cy:`5`,r:`1.9`,fill:`#18181b`,stroke:`none`}),(0,$.jsx)(`circle`,{cx:`20`,cy:`5`,r:`1.9`,fill:`#18181b`,stroke:`none`}),(0,$.jsx)(`circle`,{cx:`12`,cy:`20`,r:`1.9`,fill:`#18181b`,stroke:`none`})]})})}),(0,$.jsxs)(`div`,{className:`nav__brandtext`,children:[(0,$.jsx)(`b`,{children:`MikroTik MCP`}),(0,$.jsx)(`small`,{children:`Observability`})]})]}),(0,$.jsx)(`nav`,{className:`nav__items`,children:$c.map(e=>(0,$.jsxs)(`button`,{className:`nav__item${N===e.id?` is-active`:``}`,onClick:()=>P(e.id),children:[(0,$.jsx)(el,{name:e.id}),(0,$.jsx)(`span`,{children:e.label}),e.id===`feed`&&h.length>0&&(0,$.jsx)(`span`,{className:`nav__badge`,children:h.length>999?`999+`:h.length})]},e.id))}),(0,$.jsxs)(`div`,{className:`nav__foot`,children:[(0,$.jsxs)(`span`,{className:`hero__live${x===`off`?``:` is-on`}`,title:`Live transport: WebSocket (preferred) or SSE fallback`,children:[(0,$.jsx)(`span`,{className:`dot`}),x===`off`?`offline`:`live · ${x}`]}),(0,$.jsx)(`small`,{className:`muted`,children:n?`${ec(n.total)} events · ${n.transport}`:`connecting…`})]})]}),(0,$.jsxs)(`main`,{className:`main`,children:[(0,$.jsxs)(`header`,{className:`topline reveal`,children:[(0,$.jsxs)(`div`,{className:`topline__txt`,children:[(0,$.jsx)(`h1`,{children:_e.label}),(0,$.jsx)(`small`,{children:_e.sub})]}),(0,$.jsx)(`span`,{className:`topline__spacer`}),N===`overview`&&(0,$.jsx)(`select`,{className:`btn`,value:_,onChange:e=>v(Number(e.target.value)),title:`Stats time window`,children:cc.map(([e,t])=>(0,$.jsxs)(`option`,{value:t,children:[`window: `,e]},t))})]}),N===`overview`&&(0,$.jsxs)(`section`,{className:`view`,children:[(0,$.jsx)(`div`,{className:`cards reveal`,children:e?(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(dc,{k:`Calls (window)`,v:ec(e.total)}),(0,$.jsx)(dc,{k:`Calls / min`,v:e.callsPerMin.toFixed(1)}),(0,$.jsx)(dc,{k:`Error rate`,v:`${(e.errorRate*100).toFixed(1)}%`,sub:`${e.errors} err`,cls:me}),(0,$.jsx)(dc,{k:`Avg latency`,v:Zs(e.latency.avg)}),(0,$.jsx)(dc,{k:`p95 latency`,v:Zs(e.latency.p95)}),(0,$.jsx)(dc,{k:`p99 latency`,v:Zs(e.latency.p99)}),(0,$.jsx)(dc,{k:`Distinct tools`,v:ec(e.distinctTools)}),(0,$.jsx)(dc,{k:`Output volume`,v:Qs(e.outputBytes)})]}):(0,$.jsxs)(`div`,{className:`stat`,children:[(0,$.jsx)(`p`,{className:`k`,children:`Loading…`}),(0,$.jsx)(`div`,{className:`v`,children:`—`})]})}),(0,$.jsxs)(`div`,{className:`bento reveal`,children:[(0,$.jsxs)(uc,{title:`Calls over time`,className:`b-series`,children:[e?(0,$.jsx)(fc,{series:e.series}):(0,$.jsx)(`div`,{className:`muted`,children:`no data`}),(0,$.jsxs)(`div`,{className:`legend`,children:[(0,$.jsxs)(`span`,{children:[(0,$.jsx)(`i`,{style:{background:`#d4d4d8`}}),`ok`]}),(0,$.jsxs)(`span`,{children:[(0,$.jsx)(`i`,{style:{background:`#f87171`}}),`error`]})]})]}),e&&(0,$.jsxs)($.Fragment,{children:[(0,$.jsx)(uc,{title:`By risk`,className:`b-risk`,children:(0,$.jsx)(pc,{segments:Object.keys(e.byRisk).map(t=>({label:t,value:e.byRisk[t],color:nc[t]}))})}),(0,$.jsx)(uc,{title:`Top tools`,className:`b-tools`,children:(0,$.jsx)(mc,{rows:e.byTool.map(e=>({label:e.tool,value:e.count,sub:`${e.count}× · ${Zs(e.p95Ms)} p95${e.errors?` · ${e.errors} err`:``}`,color:e.errors?`#f87171`:void 0}))})}),(0,$.jsx)(uc,{title:`Status`,className:`b-status`,children:(0,$.jsx)(pc,{segments:[{label:`ok`,value:V.ok,color:`#d4d4d8`},{label:`error`,value:V.error,color:`#f87171`}]})}),(0,$.jsx)(uc,{title:`By device`,className:`b-device`,children:e.byDevice.length?(0,$.jsx)(mc,{rows:e.byDevice.map(e=>({label:e.device,value:e.count}))}):(0,$.jsx)(`div`,{className:`muted`,children:`single device`})}),(0,$.jsx)(uc,{title:`Recent errors`,className:`b-errors`,children:re.length?(0,$.jsx)(`div`,{className:`hbar`,children:re.slice(0,8).map(e=>(0,$.jsxs)(`div`,{className:`hbar__row conn-errrow`,style:{gridTemplateColumns:`auto 1fr`},onClick:()=>void ie(e),children:[(0,$.jsx)(`span`,{className:`muted`,children:$s(e.ts)}),(0,$.jsxs)(`span`,{style:{color:`var(--mt-bad)`,whiteSpace:`nowrap`,overflow:`hidden`,textOverflow:`ellipsis`,minWidth:0},title:e.error??e.output,children:[e.tool,`: `,e.error??e.output??`error`]})]},e.id))}):(0,$.jsx)(`div`,{className:`muted`,children:`no errors 🎉`})})]})]})]}),N===`devices`&&(i&&i.devices.length>0?(0,$.jsxs)(`section`,{className:`view`,children:[(0,$.jsxs)(`div`,{className:`dev-toolbar reveal`,children:[(0,$.jsx)(`input`,{className:`search`,type:`search`,placeholder:`Search devices by name or address…`,value:F,onChange:e=>I(e.target.value),style:{flex:1,minWidth:180}}),(0,$.jsx)(`div`,{className:`dev-filters`,children:[`all`,`online`,`offline`].map(e=>(0,$.jsx)(`button`,{className:`dev-fbtn${L===e?` is-active`:``}`,onClick:()=>ee(e),children:e===`all`?`All ${te.total}`:e===`online`?`Online ${te.online}`:`Offline ${te.offline}`},e))}),(0,$.jsxs)(`span`,{className:`muted`,children:[ne.length,`/`,te.total,` shown`]})]}),(0,$.jsxs)(`details`,{className:`dev-collapse reveal`,open:te.total<=8,children:[(0,$.jsxs)(`summary`,{children:[`Connectivity radar`,(0,$.jsxs)(`span`,{className:`muted`,children:[` `,`· `,te.online,` online · `,te.offline,` offline ·`,` `,te.total,` total`]})]}),(0,$.jsx)(Tc,{payload:i,pulses:D})]}),ne.length===0?(0,$.jsxs)(`div`,{className:`feed-empty reveal`,children:[(0,$.jsx)(`div`,{className:`feed-empty__icon`,children:`🔍`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:`No devices match`}),(0,$.jsx)(`p`,{className:`feed-empty__sub`,children:`Try a different search or status filter.`})]}):(0,$.jsx)(`div`,{className:`dev-grid-wide reveal`,children:ne.map(e=>(0,$.jsx)(Ec,{d:e},e.name))}),ne.length>0&&(0,$.jsx)(uc,{title:`Device system health`,className:`reveal`,extra:(0,$.jsx)(`span`,{className:`muted`,children:`CPU · memory · disk · latency · live probe`}),children:(0,$.jsx)(`div`,{className:`health-grid`,children:ne.map(e=>(0,$.jsx)(Ac,{d:e},e.name))})})]}):(0,$.jsxs)(`div`,{className:`feed-empty`,children:[(0,$.jsx)(`div`,{className:`feed-empty__icon`,children:`🖧`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:`No devices configured`}),(0,$.jsx)(`p`,{className:`feed-empty__sub`,children:`Add a device to your config to see connectivity and system health here.`})]})),N===`topology`&&(o&&o.nodes.length>0?(0,$.jsx)(`section`,{className:`view`,children:(0,$.jsx)(uc,{title:`Network topology`,className:`reveal`,extra:(0,$.jsx)(`span`,{className:`muted`,children:`Layer-2 neighbours via MNDP/CDP/LLDP · click a neighbour to onboard it`}),children:(0,$.jsx)(Rc,{topo:o,onOnboard:(e,t)=>{m({name:e,body:t}),f(!0),P(`config`)}})})}):(0,$.jsxs)(`div`,{className:`feed-empty`,children:[(0,$.jsx)(`div`,{className:`feed-empty__icon`,children:`🛰️`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:`No neighbours discovered yet`}),(0,$.jsx)(`p`,{className:`feed-empty__sub`,children:`Layer-2 neighbours (MNDP / CDP / LLDP) appear here as the device reports them.`})]})),N===`packets`&&(0,$.jsx)(`section`,{className:`view`,children:(0,$.jsx)(uc,{title:`Packet capture`,className:`reveal`,extra:(0,$.jsx)(`span`,{className:`muted`,children:`live TZSP decode · /tool sniffer streaming`}),children:(0,$.jsx)(Wc,{})})}),N===`snapshots`&&(0,$.jsx)(Kc,{}),N===`plan`&&(0,$.jsx)(Jc,{}),N===`s3`&&(0,$.jsx)(Yc,{}),N===`backups`&&(0,$.jsx)(Xc,{}),N===`config`&&(c?(0,$.jsx)(`section`,{className:`view`,children:(0,$.jsx)(uc,{title:`Configuration`,className:`reveal`,extra:(0,$.jsx)(`button`,{className:`btn`,onClick:()=>f(e=>!e),title:`Edit the config JSON with autocomplete, validation and safe-apply`,children:d?`View`:`✎ Edit config`}),children:d?(0,$.jsx)(Ic,{initial:p?{...c,devices:{...c.devices,[p.name]:p.body}}:c,onClose:()=>{f(!1),m(null)},onReload:()=>{m(null),ac(`/api/config`).then(u).catch(()=>{})}},p?`seed-${p.name}`:`config`):(0,$.jsxs)($.Fragment,{children:[(0,$.jsxs)(`div`,{className:`legend`,style:{margin:`0 0 10px`},children:[(0,$.jsxs)(`span`,{children:[`transport: `,tc(he.transport)]}),(0,$.jsxs)(`span`,{children:[`read-only: `,c.readOnly?`yes`:`no`]}),(0,$.jsxs)(`span`,{children:[`dashboard: `,tc(ge.host),`:`,tc(ge.port)]}),(0,$.jsxs)(`span`,{children:[`capture: `,ge.captureBody?`on`:`off`]}),(0,$.jsxs)(`span`,{children:[`s3: `,c.s3?`configured`:`off`]})]}),(0,$.jsxs)(`details`,{className:`cfg`,children:[(0,$.jsx)(`summary`,{children:`Full effective configuration (secrets redacted)`}),(0,$.jsx)(Gc,{value:c,maxHeight:340})]})]})})}):(0,$.jsxs)(`div`,{className:`feed-empty`,children:[(0,$.jsx)(`div`,{className:`feed-empty__icon`,children:`⚙️`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:`Loading configuration…`})]})),N===`feed`&&(0,$.jsxs)(`div`,{className:`panel reveal`,children:[(0,$.jsxs)(`div`,{className:`sheet__hd`,style:{marginBottom:12},children:[(0,$.jsx)(`h2`,{style:{margin:0},children:`Live tool calls`}),(0,$.jsx)(`span`,{style:{flex:1}}),(0,$.jsxs)(`span`,{className:`muted`,children:[z.length,` shown · `,h.length,` buffered`]})]}),(0,$.jsxs)(`div`,{className:`toolbar`,style:{marginBottom:12},children:[(0,$.jsx)(`div`,{className:`grow`,style:{flex:1,minWidth:180},children:(0,$.jsx)(`input`,{className:`search`,type:`search`,placeholder:`Search tool / input / output / error…`,value:k.q,onChange:e=>A(t=>({...t,q:e.target.value}))})}),U(`tool`,`all tools`,n?.tools??[]),U(`risk`,`all risk`,[`READ`,`WRITE`,`WRITE_IDEMPOTENT`,`DESTRUCTIVE`,`DANGEROUS`]),U(`device`,`all devices`,n?.devices??[]),U(`status`,`all status`,[`ok`,`error`]),(0,$.jsx)(`button`,{className:`btn${y?` is-active`:``}`,onClick:()=>b(e=>!e),children:y?`▶ Resume`:`⏸ Pause`}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>pe(`csv`),children:`CSV`}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>pe(`json`),children:`JSON`}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>A({tool:``,risk:``,device:``,status:``,q:``}),children:`Clear`}),ue&&T.size>0?(0,$.jsxs)($.Fragment,{children:[(0,$.jsxs)(`button`,{className:`btn btn-danger`,onClick:()=>void fe(),children:[`✓ Confirm delete (`,T.size,`)`]}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>de(!1),children:`Cancel`})]}):(0,$.jsxs)(`button`,{className:`btn`,disabled:T.size===0,onClick:()=>de(!0),title:`Delete the selected rows`,children:[`🗑 Delete`,T.size>0?` (${T.size})`:``]})]}),z.length===0?B?(0,$.jsxs)(`div`,{className:`feed-empty`,children:[(0,$.jsx)(`div`,{className:`feed-empty__icon`,children:`🔍`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:`No calls match your filters`}),(0,$.jsxs)(`p`,{className:`feed-empty__sub`,children:[h.length,` call`,h.length===1?``:`s`,` buffered — try widening the search or the risk / device / status filters.`]}),(0,$.jsx)(`button`,{className:`btn`,onClick:()=>A({tool:``,risk:``,device:``,status:``,q:``}),children:`Clear filters`})]}):(0,$.jsxs)(`div`,{className:`feed-empty`,children:[(0,$.jsx)(`div`,{className:`feed-empty__pulse${x===`off`?``:` is-on`}`}),(0,$.jsx)(`p`,{className:`feed-empty__title`,children:x===`off`?`Not connected`:`Listening for tool calls…`}),(0,$.jsx)(`p`,{className:`feed-empty__sub`,children:x===`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,$.jsx)(`div`,{className:`feedwrap`,children:(0,$.jsxs)(`table`,{className:`feed`,children:[(0,$.jsx)(`thead`,{children:(0,$.jsxs)(`tr`,{children:[(0,$.jsx)(`th`,{style:{width:28},children:(0,$.jsx)(`input`,{type:`checkbox`,"aria-label":`Select all shown rows`,checked:se,ref:e=>{e&&(e.indeterminate=H)},onChange:le})}),(0,$.jsx)(`th`,{children:`time`}),(0,$.jsx)(`th`,{children:`tool`}),(0,$.jsx)(`th`,{children:`risk`}),(0,$.jsx)(`th`,{children:`device`}),(0,$.jsx)(`th`,{className:`num`,children:`dur`}),(0,$.jsx)(`th`,{children:`status`}),(0,$.jsx)(`th`,{children:`output`})]})}),(0,$.jsx)(`tbody`,{children:ae.map(e=>(0,$.jsxs)(`tr`,{className:`${e.isError?`is-err`:``}${T.has(e.id)?` is-selected`:``}`.trim()||void 0,onClick:()=>void ie(e),children:[(0,$.jsx)(`td`,{onClick:e=>e.stopPropagation(),children:(0,$.jsx)(`input`,{type:`checkbox`,"aria-label":`Select row`,checked:T.has(e.id),onChange:()=>ce(e.id)})}),(0,$.jsx)(`td`,{children:$s(e.ts)}),(0,$.jsx)(`td`,{children:e.tool}),(0,$.jsx)(`td`,{children:(0,$.jsx)(`span`,{className:`risk risk-${e.risk}`,children:e.risk.replace(`WRITE_IDEMPOTENT`,`WRITE·I`)})}),(0,$.jsx)(`td`,{children:e.device??`—`}),(0,$.jsx)(`td`,{className:`num`,children:Zs(e.durationMs)}),(0,$.jsx)(`td`,{children:(0,$.jsx)(`span`,{className:e.isError?`status-err`:`status-ok`,children:e.isError?`error`:`ok`})}),(0,$.jsx)(`td`,{className:`preview`,children:e.isError?e.error??`error`:e.output||`—`})]},e.id))})]})})]})]}),C&&(0,$.jsx)(Zc,{event:C,onClose:()=>w(null)})]})}var nl=document.getElementById(`root`);nl&&(0,u.createRoot)(nl).render((0,$.jsx)(tl,{}));
29
29
  </script>
30
30
  <style>
31
- @import "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap";:root{--mt-bg:var(--color-background-primary,#0b0d10);--mt-surface:var(--color-background-secondary,#14171c);--mt-surface-2:var(--color-background-tertiary,#1b1f26);--mt-border:var(--color-border-primary,#262b33);--mt-text:var(--color-text-primary,#e8eaed);--mt-text-dim:var(--color-text-secondary,#9aa3af);--mt-text-faint:var(--color-text-tertiary,#6b7280);--mt-accent:var(--color-accent-primary,#6ea8fe);--mt-good:#34d399;--mt-warn:#fbbf24;--mt-bad:#f87171;--mt-radius:var(--border-radius-md,14px);--mt-radius-sm:var(--border-radius-sm,9px);--mt-mono:var(--font-mono,ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace);--mt-sans:var(--font-sans,system-ui, -apple-system, "Segoe UI", sans-serif);--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}*{box-sizing:border-box}body{background:var(--mt-bg);color:var(--mt-text);font-family:var(--mt-sans);-webkit-font-smoothing:antialiased;margin:0;padding:16px;font-size:13px;line-height:1.5}.app{gap:14px;max-width:960px;margin:0 auto;display:grid}.hd{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.hd__dot{background:var(--mt-good);width:9px;height:9px;box-shadow:0 0 0 4px color-mix(in srgb, var(--mt-good) 22%, transparent);border-radius:50%}.hd__title{letter-spacing:-.01em;margin:0;font-size:17px;font-weight:650}.hd__sub{color:var(--mt-text-dim);font-family:var(--mt-mono);margin:0;font-size:12px}.hd__spacer{flex:1}.pill{border:1px solid var(--mt-border);background:var(--mt-surface);color:var(--mt-text-dim);font-family:var(--mt-mono);border-radius:999px;align-items:center;gap:6px;padding:3px 9px;font-size:11px;display:inline-flex}.pill b{color:var(--mt-text);font-weight:600}.grid{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;display:grid}.card{background:linear-gradient(180deg, var(--mt-surface), var(--mt-surface-2));border:1px solid var(--mt-border);border-radius:var(--mt-radius);padding:14px}.card__label{color:var(--mt-text-dim);text-transform:uppercase;letter-spacing:.06em;margin:0 0 8px;font-size:11px}.card__value{font-family:var(--mt-mono);letter-spacing:-.01em;font-size:20px;font-weight:600}.toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.toolbar .grow{flex:1;min-width:140px}.search{appearance:none;border:1px solid var(--mt-border);background:var(--mt-surface);width:100%;color:var(--mt-text);font:inherit;border-radius:var(--mt-radius-sm);padding:7px 11px;font-size:13px}.search:focus{border-color:var(--mt-accent);outline:none}.search::placeholder{color:var(--mt-text-faint)}.btn{appearance:none;border:1px solid var(--mt-border);background:var(--mt-surface);color:var(--mt-text);font:inherit;border-radius:var(--mt-radius-sm);cursor:pointer;white-space:nowrap;padding:6px 11px;font-size:12px;transition:border-color .15s,background .15s}.btn:disabled{opacity:.5;cursor:default}.chip{border:1px solid var(--mt-border);background:var(--mt-surface);color:var(--mt-text-dim);font-family:var(--mt-mono);border-radius:999px;align-items:center;gap:4px;padding:1px 7px;font-size:11px;display:inline-flex}.chip.is-good{color:var(--mt-good);border-color:color-mix(in srgb, var(--mt-good) 45%, var(--mt-border))}.chip.is-warn{color:var(--mt-warn);border-color:color-mix(in srgb, var(--mt-warn) 45%, var(--mt-border))}.chip.is-bad{color:var(--mt-bad);border-color:color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border))}.tablewrap{border:1px solid var(--mt-border);border-radius:var(--mt-radius);max-height:70vh;overflow:auto}table.tbl{border-collapse:collapse;width:100%;font-size:12px}table.tbl th,table.tbl td{text-align:left;border-bottom:1px solid color-mix(in srgb, var(--mt-border) 55%, transparent);white-space:nowrap;font-family:var(--mt-mono);padding:7px 12px}table.tbl thead th{z-index:1;background:var(--mt-surface-2);color:var(--mt-text-dim);cursor:pointer;-webkit-user-select:none;user-select:none;font-weight:600;position:sticky;top:0}table.tbl thead th:hover{color:var(--mt-text)}table.tbl thead th .arrow{color:var(--mt-accent);margin-left:4px}table.tbl tbody tr{cursor:pointer}table.tbl tbody tr:hover{background:color-mix(in srgb, var(--mt-accent) 8%, transparent)}table.tbl tbody tr.is-disabled td,table.tbl td.col-num{color:var(--mt-text-faint)}.kv__body{border:1px solid var(--mt-border);border-radius:var(--mt-radius);grid-template-columns:minmax(120px,.4fr) 1fr;display:grid;overflow:hidden}.kv__body>div{border-bottom:1px solid color-mix(in srgb, var(--mt-border) 55%, transparent);overflow-wrap:anywhere;min-width:0;font-size:12px;font-family:var(--mt-mono);padding:7px 14px}.kv__k{color:var(--mt-text-dim);background:color-mix(in srgb, var(--mt-surface) 60%, transparent)}.kv__v{color:var(--mt-text)}.drawer{border:1px solid var(--mt-accent);border-radius:var(--mt-radius);background:var(--mt-surface);gap:10px;padding:12px;display:grid}.drawer__hd{align-items:center;gap:8px;display:flex}.drawer__hd b{font-family:var(--mt-mono)}pre.raw{border:1px solid var(--mt-border);border-radius:var(--mt-radius);background:var(--mt-surface);color:var(--mt-text);font-family:var(--mt-mono);white-space:pre;max-height:70vh;margin:0;padding:14px;font-size:12px;overflow:auto}.foot{color:var(--mt-text-faint);font-size:11px;font-family:var(--mt-mono);flex-wrap:wrap;align-items:center;gap:10px;display:flex}.foot .grow{flex:1}.skeleton,.empty{color:var(--mt-text-faint);text-align:center;padding:40px 0}:root{--mt-sans:"Space Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;--mt-mono:"DM Mono", ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;--mt-bg:#09090b;--mt-surface:#18181b;--mt-surface-2:#27272a;--mt-border:#27272a;--mt-text:#fafafa;--mt-text-dim:#a1a1aa;--mt-text-faint:#71717a;--mt-accent:#e4e4e7;--mt-good:#d4d4d8;--mt-warn:#a1a1aa;--mt-bad:#f87171;--mt-radius:12px;--mt-radius-sm:8px;--sky:#d4d4d8;--teal:#e4e4e7;--cyan:#a1a1aa;--lime:#71717a;--coral:#f87171;--amber:#a1a1aa;--grad:linear-gradient(115deg, #e4e4e7, #a1a1aa 50%, #52525b);--ease:cubic-bezier(.22, 1, .36, 1);--glass:#18181b8c}html,body{background:var(--mt-bg);color:var(--mt-text);font-family:var(--mt-sans);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;padding:0}body:before{content:"";z-index:-2;background:radial-gradient(58% 42% at 12% -6%, color-mix(in srgb, var(--teal) 16%, transparent), transparent 70%), radial-gradient(50% 40% at 100% 0%, color-mix(in srgb, var(--sky) 13%, transparent), transparent 68%), radial-gradient(60% 50% at 84% 108%, color-mix(in srgb, var(--lime) 8%, transparent), transparent 70%);pointer-events:none;position:fixed;inset:0}body:after{content:"";z-index:-2;background-image:linear-gradient(color-mix(in srgb, var(--mt-border) 70%, transparent) 1px, transparent 1px), linear-gradient(90deg, color-mix(in srgb, var(--mt-border) 70%, transparent) 1px, transparent 1px);opacity:.5;pointer-events:none;background-size:46px 46px;position:fixed;inset:0;-webkit-mask-image:radial-gradient(circle at 50% 0,#000,#0000 75%);mask-image:radial-gradient(circle at 50% 0,#000,#0000 75%)}.shell{grid-template-columns:250px minmax(0,1fr);align-items:start;min-height:100vh;display:grid}.nav{border-right:1px solid var(--mt-border);background:color-mix(in srgb, var(--mt-surface) 55%, transparent);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);flex-direction:column;gap:10px;height:100vh;padding:18px 14px;display:flex;position:sticky;top:0}.nav__brand{align-items:center;gap:11px;padding:6px 8px 14px;display:flex}.nav__mark{background:var(--grad);width:38px;height:38px;box-shadow:0 8px 22px color-mix(in srgb, var(--teal) 38%, transparent);border-radius:11px;flex:none;place-items:center;display:grid}.nav__mark svg{width:22px;height:22px;display:block}.nav__brandtext{flex-direction:column;min-width:0;line-height:1.1;display:flex}.nav__brandtext b{letter-spacing:-.02em;font-size:15px;font-weight:700}.nav__brandtext small{font-family:var(--mt-mono);letter-spacing:.16em;text-transform:uppercase;color:var(--mt-text-dim);margin-top:3px;font-size:10px}.nav__items{flex-direction:column;flex:1;gap:3px;display:flex}.nav__item{color:var(--mt-text-dim);font-family:var(--mt-sans);cursor:pointer;text-align:left;transition:background .18s var(--ease), color .18s, border-color .18s, transform .18s var(--ease);background:0 0;border:1px solid #0000;border-radius:11px;align-items:center;gap:11px;padding:10px 12px;font-size:13px;font-weight:500;display:flex}.nav__item svg{opacity:.85;flex:none;width:18px;height:18px}.nav__item:hover{color:var(--mt-text);background:color-mix(in srgb, var(--teal) 9%, transparent)}.nav__item.is-active{color:var(--mt-text);border-color:color-mix(in srgb, var(--teal) 32%, var(--mt-border));background:linear-gradient(100deg, color-mix(in srgb, var(--teal) 18%, transparent), color-mix(in srgb, var(--sky) 7%, transparent))}.nav__item.is-active svg{opacity:1;color:var(--teal)}.nav__badge{font-family:var(--mt-mono);background:color-mix(in srgb, var(--teal) 22%, transparent);color:var(--mt-text);border-radius:999px;margin-left:auto;padding:1px 7px;font-size:10px}.nav__foot{border-top:1px solid var(--mt-border);flex-direction:column;gap:8px;padding:10px 8px 2px;display:flex}.main{align-content:start;gap:18px;min-width:0;padding:22px clamp(16px,3vw,44px) 64px;display:grid}.topline{flex-wrap:wrap;align-items:center;gap:14px;display:flex}.topline__txt h1{letter-spacing:-.025em;background:linear-gradient(100deg, var(--mt-text), color-mix(in srgb, var(--teal) 65%, var(--mt-text)));-webkit-text-fill-color:transparent;-webkit-background-clip:text;background-clip:text;margin:0;font-size:clamp(19px,2.4vw,26px);font-weight:700}.topline__txt small{color:var(--mt-text-dim);font-family:var(--mt-mono);margin-top:3px;font-size:12px;display:block}.topline__spacer{flex:1;min-width:8px}.view{align-content:start;gap:18px;display:grid}.bento{grid-template-columns:repeat(6,1fr);gap:18px;display:grid}.bento .panel{margin:0}.b-series{grid-column:span 4}.b-risk{grid-column:span 2}.b-tools,.b-status,.b-device,.b-errors{grid-column:span 3}.b-risk,.b-status{flex-direction:column;display:flex}@media (width<=1040px){.bento{grid-template-columns:repeat(2,1fr)}.b-series,.b-risk,.b-tools,.b-status,.b-device,.b-errors{grid-column:span 2}}@media (width<=900px){.shell{grid-template-columns:1fr}.nav{border-right:0;border-bottom:1px solid var(--mt-border);z-index:5;flex-direction:row;align-items:center;gap:8px;height:auto;padding:10px 12px;position:sticky;top:0;overflow-x:auto}.nav__brand{padding:0 6px 0 2px}.nav__brandtext{display:none}.nav__items{flex-direction:row;flex:1;gap:4px}.nav__item span{display:none}.nav__item{padding:9px 11px}.nav__foot{border-top:0;flex-direction:row;align-items:center;padding:0}.nav__foot .muted{display:none}}@media (width<=560px){.nav__brand{display:none}}@media (prefers-reduced-motion:no-preference){.js-motion .reveal{opacity:0}}.hero__live{border:1px solid var(--mt-border);background:var(--glass);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);font-family:var(--mt-mono);letter-spacing:.04em;text-transform:uppercase;color:var(--mt-text-dim);border-radius:999px;align-items:center;gap:7px;padding:7px 13px;font-size:11px;display:inline-flex}.hero__live .dot{background:var(--mt-bad);border-radius:50%;width:8px;height:8px;transition:background .3s}.hero__live.is-on{color:var(--mt-text);border-color:color-mix(in srgb, var(--mt-good) 40%, var(--mt-border))}.hero__live.is-on .dot{background:var(--mt-good);box-shadow:0 0 0 4px color-mix(in srgb, var(--mt-good) 22%, transparent);animation:1.8s ease-out infinite hero-pulse}@keyframes hero-pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb, var(--mt-good) 55%, transparent)}70%{box-shadow:0 0 0 9px color-mix(in srgb, var(--mt-good) 0%, transparent)}to{box-shadow:0 0 0 0 color-mix(in srgb, var(--mt-good) 0%, transparent)}}.panel>h2,.sheet__hd h2{font-family:var(--mt-sans)}.cards{grid-template-columns:repeat(2,1fr);gap:12px;display:grid}@media (width>=560px){.cards{grid-template-columns:repeat(4,1fr)}}@media (width>=1040px){.cards{grid-template-columns:repeat(8,1fr)}}.stat{background:linear-gradient(180deg, var(--mt-surface), color-mix(in srgb, var(--mt-surface-2) 80%, transparent));border:1px solid var(--mt-border);border-radius:var(--mt-radius);transition:transform .3s var(--ease), border-color .3s var(--ease);padding:14px 15px;position:relative;overflow:hidden}.stat:before{content:"";background:var(--grad);opacity:0;height:2px;transition:opacity .3s var(--ease);position:absolute;inset:0 0 auto}.stat:hover{border-color:color-mix(in srgb, var(--teal) 38%, var(--mt-border));transform:translateY(-3px)}.stat:hover:before{opacity:1}.stat .k{color:var(--mt-text-dim);text-transform:uppercase;letter-spacing:.08em;margin:0 0 7px;font-size:10px}.stat .v{font-family:var(--mt-mono);letter-spacing:-.02em;font-variant-numeric:tabular-nums;font-size:23px;font-weight:500}.stat .v small{color:var(--mt-text-dim);font-size:12px;font-weight:400}.stat.is-bad .v{color:var(--mt-bad)}.stat.is-warn .v{color:var(--mt-warn)}.stat.is-good .v{color:var(--mt-good)}.panel{background:var(--glass);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border:1px solid var(--mt-border);border-radius:var(--mt-radius);padding:18px;position:relative;box-shadow:inset 0 1px #ffffff0a}.panel>h2{color:var(--mt-text-dim);text-transform:uppercase;letter-spacing:.07em;align-items:center;gap:9px;margin:0 0 14px;font-size:12px;font-weight:600;display:inline-flex}.panel>h2:before,.sheet__hd h2:first-child:before{content:"";background:var(--grad);border-radius:2px;flex:none;width:3px;height:13px}.cols{grid-template-columns:1.4fr 1fr;gap:18px;display:grid}@media (width<=820px){.cols{grid-template-columns:1fr}}.cols-3{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:18px;display:grid}.cols-3 .panel{flex-direction:column;display:flex}.hbar{gap:9px;display:grid}.hbar__row{grid-template-columns:minmax(90px,.9fr) 2fr auto;align-items:center;gap:10px;font-size:12px;display:grid}.hbar__label{font-family:var(--mt-mono);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.hbar__track{background:color-mix(in srgb, var(--mt-surface-2) 90%, transparent);border-radius:999px;height:8px;overflow:hidden}.hbar__fill{background:var(--grad);height:100%;box-shadow:0 0 10px color-mix(in srgb, var(--teal) 45%, transparent);border-radius:999px}.hbar__val{font-family:var(--mt-mono);color:var(--mt-text-dim);text-align:right;font-variant-numeric:tabular-nums;font-size:11px}.legend{font-family:var(--mt-mono);color:var(--mt-text-dim);flex-wrap:wrap;gap:12px;margin-top:12px;font-size:11px;display:flex}.legend span{align-items:center;gap:5px;display:inline-flex}.legend i{border-radius:2px;width:9px;height:9px;display:inline-block}.risk{font-family:var(--mt-mono);border:1px solid var(--mt-border);text-transform:uppercase;letter-spacing:.04em;border-radius:999px;padding:1px 8px;font-size:10px;display:inline-block}.risk-READ{color:var(--mt-good);border-color:color-mix(in srgb, var(--mt-good) 45%, var(--mt-border));background:color-mix(in srgb, var(--mt-good) 10%, transparent)}.risk-WRITE,.risk-WRITE_IDEMPOTENT{color:var(--mt-warn);border-color:color-mix(in srgb, var(--mt-warn) 45%, var(--mt-border));background:color-mix(in srgb, var(--mt-warn) 10%, transparent)}.risk-DESTRUCTIVE,.risk-DANGEROUS{color:var(--mt-bad);border-color:color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border));background:color-mix(in srgb, var(--mt-bad) 10%, transparent)}.feed{border-collapse:collapse;width:100%;font-size:12px}.feed th,.feed td{text-align:left;border-bottom:1px solid color-mix(in srgb, var(--mt-border) 55%, transparent);white-space:nowrap;font-family:var(--mt-mono);padding:7px 11px}.feed thead th{background:color-mix(in srgb, var(--mt-surface-2) 96%, transparent);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);color:var(--mt-text-dim);text-transform:uppercase;letter-spacing:.05em;z-index:1;font-size:10px;font-weight:600;position:sticky;top:0}.feed tbody tr{cursor:pointer;transition:background .12s}.feed tbody tr:hover{background:color-mix(in srgb, var(--mt-accent) 9%, transparent)}.feed tbody tr.is-err{background:color-mix(in srgb, var(--mt-bad) 9%, transparent)}.feed tbody tr.is-selected{background:color-mix(in srgb, var(--mt-accent) 16%, transparent)}.feed td.preview{white-space:nowrap;text-overflow:ellipsis;max-width:320px;color:var(--mt-text-dim);overflow:hidden}.feed td.num{text-align:right;color:var(--mt-text-dim);font-variant-numeric:tabular-nums}.status-ok{color:var(--mt-good)}.status-err{color:var(--mt-bad)}.feedwrap{border:1px solid var(--mt-border);border-radius:var(--mt-radius);max-height:60vh;overflow:auto}.feed-empty{border:1px dashed var(--mt-border);border-radius:var(--mt-radius);text-align:center;justify-items:center;gap:8px;padding:54px 16px;display:grid}.feed-empty__icon{opacity:.8;font-size:28px}.feed-empty__title{color:var(--mt-text);margin:0;font-weight:600}.feed-empty__sub{max-width:460px;color:var(--mt-text-faint);margin:0;font-size:12px}.feed-empty .btn{margin-top:6px}.feed-empty__pulse{background:var(--mt-text-faint);border-radius:50%;width:12px;height:12px}.feed-empty__pulse.is-on{background:var(--mt-good);animation:1.6s ease-out infinite feed-pulse}@keyframes feed-pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb, var(--mt-good) 55%, transparent)}70%{box-shadow:0 0 0 12px color-mix(in srgb, var(--mt-good) 0%, transparent)}to{box-shadow:0 0 0 0 color-mix(in srgb, var(--mt-good) 0%, transparent)}}.conn{display:block;overflow:visible}.conn-grid{fill:none;stroke:var(--mt-border);stroke-opacity:.7;stroke-dasharray:2 7}.conn-sonar{fill:none;stroke:var(--mt-accent);stroke-width:1.5px;opacity:0;animation:3.3s ease-out infinite conn-sonar}@keyframes conn-sonar{0%{r:30px;opacity:.5}to{r:150px;opacity:0}}.conn-link{fill:none;stroke-width:1.6px;opacity:.38}.conn-flow{fill:none;stroke-width:2.4px;stroke-linecap:round;stroke-dasharray:2 13;opacity:.9;filter:drop-shadow(0 0 4px);animation:1.05s linear infinite conn-flow}@keyframes conn-flow{to{stroke-dashoffset:-60px}}.conn-packet{filter:drop-shadow(0 0 6px var(--teal))}.conn-hub-glow{fill:var(--mt-accent);opacity:.22;filter:blur(11px)}.conn-hub-ring{fill:none;stroke:var(--mt-accent);stroke-width:1.4px;stroke-dasharray:4 9;opacity:.7;transform-box:fill-box;transform-origin:50%;animation:15s linear infinite conn-rot}@keyframes conn-rot{to{transform:rotate(360deg)}}.conn-node-halo{fill:none;stroke-width:2px;transform-box:fill-box;transform-origin:50%;animation:2.4s ease-in-out infinite conn-halo}@keyframes conn-halo{0%,to{opacity:.45;transform:scale(1)}50%{opacity:0;transform:scale(1.55)}}.conn-blink{animation:1.6s ease-in-out infinite conn-blink}@keyframes conn-blink{0%,to{opacity:1}50%{opacity:.35}}.conn-node text{pointer-events:none}@media (prefers-reduced-motion:reduce){.conn-sonar,.conn-flow,.conn-hub-ring,.conn-node-halo,.conn-blink,.conn-packet animateMotion{animation:none}.conn-sonar{display:none}}.dev-grid{grid-template-columns:1fr;align-content:start;gap:12px;display:grid}@media (width>=600px) and (width<=819px){.dev-grid{grid-template-columns:1fr 1fr}}.dev-toolbar{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.dev-filters{border:1px solid var(--mt-border);background:color-mix(in srgb, var(--mt-surface-2) 70%, transparent);border-radius:999px;gap:4px;padding:3px;display:inline-flex}.dev-fbtn{appearance:none;color:var(--mt-text-dim);font:11px var(--mt-mono);cursor:pointer;white-space:nowrap;background:0 0;border:0;border-radius:999px;padding:5px 12px;transition:background .18s,color .18s}.dev-fbtn:hover{color:var(--mt-text)}.dev-fbtn.is-active{background:var(--grad);color:var(--mt-bg);font-weight:600}.dev-grid-wide{grid-template-columns:repeat(auto-fill,minmax(280px,1fr));align-content:start;gap:14px;display:grid}.dev-collapse{border:1px solid var(--mt-border);border-radius:var(--mt-radius);background:var(--glass);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:6px 18px 18px}.dev-collapse>summary{cursor:pointer;text-transform:uppercase;letter-spacing:.07em;color:var(--mt-text-dim);align-items:center;gap:8px;padding:12px 0;font-size:12px;font-weight:600;list-style:none;display:flex}.dev-collapse>summary::-webkit-details-marker{display:none}.dev-collapse>summary:before{content:"▸";transition:transform .2s var(--ease);color:var(--teal);display:inline-block}.dev-collapse[open]>summary:before{transform:rotate(90deg)}.dev-card .dot--status{width:7px;height:7px}.dev-card{transition:transform .3s var(--ease), border-color .3s var(--ease);gap:8px;padding:14px 15px;display:grid}.dev-card:hover{border-color:color-mix(in srgb, var(--teal) 35%, var(--mt-border));transform:translateY(-2px)}.dev-card__top{align-items:center;gap:8px;display:flex}.dev-card__name{font-family:var(--mt-mono);font-size:13px;font-weight:500}.dev-card .dot{border-radius:50%;flex:none;width:9px;height:9px}.dev-card__meta{font-family:var(--mt-mono);color:var(--mt-text-dim);grid-template-columns:auto 1fr;gap:3px 12px;font-size:11px;display:grid}.dev-card__meta b{color:var(--mt-text);overflow-wrap:anywhere;font-weight:500}details.cfg>summary{cursor:pointer;color:var(--mt-text-dim);font-family:var(--mt-mono);-webkit-user-select:none;user-select:none;padding:4px 0;font-size:12px}.badge{font-family:var(--mt-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--mt-bg);background:var(--grad);border-radius:999px;padding:1px 7px;font-size:9px;font-weight:600}.overlay{-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);z-index:10;background:#0000009e;justify-content:flex-end;display:flex;position:fixed;inset:0}.sheet{background:color-mix(in srgb, var(--mt-surface) 96%, var(--mt-bg));border-left:1px solid color-mix(in srgb, var(--teal) 28%, var(--mt-border));align-content:start;gap:12px;width:min(580px,100%);height:100%;padding:18px;display:grid;overflow:auto;box-shadow:-24px 0 60px #00000080}.sheet__hd{align-items:center;gap:10px;display:flex}.sheet__hd h3{font-family:var(--mt-mono);margin:0;font-size:15px}pre.body{background:var(--mt-bg);border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);font-family:var(--mt-mono);white-space:pre-wrap;word-break:break-word;max-height:40vh;margin:0;padding:12px;font-size:12px;overflow:auto}pre.body.json{color:#a1a1aa}pre.body.json .j-key{color:#7dd3fc}pre.body.json .j-str{color:#86efac}pre.body.json .j-num{color:#fcd34d}pre.body.json .j-bool{color:#d8b4fe}pre.body.json .j-null{color:#71717a}.muted{color:var(--mt-text-faint);font-family:var(--mt-mono);font-size:11px}.btn-danger{color:var(--mt-bad);border-color:color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border));background:color-mix(in srgb, var(--mt-bad) 12%, transparent)}.btn-danger:hover{background:color-mix(in srgb, var(--mt-bad) 22%, transparent);border-color:var(--mt-bad)}.btn:hover{border-color:color-mix(in srgb, var(--teal) 55%, var(--mt-border));background:color-mix(in srgb, var(--teal) 10%, var(--mt-surface-2))}.btn.is-active{border-color:var(--teal);color:var(--teal)}.conn-legend{font-family:var(--mt-mono);color:var(--mt-text-dim);flex-wrap:wrap;justify-content:center;gap:16px;margin-top:8px;font-size:11px;display:flex}.conn-legend span{align-items:center;gap:6px;display:inline-flex}.conn-legend i{border-radius:3px;width:10px;height:10px;display:inline-block}.conn-chevron{fill:none;stroke-width:1.6px;stroke-linecap:round;stroke-linejoin:round;opacity:.85}.conn-errrow{cursor:pointer;border-radius:6px}.conn-errrow:hover{background:color-mix(in srgb, var(--mt-bad) 12%, transparent)}@media (prefers-reduced-motion:reduce){.conn-chevron{opacity:.6}}.health-grid{grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:14px;display:grid}.health-card{flex-direction:column;gap:10px;padding:16px;display:flex}.health-card--na{justify-content:center;gap:6px;min-height:120px}.health-card__hd{align-items:center;gap:8px;display:flex}.health-card__sub{font-family:var(--mt-mono);margin-top:-4px;font-size:11px}.health-card__gauges{justify-content:space-around;gap:14px;display:flex}.gauge{flex-direction:column;align-items:center;gap:2px;display:flex}.gauge__label{font-family:var(--mt-mono);letter-spacing:.06em;color:var(--mt-text-dim);font-size:10px}.health-card__charts{gap:8px;display:grid}.health-chart{gap:2px;display:grid}.health-chart__k{font-family:var(--mt-mono);text-transform:uppercase;letter-spacing:.05em;color:var(--mt-text-dim);font-size:10px}.spark{background:color-mix(in srgb, var(--mt-surface-2) 60%, transparent);border-radius:6px;display:block}.spark--empty{font-family:var(--mt-mono);color:var(--mt-text-faint);text-align:center;padding:12px 6px;font-size:11px}.health-card__foot{font-family:var(--mt-mono);font-size:11px}.topo{flex-direction:column;gap:10px;display:flex}.topo svg{background:radial-gradient(circle at 50% 45%, color-mix(in srgb, var(--mt-accent) 8%, transparent), transparent 60%);border-radius:var(--mt-radius-sm);width:100%;display:block}.topo-edge{stroke:color-mix(in srgb, var(--mt-text-faint) 55%, transparent);stroke-width:1.4px}.topo-edge.is-dashed{stroke:color-mix(in srgb, var(--mt-accent) 60%, transparent);stroke-dasharray:4 4}.topo-node{cursor:pointer}.topo-node rect{fill:var(--mt-surface-2);stroke-width:1.5px;transition:filter .15s}.topo-node.is-neighbor rect{fill:color-mix(in srgb, var(--mt-surface) 70%, transparent);stroke-dasharray:4 3}.topo-node.is-onboard rect{stroke-dasharray:4 3}.topo-node:hover rect,.topo-node.is-picked rect{filter:drop-shadow(0 0 6px color-mix(in srgb, var(--mt-accent) 55%, transparent))}.topo-node.is-picked rect{stroke-width:2.2px}.topo-label{fill:var(--mt-text);font:600 12px var(--mt-mono)}.topo-sub{fill:var(--mt-text-dim);font:10px var(--mt-mono)}.topo-bar-bg{fill:color-mix(in srgb, var(--mt-text-faint) 35%, transparent)}.topo-foot{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.topo-foot .legend{margin-top:0}.topo-pop{background:var(--mt-surface);border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);flex:1;min-width:280px;max-width:460px;padding:12px}.topo-pop__hd{align-items:center;gap:8px;margin-bottom:6px;display:flex}.topo-pop__hd .muted{font-size:11px}.topo-stub{background:var(--mt-bg);border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);font:11px/1.5 var(--mt-mono);color:var(--mt-text);white-space:pre;margin:0 0 8px;padding:8px 10px;overflow-x:auto}.topo-btn{font:11px var(--mt-mono);color:var(--mt-text);background:var(--mt-surface-2);border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);cursor:pointer;padding:5px 10px;transition:border-color .15s,color .15s}.topo-btn:hover{border-color:var(--mt-accent);color:var(--mt-accent)}.cfgstudio{flex-direction:column;gap:10px;display:flex}.cfg-toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.cfg-status{font:11px var(--mt-mono);border:1px solid var(--mt-border);border-radius:999px;padding:4px 10px}.cfg-status.is-ok{color:var(--mt-good);border-color:color-mix(in srgb, var(--mt-good) 45%, var(--mt-border));background:color-mix(in srgb, var(--mt-good) 10%, transparent)}.cfg-status.is-bad{color:var(--mt-bad);border-color:color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border));background:color-mix(in srgb, var(--mt-bad) 10%, transparent)}.cfg-select{font:11px var(--mt-mono);color:var(--mt-text);background:var(--mt-surface-2);border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);padding:5px 9px}.cfg-save{border-color:color-mix(in srgb, var(--mt-accent) 55%, var(--mt-border));color:var(--mt-accent)}.cfg-msg{font:12px var(--mt-mono);color:var(--mt-text-dim)}.cfg-banner{border-radius:var(--mt-radius-sm);background:color-mix(in srgb, var(--mt-warn) 12%, var(--mt-surface));border:1px solid color-mix(in srgb, var(--mt-warn) 45%, var(--mt-border));flex-wrap:wrap;align-items:center;gap:8px;padding:10px 13px;font-size:12px;display:flex}.cfg-count{font-family:var(--mt-mono);color:var(--mt-warn);font-weight:700}.cfg-warn{color:var(--mt-warn);font-size:11px}.cfg-editor{border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);background:var(--mt-bg);max-height:420px;display:flex;overflow:hidden}.cfg-gutter{text-align:right;color:var(--mt-text-faint);background:var(--mt-surface-2);font:12px/1.5 var(--mt-mono);-webkit-user-select:none;user-select:none;min-width:38px;margin:0;padding:10px 8px;overflow:hidden}.cfg-ta-wrap{flex:1;position:relative}.cfg-hl,.cfg-ta{box-sizing:border-box;font:12px/1.5 var(--mt-mono);white-space:pre;tab-size:2;letter-spacing:normal;border:0;margin:0;padding:10px 12px}.cfg-hl{z-index:0;pointer-events:none;color:var(--mt-text-dim);position:absolute;inset:0;overflow:hidden}.cfg-ta{z-index:1;resize:vertical;color:#0000;width:100%;height:400px;caret-color:var(--mt-accent);background:0 0;outline:none;position:relative}.cfg-ta::selection{background:color-mix(in srgb, var(--mt-accent) 35%, transparent)}.cfg-hl .j-key{color:#7dd3fc}.cfg-hl .j-str{color:#86efac}.cfg-hl .j-num{color:#fcd34d}.cfg-hl .j-bool{color:#d8b4fe}.cfg-hl .j-null{color:#71717a}.cfg-ac{z-index:5;background:var(--mt-surface);border:1px solid var(--mt-accent);border-radius:var(--mt-radius-sm);min-width:150px;max-height:184px;position:absolute;overflow-y:auto;box-shadow:0 8px 24px #00000080}.cfg-ac-item{font:12px var(--mt-mono);color:var(--mt-text-dim);cursor:pointer;padding:5px 11px}.cfg-ac-item.is-sel{background:color-mix(in srgb, var(--mt-accent) 22%, transparent);color:var(--mt-text)}.cfg-chips{flex-wrap:wrap;gap:6px;display:flex}.cfg-chip{font:11px var(--mt-mono);border:1px solid var(--mt-border);border-radius:999px;padding:4px 10px}.cfg-chip.is-ok{color:var(--mt-good);border-color:color-mix(in srgb, var(--mt-good) 45%, var(--mt-border))}.cfg-chip.is-bad{color:var(--mt-bad);border-color:color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border))}.cfg-errors{flex-direction:column;gap:3px;display:flex}.cfg-err{font:11px var(--mt-mono);color:var(--mt-bad)}.cfg-err code{color:var(--mt-warn)}.cfg-preview{border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);overflow:hidden}.cfg-preview__hd{background:var(--mt-surface-2);align-items:center;gap:8px;padding:9px 13px;font-size:12px;display:flex}.cfg-diff{max-height:320px;font:11px/1.5 var(--mt-mono);color:var(--mt-text-dim);background:var(--mt-bg);margin:0;padding:10px 12px;overflow:auto}.cfg-diff .d-add{color:var(--mt-good)}.cfg-diff .d-del{color:var(--mt-bad)}.cfg-diff .d-hunk{color:var(--mt-accent)}.cap-idle{font-size:13px;line-height:1.5}.cap-idle code{color:var(--mt-accent);font-family:var(--mt-mono)}.cap{flex-direction:column;gap:12px;display:flex}.cap-bar{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.cap-dot{background:var(--mt-text-faint);border-radius:999px;width:9px;height:9px}.cap-dot.is-on{background:var(--mt-good);box-shadow:0 0 0 4px color-mix(in srgb, var(--mt-good) 22%, transparent);animation:1.6s ease-out infinite feed-pulse}.cap-cols{grid-template-columns:220px 1fr;gap:12px;display:grid}@media (width<=720px){.cap-cols{grid-template-columns:1fr}}.cap-side{flex-direction:column;gap:5px;display:flex}.cap-h{text-transform:uppercase;letter-spacing:.05em;color:var(--mt-text-dim);font-size:11px}.cap-pbar{font:11px var(--mt-mono);grid-template-columns:52px 1fr 34px;align-items:center;gap:6px;display:grid}.cap-ptrack{background:var(--mt-surface-2);border-radius:999px;height:8px;overflow:hidden}.cap-ptrack i{border-radius:999px;height:100%;display:block}.cap-pn{text-align:right;color:var(--mt-text-dim)}.cap-talker{font:11px var(--mt-mono);color:var(--mt-text-dim);justify-content:space-between;display:flex}.cap-talker b{color:var(--mt-text)}.cap-list{border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);background:var(--mt-bg);height:360px;font:11px/1.6 var(--mt-mono);overflow-y:auto}.cap-row{white-space:nowrap;grid-template-columns:90px 64px 48px 1fr;gap:8px;padding:1px 10px;display:grid;overflow:hidden}.cap-row:nth-child(odd){background:color-mix(in srgb, var(--mt-surface) 40%, transparent)}.cap-tt{color:var(--mt-text-faint)}.cap-proto{font-weight:600}.cap-len{color:var(--mt-text-dim);text-align:right}.cap-info{color:var(--mt-text);text-overflow:ellipsis;overflow:hidden}.plan-input{resize:vertical;box-sizing:border-box;border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);background:var(--mt-bg);width:100%;min-height:160px;color:var(--mt-text);font:12px/1.6 var(--mt-mono);outline:none;padding:12px 14px}.plan-input:focus{border-color:color-mix(in srgb, var(--teal) 55%, var(--mt-border))}.plan-input::placeholder{color:var(--mt-text-faint)}.plan-steps{flex-direction:column;gap:6px;margin-top:12px;display:flex}.plan-step{border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);background:color-mix(in srgb, var(--mt-surface-2) 50%, transparent);font:12px var(--mt-mono);border-left-width:3px;align-items:center;gap:10px;padding:8px 12px;display:flex}.plan-step code{color:var(--mt-text);overflow-wrap:anywhere;flex:1}.plan-step.risk-high{border-left-color:var(--mt-bad)}.plan-step.risk-medium{border-left-color:var(--mt-warn)}.plan-step.risk-low{border-left-color:var(--mt-good)}.plan-op{background:color-mix(in srgb, var(--teal) 18%, transparent);width:18px;height:18px;color:var(--teal);border-radius:5px;flex:none;place-items:center;font-weight:700;display:grid}.plan-risk{text-transform:uppercase;letter-spacing:.05em;color:var(--mt-text-dim);font-size:10px}.plan-lock{color:var(--mt-bad);border:1px solid color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border));border-radius:999px;padding:1px 7px;font-size:10px}
31
+ @import "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap";:root{--mt-bg:var(--color-background-primary,#0b0d10);--mt-surface:var(--color-background-secondary,#14171c);--mt-surface-2:var(--color-background-tertiary,#1b1f26);--mt-border:var(--color-border-primary,#262b33);--mt-text:var(--color-text-primary,#e8eaed);--mt-text-dim:var(--color-text-secondary,#9aa3af);--mt-text-faint:var(--color-text-tertiary,#6b7280);--mt-accent:var(--color-accent-primary,#6ea8fe);--mt-good:#34d399;--mt-warn:#fbbf24;--mt-bad:#f87171;--mt-radius:var(--border-radius-md,14px);--mt-radius-sm:var(--border-radius-sm,9px);--mt-mono:var(--font-mono,ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace);--mt-sans:var(--font-sans,system-ui, -apple-system, "Segoe UI", sans-serif);--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}*{box-sizing:border-box}body{background:var(--mt-bg);color:var(--mt-text);font-family:var(--mt-sans);-webkit-font-smoothing:antialiased;margin:0;padding:16px;font-size:13px;line-height:1.5}.app{gap:14px;max-width:960px;margin:0 auto;display:grid}.hd{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.hd__dot{background:var(--mt-good);width:9px;height:9px;box-shadow:0 0 0 4px color-mix(in srgb, var(--mt-good) 22%, transparent);border-radius:50%}.hd__title{letter-spacing:-.01em;margin:0;font-size:17px;font-weight:650}.hd__sub{color:var(--mt-text-dim);font-family:var(--mt-mono);margin:0;font-size:12px}.hd__spacer{flex:1}.pill{border:1px solid var(--mt-border);background:var(--mt-surface);color:var(--mt-text-dim);font-family:var(--mt-mono);border-radius:999px;align-items:center;gap:6px;padding:3px 9px;font-size:11px;display:inline-flex}.pill b{color:var(--mt-text);font-weight:600}.grid{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;display:grid}.card{background:linear-gradient(180deg, var(--mt-surface), var(--mt-surface-2));border:1px solid var(--mt-border);border-radius:var(--mt-radius);padding:14px}.card__label{color:var(--mt-text-dim);text-transform:uppercase;letter-spacing:.06em;margin:0 0 8px;font-size:11px}.card__value{font-family:var(--mt-mono);letter-spacing:-.01em;font-size:20px;font-weight:600}.toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.toolbar .grow{flex:1;min-width:140px}.search{appearance:none;border:1px solid var(--mt-border);background:var(--mt-surface);width:100%;color:var(--mt-text);font:inherit;border-radius:var(--mt-radius-sm);padding:7px 11px;font-size:13px}.search:focus{border-color:var(--mt-accent);outline:none}.search::placeholder{color:var(--mt-text-faint)}.btn{appearance:none;border:1px solid var(--mt-border);background:var(--mt-surface);color:var(--mt-text);font:inherit;border-radius:var(--mt-radius-sm);cursor:pointer;white-space:nowrap;padding:6px 11px;font-size:12px;transition:border-color .15s,background .15s}.btn:disabled{opacity:.5;cursor:default}.chip{border:1px solid var(--mt-border);background:var(--mt-surface);color:var(--mt-text-dim);font-family:var(--mt-mono);border-radius:999px;align-items:center;gap:4px;padding:1px 7px;font-size:11px;display:inline-flex}.chip.is-good{color:var(--mt-good);border-color:color-mix(in srgb, var(--mt-good) 45%, var(--mt-border))}.chip.is-warn{color:var(--mt-warn);border-color:color-mix(in srgb, var(--mt-warn) 45%, var(--mt-border))}.chip.is-bad{color:var(--mt-bad);border-color:color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border))}.tablewrap{border:1px solid var(--mt-border);border-radius:var(--mt-radius);max-height:70vh;overflow:auto}table.tbl{border-collapse:collapse;width:100%;font-size:12px}table.tbl th,table.tbl td{text-align:left;border-bottom:1px solid color-mix(in srgb, var(--mt-border) 55%, transparent);white-space:nowrap;font-family:var(--mt-mono);padding:7px 12px}table.tbl thead th{z-index:1;background:var(--mt-surface-2);color:var(--mt-text-dim);cursor:pointer;-webkit-user-select:none;user-select:none;font-weight:600;position:sticky;top:0}table.tbl thead th:hover{color:var(--mt-text)}table.tbl thead th .arrow{color:var(--mt-accent);margin-left:4px}table.tbl tbody tr{cursor:pointer}table.tbl tbody tr:hover{background:color-mix(in srgb, var(--mt-accent) 8%, transparent)}table.tbl tbody tr.is-disabled td,table.tbl td.col-num{color:var(--mt-text-faint)}.kv__body{border:1px solid var(--mt-border);border-radius:var(--mt-radius);grid-template-columns:minmax(120px,.4fr) 1fr;display:grid;overflow:hidden}.kv__body>div{border-bottom:1px solid color-mix(in srgb, var(--mt-border) 55%, transparent);overflow-wrap:anywhere;min-width:0;font-size:12px;font-family:var(--mt-mono);padding:7px 14px}.kv__k{color:var(--mt-text-dim);background:color-mix(in srgb, var(--mt-surface) 60%, transparent)}.kv__v{color:var(--mt-text)}.drawer{border:1px solid var(--mt-accent);border-radius:var(--mt-radius);background:var(--mt-surface);gap:10px;padding:12px;display:grid}.drawer__hd{align-items:center;gap:8px;display:flex}.drawer__hd b{font-family:var(--mt-mono)}pre.raw{border:1px solid var(--mt-border);border-radius:var(--mt-radius);background:var(--mt-surface);color:var(--mt-text);font-family:var(--mt-mono);white-space:pre;max-height:70vh;margin:0;padding:14px;font-size:12px;overflow:auto}.foot{color:var(--mt-text-faint);font-size:11px;font-family:var(--mt-mono);flex-wrap:wrap;align-items:center;gap:10px;display:flex}.foot .grow{flex:1}.skeleton,.empty{color:var(--mt-text-faint);text-align:center;padding:40px 0}:root{--mt-sans:"Space Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;--mt-mono:"DM Mono", ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;--mt-bg:#09090b;--mt-surface:#18181b;--mt-surface-2:#27272a;--mt-border:#27272a;--mt-text:#fafafa;--mt-text-dim:#a1a1aa;--mt-text-faint:#71717a;--mt-accent:#e4e4e7;--mt-good:#d4d4d8;--mt-warn:#a1a1aa;--mt-bad:#f87171;--mt-radius:12px;--mt-radius-sm:8px;--sky:#d4d4d8;--teal:#e4e4e7;--cyan:#a1a1aa;--lime:#71717a;--coral:#f87171;--amber:#a1a1aa;--grad:linear-gradient(115deg, #e4e4e7, #a1a1aa 50%, #52525b);--ease:cubic-bezier(.22, 1, .36, 1);--glass:#18181b8c}html,body{background:var(--mt-bg);color:var(--mt-text);font-family:var(--mt-sans);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;padding:0}body:before{content:"";z-index:-2;background:radial-gradient(58% 42% at 12% -6%, color-mix(in srgb, var(--teal) 16%, transparent), transparent 70%), radial-gradient(50% 40% at 100% 0%, color-mix(in srgb, var(--sky) 13%, transparent), transparent 68%), radial-gradient(60% 50% at 84% 108%, color-mix(in srgb, var(--lime) 8%, transparent), transparent 70%);pointer-events:none;position:fixed;inset:0}body:after{content:"";z-index:-2;background-image:linear-gradient(color-mix(in srgb, var(--mt-border) 70%, transparent) 1px, transparent 1px), linear-gradient(90deg, color-mix(in srgb, var(--mt-border) 70%, transparent) 1px, transparent 1px);opacity:.5;pointer-events:none;background-size:46px 46px;position:fixed;inset:0;-webkit-mask-image:radial-gradient(circle at 50% 0,#000,#0000 75%);mask-image:radial-gradient(circle at 50% 0,#000,#0000 75%)}.shell{grid-template-columns:250px minmax(0,1fr);align-items:start;min-height:100vh;display:grid}.nav{border-right:1px solid var(--mt-border);background:color-mix(in srgb, var(--mt-surface) 55%, transparent);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);flex-direction:column;gap:10px;height:100vh;padding:18px 14px;display:flex;position:sticky;top:0}.nav__brand{align-items:center;gap:11px;padding:6px 8px 14px;display:flex}.nav__mark{background:var(--grad);width:38px;height:38px;box-shadow:0 8px 22px color-mix(in srgb, var(--teal) 38%, transparent);border-radius:11px;flex:none;place-items:center;display:grid}.nav__mark svg{width:22px;height:22px;display:block}.nav__brandtext{flex-direction:column;min-width:0;line-height:1.1;display:flex}.nav__brandtext b{letter-spacing:-.02em;font-size:15px;font-weight:700}.nav__brandtext small{font-family:var(--mt-mono);letter-spacing:.16em;text-transform:uppercase;color:var(--mt-text-dim);margin-top:3px;font-size:10px}.nav__items{flex-direction:column;flex:1;gap:3px;display:flex}.nav__item{color:var(--mt-text-dim);font-family:var(--mt-sans);cursor:pointer;text-align:left;transition:background .18s var(--ease), color .18s, border-color .18s, transform .18s var(--ease);background:0 0;border:1px solid #0000;border-radius:11px;align-items:center;gap:11px;padding:10px 12px;font-size:13px;font-weight:500;display:flex}.nav__item svg{opacity:.85;flex:none;width:18px;height:18px}.nav__item:hover{color:var(--mt-text);background:color-mix(in srgb, var(--teal) 9%, transparent)}.nav__item.is-active{color:var(--mt-text);border-color:color-mix(in srgb, var(--teal) 32%, var(--mt-border));background:linear-gradient(100deg, color-mix(in srgb, var(--teal) 18%, transparent), color-mix(in srgb, var(--sky) 7%, transparent))}.nav__item.is-active svg{opacity:1;color:var(--teal)}.nav__badge{font-family:var(--mt-mono);background:color-mix(in srgb, var(--teal) 22%, transparent);color:var(--mt-text);border-radius:999px;margin-left:auto;padding:1px 7px;font-size:10px}.nav__foot{border-top:1px solid var(--mt-border);flex-direction:column;gap:8px;padding:10px 8px 2px;display:flex}.main{align-content:start;gap:18px;min-width:0;padding:22px clamp(16px,3vw,44px) 64px;display:grid}.topline{flex-wrap:wrap;align-items:center;gap:14px;display:flex}.topline__txt h1{letter-spacing:-.025em;background:linear-gradient(100deg, var(--mt-text), color-mix(in srgb, var(--teal) 65%, var(--mt-text)));-webkit-text-fill-color:transparent;-webkit-background-clip:text;background-clip:text;margin:0;font-size:clamp(19px,2.4vw,26px);font-weight:700}.topline__txt small{color:var(--mt-text-dim);font-family:var(--mt-mono);margin-top:3px;font-size:12px;display:block}.topline__spacer{flex:1;min-width:8px}.view{align-content:start;gap:18px;display:grid}.bento{grid-template-columns:repeat(6,1fr);gap:18px;display:grid}.bento .panel{margin:0}.b-series{grid-column:span 4}.b-risk{grid-column:span 2}.b-tools,.b-status,.b-device,.b-errors{grid-column:span 3}.b-risk,.b-status{flex-direction:column;display:flex}@media (width<=1040px){.bento{grid-template-columns:repeat(2,1fr)}.b-series,.b-risk,.b-tools,.b-status,.b-device,.b-errors{grid-column:span 2}}@media (width<=900px){.shell{grid-template-columns:1fr}.nav{border-right:0;border-bottom:1px solid var(--mt-border);z-index:5;flex-direction:row;align-items:center;gap:8px;height:auto;padding:10px 12px;position:sticky;top:0;overflow-x:auto}.nav__brand{padding:0 6px 0 2px}.nav__brandtext{display:none}.nav__items{flex-direction:row;flex:1;gap:4px}.nav__item span{display:none}.nav__item{padding:9px 11px}.nav__foot{border-top:0;flex-direction:row;align-items:center;padding:0}.nav__foot .muted{display:none}}@media (width<=560px){.nav__brand{display:none}}@media (prefers-reduced-motion:no-preference){.js-motion .reveal{opacity:0}}.hero__live{border:1px solid var(--mt-border);background:var(--glass);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);font-family:var(--mt-mono);letter-spacing:.04em;text-transform:uppercase;color:var(--mt-text-dim);border-radius:999px;align-items:center;gap:7px;padding:7px 13px;font-size:11px;display:inline-flex}.hero__live .dot{background:var(--mt-bad);border-radius:50%;width:8px;height:8px;transition:background .3s}.hero__live.is-on{color:var(--mt-text);border-color:color-mix(in srgb, var(--mt-good) 40%, var(--mt-border))}.hero__live.is-on .dot{background:var(--mt-good);box-shadow:0 0 0 4px color-mix(in srgb, var(--mt-good) 22%, transparent);animation:1.8s ease-out infinite hero-pulse}@keyframes hero-pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb, var(--mt-good) 55%, transparent)}70%{box-shadow:0 0 0 9px color-mix(in srgb, var(--mt-good) 0%, transparent)}to{box-shadow:0 0 0 0 color-mix(in srgb, var(--mt-good) 0%, transparent)}}.panel>h2,.sheet__hd h2{font-family:var(--mt-sans)}.cards{grid-template-columns:repeat(2,1fr);gap:12px;display:grid}@media (width>=560px){.cards{grid-template-columns:repeat(4,1fr)}}@media (width>=1040px){.cards{grid-template-columns:repeat(8,1fr)}}.stat{background:linear-gradient(180deg, var(--mt-surface), color-mix(in srgb, var(--mt-surface-2) 80%, transparent));border:1px solid var(--mt-border);border-radius:var(--mt-radius);transition:transform .3s var(--ease), border-color .3s var(--ease);padding:14px 15px;position:relative;overflow:hidden}.stat:before{content:"";background:var(--grad);opacity:0;height:2px;transition:opacity .3s var(--ease);position:absolute;inset:0 0 auto}.stat:hover{border-color:color-mix(in srgb, var(--teal) 38%, var(--mt-border));transform:translateY(-3px)}.stat:hover:before{opacity:1}.stat .k{color:var(--mt-text-dim);text-transform:uppercase;letter-spacing:.08em;margin:0 0 7px;font-size:10px}.stat .v{font-family:var(--mt-mono);letter-spacing:-.02em;font-variant-numeric:tabular-nums;font-size:23px;font-weight:500}.stat .v small{color:var(--mt-text-dim);font-size:12px;font-weight:400}.stat.is-bad .v{color:var(--mt-bad)}.stat.is-warn .v{color:var(--mt-warn)}.stat.is-good .v{color:var(--mt-good)}.panel{background:var(--glass);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border:1px solid var(--mt-border);border-radius:var(--mt-radius);padding:18px;position:relative;box-shadow:inset 0 1px #ffffff0a}.panel>h2{color:var(--mt-text-dim);text-transform:uppercase;letter-spacing:.07em;align-items:center;gap:9px;margin:0 0 14px;font-size:12px;font-weight:600;display:inline-flex}.panel>h2:before,.sheet__hd h2:first-child:before{content:"";background:var(--grad);border-radius:2px;flex:none;width:3px;height:13px}.cols{grid-template-columns:1.4fr 1fr;gap:18px;display:grid}@media (width<=820px){.cols{grid-template-columns:1fr}}.cols-3{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:18px;display:grid}.cols-3 .panel{flex-direction:column;display:flex}.hbar{gap:9px;display:grid}.hbar__row{grid-template-columns:minmax(90px,.9fr) 2fr auto;align-items:center;gap:10px;font-size:12px;display:grid}.hbar__label{font-family:var(--mt-mono);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.hbar__track{background:color-mix(in srgb, var(--mt-surface-2) 90%, transparent);border-radius:999px;height:8px;overflow:hidden}.hbar__fill{background:var(--grad);height:100%;box-shadow:0 0 10px color-mix(in srgb, var(--teal) 45%, transparent);border-radius:999px}.hbar__val{font-family:var(--mt-mono);color:var(--mt-text-dim);text-align:right;font-variant-numeric:tabular-nums;font-size:11px}.legend{font-family:var(--mt-mono);color:var(--mt-text-dim);flex-wrap:wrap;gap:12px;margin-top:12px;font-size:11px;display:flex}.legend span{align-items:center;gap:5px;display:inline-flex}.legend i{border-radius:2px;width:9px;height:9px;display:inline-block}.risk{font-family:var(--mt-mono);border:1px solid var(--mt-border);text-transform:uppercase;letter-spacing:.04em;border-radius:999px;padding:1px 8px;font-size:10px;display:inline-block}.risk-READ{color:var(--mt-good);border-color:color-mix(in srgb, var(--mt-good) 45%, var(--mt-border));background:color-mix(in srgb, var(--mt-good) 10%, transparent)}.risk-WRITE,.risk-WRITE_IDEMPOTENT{color:var(--mt-warn);border-color:color-mix(in srgb, var(--mt-warn) 45%, var(--mt-border));background:color-mix(in srgb, var(--mt-warn) 10%, transparent)}.risk-DESTRUCTIVE,.risk-DANGEROUS{color:var(--mt-bad);border-color:color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border));background:color-mix(in srgb, var(--mt-bad) 10%, transparent)}.feed{border-collapse:collapse;width:100%;font-size:12px}.feed th,.feed td{text-align:left;border-bottom:1px solid color-mix(in srgb, var(--mt-border) 55%, transparent);white-space:nowrap;font-family:var(--mt-mono);padding:7px 11px}.feed thead th{background:color-mix(in srgb, var(--mt-surface-2) 96%, transparent);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);color:var(--mt-text-dim);text-transform:uppercase;letter-spacing:.05em;z-index:1;font-size:10px;font-weight:600;position:sticky;top:0}.feed tbody tr{cursor:pointer;transition:background .12s}.feed tbody tr:hover{background:color-mix(in srgb, var(--mt-accent) 9%, transparent)}.feed tbody tr.is-err{background:color-mix(in srgb, var(--mt-bad) 9%, transparent)}.feed tbody tr.is-selected{background:color-mix(in srgb, var(--mt-accent) 16%, transparent)}.feed td.preview{white-space:nowrap;text-overflow:ellipsis;max-width:320px;color:var(--mt-text-dim);overflow:hidden}.feed td.num{text-align:right;color:var(--mt-text-dim);font-variant-numeric:tabular-nums}.status-ok{color:var(--mt-good)}.status-err{color:var(--mt-bad)}.feedwrap{border:1px solid var(--mt-border);border-radius:var(--mt-radius);max-height:60vh;overflow:auto}.feed-empty{border:1px dashed var(--mt-border);border-radius:var(--mt-radius);text-align:center;justify-items:center;gap:8px;padding:54px 16px;display:grid}.feed-empty__icon{opacity:.8;font-size:28px}.feed-empty__title{color:var(--mt-text);margin:0;font-weight:600}.feed-empty__sub{max-width:460px;color:var(--mt-text-faint);margin:0;font-size:12px}.feed-empty .btn{margin-top:6px}.feed-empty__pulse{background:var(--mt-text-faint);border-radius:50%;width:12px;height:12px}.feed-empty__pulse.is-on{background:var(--mt-good);animation:1.6s ease-out infinite feed-pulse}@keyframes feed-pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb, var(--mt-good) 55%, transparent)}70%{box-shadow:0 0 0 12px color-mix(in srgb, var(--mt-good) 0%, transparent)}to{box-shadow:0 0 0 0 color-mix(in srgb, var(--mt-good) 0%, transparent)}}.conn{display:block;overflow:visible}.conn-grid{fill:none;stroke:var(--mt-border);stroke-opacity:.7;stroke-dasharray:2 7}.conn-sonar{fill:none;stroke:var(--mt-accent);stroke-width:1.5px;opacity:0;animation:3.3s ease-out infinite conn-sonar}@keyframes conn-sonar{0%{r:30px;opacity:.5}to{r:150px;opacity:0}}.conn-link{fill:none;stroke-width:1.6px;opacity:.38}.conn-flow{fill:none;stroke-width:2.4px;stroke-linecap:round;stroke-dasharray:2 13;opacity:.9;filter:drop-shadow(0 0 4px);animation:1.05s linear infinite conn-flow}@keyframes conn-flow{to{stroke-dashoffset:-60px}}.conn-packet{filter:drop-shadow(0 0 6px var(--teal))}.conn-hub-glow{fill:var(--mt-accent);opacity:.22;filter:blur(11px)}.conn-hub-ring{fill:none;stroke:var(--mt-accent);stroke-width:1.4px;stroke-dasharray:4 9;opacity:.7;transform-box:fill-box;transform-origin:50%;animation:15s linear infinite conn-rot}@keyframes conn-rot{to{transform:rotate(360deg)}}.conn-node-halo{fill:none;stroke-width:2px;transform-box:fill-box;transform-origin:50%;animation:2.4s ease-in-out infinite conn-halo}@keyframes conn-halo{0%,to{opacity:.45;transform:scale(1)}50%{opacity:0;transform:scale(1.55)}}.conn-blink{animation:1.6s ease-in-out infinite conn-blink}@keyframes conn-blink{0%,to{opacity:1}50%{opacity:.35}}.conn-node text{pointer-events:none}@media (prefers-reduced-motion:reduce){.conn-sonar,.conn-flow,.conn-hub-ring,.conn-node-halo,.conn-blink,.conn-packet animateMotion{animation:none}.conn-sonar{display:none}}.dev-grid{grid-template-columns:1fr;align-content:start;gap:12px;display:grid}@media (width>=600px) and (width<=819px){.dev-grid{grid-template-columns:1fr 1fr}}.dev-toolbar{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.dev-filters{border:1px solid var(--mt-border);background:color-mix(in srgb, var(--mt-surface-2) 70%, transparent);border-radius:999px;gap:4px;padding:3px;display:inline-flex}.dev-fbtn{appearance:none;color:var(--mt-text-dim);font:11px var(--mt-mono);cursor:pointer;white-space:nowrap;background:0 0;border:0;border-radius:999px;padding:5px 12px;transition:background .18s,color .18s}.dev-fbtn:hover{color:var(--mt-text)}.dev-fbtn.is-active{background:var(--grad);color:var(--mt-bg);font-weight:600}.dev-grid-wide{grid-template-columns:repeat(auto-fill,minmax(280px,1fr));align-content:start;gap:14px;display:grid}.dev-collapse{border:1px solid var(--mt-border);border-radius:var(--mt-radius);background:var(--glass);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:6px 18px 18px}.dev-collapse>summary{cursor:pointer;text-transform:uppercase;letter-spacing:.07em;color:var(--mt-text-dim);align-items:center;gap:8px;padding:12px 0;font-size:12px;font-weight:600;list-style:none;display:flex}.dev-collapse>summary::-webkit-details-marker{display:none}.dev-collapse>summary:before{content:"▸";transition:transform .2s var(--ease);color:var(--teal);display:inline-block}.dev-collapse[open]>summary:before{transform:rotate(90deg)}.dev-card .dot--status{width:7px;height:7px}.dev-card{transition:transform .3s var(--ease), border-color .3s var(--ease);gap:8px;padding:14px 15px;display:grid}.dev-card:hover{border-color:color-mix(in srgb, var(--teal) 35%, var(--mt-border));transform:translateY(-2px)}.dev-card__top{align-items:center;gap:8px;display:flex}.dev-card__name{font-family:var(--mt-mono);font-size:13px;font-weight:500}.dev-card .dot{border-radius:50%;flex:none;width:9px;height:9px}.dev-card__meta{font-family:var(--mt-mono);color:var(--mt-text-dim);grid-template-columns:auto 1fr;gap:3px 12px;font-size:11px;display:grid}.dev-card__meta b{color:var(--mt-text);overflow-wrap:anywhere;font-weight:500}details.cfg>summary{cursor:pointer;color:var(--mt-text-dim);font-family:var(--mt-mono);-webkit-user-select:none;user-select:none;padding:4px 0;font-size:12px}.badge{font-family:var(--mt-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--mt-bg);background:var(--grad);border-radius:999px;padding:1px 7px;font-size:9px;font-weight:600}.overlay{-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);z-index:10;background:#0000009e;justify-content:flex-end;display:flex;position:fixed;inset:0}.sheet{background:color-mix(in srgb, var(--mt-surface) 96%, var(--mt-bg));border-left:1px solid color-mix(in srgb, var(--teal) 28%, var(--mt-border));align-content:start;gap:12px;width:min(580px,100%);height:100%;padding:18px;display:grid;overflow:auto;box-shadow:-24px 0 60px #00000080}.sheet__hd{align-items:center;gap:10px;display:flex}.sheet__hd h3{font-family:var(--mt-mono);margin:0;font-size:15px}pre.body{background:var(--mt-bg);border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);font-family:var(--mt-mono);white-space:pre-wrap;word-break:break-word;max-height:40vh;margin:0;padding:12px;font-size:12px;overflow:auto}pre.body.json{color:#a1a1aa}pre.body.json .j-key{color:#7dd3fc}pre.body.json .j-str{color:#86efac}pre.body.json .j-num{color:#fcd34d}pre.body.json .j-bool{color:#d8b4fe}pre.body.json .j-null{color:#71717a}.muted{color:var(--mt-text-faint);font-family:var(--mt-mono);font-size:11px}.btn-danger{color:var(--mt-bad);border-color:color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border));background:color-mix(in srgb, var(--mt-bad) 12%, transparent)}.btn-danger:hover{background:color-mix(in srgb, var(--mt-bad) 22%, transparent);border-color:var(--mt-bad)}.btn:hover{border-color:color-mix(in srgb, var(--teal) 55%, var(--mt-border));background:color-mix(in srgb, var(--teal) 10%, var(--mt-surface-2))}.btn.is-active{border-color:var(--teal);color:var(--teal)}.conn-legend{font-family:var(--mt-mono);color:var(--mt-text-dim);flex-wrap:wrap;justify-content:center;gap:16px;margin-top:8px;font-size:11px;display:flex}.conn-legend span{align-items:center;gap:6px;display:inline-flex}.conn-legend i{border-radius:3px;width:10px;height:10px;display:inline-block}.conn-chevron{fill:none;stroke-width:1.6px;stroke-linecap:round;stroke-linejoin:round;opacity:.85}.conn-errrow{cursor:pointer;border-radius:6px}.conn-errrow:hover{background:color-mix(in srgb, var(--mt-bad) 12%, transparent)}@media (prefers-reduced-motion:reduce){.conn-chevron{opacity:.6}}.health-grid{grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:14px;display:grid}.health-card{flex-direction:column;gap:10px;padding:16px;display:flex}.health-card--na{justify-content:center;gap:6px;min-height:120px}.health-card__hd{align-items:center;gap:8px;display:flex}.health-card__sub{font-family:var(--mt-mono);margin-top:-4px;font-size:11px}.health-card__gauges{justify-content:space-around;gap:14px;display:flex}.gauge{flex-direction:column;align-items:center;gap:2px;display:flex}.gauge__label{font-family:var(--mt-mono);letter-spacing:.06em;color:var(--mt-text-dim);font-size:10px}.health-card__charts{gap:8px;display:grid}.health-chart{gap:2px;display:grid}.health-chart__k{font-family:var(--mt-mono);text-transform:uppercase;letter-spacing:.05em;color:var(--mt-text-dim);font-size:10px}.spark{background:color-mix(in srgb, var(--mt-surface-2) 60%, transparent);border-radius:6px;display:block}.spark--empty{font-family:var(--mt-mono);color:var(--mt-text-faint);text-align:center;padding:12px 6px;font-size:11px}.health-card__foot{font-family:var(--mt-mono);font-size:11px}.topo{flex-direction:column;gap:10px;display:flex}.topo svg{background:radial-gradient(circle at 50% 45%, color-mix(in srgb, var(--mt-accent) 8%, transparent), transparent 60%);border-radius:var(--mt-radius-sm);width:100%;display:block}.topo-edge{stroke:color-mix(in srgb, var(--mt-text-faint) 55%, transparent);stroke-width:1.4px}.topo-edge.is-dashed{stroke:color-mix(in srgb, var(--mt-accent) 60%, transparent);stroke-dasharray:4 4}.topo-node{cursor:pointer}.topo-node rect{fill:var(--mt-surface-2);stroke-width:1.5px;transition:filter .15s}.topo-node.is-neighbor rect{fill:color-mix(in srgb, var(--mt-surface) 70%, transparent);stroke-dasharray:4 3}.topo-node.is-onboard rect{stroke-dasharray:4 3}.topo-node:hover rect,.topo-node.is-picked rect{filter:drop-shadow(0 0 6px color-mix(in srgb, var(--mt-accent) 55%, transparent))}.topo-node.is-picked rect{stroke-width:2.2px}.topo-label{fill:var(--mt-text);font:600 12px var(--mt-mono)}.topo-sub{fill:var(--mt-text-dim);font:10px var(--mt-mono)}.topo-bar-bg{fill:color-mix(in srgb, var(--mt-text-faint) 35%, transparent)}.topo-foot{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.topo-foot .legend{margin-top:0}.topo-pop{background:var(--mt-surface);border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);flex:1;min-width:280px;max-width:460px;padding:12px}.topo-pop__hd{align-items:center;gap:8px;margin-bottom:6px;display:flex}.topo-pop__hd .muted{font-size:11px}.topo-stub{background:var(--mt-bg);border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);font:11px/1.5 var(--mt-mono);color:var(--mt-text);white-space:pre;margin:0 0 8px;padding:8px 10px;overflow-x:auto}.topo-btn{font:11px var(--mt-mono);color:var(--mt-text);background:var(--mt-surface-2);border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);cursor:pointer;padding:5px 10px;transition:border-color .15s,color .15s}.topo-btn:hover{border-color:var(--mt-accent);color:var(--mt-accent)}.cfgstudio{flex-direction:column;gap:10px;display:flex}.cfg-toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.cfg-status{font:11px var(--mt-mono);border:1px solid var(--mt-border);border-radius:999px;padding:4px 10px}.cfg-status.is-ok{color:var(--mt-good);border-color:color-mix(in srgb, var(--mt-good) 45%, var(--mt-border));background:color-mix(in srgb, var(--mt-good) 10%, transparent)}.cfg-status.is-bad{color:var(--mt-bad);border-color:color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border));background:color-mix(in srgb, var(--mt-bad) 10%, transparent)}.cfg-select{font:11px var(--mt-mono);color:var(--mt-text);background:var(--mt-surface-2);border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);padding:5px 9px}.cfg-save{border-color:color-mix(in srgb, var(--mt-accent) 55%, var(--mt-border));color:var(--mt-accent)}.backup-path{align-items:center;gap:8px;max-width:min(60vw,520px);display:inline-flex}.backup-path>span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.backup-path-input{font:11px var(--mt-mono);color:var(--mt-text);background:var(--mt-surface-2);border:1px solid color-mix(in srgb, var(--mt-accent) 45%, var(--mt-border));border-radius:var(--mt-radius-sm);width:min(48vw,380px);padding:5px 9px}.linkish{font:inherit;color:var(--mt-accent);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0}.linkish:hover{text-decoration:underline}.cfg-msg{font:12px var(--mt-mono);color:var(--mt-text-dim)}.cfg-banner{border-radius:var(--mt-radius-sm);background:color-mix(in srgb, var(--mt-warn) 12%, var(--mt-surface));border:1px solid color-mix(in srgb, var(--mt-warn) 45%, var(--mt-border));flex-wrap:wrap;align-items:center;gap:8px;padding:10px 13px;font-size:12px;display:flex}.cfg-count{font-family:var(--mt-mono);color:var(--mt-warn);font-weight:700}.cfg-warn{color:var(--mt-warn);font-size:11px}.cfg-editor{border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);background:var(--mt-bg);max-height:420px;display:flex;overflow:hidden}.cfg-gutter{text-align:right;color:var(--mt-text-faint);background:var(--mt-surface-2);font:12px/1.5 var(--mt-mono);-webkit-user-select:none;user-select:none;min-width:38px;margin:0;padding:10px 8px;overflow:hidden}.cfg-ta-wrap{flex:1;position:relative}.cfg-hl,.cfg-ta{box-sizing:border-box;font:12px/1.5 var(--mt-mono);white-space:pre;tab-size:2;letter-spacing:normal;border:0;margin:0;padding:10px 12px}.cfg-hl{z-index:0;pointer-events:none;color:var(--mt-text-dim);position:absolute;inset:0;overflow:hidden}.cfg-ta{z-index:1;resize:vertical;color:#0000;width:100%;height:400px;caret-color:var(--mt-accent);background:0 0;outline:none;position:relative}.cfg-ta::selection{background:color-mix(in srgb, var(--mt-accent) 35%, transparent)}.cfg-hl .j-key{color:#7dd3fc}.cfg-hl .j-str{color:#86efac}.cfg-hl .j-num{color:#fcd34d}.cfg-hl .j-bool{color:#d8b4fe}.cfg-hl .j-null{color:#71717a}.cfg-ac{z-index:5;background:var(--mt-surface);border:1px solid var(--mt-accent);border-radius:var(--mt-radius-sm);min-width:150px;max-height:184px;position:absolute;overflow-y:auto;box-shadow:0 8px 24px #00000080}.cfg-ac-item{font:12px var(--mt-mono);color:var(--mt-text-dim);cursor:pointer;padding:5px 11px}.cfg-ac-item.is-sel{background:color-mix(in srgb, var(--mt-accent) 22%, transparent);color:var(--mt-text)}.cfg-chips{flex-wrap:wrap;gap:6px;display:flex}.cfg-chip{font:11px var(--mt-mono);border:1px solid var(--mt-border);border-radius:999px;padding:4px 10px}.cfg-chip.is-ok{color:var(--mt-good);border-color:color-mix(in srgb, var(--mt-good) 45%, var(--mt-border))}.cfg-chip.is-bad{color:var(--mt-bad);border-color:color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border))}.cfg-errors{flex-direction:column;gap:3px;display:flex}.cfg-err{font:11px var(--mt-mono);color:var(--mt-bad)}.cfg-err code{color:var(--mt-warn)}.cfg-preview{border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);overflow:hidden}.cfg-preview__hd{background:var(--mt-surface-2);align-items:center;gap:8px;padding:9px 13px;font-size:12px;display:flex}.cfg-diff{max-height:320px;font:11px/1.5 var(--mt-mono);color:var(--mt-text-dim);background:var(--mt-bg);margin:0;padding:10px 12px;overflow:auto}.cfg-diff .d-add{color:var(--mt-good)}.cfg-diff .d-del{color:var(--mt-bad)}.cfg-diff .d-hunk{color:var(--mt-accent)}.cap-idle{font-size:13px;line-height:1.5}.cap-idle code{color:var(--mt-accent);font-family:var(--mt-mono)}.cap{flex-direction:column;gap:12px;display:flex}.cap-bar{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.cap-dot{background:var(--mt-text-faint);border-radius:999px;width:9px;height:9px}.cap-dot.is-on{background:var(--mt-good);box-shadow:0 0 0 4px color-mix(in srgb, var(--mt-good) 22%, transparent);animation:1.6s ease-out infinite feed-pulse}.cap-cols{grid-template-columns:220px 1fr;gap:12px;display:grid}@media (width<=720px){.cap-cols{grid-template-columns:1fr}}.cap-side{flex-direction:column;gap:5px;display:flex}.cap-h{text-transform:uppercase;letter-spacing:.05em;color:var(--mt-text-dim);font-size:11px}.cap-pbar{font:11px var(--mt-mono);grid-template-columns:52px 1fr 34px;align-items:center;gap:6px;display:grid}.cap-ptrack{background:var(--mt-surface-2);border-radius:999px;height:8px;overflow:hidden}.cap-ptrack i{border-radius:999px;height:100%;display:block}.cap-pn{text-align:right;color:var(--mt-text-dim)}.cap-talker{font:11px var(--mt-mono);color:var(--mt-text-dim);justify-content:space-between;display:flex}.cap-talker b{color:var(--mt-text)}.cap-list{border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);background:var(--mt-bg);height:360px;font:11px/1.6 var(--mt-mono);overflow-y:auto}.cap-row{white-space:nowrap;grid-template-columns:90px 64px 48px 1fr;gap:8px;padding:1px 10px;display:grid;overflow:hidden}.cap-row:nth-child(odd){background:color-mix(in srgb, var(--mt-surface) 40%, transparent)}.cap-tt{color:var(--mt-text-faint)}.cap-proto{font-weight:600}.cap-len{color:var(--mt-text-dim);text-align:right}.cap-info{color:var(--mt-text);text-overflow:ellipsis;overflow:hidden}.plan-input{resize:vertical;box-sizing:border-box;border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);background:var(--mt-bg);width:100%;min-height:160px;color:var(--mt-text);font:12px/1.6 var(--mt-mono);outline:none;padding:12px 14px}.plan-input:focus{border-color:color-mix(in srgb, var(--teal) 55%, var(--mt-border))}.plan-input::placeholder{color:var(--mt-text-faint)}.plan-steps{flex-direction:column;gap:6px;margin-top:12px;display:flex}.plan-step{border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);background:color-mix(in srgb, var(--mt-surface-2) 50%, transparent);font:12px var(--mt-mono);border-left-width:3px;align-items:center;gap:10px;padding:8px 12px;display:flex}.plan-step code{color:var(--mt-text);overflow-wrap:anywhere;flex:1}.plan-step.risk-high{border-left-color:var(--mt-bad)}.plan-step.risk-medium{border-left-color:var(--mt-warn)}.plan-step.risk-low{border-left-color:var(--mt-good)}.plan-op{background:color-mix(in srgb, var(--teal) 18%, transparent);width:18px;height:18px;color:var(--teal);border-radius:5px;flex:none;place-items:center;font-weight:700;display:grid}.plan-risk{text-transform:uppercase;letter-spacing:.05em;color:var(--mt-text-dim);font-size:10px}.plan-lock{color:var(--mt-bad);border:1px solid color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border));border-radius:999px;padding:1px 7px;font-size:10px}
32
32
  /*$vite$:1*/
33
33
  </style>
34
34
  </head>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usex/mikrotik-mcp",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "MCP server for MikroTik RouterOS — 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
5
5
  "keywords": [
6
6
  "ai",
package/schemas/README.md CHANGED
@@ -7,7 +7,7 @@ edit by hand — regenerate instead.
7
7
  | File | Contents |
8
8
  | -------------------- | ------------------------------------------------------------------------------------------ |
9
9
  | `config.schema.json` | The runtime configuration object (env vars / CLI flags). |
10
- | `tool-catalog.json` | Every one of the 664 tools: `name`, `risk`, `title`, `description`, and input JSON Schema. |
10
+ | `tool-catalog.json` | Every one of the 670 tools: `name`, `risk`, `title`, `description`, and input JSON Schema. |
11
11
  | `tools/<name>.json` | The input JSON Schema for a single tool. |
12
12
 
13
13
  `risk` is derived from the MCP tool annotations:
@@ -226,6 +226,9 @@
226
226
  "readOnly": {
227
227
  "default": false,
228
228
  "type": "boolean"
229
+ },
230
+ "backupDir": {
231
+ "type": "string"
229
232
  }
230
233
  },
231
234
  "required": ["devices", "defaultDevice", "mcp", "dashboard", "readOnly"],
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "2.5.0",
3
+ "version": "3.3.0",
4
4
  "generated": "by scripts/gen-schemas.ts — do not edit by hand",
5
- "toolCount": 664,
5
+ "toolCount": 670,
6
6
  "tools": [
7
7
  {
8
8
  "name": "list_interfaces",
@@ -2089,7 +2089,7 @@
2089
2089
  "destructiveHint": false,
2090
2090
  "openWorldHint": false
2091
2091
  },
2092
- "description": "Adds an IPv4 static route (`/ip route add`) to the routing table — use this to define any non-default unicast next-hop (host, subnet, or summarized prefix). For the 0.0.0.0/0 default gateway use add_default_route; for null/drop routes use add_blackhole_route; for IPv6 use add_ipv6_route. `distance` sets priority (1–255, lower wins); `routing_mark` assigns the route to a policy-routing table; `check_gateway` (\"ping\" or \"arp\") enables active gateway monitoring. Returns the created route's detail including its `.id`.",
2092
+ "description": "Adds an IPv4 static route (`/ip route add`) to the routing table — use this to define any non-default unicast next-hop (host, subnet, or summarized prefix). For the 0.0.0.0/0 default gateway use add_default_route; for null/drop routes use add_blackhole_route; for IPv6 use add_ipv6_route. `distance` sets priority (1–255, lower wins); `routing_table` assigns the route to a policy-routing table (must already exist — see add_routing_table); `check_gateway` (\"ping\" or \"arp\") enables active gateway monitoring. Returns the created route's detail including its `.id`.",
2093
2093
  "inputSchema": {
2094
2094
  "$schema": "https://json-schema.org/draft/2020-12/schema",
2095
2095
  "type": "object",
@@ -2117,7 +2117,12 @@
2117
2117
  "minimum": -9007199254740991,
2118
2118
  "maximum": 9007199254740991
2119
2119
  },
2120
+ "routing_table": {
2121
+ "description": "Policy-routing table name, e.g. \"main\" or a custom table (RouterOS v7)",
2122
+ "type": "string"
2123
+ },
2120
2124
  "routing_mark": {
2125
+ "description": "Deprecated alias for routing_table (RouterOS v6 name)",
2121
2126
  "type": "string"
2122
2127
  },
2123
2128
  "comment": {
@@ -2151,7 +2156,7 @@
2151
2156
  "idempotentHint": true,
2152
2157
  "openWorldHint": false
2153
2158
  },
2154
- "description": "Lists IPv4 routes from `/ip route` with optional filters — the primary tool for inspecting what routes the router knows. `dst_filter` and `gateway_filter` do substring matching; `routing_mark_filter` and `distance_filter` do exact matching; `active_only`/`disabled_only`/`dynamic_only`/`static_only` are boolean flags. For a table-scoped view of active routes use get_routing_table; for counts and summary stats use get_route_statistics; for IPv6 use list_ipv6_routes. Returns all matching route entries.",
2159
+ "description": "Lists IPv4 routes from `/ip route` with optional filters — the primary tool for inspecting what routes the router knows. `dst_filter` and `gateway_filter` do substring matching; `routing_table_filter` and `distance_filter` do exact matching; `active_only`/`disabled_only`/`dynamic_only`/`static_only` are boolean flags. For a table-scoped view of active routes use get_routing_table; for counts and summary stats use get_route_statistics; for IPv6 use list_ipv6_routes. Returns all matching route entries.",
2155
2160
  "inputSchema": {
2156
2161
  "$schema": "https://json-schema.org/draft/2020-12/schema",
2157
2162
  "type": "object",
@@ -2162,7 +2167,12 @@
2162
2167
  "gateway_filter": {
2163
2168
  "type": "string"
2164
2169
  },
2170
+ "routing_table_filter": {
2171
+ "description": "Exact policy-routing table name (RouterOS v7)",
2172
+ "type": "string"
2173
+ },
2165
2174
  "routing_mark_filter": {
2175
+ "description": "Deprecated alias for routing_table_filter",
2166
2176
  "type": "string"
2167
2177
  },
2168
2178
  "distance_filter": {
@@ -2223,7 +2233,7 @@
2223
2233
  "idempotentHint": true,
2224
2234
  "openWorldHint": false
2225
2235
  },
2226
- "description": "Modifies an existing IPv4 static route (`/ip route set`) — change its gateway, dst-address, distance, scope, routing-mark, VRF interface, preferred-source, or gateway check method. `route_id` is the `*N` `.id` from list_routes. Pass an empty string (\"\") for `routing_mark`, `vrf_interface`, or `pref_src` to clear those fields. For toggling active state without editing attributes use enable_route / disable_route. Returns the updated route detail.",
2236
+ "description": "Modifies an existing IPv4 static route (`/ip route set`) — change its gateway, dst-address, distance, scope, routing-table, VRF interface, preferred-source, or gateway check method. `route_id` is the `*N` `.id` from list_routes. Pass an empty string (\"\") for `routing_table`, `vrf_interface`, or `pref_src` to clear those fields. For toggling active state without editing attributes use enable_route / disable_route. Returns the updated route detail.",
2227
2237
  "inputSchema": {
2228
2238
  "$schema": "https://json-schema.org/draft/2020-12/schema",
2229
2239
  "type": "object",
@@ -2255,7 +2265,12 @@
2255
2265
  "minimum": -9007199254740991,
2256
2266
  "maximum": 9007199254740991
2257
2267
  },
2268
+ "routing_table": {
2269
+ "description": "Policy-routing table name (RouterOS v7); \"\" clears it",
2270
+ "type": "string"
2271
+ },
2258
2272
  "routing_mark": {
2273
+ "description": "Deprecated alias for routing_table (RouterOS v6 name)",
2259
2274
  "type": "string"
2260
2275
  },
2261
2276
  "comment": {
@@ -12830,7 +12845,7 @@
12830
12845
  "destructiveHint": false,
12831
12846
  "openWorldHint": false
12832
12847
  },
12833
- "description": "Create an outbound SSTP client tunnel interface (`/interface sstp-client add`) so this router dials out to a remote SSTP server over TLS. Use when this device must act as a VPN client, not the VPN server — for server-side settings use `set_sstp_server`. `connect_to` is the remote host:port or IP address. For L2TP outbound tunnels use `create_l2tp_client`, for OpenVPN use `create_ovpn_client`, for PPTP use `create_pptp_client`. Credentials are accepted but redacted from return values. Returns the created interface detail (name, status, remote address); use the interface `name` with `get_sstp_client` or `remove_sstp_client`.",
12848
+ "description": "Create an outbound SSTP client tunnel interface (`/interface sstp-client add`) so this router dials out to a remote SSTP server over TLS. Use when this device must act as a VPN client, not the VPN server — for server-side settings use `set_sstp_server`. `connect_to` is the remote server address (IP or DNS name); the TCP port is separate. You may pass the port in `port` or inline as `host:port` in `connect_to` (it is split out automatically — RouterOS rejects a port embedded in connect-to). SSTP defaults to 443. For L2TP outbound tunnels use `create_l2tp_client`, for OpenVPN use `create_ovpn_client`, for PPTP use `create_pptp_client`. Credentials are accepted but redacted from return values. Returns the created interface detail (name, status, remote address); use the interface `name` with `get_sstp_client` or `remove_sstp_client`.",
12834
12849
  "inputSchema": {
12835
12850
  "$schema": "https://json-schema.org/draft/2020-12/schema",
12836
12851
  "type": "object",
@@ -12841,7 +12856,13 @@
12841
12856
  },
12842
12857
  "connect_to": {
12843
12858
  "type": "string",
12844
- "description": "Remote SSTP server address (host:port or IP)"
12859
+ "description": "Remote SSTP server address (IP or DNS name; host:port also accepted)"
12860
+ },
12861
+ "port": {
12862
+ "description": "TCP port (default 443 if omitted)",
12863
+ "type": "integer",
12864
+ "minimum": -9007199254740991,
12865
+ "maximum": 9007199254740991
12845
12866
  },
12846
12867
  "user": {
12847
12868
  "type": "string"
@@ -19814,6 +19835,150 @@
19814
19835
  "additionalProperties": false
19815
19836
  }
19816
19837
  },
19838
+ {
19839
+ "name": "create_local_backup",
19840
+ "title": "Create Local Config Backup (host vault)",
19841
+ "risk": "write",
19842
+ "annotations": {
19843
+ "destructiveHint": false,
19844
+ "openWorldHint": false
19845
+ },
19846
+ "description": "Captures the device's full configuration with `/export` and saves it as a timestamped plain-text `.rsc` file in the MCP server's LOCAL backup vault (default `~/.mikrotik-mcp/backups/`, override with the `MIKROTIK_BACKUP_DIR` env var) — NOT on the device and NOT in S3. The filename is `<device>_<date_time>.rsc` stamped in the device's local clock (24-hour; Jalali for the Tehran timezone, Gregorian otherwise). This is a host-side, human-readable, diffable copy you can restore later with restore_local_backup. Compare: create_backup makes a binary `/system backup` file ON the device; capture_config_snapshot stores an export in the local snapshot database; upload_backup_to_s3 pushes to S3. show_sensitive=true includes secrets (keys/passwords) in the export. Returns the saved filename, byte size and vault path.",
19847
+ "inputSchema": {
19848
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
19849
+ "type": "object",
19850
+ "properties": {
19851
+ "label": {
19852
+ "description": "Optional label appended to the filename, e.g. \"pre-upgrade\".",
19853
+ "type": "string"
19854
+ },
19855
+ "show_sensitive": {
19856
+ "default": false,
19857
+ "description": "Include secrets (keys/passwords) in the export. Default false.",
19858
+ "type": "boolean"
19859
+ }
19860
+ },
19861
+ "required": ["show_sensitive"],
19862
+ "additionalProperties": false
19863
+ }
19864
+ },
19865
+ {
19866
+ "name": "list_local_backups",
19867
+ "title": "List Local Config Backups (host vault)",
19868
+ "risk": "read",
19869
+ "annotations": {
19870
+ "readOnlyHint": true,
19871
+ "idempotentHint": true,
19872
+ "openWorldHint": false
19873
+ },
19874
+ "description": "Lists the `/export` config backups stored in the MCP server's LOCAL backup vault (`~/.mikrotik-mcp/backups/` or `MIKROTIK_BACKUP_DIR`) — each with its filename, source device, byte size and capture time, newest first. These are host-side text backups created by create_local_backup, distinct from list_backups (binary `.backup` files ON the device) and list_s3_backups (objects in an S3 bucket). Use a returned filename with get_local_backup, restore_local_backup, rename_local_backup or delete_local_backup.",
19875
+ "inputSchema": {
19876
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
19877
+ "type": "object",
19878
+ "properties": {},
19879
+ "additionalProperties": false
19880
+ }
19881
+ },
19882
+ {
19883
+ "name": "get_local_backup",
19884
+ "title": "Get Local Config Backup Contents (host vault)",
19885
+ "risk": "read",
19886
+ "annotations": {
19887
+ "readOnlyHint": true,
19888
+ "idempotentHint": true,
19889
+ "openWorldHint": false
19890
+ },
19891
+ "description": "Returns the full `/export` `.rsc` text of one backup in the MCP server's LOCAL vault, by filename (get the name from list_local_backups). This is the exact configuration script that restore_local_backup would replay onto a device — useful for reviewing or diffing a saved config before restoring. Returns a 'not found' message if the filename doesn't exist. `name` is the exact filename from list_local_backups.",
19892
+ "inputSchema": {
19893
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
19894
+ "type": "object",
19895
+ "properties": {
19896
+ "name": {
19897
+ "type": "string",
19898
+ "description": "Backup filename, e.g. 'edge_2026-06-25_1430.rsc'."
19899
+ }
19900
+ },
19901
+ "required": ["name"],
19902
+ "additionalProperties": false
19903
+ }
19904
+ },
19905
+ {
19906
+ "name": "rename_local_backup",
19907
+ "title": "Rename Local Config Backup (host vault)",
19908
+ "risk": "write-idempotent",
19909
+ "annotations": {
19910
+ "destructiveHint": false,
19911
+ "idempotentHint": true,
19912
+ "openWorldHint": false
19913
+ },
19914
+ "description": "Renames a backup file in the MCP server's LOCAL vault (e.g. to label it 'pre-upgrade' or 'golden'). Forces an `.rsc` extension and refuses if the target name already exists. Host-side only — it does NOT touch the device or S3. `name` is the current filename (from list_local_backups); `new_name` is the desired filename. Returns the new filename.",
19915
+ "inputSchema": {
19916
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
19917
+ "type": "object",
19918
+ "properties": {
19919
+ "name": {
19920
+ "type": "string",
19921
+ "description": "Current backup filename."
19922
+ },
19923
+ "new_name": {
19924
+ "type": "string",
19925
+ "description": "Desired filename (an `.rsc` suffix is added if missing)."
19926
+ }
19927
+ },
19928
+ "required": ["name", "new_name"],
19929
+ "additionalProperties": false
19930
+ }
19931
+ },
19932
+ {
19933
+ "name": "delete_local_backup",
19934
+ "title": "Delete Local Config Backup (host vault)",
19935
+ "risk": "destructive",
19936
+ "annotations": {
19937
+ "destructiveHint": true,
19938
+ "idempotentHint": true,
19939
+ "openWorldHint": false
19940
+ },
19941
+ "description": "Permanently deletes one backup file from the MCP server's LOCAL vault by filename. This removes only the host-side copy — it does NOT touch the device's configuration, its on-device `.backup` files, or any S3 object. Use list_local_backups to find the name; for deleting an S3 object use delete_s3_backup instead. Returns a 'not found' message if the filename doesn't exist.",
19942
+ "inputSchema": {
19943
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
19944
+ "type": "object",
19945
+ "properties": {
19946
+ "name": {
19947
+ "type": "string",
19948
+ "description": "Backup filename to delete."
19949
+ }
19950
+ },
19951
+ "required": ["name"],
19952
+ "additionalProperties": false
19953
+ }
19954
+ },
19955
+ {
19956
+ "name": "restore_local_backup",
19957
+ "title": "Restore Local Config Backup to Device (Safe Mode)",
19958
+ "risk": "dangerous",
19959
+ "annotations": {
19960
+ "destructiveHint": true,
19961
+ "openWorldHint": false
19962
+ },
19963
+ "description": "Replays a LOCAL vault backup's `/export` configuration onto a device INSIDE RouterOS Safe Mode — every command is held in memory and AUTO-REVERTS if the SSH session drops, so a change that would lock you out is undone instead of sticking. With confirm=false (default) it applies the whole config, then rolls it all back — a true dry-run that proves the restore runs cleanly. With confirm=true it commits, but ONLY after verifying the device still answers. Because a `/export` is additive, restoring onto an already-configured device LAYERS the saved config on top (duplicate rules are possible) — it is cleanest on a freshly reset device. Requires SSH (Safe Mode is not available on MAC-Telnet devices). Alternatives: import_configuration runs a `.rsc` already ON the device; restore_backup loads a binary `.backup` (and reboots). `name` is the filename from list_local_backups.",
19964
+ "inputSchema": {
19965
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
19966
+ "type": "object",
19967
+ "properties": {
19968
+ "name": {
19969
+ "type": "string",
19970
+ "description": "Backup filename to restore (from list_local_backups)."
19971
+ },
19972
+ "confirm": {
19973
+ "default": false,
19974
+ "description": "false = apply, then roll back (dry-run). true = commit if still reachable.",
19975
+ "type": "boolean"
19976
+ }
19977
+ },
19978
+ "required": ["name", "confirm"],
19979
+ "additionalProperties": false
19980
+ }
19981
+ },
19817
19982
  {
19818
19983
  "name": "list_disks",
19819
19984
  "title": "List All Attached Disks",
@@ -26,7 +26,12 @@
26
26
  "minimum": -9007199254740991,
27
27
  "maximum": 9007199254740991
28
28
  },
29
+ "routing_table": {
30
+ "description": "Policy-routing table name, e.g. \"main\" or a custom table (RouterOS v7)",
31
+ "type": "string"
32
+ },
29
33
  "routing_mark": {
34
+ "description": "Deprecated alias for routing_table (RouterOS v6 name)",
30
35
  "type": "string"
31
36
  },
32
37
  "comment": {