@kedem/okdb 1.9.2 → 2.0.1

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.
Files changed (49) hide show
  1. package/README.md +3 -2
  2. package/algorithm-worker-thread.js +1 -0
  3. package/bin/okdb.js +1 -1
  4. package/docs/deployment.md +155 -0
  5. package/docs/diagnostics.md +115 -0
  6. package/docs/embeddings.md +1 -1
  7. package/docs/functions.md +30 -27
  8. package/docs/getting-started.md +14 -3
  9. package/docs/http-api.md +3 -3
  10. package/docs/http-cluster.md +93 -60
  11. package/docs/index.md +13 -1
  12. package/docs/manifest.json +6 -3
  13. package/docs/pipelines.md +1 -1
  14. package/docs/process-registry.md +5 -5
  15. package/docs/processors.md +35 -18
  16. package/docs/queue.md +112 -42
  17. package/docs/sync.md +10 -7
  18. package/docs/ttl.md +1 -1
  19. package/docs/upgrade-2.0.md +290 -0
  20. package/okdb-functions-sandbox-worker.js +1 -1
  21. package/okdb-http-worker-child.js +1 -1
  22. package/okdb-views-bootstrap-worker.js +1 -1
  23. package/okdb.js +1 -1
  24. package/package.json +1 -1
  25. package/public/layouts/_default.ok.html +1 -1
  26. package/public/sections/db/modals/create-env-modal.ok.js +1 -1
  27. package/public/sections/db/modals/demo-env-modal.ok.js +1 -1
  28. package/public/sections/db/parts/db-overview.ok.js +1 -1
  29. package/public/sections/engines/engine-ui-utils.js +1 -1
  30. package/public/sections/engines/parts/engine-declaration-editor.ok.js +1 -1
  31. package/public/sections/engines/parts/generic-engine-panel.ok.js +1 -1
  32. package/public/sections/queue/parts/code-panel.ok.js +1 -1
  33. package/public/sections/queue/parts/job-log-panel.ok.js +1 -1
  34. package/public/sections/queue/parts/queue-jobs.ok.js +1 -1
  35. package/public/sections/sync/parts/sync-topology.ok.js +1 -1
  36. package/public/sections/system/index.ok.html +1 -1
  37. package/public/sections/system/parts/system-process-panel.ok.js +1 -1
  38. package/public/sections/system/parts/system-processing-panel.ok.js +1 -1
  39. package/public/sections/system/parts/system-runtime-overview.ok.js +1 -1
  40. package/public/setup-app.js +1 -1
  41. package/public/setup.html +116 -55
  42. package/types/index.d.ts +33 -3
  43. package/types/options.d.ts +19 -10
  44. package/docs/worker-fleet.md +0 -139
  45. package/okdb-functions-runner-child.js +0 -1
  46. package/okdb-queue-load-handler.js +0 -1
  47. package/okdb-queue-spawn-child.js +0 -1
  48. package/okdb-worker-child.js +0 -1
  49. package/public/sections/system/parts/system-workers-panel.ok.js +0 -1
@@ -1 +1 @@
1
- <script type="module">import e from"../db/modals/create-env-modal.ok.js";import t from"../db/modals/demo-env-modal.ok.js";import o from"../db/modals/remove-env-modal.ok.js";import s from"./modals/data-ops-modal.ok.js";import n from"./parts/system-log.ok.js";import a from"./parts/system-log-history.ok.js";import r from"./parts/system-sync-panel.ok.js";import i from"./parts/system-auth-panel.ok.js";import c from"./parts/system-api-panel.ok.js";import l from"./parts/system-locks-panel.ok.js";import d from"./parts/system-processing-panel.ok.js";import m from"./parts/system-process-panel.ok.js";import h from"./parts/system-workers-panel.ok.js";import p from"./parts/system-runtime-overview.ok.js";import g from"../license/parts/license-panel.ok.js";import u from"@toolkit/components/ok-doc-viewer.ok.js";export default{tag:"system-index",unwrap:!0,dependencies:[n,a,r,i,c,l,d,m,h,p,g,u,e,t,o,s],context:e=>({_systemWildcard(){const t=e.$vars.$router?.match("/system/*");return t?.params?.["*"]??""},get subView(){return this._parseSubView(this._systemWildcard())},get isRuntime(){const e=this.subView;return"runtime"===e||"processes"===e||"workers"===e||"processing"===e},get runtimeTab(){const e=this.subView;return"runtime"===e?"overview":e},_parseSubView(e){const t=e??"";return t&&"overview"!==t?"logs"===t?"logs":"events"===t?"events":"sync"===t?"sync":"processing"===t?"processing":"processes"===t?"processes":"workers"===t?"workers":"runtime"===t?"runtime":"api"===t?"api":"auth"===t?"auth":"license"===t?"license":t.startsWith("docs")?"docs":"overview":"overview"},goToSystemSub(t){const o=e.$vars.$router;if(!o)return;const s=e.$ok.stores.okdb.selectedEnv,n=s?`?env=${encodeURIComponent(s)}`:"";o.push(`/system/${t}${n}`)},docsManifest:[],docsAnchor:"",get docsCurrentFile(){return this._parseDocsFile(this._systemWildcard())},_parseDocsFile(e){const t=e??"",o=t.startsWith("docs/")?t.slice(5):"";return o?o.replace(/\.md$/,"")+".md":"index.md"},docsIsActive(e){return this.docsCurrentFile===e},docsNavigateTo(e){const t=e.replace(/\.md$/,"");this.docsAnchor="",this.goToSystemSub("docs/"+t)},docsOnAnchorChange(e){this.docsAnchor=e},async loadDocsManifest(){if(!this.docsManifest.length)try{const e=await fetch("/docs/manifest.json");e.ok&&(this.docsManifest=await e.json())}catch{}},info:null,runtime:null,syncInfo:null,envs:[],loading:!1,error:null,autoRefresh:!0,_timer:null,metrics:null,async load(){this.loading=!0,this.error=null;try{const e=await fetch(`${__ENV__.API}/info`),t=await e.json();if(!e.ok)throw new Error(t?.error?.message||"Failed to load info");this.info=t.data??t}catch(e){this.error=e?.message||String(e)}finally{this.loading=!1}try{const t=await fetch(`${__ENV__.API}/envs`);if(t.ok){const o=await t.json();this.envs=o.data??[],e.$ok.stores.okdb.envs=this.envs}}catch{}try{const e=await fetch(`${__ENV__.API}/system/runtime`);if(e.ok){const t=await e.json();this.runtime=t.data??t}}catch{}try{const e=await fetch(`${__ENV__.API}/system/metrics`);if(e.ok){const t=await e.json();this.metrics=t.data??null}}catch{}try{const e=await fetch(`${__ENV__.API}/sync/info`);this.syncInfo=e.ok?(await e.json()).data??null:null}catch{this.syncInfo=null}},get approxSize(){const e=(this.envs||[]).reduce((e,t)=>e+(t.size||0),0);if(!e)return"--";const t=["B","KB","MB","GB","TB"];let o=e,s=0;for(;o>=1024&&s<t.length-1;)o/=1024,s++;return o.toFixed(2)+" "+t[s]},get typeList(){const e=[];for(const t of this.envs||[])for(const o of t.types||[])o.name.startsWith("~")||e.push({name:o.name,env:t.name,count:o.count??0});return e.sort((e,t)=>e.name.localeCompare(t.name)||e.env.localeCompare(t.env))},get systemTypeList(){const e=[];for(const t of this.envs||[])for(const o of t.types||[])o.name.startsWith("~")&&e.push({name:o.name,env:t.name,count:o.count??0});return e.sort((e,t)=>e.name.localeCompare(t.name)||e.env.localeCompare(t.env))},get totalTypeCount(){return this.typeList.length},get plugins(){return(this.info?.plugins||[]).filter(Boolean)},get config(){return this.info?.config??null},get durabilityLabel(){const e=this.config?.durability;return{strict:{label:"Strict",color:"var(--okdb-success)",hint:"Full fsync on every commit."},balanced:{label:"Balanced",color:"var(--okdb-accent)",hint:"Async fsync worker — crash-safe, fast."},fast:{label:"Fast",color:"var(--okdb-warning)",hint:"No per-commit fsync. May lose last tx on crash."},custom:{label:"Custom",color:"var(--okdb-purple)",hint:"Manual LMDB flags. No preset applied."}}[e]??{label:e??"--",color:"var(--okdb-app-fg-muted)",hint:""}},get configWarnings(){return this.config?.warnings??[]},formatBytes(e){if(null==e)return"--";const t=["B","KB","MB","GB","TB"];let o=e,s=0;for(;o>=1024&&s<t.length-1;)o/=1024,s++;return o.toFixed(0===s?0:2)+" "+t[s]},formatUptime(e){if(null==e)return"--";const t=Math.floor(e),o=Math.floor(t/86400),s=Math.floor(t%86400/3600),n=Math.floor(t%3600/60),a=t%60;return o>0?`${o}d ${s}h ${n}m`:s>0?`${s}h ${n}m ${a}s`:n>0?`${n}m ${a}s`:`${a}s`},formatLoad:e=>e&&e.length?e.map(e=>e.toFixed(2)).join(" / "):"--",processDetail:e=>e?.meta?.env?e.meta.queueType?`${e.meta.env} · ${e.meta.queueType}`:e.meta.env:e?.stats?.threads?.count?`${e.stats.threads.count} worker thread${1!==e.stats.threads.count?"s":""}`:"",pct:(e,t)=>e&&t?Math.round(e/t*100):0,get clock(){return this.syncInfo?.clock??this.info?.clock??"--"},get nodeId(){return this.syncInfo?.node?.id??this.info?.id??"--"},get nodeAddress(){return this.syncInfo?.node?.address??null},formatNum:e=>null==e?"--":Number(e).toLocaleString(),copyId(){navigator.clipboard?.writeText(this.nodeId).catch(()=>{})},changelogOpen:!1,changelogBusy:!1,changelogError:null,changes:[],changelogLimit:50,async loadChangelog(){this.changelogBusy=!0,this.changelogError=null;try{const e=new URLSearchParams({limit:this.changelogLimit}),t=await fetch(`${__ENV__.API}/system/changelog?${e}`),o=await t.json();if(!t.ok)throw new Error(o?.error?.message||"Failed");this.changes=o.data??[]}catch(e){this.changelogError=e?.message||String(e)}finally{this.changelogBusy=!1}},get changelogHasOlder(){return this.changes.length===this.changelogLimit},loadMoreChangelog(){this.changelogLimit=Math.min(this.changelogLimit+100,1e3),this.loadChangelog()},toggleChangelog(){this.changelogOpen=!this.changelogOpen,this.changelogOpen&&!this.changes.length&&this.loadChangelog()},actionColor:e=>({put:"var(--okdb-success)",remove:"var(--okdb-danger)",type_register:"var(--okdb-accent)",type_drop:"var(--okdb-danger)",index_register:"var(--okdb-purple)",index_drop:"var(--okdb-warning)"}[e]||"var(--okdb-app-fg-muted)"),actionLabel:e=>(e||"").replace(/_/g," ").toUpperCase(),shortKey:e=>e?e.length<=20?e:e.slice(0,10)+"…"+e.slice(-6):"—",relTime(e){if(!e)return"—";const t=Date.now()-e;return t<5e3?"just now":t<6e4?`${Math.round(t/1e3)}s ago`:t<36e5?`${Math.round(t/6e4)}m ago`:t<864e5?`${Math.round(t/36e5)}h ago`:new Date(e).toLocaleDateString()},showSystemEnvs:!1,get totalEnvSize(){return(this.envs||[]).reduce((e,t)=>e+(t.size||0),0)},get userEnvs(){return(this.envs||[]).filter(e=>!e.name.startsWith("~"))},get systemEnvs(){return(this.envs||[]).filter(e=>e.name.startsWith("~"))},get visibleEnvs(){return this.showSystemEnvs?this.envs:this.userEnvs},get orphanedEnvs(){return(this.envs||[]).filter(e=>e.orphaned)},goToEnv(t){e.$ok.stores.okdb.selectEnv(t),e.$vars.$router?.push?.("/db?env="+encodeURIComponent(t))},async openCreateEnv(){await e.$ok.shared.modal.open({owner:e,title:"Create Environment",content:'<create-env-modal :modal="modal" :on-done="onDone" />',context:t=>({modal:t,onDone:async()=>{await e.$ok.stores.okdb.reloadEnvs(),await this.load()}})})},async openDemoModal(){await e.$ok.shared.modal.open({owner:e,title:'<span style="background:linear-gradient(135deg,var(--okdb-accent),var(--okdb-purple) 50%,var(--okdb-success));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;font-weight:700;font-size:15px;">✦ Demo Environment</span>',canDrag:!0,content:'<demo-env-modal :modal="modal" :on-done="onDone" />',context:t=>({modal:t,onDone:async t=>{e.$ok.stores.okdb.selectEnv(t),await e.$ok.stores.okdb.reloadEnvs(),await this.load(),e.$ok.shared?.toast?.success?.(`✦ Demo environment "${t}" is ready!`)}})})},_compactingEnvs:{},async compactEnv(t){if(!this._compactingEnvs[t]){this._compactingEnvs={...this._compactingEnvs,[t]:!0};try{const o=await fetch(`${__ENV__.API}/env/${encodeURIComponent(t)}/compact`,{method:"POST"}),s=await o.json();if(!o.ok)throw new Error(s?.error?.message||"Compact failed");const n=s.data??{},a=n.saved>0?`✓ Compacted "${t}": ${this.formatBytes(n.sizeBefore)} → ${this.formatBytes(n.sizeAfter)} (saved ${this.formatBytes(n.saved)}, ${n.savedPct}%)`:`✓ Compacted "${t}" — no space reclaimed (${this.formatBytes(n.sizeAfter)})`;e.$ok.shared?.toast?.success?.(a),await this.load()}catch(t){e.$ok.shared?.toast?.error?.("Compact failed: "+(t?.message||t))}finally{const e={...this._compactingEnvs};delete e[t],this._compactingEnvs=e}}},async openRemoveEnv(t){await e.$ok.shared.modal.open({owner:e,title:"Remove Environment",content:'<remove-env-modal :modal="modal" :env-name="envName" :on-done="onDone" />',context:o=>({modal:o,envName:t,onDone:async(t,o)=>{await e.$ok.stores.okdb.reloadEnvs(),e.$ok.stores.okdb.selectedEnv===t&&e.$ok.stores.okdb.selectEnv(e.$ok.stores.okdb._pickBestEnv()),await this.load(),o?.filesLocked?.length?e.$ok.shared?.toast?.warn?.(`"${t}" removed — some files could not be deleted (locked)`):e.$ok.shared?.toast?.success?.(`Environment "${t}" removed and files deleted`)}})})},async dismissOrphan(t){try{const o=await fetch(`${__ENV__.API}/env/${encodeURIComponent(t)}`,{method:"DELETE"}),s=await o.json().catch(()=>({}));if(!o.ok)throw new Error(s?.error?.message||o.statusText);const n=s.data??s;await e.$ok.stores.okdb.reloadEnvs(),await this.load(),n?.filesLocked?.length?e.$ok.shared?.toast?.warn?.(`Orphan "${t}" dismissed — some files could not be deleted (locked)`):e.$ok.shared?.toast?.info?.(`Orphan "${t}" dismissed`)}catch(t){e.$ok.shared?.toast?.error?.("Dismiss failed: "+(t?.message||t))}},async openDataOps(t){await e.$ok.shared.modal.open({owner:e,title:"⬦ Data Operations",canDrag:!0,content:'<data-ops-modal :modal="modal" :envs="envs" :on-done="onDone" :initial-tab="initialTab" />',context:o=>({modal:o,envs:this.envs,initialTab:t||"export",onDone:async()=>{await this.load(),e.$ok.shared?.toast?.success?.("Operation complete")}})})},logView:"live"}),async mount(e){await this.load(),this._timer=setInterval(()=>{this.autoRefresh&&"overview"===this.subView&&this.load()},1e4);const t=()=>{fetch(`${__ENV__.API}/envs`).then(e=>e.json()).then(t=>{this.envs=t.data??[],e.$ok.stores.okdb.envs=this.envs}).catch(()=>{})},o=e.$ok.stores.events?.on?.("env:opened",t),s=e.$ok.stores.events?.on?.("env:removed",t);this._unsubEnv=()=>{o?.(),s?.()},await this.loadDocsManifest()},unmount(){clearInterval(this._timer),this._unsubEnv?.()}};</script><template><component import="../../layouts/_default.ok.html"><template name="header"></template><template name="sidebar"><if :="subView === 'docs'"><nav class="sys-subnav" style="margin-bottom:6px"><button class="sys-subnav__item" @click="goToSystemSub('overview')" style="opacity:.6;font-size:11px">← System</button></nav><nav class="docs-nav" aria-label="Documentation"><each :of="docsManifest" let:item="item"><if :="item.file"><button class="docs-nav__item docs-nav__item--standalone" :class="{'docs-nav__item--active': docsIsActive(item.file)}" @click="docsNavigateTo(item.file)">{{ item.label }}</button></if><if :="item.children"><div class="docs-nav__group"><span class="docs-nav__group-label">{{ item.label }}</span><each :of="item.children" let:child="child"><button class="docs-nav__item" :class="{'docs-nav__item--active': docsIsActive(child.file)}" @click="docsNavigateTo(child.file)">{{ child.label }}</button></each></div></if></each></nav></if><if :="subView !== 'docs'"><nav class="sys-subnav" aria-label="System navigation"><button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='overview'}" @click="goToSystemSub('overview')"><span class="sys-subnav__icon">⚙</span> Overview</button> <button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='logs'}" @click="goToSystemSub('logs')"><span class="sys-subnav__icon">📋</span> Logs</button> <button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='events'}" @click="goToSystemSub('events')"><span class="sys-subnav__icon">⚡</span> Events</button><div class="sys-subnav__divider"></div><button class="sys-subnav__item" :class="{'sys-subnav__item--active': isRuntime}" @click="goToSystemSub('runtime')"><span class="sys-subnav__icon">⊞</span> Runtime</button> <button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='sync'}" @click="goToSystemSub('sync')"><span class="sys-subnav__icon">⇄</span> Sync</button> <button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='auth'}" @click="goToSystemSub('auth')"><span class="sys-subnav__icon">🔐</span> Auth</button> <button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='license'}" @click="goToSystemSub('license')"><span class="sys-subnav__icon">🔑</span> License</button><div class="sys-subnav__divider"></div><button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='api'}" @click="goToSystemSub('api')"><span class="sys-subnav__icon">⌁</span> API</button> <button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='docs'}" @click="goToSystemSub('docs')"><span class="sys-subnav__icon">≡</span> Docs</button></nav></if></template><template name="default"><if :="subView === 'logs'"><div style="display:flex;flex-direction:column;gap:12px;height:100%;min-height:0"><div style="display:flex;align-items:center;gap:8px;flex-shrink:0"><div style="font-weight:600;font-size:15px;opacity:.8">Logs</div><div style="display:flex;gap:2px;padding:2px;background:var(--okdb-app-surface);border-radius:8px;border:1px solid var(--okdb-app-border)"><button @click="logView='live'" :style="logView==='live' ?'background:var(--okdb-app-surface-2);color:var(--okdb-accent);border-color:color-mix(in srgb, var(--okdb-accent) 27%, transparent);':'background:transparent;border-color:transparent;opacity:.7;'" style="padding:4px 14px;border-radius:6px;font-size:12px">Live</button> <button @click="logView='history'" :style="logView==='history' ?'background:var(--okdb-app-surface-2);color:var(--okdb-accent);border-color:color-mix(in srgb, var(--okdb-accent) 27%, transparent);':'background:transparent;border-color:transparent;opacity:.7;'" style="padding:4px 14px;border-radius:6px;font-size:12px">History</button></div></div><if :="logView === 'live'"><system-log :initial-source="'log'" style="flex:1;min-height:300px"/></if><if :="logView === 'history'"><system-log-history style="flex:1;min-height:300px"/></if></div></if><if :="subView === 'events'"><div style="display:flex;flex-direction:column;gap:12px;height:100%;min-height:0"><div style="font-weight:600;font-size:15px;opacity:.8;flex-shrink:0">Events</div><system-log :initial-source="'event'" style="flex:1;min-height:300px"/></div></if><if :="isRuntime"><div style="display:flex;flex-direction:column;gap:14px;height:100%;min-height:0"><nav style="display:flex;gap:4px;border-bottom:1px solid var(--okdb-app-border);padding-bottom:0"><button @click="goToSystemSub('runtime')" :style="'font-size:12px;padding:6px 12px;background:none;border:none;border-bottom:2px solid ' + (runtimeTab==='overview' ? 'var(--okdb-accent)' : 'transparent') + ';cursor:pointer;color:' + (runtimeTab==='overview' ? 'var(--okdb-app-fg-strong)' : 'var(--okdb-app-fg-subtle)') + ';'">Overview</button> <button @click="goToSystemSub('processes')" :style="'font-size:12px;padding:6px 12px;background:none;border:none;border-bottom:2px solid ' + (runtimeTab==='processes' ? 'var(--okdb-accent)' : 'transparent') + ';cursor:pointer;color:' + (runtimeTab==='processes' ? 'var(--okdb-app-fg-strong)' : 'var(--okdb-app-fg-subtle)') + ';'">Processes</button> <button @click="goToSystemSub('workers')" :style="'font-size:12px;padding:6px 12px;background:none;border:none;border-bottom:2px solid ' + (runtimeTab==='workers' ? 'var(--okdb-accent)' : 'transparent') + ';cursor:pointer;color:' + (runtimeTab==='workers' ? 'var(--okdb-app-fg-strong)' : 'var(--okdb-app-fg-subtle)') + ';'">Workers</button> <button @click="goToSystemSub('processing')" :style="'font-size:12px;padding:6px 12px;background:none;border:none;border-bottom:2px solid ' + (runtimeTab==='processing' ? 'var(--okdb-accent)' : 'transparent') + ';cursor:pointer;color:' + (runtimeTab==='processing' ? 'var(--okdb-app-fg-strong)' : 'var(--okdb-app-fg-subtle)') + ';'">Processors</button></nav><div style="flex:1;min-height:0;overflow:auto"><if :="runtimeTab === 'overview'"><system-runtime-overview/></if><if :="runtimeTab === 'processes'"><system-process-panel style="height:100%;min-height:0"/></if><if :="runtimeTab === 'workers'"><system-workers-panel/></if><if :="runtimeTab === 'processing'"><system-processing-panel/></if></div></div></if><if :="subView === 'sync'"><system-sync-panel style="height:100%;min-height:0"/></if><if :="subView === 'auth'"><system-auth-panel style="height:100%;min-height:0"/></if><if :="subView === 'api'"><system-api-panel style="height:100%;min-height:0"/></if><if :="subView === 'docs'"><div class="docs-wrap"><ok-doc-viewer :blocks="[]" root="/docs/" prefix="#/system/docs/" :file="docsCurrentFile" :anchor="docsAnchor" :manifest="docsManifest" sidebar-title="On this page" @anchor-change="docsOnAnchorChange($event.detail)"/></div></if><if :="subView === 'overview'"><div style="display:flex;flex-direction:column;gap:20px;max-width:900px"><div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap"><div style="font-weight:600;font-size:16px">Overview</div><button @click="load()" :disabled="loading" style="opacity:.8;font-size:12px">↻ Refresh</button> <label style="display:flex;align-items:center;gap:6px;font-size:12px;opacity:.7;cursor:pointer"><input type="checkbox" :checked="autoRefresh" @change="autoRefresh = $el.checked"/> Auto-refresh 10s</label><if :="loading"><span style="font-size:12px;opacity:.55">Loading…</span></if></div><if :="error"><div style="color:var(--okdb-danger);font-size:13px;padding:10px;border:1px solid var(--okdb-danger);border-radius:8px">{{error}}</div></if><div style="display:grid;grid-template-columns:1fr 1fr;gap:16px"><div class="card"><div class="card-title">Node Identity</div><div class="kv-grid"><div class="kv-key">Node ID</div><div class="kv-val" style="display:flex;align-items:center;gap:6px"><span style="font-family:monospace;font-size:12px;word-break:break-all">{{nodeId}}</span> <button @click="copyId()" style="font-size:10px;padding:2px 6px;opacity:.7">copy</button></div><if :="nodeAddress"><div class="kv-key">Address</div><div class="kv-val" style="font-family:monospace;font-size:12px">{{nodeAddress}}</div></if><div class="kv-key">Clock</div><div class="kv-val" style="font-family:monospace">{{formatNum(clock)}}</div><div class="kv-key">DB State</div><div class="kv-val" style="color:var(--okdb-success)">open</div><div class="kv-key">OKDB</div><div class="kv-val" style="font-family:monospace">v{{info?.versions?.okdb ?? info?.version ?? '--'}}</div><div class="kv-key">lmdb-js</div><div class="kv-val" style="font-family:monospace">v{{info?.versions?.lmdb ?? '--'}}</div><div class="kv-key">LMDB</div><div class="kv-val" style="font-family:monospace;font-size:11px;opacity:.7">{{info?.versions?.lmdbNative ?? '--'}}</div></div></div><div class="card"><div class="card-title">Storage (all environments)</div><div class="kv-grid"><div class="kv-key">Total Size</div><div class="kv-val" style="font-size:20px;font-weight:600;color:var(--okdb-accent)">{{approxSize}}</div><div class="kv-key">Environments</div><div class="kv-val">{{(envs || []).length}}</div><div class="kv-key">User Types</div><div class="kv-val">{{totalTypeCount}}</div><div class="kv-key">Page Size</div><div class="kv-val">{{formatNum(info?.pageSize)}} B</div><div class="kv-key">Map Size</div><div class="kv-val">{{info?.config?.lmdbOptions?.mapSize ? (info.config.lmdbOptions.mapSize/1024/1024/1024).toFixed(2)+' GB' : '--'}} <span style="opacity:.4;font-size:11px">(per env)</span></div></div></div></div><if :="envs && envs.length > 0"><div class="card"><div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap"><div style="font-weight:600;font-size:14px;flex:1">Environments ({{userEnvs.length}}<if :="systemEnvs.length > 0"><span style="opacity:.4;font-size:12px;font-weight:400">+ {{systemEnvs.length}} system</span></if>)</div><span style="font-size:12px;opacity:.5">{{formatBytes(totalEnvSize)}}</span><if :="systemEnvs.length > 0"><button @click="showSystemEnvs = !showSystemEnvs" style="font-size:11px;padding:3px 8px;opacity:.6">{{showSystemEnvs ? '▴ hide system' : '⚙ show system'}}</button></if><button @click="openCreateEnv()" style="font-size:11px;padding:4px 10px;opacity:.8">+ Create</button> <button @click="openDemoModal()" style="font-size:11px;padding:4px 12px;background:linear-gradient(135deg,color-mix(in srgb,var(--okdb-accent) 7%,transparent),color-mix(in srgb,var(--okdb-purple) 7%,transparent));border:1px solid color-mix(in srgb,var(--okdb-accent) 27%,transparent);color:var(--okdb-accent);border-radius:6px;font-weight:600" title="Generate a demo environment">✦ Demo</button></div><table class="sys-table"><thead><tr><th>Name</th><th style="text-align:right">Size</th><th style="text-align:right">Clock</th><th style="text-align:right">Types</th><th style="text-align:center">Sync</th><th></th></tr></thead><tbody><tr as="repeat" :of="visibleEnvs" let:item="e" class="sys-table-row"><td style="font-family:monospace;font-size:12px"><a @click.prevent="goToEnv(e.name)" href="#" style="color:var(--okdb-accent);text-decoration:none" :title="'Explore '+e.name">{{e.name}}</a><if :="e.parentEnv"><span style="font-size:10px;opacity:.4;font-family:monospace;margin-left:4px">↳ {{e.parentEnv}}</span></if></td><td style="text-align:right;font-size:12px;font-family:monospace;opacity:.8">{{formatBytes(e.size)}}</td><td style="text-align:right;font-family:monospace;font-size:12px;opacity:.7">{{e.sync ? formatNum(e.clock) : '—'}}</td><td style="text-align:right;font-size:12px;opacity:.7">{{e.types ? e.types.length : 0}}</td><td style="text-align:center"><span :style="e.sync?'color:var(--okdb-success);font-size:11px;':'opacity:.4;font-size:11px;'">{{e.sync?'✓':'✗'}}</span></td><td style="text-align:right;white-space:nowrap"><button @click="compactEnv(e.name)" :disabled="_compactingEnvs[e.name]" :style="_compactingEnvs[e.name]?'font-size:10px;padding:2px 8px;opacity:.35;cursor:not-allowed;':'font-size:10px;padding:2px 8px;opacity:.55;'">{{_compactingEnvs[e.name]?'⏳ compacting…':'⬡ compact'}}</button><if :="e.name !== '~system'"><button @click="openRemoveEnv(e.name)" style="font-size:10px;padding:2px 8px;opacity:.45;color:var(--okdb-danger);border-color:color-mix(in srgb,var(--okdb-danger) 20%,transparent);margin-left:4px">🗑 remove</button></if></td></tr></tbody></table><if :="orphanedEnvs.length > 0"><div style="margin-top:16px;border-top:1px solid var(--okdb-app-border);padding-top:14px"><div style="font-size:12px;font-weight:600;opacity:.6;margin-bottom:8px;display:flex;align-items:center;gap:6px"><span style="color:var(--okdb-orange)">⚠</span> Orphaned sub-environments ({{orphanedEnvs.length}}) <span style="font-size:11px;font-weight:400;opacity:.55;margin-left:2px">— parent was removed; data on disk is intact</span></div><table class="sys-table"><tbody><tr as="repeat" :of="orphanedEnvs" let:item="e" class="sys-table-row"><td style="font-family:monospace;font-size:12px;opacity:.5;text-decoration:line-through">{{e.name}}</td><td style="font-size:11px;opacity:.4;font-style:italic">orphaned from <span style="font-family:monospace">{{e.orphanedFrom ?? '—'}}</span></td><td style="text-align:right;white-space:nowrap"><button @click="dismissOrphan(e.name)" style="font-size:10px;padding:2px 8px;opacity:.5;color:var(--okdb-danger);border-color:color-mix(in srgb,var(--okdb-danger) 20%,transparent)">✕ dismiss</button></td></tr></tbody></table></div></if></div></if><div class="card" style="border:1px solid color-mix(in srgb,var(--okdb-accent) 9%,transparent);background:linear-gradient(135deg,var(--okdb-app-bg-deep),var(--okdb-app-surface))"><div style="display:flex;align-items:center;gap:14px;flex-wrap:wrap"><div style="flex:1;min-width:200px"><div style="font-weight:600;font-size:14px;margin-bottom:4px;display:flex;align-items:center;gap:8px"><span style="color:var(--okdb-accent);opacity:.7">⬦</span> Data Operations</div><div style="font-size:12px;opacity:.45;line-height:1.5">Export &amp; import logical NDJSON snapshots · hot filesystem backup</div></div><div style="display:flex;gap:8px;flex-shrink:0;flex-wrap:wrap;align-items:center"><button @click="openDataOps('export')" style="display:flex;align-items:center;gap:6px;padding:7px 15px;border-radius:8px;font-size:12px;font-weight:600;background:linear-gradient(135deg,var(--okdb-app-bg-deep),var(--okdb-app-bg-deep));border:1px solid color-mix(in srgb,var(--okdb-accent) 27%,transparent);color:var(--okdb-accent);cursor:pointer;white-space:nowrap">⬇ Export</button> <button @click="openDataOps('import')" style="display:flex;align-items:center;gap:6px;padding:7px 15px;border-radius:8px;font-size:12px;font-weight:600;background:linear-gradient(135deg,var(--okdb-app-bg-deep),var(--okdb-app-bg-deep));border:1px solid color-mix(in srgb,var(--okdb-accent) 27%,transparent);color:var(--okdb-accent);cursor:pointer;white-space:nowrap">⬆ Import</button><div style="width:1px;height:24px;background:var(--okdb-app-border);margin:0 2px;flex-shrink:0"></div><button @click="openDataOps('backup')" style="display:flex;align-items:center;gap:6px;padding:7px 15px;border-radius:8px;font-size:12px;font-weight:600;background:linear-gradient(135deg,var(--okdb-app-bg-deep),var(--okdb-app-bg-deep));border:1px solid color-mix(in srgb,var(--okdb-purple) 27%,transparent);color:var(--okdb-purple);cursor:pointer;white-space:nowrap">🗄 Backup</button></div></div></div><div style="display:grid;grid-template-columns:1fr 1fr;gap:16px"><div class="card"><div class="card-title">Configuration</div><div class="kv-grid"><div class="kv-key">Durability</div><div class="kv-val" style="display:flex;align-items:center;gap:6px"><span :style="'font-size:11px;font-weight:600;padding:2px 8px;border-radius:999px;border:1px solid;color:'+durabilityLabel.color+';border-color:'+durabilityLabel.color+'44;background:'+durabilityLabel.color+'11;'">{{durabilityLabel.label}}</span> <span style="opacity:.5;font-size:11px;flex:1">{{durabilityLabel.hint}}</span></div><div class="kv-key">Compression</div><div class="kv-val"><span :style="config?.lmdbOptions?.compression?'color:var(--okdb-success)':'opacity:.5'">{{config?.lmdbOptions?.compression?'LZ4 enabled':'disabled'}}</span></div><div class="kv-key">Encryption</div><div class="kv-val"><span :style="config?.lmdbOptions?.encryptionKey?'color:var(--okdb-success)':'opacity:.5'">{{config?.lmdbOptions?.encryptionKey?'Chacha8 enabled':'none'}}</span></div><div class="kv-key">Map Size</div><div class="kv-val">{{formatBytes(config?.lmdbOptions?.mapSize)}}</div><div class="kv-key">Max DBs</div><div class="kv-val">{{formatNum(config?.lmdbOptions?.maxDbs)}}</div></div><if :="configWarnings.length > 0"><div style="margin-top:10px;display:flex;flex-direction:column;gap:4px"><for :="configWarnings" let:w><div style="font-size:11px;color:var(--okdb-warning);background:color-mix(in srgb,var(--okdb-warning) 7%,transparent);border:1px solid color-mix(in srgb,var(--okdb-warning) 20%,transparent);border-radius:6px;padding:5px 8px;line-height:1.4">⚠ {{w}}</div></for></div></if></div><div class="card"><div class="card-title">Process <span style="font-size:11px;opacity:.5;font-weight:400">this responder only{{runtime?.processes ? ' — all processes below' : ''}}</span></div><if :="!runtime"><div style="opacity:.4;font-size:12px">Loading…</div></if><if :="runtime"><div class="kv-grid"><div class="kv-key">PID</div><div class="kv-val" style="font-family:monospace">{{runtime.process?.pid}}</div><div class="kv-key">Uptime</div><div class="kv-val">{{formatUptime(runtime.process?.uptime)}}</div><div class="kv-key">Node.js</div><div class="kv-val" style="font-family:monospace">v{{runtime.process?.node}}</div><div class="kv-key" style="align-self:center">CPU</div><div class="kv-val" style="display:flex;align-items:center;gap:6px"><div style="flex:1;height:6px;background:var(--okdb-app-border);border-radius:3px;overflow:hidden"><div :style="'height:100%;border-radius:3px;transition:width .4s;background:'+(runtime.process?.cpu?.pct>80?'var(--okdb-danger)':runtime.process?.cpu?.pct>40?'var(--okdb-warning)':'var(--okdb-success)')+';width:'+(runtime.process?.cpu?.pct??0)+'%;'"></div></div><span style="font-size:11px;opacity:.6;min-width:30px">{{runtime.process?.cpu?.pct??0}}%</span></div><div class="kv-key">RSS</div><div class="kv-val">{{formatBytes(runtime.process?.memory?.rss)}}</div><div class="kv-key">Heap Used</div><div class="kv-val">{{formatBytes(runtime.process?.memory?.heapUsed)}} <span style="opacity:.45;font-size:11px">/ {{formatBytes(runtime.process?.memory?.heapTotal)}}</span></div><div class="kv-key" style="align-self:center">Heap %</div><div class="kv-val" style="display:flex;align-items:center;gap:6px"><div style="flex:1;height:6px;background:var(--okdb-app-border);border-radius:3px;overflow:hidden"><div :style="'height:100%;background:var(--okdb-accent);border-radius:3px;width:'+pct(runtime.process?.memory?.heapUsed,runtime.process?.memory?.heapTotal)+'%;'"></div></div><span style="font-size:11px;opacity:.6;min-width:30px">{{pct(runtime.process?.memory?.heapUsed,runtime.process?.memory?.heapTotal)}}%</span></div><div class="kv-key">External</div><div class="kv-val">{{formatBytes(runtime.process?.memory?.external)}}</div></div></if></div></div><if :="runtime?.processes"><div class="card"><div class="card-title">Processes <span style="font-size:11px;opacity:.5;font-weight:400">{{runtime.processes.totals?.processes}} · Σ RSS {{formatBytes(runtime.processes.totals?.rss)}} · Σ heap {{formatBytes(runtime.processes.totals?.heapUsed)}}</span></div><div style="display:flex;flex-direction:column;gap:4px"><div style="display:grid;grid-template-columns:110px 70px 1fr 90px 110px 56px;gap:8px;font-size:10px;opacity:.45;text-transform:uppercase;letter-spacing:.05em"><div>Kind</div><div>PID</div><div></div><div style="text-align:right">RSS</div><div style="text-align:right">Heap</div><div style="text-align:right">CPU</div></div><for :="runtime.processes.nodes" let:n><div style="display:grid;grid-template-columns:110px 70px 1fr 90px 110px 56px;gap:8px;font-size:12px;align-items:center"><div style="font-family:monospace">{{n.kind}}</div><div style="font-family:monospace;opacity:.7">{{n.pid}}</div><div style="font-size:11px;opacity:.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{processDetail(n)}}</div><div style="text-align:right;font-family:monospace">{{n.stats ? formatBytes(n.stats.rss) : '--'}}</div><div style="text-align:right;font-family:monospace">{{n.stats ? formatBytes((n.stats.heapUsed||0)+(n.stats.threads?.heapUsed||0)) : '--'}}</div><div style="text-align:right;font-family:monospace">{{n.stats?.cpuPct != null ? n.stats.cpuPct+'%' : '--'}}</div></div></for></div></div></if><div class="card"><div class="card-title">Machine</div><if :="!runtime"><div style="opacity:.4;font-size:12px">Loading…</div></if><if :="runtime"><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px"><div><div style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Memory</div><div style="font-size:22px;font-weight:600;color:var(--okdb-accent)">{{formatBytes(runtime.os?.freeMemory)}}</div><div style="font-size:11px;opacity:.5;margin-top:2px">free of {{formatBytes(runtime.os?.totalMemory)}}</div><div style="margin-top:8px;height:6px;background:var(--okdb-app-border);border-radius:3px;overflow:hidden"><div :style="'height:100%;background:var(--okdb-accent);border-radius:3px;width:'+pct(runtime.os?.totalMemory-runtime.os?.freeMemory,runtime.os?.totalMemory)+'%;'"></div></div><div style="font-size:11px;opacity:.45;margin-top:3px">{{pct(runtime.os?.totalMemory-runtime.os?.freeMemory,runtime.os?.totalMemory)}}% used</div></div><div><div style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">CPU</div><div style="font-size:14px;font-weight:600">{{runtime.os?.cpuCount}} core{{runtime.os?.cpuCount!==1?'s':''}}</div><div style="font-size:11px;opacity:.5;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" :title="runtime.os?.cpuModel">{{runtime.os?.cpuModel}}</div><div style="margin-top:8px;font-size:11px;opacity:.6">Load avg (1m/5m/15m)</div><div style="font-size:13px;font-family:monospace;margin-top:2px">{{formatLoad(runtime.os?.loadAvg)}}</div></div><div><div style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Platform</div><div style="font-size:14px;font-weight:600;text-transform:capitalize">{{runtime.os?.platform}}</div><div style="font-size:11px;opacity:.5;margin-top:3px">{{runtime.os?.arch}}</div><div style="font-size:11px;opacity:.35;margin-top:6px;word-break:break-all">{{runtime.os?.release}}</div><div style="font-size:11px;opacity:.4;margin-top:3px;font-family:monospace">{{runtime.os?.hostname}}</div></div></div></if></div><if :="metrics"><div class="card"><div class="card-title">API Metrics</div><div style="display:grid;grid-template-columns:1fr 1fr;gap:16px"><div><div style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Lifetime</div><div class="kv-grid"><div class="kv-key">Requests</div><div class="kv-val" style="color:var(--okdb-accent);font-weight:600">{{formatNum(metrics.lifetime?.requests)}}</div><div class="kv-key">Reads</div><div class="kv-val">{{formatNum(metrics.lifetime?.reads)}}</div><div class="kv-key">Writes</div><div class="kv-val">{{formatNum(metrics.lifetime?.writes)}}</div><div class="kv-key">Errors</div><div class="kv-val" :style="metrics.lifetime?.errors>0?'color:var(--okdb-danger);':'opacity:.5;'">{{formatNum(metrics.lifetime?.errors)}}</div><div class="kv-key">Avg Read</div><div class="kv-val" style="font-family:monospace">{{metrics.lifetime?.avgReadMs!=null?metrics.lifetime.avgReadMs+'ms':'—'}}</div><div class="kv-key">Avg Write</div><div class="kv-val" style="font-family:monospace">{{metrics.lifetime?.avgWriteMs!=null?metrics.lifetime.avgWriteMs+'ms':'—'}}</div></div></div><div><div style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Last 60 seconds</div><div class="kv-grid"><div class="kv-key">Req/s</div><div class="kv-val" style="color:var(--okdb-success);font-weight:600">{{metrics.window?.reqPerSec}}</div><div class="kv-key">Read/s</div><div class="kv-val">{{metrics.window?.readPerSec}}</div><div class="kv-key">Write/s</div><div class="kv-val">{{metrics.window?.writePerSec}}</div><div class="kv-key">Requests</div><div class="kv-val">{{formatNum(metrics.window?.requests)}}</div><div class="kv-key">Avg Read</div><div class="kv-val" style="font-family:monospace">{{metrics.window?.avgReadMs!=null?metrics.window.avgReadMs+'ms':'—'}}</div><div class="kv-key">Avg Write</div><div class="kv-val" style="font-family:monospace">{{metrics.window?.avgWriteMs!=null?metrics.window.avgWriteMs+'ms':'—'}}</div></div></div></div></div></if><if :="plugins.length > 0"><div class="card"><div class="card-title">Plugins ({{plugins.length}})</div><div style="display:flex;flex-wrap:wrap;gap:8px"><for :="plugins" let:p><span style="padding:4px 12px;border-radius:999px;font-size:12px;border:1px solid color-mix(in srgb,var(--okdb-accent) 33%,transparent);color:var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 7%,transparent)">{{p}}</span></for></div></div></if><if :="typeList.length > 0"><div class="card"><div class="card-title">Types ({{typeList.length}} across all environments)</div><table class="sys-table"><thead><tr><th>Type</th><th>Environment</th><th style="text-align:right">Records</th></tr></thead><tbody><tr as="repeat" :of="typeList" let:item="entry"><td style="font-family:monospace;font-size:12px">{{entry.name}}</td><td style="font-family:monospace;font-size:11px;opacity:.55">{{entry.env}}</td><td style="text-align:right;color:var(--okdb-accent)">{{formatNum(entry.count)}}</td></tr></tbody></table></div></if><if :="systemTypeList.length > 0"><div class="card"><details><summary style="cursor:pointer;font-weight:600;font-size:13px;list-style:none;display:flex;align-items:center;gap:8px"><span>▶</span><span>Internal / System Types ({{systemTypeList.length}})</span></summary><table class="sys-table" style="margin-top:10px"><thead><tr><th>Type</th><th>Environment</th><th style="text-align:right">Records</th></tr></thead><tbody><tr as="repeat" :of="systemTypeList" let:item="entry"><td style="font-family:monospace;font-size:12px;opacity:.7">{{entry.name}}</td><td style="font-family:monospace;font-size:11px;opacity:.45">{{entry.env}}</td><td style="text-align:right;color:var(--okdb-accent);opacity:.7">{{formatNum(entry.count)}}</td></tr></tbody></table></details></div></if><div class="card"><div style="display:flex;align-items:center;gap:10px;cursor:pointer" @click="toggleChangelog()"><div style="font-weight:600;font-size:14px;flex:1">System Change Log</div><span style="font-size:12px;opacity:.5">all environments</span> <span style="opacity:.5;font-size:12px">{{changelogOpen ? '▲' : '▼'}}</span></div><if :="changelogOpen"><div style="margin-top:12px;display:flex;flex-direction:column;gap:8px"><if :="changelogError"><div style="color:var(--okdb-danger);font-size:12px">{{changelogError}}</div></if><div style="display:flex;gap:6px;align-items:center"><button @click="loadChangelog()" :disabled="changelogBusy" style="font-size:12px;opacity:.8">↻ Refresh</button> <span style="font-size:12px;opacity:.5;margin-left:auto">showing {{changes.length}} entries</span></div><div style="overflow:auto;max-height:400px"><table style="width:100%;border-collapse:collapse;font-size:12px"><thead style="position:sticky;top:0;background:var(--okdb-app-surface);z-index:1"><tr><th class="cl-th" style="width:80px">Env</th><th class="cl-th" style="width:110px">Action</th><th class="cl-th" style="width:80px">Type</th><th class="cl-th">Key</th><th class="cl-th" style="width:90px;text-align:right">When</th></tr></thead><tbody><tr as="repeat" :of="changes" let:item="ch" class="cl-row"><td class="cl-td" style="font-family:monospace;font-size:10px;opacity:.5;white-space:nowrap;max-width:80px;overflow:hidden;text-overflow:ellipsis" :title="ch._env">{{ch._env||'default'}}</td><td class="cl-td"><span :style="'font-size:10px;padding:2px 5px;border-radius:3px;border:1px solid;color:'+actionColor(ch.action)+';border-color:'+actionColor(ch.action)+'44;'">{{actionLabel(ch.action)}}</span></td><td class="cl-td" style="font-family:monospace;font-size:11px;opacity:.75;max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" :title="ch.type">{{ch.type||'—'}}</td><td class="cl-td" style="font-family:monospace;font-size:11px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" :title="ch.key">{{shortKey(ch.key)}}</td><td class="cl-td" style="text-align:right;opacity:.55;white-space:nowrap;font-size:11px">{{relTime(ch.timestamp)}}</td></tr></tbody></table></div><if :="changelogHasOlder"><button @click="loadMoreChangelog()" :disabled="changelogBusy" style="font-size:12px;align-self:flex-end">Load more ▼ ({{changelogLimit+100}})</button></if></div></if></div><div class="card"><system-locks-panel/></div></div></if><if :="subView === 'license'"><license-panel/></if></template></component></template><style>.sys-subnav{display:flex;flex-direction:column;gap:2px}.sys-subnav__item{display:flex;align-items:center;gap:6px;width:100%;text-align:left;padding:6px 10px;border-radius:7px;background:0 0;border-color:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;transition:color .13s,background .13s,border-color .13s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sys-subnav__item:hover{background:var(--okdb-app-surface-2);border-color:var(--okdb-app-border);color:var(--okdb-app-fg)}.sys-subnav__item--active{background:var(--okdb-app-bg-deep)!important;border-color:color-mix(in srgb,var(--okdb-accent) 27%,transparent)!important;color:var(--okdb-accent)!important}.sys-subnav__icon{font-size:12px;opacity:.7;flex-shrink:0}.sys-subnav__item--active .sys-subnav__icon{opacity:1}.sys-subnav__divider{height:1px;background:var(--okdb-app-surface);margin:3px 2px}.docs-nav{display:flex;flex-direction:column;gap:1px;margin:0 -10px;padding:2px 0}.docs-nav__item{display:block;width:100%;text-align:left;padding:5px 14px;border-radius:0;border:none;border-left:2px solid transparent;background:0 0;color:var(--okdb-app-fg-muted);font-size:12.5px;cursor:pointer;transition:color .13s,background .13s,border-color .13s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.55;letter-spacing:.01em}.docs-nav__item--standalone{font-weight:500;color:var(--okdb-app-fg);margin-top:2px}.docs-nav__item:hover{background:var(--okdb-app-surface-2);color:var(--okdb-app-fg);border-left-color:var(--okdb-app-border-strong)}.docs-nav__item--active{background:var(--okdb-accent-soft-bg)!important;color:var(--okdb-accent)!important;border-left-color:var(--okdb-accent)!important;font-weight:500}.docs-nav__group{margin-top:6px;padding-top:4px;border-top:1px solid var(--okdb-app-surface)}.docs-nav__group-label{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--okdb-app-fg-faint);padding:4px 14px 5px;pointer-events:none;user-select:none}.docs-wrap{margin:-20px -24px -6px;min-height:calc(100% + 26px)}.docs-wrap ok-doc-viewer{--ok_color-accent:var(--okdb-accent);--ok_color-fg:var(--okdb-app-fg);--ok_color-muted-fg:var(--okdb-app-fg-muted);--ok_color-muted-border:var(--okdb-app-border);--font-mono:'Cascadia Code','Fira Code','Consolas',monospace;display:block;min-height:100%}.docs-wrap ok-doc-viewer .md table{border-collapse:collapse;width:100%;margin:1.2rem 0;font-size:13px}.docs-wrap ok-doc-viewer .md td,.docs-wrap ok-doc-viewer .md th{border:1px solid var(--okdb-app-border);padding:7px 12px;text-align:left}.docs-wrap ok-doc-viewer .md th{background:var(--okdb-app-surface-2);color:#8fc8e8;font-weight:600;font-size:12px;letter-spacing:.03em}.docs-wrap ok-doc-viewer .md tr:hover td{background:var(--okdb-app-surface)}.docs-wrap ok-doc-viewer .md a{color:var(--okdb-accent);text-decoration:none;border-bottom:1px solid color-mix(in srgb,var(--okdb-accent) 27%,transparent);transition:border-color .15s,color .15s}.docs-wrap ok-doc-viewer .md a:hover{color:#7fd8ff;border-bottom-color:#7fd8ff88}.docs-wrap ok-doc-viewer .md code:not(pre code){background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);color:var(--okdb-accent);padding:1px 6px;border-radius:4px;font-size:.9em}.docs-wrap ok-doc-viewer .md blockquote{border-left:3px solid color-mix(in srgb,var(--okdb-accent) 27%,transparent);margin:1rem 0;padding:6px 14px;background:var(--okdb-app-surface);border-radius:0 6px 6px 0;color:var(--okdb-app-fg-muted);font-style:italic}.card{background:var(--okdb-app-surface);border-radius:12px;padding:16px;box-shadow:0 0 5px #ffffff1a}.card-title{font-weight:600;margin-bottom:12px;font-size:14px}.kv-grid{display:grid;grid-template-columns:110px 1fr;gap:6px 12px;font-size:13px}.kv-key{opacity:.6}.kv-val{word-break:break-all}.sys-table{width:100%;border-collapse:collapse;font-size:13px}.sys-table th{text-align:left;padding:6px 8px;border-bottom:1px solid var(--okdb-app-border);opacity:.6;font-weight:500;font-size:11px;text-transform:uppercase;letter-spacing:.04em}.sys-table td{padding:5px 8px;border-bottom:1px solid var(--okdb-app-border)}.sys-table tr:last-child td{border-bottom:none}.sys-table tr:hover td{background:#ffffff08}.cl-th{padding:5px 8px;text-align:left;font-weight:500;font-size:10px;text-transform:uppercase;letter-spacing:.04em;opacity:.5;border-bottom:1px solid var(--okdb-app-border)}.cl-td{padding:5px 8px;border-bottom:1px solid var(--okdb-app-border)}.cl-row:hover .cl-td{background:#ffffff06}</style>
1
+ <script type="module">import e from"../db/modals/create-env-modal.ok.js";import t from"../db/modals/demo-env-modal.ok.js";import o from"../db/modals/remove-env-modal.ok.js";import s from"./modals/data-ops-modal.ok.js";import n from"./parts/system-log.ok.js";import a from"./parts/system-log-history.ok.js";import r from"./parts/system-sync-panel.ok.js";import i from"./parts/system-auth-panel.ok.js";import c from"./parts/system-api-panel.ok.js";import l from"./parts/system-locks-panel.ok.js";import d from"./parts/system-processing-panel.ok.js";import h from"./parts/system-process-panel.ok.js";import m from"./parts/system-runtime-overview.ok.js";import p from"../license/parts/license-panel.ok.js";import g from"@toolkit/components/ok-doc-viewer.ok.js";export default{tag:"system-index",unwrap:!0,dependencies:[n,a,r,i,c,l,d,h,m,p,g,e,t,o,s],context:e=>({_systemWildcard(){const t=e.$vars.$router?.match("/system/*");return t?.params?.["*"]??""},get subView(){return this._parseSubView(this._systemWildcard())},get isRuntime(){const e=this.subView;return"runtime"===e||"processes"===e||"processing"===e},get runtimeTab(){const e=this.subView;return"runtime"===e?"overview":e},_parseSubView(e){const t=e??"";return t&&"overview"!==t?"logs"===t?"logs":"events"===t?"events":"sync"===t?"sync":"processing"===t?"processing":"processes"===t?"processes":"workers"===t||"runtime"===t?"runtime":"api"===t?"api":"auth"===t?"auth":"license"===t?"license":t.startsWith("docs")?"docs":"overview":"overview"},goToSystemSub(t){const o=e.$vars.$router;if(!o)return;const s=e.$ok.stores.okdb.selectedEnv,n=s?`?env=${encodeURIComponent(s)}`:"";o.push(`/system/${t}${n}`)},docsManifest:[],docsAnchor:"",get docsCurrentFile(){return this._parseDocsFile(this._systemWildcard())},_parseDocsFile(e){const t=e??"",o=t.startsWith("docs/")?t.slice(5):"";return o?o.replace(/\.md$/,"")+".md":"index.md"},docsIsActive(e){return this.docsCurrentFile===e},docsNavigateTo(e){const t=e.replace(/\.md$/,"");this.docsAnchor="",this.goToSystemSub("docs/"+t)},docsOnAnchorChange(e){this.docsAnchor=e},async loadDocsManifest(){if(!this.docsManifest.length)try{const e=await fetch("/docs/manifest.json");e.ok&&(this.docsManifest=await e.json())}catch{}},info:null,runtime:null,syncInfo:null,envs:[],loading:!1,error:null,autoRefresh:!0,_timer:null,metrics:null,async load(){this.loading=!0,this.error=null;try{const e=await fetch(`${__ENV__.API}/info`),t=await e.json();if(!e.ok)throw new Error(t?.error?.message||"Failed to load info");this.info=t.data??t}catch(e){this.error=e?.message||String(e)}finally{this.loading=!1}try{const t=await fetch(`${__ENV__.API}/envs`);if(t.ok){const o=await t.json();this.envs=o.data??[],e.$ok.stores.okdb.envs=this.envs}}catch{}try{const e=await fetch(`${__ENV__.API}/system/runtime`);if(e.ok){const t=await e.json();this.runtime=t.data??t}}catch{}try{const e=await fetch(`${__ENV__.API}/system/metrics`);if(e.ok){const t=await e.json();this.metrics=t.data??null}}catch{}try{const e=await fetch(`${__ENV__.API}/sync/info`);this.syncInfo=e.ok?(await e.json()).data??null:null}catch{this.syncInfo=null}},get approxSize(){const e=(this.envs||[]).reduce((e,t)=>e+(t.size||0),0);if(!e)return"--";const t=["B","KB","MB","GB","TB"];let o=e,s=0;for(;o>=1024&&s<t.length-1;)o/=1024,s++;return o.toFixed(2)+" "+t[s]},get typeList(){const e=[];for(const t of this.envs||[])for(const o of t.types||[])o.name.startsWith("~")||e.push({name:o.name,env:t.name,count:o.count??0});return e.sort((e,t)=>e.name.localeCompare(t.name)||e.env.localeCompare(t.env))},get systemTypeList(){const e=[];for(const t of this.envs||[])for(const o of t.types||[])o.name.startsWith("~")&&e.push({name:o.name,env:t.name,count:o.count??0});return e.sort((e,t)=>e.name.localeCompare(t.name)||e.env.localeCompare(t.env))},get totalTypeCount(){return this.typeList.length},get plugins(){return(this.info?.plugins||[]).filter(Boolean)},get config(){return this.info?.config??null},get durabilityLabel(){const e=this.config?.durability;return{strict:{label:"Strict",color:"var(--okdb-success)",hint:"Full fsync on every commit."},balanced:{label:"Balanced",color:"var(--okdb-accent)",hint:"Async fsync worker — crash-safe, fast."},fast:{label:"Fast",color:"var(--okdb-warning)",hint:"No per-commit fsync. May lose last tx on crash."},custom:{label:"Custom",color:"var(--okdb-purple)",hint:"Manual LMDB flags. No preset applied."}}[e]??{label:e??"--",color:"var(--okdb-app-fg-muted)",hint:""}},get configWarnings(){return this.config?.warnings??[]},formatBytes(e){if(null==e)return"--";const t=["B","KB","MB","GB","TB"];let o=e,s=0;for(;o>=1024&&s<t.length-1;)o/=1024,s++;return o.toFixed(0===s?0:2)+" "+t[s]},formatUptime(e){if(null==e)return"--";const t=Math.floor(e),o=Math.floor(t/86400),s=Math.floor(t%86400/3600),n=Math.floor(t%3600/60),a=t%60;return o>0?`${o}d ${s}h ${n}m`:s>0?`${s}h ${n}m ${a}s`:n>0?`${n}m ${a}s`:`${a}s`},formatLoad:e=>e&&e.length?e.map(e=>e.toFixed(2)).join(" / "):"--",processDetail:e=>e?.meta?.env?e.meta.queueType?`${e.meta.env} · ${e.meta.queueType}`:e.meta.env:e?.stats?.threads?.count?`${e.stats.threads.count} worker thread${1!==e.stats.threads.count?"s":""}`:"",pct:(e,t)=>e&&t?Math.round(e/t*100):0,get clock(){return this.syncInfo?.clock??this.info?.clock??"--"},get nodeId(){return this.syncInfo?.node?.id??this.info?.id??"--"},get nodeAddress(){return this.syncInfo?.node?.address??null},formatNum:e=>null==e?"--":Number(e).toLocaleString(),copyId(){navigator.clipboard?.writeText(this.nodeId).catch(()=>{})},changelogOpen:!1,changelogBusy:!1,changelogError:null,changes:[],changelogLimit:50,async loadChangelog(){this.changelogBusy=!0,this.changelogError=null;try{const e=new URLSearchParams({limit:this.changelogLimit}),t=await fetch(`${__ENV__.API}/system/changelog?${e}`),o=await t.json();if(!t.ok)throw new Error(o?.error?.message||"Failed");this.changes=o.data??[]}catch(e){this.changelogError=e?.message||String(e)}finally{this.changelogBusy=!1}},get changelogHasOlder(){return this.changes.length===this.changelogLimit},loadMoreChangelog(){this.changelogLimit=Math.min(this.changelogLimit+100,1e3),this.loadChangelog()},toggleChangelog(){this.changelogOpen=!this.changelogOpen,this.changelogOpen&&!this.changes.length&&this.loadChangelog()},actionColor:e=>({put:"var(--okdb-success)",remove:"var(--okdb-danger)",type_register:"var(--okdb-accent)",type_drop:"var(--okdb-danger)",index_register:"var(--okdb-purple)",index_drop:"var(--okdb-warning)"}[e]||"var(--okdb-app-fg-muted)"),actionLabel:e=>(e||"").replace(/_/g," ").toUpperCase(),shortKey:e=>e?e.length<=20?e:e.slice(0,10)+"…"+e.slice(-6):"—",relTime(e){if(!e)return"—";const t=Date.now()-e;return t<5e3?"just now":t<6e4?`${Math.round(t/1e3)}s ago`:t<36e5?`${Math.round(t/6e4)}m ago`:t<864e5?`${Math.round(t/36e5)}h ago`:new Date(e).toLocaleDateString()},showSystemEnvs:!1,get totalEnvSize(){return(this.envs||[]).reduce((e,t)=>e+(t.size||0),0)},get userEnvs(){return(this.envs||[]).filter(e=>!e.name.startsWith("~"))},get systemEnvs(){return(this.envs||[]).filter(e=>e.name.startsWith("~"))},get visibleEnvs(){return this.showSystemEnvs?this.envs:this.userEnvs},get orphanedEnvs(){return(this.envs||[]).filter(e=>e.orphaned)},goToEnv(t){e.$ok.stores.okdb.selectEnv(t),e.$vars.$router?.push?.("/db?env="+encodeURIComponent(t))},async openCreateEnv(){await e.$ok.shared.modal.open({owner:e,title:"Create Environment",content:'<create-env-modal :modal="modal" :on-done="onDone" />',context:t=>({modal:t,onDone:async()=>{await e.$ok.stores.okdb.reloadEnvs(),await this.load()}})})},async openDemoModal(){await e.$ok.shared.modal.open({owner:e,title:'<span style="background:linear-gradient(135deg,var(--okdb-accent),var(--okdb-purple) 50%,var(--okdb-success));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;font-weight:700;font-size:15px;">✦ Demo Environment</span>',canDrag:!0,content:'<demo-env-modal :modal="modal" :on-done="onDone" />',context:t=>({modal:t,onDone:async t=>{e.$ok.stores.okdb.selectEnv(t),await e.$ok.stores.okdb.reloadEnvs(),await this.load(),e.$ok.shared?.toast?.success?.(`✦ Demo environment "${t}" is ready!`)}})})},_compactingEnvs:{},async compactEnv(t){if(!this._compactingEnvs[t]){this._compactingEnvs={...this._compactingEnvs,[t]:!0};try{const o=await fetch(`${__ENV__.API}/env/${encodeURIComponent(t)}/compact`,{method:"POST"}),s=await o.json();if(!o.ok)throw new Error(s?.error?.message||"Compact failed");const n=s.data??{},a=n.saved>0?`✓ Compacted "${t}": ${this.formatBytes(n.sizeBefore)} → ${this.formatBytes(n.sizeAfter)} (saved ${this.formatBytes(n.saved)}, ${n.savedPct}%)`:`✓ Compacted "${t}" — no space reclaimed (${this.formatBytes(n.sizeAfter)})`;e.$ok.shared?.toast?.success?.(a),await this.load()}catch(t){e.$ok.shared?.toast?.error?.("Compact failed: "+(t?.message||t))}finally{const e={...this._compactingEnvs};delete e[t],this._compactingEnvs=e}}},async openRemoveEnv(t){await e.$ok.shared.modal.open({owner:e,title:"Remove Environment",content:'<remove-env-modal :modal="modal" :env-name="envName" :on-done="onDone" />',context:o=>({modal:o,envName:t,onDone:async(t,o)=>{await e.$ok.stores.okdb.reloadEnvs(),e.$ok.stores.okdb.selectedEnv===t&&e.$ok.stores.okdb.selectEnv(e.$ok.stores.okdb._pickBestEnv()),await this.load(),o?.filesLocked?.length?e.$ok.shared?.toast?.warn?.(`"${t}" removed — some files could not be deleted (locked)`):e.$ok.shared?.toast?.success?.(`Environment "${t}" removed and files deleted`)}})})},async dismissOrphan(t){try{const o=await fetch(`${__ENV__.API}/env/${encodeURIComponent(t)}`,{method:"DELETE"}),s=await o.json().catch(()=>({}));if(!o.ok)throw new Error(s?.error?.message||o.statusText);const n=s.data??s;await e.$ok.stores.okdb.reloadEnvs(),await this.load(),n?.filesLocked?.length?e.$ok.shared?.toast?.warn?.(`Orphan "${t}" dismissed — some files could not be deleted (locked)`):e.$ok.shared?.toast?.info?.(`Orphan "${t}" dismissed`)}catch(t){e.$ok.shared?.toast?.error?.("Dismiss failed: "+(t?.message||t))}},async openDataOps(t){await e.$ok.shared.modal.open({owner:e,title:"⬦ Data Operations",canDrag:!0,content:'<data-ops-modal :modal="modal" :envs="envs" :on-done="onDone" :initial-tab="initialTab" />',context:o=>({modal:o,envs:this.envs,initialTab:t||"export",onDone:async()=>{await this.load(),e.$ok.shared?.toast?.success?.("Operation complete")}})})},logView:"live"}),async mount(e){await this.load(),this._timer=setInterval(()=>{this.autoRefresh&&"overview"===this.subView&&this.load()},1e4);const t=()=>{fetch(`${__ENV__.API}/envs`).then(e=>e.json()).then(t=>{this.envs=t.data??[],e.$ok.stores.okdb.envs=this.envs}).catch(()=>{})},o=e.$ok.stores.events?.on?.("env:opened",t),s=e.$ok.stores.events?.on?.("env:removed",t);this._unsubEnv=()=>{o?.(),s?.()},await this.loadDocsManifest()},unmount(){clearInterval(this._timer),this._unsubEnv?.()}};</script><template><component import="../../layouts/_default.ok.html"><template name="header"></template><template name="sidebar"><if :="subView === 'docs'"><nav class="sys-subnav" style="margin-bottom:6px"><button class="sys-subnav__item" @click="goToSystemSub('overview')" style="opacity:.6;font-size:11px">← System</button></nav><nav class="docs-nav" aria-label="Documentation"><each :of="docsManifest" let:item="item"><if :="item.file"><button class="docs-nav__item docs-nav__item--standalone" :class="{'docs-nav__item--active': docsIsActive(item.file)}" @click="docsNavigateTo(item.file)">{{ item.label }}</button></if><if :="item.children"><div class="docs-nav__group"><span class="docs-nav__group-label">{{ item.label }}</span><each :of="item.children" let:child="child"><button class="docs-nav__item" :class="{'docs-nav__item--active': docsIsActive(child.file)}" @click="docsNavigateTo(child.file)">{{ child.label }}</button></each></div></if></each></nav></if><if :="subView !== 'docs'"><nav class="sys-subnav" aria-label="System navigation"><button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='overview'}" @click="goToSystemSub('overview')"><span class="sys-subnav__icon">⚙</span> Overview</button> <button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='logs'}" @click="goToSystemSub('logs')"><span class="sys-subnav__icon">📋</span> Logs</button> <button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='events'}" @click="goToSystemSub('events')"><span class="sys-subnav__icon">⚡</span> Events</button><div class="sys-subnav__divider"></div><button class="sys-subnav__item" :class="{'sys-subnav__item--active': isRuntime}" @click="goToSystemSub('runtime')"><span class="sys-subnav__icon">⊞</span> Runtime</button> <button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='sync'}" @click="goToSystemSub('sync')"><span class="sys-subnav__icon">⇄</span> Sync</button> <button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='auth'}" @click="goToSystemSub('auth')"><span class="sys-subnav__icon">🔐</span> Auth</button> <button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='license'}" @click="goToSystemSub('license')"><span class="sys-subnav__icon">🔑</span> License</button><div class="sys-subnav__divider"></div><button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='api'}" @click="goToSystemSub('api')"><span class="sys-subnav__icon">⌁</span> API</button> <button class="sys-subnav__item" :class="{'sys-subnav__item--active': subView==='docs'}" @click="goToSystemSub('docs')"><span class="sys-subnav__icon">≡</span> Docs</button></nav></if></template><template name="default"><if :="subView === 'logs'"><div style="display:flex;flex-direction:column;gap:12px;height:100%;min-height:0"><div style="display:flex;align-items:center;gap:8px;flex-shrink:0"><div style="font-weight:600;font-size:15px;opacity:.8">Logs</div><div style="display:flex;gap:2px;padding:2px;background:var(--okdb-app-surface);border-radius:8px;border:1px solid var(--okdb-app-border)"><button @click="logView='live'" :style="logView==='live' ?'background:var(--okdb-app-surface-2);color:var(--okdb-accent);border-color:color-mix(in srgb, var(--okdb-accent) 27%, transparent);':'background:transparent;border-color:transparent;opacity:.7;'" style="padding:4px 14px;border-radius:6px;font-size:12px">Live</button> <button @click="logView='history'" :style="logView==='history' ?'background:var(--okdb-app-surface-2);color:var(--okdb-accent);border-color:color-mix(in srgb, var(--okdb-accent) 27%, transparent);':'background:transparent;border-color:transparent;opacity:.7;'" style="padding:4px 14px;border-radius:6px;font-size:12px">History</button></div></div><if :="logView === 'live'"><system-log :initial-source="'log'" style="flex:1;min-height:300px"/></if><if :="logView === 'history'"><system-log-history style="flex:1;min-height:300px"/></if></div></if><if :="subView === 'events'"><div style="display:flex;flex-direction:column;gap:12px;height:100%;min-height:0"><div style="font-weight:600;font-size:15px;opacity:.8;flex-shrink:0">Events</div><system-log :initial-source="'event'" style="flex:1;min-height:300px"/></div></if><if :="isRuntime"><div style="display:flex;flex-direction:column;gap:14px;height:100%;min-height:0"><nav style="display:flex;gap:4px;border-bottom:1px solid var(--okdb-app-border);padding-bottom:0"><button @click="goToSystemSub('runtime')" :style="'font-size:12px;padding:6px 12px;background:none;border:none;border-bottom:2px solid ' + (runtimeTab==='overview' ? 'var(--okdb-accent)' : 'transparent') + ';cursor:pointer;color:' + (runtimeTab==='overview' ? 'var(--okdb-app-fg-strong)' : 'var(--okdb-app-fg-subtle)') + ';'">Overview</button> <button @click="goToSystemSub('processes')" :style="'font-size:12px;padding:6px 12px;background:none;border:none;border-bottom:2px solid ' + (runtimeTab==='processes' ? 'var(--okdb-accent)' : 'transparent') + ';cursor:pointer;color:' + (runtimeTab==='processes' ? 'var(--okdb-app-fg-strong)' : 'var(--okdb-app-fg-subtle)') + ';'">Processes</button> <button @click="goToSystemSub('processing')" :style="'font-size:12px;padding:6px 12px;background:none;border:none;border-bottom:2px solid ' + (runtimeTab==='processing' ? 'var(--okdb-accent)' : 'transparent') + ';cursor:pointer;color:' + (runtimeTab==='processing' ? 'var(--okdb-app-fg-strong)' : 'var(--okdb-app-fg-subtle)') + ';'">Processors</button></nav><div style="flex:1;min-height:0;overflow:auto"><if :="runtimeTab === 'overview'"><system-runtime-overview/></if><if :="runtimeTab === 'processes'"><system-process-panel style="height:100%;min-height:0"/></if><if :="runtimeTab === 'processing'"><system-processing-panel/></if></div></div></if><if :="subView === 'sync'"><system-sync-panel style="height:100%;min-height:0"/></if><if :="subView === 'auth'"><system-auth-panel style="height:100%;min-height:0"/></if><if :="subView === 'api'"><system-api-panel style="height:100%;min-height:0"/></if><if :="subView === 'docs'"><div class="docs-wrap"><ok-doc-viewer :blocks="[]" root="/docs/" prefix="#/system/docs/" :file="docsCurrentFile" :anchor="docsAnchor" :manifest="docsManifest" sidebar-title="On this page" @anchor-change="docsOnAnchorChange($event.detail)"/></div></if><if :="subView === 'overview'"><div style="display:flex;flex-direction:column;gap:20px;max-width:900px"><div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap"><div style="font-weight:600;font-size:16px">Overview</div><button @click="load()" :disabled="loading" style="opacity:.8;font-size:12px">↻ Refresh</button> <label style="display:flex;align-items:center;gap:6px;font-size:12px;opacity:.7;cursor:pointer"><input type="checkbox" :checked="autoRefresh" @change="autoRefresh = $el.checked"/> Auto-refresh 10s</label><if :="loading"><span style="font-size:12px;opacity:.55">Loading…</span></if></div><if :="error"><div style="color:var(--okdb-danger);font-size:13px;padding:10px;border:1px solid var(--okdb-danger);border-radius:8px">{{error}}</div></if><div style="display:grid;grid-template-columns:1fr 1fr;gap:16px"><div class="card"><div class="card-title">Node Identity</div><div class="kv-grid"><div class="kv-key">Node ID</div><div class="kv-val" style="display:flex;align-items:center;gap:6px"><span style="font-family:monospace;font-size:12px;word-break:break-all">{{nodeId}}</span> <button @click="copyId()" style="font-size:10px;padding:2px 6px;opacity:.7">copy</button></div><if :="nodeAddress"><div class="kv-key">Address</div><div class="kv-val" style="font-family:monospace;font-size:12px">{{nodeAddress}}</div></if><div class="kv-key">Clock</div><div class="kv-val" style="font-family:monospace">{{formatNum(clock)}}</div><div class="kv-key">DB State</div><div class="kv-val" style="color:var(--okdb-success)">open</div><div class="kv-key">OKDB</div><div class="kv-val" style="font-family:monospace">v{{info?.versions?.okdb ?? info?.version ?? '--'}}</div><div class="kv-key">lmdb-js</div><div class="kv-val" style="font-family:monospace">v{{info?.versions?.lmdb ?? '--'}}</div><div class="kv-key">LMDB</div><div class="kv-val" style="font-family:monospace;font-size:11px;opacity:.7">{{info?.versions?.lmdbNative ?? '--'}}</div></div></div><div class="card"><div class="card-title">Storage (all environments)</div><div class="kv-grid"><div class="kv-key">Total Size</div><div class="kv-val" style="font-size:20px;font-weight:600;color:var(--okdb-accent)">{{approxSize}}</div><div class="kv-key">Environments</div><div class="kv-val">{{(envs || []).length}}</div><div class="kv-key">User Types</div><div class="kv-val">{{totalTypeCount}}</div><div class="kv-key">Page Size</div><div class="kv-val">{{formatNum(info?.pageSize)}} B</div><div class="kv-key">Map Size</div><div class="kv-val">{{info?.config?.lmdbOptions?.mapSize ? (info.config.lmdbOptions.mapSize/1024/1024/1024).toFixed(2)+' GB' : '--'}} <span style="opacity:.4;font-size:11px">(per env)</span></div></div></div></div><if :="envs && envs.length > 0"><div class="card"><div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap"><div style="font-weight:600;font-size:14px;flex:1">Environments ({{userEnvs.length}}<if :="systemEnvs.length > 0"><span style="opacity:.4;font-size:12px;font-weight:400">+ {{systemEnvs.length}} system</span></if>)</div><span style="font-size:12px;opacity:.5">{{formatBytes(totalEnvSize)}}</span><if :="systemEnvs.length > 0"><button @click="showSystemEnvs = !showSystemEnvs" style="font-size:11px;padding:3px 8px;opacity:.6">{{showSystemEnvs ? '▴ hide system' : '⚙ show system'}}</button></if><button @click="openCreateEnv()" style="font-size:11px;padding:4px 10px;opacity:.8">+ Create</button> <button @click="openDemoModal()" style="font-size:11px;padding:4px 12px;background:linear-gradient(135deg,color-mix(in srgb,var(--okdb-accent) 7%,transparent),color-mix(in srgb,var(--okdb-purple) 7%,transparent));border:1px solid color-mix(in srgb,var(--okdb-accent) 27%,transparent);color:var(--okdb-accent);border-radius:6px;font-weight:600" title="Generate a demo environment">✦ Demo</button></div><table class="sys-table"><thead><tr><th>Name</th><th style="text-align:right">Size</th><th style="text-align:right">Clock</th><th style="text-align:right">Types</th><th style="text-align:center">Sync</th><th></th></tr></thead><tbody><tr as="repeat" :of="visibleEnvs" let:item="e" class="sys-table-row"><td style="font-family:monospace;font-size:12px"><a @click.prevent="goToEnv(e.name)" href="#" style="color:var(--okdb-accent);text-decoration:none" :title="'Explore '+e.name">{{e.name}}</a><if :="e.parentEnv"><span style="font-size:10px;opacity:.4;font-family:monospace;margin-left:4px">↳ {{e.parentEnv}}</span></if></td><td style="text-align:right;font-size:12px;font-family:monospace;opacity:.8">{{formatBytes(e.size)}}</td><td style="text-align:right;font-family:monospace;font-size:12px;opacity:.7">{{e.sync ? formatNum(e.clock) : '—'}}</td><td style="text-align:right;font-size:12px;opacity:.7">{{e.types ? e.types.length : 0}}</td><td style="text-align:center"><span :style="e.sync?'color:var(--okdb-success);font-size:11px;':'opacity:.4;font-size:11px;'">{{e.sync?'✓':'✗'}}</span></td><td style="text-align:right;white-space:nowrap"><button @click="compactEnv(e.name)" :disabled="_compactingEnvs[e.name]" :style="_compactingEnvs[e.name]?'font-size:10px;padding:2px 8px;opacity:.35;cursor:not-allowed;':'font-size:10px;padding:2px 8px;opacity:.55;'">{{_compactingEnvs[e.name]?'⏳ compacting…':'⬡ compact'}}</button><if :="e.name !== '~system'"><button @click="openRemoveEnv(e.name)" style="font-size:10px;padding:2px 8px;opacity:.45;color:var(--okdb-danger);border-color:color-mix(in srgb,var(--okdb-danger) 20%,transparent);margin-left:4px">🗑 remove</button></if></td></tr></tbody></table><if :="orphanedEnvs.length > 0"><div style="margin-top:16px;border-top:1px solid var(--okdb-app-border);padding-top:14px"><div style="font-size:12px;font-weight:600;opacity:.6;margin-bottom:8px;display:flex;align-items:center;gap:6px"><span style="color:var(--okdb-orange)">⚠</span> Orphaned sub-environments ({{orphanedEnvs.length}}) <span style="font-size:11px;font-weight:400;opacity:.55;margin-left:2px">— parent was removed; data on disk is intact</span></div><table class="sys-table"><tbody><tr as="repeat" :of="orphanedEnvs" let:item="e" class="sys-table-row"><td style="font-family:monospace;font-size:12px;opacity:.5;text-decoration:line-through">{{e.name}}</td><td style="font-size:11px;opacity:.4;font-style:italic">orphaned from <span style="font-family:monospace">{{e.orphanedFrom ?? '—'}}</span></td><td style="text-align:right;white-space:nowrap"><button @click="dismissOrphan(e.name)" style="font-size:10px;padding:2px 8px;opacity:.5;color:var(--okdb-danger);border-color:color-mix(in srgb,var(--okdb-danger) 20%,transparent)">✕ dismiss</button></td></tr></tbody></table></div></if></div></if><div class="card" style="border:1px solid color-mix(in srgb,var(--okdb-accent) 9%,transparent);background:linear-gradient(135deg,var(--okdb-app-bg-deep),var(--okdb-app-surface))"><div style="display:flex;align-items:center;gap:14px;flex-wrap:wrap"><div style="flex:1;min-width:200px"><div style="font-weight:600;font-size:14px;margin-bottom:4px;display:flex;align-items:center;gap:8px"><span style="color:var(--okdb-accent);opacity:.7">⬦</span> Data Operations</div><div style="font-size:12px;opacity:.45;line-height:1.5">Export &amp; import logical NDJSON snapshots · hot filesystem backup</div></div><div style="display:flex;gap:8px;flex-shrink:0;flex-wrap:wrap;align-items:center"><button @click="openDataOps('export')" style="display:flex;align-items:center;gap:6px;padding:7px 15px;border-radius:8px;font-size:12px;font-weight:600;background:linear-gradient(135deg,var(--okdb-app-bg-deep),var(--okdb-app-bg-deep));border:1px solid color-mix(in srgb,var(--okdb-accent) 27%,transparent);color:var(--okdb-accent);cursor:pointer;white-space:nowrap">⬇ Export</button> <button @click="openDataOps('import')" style="display:flex;align-items:center;gap:6px;padding:7px 15px;border-radius:8px;font-size:12px;font-weight:600;background:linear-gradient(135deg,var(--okdb-app-bg-deep),var(--okdb-app-bg-deep));border:1px solid color-mix(in srgb,var(--okdb-accent) 27%,transparent);color:var(--okdb-accent);cursor:pointer;white-space:nowrap">⬆ Import</button><div style="width:1px;height:24px;background:var(--okdb-app-border);margin:0 2px;flex-shrink:0"></div><button @click="openDataOps('backup')" style="display:flex;align-items:center;gap:6px;padding:7px 15px;border-radius:8px;font-size:12px;font-weight:600;background:linear-gradient(135deg,var(--okdb-app-bg-deep),var(--okdb-app-bg-deep));border:1px solid color-mix(in srgb,var(--okdb-purple) 27%,transparent);color:var(--okdb-purple);cursor:pointer;white-space:nowrap">🗄 Backup</button></div></div></div><div style="display:grid;grid-template-columns:1fr 1fr;gap:16px"><div class="card"><div class="card-title">Configuration</div><div class="kv-grid"><div class="kv-key">Durability</div><div class="kv-val" style="display:flex;align-items:center;gap:6px"><span :style="'font-size:11px;font-weight:600;padding:2px 8px;border-radius:999px;border:1px solid;color:'+durabilityLabel.color+';border-color:'+durabilityLabel.color+'44;background:'+durabilityLabel.color+'11;'">{{durabilityLabel.label}}</span> <span style="opacity:.5;font-size:11px;flex:1">{{durabilityLabel.hint}}</span></div><div class="kv-key">Compression</div><div class="kv-val"><span :style="config?.lmdbOptions?.compression?'color:var(--okdb-success)':'opacity:.5'">{{config?.lmdbOptions?.compression?'LZ4 enabled':'disabled'}}</span></div><div class="kv-key">Encryption</div><div class="kv-val"><span :style="config?.lmdbOptions?.encryptionKey?'color:var(--okdb-success)':'opacity:.5'">{{config?.lmdbOptions?.encryptionKey?'Chacha8 enabled':'none'}}</span></div><div class="kv-key">Map Size</div><div class="kv-val">{{formatBytes(config?.lmdbOptions?.mapSize)}}</div><div class="kv-key">Max DBs</div><div class="kv-val">{{formatNum(config?.lmdbOptions?.maxDbs)}}</div></div><if :="configWarnings.length > 0"><div style="margin-top:10px;display:flex;flex-direction:column;gap:4px"><for :="configWarnings" let:w><div style="font-size:11px;color:var(--okdb-warning);background:color-mix(in srgb,var(--okdb-warning) 7%,transparent);border:1px solid color-mix(in srgb,var(--okdb-warning) 20%,transparent);border-radius:6px;padding:5px 8px;line-height:1.4">⚠ {{w}}</div></for></div></if></div><div class="card"><div class="card-title">Process <span style="font-size:11px;opacity:.5;font-weight:400">this responder only{{runtime?.processes ? ' — all processes below' : ''}}</span></div><if :="!runtime"><div style="opacity:.4;font-size:12px">Loading…</div></if><if :="runtime"><div class="kv-grid"><div class="kv-key">PID</div><div class="kv-val" style="font-family:monospace">{{runtime.process?.pid}}</div><div class="kv-key">Uptime</div><div class="kv-val">{{formatUptime(runtime.process?.uptime)}}</div><div class="kv-key">Node.js</div><div class="kv-val" style="font-family:monospace">v{{runtime.process?.node}}</div><div class="kv-key" style="align-self:center">CPU</div><div class="kv-val" style="display:flex;align-items:center;gap:6px"><div style="flex:1;height:6px;background:var(--okdb-app-border);border-radius:3px;overflow:hidden"><div :style="'height:100%;border-radius:3px;transition:width .4s;background:'+(runtime.process?.cpu?.pct>80?'var(--okdb-danger)':runtime.process?.cpu?.pct>40?'var(--okdb-warning)':'var(--okdb-success)')+';width:'+(runtime.process?.cpu?.pct??0)+'%;'"></div></div><span style="font-size:11px;opacity:.6;min-width:30px">{{runtime.process?.cpu?.pct??0}}%</span></div><div class="kv-key">RSS</div><div class="kv-val">{{formatBytes(runtime.process?.memory?.rss)}}</div><div class="kv-key">Heap Used</div><div class="kv-val">{{formatBytes(runtime.process?.memory?.heapUsed)}} <span style="opacity:.45;font-size:11px">/ {{formatBytes(runtime.process?.memory?.heapTotal)}}</span></div><div class="kv-key" style="align-self:center">Heap %</div><div class="kv-val" style="display:flex;align-items:center;gap:6px"><div style="flex:1;height:6px;background:var(--okdb-app-border);border-radius:3px;overflow:hidden"><div :style="'height:100%;background:var(--okdb-accent);border-radius:3px;width:'+pct(runtime.process?.memory?.heapUsed,runtime.process?.memory?.heapTotal)+'%;'"></div></div><span style="font-size:11px;opacity:.6;min-width:30px">{{pct(runtime.process?.memory?.heapUsed,runtime.process?.memory?.heapTotal)}}%</span></div><div class="kv-key">External</div><div class="kv-val">{{formatBytes(runtime.process?.memory?.external)}}</div></div></if></div></div><if :="runtime?.processes"><div class="card"><div class="card-title">Processes <span style="font-size:11px;opacity:.5;font-weight:400">{{runtime.processes.totals?.processes}} · Σ RSS {{formatBytes(runtime.processes.totals?.rss)}} · Σ heap {{formatBytes(runtime.processes.totals?.heapUsed)}}</span></div><div style="display:flex;flex-direction:column;gap:4px"><div style="display:grid;grid-template-columns:110px 70px 1fr 90px 110px 56px;gap:8px;font-size:10px;opacity:.45;text-transform:uppercase;letter-spacing:.05em"><div>Kind</div><div>PID</div><div></div><div style="text-align:right">RSS</div><div style="text-align:right">Heap</div><div style="text-align:right">CPU</div></div><for :="runtime.processes.nodes" let:n><div style="display:grid;grid-template-columns:110px 70px 1fr 90px 110px 56px;gap:8px;font-size:12px;align-items:center"><div style="font-family:monospace">{{n.kind}}</div><div style="font-family:monospace;opacity:.7">{{n.pid}}</div><div style="font-size:11px;opacity:.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{processDetail(n)}}</div><div style="text-align:right;font-family:monospace">{{n.stats ? formatBytes(n.stats.rss) : '--'}}</div><div style="text-align:right;font-family:monospace">{{n.stats ? formatBytes((n.stats.heapUsed||0)+(n.stats.threads?.heapUsed||0)) : '--'}}</div><div style="text-align:right;font-family:monospace">{{n.stats?.cpuPct != null ? n.stats.cpuPct+'%' : '--'}}</div></div></for></div></div></if><div class="card"><div class="card-title">Machine</div><if :="!runtime"><div style="opacity:.4;font-size:12px">Loading…</div></if><if :="runtime"><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px"><div><div style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Memory</div><div style="font-size:22px;font-weight:600;color:var(--okdb-accent)">{{formatBytes(runtime.os?.freeMemory)}}</div><div style="font-size:11px;opacity:.5;margin-top:2px">free of {{formatBytes(runtime.os?.totalMemory)}}</div><div style="margin-top:8px;height:6px;background:var(--okdb-app-border);border-radius:3px;overflow:hidden"><div :style="'height:100%;background:var(--okdb-accent);border-radius:3px;width:'+pct(runtime.os?.totalMemory-runtime.os?.freeMemory,runtime.os?.totalMemory)+'%;'"></div></div><div style="font-size:11px;opacity:.45;margin-top:3px">{{pct(runtime.os?.totalMemory-runtime.os?.freeMemory,runtime.os?.totalMemory)}}% used</div></div><div><div style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">CPU</div><div style="font-size:14px;font-weight:600">{{runtime.os?.cpuCount}} core{{runtime.os?.cpuCount!==1?'s':''}}</div><div style="font-size:11px;opacity:.5;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" :title="runtime.os?.cpuModel">{{runtime.os?.cpuModel}}</div><div style="margin-top:8px;font-size:11px;opacity:.6">Load avg (1m/5m/15m)</div><div style="font-size:13px;font-family:monospace;margin-top:2px">{{formatLoad(runtime.os?.loadAvg)}}</div></div><div><div style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Platform</div><div style="font-size:14px;font-weight:600;text-transform:capitalize">{{runtime.os?.platform}}</div><div style="font-size:11px;opacity:.5;margin-top:3px">{{runtime.os?.arch}}</div><div style="font-size:11px;opacity:.35;margin-top:6px;word-break:break-all">{{runtime.os?.release}}</div><div style="font-size:11px;opacity:.4;margin-top:3px;font-family:monospace">{{runtime.os?.hostname}}</div></div></div></if></div><if :="metrics"><div class="card"><div class="card-title">API Metrics</div><div style="display:grid;grid-template-columns:1fr 1fr;gap:16px"><div><div style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Lifetime</div><div class="kv-grid"><div class="kv-key">Requests</div><div class="kv-val" style="color:var(--okdb-accent);font-weight:600">{{formatNum(metrics.lifetime?.requests)}}</div><div class="kv-key">Reads</div><div class="kv-val">{{formatNum(metrics.lifetime?.reads)}}</div><div class="kv-key">Writes</div><div class="kv-val">{{formatNum(metrics.lifetime?.writes)}}</div><div class="kv-key">Errors</div><div class="kv-val" :style="metrics.lifetime?.errors>0?'color:var(--okdb-danger);':'opacity:.5;'">{{formatNum(metrics.lifetime?.errors)}}</div><div class="kv-key">Avg Read</div><div class="kv-val" style="font-family:monospace">{{metrics.lifetime?.avgReadMs!=null?metrics.lifetime.avgReadMs+'ms':'—'}}</div><div class="kv-key">Avg Write</div><div class="kv-val" style="font-family:monospace">{{metrics.lifetime?.avgWriteMs!=null?metrics.lifetime.avgWriteMs+'ms':'—'}}</div></div></div><div><div style="font-size:11px;opacity:.5;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Last 60 seconds</div><div class="kv-grid"><div class="kv-key">Req/s</div><div class="kv-val" style="color:var(--okdb-success);font-weight:600">{{metrics.window?.reqPerSec}}</div><div class="kv-key">Read/s</div><div class="kv-val">{{metrics.window?.readPerSec}}</div><div class="kv-key">Write/s</div><div class="kv-val">{{metrics.window?.writePerSec}}</div><div class="kv-key">Requests</div><div class="kv-val">{{formatNum(metrics.window?.requests)}}</div><div class="kv-key">Avg Read</div><div class="kv-val" style="font-family:monospace">{{metrics.window?.avgReadMs!=null?metrics.window.avgReadMs+'ms':'—'}}</div><div class="kv-key">Avg Write</div><div class="kv-val" style="font-family:monospace">{{metrics.window?.avgWriteMs!=null?metrics.window.avgWriteMs+'ms':'—'}}</div></div></div></div></div></if><if :="plugins.length > 0"><div class="card"><div class="card-title">Plugins ({{plugins.length}})</div><div style="display:flex;flex-wrap:wrap;gap:8px"><for :="plugins" let:p><span style="padding:4px 12px;border-radius:999px;font-size:12px;border:1px solid color-mix(in srgb,var(--okdb-accent) 33%,transparent);color:var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 7%,transparent)">{{p}}</span></for></div></div></if><if :="typeList.length > 0"><div class="card"><div class="card-title">Types ({{typeList.length}} across all environments)</div><table class="sys-table"><thead><tr><th>Type</th><th>Environment</th><th style="text-align:right">Records</th></tr></thead><tbody><tr as="repeat" :of="typeList" let:item="entry"><td style="font-family:monospace;font-size:12px">{{entry.name}}</td><td style="font-family:monospace;font-size:11px;opacity:.55">{{entry.env}}</td><td style="text-align:right;color:var(--okdb-accent)">{{formatNum(entry.count)}}</td></tr></tbody></table></div></if><if :="systemTypeList.length > 0"><div class="card"><details><summary style="cursor:pointer;font-weight:600;font-size:13px;list-style:none;display:flex;align-items:center;gap:8px"><span>▶</span><span>Internal / System Types ({{systemTypeList.length}})</span></summary><table class="sys-table" style="margin-top:10px"><thead><tr><th>Type</th><th>Environment</th><th style="text-align:right">Records</th></tr></thead><tbody><tr as="repeat" :of="systemTypeList" let:item="entry"><td style="font-family:monospace;font-size:12px;opacity:.7">{{entry.name}}</td><td style="font-family:monospace;font-size:11px;opacity:.45">{{entry.env}}</td><td style="text-align:right;color:var(--okdb-accent);opacity:.7">{{formatNum(entry.count)}}</td></tr></tbody></table></details></div></if><div class="card"><div style="display:flex;align-items:center;gap:10px;cursor:pointer" @click="toggleChangelog()"><div style="font-weight:600;font-size:14px;flex:1">System Change Log</div><span style="font-size:12px;opacity:.5">all environments</span> <span style="opacity:.5;font-size:12px">{{changelogOpen ? '▲' : '▼'}}</span></div><if :="changelogOpen"><div style="margin-top:12px;display:flex;flex-direction:column;gap:8px"><if :="changelogError"><div style="color:var(--okdb-danger);font-size:12px">{{changelogError}}</div></if><div style="display:flex;gap:6px;align-items:center"><button @click="loadChangelog()" :disabled="changelogBusy" style="font-size:12px;opacity:.8">↻ Refresh</button> <span style="font-size:12px;opacity:.5;margin-left:auto">showing {{changes.length}} entries</span></div><div style="overflow:auto;max-height:400px"><table style="width:100%;border-collapse:collapse;font-size:12px"><thead style="position:sticky;top:0;background:var(--okdb-app-surface);z-index:1"><tr><th class="cl-th" style="width:80px">Env</th><th class="cl-th" style="width:110px">Action</th><th class="cl-th" style="width:80px">Type</th><th class="cl-th">Key</th><th class="cl-th" style="width:90px;text-align:right">When</th></tr></thead><tbody><tr as="repeat" :of="changes" let:item="ch" class="cl-row"><td class="cl-td" style="font-family:monospace;font-size:10px;opacity:.5;white-space:nowrap;max-width:80px;overflow:hidden;text-overflow:ellipsis" :title="ch._env">{{ch._env||'default'}}</td><td class="cl-td"><span :style="'font-size:10px;padding:2px 5px;border-radius:3px;border:1px solid;color:'+actionColor(ch.action)+';border-color:'+actionColor(ch.action)+'44;'">{{actionLabel(ch.action)}}</span></td><td class="cl-td" style="font-family:monospace;font-size:11px;opacity:.75;max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" :title="ch.type">{{ch.type||'—'}}</td><td class="cl-td" style="font-family:monospace;font-size:11px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" :title="ch.key">{{shortKey(ch.key)}}</td><td class="cl-td" style="text-align:right;opacity:.55;white-space:nowrap;font-size:11px">{{relTime(ch.timestamp)}}</td></tr></tbody></table></div><if :="changelogHasOlder"><button @click="loadMoreChangelog()" :disabled="changelogBusy" style="font-size:12px;align-self:flex-end">Load more ▼ ({{changelogLimit+100}})</button></if></div></if></div><div class="card"><system-locks-panel/></div></div></if><if :="subView === 'license'"><license-panel/></if></template></component></template><style>.sys-subnav{display:flex;flex-direction:column;gap:2px}.sys-subnav__item{display:flex;align-items:center;gap:6px;width:100%;text-align:left;padding:6px 10px;border-radius:7px;background:0 0;border-color:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;transition:color .13s,background .13s,border-color .13s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sys-subnav__item:hover{background:var(--okdb-app-surface-2);border-color:var(--okdb-app-border);color:var(--okdb-app-fg)}.sys-subnav__item--active{background:var(--okdb-app-bg-deep)!important;border-color:color-mix(in srgb,var(--okdb-accent) 27%,transparent)!important;color:var(--okdb-accent)!important}.sys-subnav__icon{font-size:12px;opacity:.7;flex-shrink:0}.sys-subnav__item--active .sys-subnav__icon{opacity:1}.sys-subnav__divider{height:1px;background:var(--okdb-app-surface);margin:3px 2px}.docs-nav{display:flex;flex-direction:column;gap:1px;margin:0 -10px;padding:2px 0}.docs-nav__item{display:block;width:100%;text-align:left;padding:5px 14px;border-radius:0;border:none;border-left:2px solid transparent;background:0 0;color:var(--okdb-app-fg-muted);font-size:12.5px;cursor:pointer;transition:color .13s,background .13s,border-color .13s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.55;letter-spacing:.01em}.docs-nav__item--standalone{font-weight:500;color:var(--okdb-app-fg);margin-top:2px}.docs-nav__item:hover{background:var(--okdb-app-surface-2);color:var(--okdb-app-fg);border-left-color:var(--okdb-app-border-strong)}.docs-nav__item--active{background:var(--okdb-accent-soft-bg)!important;color:var(--okdb-accent)!important;border-left-color:var(--okdb-accent)!important;font-weight:500}.docs-nav__group{margin-top:6px;padding-top:4px;border-top:1px solid var(--okdb-app-surface)}.docs-nav__group-label{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--okdb-app-fg-faint);padding:4px 14px 5px;pointer-events:none;user-select:none}.docs-wrap{margin:-20px -24px -6px;min-height:calc(100% + 26px)}.docs-wrap ok-doc-viewer{--ok_color-accent:var(--okdb-accent);--ok_color-fg:var(--okdb-app-fg);--ok_color-muted-fg:var(--okdb-app-fg-muted);--ok_color-muted-border:var(--okdb-app-border);--font-mono:'Cascadia Code','Fira Code','Consolas',monospace;display:block;min-height:100%}.docs-wrap ok-doc-viewer .md table{border-collapse:collapse;width:100%;margin:1.2rem 0;font-size:13px}.docs-wrap ok-doc-viewer .md td,.docs-wrap ok-doc-viewer .md th{border:1px solid var(--okdb-app-border);padding:7px 12px;text-align:left}.docs-wrap ok-doc-viewer .md th{background:var(--okdb-app-surface-2);color:#8fc8e8;font-weight:600;font-size:12px;letter-spacing:.03em}.docs-wrap ok-doc-viewer .md tr:hover td{background:var(--okdb-app-surface)}.docs-wrap ok-doc-viewer .md a{color:var(--okdb-accent);text-decoration:none;border-bottom:1px solid color-mix(in srgb,var(--okdb-accent) 27%,transparent);transition:border-color .15s,color .15s}.docs-wrap ok-doc-viewer .md a:hover{color:#7fd8ff;border-bottom-color:#7fd8ff88}.docs-wrap ok-doc-viewer .md code:not(pre code){background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);color:var(--okdb-accent);padding:1px 6px;border-radius:4px;font-size:.9em}.docs-wrap ok-doc-viewer .md blockquote{border-left:3px solid color-mix(in srgb,var(--okdb-accent) 27%,transparent);margin:1rem 0;padding:6px 14px;background:var(--okdb-app-surface);border-radius:0 6px 6px 0;color:var(--okdb-app-fg-muted);font-style:italic}.card{background:var(--okdb-app-surface);border-radius:12px;padding:16px;box-shadow:0 0 5px #ffffff1a}.card-title{font-weight:600;margin-bottom:12px;font-size:14px}.kv-grid{display:grid;grid-template-columns:110px 1fr;gap:6px 12px;font-size:13px}.kv-key{opacity:.6}.kv-val{word-break:break-all}.sys-table{width:100%;border-collapse:collapse;font-size:13px}.sys-table th{text-align:left;padding:6px 8px;border-bottom:1px solid var(--okdb-app-border);opacity:.6;font-weight:500;font-size:11px;text-transform:uppercase;letter-spacing:.04em}.sys-table td{padding:5px 8px;border-bottom:1px solid var(--okdb-app-border)}.sys-table tr:last-child td{border-bottom:none}.sys-table tr:hover td{background:#ffffff08}.cl-th{padding:5px 8px;text-align:left;font-weight:500;font-size:10px;text-transform:uppercase;letter-spacing:.04em;opacity:.5;border-bottom:1px solid var(--okdb-app-border)}.cl-td{padding:5px 8px;border-bottom:1px solid var(--okdb-app-border)}.cl-row:hover .cl-td{background:#ffffff06}</style>
@@ -1 +1 @@
1
- export default{tag:"system-process-panel",context:n=>({tree:[],busy:!1,error:null,expanded:{},_timer:null,get rows(){const n=[],e=(o,s)=>{const r=new Map;for(const n of o||[])r.has(n.kind)||r.set(n.kind,[]),r.get(n.kind).push(n);for(const[o,i]of r){if(i.length>8){const e=`g:${s}:${o}:${i[0]?.processKey}`;if(n.push({group:!0,kind:o,count:i.length,depth:s,key:e,open:!!this.expanded[e]}),!this.expanded[e])continue}for(const o of i){if(n.push({node:o,depth:s}),o.instances&&o.instances.length>1&&this.expanded[`i:${o.processKey}`])for(const e of o.instances)n.push({instance:e,node:o,depth:s+1});o.children&&o.children.length&&e(o.children,s+1)}}};return e(this.tree,0),n},get nodeCount(){let n=0;const e=o=>{for(const s of o||[])n++,s.children&&e(s.children)};return e(this.tree),n},toggle(n){this.expanded={...this.expanded,[n]:!this.expanded[n]}},toggleInstances(n){n?.instances&&n.instances.length>1&&this.toggle(`i:${n.processKey}`)},async load(){this.busy=!0;try{const n=await fetch(`${__ENV__.API}/processes/tree`);if(n.ok){const e=await n.json();this.tree=e.data??e.result??[],this.error=null}}catch(n){this.error=n?.message||String(n)}finally{this.busy=!1}},async doDecommission(e){e?.caps?.decommissionable&&(n.$ok?.shared?.confirm?await n.$ok.shared.confirm(`Decommission ${this.kindLabel(e.kind)} (pid ${e.pid})? It will drain and stay down until recommissioned.`):window.confirm(`Decommission ${this.kindLabel(e.kind)} (pid ${e.pid})?`))&&(e.lifecycle={state:"decommissioning"},await fetch(`${__ENV__.API}/processes/${encodeURIComponent(e.nodeKey)}/decommission`,{method:"POST"}),await this.load())},async doRecommission(e){const o=e?.slotKey||e?.nodeKey;if(!o)return;const s=`Recommission ${this.kindLabel(e.kind)}? The supervisor will fork it again.`;(n.$ok?.shared?.confirm?await n.$ok.shared.confirm(s):window.confirm(s))&&(await fetch(`${__ENV__.API}/processes/${encodeURIComponent(o)}/recommission`,{method:"POST"}),await this.load())},isDecommissioning:n=>"decommissioning"===n?.lifecycle?.state,async doKill(e){if(!e?.caps?.killable)return;const o="primary"===e.kind,s=o?`HARD-KILL the primary (pid ${e.pid})? This tears down the ENTIRE cluster — all workers exit. This cannot be undone.`:`HARD-KILL ${this.kindLabel(e.kind)} (pid ${e.pid})? It is terminated immediately (no drain).`;if(!(n.$ok?.shared?.confirm?await n.$ok.shared.confirm(s):window.confirm(s)))return;const r="force=1"+(o?"&confirm=1":"");await fetch(`${__ENV__.API}/processes/${encodeURIComponent(e.nodeKey)}/kill?${r}`,{method:"POST"}),await this.load()},kindLabel:n=>({primary:"Primary","http-server":"HTTP server","queue-worker":"Queue worker","fn-runner":"Function runner",cli:"CLI",standalone:"Standalone",embedded:"Embedded"}[n]||n||"process"),kindColor(n){switch(n){case"primary":case"standalone":case"cli":return"var(--okdb-accent)";case"http-server":return"var(--okdb-success)";case"queue-worker":case"fn-runner":return"var(--okdb-purple)";case"embedded":return"var(--okdb-app-fg-muted)";default:return"var(--okdb-app-fg-subtle)"}},capabilities(n){const e=n?.role||{},o=[];return e.asyncProcessors&&o.push("process"),e.http&&o.push("serve"),e.engines&&o.push("queue"),"active"===e.compaction&&o.push("compact"),"fn-runner"===n?.kind&&o.push("functions"),o.unshift("ingest"),o},capStrip(n){return this.capabilities(n).join(" · ")},indentPx:n=>22*n,formatUptime(n){if(null==n)return"--";const e=Math.floor(n),o=Math.floor(e/86400),s=Math.floor(e%86400/3600),r=Math.floor(e%3600/60),i=e%60;return o>0?`${o}d ${s}h ${r}m`:s>0?`${s}h ${r}m ${i}s`:r>0?`${r}m ${i}s`:`${i}s`},facetSummary(n){const e=n.processing;return e?{procs:(e.processors||[]).filter(n=>"single"===n.mode||"fanout"===n.mode).length}:null},maxLag(n){const e=n.processing;if(!e)return 0;let o=0;for(const n of e.processors||[])(n.lag||0)>o&&(o=n.lag);return o},lagColor:n=>n?n<1e3?"var(--okdb-warning)":"var(--okdb-danger)":"var(--okdb-success)"}),init(n){this.load(),this._timer=setInterval(()=>this.load(),3e3),this._timer.unref&&this._timer.unref()},unmount(){this._timer&&clearInterval(this._timer),this._timer=null},template:'\n <div style="display:flex;flex-direction:column;gap:16px;">\n\n <div style="display:flex;align-items:center;gap:10px;font-size:12px;color:var(--okdb-method-default);">\n <span>Live processes sharing this root: <strong style="color:var(--okdb-app-fg-strong);">{{ nodeCount }}</strong></span>\n <button @click="load()" style="margin-left:auto;font-size:11px;padding:2px 8px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:4px;cursor:pointer;color:var(--okdb-app-fg);" :disabled="busy">Refresh</button>\n </div>\n\n <if :="error">\n <div style="color:var(--okdb-danger);font-size:12px;padding:8px;border:1px solid var(--okdb-danger);border-radius:6px;">{{ error }}</div>\n </if>\n\n <if :="rows.length === 0">\n <div style="font-size:12px;color:var(--okdb-app-fg-subtle);padding:8px 0;">No processes reporting. The registry is enabled on clustered / supervisor processes.</div>\n </if>\n\n <div style="display:flex;flex-direction:column;font-size:12px;">\n <div style="display:grid;grid-template-columns:2.4fr 1fr 0.8fr 1.2fr 1fr 1.4fr 1.1fr;gap:10px;padding:5px 10px;color:var(--okdb-app-fg-subtle);border-bottom:1px solid var(--okdb-app-border);font-weight:500;">\n <div>Process</div><div>Capabilities</div><div>PID</div><div>Listen</div><div>Uptime</div><div>Processors</div><div></div>\n </div>\n <each :of="rows" let:item="r">\n \x3c!-- collapsed same-kind sibling group (expand to reveal) --\x3e\n <if :="r.group">\n <div @click="toggle(r.key)" :style="\'padding:6px 10px;padding-left:\' + (indentPx(r.depth)+10) + \'px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));cursor:pointer;color:var(--okdb-method-default);display:flex;align-items:center;gap:7px;\'">\n <span style="opacity:.6;">{{ r.open ? \'▾\' : \'▸\' }}</span>\n <span :style="\'font-size:10px;padding:2px 7px;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;color:\' + kindColor(r.kind)">{{ kindLabel(r.kind) }}</span>\n <span style="opacity:.7;">{{ r.count }} processes</span>\n </div>\n </if>\n \x3c!-- instance sub-row of a multi-instance process --\x3e\n <if :="r.instance">\n <div :style="\'display:grid;grid-template-columns:2.4fr 1fr 0.8fr 1.2fr 1fr 1.4fr 1.1fr;gap:10px;padding:4px 10px;padding-left:\' + (indentPx(r.depth)+10) + \'px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));align-items:center;color:var(--okdb-app-fg-subtle);font-size:11px;\'">\n <span style="display:flex;align-items:center;gap:6px;"><span style="opacity:.35;">▪</span> instance <span style="font-family:monospace;">{{ r.instance.instanceId }}</span></span>\n <span>{{ capStrip({ role: r.instance.role, kind: r.node.kind }) }}</span>\n <span></span><span style="font-family:monospace;">{{ r.instance.listenAddr || \'—\' }}</span><span></span>\n <span>\n <if :="r.instance.processing"><span>{{ (r.instance.processing.processors || []).length }} proc</span></if>\n <if :="!r.instance.processing"><span style="opacity:.4;">—</span></if>\n </span>\n <span></span>\n </div>\n </if>\n \x3c!-- a process --\x3e\n <if :="r.node && !r.instance">\n <div style="display:grid;grid-template-columns:2.4fr 1fr 0.8fr 1.2fr 1fr 1.4fr 1.1fr;gap:10px;padding:7px 10px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));align-items:center;color:var(--okdb-method-default);">\n <span :style="\'padding-left:\' + indentPx(r.depth) + \'px;display:flex;align-items:center;gap:7px;\'">\n <if :="r.depth > 0"><span style="opacity:.35;">↳</span></if>\n <span :style="\'font-size:10px;padding:2px 7px;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;color:\' + kindColor(r.node.kind)">{{ kindLabel(r.node.kind) }}</span>\n <span style="font-family:monospace;opacity:.6;font-size:11px;">{{ r.node.host }}</span>\n <if :="r.node.meta?.env"><span :title="\'serves env \' + r.node.meta.env + (r.node.meta.queueType ? \' · \' + r.node.meta.queueType : \'\')" style="font-size:10px;padding:1px 6px;border-radius:4px;background:var(--okdb-app-bg-2);color:var(--okdb-accent);font-family:monospace;">{{ r.node.meta.env }}</span></if>\n <if :="r.node.instances && r.node.instances.length > 1"><span @click="toggleInstances(r.node)" style="font-size:10px;padding:1px 6px;border-radius:4px;background:var(--okdb-app-bg-2);color:var(--okdb-method-default);cursor:pointer;">{{ r.node.instances.length }} instances</span></if>\n <if :="isDecommissioning(r.node)"><span style="font-size:10px;color:var(--okdb-warning);">⏻ decommissioning</span></if>\n </span>\n <span :style="\'font-size:11px;opacity:.85;\'">{{ capStrip(r.node) }}</span>\n <span style="font-family:monospace;">{{ r.node.pid }}</span>\n <span style="font-family:monospace;font-size:11px;opacity:.8;">{{ r.node.listenAddr || \'—\' }}</span>\n <span>{{ formatUptime(r.node.uptime) }}</span>\n <span>\n <if :="facetSummary(r.node)">\n <span>{{ facetSummary(r.node).procs }} proc</span>\n <span :style="\'margin-left:6px;color:\' + lagColor(maxLag(r.node))">lag {{ maxLag(r.node) }}</span>\n </if>\n <if :="!facetSummary(r.node)"><span style="opacity:.4;">—</span></if>\n </span>\n <span style="display:flex;gap:5px;justify-content:flex-end;">\n <if :="r.node.caps?.decommissionable && !isDecommissioning(r.node)">\n <button @click="doDecommission(r.node)" title="Decommission — drain &amp; stay down" style="font-size:10px;padding:2px 8px;border-radius:3px;border:1px solid color-mix(in srgb, var(--okdb-warning) 30%, transparent);background:var(--okdb-app-surface);color:var(--okdb-warning);cursor:pointer;">⏻ Decommission</button>\n </if>\n <if :="isDecommissioning(r.node)">\n <button @click="doRecommission(r.node)" title="Recommission — re-fork this slot" style="font-size:10px;padding:2px 8px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">▶ Recommission</button>\n </if>\n <if :="r.node.caps?.killable">\n <button @click="doKill(r.node)" :title="r.node.kind === \'primary\' ? \'Hard-kill — tears down the whole cluster\' : \'Hard-kill — immediate, no drain\'" style="font-size:10px;padding:2px 8px;border-radius:3px;border:1px solid color-mix(in srgb, var(--okdb-danger) 35%, transparent);background:var(--okdb-app-surface);color:var(--okdb-danger);cursor:pointer;">⨯ Kill</button>\n </if>\n </span>\n </div>\n </if>\n </each>\n </div>\n\n </div>\n '};
1
+ export default{tag:"system-process-panel",context:n=>({tree:[],busy:!1,error:null,expanded:{},_timer:null,get rows(){const n=[],e=(o,s)=>{const r=new Map;for(const n of o||[])r.has(n.kind)||r.set(n.kind,[]),r.get(n.kind).push(n);for(const[o,i]of r){if(i.length>8){const e=`g:${s}:${o}:${i[0]?.processKey}`;if(n.push({group:!0,kind:o,count:i.length,depth:s,key:e,open:!!this.expanded[e]}),!this.expanded[e])continue}for(const o of i){if(n.push({node:o,depth:s}),o.instances&&o.instances.length>1&&this.expanded[`i:${o.processKey}`])for(const e of o.instances)n.push({instance:e,node:o,depth:s+1});o.children&&o.children.length&&e(o.children,s+1)}}};return e(this.tree,0),n},get nodeCount(){let n=0;const e=o=>{for(const s of o||[])n++,s.children&&e(s.children)};return e(this.tree),n},toggle(n){this.expanded={...this.expanded,[n]:!this.expanded[n]}},toggleInstances(n){n?.instances&&n.instances.length>1&&this.toggle(`i:${n.processKey}`)},async load(){this.busy=!0;try{const n=await fetch(`${__ENV__.API}/processes/tree`);if(n.ok){const e=await n.json();this.tree=e.data??e.result??[],this.error=null}}catch(n){this.error=n?.message||String(n)}finally{this.busy=!1}},async doDecommission(e){e?.caps?.decommissionable&&(n.$ok?.shared?.confirm?await n.$ok.shared.confirm(`Decommission ${this.kindLabel(e.kind)} (pid ${e.pid})? It will drain and stay down until recommissioned.`):window.confirm(`Decommission ${this.kindLabel(e.kind)} (pid ${e.pid})?`))&&(e.lifecycle={state:"decommissioning"},await fetch(`${__ENV__.API}/processes/${encodeURIComponent(e.nodeKey)}/decommission`,{method:"POST"}),await this.load())},async doRecommission(e){const o=e?.slotKey||e?.nodeKey;if(!o)return;const s=`Recommission ${this.kindLabel(e.kind)}? The supervisor will fork it again.`;(n.$ok?.shared?.confirm?await n.$ok.shared.confirm(s):window.confirm(s))&&(await fetch(`${__ENV__.API}/processes/${encodeURIComponent(o)}/recommission`,{method:"POST"}),await this.load())},isDecommissioning:n=>"decommissioning"===n?.lifecycle?.state,async doKill(e){if(!e?.caps?.killable)return;const o="primary"===e.kind,s=o?`HARD-KILL the primary (pid ${e.pid})? This tears down the ENTIRE cluster — all workers exit. This cannot be undone.`:`HARD-KILL ${this.kindLabel(e.kind)} (pid ${e.pid})? It is terminated immediately (no drain).`;if(!(n.$ok?.shared?.confirm?await n.$ok.shared.confirm(s):window.confirm(s)))return;const r="force=1"+(o?"&confirm=1":"");await fetch(`${__ENV__.API}/processes/${encodeURIComponent(e.nodeKey)}/kill?${r}`,{method:"POST"}),await this.load()},kindLabel:n=>({primary:"Primary","http-server":"HTTP server","queue-worker":"Queue worker","fn-runner":"Function runner",cli:"CLI",standalone:"Standalone",embedded:"Embedded"}[n]||n||"process"),kindColor(n){switch(n){case"primary":case"standalone":case"cli":return"var(--okdb-accent)";case"http-server":return"var(--okdb-success)";case"queue-worker":case"fn-runner":return"var(--okdb-purple)";case"embedded":return"var(--okdb-app-fg-muted)";default:return"var(--okdb-app-fg-subtle)"}},capabilities(n){const e=n?.role||{},o=[];o.push("ingest"),e.http&&o.push("serve"),e.processors&&o.push("process"),e.engines&&o.push("queue");const s=n?.singletonClaims||{};if(s.compact&&s.compact.length)for(const n of s.compact)o.push("compacting:"+n);else!0===e.compaction&&o.push("compact");for(const n of s.replicate||[])o.push("replicate:"+n);for(const e of n?.engines||[])o.push("serves "+e);return"fn-runner"===n?.kind&&o.push("functions"),o},capStrip(n){return this.capabilities(n).join(" · ")},indentPx:n=>22*n,formatUptime(n){if(null==n)return"--";const e=Math.floor(n),o=Math.floor(e/86400),s=Math.floor(e%86400/3600),r=Math.floor(e%3600/60),i=e%60;return o>0?`${o}d ${s}h ${r}m`:s>0?`${s}h ${r}m ${i}s`:r>0?`${r}m ${i}s`:`${i}s`},sandboxLabel(n){const e=n?.threads;return e&&e.count?(e.busy||0)>0?`${e.count} (${e.busy}▶)`:String(e.count):"—"},facetSummary(n){const e=n.processing;return e?{procs:(e.processors||[]).filter(n=>"single"===n.mode||"fanout"===n.mode).length}:null},maxLag(n){const e=n.processing;if(!e)return 0;let o=0;for(const n of e.processors||[])(n.lag||0)>o&&(o=n.lag);return o},lagColor:n=>n?n<1e3?"var(--okdb-warning)":"var(--okdb-danger)":"var(--okdb-success)"}),init(n){this.load(),this._timer=setInterval(()=>this.load(),3e3),this._timer.unref&&this._timer.unref()},unmount(){this._timer&&clearInterval(this._timer),this._timer=null},template:'\n <div style="display:flex;flex-direction:column;gap:16px;">\n\n <div style="display:flex;align-items:center;gap:10px;font-size:12px;color:var(--okdb-method-default);">\n <span>Live processes sharing this root: <strong style="color:var(--okdb-app-fg-strong);">{{ nodeCount }}</strong></span>\n <button @click="load()" style="margin-left:auto;font-size:11px;padding:2px 8px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:4px;cursor:pointer;color:var(--okdb-app-fg);" :disabled="busy">Refresh</button>\n </div>\n\n <if :="error">\n <div style="color:var(--okdb-danger);font-size:12px;padding:8px;border:1px solid var(--okdb-danger);border-radius:6px;">{{ error }}</div>\n </if>\n\n <if :="rows.length === 0">\n <div style="font-size:12px;color:var(--okdb-app-fg-subtle);padding:8px 0;">No processes reporting. The registry is enabled on clustered / supervisor processes.</div>\n </if>\n\n <div style="display:flex;flex-direction:column;font-size:12px;">\n <div style="display:grid;grid-template-columns:2.4fr 1fr 0.7fr 1.1fr 0.9fr 0.8fr 1.3fr 1fr;gap:10px;padding:5px 10px;color:var(--okdb-app-fg-subtle);border-bottom:1px solid var(--okdb-app-border);font-weight:500;">\n <div>Process</div><div>Capabilities</div><div>PID</div><div>Listen</div><div>Uptime</div><div>Sandbox</div><div>Processors</div><div></div>\n </div>\n <each :of="rows" let:item="r">\n \x3c!-- collapsed same-kind sibling group (expand to reveal) --\x3e\n <if :="r.group">\n <div @click="toggle(r.key)" :style="\'padding:6px 10px;padding-left:\' + (indentPx(r.depth)+10) + \'px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));cursor:pointer;color:var(--okdb-method-default);display:flex;align-items:center;gap:7px;\'">\n <span style="opacity:.6;">{{ r.open ? \'▾\' : \'▸\' }}</span>\n <span :style="\'font-size:10px;padding:2px 7px;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;color:\' + kindColor(r.kind)">{{ kindLabel(r.kind) }}</span>\n <span style="opacity:.7;">{{ r.count }} processes</span>\n </div>\n </if>\n \x3c!-- instance sub-row of a multi-instance process --\x3e\n <if :="r.instance">\n <div :style="\'display:grid;grid-template-columns:2.4fr 1fr 0.7fr 1.1fr 0.9fr 0.8fr 1.3fr 1fr;gap:10px;padding:4px 10px;padding-left:\' + (indentPx(r.depth)+10) + \'px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));align-items:center;color:var(--okdb-app-fg-subtle);font-size:11px;\'">\n <span style="display:flex;align-items:center;gap:6px;"><span style="opacity:.35;">▪</span> instance <span style="font-family:monospace;">{{ r.instance.instanceId }}</span></span>\n <span>{{ capStrip({ role: r.instance.role, kind: r.node.kind }) }}</span>\n <span></span><span style="font-family:monospace;">{{ r.instance.listenAddr || \'—\' }}</span><span></span>\n <span style="opacity:.4;">—</span>\n <span>\n <if :="r.instance.processing"><span>{{ (r.instance.processing.processors || []).length }} proc</span></if>\n <if :="!r.instance.processing"><span style="opacity:.4;">—</span></if>\n </span>\n <span></span>\n </div>\n </if>\n \x3c!-- a process --\x3e\n <if :="r.node && !r.instance">\n <div style="display:grid;grid-template-columns:2.4fr 1fr 0.7fr 1.1fr 0.9fr 0.8fr 1.3fr 1fr;gap:10px;padding:7px 10px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));align-items:center;color:var(--okdb-method-default);">\n <span :style="\'padding-left:\' + indentPx(r.depth) + \'px;display:flex;align-items:center;gap:7px;\'">\n <if :="r.depth > 0"><span style="opacity:.35;">↳</span></if>\n <span :style="\'font-size:10px;padding:2px 7px;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;color:\' + kindColor(r.node.kind)">{{ kindLabel(r.node.kind) }}</span>\n <if :="r.node.meta?.name"><strong style="color:var(--okdb-app-fg-strong);font-size:12px;">{{ r.node.meta.name }}</strong></if>\n <span style="font-family:monospace;opacity:.6;font-size:11px;">{{ r.node.host }}</span>\n <if :="r.node.meta?.env"><span :title="\'serves env \' + r.node.meta.env + (r.node.meta.queueType ? \' · \' + r.node.meta.queueType : \'\')" style="font-size:10px;padding:1px 6px;border-radius:4px;background:var(--okdb-app-bg-2);color:var(--okdb-accent);font-family:monospace;">{{ r.node.meta.env }}</span></if>\n <if :="r.node.instances && r.node.instances.length > 1"><span @click="toggleInstances(r.node)" style="font-size:10px;padding:1px 6px;border-radius:4px;background:var(--okdb-app-bg-2);color:var(--okdb-method-default);cursor:pointer;">{{ r.node.instances.length }} instances</span></if>\n <if :="isDecommissioning(r.node)"><span style="font-size:10px;color:var(--okdb-warning);">⏻ decommissioning</span></if>\n </span>\n <span :style="\'font-size:11px;opacity:.85;\'">{{ capStrip(r.node) }}</span>\n <span style="font-family:monospace;">{{ r.node.pid }}</span>\n <span style="font-family:monospace;font-size:11px;opacity:.8;">{{ r.node.listenAddr || \'—\' }}</span>\n <span>{{ formatUptime(r.node.uptime) }}</span>\n <span :title="r.node.stats && r.node.stats.threads && r.node.stats.threads.count ? (r.node.stats.threads.count + \' function-sandbox thread(s), \' + (r.node.stats.threads.busy||0) + \' invocation(s) in flight\') : \'no sandbox thread (spawns on first function call)\'" style="font-family:monospace;font-size:11px;">{{ sandboxLabel(r.node.stats) }}</span>\n <span>\n <if :="facetSummary(r.node)">\n <span>{{ facetSummary(r.node).procs }} proc</span>\n <span :style="\'margin-left:6px;color:\' + lagColor(maxLag(r.node))">lag {{ maxLag(r.node) }}</span>\n </if>\n <if :="!facetSummary(r.node)"><span style="opacity:.4;">—</span></if>\n </span>\n <span style="display:flex;gap:5px;justify-content:flex-end;">\n <if :="r.node.caps?.decommissionable && !isDecommissioning(r.node)">\n <button @click="doDecommission(r.node)" title="Decommission — drain &amp; stay down" style="font-size:10px;padding:2px 8px;border-radius:3px;border:1px solid color-mix(in srgb, var(--okdb-warning) 30%, transparent);background:var(--okdb-app-surface);color:var(--okdb-warning);cursor:pointer;">⏻ Decommission</button>\n </if>\n <if :="isDecommissioning(r.node)">\n <button @click="doRecommission(r.node)" title="Recommission — re-fork this slot" style="font-size:10px;padding:2px 8px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">▶ Recommission</button>\n </if>\n <if :="r.node.caps?.killable">\n <button @click="doKill(r.node)" :title="r.node.kind === \'primary\' ? \'Hard-kill — tears down the whole cluster\' : \'Hard-kill — immediate, no drain\'" style="font-size:10px;padding:2px 8px;border-radius:3px;border:1px solid color-mix(in srgb, var(--okdb-danger) 35%, transparent);background:var(--okdb-app-surface);color:var(--okdb-danger);cursor:pointer;">⨯ Kill</button>\n </if>\n </span>\n </div>\n </if>\n </each>\n </div>\n\n </div>\n '};
@@ -1 +1 @@
1
- import e from"../../../stores/events-store.js";export default{tag:"system-processing-panel",context:e=>({status:{policy:""},processors:[],busy:!1,actionBusy:null,_timer:null,_unsubs:[],get tierProcessors(){return this.processors.filter(e=>"single"===e.mode||"fanout"===e.mode)},get inlineCount(){return this.processors.filter(e=>"inline"===e.mode).length},async load(){this.busy=!0;try{const[e,o]=await Promise.all([fetch(`${__ENV__.API}/processing/status`),fetch(`${__ENV__.API}/processors`)]);if(e.ok){const o=await e.json();this.status=o.data??o.result??{policy:""}}if(o.ok){const e=await o.json();this.processors=e.data??e.result??[]}}catch{}finally{this.busy=!1}},async _action(o,r,t){if(!this.actionBusy){this.actionBusy=o;try{const o=await r();if(o&&!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message||e?.error||`HTTP ${o.status}`)}t&&e.$ok.shared?.toast?.success?.(t),await this.load()}catch(o){e.$ok.shared?.toast?.error?.(o?.message||"action failed")}finally{this.actionBusy=null}}},btnIcon(e,o){return this.actionBusy===e?"⏳":o},doPause(e){return this._action(`pause:${e}`,()=>fetch(`${__ENV__.API}/processors/${encodeURIComponent(e)}/pause`,{method:"POST"}),"processor paused")},doResume(e){return this._action(`resume:${e}`,()=>fetch(`${__ENV__.API}/processors/${encodeURIComponent(e)}/resume`,{method:"POST"}),"processor resumed")},doRetry(e){return this._action(`retry:${e}`,()=>fetch(`${__ENV__.API}/processors/${encodeURIComponent(e)}/retry`,{method:"POST"}),"retry requested")},async doResetCursor(o,r){if(!o)return;const t=`Reset cursor "${o}"${r?` in ${r}`:""}? This reprocesses every document of this type from the beginning — a full rebuild.`;return(e.$ok?.shared?.confirm?await e.$ok.shared.confirm(t):window.confirm(t))?this._action(`reset:${o}`,()=>fetch(`${__ENV__.API}/processors/cursor/reset`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({cursorKey:o,env:r})}),"cursor reset requested — full reprocess will start"):void 0},lagColor:e=>0===e?"var(--okdb-success)":e<1e3?"var(--okdb-warning)":"var(--okdb-danger)",stateColor(e){switch(e){case"online":return"var(--okdb-success)";case"building":return"var(--okdb-warning)";case"error":return"var(--okdb-danger)";case"waiting":return"var(--okdb-method-default)";case"paused":return"#818cf8";case"delegated":return"var(--okdb-accent)";default:return"var(--okdb-app-fg-subtle)"}},procStateLabel:e=>e.claimGated?"delegated":e.paused?"paused":e.state,isOnline:e=>!e.paused&&"online"===e.state,isUserPaused:e=>e.paused&&!e.claimGated,isError:e=>"error"===e.state,stateDetail:e=>e.claimGated?e.heldBy?`draining on worker pid ${e.heldBy.pid}${e.heldBy.hostname?" on "+e.heldBy.hostname:""}`:"delegated to workers — not yet claimed":"building"===e.state&&"worker"===e.progress?.placement?"running"===e.progress.state&&e.progress.claimedBy?`bootstrap running on worker pid ${e.progress.claimedBy}`:"bootstrap queued on workers":"online"!==e.state&&"building"!==e.state||"worker"!==e.owner?.kind?"waiting"!==e.state&&"error"!==e.state||!e.error?"":e.error:`on worker pid ${e.owner.pid}`,_logHeadKnown:e=>e.logHead>=e.lastClock&&(e.logHead>0||0===e.envClock),clockLabel(e){return null==e.logHead||null==e.envClock?`${e.lastClock} / ${e.headClock}`:this._logHeadKnown(e)?`${e.lastClock} / ${e.logHead}`:`${e.lastClock} / —`},clockTitle(e){return null==e.logHead||null==e.envClock||this._logHeadKnown(e)?"":`head unknown: the change log for this type has no entries at/after the cursor (old entries are deleted by compaction/sync-GC once processed). Nothing is pending; the head shows again on the next write to this type. Cursor ${e.lastClock} · env clock ${e.envClock}.`}}),init(o){this.load(),this._timer=setInterval(()=>this.load(),3e3),this._timer.unref&&this._timer.unref();const r=()=>this.load();this._unsubs=[e.on("processor:ready",r),e.on("processor:error",r),e.on("processor:paused",r),e.on("processor:resumed",r),e.on("processor:retry",r)]},unmount(){this._timer&&clearInterval(this._timer),this._timer=null;for(const e of this._unsubs)e();this._unsubs=[]},template:'\n <div style="display:flex;flex-direction:column;gap:20px;">\n\n <div style="display:flex;align-items:center;gap:10px;font-size:12px;color:var(--okdb-method-default);">\n <span>Policy: <strong style="color:var(--okdb-app-fg-strong);">{{ status.policy || \'unknown\' }}</strong></span>\n <button @click="load()" style="margin-left:auto;font-size:11px;padding:2px 8px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:4px;cursor:pointer;color:var(--okdb-app-fg);" :disabled="busy">Refresh</button>\n </div>\n\n \x3c!-- ── Async/worker processors (the tier; inline excluded) ── --\x3e\n <div>\n <div style="font-size:12px;font-weight:600;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px;">Async &amp; Worker Processors</div>\n <if :="tierProcessors.length === 0">\n <div style="font-size:12px;color:var(--okdb-app-fg-subtle);padding:8px 0;">No async or worker processors running.</div>\n </if>\n <if :="tierProcessors.length > 0">\n <div style="display:flex;flex-direction:column;font-size:12px;">\n <div style="display:grid;grid-template-columns:1fr 1.4fr 0.9fr 0.9fr 1.3fr 0.7fr 1.8fr;gap:10px;padding:5px 10px;color:var(--okdb-app-fg-subtle);border-bottom:1px solid var(--okdb-app-border);font-weight:500;">\n <div>Env</div><div>Processor</div><div>Mode</div><div>State</div><div>Clock</div><div>Lag</div><div></div>\n </div>\n <each :of="tierProcessors" let:item="p">\n <div style="display:grid;grid-template-columns:1fr 1.4fr 0.9fr 0.9fr 1.3fr 0.7fr 1.8fr;gap:10px;padding:7px 10px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));align-items:center;color:var(--okdb-method-default);">\n <span style="color:var(--okdb-accent);">{{ p.env }}</span>\n <span><strong style="color:var(--okdb-app-fg);">{{ p.meta?.name || p.type }}</strong></span>\n <span>{{ p.mode }}</span>\n <span :title="stateDetail(p)">\n <span :style="\'color:\' + stateColor(procStateLabel(p))">{{ procStateLabel(p) }}</span>\n <if :="stateDetail(p)">\n <span style="display:block;font-size:10px;opacity:.75;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ stateDetail(p) }}</span>\n </if>\n </span>\n <span style="font-family:monospace;" :title="clockTitle(p)">{{ clockLabel(p) }}</span>\n <span :style="\'color:\' + lagColor(p.lag)">{{ p.lag }}</span>\n <span style="display:flex;gap:5px;justify-content:flex-end;">\n <if :="isOnline(p)">\n <button @click="doPause(p.id)" :disabled="!!actionBusy" title="Pause" style="font-size:10px;padding:2px 7px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">{{ btnIcon(\'pause:\' + p.id, \'⏸\') }}</button>\n </if>\n <if :="isUserPaused(p)">\n <button @click="doResume(p.id)" :disabled="!!actionBusy" title="Resume" style="font-size:10px;padding:2px 7px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">{{ btnIcon(\'resume:\' + p.id, \'▶\') }}</button>\n </if>\n <if :="isError(p)">\n <button @click="doRetry(p.id)" :disabled="!!actionBusy" title="Retry" style="font-size:10px;padding:2px 7px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">{{ btnIcon(\'retry:\' + p.id, \'↻\') }}</button>\n </if>\n <if :="p.cursorKey">\n <button @click="doResetCursor(p.cursorKey, p.env)" :disabled="!!actionBusy" title="Reset cursor — full reprocess" style="font-size:10px;padding:2px 7px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">{{ btnIcon(\'reset:\' + p.cursorKey, \'⟲\') }}</button>\n </if>\n </span>\n </div>\n </each>\n </div>\n </if>\n <if :="inlineCount > 0">\n <div style="font-size:11px;color:var(--okdb-app-fg-subtle);padding:8px 2px 0;opacity:.7;">\n + {{ inlineCount }} inline processor(s) (index/view maintenance) — synchronous &amp; always consistent, not scheduled by the tier.\n </div>\n </if>\n </div>\n\n </div>\n '};
1
+ import e from"../../../stores/events-store.js";export default{tag:"system-processing-panel",context:e=>({status:{},processors:[],busy:!1,actionBusy:null,_timer:null,_unsubs:[],get tierProcessors(){return this.processors.filter(e=>"single"===e.mode||"fanout"===e.mode)},get inlineCount(){return this.processors.filter(e=>"inline"===e.mode||"sync"===e.mode).length},get writerGroups(){const e=this.status?.writersByProcess;if(Array.isArray(e)&&e.length>0)return e.map(e=>({label:`pid ${e.pid}${e.name?" — "+e.name:e.kind?" — "+e.kind:""}${e.self?" (this process)":""}`,rows:Object.entries(e.writers||{}).map(([e,t])=>({env:e,...t}))}));const t=this.status?.writers||{},o=Object.entries(t).map(([e,t])=>({env:e,...t}));return o.length?[{label:"this process",rows:o}]:[]},get writerRows(){return this.writerGroups.flatMap(e=>e.rows)},get hasBusyWriter(){return this.writerRows.some(e=>e.depth>0||e.draining||e.oldestPendingMs>250)},async load(){this.busy=!0;try{const[e,t]=await Promise.all([fetch(`${__ENV__.API}/processors/status`),fetch(`${__ENV__.API}/processors`)]);if(e.ok){const t=await e.json();this.status=t.data??t.result??{}}if(t.ok){const e=await t.json();this.processors=e.data??e.result??[]}}catch{}finally{this.busy=!1}},async _action(t,o,r){if(!this.actionBusy){this.actionBusy=t;try{const t=await o();if(t&&!t.ok){const e=await t.json().catch(()=>({}));throw new Error(e?.error?.message||e?.error||`HTTP ${t.status}`)}r&&e.$ok.shared?.toast?.success?.(r),await this.load()}catch(t){e.$ok.shared?.toast?.error?.(t?.message||"action failed")}finally{this.actionBusy=null}}},btnIcon(e,t){return this.actionBusy===e?"⏳":t},doPause(e){return this._action(`pause:${e}`,()=>fetch(`${__ENV__.API}/processors/${encodeURIComponent(e)}/pause`,{method:"POST"}),"processor paused")},doResume(e){return this._action(`resume:${e}`,()=>fetch(`${__ENV__.API}/processors/${encodeURIComponent(e)}/resume`,{method:"POST"}),"processor resumed")},doRetry(e){return this._action(`retry:${e}`,()=>fetch(`${__ENV__.API}/processors/${encodeURIComponent(e)}/retry`,{method:"POST"}),"retry requested")},async doResetCursor(t,o){if(!t)return;const r=`Reset cursor "${t}"${o?` in ${o}`:""}? This reprocesses every document of this type from the beginning — a full rebuild.`;return(e.$ok?.shared?.confirm?await e.$ok.shared.confirm(r):window.confirm(r))?this._action(`reset:${t}`,()=>fetch(`${__ENV__.API}/processors/cursor/reset`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({cursorKey:t,env:o})}),"cursor reset requested — full reprocess will start"):void 0},modeLabel:e=>"single"===e.mode?"single · 1-of-N":"fanout"===e.mode?"fanout · N-of-N":"inline"===e.mode||"sync"===e.mode?"sync":e.mode,lagColor:e=>0===e?"var(--okdb-success)":e<1e3?"var(--okdb-warning)":"var(--okdb-danger)",ageColor:e=>!e||e<250?"var(--okdb-success)":e<2e3?"var(--okdb-warning)":"var(--okdb-danger)",stateColor(e){switch(e){case"online":return"var(--okdb-success)";case"building":return"var(--okdb-warning)";case"error":case"unclaimed":return"var(--okdb-danger)";case"waiting":return"var(--okdb-method-default)";case"paused":return"#818cf8";case"not claiming":return"var(--okdb-accent)";default:return"var(--okdb-app-fg-subtle)"}},procStateLabel:e=>e.unclaimed?"unclaimed":e.claimGated?"not claiming":e.paused?"paused":e.state,isOnline:e=>!e.paused&&"online"===e.state,isUserPaused:e=>e.paused&&!e.claimGated,isError:e=>"error"===e.state,handlerLabel(e){if(e.handler)return e.handler;if(e.module){const t=(e.module.path||"").split("/").pop()||e.module.path||"";return e.module.export?`${e.module.export} @ ${t}`:t}return null},stateDetail(e){if(e.unclaimed)return"no live claimant — add a node with processors:true to cover it";if(e.claimGated)return e.heldBy?`held by pid ${e.heldBy.pid}${e.heldBy.hostname?" on "+e.heldBy.hostname:""}`:"this node does not claim — no participating node has claimed it yet";if("online"===e.state||"building"===e.state){const t=e.heldBy?.pid??e.owner?.pid??null;return null!=t?`held by pid ${t}${e.owner?.kind&&"primary"!==e.owner.kind?` (${e.owner.kind})`:""}`:""}return"waiting"===e.state?e.heldBy?`standby — held by pid ${e.heldBy.pid}${e.heldBy.hostname?" on "+e.heldBy.hostname:""}`:e.error||"standby — contending for the lease":"error"===e.state&&e.error?e.error:""},_logHeadKnown:e=>e.logHead>=e.lastClock&&(e.logHead>0||0===e.envClock),clockLabel(e){return null==e.logHead||null==e.envClock?`${e.lastClock} / ${e.headClock}`:this._logHeadKnown(e)?`${e.lastClock} / ${e.logHead}`:`${e.lastClock} / —`},clockTitle(e){return null==e.logHead||null==e.envClock||this._logHeadKnown(e)?"":`head unknown: the change log for this type has no entries at/after the cursor (old entries are deleted by compaction/sync-GC once processed). Nothing is pending; the head shows again on the next write to this type. Cursor ${e.lastClock} · env clock ${e.envClock}.`}}),init(t){this.load(),this._timer=setInterval(()=>this.load(),3e3),this._timer.unref&&this._timer.unref();const o=()=>this.load();this._unsubs=[e.on("processor:ready",o),e.on("processor:error",o),e.on("processor:paused",o),e.on("processor:resumed",o),e.on("processor:retry",o)]},unmount(){this._timer&&clearInterval(this._timer),this._timer=null;for(const e of this._unsubs)e();this._unsubs=[]},template:'\n <div style="display:flex;flex-direction:column;gap:20px;">\n\n <div style="display:flex;align-items:center;gap:16px;font-size:12px;color:var(--okdb-method-default);">\n <span title="Nodes with processors:true (participate in claiming) vs total live nodes">Participating: <strong style="color:var(--okdb-app-fg-strong);">{{ status.participation ? (status.participation.participating + \' / \' + status.participation.total + \' nodes\') : \'—\' }}</strong></span>\n <button @click="load()" style="margin-left:auto;font-size:11px;padding:2px 8px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:4px;cursor:pointer;color:var(--okdb-app-fg);" :disabled="busy">Refresh</button>\n </div>\n\n \x3c!-- ── Background processors (single/fanout; inline excluded) ── --\x3e\n <div>\n <div style="font-size:12px;font-weight:600;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px;">Background Processors</div>\n <if :="tierProcessors.length === 0">\n <div style="font-size:12px;color:var(--okdb-app-fg-subtle);padding:8px 0;">No background processors registered.</div>\n </if>\n <if :="tierProcessors.length > 0">\n <div style="display:flex;flex-direction:column;font-size:12px;">\n <div style="display:grid;grid-template-columns:1fr 1.4fr 0.9fr 0.9fr 1.3fr 0.7fr 1.8fr;gap:10px;padding:5px 10px;color:var(--okdb-app-fg-subtle);border-bottom:1px solid var(--okdb-app-border);font-weight:500;">\n <div>Env</div><div>Processor</div><div>Mode</div><div>State</div><div>Clock</div><div>Lag</div><div></div>\n </div>\n <each :of="tierProcessors" let:item="p">\n <div style="display:grid;grid-template-columns:1fr 1.4fr 0.9fr 0.9fr 1.3fr 0.7fr 1.8fr;gap:10px;padding:7px 10px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));align-items:center;color:var(--okdb-method-default);">\n <span style="color:var(--okdb-accent);">{{ p.env }}</span>\n <span>\n <strong style="color:var(--okdb-app-fg);">{{ p.meta?.name || p.type }}</strong>\n <if :="handlerLabel(p)">\n <span style="display:block;font-size:10px;opacity:.6;font-family:monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" :title="handlerLabel(p)">{{ handlerLabel(p) }}</span>\n </if>\n </span>\n <span>{{ modeLabel(p) }}</span>\n <span :title="stateDetail(p)">\n <span :style="\'color:\' + stateColor(procStateLabel(p))">{{ procStateLabel(p) }}</span>\n <if :="stateDetail(p)">\n <span style="display:block;font-size:10px;opacity:.75;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ stateDetail(p) }}</span>\n </if>\n </span>\n <span style="font-family:monospace;" :title="clockTitle(p)">{{ clockLabel(p) }}</span>\n <span :style="\'color:\' + lagColor(p.lag)">{{ p.lag }}</span>\n <span style="display:flex;gap:5px;justify-content:flex-end;">\n <if :="isOnline(p)">\n <button @click="doPause(p.id)" :disabled="!!actionBusy" title="Pause" style="font-size:10px;padding:2px 7px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">{{ btnIcon(\'pause:\' + p.id, \'⏸\') }}</button>\n </if>\n <if :="isUserPaused(p)">\n <button @click="doResume(p.id)" :disabled="!!actionBusy" title="Resume" style="font-size:10px;padding:2px 7px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">{{ btnIcon(\'resume:\' + p.id, \'▶\') }}</button>\n </if>\n <if :="isError(p)">\n <button @click="doRetry(p.id)" :disabled="!!actionBusy" title="Retry" style="font-size:10px;padding:2px 7px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">{{ btnIcon(\'retry:\' + p.id, \'↻\') }}</button>\n </if>\n <if :="p.cursorKey">\n <button @click="doResetCursor(p.cursorKey, p.env)" :disabled="!!actionBusy" title="Reset cursor — full reprocess" style="font-size:10px;padding:2px 7px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">{{ btnIcon(\'reset:\' + p.cursorKey, \'⟲\') }}</button>\n </if>\n </span>\n </div>\n </each>\n </div>\n </if>\n <if :="inlineCount > 0">\n <div style="font-size:11px;color:var(--okdb-app-fg-subtle);padding:8px 2px 0;opacity:.7;">\n + {{ inlineCount }} sync (inline) processor(s) (index/view maintenance) — run inside every commit, always consistent, nothing to schedule.\n </div>\n </if>\n </div>\n\n \x3c!-- ── Write queue: one section per PROCESS (each process has its own per-env writer) ── --\x3e\n <if :="writerGroups.length > 0">\n <div>\n <div style="font-size:12px;font-weight:600;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px;">Write Queue (per process · per env)</div>\n <each :of="writerGroups" let:item="g">\n <div style="margin-bottom:12px;">\n <div style="font-size:11px;color:var(--okdb-app-fg-subtle);padding:2px 2px 4px;font-family:monospace;">{{ g.label }}</div>\n <div style="display:flex;flex-direction:column;font-size:12px;">\n <div style="display:grid;grid-template-columns:1.3fr 0.8fr 0.8fr 1fr 0.8fr 0.8fr 0.8fr;gap:10px;padding:5px 10px;color:var(--okdb-app-fg-subtle);border-bottom:1px solid var(--okdb-app-border);font-weight:500;">\n <div>Env</div><div title="Writes in flight across all processes (beforeWrite→afterWrite)">In-flight</div><div title="Child-transactions issued to LMDB but not yet committed by this process">Queue depth</div><div title="Age of the oldest un-committed write — the stall signal (seconds = stuck/parked, not busy)">Oldest pending</div><div title="Commits completed in the last second">Commits/s</div><div title="p99 of recent commit durations">Commit p99</div><div title="Writes are parked because a compaction drain is in progress on this env">Drain</div>\n </div>\n <each :of="g.rows" let:item="w">\n <div style="display:grid;grid-template-columns:1.3fr 0.8fr 0.8fr 1fr 0.8fr 0.8fr 0.8fr;gap:10px;padding:7px 10px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));align-items:center;color:var(--okdb-method-default);">\n <span style="color:var(--okdb-accent);">{{ w.env }}</span>\n <span style="font-family:monospace;">{{ w.activeWriters == null ? \'—\' : w.activeWriters }}</span>\n <span style="font-family:monospace;">{{ w.depth }}</span>\n <span style="font-family:monospace;" :style="\'color:\' + ageColor(w.oldestPendingMs)">{{ w.oldestPendingMs }}ms</span>\n <span style="font-family:monospace;">{{ w.commitsPerSec }}</span>\n <span style="font-family:monospace;">{{ w.commitP99Ms }}ms</span>\n <span :style="\'color:\' + (w.draining ? \'var(--okdb-danger)\' : \'var(--okdb-app-fg-subtle)\')">{{ w.draining ? \'parked\' : \'—\' }}</span>\n </div>\n </each>\n </div>\n </div>\n </each>\n <div style="font-size:11px;color:var(--okdb-app-fg-subtle);padding:0 2px;opacity:.7;">\n Each process\'s own async writer, from its registry heartbeat (this process\'s rows are live). A deep queue that drains fast is just busy; a small depth with a multi-second “oldest pending”, or “parked”, is the stall.\n </div>\n </div>\n </if>\n\n </div>\n '};
@@ -1 +1 @@
1
- export default{tag:"system-runtime-overview",context:()=>({procCount:null,workers:null,procs:null,busy:!1,_timer:null,countTree(r){let e=0;const t=r=>{for(const o of r||[])e++,o.children&&t(o.children)};return t(r),e},async load(){this.busy=!0;try{const[r,e,t]=await Promise.all([fetch(`${__ENV__.API}/processes/tree`).catch(()=>null),fetch(`${__ENV__.API}/workers`).catch(()=>null),fetch(`${__ENV__.API}/processors/status`).catch(()=>null)]);if(r?.ok){const e=await r.json();this.procCount=this.countTree(e.data??e.result??[])}if(e?.ok){const r=await e.json();this.workers=r.data??r.result??null}if(t?.ok){const r=await t.json(),e=r.data??r.result??{},o=e.envs??e,s=[];for(const r of Object.keys(o||{}))if(Array.isArray(o[r]))for(const e of o[r])s.push(e);this.procs=s}}catch{}finally{this.busy=!1}},get workerLine(){const r=this.workers;if(!r)return"";if(0===r.scale)return"off (self-processing)";const e="auto"===r.scale?"auto":null!=r.scale?.max?`${r.scale.min}–${r.scale.max}`:`min ${r.scale?.min}`;return`${r.live} live · desired ${e}`},get procHealth(){if(!this.procs)return{text:"—",color:"var(--okdb-app-fg-subtle)"};if(0===this.procs.length)return{text:"none registered",color:"var(--okdb-app-fg-subtle)"};const r=this.procs.filter(r=>"error"===r.state).length,e=this.procs.filter(r=>(r.lag||0)>1e3).length;return r?{text:`${r} error`,color:"var(--okdb-danger)"}:e?{text:`${e} lagging`,color:"var(--okdb-warning)"}:{text:"all caught up",color:"var(--okdb-success)"}}}),init(){this.load(),this._timer=setInterval(()=>this.load(),4e3),this._timer.unref&&this._timer.unref()},unmount(){this._timer&&clearInterval(this._timer),this._timer=null},template:'\n <div style="display:flex;flex-direction:column;gap:14px;">\n <div style="font-size:12px;color:var(--okdb-method-default);">A snapshot of everything running under this root. Open a tab for detail and controls.</div>\n <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">\n\n \x3c!-- Processes --\x3e\n <div style="padding:14px 16px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:8px;">\n <div style="font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--okdb-app-fg-subtle);">Processes</div>\n <div style="font-size:26px;font-weight:600;color:var(--okdb-app-fg-strong);margin-top:4px;">{{ procCount == null ? \'—\' : procCount }}</div>\n <div style="font-size:11px;color:var(--okdb-app-fg-subtle);">live processes under this root</div>\n </div>\n\n \x3c!-- Workers --\x3e\n <div style="padding:14px 16px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:8px;">\n <div style="font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--okdb-app-fg-subtle);">Workers</div>\n <div style="font-size:26px;font-weight:600;color:var(--okdb-app-fg-strong);margin-top:4px;">{{ workers ? workers.live : \'—\' }}</div>\n <div style="font-size:11px;color:var(--okdb-app-fg-subtle);">{{ workerLine }}</div>\n <if :="workers && workers.why"><div style="font-size:10px;color:var(--okdb-app-fg-muted);margin-top:3px;">{{ workers.why }}</div></if>\n </div>\n\n \x3c!-- Processors --\x3e\n <div style="padding:14px 16px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:8px;">\n <div style="font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--okdb-app-fg-subtle);">Processors</div>\n <div style="font-size:26px;font-weight:600;color:var(--okdb-app-fg-strong);margin-top:4px;">{{ procs == null ? \'—\' : procs.length }}</div>\n <div :style="\'font-size:11px;color:\' + procHealth.color + \';\'">{{ procHealth.text }}</div>\n </div>\n </div>\n </div>\n '};
1
+ export default{tag:"system-runtime-overview",context:()=>({procCount:null,procs:null,hostTotals:null,participation:null,writers:null,writersByProcess:null,busy:!1,_timer:null,async load(){this.busy=!0;try{const[t,r]=await Promise.all([fetch(`${__ENV__.API}/system/runtime`).catch(()=>null),fetch(`${__ENV__.API}/processors/status`).catch(()=>null)]);if(t?.ok){const r=await t.json(),e=(r.data??r.result??r).processes;e&&(this.procCount=e.totals?.processes??null,this.hostTotals=e.totals??null)}if(r?.ok){const t=await r.json(),e=t.data??t.result??{},o=e.envs??e,s=[];for(const t of Object.keys(o||{}))if(Array.isArray(o[t]))for(const r of o[t])s.push(r);this.procs=s,this.participation=e.participation??("boolean"==typeof e.participate?{participating:e.participate?1:0,total:1}:null),this.writers=e.writers??null,this.writersByProcess=e.writersByProcess??null}}catch{}finally{this.busy=!1}},get procHealth(){if(!this.procs)return{text:"—",color:"var(--okdb-app-fg-subtle)"};if(0===this.procs.length)return{text:"none registered",color:"var(--okdb-app-fg-subtle)"};const t=t=>this.procs.filter(t).length,r=t(t=>"error"===t.state),e=t(t=>t.unclaimed),o=t(t=>(t.lag||0)>1e3),s=t(t=>t.paused);return r?{text:`${r} error`,color:"var(--okdb-danger)"}:e?{text:`${e} unclaimed`,color:"var(--okdb-danger)"}:o?{text:`${o} lagging`,color:"var(--okdb-warning)"}:s?{text:`${s} paused`,color:"#818cf8"}:{text:"all caught up",color:"var(--okdb-success)"}},get writerSummary(){const t=Array.isArray(this.writersByProcess)&&this.writersByProcess.length>0?this.writersByProcess.map(t=>t.writers||{}):this.writers?[this.writers]:[];if(0===t.length)return null;let r=0,e=0,o=0,s=0;for(const i of t)for(const t of Object.values(i))s++,r+=t.depth||0,(t.oldestPendingMs||0)>e&&(e=t.oldestPendingMs),t.draining&&o++;return{envs:s,depth:r,oldest:e,draining:o,procs:t.length}},get writerHealth(){const t=this.writerSummary;return t?t.draining?{text:`${t.draining} env(s) parked on drain`,color:"var(--okdb-danger)"}:t.oldest>=2e3?{text:`oldest pending ${(t.oldest/1e3).toFixed(1)}s`,color:"var(--okdb-danger)"}:t.oldest>=250?{text:`oldest pending ${t.oldest}ms`,color:"var(--okdb-warning)"}:{text:"flowing",color:"var(--okdb-success)"}:{text:"—",color:"var(--okdb-app-fg-subtle)"}},get sandboxLabel(){const t=this.hostTotals?.threads;return t&&t.count?(t.busy||0)>0?`${t.count} fn sandbox (${t.busy} busy)`:`${t.count} fn sandbox idle`:null},rssLabel:t=>t?t>=1073741824?(t/1073741824).toFixed(1)+" GB":Math.round(t/1048576)+" MB":"—"}),init(){this.load(),this._timer=setInterval(()=>this.load(),4e3),this._timer.unref&&this._timer.unref()},unmount(){this._timer&&clearInterval(this._timer),this._timer=null},template:'\n <div style="display:flex;flex-direction:column;gap:14px;">\n <div style="font-size:12px;color:var(--okdb-method-default);">A snapshot of everything running under this root. Open a tab for detail and controls.</div>\n\n \x3c!-- ── The four operator axes: processes · processors · write queue · memory ── --\x3e\n <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;">\n\n \x3c!-- Processes --\x3e\n <div style="padding:14px 16px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:8px;">\n <div style="font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--okdb-app-fg-subtle);">Processes</div>\n <div style="font-size:26px;font-weight:600;color:var(--okdb-app-fg-strong);margin-top:4px;">{{ procCount == null ? \'—\' : procCount }}</div>\n <div style="font-size:11px;color:var(--okdb-app-fg-subtle);">{{ participation ? participation.participating + \' of \' + participation.total + \' claim processors\' : \'live under this root\' }}</div>\n </div>\n\n \x3c!-- Processors (claims) --\x3e\n <div style="padding:14px 16px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:8px;">\n <div style="font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--okdb-app-fg-subtle);">Processors</div>\n <div style="font-size:26px;font-weight:600;color:var(--okdb-app-fg-strong);margin-top:4px;">{{ procs == null ? \'—\' : procs.length }}</div>\n <div :style="\'font-size:11px;color:\' + procHealth.color + \';\'">{{ procHealth.text }}</div>\n </div>\n\n \x3c!-- Write queue --\x3e\n <div style="padding:14px 16px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:8px;" title="The per-env async writer, summed across every live process (each publishes its writer stats in its registry heartbeat). A deep queue that drains fast is just busy; a multi-second oldest-pending age, or \'parked\', is the stall.">\n <div style="font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--okdb-app-fg-subtle);">Write queue</div>\n <div style="font-size:26px;font-weight:600;color:var(--okdb-app-fg-strong);margin-top:4px;">{{ writerSummary ? writerSummary.depth : \'—\' }}</div>\n <div :style="\'font-size:11px;color:\' + writerHealth.color + \';\'">{{ writerHealth.text }}{{ writerSummary && writerSummary.procs > 1 ? \' · \' + writerSummary.procs + \' processes\' : \'\' }}</div>\n </div>\n\n \x3c!-- Memory --\x3e\n <div style="padding:14px 16px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:8px;">\n <div style="font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--okdb-app-fg-subtle);">Memory</div>\n <div style="font-size:26px;font-weight:600;color:var(--okdb-app-fg-strong);margin-top:4px;">{{ rssLabel(hostTotals && hostTotals.rss) }}</div>\n <div style="font-size:11px;color:var(--okdb-app-fg-subtle);">{{ sandboxLabel || \'total RSS, all processes\' }}</div>\n </div>\n </div>\n </div>\n '};
@@ -1 +1 @@
1
- document.getElementById("createForm").addEventListener("submit",async e=>{e.preventDefault();const t=document.getElementById("createError");t.hidden=!0;const n=document.getElementById("createName").value.trim(),o=document.getElementById("createPassword").value,r=document.getElementById("createConfirm").value,d=document.getElementById("createLabel").value.trim()||"Initial admin";if(!n)return t.hidden=!1,void(t.textContent="Username is required");if(!o)return t.hidden=!1,void(t.textContent="Password is required");if(o!==r)return t.hidden=!1,void(t.textContent="Passwords do not match");const i=await fetch("/api/auth/tokens",{method:"POST",headers:{"content-type":"application/json"},credentials:"same-origin",body:JSON.stringify({name:n,password:o,label:d,permissions:["*"]})}),a=await i.json().catch(()=>({}));if(!i.ok)return t.hidden=!1,void(t.textContent=a?.error?.message||a?.error||a?.data?.error||"Failed to create admin token");window.location.replace("/admin/login")}),document.getElementById("joinForm").addEventListener("submit",async e=>{e.preventDefault();const t=document.getElementById("joinError");t.hidden=!0;const n=document.getElementById("joinAddress").value.trim(),o=document.getElementById("joinToken").value.trim();if(!n)return t.hidden=!1,void(t.textContent="Remote node address is required");if(!o)return t.hidden=!1,void(t.textContent="Cluster sync token is required");const r=await fetch("/admin/setup/join",{method:"POST",headers:{"content-type":"application/json"},credentials:"same-origin",body:JSON.stringify({address:n,token:o})}),d=await r.json().catch(()=>({}));if(!r.ok)return t.hidden=!1,void(t.textContent=d?.error?.message||d?.error||d?.data?.error||"Failed to join cluster");window.location.replace("/admin/login")});
1
+ document.getElementById("createForm").addEventListener("submit",async e=>{e.preventDefault();const t=document.getElementById("createError");t.hidden=!0;const n=document.getElementById("createName").value.trim(),o=document.getElementById("createPassword").value,d=document.getElementById("createConfirm").value,i=document.getElementById("createLabel").value.trim()||"Initial admin";if(!n)return t.hidden=!1,void(t.textContent="Username is required");if(!o)return t.hidden=!1,void(t.textContent="Password is required");if(o!==d)return t.hidden=!1,void(t.textContent="Passwords do not match");const r=await fetch("/api/auth/tokens",{method:"POST",headers:{"content-type":"application/json"},credentials:"same-origin",body:JSON.stringify({name:n,password:o,label:i,permissions:["*"]})}),a=await r.json().catch(()=>({}));if(!r.ok)return t.hidden=!1,void(t.textContent=a?.error?.message||a?.error||a?.data?.error||"Failed to create admin token");window.location.replace("/admin/login")});const createView=document.getElementById("createView"),joinView=document.getElementById("joinView");document.getElementById("showJoin").addEventListener("click",()=>{createView.hidden=!0,joinView.hidden=!1,document.getElementById("joinAddress").focus()}),document.getElementById("backToCreate").addEventListener("click",()=>{joinView.hidden=!0,createView.hidden=!1});let joinMethod="token";const authSeg=document.getElementById("joinAuthSeg");authSeg.addEventListener("click",e=>{const t=e.target.closest("button[data-method]");if(t){joinMethod=t.dataset.method;for(const e of authSeg.querySelectorAll("button[data-method]"))e.classList.toggle("seg-active",e===t);for(const e of document.querySelectorAll("[data-auth]"))e.hidden=e.getAttribute("data-auth")!==joinMethod}}),document.getElementById("joinForm").addEventListener("submit",async e=>{e.preventDefault();const t=document.getElementById("joinError");t.hidden=!0;const n=document.getElementById("joinAddress").value.trim();if(!n)return t.hidden=!1,void(t.textContent="Remote node address is required");const o={address:n,data_link:document.getElementById("joinDataLink").checked};if("token"===joinMethod){const e=document.getElementById("joinToken").value.trim();if(!e)return t.hidden=!1,void(t.textContent="Cluster sync token is required");o.token=e}else if("login"===joinMethod){const e=document.getElementById("joinUsername").value.trim(),n=document.getElementById("joinPassword").value;if(!e)return t.hidden=!1,void(t.textContent="Username is required");o.username=e,o.password=n}const d=await fetch("/admin/setup/join",{method:"POST",headers:{"content-type":"application/json"},credentials:"same-origin",body:JSON.stringify(o)}),i=await d.json().catch(()=>({}));if(!d.ok)return t.hidden=!1,void(t.textContent=i?.error?.message||i?.error||i?.data?.error||"Failed to join cluster");window.location.replace("/admin/login")});