@kedem/okdb 1.6.4 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/okdb.js +1 -1
- package/docs/change-log.md +23 -0
- package/docs/pipelines.md +6 -1
- package/docs/time-machine.md +177 -0
- package/okdb.js +1 -1
- package/package.json +1 -1
- package/public/sections/db/parts/db-overview.ok.js +1 -1
- package/public/sections/db/parts/type-viewer.ok.js +1 -1
- package/public/sections/system/index.ok.html +1 -1
- package/public/sections/system/parts/system-locks-panel.ok.js +1 -0
- package/public/sections/views/parts/view-detail.ok.js +1 -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"../license/parts/license-panel.ok.js";import d from"@toolkit/components/ok-doc-viewer.ok.js";export default{tag:"system-index",unwrap:!0,dependencies:[n,a,r,i,c,l,d,e,t,o,s],context:e=>({_systemWildcard(){const t=e.$vars.$router?.match("/system/*");return t?.params?.["*"]??""},get subView(){return this._parseSubView(this._systemWildcard())},_parseSubView(e){const t=e??"";return t&&"overview"!==t?"logs"===t?"logs":"events"===t?"events":"sync"===t?"sync":"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(" / "):"--",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': 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 :="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 & 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</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><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></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"../license/parts/license-panel.ok.js";import h from"@toolkit/components/ok-doc-viewer.ok.js";export default{tag:"system-index",unwrap:!0,dependencies:[n,a,r,i,c,l,d,h,e,t,o,s],context:e=>({_systemWildcard(){const t=e.$vars.$router?.match("/system/*");return t?.params?.["*"]??""},get subView(){return this._parseSubView(this._systemWildcard())},_parseSubView(e){const t=e??"";return t&&"overview"!==t?"logs"===t?"logs":"events"===t?"events":"sync"===t?"sync":"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(" / "):"--",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': 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 :="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 & 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</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><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>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default{tag:"system-locks-panel",context:e=>({locks:[],busy:!1,releaseConfirm:null,releaseError:null,async load(){this.busy=!0;try{const e=await fetch(`${__ENV__.API}/locks`);e.ok&&(this.locks=(await e.json()).result??[])}catch{}finally{this.busy=!1}},ageLabel(e){const o=Date.now()-(e.acquiredAt??0);return o<2e3?"<1s":o<6e4?Math.floor(o/1e3)+"s":o<36e5?Math.floor(o/6e4)+"m":Math.floor(o/36e5)+"h"},holderLabel(e){const o=e.holder;return o?o.hostname?`pid:${o.pid}@${o.hostname}`:`pid:${o.pid}`:"—"},confirmRelease(e){this.releaseConfirm=e,this.releaseError=null},cancelRelease(){this.releaseConfirm=null,this.releaseError=null},async doRelease(){const e=this.releaseConfirm;if(e){this.releaseError=null;try{const o=e.env?`?env=${encodeURIComponent(e.env)}`:"",t=await fetch(`${__ENV__.API}/locks/${encodeURIComponent(e.namespace)}/${encodeURIComponent(e.id)}${o}`,{method:"DELETE"}),n=await t.json();if(!n.result?.released)return void(this.releaseError=n.result?.reason??"Release failed");this.releaseConfirm=null,await this.load()}catch(e){this.releaseError=e?.message??String(e)}}}}),async mount(){await this.load()},template:'\n <div style="display:flex;flex-direction:column;gap:10px;">\n <div style="display:flex;align-items:center;gap:8px;">\n <span style="font-size:12px;font-weight:600;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:.04em;">Locks</span>\n <button @click="load()" style="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 <div ok-if="releaseConfirm" style="padding:10px 12px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-danger);border-radius:6px;font-size:12px;display:flex;flex-direction:column;gap:8px;">\n <span style="color:var(--okdb-danger);font-weight:600;">Force-release lock?</span>\n <span style="color:var(--okdb-app-fg-subtle);">{{ releaseConfirm.namespace }}:{{ releaseConfirm.id }} on {{ releaseConfirm.env }}</span>\n <span ok-if="releaseError" style="color:var(--okdb-danger);">{{ releaseError }}</span>\n <div style="display:flex;gap:8px;">\n <button @click="doRelease()" style="padding:3px 10px;background:var(--okdb-danger);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;">Release</button>\n <button @click="cancelRelease()" style="padding:3px 10px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:4px;cursor:pointer;font-size:11px;color:var(--okdb-app-fg);">Cancel</button>\n </div>\n </div>\n\n <div ok-if="!locks.length && !busy" style="font-size:12px;color:var(--okdb-app-fg-subtle);padding:8px 0;">No active locks.</div>\n\n <table ok-if="locks.length" style="width:100%;border-collapse:collapse;font-size:12px;">\n <thead>\n <tr style="color:var(--okdb-app-fg-subtle);text-align:left;border-bottom:1px solid var(--okdb-app-border);">\n <th style="padding:4px 8px;font-weight:500;">env</th>\n <th style="padding:4px 8px;font-weight:500;">namespace</th>\n <th style="padding:4px 8px;font-weight:500;">id</th>\n <th style="padding:4px 8px;font-weight:500;">holder</th>\n <th style="padding:4px 8px;font-weight:500;">age</th>\n <th style="padding:4px 8px;"></th>\n </tr>\n </thead>\n <tbody>\n <tr ok-for="lock of locks" style="border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));">\n <td style="padding:4px 8px;color:var(--okdb-app-fg-subtle);">{{ lock.env }}</td>\n <td style="padding:4px 8px;">{{ lock.namespace }}</td>\n <td style="padding:4px 8px;font-family:monospace;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" :title="lock.id">{{ lock.id }}</td>\n <td style="padding:4px 8px;font-family:monospace;">{{ holderLabel(lock.entry) }}</td>\n <td style="padding:4px 8px;color:var(--okdb-app-fg-subtle);">{{ ageLabel(lock.entry) }}</td>\n <td style="padding:4px 8px;">\n <button @click="confirmRelease(lock)" style="font-size:10px;padding:2px 7px;background:transparent;border:1px solid var(--okdb-danger);color:var(--okdb-danger);border-radius:3px;cursor:pointer;">Release</button>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n '};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"@toolkit/components/ok-chart.ok.js";export default{tag:"view-detail",dependencies:[e],attr:{name:{}},context:e=>({output:null,meta:null,definition:null,loading:!0,error:null,actionLoading:!1,showDeleteConfirm:!1,showOrphanedIndexConfirm:!1,orphanedIndexes:[],rangeData:[],rangeLoading:!1,rangeError:null,rangeFrom:"",rangeTo:"",rangeIncludePartial:!1,rangePreset:"30d",bucketsData:[],bucketsLoading:!1,bucketsError:null,async load(){this.loading=!0,this.error=null,this.output=null,this.meta=null,this.definition=null;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,[r,o,n]=await Promise.all([fetch(`${t}/views/${a}`),fetch(`${t}/views/${a}/meta`),fetch(`${t}/views/${a}/definition`)]),i=r.ok?await r.json():null,d=o.ok?await o.json():null,s=n.ok?await n.json():null;this.output=i?.data??i?.result??null,this.meta=d?.data??d?.result??null,this.definition=s?.data??s?.result??null,this.isBucketed&&(this.rangePreset="30d",this.rangeFrom=new Date(Date.now()-2592e6).toISOString().split("T")[0],this.rangeTo="",this.loadRange(),this.loadBuckets())}catch(e){this.error=e.message}finally{this.loading=!1}},async doAction(t){this.actionLoading=!0;try{const a=e.$ok.stores.okdb.apiBase,r=e.$attr.name,o=await fetch(`${a}/views/${r}/${t}`,{method:"POST"});if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message??`${t} failed`)}await this.load(),e.$dispatch("state-changed")}catch(e){this.error=e.message}finally{this.actionLoading=!1}},async deleteView(){this.actionLoading=!0;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,r=await fetch(`${t}/views/${a}`,{method:"DELETE"});if(!r.ok){const e=await r.json().catch(()=>({})),t=e?.error?.code;if("VIEW_HAS_ORPHANED_INDEXES"===t)return this.orphanedIndexes=e?.error?.details?.orphanedIndexes??[],this.showDeleteConfirm=!1,void(this.showOrphanedIndexConfirm=!0);throw new Error(e?.error?.message??"Delete failed")}e.$dispatch("removed")}catch(e){this.error=e.message}finally{this.actionLoading=!1,this.showOrphanedIndexConfirm||(this.showDeleteConfirm=!1)}},async deleteViewWithIndexes(t){this.actionLoading=!0;try{const a=e.$ok.stores.okdb.apiBase,r=e.$attr.name,o=await fetch(`${a}/views/${r}`,{method:"DELETE",headers:{"Content-Type":"application/json"},body:JSON.stringify({managedIndexes:t})});if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message??"Delete failed")}e.$dispatch("removed")}catch(e){this.error=e.message}finally{this.actionLoading=!1,this.showOrphanedIndexConfirm=!1,this.orphanedIndexes=[]}},formatJson(e){if(null==e)return"";try{return JSON.stringify(e,null,2)}catch{return String(e)}},stateColor(e){switch(e){case"ready":return"#4caf50";case"creating":case"resetting":return"#4FC1FF";case"halted":return"#f44336";case"stopped":return"#fbbf24";default:return"#666"}},get state(){return this.meta?.state??"unknown"},get isInProgress(){return"creating"===this.state||"resetting"===this.state},get isBucketed(){return this.definition&&this.definition.bucket},get chartType(){return this.isBucketed?"time"===this.definition.bucket.preset?"line":"bar":null},get chartData(){const e=this.rangeData||[];if(e.length<2)return null;if(!this.isBucketed)return null;const t=e.map(e=>e.bucketKey),a=e[0]?.reducers||{};return{labels:t,datasets:Object.entries(a).filter(([,e])=>e&&"number"==typeof e.value).map(([e])=>e).map(t=>({label:t,data:e.map(e=>{const a=e.reducers[t];return a&&"number"==typeof a.value?a.value:null})}))}},setQuickRange(e){const t=new Date,a=e=>new Date(t-864e5*e).toISOString().split("T")[0];switch(this.rangePreset=e,e){case"7d":this.rangeFrom=a(7),this.rangeTo="";break;case"30d":this.rangeFrom=a(30),this.rangeTo="";break;case"90d":this.rangeFrom=a(90),this.rangeTo="";break;case"1y":this.rangeFrom=a(365),this.rangeTo="";break;case"all":this.rangeFrom="",this.rangeTo=""}this.loadRange()},shiftRange(e){const t=e=>e?new Date(e+"T00:00:00Z"):null,a=e=>e.toISOString().split("T")[0],r=new Date;let o=t(this.rangeFrom),n=t(this.rangeTo);o||n?o?n||(n=new Date(r)):o=new Date(n-2592e6):(n=r,o=new Date(r-2592e6));const i=n-o;this.rangeFrom=a(new Date(o.getTime()+e*i)),this.rangeTo=a(new Date(n.getTime()+e*i)),this.rangePreset="",this.loadRange()},async loadRange(){if(this.isBucketed){this.rangeLoading=!0,this.rangeError=null;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,r=new URLSearchParams;this.rangeFrom&&r.set("from",this.rangeFrom),this.rangeTo&&r.set("to",this.rangeTo),this.rangeIncludePartial&&r.set("includePartial","true");const o=this.definition.bucket.granularity;r.set("granularity",o);const n=await fetch(`${t}/views/${a}/range?${r}`);if(!n.ok){const e=await n.json().catch(()=>({}));throw new Error(e?.error?.message??"Range fetch failed")}const i=await n.json();this.rangeData=i.data??i.result??[]}catch(e){this.rangeError=e.message,this.rangeData=[]}finally{this.rangeLoading=!1}}},async loadBuckets(){if(this.isBucketed){this.bucketsLoading=!0,this.bucketsError=null;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,r=encodeURIComponent(this.definition.bucket.granularity),o=await fetch(`${t}/views/${a}/buckets?granularity=${r}&limit=10000`);if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message??"Bucket list failed")}const n=await o.json();this.bucketsData=n.data??n.result??[]}catch(e){this.bucketsError=e.message,this.bucketsData=[]}finally{this.bucketsLoading=!1}}},get rangeColumns(){return this.rangeData.length?Object.keys(this.rangeData[0].reducers||{}):[]},reducerDisplayValue(e,t){const a=e.reducers?.[t];if(null==a)return"";if("object"==typeof a&&"number"==typeof a.value)return a.value;if("object"==typeof a&&!Array.isArray(a)){const e=Object.entries(a);if(!e.length)return"—";const t=e[0][1];return"object"==typeof t&&"number"==typeof t.value?e.sort((e,t)=>(t[1].value??0)-(e[1].value??0)).slice(0,3).map(([e,t])=>`${e}: ${t.value}`).join(" · ")+(e.length>3?" +"+(e.length-3):""):e.map(([e,t])=>{const a=Object.values(t||{})[0];return`${e}: ${"number"==typeof a?.value?a.value:"?"}`}).slice(0,3).join(" · ")+(e.length>3?" +"+(e.length-3):"")}return String(a)},get rangeTotals(){if(!this.rangeData.length)return null;const e=this.rangeColumns;if(!e.length)return null;const t=this.definition?.reduce||{},a={};for(const r of e){const e=t[r],o=e?Object.keys(e).find(e=>"items"!==e):null,n=this.rangeData.map(e=>e.reducers?.[r]).filter(e=>null!=e&&"number"==typeof e.value).map(e=>e.value);if(!n.length){a[r]=null;continue}const i=e=>"number"==typeof e&&isFinite(e)?parseFloat(e.toPrecision(10)):e;a[r]="$min"===o?i(Math.min(...n)):"$max"===o?i(Math.max(...n)):"$avg"===o?{approx:i(n.reduce((e,t)=>e+t,0)/n.length)}:i(n.reduce((e,t)=>e+t,0))}return a},rangeTotalDisplay(e){const t=this.rangeTotals;if(!t)return"—";const a=t[e];return null==a?"—":"object"==typeof a&&null!=a.approx?"~"+Math.round(1e3*a.approx)/1e3:a}}),mount(e){e.$watch(()=>e.$attr.name,()=>{this.loading=!0,this.output=null,this.meta=null,this.definition=null,this.showDeleteConfirm=!1,this.showOrphanedIndexConfirm=!1,this.orphanedIndexes=[],this.load()}),this.load()},style:"view-detail{gap:0;height:100%;overflow-y:auto}.vd,.vd__header,view-detail{display:flex;flex-direction:column}.vd{gap:20px;padding:20px 24px}.vd__header{gap:10px;padding-bottom:16px;border-bottom:1px solid var(--okdb-app-border)}.vd__title-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.vd__name,.vd__state-badge{font-weight:600;white-space:nowrap}.vd__name{font-size:20px;color:var(--okdb-app-fg-strong);margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;flex-shrink:1}.vd__state-badge{display:inline-flex;align-items:center;gap:6px;padding:3px 8px;border-radius:999px;border:1px solid var(--okdb-app-border);font-size:11px;text-transform:uppercase;letter-spacing:.04em}.vd__state-dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0}.vd__type-label{font-size:11px;color:var(--okdb-app-fg-subtle);font-family:monospace}.vd__actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.vd__btn{padding:5px 12px;border-radius:4px;font-size:12px;font-weight:600;border:1px solid;background:0 0;cursor:pointer;transition:all .15s ease}.vd__btn--primary{border-color:var(--okdb-accent);color:var(--okdb-accent)}.vd__btn--primary:hover{background:color-mix(in srgb,var(--okdb-accent) 10%,transparent);transform:none;box-shadow:none}.vd__btn--warning{border-color:var(--okdb-warning);color:var(--okdb-warning)}.vd__btn--warning:hover{background:color-mix(in srgb,var(--okdb-warning) 10%,transparent);transform:none;box-shadow:none}.vd__btn--danger{border-color:var(--okdb-danger);color:var(--okdb-danger)}.vd__btn--danger:hover{background:color-mix(in srgb,var(--okdb-danger) 10%,transparent);transform:none;box-shadow:none}.vd__btn:disabled{opacity:.4;cursor:not-allowed}.vd__delete-confirm{display:flex;align-items:center;gap:10px;padding:8px 12px;background:color-mix(in srgb,var(--okdb-danger) 7%,transparent);border:1px solid color-mix(in srgb,var(--okdb-danger) 25%,transparent);border-radius:5px;font-size:12px;color:var(--okdb-danger);flex-wrap:wrap}.vd__delete-confirm-text{flex:1;min-width:0}.vd__delete-confirm-actions{display:flex;gap:6px}.vd__section{display:flex;flex-direction:column;gap:10px}.vd__section-title{font-size:11px;font-weight:600;color:var(--okdb-app-fg-muted);text-transform:uppercase;letter-spacing:.05em}.vd__pre{font-family:monospace;font-size:12px;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:5px;padding:12px 14px;overflow-x:auto;color:var(--okdb-app-fg);white-space:pre;margin:0;line-height:1.5}.vd__meta-grid{display:flex;flex-direction:column;gap:6px;font-size:12px}.vd__meta-row{display:flex;gap:8px}.vd__meta-key{color:var(--okdb-app-fg-subtle);width:80px;flex-shrink:0}.vd__meta-val{color:var(--okdb-app-fg);font-family:monospace;word-break:break-all}.vd__meta-val--error{color:var(--okdb-danger)}.vd__empty,.vd__error{font-size:12px;color:var(--okdb-app-fg-subtle)}.vd__error{color:var(--okdb-danger);padding:8px;background:color-mix(in srgb,var(--okdb-danger) 7%,transparent);border:1px solid color-mix(in srgb,var(--okdb-danger) 20%,transparent);border-radius:4px}.vd__loading{font-size:12px;opacity:.5;padding:40px;text-align:center}.vd__refs{display:flex;flex-direction:column;gap:4px;margin-top:4px}.vd__ref-row{display:flex;gap:8px;font-size:11px;padding:4px 8px;background:var(--okdb-app-bg-deep);border-radius:3px;border:1px solid var(--okdb-app-border)}.vd__ref-name{color:var(--okdb-sky);font-family:monospace;flex:1}.vd__ref-state{color:var(--okdb-app-fg-subtle)}",template:'<div class="vd"><if :="loading"><div class="vd__loading">Loading…</div></if><if :="!loading"><if :="error"><div class="vd__error">{{ error }}</div></if><div class="vd__header"><div class="vd__title-row"><div style="flex:1; min-width:0; display:flex; align-items:baseline; gap:8px; overflow:hidden;"><h2 class="vd__name">{{ $attr.name }}</h2><if :="definition != null && definition.type != null && definition.type !== \'\'"><span class="vd__type-label">{{ definition.type }}</span></if></div><span class="vd__state-badge" :style="\'color:\' + stateColor(state)"><span class="vd__state-dot" :style="\'background:\' + stateColor(state)"/> {{ state }}</span></div><div class="vd__actions"><if :="!isInProgress"><if :="state === \'ready\'"><button class="vd__btn vd__btn--warning" :disabled="actionLoading" @click="doAction(\'stop\')">Stop</button></if><if :="state === \'stopped\' || state === \'halted\'"><button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="doAction(\'start\')">Start</button></if><button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="doAction(\'rebuild\')">Rebuild</button> <button class="vd__btn vd__btn--danger" :disabled="actionLoading" @click="showDeleteConfirm = true">Delete</button></if><if :="isInProgress"><button class="vd__btn vd__btn--primary" disabled="disabled">Stop</button> <button class="vd__btn vd__btn--primary" disabled="disabled">Rebuild</button> <button class="vd__btn vd__btn--danger" disabled="disabled">Delete</button></if></div><if :="showDeleteConfirm"><div class="vd__delete-confirm"><span class="vd__delete-confirm-text">Delete view <strong>{{ $attr.name }}</strong>? This cannot be undone.</span><div class="vd__delete-confirm-actions"><button class="vd__btn vd__btn--danger" :disabled="actionLoading" @click="deleteView()">Confirm</button> <button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="showDeleteConfirm = false">Cancel</button></div></div></if><if :="showOrphanedIndexConfirm"><div class="vd__delete-confirm"><span class="vd__delete-confirm-text">Deleting this view will orphan: <strong>{{ orphanedIndexes.join(\', \') }}</strong>. Drop or keep these indexes?</span><div class="vd__delete-confirm-actions"><button class="vd__btn vd__btn--danger" :disabled="actionLoading" @click="deleteViewWithIndexes(\'drop\')">Drop</button> <button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="deleteViewWithIndexes(\'keep\')">Keep</button> <button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="showOrphanedIndexConfirm = false">Cancel</button></div></div></if></div><if :="isBucketed"><div class="vd__section"><div class="vd__section-title">Bucketing</div><div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:12px;"><span style="font-family:monospace;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:4px;padding:2px 8px;white-space:nowrap;">{{ definition.bucket.field }} · {{ definition.bucket.granularity }} · {{ definition.bucket.timezone || \'UTC\' }} · {{ definition.bucket.preset || \'custom\' }}</span><if :="meta && meta.bucketing && meta.bucketing.unbucketedCount > 0"><span style="font-size:11px;color:var(--okdb-warning);background:color-mix(in srgb,var(--okdb-warning) 10%,transparent);border:1px solid color-mix(in srgb,var(--okdb-warning) 30%,transparent);border-radius:4px;padding:2px 8px;">{{ meta.bucketing.unbucketedCount }} records have no bucket (missing or null bucket field)</span></if></div><div><if :="bucketsLoading"><span style="font-size:11px;color:var(--okdb-app-fg-subtle);">Loading buckets…</span></if><if :="!bucketsLoading && bucketsError"><span style="font-size:11px;color:var(--okdb-danger);">{{ bucketsError }}</span></if><if :="!bucketsLoading && !bucketsError && bucketsData.length === 0"><span style="font-size:11px;color:var(--okdb-app-fg-subtle);">No buckets yet</span></if><if :="!bucketsLoading && !bucketsError && bucketsData.length > 0"><span style="font-size:11px;color:var(--okdb-app-fg-muted);font-family:monospace;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:4px;padding:2px 8px;">{{ bucketsData.length }} bucket{{ bucketsData.length === 1 ? \'\' : \'s\' }} · earliest {{ bucketsData[0].bucketKey }} · latest {{ bucketsData[bucketsData.length - 1].bucketKey }}{{ bucketsData.length >= 10000 ? \' (showing first 10000)\' : \'\' }}</span></if></div></div><div class="vd__section"><div class="vd__section-title">Per-bucket aggregates</div><if :="definition.bucket.preset === \'time\'"><div style="display:flex;align-items:center;gap:4px;flex-wrap:wrap;"><button style="padding:4px 8px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;line-height:1;" @click="shiftRange(-1)" :disabled="rangeLoading" title="Previous period">←</button> <button :style="rangePreset === \'7d\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'7d\')" :disabled="rangeLoading">7d</button> <button :style="rangePreset === \'30d\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'30d\')" :disabled="rangeLoading">30d</button> <button :style="rangePreset === \'90d\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'90d\')" :disabled="rangeLoading">90d</button> <button :style="rangePreset === \'1y\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'1y\')" :disabled="rangeLoading">1y</button> <button :style="rangePreset === \'all\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'all\')" :disabled="rangeLoading">All</button> <button style="padding:4px 8px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;line-height:1;" @click="shiftRange(1)" :disabled="rangeLoading" title="Next period">→</button></div></if><div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;"><div style="display:flex;flex-direction:column;gap:3px;"><span style="font-size:10px;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:0.05em;">From</span><if :="definition.bucket.preset === \'time\'"><input type="date" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeFrom" @input="rangeFrom = $el.value"></if><if :="definition.bucket.preset !== \'time\'"><input type="text" placeholder="start key" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeFrom" @input="rangeFrom = $el.value"></if></div><div style="display:flex;flex-direction:column;gap:3px;"><span style="font-size:10px;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:0.05em;">To</span><if :="definition.bucket.preset === \'time\'"><input type="date" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeTo" @input="rangeTo = $el.value"></if><if :="definition.bucket.preset !== \'time\'"><input type="text" placeholder="end key" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeTo" @input="rangeTo = $el.value"></if></div><label style="display:flex;align-items:center;gap:5px;font-size:12px;cursor:pointer;color:var(--okdb-app-fg-muted);padding-bottom:4px;"><input type="checkbox" :checked="rangeIncludePartial" @change="rangeIncludePartial = $event.target.checked"> Include partial</label> <button style="padding:5px 12px;border-radius:5px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;" @click="loadRange()" :disabled="rangeLoading">Reload</button></div><if :="rangeError"><div class="vd__error">{{ rangeError }}</div></if><if :="rangeLoading"><div class="vd__empty">Loading…</div></if><if :="!rangeLoading && !rangeError && rangeData.length === 0"><div class="vd__empty">No data for the selected range</div></if><if :="!rangeLoading && !rangeError && rangeData.length > 0"><div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:8px 12px;background:color-mix(in srgb,var(--okdb-accent) 6%,var(--okdb-app-bg-deep));border:1px solid color-mix(in srgb,var(--okdb-accent) 20%,var(--okdb-app-border));border-radius:5px;font-size:12px;"><span style="font-size:10px;text-transform:uppercase;letter-spacing:0.06em;color:var(--okdb-app-fg-subtle);white-space:nowrap;">Range totals</span><each :of="rangeColumns" let:item="col"><span style="display:flex;align-items:center;gap:5px;"><span style="font-size:10px;color:var(--okdb-app-fg-subtle);font-family:monospace;">{{ col }}</span> <span style="font-family:monospace;font-weight:600;color:var(--okdb-app-fg);">{{ rangeTotalDisplay(col) }}</span></span></each></div><div style="overflow-x:auto;border:1px solid var(--okdb-app-border);border-radius:5px;font-size:12px;"><div style="display:flex;border-bottom:1px solid var(--okdb-app-border);background:var(--okdb-app-bg-deep);"><div style="flex:0 0 160px;padding:6px 10px;font-size:10px;text-transform:uppercase;letter-spacing:0.05em;color:var(--okdb-app-fg-subtle);white-space:nowrap;">Bucket</div><each :of="rangeColumns" let:item="col"><div style="flex:1;min-width:80px;padding:6px 10px;font-size:10px;text-transform:uppercase;letter-spacing:0.05em;color:var(--okdb-app-fg-subtle);white-space:nowrap;font-family:monospace;text-align:right;">{{ col }}</div></each></div><each :of="rangeData" let:item="row"><div style="display:flex;border-bottom:1px solid var(--okdb-app-border);"><div style="flex:0 0 160px;padding:5px 10px;font-family:monospace;color:var(--okdb-app-fg-muted);">{{ row.bucketKey }}</div><each :of="rangeColumns" let:item="col"><div style="flex:1;min-width:80px;padding:5px 10px;font-family:monospace;text-align:right;">{{ reducerDisplayValue(row, col) }}</div></each></div></each><div style="display:flex;border-top:2px solid var(--okdb-app-border);background:var(--okdb-app-bg-deep);"><div style="flex:0 0 160px;padding:5px 10px;font-family:monospace;font-size:11px;font-weight:600;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:0.04em;">Total</div><each :of="rangeColumns" let:item="col"><div style="flex:1;min-width:80px;padding:5px 10px;font-family:monospace;font-weight:600;text-align:right;">{{ rangeTotalDisplay(col) }}</div></each></div></div></if></div><if :="isBucketed && chartData != null && !rangeLoading"><div class="vd__section"><div class="vd__section-title">Trend</div><div style="height:300px;"><ok-chart :type="chartType" :data="chartData"></div></div></if><if :="isBucketed && chartData == null && rangeData != null && !rangeLoading"><div class="vd__section"><div class="vd__section-title">Trend</div><div class="vd__empty">Not enough data to chart yet (need at least 2 buckets).</div></div></if></if><div class="vd__section"><div class="vd__section-title">Output<if :="isBucketed"><span style="font-size:10px;font-weight:400;color:var(--okdb-app-fg-subtle);text-transform:none;letter-spacing:0;margin-left:6px;">global totals · all time</span></if></div><if :="output != null"><pre class="vd__pre">{{ formatJson(output) }}</pre></if><if :="output == null"><div class="vd__empty">No output available</div></if></div><div class="vd__section"><div class="vd__section-title">Meta</div><if :="meta"><div class="vd__meta-grid"><div class="vd__meta-row"><span class="vd__meta-key">state</span> <span class="vd__meta-val" :style="\'color:\' + stateColor(meta.state)">{{ meta.state ?? \'—\' }}</span></div><div class="vd__meta-row"><span class="vd__meta-key">clock</span> <span class="vd__meta-val">{{ meta.clock ?? \'—\' }}</span></div><if :="meta.error"><div class="vd__meta-row"><span class="vd__meta-key">error</span> <span class="vd__meta-val vd__meta-val--error">{{ meta.error }}</span></div></if></div><if :="meta.refs && Object.keys(meta.refs).length > 0"><div class="vd__section-title" style="margin-top: 8px;">Refs</div><div class="vd__refs"><each :of="Object.entries(meta.refs)" let:item="entry"><div class="vd__ref-row"><span class="vd__ref-name">{{ entry[0] }}</span> <span class="vd__ref-state">{{ typeof entry[1] === \'object\' ? (entry[1]?.state ?? JSON.stringify(entry[1])) : entry[1] }}</span></div></each></div></if></if><if :="!meta"><div class="vd__empty">No meta available</div></if></div><div class="vd__section"><div class="vd__section-title">Definition</div><if :="definition != null"><pre class="vd__pre">{{ formatJson(definition) }}</pre></if><if :="definition == null"><div class="vd__empty">No definition available</div></if></div></if></div>'};
|
|
1
|
+
import e from"@toolkit/components/ok-chart.ok.js";export default{tag:"view-detail",dependencies:[e],attr:{name:{}},context:e=>({output:null,meta:null,definition:null,loading:!0,error:null,actionLoading:!1,showDeleteConfirm:!1,showOrphanedIndexConfirm:!1,orphanedIndexes:[],rangeData:[],rangeLoading:!1,rangeError:null,rangeFrom:"",rangeTo:"",rangeIncludePartial:!1,rangePreset:"30d",bucketsData:[],bucketsLoading:!1,bucketsError:null,async load(){this.loading=!0,this.error=null,this.output=null,this.meta=null,this.definition=null;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,[r,o,i]=await Promise.all([fetch(`${t}/views/${a}`),fetch(`${t}/views/${a}/meta`),fetch(`${t}/views/${a}/definition`)]),n=r.ok?await r.json():null,s=o.ok?await o.json():null,d=i.ok?await i.json():null;this.output=n?.data??n?.result??null,this.meta=s?.data??s?.result??null,this.definition=d?.data??d?.result??null,this.isBucketed&&(this.rangePreset="30d",this.rangeFrom=new Date(Date.now()-2592e6).toISOString().split("T")[0],this.rangeTo="",this.loadRange(),this.loadBuckets())}catch(e){this.error=e.message}finally{this.loading=!1}},async doAction(t){this.actionLoading=!0;try{const a=e.$ok.stores.okdb.apiBase,r=e.$attr.name,o=await fetch(`${a}/views/${r}/${t}`,{method:"POST"});if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message??`${t} failed`)}await this.load(),e.$dispatch("state-changed")}catch(e){this.error=e.message}finally{this.actionLoading=!1}},async deleteView(){this.actionLoading=!0;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,r=await fetch(`${t}/views/${a}`,{method:"DELETE"});if(!r.ok){const e=await r.json().catch(()=>({})),t=e?.error?.code;if("VIEW_HAS_ORPHANED_INDEXES"===t)return this.orphanedIndexes=e?.error?.details?.orphanedIndexes??[],this.showDeleteConfirm=!1,void(this.showOrphanedIndexConfirm=!0);throw new Error(e?.error?.message??"Delete failed")}e.$dispatch("removed")}catch(e){this.error=e.message}finally{this.actionLoading=!1,this.showOrphanedIndexConfirm||(this.showDeleteConfirm=!1)}},async deleteViewWithIndexes(t){this.actionLoading=!0;try{const a=e.$ok.stores.okdb.apiBase,r=e.$attr.name,o=await fetch(`${a}/views/${r}`,{method:"DELETE",headers:{"Content-Type":"application/json"},body:JSON.stringify({managedIndexes:t})});if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message??"Delete failed")}e.$dispatch("removed")}catch(e){this.error=e.message}finally{this.actionLoading=!1,this.showOrphanedIndexConfirm=!1,this.orphanedIndexes=[]}},formatJson(e){if(null==e)return"";try{return JSON.stringify(e,null,2)}catch{return String(e)}},stateColor(e){switch(e){case"ready":return"#4caf50";case"creating":case"resetting":case"partially-ready":return"#4FC1FF";case"halted":return"#f44336";case"stopped":return"#fbbf24";default:return"#666"}},get state(){return this.meta?.state??"unknown"},get isInProgress(){return"creating"===this.state||"resetting"===this.state||"partially-ready"===this.state},get sections(){return this.meta?.sections??null},get isBucketed(){return this.definition&&this.definition.bucket},get chartType(){return this.isBucketed?"time"===this.definition.bucket.preset?"line":"bar":null},get chartData(){const e=this.rangeData||[];if(e.length<2)return null;if(!this.isBucketed)return null;const t=e.map(e=>e.bucketKey),a=e[0]?.reducers||{};return{labels:t,datasets:Object.entries(a).filter(([,e])=>e&&"number"==typeof e.value).map(([e])=>e).map(t=>({label:t,data:e.map(e=>{const a=e.reducers[t];return a&&"number"==typeof a.value?a.value:null})}))}},setQuickRange(e){const t=new Date,a=e=>new Date(t-864e5*e).toISOString().split("T")[0];switch(this.rangePreset=e,e){case"7d":this.rangeFrom=a(7),this.rangeTo="";break;case"30d":this.rangeFrom=a(30),this.rangeTo="";break;case"90d":this.rangeFrom=a(90),this.rangeTo="";break;case"1y":this.rangeFrom=a(365),this.rangeTo="";break;case"all":this.rangeFrom="",this.rangeTo=""}this.loadRange()},shiftRange(e){const t=e=>e?new Date(e+"T00:00:00Z"):null,a=e=>e.toISOString().split("T")[0],r=new Date;let o=t(this.rangeFrom),i=t(this.rangeTo);o||i?o?i||(i=new Date(r)):o=new Date(i-2592e6):(i=r,o=new Date(r-2592e6));const n=i-o;this.rangeFrom=a(new Date(o.getTime()+e*n)),this.rangeTo=a(new Date(i.getTime()+e*n)),this.rangePreset="",this.loadRange()},async loadRange(){if(this.isBucketed){this.rangeLoading=!0,this.rangeError=null;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,r=new URLSearchParams;this.rangeFrom&&r.set("from",this.rangeFrom),this.rangeTo&&r.set("to",this.rangeTo),this.rangeIncludePartial&&r.set("includePartial","true");const o=this.definition.bucket.granularity;r.set("granularity",o);const i=await fetch(`${t}/views/${a}/range?${r}`);if(!i.ok){const e=await i.json().catch(()=>({}));throw new Error(e?.error?.message??"Range fetch failed")}const n=await i.json();this.rangeData=n.data??n.result??[]}catch(e){this.rangeError=e.message,this.rangeData=[]}finally{this.rangeLoading=!1}}},async loadBuckets(){if(this.isBucketed){this.bucketsLoading=!0,this.bucketsError=null;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,r=encodeURIComponent(this.definition.bucket.granularity),o=await fetch(`${t}/views/${a}/buckets?granularity=${r}&limit=10000`);if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message??"Bucket list failed")}const i=await o.json();this.bucketsData=i.data??i.result??[]}catch(e){this.bucketsError=e.message,this.bucketsData=[]}finally{this.bucketsLoading=!1}}},get rangeColumns(){return this.rangeData.length?Object.keys(this.rangeData[0].reducers||{}):[]},reducerDisplayValue(e,t){const a=e.reducers?.[t];if(null==a)return"";if("object"==typeof a&&"number"==typeof a.value)return a.value;if("object"==typeof a&&!Array.isArray(a)){const e=Object.entries(a);if(!e.length)return"—";const t=e[0][1];return"object"==typeof t&&"number"==typeof t.value?e.sort((e,t)=>(t[1].value??0)-(e[1].value??0)).slice(0,3).map(([e,t])=>`${e}: ${t.value}`).join(" · ")+(e.length>3?" +"+(e.length-3):""):e.map(([e,t])=>{const a=Object.values(t||{})[0];return`${e}: ${"number"==typeof a?.value?a.value:"?"}`}).slice(0,3).join(" · ")+(e.length>3?" +"+(e.length-3):"")}return String(a)},get rangeTotals(){if(!this.rangeData.length)return null;const e=this.rangeColumns;if(!e.length)return null;const t=this.definition?.reduce||{},a={};for(const r of e){const e=t[r],o=e?Object.keys(e).find(e=>"items"!==e):null,i=this.rangeData.map(e=>e.reducers?.[r]).filter(e=>null!=e&&"number"==typeof e.value).map(e=>e.value);if(!i.length){a[r]=null;continue}const n=e=>"number"==typeof e&&isFinite(e)?parseFloat(e.toPrecision(10)):e;a[r]="$min"===o?n(Math.min(...i)):"$max"===o?n(Math.max(...i)):"$avg"===o?{approx:n(i.reduce((e,t)=>e+t,0)/i.length)}:n(i.reduce((e,t)=>e+t,0))}return a},rangeTotalDisplay(e){const t=this.rangeTotals;if(!t)return"—";const a=t[e];return null==a?"—":"object"==typeof a&&null!=a.approx?"~"+Math.round(1e3*a.approx)/1e3:a}}),mount(e){e.$watch(()=>e.$attr.name,()=>{this.loading=!0,this.output=null,this.meta=null,this.definition=null,this.showDeleteConfirm=!1,this.showOrphanedIndexConfirm=!1,this.orphanedIndexes=[],this.load()}),this.load()},style:"view-detail{gap:0;height:100%;overflow-y:auto}.vd,.vd__header,view-detail{display:flex;flex-direction:column}.vd{gap:20px;padding:20px 24px}.vd__header{gap:10px;padding-bottom:16px;border-bottom:1px solid var(--okdb-app-border)}.vd__title-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.vd__name,.vd__state-badge{font-weight:600;white-space:nowrap}.vd__name{font-size:20px;color:var(--okdb-app-fg-strong);margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;flex-shrink:1}.vd__state-badge{display:inline-flex;align-items:center;gap:6px;padding:3px 8px;border-radius:999px;border:1px solid var(--okdb-app-border);font-size:11px;text-transform:uppercase;letter-spacing:.04em}.vd__state-dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0}.vd__type-label{font-size:11px;color:var(--okdb-app-fg-subtle);font-family:monospace}.vd__actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.vd__btn{padding:5px 12px;border-radius:4px;font-size:12px;font-weight:600;border:1px solid;background:0 0;cursor:pointer;transition:all .15s ease}.vd__btn--primary{border-color:var(--okdb-accent);color:var(--okdb-accent)}.vd__btn--primary:hover{background:color-mix(in srgb,var(--okdb-accent) 10%,transparent);transform:none;box-shadow:none}.vd__btn--warning{border-color:var(--okdb-warning);color:var(--okdb-warning)}.vd__btn--warning:hover{background:color-mix(in srgb,var(--okdb-warning) 10%,transparent);transform:none;box-shadow:none}.vd__btn--danger{border-color:var(--okdb-danger);color:var(--okdb-danger)}.vd__btn--danger:hover{background:color-mix(in srgb,var(--okdb-danger) 10%,transparent);transform:none;box-shadow:none}.vd__btn:disabled{opacity:.4;cursor:not-allowed}.vd__delete-confirm{display:flex;align-items:center;gap:10px;padding:8px 12px;background:color-mix(in srgb,var(--okdb-danger) 7%,transparent);border:1px solid color-mix(in srgb,var(--okdb-danger) 25%,transparent);border-radius:5px;font-size:12px;color:var(--okdb-danger);flex-wrap:wrap}.vd__delete-confirm-text{flex:1;min-width:0}.vd__delete-confirm-actions{display:flex;gap:6px}.vd__section{display:flex;flex-direction:column;gap:10px}.vd__section-title{font-size:11px;font-weight:600;color:var(--okdb-app-fg-muted);text-transform:uppercase;letter-spacing:.05em}.vd__pre{font-family:monospace;font-size:12px;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:5px;padding:12px 14px;overflow-x:auto;color:var(--okdb-app-fg);white-space:pre;margin:0;line-height:1.5}.vd__meta-grid{display:flex;flex-direction:column;gap:6px;font-size:12px}.vd__meta-row{display:flex;gap:8px}.vd__meta-key{color:var(--okdb-app-fg-subtle);width:80px;flex-shrink:0}.vd__meta-val{color:var(--okdb-app-fg);font-family:monospace;word-break:break-all}.vd__meta-val--error{color:var(--okdb-danger)}.vd__empty,.vd__error{font-size:12px;color:var(--okdb-app-fg-subtle)}.vd__error{color:var(--okdb-danger);padding:8px;background:color-mix(in srgb,var(--okdb-danger) 7%,transparent);border:1px solid color-mix(in srgb,var(--okdb-danger) 20%,transparent);border-radius:4px}.vd__loading{font-size:12px;opacity:.5;padding:40px;text-align:center}.vd__refs{display:flex;flex-direction:column;gap:4px;margin-top:4px}.vd__ref-row{display:flex;gap:8px;font-size:11px;padding:4px 8px;background:var(--okdb-app-bg-deep);border-radius:3px;border:1px solid var(--okdb-app-border)}.vd__ref-name{color:var(--okdb-sky);font-family:monospace;flex:1}.vd__ref-state{color:var(--okdb-app-fg-subtle)}",template:'<div class="vd"><if :="loading"><div class="vd__loading">Loading…</div></if><if :="!loading"><if :="error"><div class="vd__error">{{ error }}</div></if><div class="vd__header"><div class="vd__title-row"><div style="flex:1; min-width:0; display:flex; align-items:baseline; gap:8px; overflow:hidden;"><h2 class="vd__name">{{ $attr.name }}</h2><if :="definition != null && definition.type != null && definition.type !== \'\'"><span class="vd__type-label">{{ definition.type }}</span></if></div><span class="vd__state-badge" :style="\'color:\' + stateColor(state)"><span class="vd__state-dot" :style="\'background:\' + stateColor(state)"/> {{ state }}</span></div><div class="vd__actions"><if :="!isInProgress"><if :="state === \'ready\'"><button class="vd__btn vd__btn--warning" :disabled="actionLoading" @click="doAction(\'stop\')">Stop</button></if><if :="state === \'stopped\' || state === \'halted\'"><button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="doAction(\'start\')">Start</button></if><button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="doAction(\'rebuild\')">Rebuild</button> <button class="vd__btn vd__btn--danger" :disabled="actionLoading" @click="showDeleteConfirm = true">Delete</button></if><if :="isInProgress"><button class="vd__btn vd__btn--primary" disabled="disabled">Stop</button> <button class="vd__btn vd__btn--primary" disabled="disabled">Rebuild</button> <button class="vd__btn vd__btn--danger" disabled="disabled">Delete</button></if></div><if :="isInProgress && sections"><div class="vd__section" style="margin-top:10px;"><div class="vd__section-title">Bootstrap progress</div><if :="sections.primary"><div style="display:flex;align-items:center;gap:8px;font-size:12px;margin-bottom:4px;"><span style="min-width:80px">Primary:</span> <span :style="\'color:\' + stateColor(sections.primary.state)">{{ sections.primary.state }}</span><if :="sections.primary.progress"><span>{{ Math.round(sections.primary.progress.percent * 100) }}%</span></if></div></if><for :="[refName, refSec] of Object.entries(sections.refs ?? {})"><div style="display:flex;align-items:center;gap:8px;font-size:12px;margin-bottom:4px;"><span style="min-width:80px">{{ refName }}:</span> <span :style="\'color:\' + stateColor(refSec.state)">{{ refSec.state }}</span><if :="refSec.progress"><span>{{ Math.round(refSec.progress.percent * 100) }}%</span></if></div></for></div></if><if :="showDeleteConfirm"><div class="vd__delete-confirm"><span class="vd__delete-confirm-text">Delete view <strong>{{ $attr.name }}</strong>? This cannot be undone.</span><div class="vd__delete-confirm-actions"><button class="vd__btn vd__btn--danger" :disabled="actionLoading" @click="deleteView()">Confirm</button> <button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="showDeleteConfirm = false">Cancel</button></div></div></if><if :="showOrphanedIndexConfirm"><div class="vd__delete-confirm"><span class="vd__delete-confirm-text">Deleting this view will orphan: <strong>{{ orphanedIndexes.join(\', \') }}</strong>. Drop or keep these indexes?</span><div class="vd__delete-confirm-actions"><button class="vd__btn vd__btn--danger" :disabled="actionLoading" @click="deleteViewWithIndexes(\'drop\')">Drop</button> <button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="deleteViewWithIndexes(\'keep\')">Keep</button> <button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="showOrphanedIndexConfirm = false">Cancel</button></div></div></if></div><if :="isBucketed"><div class="vd__section"><div class="vd__section-title">Bucketing</div><div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:12px;"><span style="font-family:monospace;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:4px;padding:2px 8px;white-space:nowrap;">{{ definition.bucket.field }} · {{ definition.bucket.granularity }} · {{ definition.bucket.timezone || \'UTC\' }} · {{ definition.bucket.preset || \'custom\' }}</span><if :="meta && meta.bucketing && meta.bucketing.unbucketedCount > 0"><span style="font-size:11px;color:var(--okdb-warning);background:color-mix(in srgb,var(--okdb-warning) 10%,transparent);border:1px solid color-mix(in srgb,var(--okdb-warning) 30%,transparent);border-radius:4px;padding:2px 8px;">{{ meta.bucketing.unbucketedCount }} records have no bucket (missing or null bucket field)</span></if></div><div><if :="bucketsLoading"><span style="font-size:11px;color:var(--okdb-app-fg-subtle);">Loading buckets…</span></if><if :="!bucketsLoading && bucketsError"><span style="font-size:11px;color:var(--okdb-danger);">{{ bucketsError }}</span></if><if :="!bucketsLoading && !bucketsError && bucketsData.length === 0"><span style="font-size:11px;color:var(--okdb-app-fg-subtle);">No buckets yet</span></if><if :="!bucketsLoading && !bucketsError && bucketsData.length > 0"><span style="font-size:11px;color:var(--okdb-app-fg-muted);font-family:monospace;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:4px;padding:2px 8px;">{{ bucketsData.length }} bucket{{ bucketsData.length === 1 ? \'\' : \'s\' }} · earliest {{ bucketsData[0].bucketKey }} · latest {{ bucketsData[bucketsData.length - 1].bucketKey }}{{ bucketsData.length >= 10000 ? \' (showing first 10000)\' : \'\' }}</span></if></div></div><div class="vd__section"><div class="vd__section-title">Per-bucket aggregates</div><if :="definition.bucket.preset === \'time\'"><div style="display:flex;align-items:center;gap:4px;flex-wrap:wrap;"><button style="padding:4px 8px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;line-height:1;" @click="shiftRange(-1)" :disabled="rangeLoading" title="Previous period">←</button> <button :style="rangePreset === \'7d\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'7d\')" :disabled="rangeLoading">7d</button> <button :style="rangePreset === \'30d\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'30d\')" :disabled="rangeLoading">30d</button> <button :style="rangePreset === \'90d\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'90d\')" :disabled="rangeLoading">90d</button> <button :style="rangePreset === \'1y\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'1y\')" :disabled="rangeLoading">1y</button> <button :style="rangePreset === \'all\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'all\')" :disabled="rangeLoading">All</button> <button style="padding:4px 8px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;line-height:1;" @click="shiftRange(1)" :disabled="rangeLoading" title="Next period">→</button></div></if><div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;"><div style="display:flex;flex-direction:column;gap:3px;"><span style="font-size:10px;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:0.05em;">From</span><if :="definition.bucket.preset === \'time\'"><input type="date" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeFrom" @input="rangeFrom = $el.value"></if><if :="definition.bucket.preset !== \'time\'"><input type="text" placeholder="start key" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeFrom" @input="rangeFrom = $el.value"></if></div><div style="display:flex;flex-direction:column;gap:3px;"><span style="font-size:10px;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:0.05em;">To</span><if :="definition.bucket.preset === \'time\'"><input type="date" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeTo" @input="rangeTo = $el.value"></if><if :="definition.bucket.preset !== \'time\'"><input type="text" placeholder="end key" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeTo" @input="rangeTo = $el.value"></if></div><label style="display:flex;align-items:center;gap:5px;font-size:12px;cursor:pointer;color:var(--okdb-app-fg-muted);padding-bottom:4px;"><input type="checkbox" :checked="rangeIncludePartial" @change="rangeIncludePartial = $event.target.checked"> Include partial</label> <button style="padding:5px 12px;border-radius:5px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;" @click="loadRange()" :disabled="rangeLoading">Reload</button></div><if :="rangeError"><div class="vd__error">{{ rangeError }}</div></if><if :="rangeLoading"><div class="vd__empty">Loading…</div></if><if :="!rangeLoading && !rangeError && rangeData.length === 0"><div class="vd__empty">No data for the selected range</div></if><if :="!rangeLoading && !rangeError && rangeData.length > 0"><div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:8px 12px;background:color-mix(in srgb,var(--okdb-accent) 6%,var(--okdb-app-bg-deep));border:1px solid color-mix(in srgb,var(--okdb-accent) 20%,var(--okdb-app-border));border-radius:5px;font-size:12px;"><span style="font-size:10px;text-transform:uppercase;letter-spacing:0.06em;color:var(--okdb-app-fg-subtle);white-space:nowrap;">Range totals</span><each :of="rangeColumns" let:item="col"><span style="display:flex;align-items:center;gap:5px;"><span style="font-size:10px;color:var(--okdb-app-fg-subtle);font-family:monospace;">{{ col }}</span> <span style="font-family:monospace;font-weight:600;color:var(--okdb-app-fg);">{{ rangeTotalDisplay(col) }}</span></span></each></div><div style="overflow-x:auto;border:1px solid var(--okdb-app-border);border-radius:5px;font-size:12px;"><div style="display:flex;border-bottom:1px solid var(--okdb-app-border);background:var(--okdb-app-bg-deep);"><div style="flex:0 0 160px;padding:6px 10px;font-size:10px;text-transform:uppercase;letter-spacing:0.05em;color:var(--okdb-app-fg-subtle);white-space:nowrap;">Bucket</div><each :of="rangeColumns" let:item="col"><div style="flex:1;min-width:80px;padding:6px 10px;font-size:10px;text-transform:uppercase;letter-spacing:0.05em;color:var(--okdb-app-fg-subtle);white-space:nowrap;font-family:monospace;text-align:right;">{{ col }}</div></each></div><each :of="rangeData" let:item="row"><div style="display:flex;border-bottom:1px solid var(--okdb-app-border);"><div style="flex:0 0 160px;padding:5px 10px;font-family:monospace;color:var(--okdb-app-fg-muted);">{{ row.bucketKey }}</div><each :of="rangeColumns" let:item="col"><div style="flex:1;min-width:80px;padding:5px 10px;font-family:monospace;text-align:right;">{{ reducerDisplayValue(row, col) }}</div></each></div></each><div style="display:flex;border-top:2px solid var(--okdb-app-border);background:var(--okdb-app-bg-deep);"><div style="flex:0 0 160px;padding:5px 10px;font-family:monospace;font-size:11px;font-weight:600;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:0.04em;">Total</div><each :of="rangeColumns" let:item="col"><div style="flex:1;min-width:80px;padding:5px 10px;font-family:monospace;font-weight:600;text-align:right;">{{ rangeTotalDisplay(col) }}</div></each></div></div></if></div><if :="isBucketed && chartData != null && !rangeLoading"><div class="vd__section"><div class="vd__section-title">Trend</div><div style="height:300px;"><ok-chart :type="chartType" :data="chartData"></div></div></if><if :="isBucketed && chartData == null && rangeData != null && !rangeLoading"><div class="vd__section"><div class="vd__section-title">Trend</div><div class="vd__empty">Not enough data to chart yet (need at least 2 buckets).</div></div></if></if><div class="vd__section"><div class="vd__section-title">Output<if :="isBucketed"><span style="font-size:10px;font-weight:400;color:var(--okdb-app-fg-subtle);text-transform:none;letter-spacing:0;margin-left:6px;">global totals · all time</span></if></div><if :="output != null"><pre class="vd__pre">{{ formatJson(output) }}</pre></if><if :="output == null"><div class="vd__empty">No output available</div></if></div><div class="vd__section"><div class="vd__section-title">Meta</div><if :="meta"><div class="vd__meta-grid"><div class="vd__meta-row"><span class="vd__meta-key">state</span> <span class="vd__meta-val" :style="\'color:\' + stateColor(meta.state)">{{ meta.state ?? \'—\' }}</span></div><div class="vd__meta-row"><span class="vd__meta-key">clock</span> <span class="vd__meta-val">{{ meta.clock ?? \'—\' }}</span></div><if :="meta.error"><div class="vd__meta-row"><span class="vd__meta-key">error</span> <span class="vd__meta-val vd__meta-val--error">{{ meta.error }}</span></div></if></div><if :="meta.refs && Object.keys(meta.refs).length > 0"><div class="vd__section-title" style="margin-top: 8px;">Refs</div><div class="vd__refs"><each :of="Object.entries(meta.refs)" let:item="entry"><div class="vd__ref-row"><span class="vd__ref-name">{{ entry[0] }}</span> <span class="vd__ref-state">{{ typeof entry[1] === \'object\' ? (entry[1]?.state ?? JSON.stringify(entry[1])) : entry[1] }}</span></div></each></div></if></if><if :="!meta"><div class="vd__empty">No meta available</div></if></div><div class="vd__section"><div class="vd__section-title">Definition</div><if :="definition != null"><pre class="vd__pre">{{ formatJson(definition) }}</pre></if><if :="definition == null"><div class="vd__empty">No definition available</div></if></div></if></div>'};
|