@kedem/okdb 1.8.6 → 1.8.7
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/okdb-functions-runner-child.js +1 -0
- package/okdb-processor-worker.js +1 -1
- package/okdb-views-bootstrap-worker.js +1 -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/system/parts/system-locks-panel.ok.js +1 -1
- package/public/sections/views/parts/view-detail.ok.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kedem/okdb",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.7",
|
|
4
4
|
"description": "A fast, type-oriented database — strong consistency and rich indexing at the core, with sync, vector embeddings, full-text search, and AI tooling built in. Designed for the AI era.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"engines": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"../../../stores/events-store.js";import n from"../modals/demo-env-modal.ok.js";export default{tag:"db-overview",dependencies:[n],attr:{stats:{}},context:e=>({recentChanges:[],recentBusy:!1,recentError:null,pipelines:[],pipelinesBusy:!1,pipelinesError:null,violations:[],violationsBusy:!1,violationsError:null,expandedViolationType:null,tmStatus:null,tmBusy:!1,tmTypeBusy:null,ftsSize:null,ftsSizeBusy:!1,get info(){return e.$ok.stores.okdb.info},get types(){return(e.$ok.stores.okdb.types||[]).filter(e=>!e.type.startsWith("~")).sort((e,n)=>e.type.localeCompare(n.type))},get systemTypes(){return(e.$ok.stores.okdb.types||[]).filter(e=>e.type.startsWith("~"))},get envName(){return e.$ok.stores.okdb.selectedEnv||"default"},get totalRecords(){return(e.$ok.stores.okdb.types||[]).filter(e=>!e.type.startsWith("~")).reduce((e,n)=>e+(n.count||0),0)},get approxSize(){const e=this.info?.lastPageNumber,n=this.info?.pageSize;if(!e||!n)return"--";let t=e*n;const o=["B","KB","MB","GB","TB"];let i=0;for(;t>=1024&&i<o.length-1;)t/=1024,i++;return t.toFixed(2)+" "+o[i]},typeIndexCount(e){const n=this.info?.types?.[e]?.indexes;return n?Object.keys(n).length:0},typePages(e){return this.info?.types?.[e]?.data?.pages??null},typeBytes(e){const n=this.info?.types?.[e];if(!n)return null;const t=this.info?.pageSize||4096;let o=n.data?.pages||0;if(n.indexes)for(const e of Object.values(n.indexes))o+=e?.pages||0;return o*t},typeBytesTooltip(e){const n=this.info?.types?.[e];if(!n)return"";const t=this.info?.pageSize||4096,o=n.data?.pages||0;let i=0;const a=[];if(n.indexes)for(const[e,o]of Object.entries(n.indexes)){const n=o?.pages||0;i+=n,a.push(` · ${e}: ${this.fmtSizeBytes(n*t)}`)}const s=[`Data: ${this.fmtSizeBytes(o*t)}`];return a.length&&(s.push(`Indexes: ${this.fmtSizeBytes(i*t)}`),s.push(...a)),s.push("(Page-based estimate — includes LMDB B-tree overhead.)"),s.join("\n")},select(n){e.$dispatch("select",n)},async loadRecent(){this.recentBusy=!0,this.recentError=null;try{const n=await fetch(`${e.$ok.stores.okdb.apiBase}/changelog?limit=10`);if(n.ok){const e=await n.json();this.recentChanges=e.data??[]}}catch(e){this.recentError=e?.message||"Failed to load changes"}finally{this.recentBusy=!1}},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," "),relTime(e){if(!e)return"—";const n=Date.now()-e;return n<5e3?"just now":n<6e4?`${Math.round(n/1e3)}s ago`:n<36e5?`${Math.round(n/6e4)}m ago`:n<864e5?`${Math.round(n/36e5)}h ago`:new Date(e).toLocaleDateString()},fmt:e=>null==e?"--":Number(e).toLocaleString(),healthColor:e=>"online"===e?"var(--okdb-success)":"degraded"===e?"var(--okdb-warning)":"error"===e?"var(--okdb-danger)":"stopped"===e?"var(--okdb-app-fg-muted)":"var(--okdb-sky)",statusColor:e=>"running"===e||"online"===e?"var(--okdb-success)":"building"===e||"degraded"===e?"var(--okdb-warning)":"error"===e?"var(--okdb-danger)":"stopped"===e||"idle"===e?"var(--okdb-app-fg-muted)":"var(--okdb-sky)",pipelineKey(e){return`${e?.env||this.envName}::${e?.name||""}`},pipelineLabel:e=>e?.meta?.label||e?.name||"Unnamed pipeline",pipelineRoleSummary(e){const n=Array.isArray(e?.engines)?e.engines:[];if(0===n.length)return"No members";const t=[];for(const e of n){const n=e?.role||e?.type;if(n&&!t.includes(n)&&(t.push(n),t.length>=3))break}return t.join(" · ")},pipelineSeverity(e){const n=String(e?.health||"").toLowerCase(),t=String(e?.status||"").toLowerCase();return"error"===n||"error"===t?0:"degraded"===n||"degraded"===t||"building"===t?1:"stopped"===n||"stopped"===t||"idle"===t?2:"online"===n||"running"===t||"online"===t?3:4},get pipelineEngineCount(){const e=new Set;for(const n of this.pipelines)for(const t of n?.engines||[]){const o=t?.key||(t?.type&&t?.name?`${t.type}@${t.name}`:t?.name)||null;o&&e.add(`${n?.env||this.envName}::${o}`)}return e.size},get pipelineHealthCounts(){const e={online:0,degraded:0,error:0,stopped:0};for(const n of this.pipelines){const t=String(n?.health||"").toLowerCase();null!=e[t]&&(e[t]+=1)}return e},get pipelineSummaryText(){const e=this.pipelines.length,n=this.pipelineEngineCount;return`${e} pipeline${1===e?"":"s"} · ${n} engine${1===n?"":"s"}`},get pipelineHighlights(){return[...this.pipelines].sort((e,n)=>{const t=this.pipelineSeverity(e)-this.pipelineSeverity(n);return 0!==t?t:this.pipelineLabel(e).localeCompare(this.pipelineLabel(n))}).slice(0,4)},async loadPipelines(){this.pipelinesBusy=!0,this.pipelinesError=null;try{const n=await fetch(`${e.$ok.stores.okdb.apiBase}/pipelines`),t=await n.json().catch(()=>({}));if(!n.ok)throw new Error(t?.error?.message||"Failed to load pipelines");this.pipelines=t?.data??t?.result??[]}catch(e){this.pipelinesError=e?.message||"Failed to load pipelines",this.pipelines=[]}finally{this.pipelinesBusy=!1}},openPipelinesWorkspace(n=null){const t=e.$vars?.$router;if(!t?.resolve||!t?.push)return;const o={env:this.envName,mode:"pipelines"};n?.name&&(o.pipeline=n.name),t.push(t.resolve("/engines",o))},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:n=>({modal:n,onDone:async n=>{e.$ok.stores.okdb.selectEnv(n),await e.$ok.stores.okdb.reloadEnvs(),e.$ok.shared?.toast?.success?.(`✦ Demo environment "${n}" is ready!`)}})})},_unsubs:[],_reloadTimer:null,_scheduleReload(){clearTimeout(this._reloadTimer),this._reloadTimer=setTimeout(()=>this.loadRecent(),600)},async _reloadTypes(){await Promise.all([e.$ok.stores.okdb.reloadTypes(),e.$ok.stores.okdb.reloadInfo()])},async loadViolations(){this.violationsBusy=!0,this.violationsError=null;try{const n=await fetch(`${e.$ok.stores.okdb.apiBase}/violations`);if(404===n.status)return void(this.violations=[]);if(!n.ok){const e=await n.json().catch(()=>({}));throw new Error(e?.error?.message||"Failed to load violations")}const t=await n.json();this.violations=t?.data?.violations??t?.result?.violations??[]}catch(e){this.violationsError=e?.message||"Failed to load violations",this.violations=[]}finally{this.violationsBusy=!1}},get violationsByType(){const e=new Map;for(const n of this.violations){const t=n.type||"?";e.has(t)||e.set(t,[]),e.get(t).push(n)}return[...e.entries()].map(([e,n])=>({type:e,count:n.length,items:n})).sort((e,n)=>n.count-e.count)},get totalViolations(){return this.violations.length},toggleViolationGroup(e){this.expandedViolationType=this.expandedViolationType===e?null:e},navigateToType(n){e.$dispatch("select",{type:n})},async loadFtsSize(){this.ftsSizeBusy=!0;try{const n=await fetch(`${e.$ok.stores.okdb.apiBase}/fts/size`);if(n.ok){const e=await n.json();this.ftsSize=e?.result??e?.data??null}else this.ftsSize=null}catch{this.ftsSize=null}finally{this.ftsSizeBusy=!1}},fmtSizeBytes(e){if(null==e)return"--";if(0===e)return"0";const n=["B","KB","MB","GB","TB"];let t=e,o=0;for(;t>=1024&&o<n.length-1;)t/=1024,o++;return t.toFixed(t>=100?0:t>=10?1:2)+" "+n[o]},get ftsSizeTooltip(){const e=this.ftsSize;return e?[`Disk file: ${this.fmtSizeBytes(e.diskBytes)}`,`Actual payload: ${this.fmtSizeBytes(e.payloadBytes)}`,` · indexes: ${this.fmtSizeBytes(e.indexPayloadBytes)}`,` · shared dicts: ${this.fmtSizeBytes(e.sharedDictBytes)}`,`Reclaimable slack: ${this.fmtSizeBytes(e.slackBytes)}`,`Indexes: ${(e.indexes||[]).length}`].join("\n"):""},async reloadOverviewData(){await Promise.all([this.loadRecent(),this.loadPipelines(),this.loadViolations(),this.loadFtsSize()])},async loadTMStatus(){try{const n=e.$ok.stores.okdb.selectedEnv||"default",t=await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/time-machine`);if(!t.ok)return void(this.tmStatus={enabled:!1,autoEnable:!1,types:[]});const o=await t.json();this.tmStatus=o.result??o.data??{enabled:!1,autoEnable:!1,types:[]}}catch{this.tmStatus={enabled:!1,autoEnable:!1,types:[]}}},async enableAllTM(){if(!this.tmBusy){this.tmBusy=!0;try{const n=e.$ok.stores.okdb.selectedEnv||"default";await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/time-machine/enable-all`,{method:"POST"}),await this.loadTMStatus()}catch{}this.tmBusy=!1}},async disableAllTM(){if(!this.tmBusy){this.tmBusy=!0;try{const n=e.$ok.stores.okdb.selectedEnv||"default";await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/time-machine/disable-all`,{method:"POST"}),await this.loadTMStatus()}catch{}this.tmBusy=!1}},async enableTMType(n){if(!this.tmTypeBusy){this.tmTypeBusy=n;try{const t=e.$ok.stores.okdb.selectedEnv||"default";await fetch(`${__ENV__.API}/env/${encodeURIComponent(t)}/time-machine/types/${encodeURIComponent(n)}/enable`,{method:"POST"}),await this.loadTMStatus()}catch{}this.tmTypeBusy=null}},async disableTMType(n){if(!this.tmTypeBusy){this.tmTypeBusy=n;try{const t=e.$ok.stores.okdb.selectedEnv||"default";await fetch(`${__ENV__.API}/env/${encodeURIComponent(t)}/time-machine/types/${encodeURIComponent(n)}/disable`,{method:"POST"}),await this.loadTMStatus()}catch{}this.tmTypeBusy=null}}}),init(e){this.loadTMStatus(),e.$watch(()=>e.$ok.stores.okdb.selectedEnv,()=>{this.reloadOverviewData(),this.loadTMStatus()})},mount(){this.reloadOverviewData();const n=()=>this.loadPipelines();this._unsubs=[e.on("type:registered",()=>this._reloadTypes()),e.on("type:drop",()=>this._reloadTypes()),e.on("index:registered",()=>this._reloadTypes()),e.on("index:drop",()=>this._reloadTypes()),e.on("engine:started",n),e.on("engine:stopped",n),e.on("engine:error",n),e.on("engine:installed",n),e.on("engine:uninstalled",n),e.on("pipeline:start",n),e.on("pipeline:stop",n),e.on("pipeline:delete",n),e.on("item:create",()=>this._scheduleReload()),e.on("item:update",()=>this._scheduleReload()),e.on("item:remove",()=>this._scheduleReload()),e.on("sync:apply",()=>this._scheduleReload()),e.on("system:clock_change",()=>this._scheduleReload()),e.on("system:poke",()=>this._scheduleReload())]},unmount(){clearTimeout(this._reloadTimer);for(const e of this._unsubs)e();this._unsubs=[]},template:'\n<div style="display:flex; flex-direction:column; gap:20px; max-width:860px;">\n\n \x3c!-- Header --\x3e\n <div style="display:flex; align-items:center; gap:10px;">\n <div style="font-weight:600; font-size:18px;">Database Overview</div>\n <if :="recentBusy">\n <span style="font-size:12px; opacity:.4;">updating…</span>\n </if>\n <div style="margin-left:auto;">\n <button @click="loadRecent()" :disabled="recentBusy"\n style="font-size:12px; padding:5px 12px; opacity:.75;">\n ↻ Refresh\n </button>\n </div>\n </div>\n\n \x3c!-- Stat cards row --\x3e\n <div class="ov-stat-row">\n <div class="ov-stat-card">\n <div class="ov-stat-label">DB Size</div>\n <div class="ov-stat-value" style="color:var(--accent);">{{ approxSize }}</div>\n </div>\n <div class="ov-stat-card">\n <div class="ov-stat-label">Clock</div>\n <div class="ov-stat-value" style="font-family:monospace; font-size:18px;">{{ fmt(info?.clock) }}</div>\n </div>\n <div class="ov-stat-card">\n <div class="ov-stat-label">User Types</div>\n <div class="ov-stat-value" style="color:var(--okdb-purple);">{{ types.length }}</div>\n </div>\n <div class="ov-stat-card">\n <div class="ov-stat-label">Total Records</div>\n <div class="ov-stat-value" style="color:var(--okdb-success);">{{ fmt(totalRecords) }}</div>\n </div>\n <if :="ftsSize && ftsSize.indexes && ftsSize.indexes.length > 0">\n <div class="ov-stat-card" :title="ftsSizeTooltip">\n <div class="ov-stat-label">FTS Storage</div>\n <div class="ov-stat-value" style="color:var(--okdb-sky);">{{ fmtSizeBytes(ftsSize.payloadBytes) }}</div>\n <div style="font-size:10px; opacity:.5; margin-top:2px;">\n {{ ftsSize.indexes.length }} idx · disk {{ fmtSizeBytes(ftsSize.diskBytes) }}\n </div>\n </div>\n </if>\n </div>\n\n \x3c!-- Empty state --\x3e\n <if :="types.length === 0">\n <div style="display:flex; flex-direction:column; gap:14px;">\n\n \x3c!-- Standard empty hint --\x3e\n <div class="ov-empty">\n <div style="font-size:30px; margin-bottom:12px;">🗄️</div>\n <div style="font-weight:600; font-size:15px; margin-bottom:5px;">No types registered yet</div>\n <div style="font-size:13px; opacity:.6; line-height:1.7;">\n Use <b>Register Type +</b> in the sidebar to create your first collection.<br>\n Types are strongly-typed document collections stored in LMDB.\n </div>\n </div>\n\n \x3c!-- Demo data card --\x3e\n <div class="ov-demo-card" @click="openDemoModal()">\n <div class="ov-demo-card__aurora"></div>\n <div style="position:relative; z-index:1; display:flex; flex-direction:column; align-items:center; gap:10px;">\n <div class="ov-demo-card__sparkle">✦</div>\n <div class="ov-demo-card__title">Generate Demo Environment</div>\n <div class="ov-demo-card__desc">\n Instantly populate a new environment with realistic data, indexes,\n and full-text search — ready to explore in seconds.\n </div>\n <div class="ov-demo-card__badges">\n <span class="ov-demo-badge" style="color:var(--okdb-success); border-color:color-mix(in srgb, var(--okdb-success) 20%, transparent); background:color-mix(in srgb, var(--okdb-success) 7%, transparent);">🛒 E-Commerce</span>\n <span class="ov-demo-badge" style="color:var(--okdb-accent); border-color:color-mix(in srgb, var(--okdb-accent) 20%, transparent); background:color-mix(in srgb, var(--okdb-accent) 7%, transparent);">📊 Analytics</span>\n <span class="ov-demo-badge" style="color:var(--okdb-purple); border-color:color-mix(in srgb, var(--okdb-purple) 20%, transparent); background:color-mix(in srgb, var(--okdb-purple) 7%, transparent);">📝 Content Hub</span>\n <span class="ov-demo-badge" style="color:var(--okdb-warning); border-color:color-mix(in srgb, var(--okdb-warning) 20%, transparent); background:color-mix(in srgb, var(--okdb-warning) 7%, transparent);">👥 Team Directory</span>\n <span class="ov-demo-badge" style="color:var(--okdb-orange); border-color:color-mix(in srgb, var(--okdb-orange) 20%, transparent); background:color-mix(in srgb, var(--okdb-orange) 7%, transparent);">🗺️ City Explorer</span>\n </div>\n <div class="ov-demo-card__cta">\n <span>Choose a preset</span>\n <span style="margin-left:6px; font-size:14px;">→</span>\n </div>\n </div>\n </div>\n\n </div>\n </if>\n\n \x3c!-- Types table --\x3e\n <if :="types.length > 0">\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">Types</span>\n <span style="font-size:12px; opacity:.45; margin-left:auto;">\n {{ types.length }} user · {{ systemTypes.length }} system\n </span>\n </div>\n <table class="ov-table">\n <thead>\n <tr>\n <th>Name</th>\n <th style="text-align:right;">Records</th>\n <th style="text-align:right;">Size</th>\n <th style="text-align:right;">Indexes</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr as="repeat" :of="types" let:item="t" class="ov-row" @click="select(t)" title="Open in type viewer">\n <td>\n <span style="font-family:monospace; font-size:13px;">{{ t.type }}</span>\n </td>\n <td style="text-align:right; color:var(--accent); font-weight:600;">{{ fmt(t.count) }}</td>\n <td style="text-align:right; font-size:12px; font-family:monospace; opacity:.7;" :title="typeBytesTooltip(t.type)">{{ fmtSizeBytes(typeBytes(t.type)) }}</td>\n <td style="text-align:right; font-size:12px;">\n <if :="typeIndexCount(t.type) > 0">\n <span style="\n padding:1px 7px; border-radius:999px;\n border:1px solid color-mix(in srgb, var(--okdb-purple) 33%, transparent); color:var(--okdb-purple);\n font-size:11px;\n ">{{ typeIndexCount(t.type) }}</span>\n </if>\n <if :="typeIndexCount(t.type) === 0">\n <span style="opacity:.3;">—</span>\n </if>\n </td>\n <td style="text-align:right; opacity:.3; font-size:11px; padding-right:16px;">→</td>\n </tr>\n </tbody>\n </table>\n </div>\n </if>\n\n \x3c!-- Violations summary --\x3e\n <if :="totalViolations > 0 || violationsBusy || violationsError">\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">Schema Violations</span>\n <if :="totalViolations > 0">\n <span style="font-size:11px; padding:2px 10px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-danger) 33%, transparent); color:var(--okdb-danger); background:color-mix(in srgb, var(--okdb-danger) 5%, transparent); font-weight:600;">{{ totalViolations }}</span>\n </if>\n <span style="font-size:12px; opacity:.45; margin-left:auto;">across all types in {{ envName }}</span>\n <button @click="loadViolations()" :disabled="violationsBusy" style="font-size:12px; padding:5px 12px; opacity:.75;">↻</button>\n </div>\n <if :="violationsBusy && violations.length === 0">\n <div style="padding:14px 16px; font-size:12px; opacity:.45;">Loading…</div>\n </if>\n <if :="violationsError">\n <div style="padding:10px 16px; color:var(--okdb-danger); font-size:12px;">{{ violationsError }}</div>\n </if>\n <if :="!violationsBusy && !violationsError && violationsByType.length === 0">\n <div style="padding:14px 16px; font-size:12px; opacity:.4;">No violations.</div>\n </if>\n <if :="violationsByType.length > 0">\n <div>\n <each :of="violationsByType" let:item="grp">\n \x3c!-- type group header --\x3e\n <div class="ov-viol-group" @click="toggleViolationGroup(grp.type)">\n <span class="ov-viol-arrow" :style="expandedViolationType === grp.type ? \'transform:rotate(90deg)\' : \'\'">›</span>\n <span style="font-family:monospace; font-size:13px; flex:1;">{{ grp.type }}</span>\n <span style="font-size:11px; padding:2px 8px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-danger) 33%, transparent); color:var(--okdb-danger); background:color-mix(in srgb, var(--okdb-danger) 5%, transparent); margin-right:8px;">{{ grp.count }}</span>\n <button\n @click:stop="navigateToType(grp.type)"\n style="font-size:11px; padding:2px 8px; border-radius:5px; border:1px solid var(--okdb-app-border); background:transparent; color:var(--okdb-sky); cursor:pointer; opacity:.8;"\n title="Open Schema tab for this type"\n >Schema →</button>\n </div>\n \x3c!-- expanded violations for this type --\x3e\n <if :="expandedViolationType === grp.type">\n <div class="ov-viol-list">\n <each :of="grp.items" let:item="v">\n <div class="ov-viol-row">\n <span style="font-family:monospace; font-size:11px; color:var(--okdb-json-key); flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ v.key }}</span>\n <span style="font-size:11px; opacity:.45; white-space:nowrap; margin-left:8px;">{{ (v.errors ?? []).length || (v.errorCount ?? 0) }} error(s)</span>\n </div>\n </each>\n </div>\n </if>\n </each>\n </div>\n </if>\n </div>\n </if>\n\n \x3c!-- Time Machine card --\x3e\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">⏱ Time Machine</span>\n <if :="tmStatus && tmStatus.autoEnable">\n <span style="font-size:10px; padding:2px 8px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-success) 33%, transparent); color:var(--okdb-success); background:color-mix(in srgb, var(--okdb-success) 7%, transparent);">auto-enable</span>\n </if>\n <if :="!tmStatus">\n <span style="font-size:11px; opacity:.4; margin-left:auto;">loading…</span>\n </if>\n <if :="tmStatus">\n <div style="margin-left:auto; display:flex; gap:6px;">\n <button @click="loadTMStatus()" :disabled="tmBusy" style="font-size:11px; padding:2px 9px; opacity:.65;">↻</button>\n <if :="!tmStatus.enabled">\n <button @click="enableAllTM()" :disabled="tmBusy" style="font-size:11px; padding:2px 10px; color:var(--okdb-success); border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);">\n {{ tmBusy ? \'…\' : \'Enable All\' }}\n </button>\n </if>\n <if :="tmStatus.enabled">\n <button @click="disableAllTM()" :disabled="tmBusy" style="font-size:11px; padding:2px 10px; color:var(--okdb-danger); border-color:color-mix(in srgb, var(--okdb-danger) 33%, transparent);">\n {{ tmBusy ? \'…\' : \'Disable All\' }}\n </button>\n </if>\n </div>\n </if>\n </div>\n <if :="tmStatus && tmStatus.types && tmStatus.types.length > 0">\n <table class="ov-table">\n <thead>\n <tr>\n <th>Type</th>\n <th style="text-align:right;">Heads</th>\n <th style="text-align:right;">Last Clock</th>\n <th style="text-align:right;"></th>\n </tr>\n </thead>\n <tbody>\n <tr as="repeat" :of="tmStatus.types" let:item="t">\n <td>\n <span style="font-family:monospace; font-size:13px;">{{ t.type }}</span>\n <if :="!t.enabled">\n <span style="font-size:10px; opacity:.4; margin-left:6px;">disabled</span>\n </if>\n </td>\n <td style="text-align:right; font-size:12px; font-family:monospace; opacity:.7;">{{ t.headCount ?? \'—\' }}</td>\n <td style="text-align:right; font-size:11px; font-family:monospace; opacity:.5;">{{ t.lastProcessedClock ?? \'—\' }}</td>\n <td style="text-align:right; padding-right:12px;">\n <if :="t.enabled">\n <button\n @click="disableTMType(t.type)"\n :disabled="tmTypeBusy === t.type"\n style="font-size:10px; padding:1px 8px; color:var(--okdb-danger); border-color:color-mix(in srgb, var(--okdb-danger) 33%, transparent);"\n >{{ tmTypeBusy === t.type ? \'…\' : \'Disable\' }}</button>\n </if>\n <if :="!t.enabled">\n <button\n @click="enableTMType(t.type)"\n :disabled="tmTypeBusy === t.type"\n style="font-size:10px; padding:1px 8px; color:var(--okdb-success); border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);"\n >{{ tmTypeBusy === t.type ? \'…\' : \'Enable\' }}</button>\n </if>\n </td>\n </tr>\n </tbody>\n </table>\n </if>\n <if :="tmStatus && (!tmStatus.types || tmStatus.types.length === 0)">\n <div style="padding:14px 16px; font-size:12px; opacity:.4;">\n No types tracked. Click "Enable All" to start tracking all registered types.\n </div>\n </if>\n </div>\n\n <div class="ov-card" style="overflow:visible;">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">Pipelines</span>\n <span style="font-size:12px; opacity:.45; margin-left:auto;">overview highlights for the current environment</span>\n </div>\n <div style="padding:16px; display:flex; flex-direction:column; gap:14px;">\n <div class="ov-pipelines-headline">\n <div style="display:flex; flex-direction:column; gap:6px; min-width:0; flex:1;">\n <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <div style="font-size:15px; font-weight:600; color:var(--okdb-app-fg);">{{ pipelineSummaryText }}</div>\n <if :="pipelineHealthCounts.error > 0">\n <span class="ov-pipeline-pill" :style="\'color:var(--okdb-danger);border-color:var(--okdb-danger-deep-border);background:#450a0a33;\'">{{ pipelineHealthCounts.error }} error</span>\n </if>\n <if :="pipelineHealthCounts.degraded > 0">\n <span class="ov-pipeline-pill" :style="\'color:#fcd34d;border-color:#854d0e;background:#451a0333;\'">{{ pipelineHealthCounts.degraded }} degraded</span>\n </if>\n <if :="pipelineHealthCounts.online > 0">\n <span class="ov-pipeline-pill" :style="\'color:#86efac;border-color:#166534;background:#052e1633;\'">{{ pipelineHealthCounts.online }} online</span>\n </if>\n </div>\n <div style="font-size:12px; color:var(--okdb-app-fg-muted); line-height:1.6; max-width:640px;">\n The overview should only surface high-signal pipeline highlights. Open the dedicated workspace when you want the full grouped pipeline list and member details.\n </div>\n </div>\n <div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center; justify-content:flex-end;">\n <button @click="loadPipelines()" :disabled="pipelinesBusy" style="font-size:12px; padding:5px 12px; opacity:.75;">↻ Refresh</button>\n <button @click="openPipelinesWorkspace()" style="font-size:12px; padding:5px 12px; border-color:color-mix(in srgb, var(--okdb-indigo) 33%, transparent); color:var(--okdb-indigo);">Open pipelines workspace →</button>\n </div>\n </div>\n\n <if :="pipelinesError">\n <div class="ov-pipelines-error">{{ pipelinesError }}</div>\n </if>\n\n <if :="pipelinesBusy && pipelines.length === 0 && !pipelinesError">\n <div style="font-size:12px; opacity:.45;">Loading pipeline highlights…</div>\n </if>\n\n <if :="!pipelinesBusy && pipelines.length === 0 && !pipelinesError">\n <div class="ov-pipelines-empty">\n <div style="font-size:24px; opacity:.3;">⛓</div>\n <div style="font-size:14px; font-weight:600; opacity:.78;">No pipelines in {{ envName }}</div>\n <div style="font-size:12px; opacity:.5; max-width:520px; line-height:1.7; text-align:center;">\n Pipelines will appear here once created or synced. This frontpage intentionally stays compact and only shows a small highlight list.\n </div>\n </div>\n </if>\n\n <if :="pipelineHighlights.length > 0">\n <div class="ov-pipelines-list">\n <each :of="pipelineHighlights" let:item="pipeline">\n <button class="ov-pipeline-highlight" @click="openPipelinesWorkspace(pipeline)">\n <div style="display:flex; align-items:center; gap:9px; min-width:0;">\n <span :style="\'width:8px;height:8px;border-radius:50%;flex-shrink:0;background:\' + healthColor(pipeline.health)"></span>\n <span style="font-size:13px; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ pipelineLabel(pipeline) }}</span>\n <span :style="\'font-size:11px;color:\' + healthColor(pipeline.health) + \';opacity:.95;white-space:nowrap;\'">{{ pipeline.health || \'unknown\' }}</span>\n </div>\n <div style="display:flex; gap:12px; flex-wrap:wrap; margin-top:6px; font-size:11px; color:var(--okdb-app-fg-muted);">\n <span><span style="opacity:.6;">Members:</span> <span style="color:var(--okdb-app-fg);">{{ fmt(pipeline.engines?.length || 0) }}</span></span>\n <if :="pipeline.status">\n <span><span style="opacity:.6;">Status:</span> <span :style="\'color:\' + statusColor(pipeline.status)">{{ pipeline.status }}</span></span>\n </if>\n <span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ pipelineRoleSummary(pipeline) }}</span>\n </div>\n </button>\n </each>\n </div>\n </if>\n\n <if :="pipelines.length > pipelineHighlights.length">\n <div style="font-size:11px; color:var(--okdb-app-fg-muted); opacity:.75;">\n Showing {{ pipelineHighlights.length }} of {{ pipelines.length }} pipelines. Open the dedicated workspace for the full inventory.\n </div>\n </if>\n </div>\n </div>\n\n \x3c!-- Recent activity --\x3e\n <if :="recentChanges.length > 0">\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">Recent Activity</span>\n <span style="font-size:12px; opacity:.4; margin-left:auto;">last {{ recentChanges.length }} changes</span>\n </div>\n <table style="width:100%; border-collapse:collapse; font-size:12px;">\n <tbody>\n <tr as="repeat" :of="recentChanges" let:item="ch" class="ov-row ov-row--sm">\n <td class="ov-ch-td" style="text-align:right; width:60px; font-family:monospace; opacity:.4; padding-left:16px;">\n {{ ch.clock }}\n </td>\n <td class="ov-ch-td" style="width:100px;">\n <span :style="\'font-size:10px; padding:2px 6px; border-radius:3px; border:1px solid; color:\'+actionColor(ch.action)+\'; border-color:\'+actionColor(ch.action)+\'55;\'">\n {{ actionLabel(ch.action) }}\n </span>\n </td>\n <td class="ov-ch-td" style="font-family:monospace; font-size:11px; opacity:.7; max-width:110px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">\n {{ ch.type || \'—\' }}\n </td>\n <td class="ov-ch-td" style="font-family:monospace; font-size:11px; opacity:.45; max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">\n {{ ch.key || \'\' }}\n </td>\n <td class="ov-ch-td" style="text-align:right; opacity:.35; white-space:nowrap; font-size:11px; padding-right:16px;">\n {{ relTime(ch.timestamp) }}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </if>\n\n</div>\n',style:"\n db-overview .ov-stat-row {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 12px;\n }\n db-overview .ov-stat-card {\n background: var(--okdb-app-surface);\n border: 1px solid var(--okdb-app-border);\n border-radius: 12px;\n padding: 16px 18px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n box-shadow: 0 2px 8px #0003;\n }\n db-overview .ov-stat-label {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: .06em;\n opacity: .5;\n }\n db-overview .ov-stat-value {\n font-size: 22px;\n font-weight: 700;\n line-height: 1;\n }\n db-overview .ov-card {\n background: var(--okdb-app-surface);\n border: 1px solid var(--okdb-app-border);\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 2px 8px #0003;\n }\n db-overview .ov-card-head {\n padding: 13px 16px;\n border-bottom: 1px solid var(--okdb-app-border);\n display: flex;\n align-items: center;\n gap: 10px;\n }\n db-overview .ov-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n }\n db-overview .ov-table th {\n padding: 7px 16px;\n text-align: left;\n font-weight: 500;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: .05em;\n opacity: .4;\n border-bottom: 1px solid var(--okdb-app-border);\n background: var(--okdb-app-bg);\n }\n db-overview .ov-table td {\n padding: 9px 16px;\n border-bottom: 1px solid var(--okdb-app-surface-2);\n }\n db-overview .ov-row {\n cursor: pointer;\n transition: background .1s;\n }\n db-overview .ov-row:hover td {\n background: #ffffff07;\n }\n db-overview .ov-table tbody tr:last-child td {\n border-bottom: none;\n }\n db-overview .ov-ch-td {\n padding: 6px 8px;\n border-bottom: 1px solid var(--okdb-app-surface);\n }\n db-overview .ov-pipelines-headline {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n align-items: flex-start;\n flex-wrap: wrap;\n }\n db-overview .ov-pipeline-pill {\n font-size: 10px;\n padding: 2px 8px;\n border-radius: 999px;\n border: 1px solid;\n white-space: nowrap;\n }\n db-overview .ov-pipelines-error {\n color: var(--okdb-danger);\n font-size: 12px;\n padding: 10px 12px;\n border-radius: 10px;\n border: 1px solid color-mix(in srgb, var(--okdb-danger) 33%, transparent);\n background: color-mix(in srgb, var(--okdb-danger) 5%, transparent);\n }\n db-overview .ov-pipelines-empty {\n border: 1px dashed var(--okdb-app-border);\n border-radius: 12px;\n padding: 24px 20px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n align-items: center;\n }\n db-overview .ov-pipelines-list {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));\n gap: 10px;\n }\n db-overview .ov-pipeline-highlight {\n width: 100%;\n text-align: left;\n border-radius: 10px;\n border: 1px solid var(--okdb-app-border);\n background: var(--okdb-app-bg-deep);\n padding: 11px 12px;\n cursor: pointer;\n }\n db-overview .ov-pipeline-highlight:hover {\n border-color: var(--okdb-app-border-strong);\n background: var(--okdb-app-bg-deep);\n }\n db-overview tbody .ov-row--sm:last-child .ov-ch-td {\n border-bottom: none;\n }\n db-overview .ov-viol-group {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 9px 16px;\n border-bottom: 1px solid var(--okdb-app-surface-2);\n cursor: pointer;\n transition: background .1s;\n }\n db-overview .ov-viol-group:hover { background: #ffffff07; }\n db-overview .ov-viol-arrow {\n display: inline-block;\n font-size: 14px;\n opacity: .5;\n transition: transform .15s;\n width: 14px;\n flex-shrink: 0;\n }\n db-overview .ov-viol-list {\n background: var(--okdb-app-bg-deep);\n border-bottom: 1px solid var(--okdb-app-surface-2);\n }\n db-overview .ov-viol-row {\n display: flex;\n align-items: center;\n padding: 6px 16px 6px 38px;\n border-bottom: 1px solid var(--okdb-app-surface);\n font-size: 12px;\n }\n db-overview .ov-viol-row:last-child { border-bottom: none; }\n db-overview .ov-empty {\n border: 1px dashed var(--okdb-app-border);\n border-radius: 14px;\n padding: 30px 32px;\n text-align: center;\n opacity: .7;\n }\n /* ── Demo data card ── */\n db-overview .ov-demo-card {\n position: relative;\n border: 1px solid var(--okdb-app-border);\n border-radius: 16px;\n padding: 28px 24px 24px;\n text-align: center;\n cursor: pointer;\n overflow: hidden;\n transition: border-color .25s, box-shadow .25s, transform .2s;\n background: var(--okdb-app-bg-deep);\n }\n db-overview .ov-demo-card:hover {\n border-color: color-mix(in srgb, var(--okdb-accent) 53%, transparent);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--okdb-accent) 13%, transparent), 0 8px 40px color-mix(in srgb, var(--okdb-accent) 9%, transparent), 0 0 80px color-mix(in srgb, var(--okdb-purple) 4%, transparent);\n transform: translateY(-2px);\n }\n db-overview .ov-demo-card__aurora {\n position: absolute;\n inset: 0;\n background:\n radial-gradient(ellipse at 20% 50%, color-mix(in srgb, var(--okdb-accent) 4%, transparent), transparent 55%),\n radial-gradient(ellipse at 80% 30%, color-mix(in srgb, var(--okdb-purple) 4%, transparent), transparent 55%),\n radial-gradient(ellipse at 50% 90%, color-mix(in srgb, var(--okdb-success) 4%, transparent), transparent 55%);\n pointer-events: none;\n animation: ov-aurora 8s ease-in-out infinite alternate;\n }\n db-overview .ov-demo-card__sparkle {\n font-size: 26px;\n background: linear-gradient(135deg, var(--okdb-accent), var(--okdb-purple) 40%, var(--okdb-success));\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n animation: ov-hue 6s linear infinite;\n }\n db-overview .ov-demo-card__title {\n font-size: 17px;\n font-weight: 700;\n background: linear-gradient(135deg, var(--okdb-app-fg-strong), var(--okdb-accent));\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n }\n db-overview .ov-demo-card__desc {\n font-size: 13px;\n opacity: .55;\n max-width: 380px;\n line-height: 1.65;\n }\n db-overview .ov-demo-card__badges {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n justify-content: center;\n }\n db-overview .ov-demo-badge {\n padding: 3px 10px;\n border-radius: 999px;\n border: 1px solid;\n font-size: 11px;\n font-weight: 500;\n transition: transform .15s;\n }\n db-overview .ov-demo-card:hover .ov-demo-badge { transform: scale(1.05); }\n db-overview .ov-demo-card__cta {\n margin-top: 4px;\n font-size: 13px;\n font-weight: 600;\n color: var(--okdb-accent);\n opacity: .8;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: opacity .15s;\n }\n db-overview .ov-demo-card:hover .ov-demo-card__cta { opacity: 1; }\n @keyframes ov-hue {\n from { filter: hue-rotate(0deg); }\n to { filter: hue-rotate(360deg); }\n }\n @keyframes ov-aurora {\n from { opacity: .7; }\n to { opacity: 1.3; }\n }\n "};
|
|
1
|
+
import e from"../../../stores/events-store.js";import n from"../modals/demo-env-modal.ok.js";export default{tag:"db-overview",dependencies:[n],attr:{stats:{}},context:e=>({recentChanges:[],recentBusy:!1,recentError:null,pipelines:[],pipelinesBusy:!1,pipelinesError:null,violations:[],violationsBusy:!1,violationsError:null,expandedViolationType:null,tmStatus:null,tmBusy:!1,tmTypeBusy:new Set,ftsSize:null,ftsSizeBusy:!1,get info(){return e.$ok.stores.okdb.info},get types(){return(e.$ok.stores.okdb.types||[]).filter(e=>!e.type.startsWith("~")).sort((e,n)=>e.type.localeCompare(n.type))},get systemTypes(){return(e.$ok.stores.okdb.types||[]).filter(e=>e.type.startsWith("~"))},get envName(){return e.$ok.stores.okdb.selectedEnv||"default"},get totalRecords(){return(e.$ok.stores.okdb.types||[]).filter(e=>!e.type.startsWith("~")).reduce((e,n)=>e+(n.count||0),0)},get approxSize(){const e=this.info?.lastPageNumber,n=this.info?.pageSize;if(!e||!n)return"--";let t=e*n;const i=["B","KB","MB","GB","TB"];let o=0;for(;t>=1024&&o<i.length-1;)t/=1024,o++;return t.toFixed(2)+" "+i[o]},typeIndexCount(e){const n=this.info?.types?.[e]?.indexes;return n?Object.keys(n).length:0},typePages(e){return this.info?.types?.[e]?.data?.pages??null},typeBytes(e){const n=this.info?.types?.[e];if(!n)return null;const t=this.info?.pageSize||4096;let i=n.data?.pages||0;if(n.indexes)for(const e of Object.values(n.indexes))i+=e?.pages||0;return i*t},typeBytesTooltip(e){const n=this.info?.types?.[e];if(!n)return"";const t=this.info?.pageSize||4096,i=n.data?.pages||0;let o=0;const s=[];if(n.indexes)for(const[e,i]of Object.entries(n.indexes)){const n=i?.pages||0;o+=n,s.push(` · ${e}: ${this.fmtSizeBytes(n*t)}`)}const a=[`Data: ${this.fmtSizeBytes(i*t)}`];return s.length&&(a.push(`Indexes: ${this.fmtSizeBytes(o*t)}`),a.push(...s)),a.push("(Page-based estimate — includes LMDB B-tree overhead.)"),a.join("\n")},select(n){e.$dispatch("select",n)},async loadRecent(){this.recentBusy=!0,this.recentError=null;try{const n=await fetch(`${e.$ok.stores.okdb.apiBase}/changelog?limit=10`);if(n.ok){const e=await n.json();this.recentChanges=e.data??[]}}catch(e){this.recentError=e?.message||"Failed to load changes"}finally{this.recentBusy=!1}},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," "),relTime(e){if(!e)return"—";const n=Date.now()-e;return n<5e3?"just now":n<6e4?`${Math.round(n/1e3)}s ago`:n<36e5?`${Math.round(n/6e4)}m ago`:n<864e5?`${Math.round(n/36e5)}h ago`:new Date(e).toLocaleDateString()},fmt:e=>null==e?"--":Number(e).toLocaleString(),healthColor:e=>"online"===e?"var(--okdb-success)":"degraded"===e?"var(--okdb-warning)":"error"===e?"var(--okdb-danger)":"stopped"===e?"var(--okdb-app-fg-muted)":"var(--okdb-sky)",statusColor:e=>"running"===e||"online"===e?"var(--okdb-success)":"building"===e||"degraded"===e?"var(--okdb-warning)":"error"===e?"var(--okdb-danger)":"stopped"===e||"idle"===e?"var(--okdb-app-fg-muted)":"var(--okdb-sky)",pipelineKey(e){return`${e?.env||this.envName}::${e?.name||""}`},pipelineLabel:e=>e?.meta?.label||e?.name||"Unnamed pipeline",pipelineRoleSummary(e){const n=Array.isArray(e?.engines)?e.engines:[];if(0===n.length)return"No members";const t=[];for(const e of n){const n=e?.role||e?.type;if(n&&!t.includes(n)&&(t.push(n),t.length>=3))break}return t.join(" · ")},pipelineSeverity(e){const n=String(e?.health||"").toLowerCase(),t=String(e?.status||"").toLowerCase();return"error"===n||"error"===t?0:"degraded"===n||"degraded"===t||"building"===t?1:"stopped"===n||"stopped"===t||"idle"===t?2:"online"===n||"running"===t||"online"===t?3:4},get pipelineEngineCount(){const e=new Set;for(const n of this.pipelines)for(const t of n?.engines||[]){const i=t?.key||(t?.type&&t?.name?`${t.type}@${t.name}`:t?.name)||null;i&&e.add(`${n?.env||this.envName}::${i}`)}return e.size},get pipelineHealthCounts(){const e={online:0,degraded:0,error:0,stopped:0};for(const n of this.pipelines){const t=String(n?.health||"").toLowerCase();null!=e[t]&&(e[t]+=1)}return e},get pipelineSummaryText(){const e=this.pipelines.length,n=this.pipelineEngineCount;return`${e} pipeline${1===e?"":"s"} · ${n} engine${1===n?"":"s"}`},get pipelineHighlights(){return[...this.pipelines].sort((e,n)=>{const t=this.pipelineSeverity(e)-this.pipelineSeverity(n);return 0!==t?t:this.pipelineLabel(e).localeCompare(this.pipelineLabel(n))}).slice(0,4)},async loadPipelines(){this.pipelinesBusy=!0,this.pipelinesError=null;try{const n=await fetch(`${e.$ok.stores.okdb.apiBase}/pipelines`),t=await n.json().catch(()=>({}));if(!n.ok)throw new Error(t?.error?.message||"Failed to load pipelines");this.pipelines=t?.data??t?.result??[]}catch(e){this.pipelinesError=e?.message||"Failed to load pipelines",this.pipelines=[]}finally{this.pipelinesBusy=!1}},openPipelinesWorkspace(n=null){const t=e.$vars?.$router;if(!t?.resolve||!t?.push)return;const i={env:this.envName,mode:"pipelines"};n?.name&&(i.pipeline=n.name),t.push(t.resolve("/engines",i))},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:n=>({modal:n,onDone:async n=>{e.$ok.stores.okdb.selectEnv(n),await e.$ok.stores.okdb.reloadEnvs(),e.$ok.shared?.toast?.success?.(`✦ Demo environment "${n}" is ready!`)}})})},_unsubs:[],_reloadTimer:null,_scheduleReload(){clearTimeout(this._reloadTimer),this._reloadTimer=setTimeout(()=>this.loadRecent(),600)},async _reloadTypes(){await Promise.all([e.$ok.stores.okdb.reloadTypes(),e.$ok.stores.okdb.reloadInfo()])},async loadViolations(){this.violationsBusy=!0,this.violationsError=null;try{const n=await fetch(`${e.$ok.stores.okdb.apiBase}/violations`);if(404===n.status)return void(this.violations=[]);if(!n.ok){const e=await n.json().catch(()=>({}));throw new Error(e?.error?.message||"Failed to load violations")}const t=await n.json();this.violations=t?.data?.violations??t?.result?.violations??[]}catch(e){this.violationsError=e?.message||"Failed to load violations",this.violations=[]}finally{this.violationsBusy=!1}},get violationsByType(){const e=new Map;for(const n of this.violations){const t=n.type||"?";e.has(t)||e.set(t,[]),e.get(t).push(n)}return[...e.entries()].map(([e,n])=>({type:e,count:n.length,items:n})).sort((e,n)=>n.count-e.count)},get totalViolations(){return this.violations.length},toggleViolationGroup(e){this.expandedViolationType=this.expandedViolationType===e?null:e},navigateToType(n){e.$dispatch("select",{type:n})},async loadFtsSize(){this.ftsSizeBusy=!0;try{const n=await fetch(`${e.$ok.stores.okdb.apiBase}/fts/size`);if(n.ok){const e=await n.json();this.ftsSize=e?.result??e?.data??null}else this.ftsSize=null}catch{this.ftsSize=null}finally{this.ftsSizeBusy=!1}},fmtSizeBytes(e){if(null==e)return"--";if(0===e)return"0";const n=["B","KB","MB","GB","TB"];let t=e,i=0;for(;t>=1024&&i<n.length-1;)t/=1024,i++;return t.toFixed(t>=100?0:t>=10?1:2)+" "+n[i]},get ftsSizeTooltip(){const e=this.ftsSize;return e?[`Disk file: ${this.fmtSizeBytes(e.diskBytes)}`,`Actual payload: ${this.fmtSizeBytes(e.payloadBytes)}`,` · indexes: ${this.fmtSizeBytes(e.indexPayloadBytes)}`,` · shared dicts: ${this.fmtSizeBytes(e.sharedDictBytes)}`,`Reclaimable slack: ${this.fmtSizeBytes(e.slackBytes)}`,`Indexes: ${(e.indexes||[]).length}`].join("\n"):""},async reloadOverviewData(){await Promise.all([this.loadRecent(),this.loadPipelines(),this.loadViolations(),this.loadFtsSize()])},async loadTMStatus(){try{const n=e.$ok.stores.okdb.selectedEnv||"default",t=await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/time-machine`);if(!t.ok)return void(this.tmStatus={enabled:!1,autoEnable:!1,types:[]});const i=await t.json();this.tmStatus=i.result??i.data??{enabled:!1,autoEnable:!1,types:[]}}catch{this.tmStatus={enabled:!1,autoEnable:!1,types:[]}}},async enableAllTM(){if(!this.tmBusy){this.tmBusy=!0;try{const n=e.$ok.stores.okdb.selectedEnv||"default";await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/time-machine/enable-all`,{method:"POST"}),await this.loadTMStatus()}catch{}this.tmBusy=!1}},async disableAllTM(){if(!this.tmBusy){this.tmBusy=!0;try{const n=e.$ok.stores.okdb.selectedEnv||"default";await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/time-machine/disable-all`,{method:"POST"}),await this.loadTMStatus()}catch{}this.tmBusy=!1}},async _toggleTMType(n,t){if(this.tmTypeBusy.has(n))return;this.tmTypeBusy.add(n),this.tmTypeBusy=new Set(this.tmTypeBusy);const i=this.tmStatus?.types?.find(e=>e.type===n),o=i?i.enabled:null;i&&(i.enabled="enable"===t);try{const s=e.$ok.stores.okdb.selectedEnv||"default";!(await fetch(`${__ENV__.API}/env/${encodeURIComponent(s)}/time-machine/types/${encodeURIComponent(n)}/${t}`,{method:"POST"})).ok&&i&&(i.enabled=o)}catch{i&&(i.enabled=o)}this.tmTypeBusy.delete(n),this.tmTypeBusy=new Set(this.tmTypeBusy),0===this.tmTypeBusy.size&&await this.loadTMStatus()},async enableTMType(e){return this._toggleTMType(e,"enable")},async disableTMType(e){return this._toggleTMType(e,"disable")}}),init(e){this.loadTMStatus(),e.$watch(()=>e.$ok.stores.okdb.selectedEnv,()=>{this.reloadOverviewData(),this.loadTMStatus()})},mount(){this.reloadOverviewData();const n=()=>this.loadPipelines();this._unsubs=[e.on("type:registered",()=>this._reloadTypes()),e.on("type:drop",()=>this._reloadTypes()),e.on("index:registered",()=>this._reloadTypes()),e.on("index:drop",()=>this._reloadTypes()),e.on("engine:started",n),e.on("engine:stopped",n),e.on("engine:error",n),e.on("engine:installed",n),e.on("engine:uninstalled",n),e.on("pipeline:start",n),e.on("pipeline:stop",n),e.on("pipeline:delete",n),e.on("item:create",()=>this._scheduleReload()),e.on("item:update",()=>this._scheduleReload()),e.on("item:remove",()=>this._scheduleReload()),e.on("sync:apply",()=>this._scheduleReload()),e.on("system:clock_change",()=>this._scheduleReload()),e.on("system:poke",()=>this._scheduleReload())]},unmount(){clearTimeout(this._reloadTimer);for(const e of this._unsubs)e();this._unsubs=[]},template:'\n<div style="display:flex; flex-direction:column; gap:20px; max-width:860px;">\n\n \x3c!-- Header --\x3e\n <div style="display:flex; align-items:center; gap:10px;">\n <div style="font-weight:600; font-size:18px;">Database Overview</div>\n <if :="recentBusy">\n <span style="font-size:12px; opacity:.4;">updating…</span>\n </if>\n <div style="margin-left:auto;">\n <button @click="loadRecent()" :disabled="recentBusy"\n style="font-size:12px; padding:5px 12px; opacity:.75;">\n ↻ Refresh\n </button>\n </div>\n </div>\n\n \x3c!-- Stat cards row --\x3e\n <div class="ov-stat-row">\n <div class="ov-stat-card">\n <div class="ov-stat-label">DB Size</div>\n <div class="ov-stat-value" style="color:var(--accent);">{{ approxSize }}</div>\n </div>\n <div class="ov-stat-card">\n <div class="ov-stat-label">Clock</div>\n <div class="ov-stat-value" style="font-family:monospace; font-size:18px;">{{ fmt(info?.clock) }}</div>\n </div>\n <div class="ov-stat-card">\n <div class="ov-stat-label">User Types</div>\n <div class="ov-stat-value" style="color:var(--okdb-purple);">{{ types.length }}</div>\n </div>\n <div class="ov-stat-card">\n <div class="ov-stat-label">Total Records</div>\n <div class="ov-stat-value" style="color:var(--okdb-success);">{{ fmt(totalRecords) }}</div>\n </div>\n <if :="ftsSize && ftsSize.indexes && ftsSize.indexes.length > 0">\n <div class="ov-stat-card" :title="ftsSizeTooltip">\n <div class="ov-stat-label">FTS Storage</div>\n <div class="ov-stat-value" style="color:var(--okdb-sky);">{{ fmtSizeBytes(ftsSize.payloadBytes) }}</div>\n <div style="font-size:10px; opacity:.5; margin-top:2px;">\n {{ ftsSize.indexes.length }} idx · disk {{ fmtSizeBytes(ftsSize.diskBytes) }}\n </div>\n </div>\n </if>\n </div>\n\n \x3c!-- Empty state --\x3e\n <if :="types.length === 0">\n <div style="display:flex; flex-direction:column; gap:14px;">\n\n \x3c!-- Standard empty hint --\x3e\n <div class="ov-empty">\n <div style="font-size:30px; margin-bottom:12px;">🗄️</div>\n <div style="font-weight:600; font-size:15px; margin-bottom:5px;">No types registered yet</div>\n <div style="font-size:13px; opacity:.6; line-height:1.7;">\n Use <b>Register Type +</b> in the sidebar to create your first collection.<br>\n Types are strongly-typed document collections stored in LMDB.\n </div>\n </div>\n\n \x3c!-- Demo data card --\x3e\n <div class="ov-demo-card" @click="openDemoModal()">\n <div class="ov-demo-card__aurora"></div>\n <div style="position:relative; z-index:1; display:flex; flex-direction:column; align-items:center; gap:10px;">\n <div class="ov-demo-card__sparkle">✦</div>\n <div class="ov-demo-card__title">Generate Demo Environment</div>\n <div class="ov-demo-card__desc">\n Instantly populate a new environment with realistic data, indexes,\n and full-text search — ready to explore in seconds.\n </div>\n <div class="ov-demo-card__badges">\n <span class="ov-demo-badge" style="color:var(--okdb-success); border-color:color-mix(in srgb, var(--okdb-success) 20%, transparent); background:color-mix(in srgb, var(--okdb-success) 7%, transparent);">🛒 E-Commerce</span>\n <span class="ov-demo-badge" style="color:var(--okdb-accent); border-color:color-mix(in srgb, var(--okdb-accent) 20%, transparent); background:color-mix(in srgb, var(--okdb-accent) 7%, transparent);">📊 Analytics</span>\n <span class="ov-demo-badge" style="color:var(--okdb-purple); border-color:color-mix(in srgb, var(--okdb-purple) 20%, transparent); background:color-mix(in srgb, var(--okdb-purple) 7%, transparent);">📝 Content Hub</span>\n <span class="ov-demo-badge" style="color:var(--okdb-warning); border-color:color-mix(in srgb, var(--okdb-warning) 20%, transparent); background:color-mix(in srgb, var(--okdb-warning) 7%, transparent);">👥 Team Directory</span>\n <span class="ov-demo-badge" style="color:var(--okdb-orange); border-color:color-mix(in srgb, var(--okdb-orange) 20%, transparent); background:color-mix(in srgb, var(--okdb-orange) 7%, transparent);">🗺️ City Explorer</span>\n </div>\n <div class="ov-demo-card__cta">\n <span>Choose a preset</span>\n <span style="margin-left:6px; font-size:14px;">→</span>\n </div>\n </div>\n </div>\n\n </div>\n </if>\n\n \x3c!-- Types table --\x3e\n <if :="types.length > 0">\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">Types</span>\n <span style="font-size:12px; opacity:.45; margin-left:auto;">\n {{ types.length }} user · {{ systemTypes.length }} system\n </span>\n </div>\n <table class="ov-table">\n <thead>\n <tr>\n <th>Name</th>\n <th style="text-align:right;">Records</th>\n <th style="text-align:right;">Size</th>\n <th style="text-align:right;">Indexes</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr as="repeat" :of="types" let:item="t" class="ov-row" @click="select(t)" title="Open in type viewer">\n <td>\n <span style="font-family:monospace; font-size:13px;">{{ t.type }}</span>\n </td>\n <td style="text-align:right; color:var(--accent); font-weight:600;">{{ fmt(t.count) }}</td>\n <td style="text-align:right; font-size:12px; font-family:monospace; opacity:.7;" :title="typeBytesTooltip(t.type)">{{ fmtSizeBytes(typeBytes(t.type)) }}</td>\n <td style="text-align:right; font-size:12px;">\n <if :="typeIndexCount(t.type) > 0">\n <span style="\n padding:1px 7px; border-radius:999px;\n border:1px solid color-mix(in srgb, var(--okdb-purple) 33%, transparent); color:var(--okdb-purple);\n font-size:11px;\n ">{{ typeIndexCount(t.type) }}</span>\n </if>\n <if :="typeIndexCount(t.type) === 0">\n <span style="opacity:.3;">—</span>\n </if>\n </td>\n <td style="text-align:right; opacity:.3; font-size:11px; padding-right:16px;">→</td>\n </tr>\n </tbody>\n </table>\n </div>\n </if>\n\n \x3c!-- Violations summary --\x3e\n <if :="totalViolations > 0 || violationsBusy || violationsError">\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">Schema Violations</span>\n <if :="totalViolations > 0">\n <span style="font-size:11px; padding:2px 10px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-danger) 33%, transparent); color:var(--okdb-danger); background:color-mix(in srgb, var(--okdb-danger) 5%, transparent); font-weight:600;">{{ totalViolations }}</span>\n </if>\n <span style="font-size:12px; opacity:.45; margin-left:auto;">across all types in {{ envName }}</span>\n <button @click="loadViolations()" :disabled="violationsBusy" style="font-size:12px; padding:5px 12px; opacity:.75;">↻</button>\n </div>\n <if :="violationsBusy && violations.length === 0">\n <div style="padding:14px 16px; font-size:12px; opacity:.45;">Loading…</div>\n </if>\n <if :="violationsError">\n <div style="padding:10px 16px; color:var(--okdb-danger); font-size:12px;">{{ violationsError }}</div>\n </if>\n <if :="!violationsBusy && !violationsError && violationsByType.length === 0">\n <div style="padding:14px 16px; font-size:12px; opacity:.4;">No violations.</div>\n </if>\n <if :="violationsByType.length > 0">\n <div>\n <each :of="violationsByType" let:item="grp">\n \x3c!-- type group header --\x3e\n <div class="ov-viol-group" @click="toggleViolationGroup(grp.type)">\n <span class="ov-viol-arrow" :style="expandedViolationType === grp.type ? \'transform:rotate(90deg)\' : \'\'">›</span>\n <span style="font-family:monospace; font-size:13px; flex:1;">{{ grp.type }}</span>\n <span style="font-size:11px; padding:2px 8px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-danger) 33%, transparent); color:var(--okdb-danger); background:color-mix(in srgb, var(--okdb-danger) 5%, transparent); margin-right:8px;">{{ grp.count }}</span>\n <button\n @click:stop="navigateToType(grp.type)"\n style="font-size:11px; padding:2px 8px; border-radius:5px; border:1px solid var(--okdb-app-border); background:transparent; color:var(--okdb-sky); cursor:pointer; opacity:.8;"\n title="Open Schema tab for this type"\n >Schema →</button>\n </div>\n \x3c!-- expanded violations for this type --\x3e\n <if :="expandedViolationType === grp.type">\n <div class="ov-viol-list">\n <each :of="grp.items" let:item="v">\n <div class="ov-viol-row">\n <span style="font-family:monospace; font-size:11px; color:var(--okdb-json-key); flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ v.key }}</span>\n <span style="font-size:11px; opacity:.45; white-space:nowrap; margin-left:8px;">{{ (v.errors ?? []).length || (v.errorCount ?? 0) }} error(s)</span>\n </div>\n </each>\n </div>\n </if>\n </each>\n </div>\n </if>\n </div>\n </if>\n\n \x3c!-- Time Machine card --\x3e\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">⏱ Time Machine</span>\n <if :="tmStatus && tmStatus.autoEnable">\n <span style="font-size:10px; padding:2px 8px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-success) 33%, transparent); color:var(--okdb-success); background:color-mix(in srgb, var(--okdb-success) 7%, transparent);">auto-enable</span>\n </if>\n <if :="!tmStatus">\n <span style="font-size:11px; opacity:.4; margin-left:auto;">loading…</span>\n </if>\n <if :="tmStatus">\n <div style="margin-left:auto; display:flex; gap:6px;">\n <button @click="loadTMStatus()" :disabled="tmBusy" style="font-size:11px; padding:2px 9px; opacity:.65;">↻</button>\n <button @click="enableAllTM()" :disabled="tmBusy" style="font-size:11px; padding:2px 10px; color:var(--okdb-success); border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);">{{ tmBusy ? \'…\' : \'Enable All\' }}</button>\n <button @click="disableAllTM()" :disabled="tmBusy" style="font-size:11px; padding:2px 10px; color:var(--okdb-danger); border-color:color-mix(in srgb, var(--okdb-danger) 33%, transparent);">{{ tmBusy ? \'…\' : \'Disable All\' }}</button>\n </div>\n </if>\n </div>\n <if :="tmStatus && tmStatus.types && tmStatus.types.length > 0">\n \x3c!-- CSS-grid rows instead of <table>: HTML\'s parser would hoist okjs\'s\n <each> directive out of <tbody> (only <tr> is valid there), breaking\n iteration. All other admin sections use this grid+each pattern. --\x3e\n <div style="display:flex; flex-direction:column; font-size:13px;">\n <div style="display:grid; grid-template-columns:1fr 80px 120px 100px; gap:8px; padding:6px 12px; font-size:11px; opacity:.55; border-bottom:1px solid var(--okdb-app-border);">\n <div>Type</div>\n <div style="text-align:right;">Heads</div>\n <div style="text-align:right;">Last Clock</div>\n <div></div>\n </div>\n <each :of="tmStatus.types" let:item="t">\n <div style="display:grid; grid-template-columns:1fr 80px 120px 100px; gap:8px; padding:6px 12px; border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05)); align-items:center;">\n <div>\n <span style="font-family:monospace;">{{ t.type }}</span>\n <if :="!t.enabled">\n <span style="font-size:10px; opacity:.4; margin-left:6px;">disabled</span>\n </if>\n </div>\n <div style="text-align:right; font-size:12px; font-family:monospace; opacity:.7;">{{ t.headCount ?? \'—\' }}</div>\n <div style="text-align:right; font-size:11px; font-family:monospace; opacity:.5;">{{ t.lastClock ?? \'—\' }}</div>\n <div style="text-align:right;">\n <if :="t.enabled">\n <button @click="disableTMType(t.type)" :disabled="tmTypeBusy.has(t.type)" style="font-size:10px; padding:1px 8px; color:var(--okdb-danger); border-color:color-mix(in srgb, var(--okdb-danger) 33%, transparent);">{{ tmTypeBusy.has(t.type) ? \'…\' : \'Disable\' }}</button>\n </if>\n <if :="!t.enabled">\n <button @click="enableTMType(t.type)" :disabled="tmTypeBusy.has(t.type)" style="font-size:10px; padding:1px 8px; color:var(--okdb-success); border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);">{{ tmTypeBusy.has(t.type) ? \'…\' : \'Enable\' }}</button>\n </if>\n </div>\n </div>\n </each>\n </div>\n </if>\n <if :="tmStatus && (!tmStatus.types || tmStatus.types.length === 0)">\n <div style="padding:14px 16px; font-size:12px; opacity:.4;">\n No types registered in this environment yet.\n </div>\n </if>\n </div>\n\n <div class="ov-card" style="overflow:visible;">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">Pipelines</span>\n <span style="font-size:12px; opacity:.45; margin-left:auto;">overview highlights for the current environment</span>\n </div>\n <div style="padding:16px; display:flex; flex-direction:column; gap:14px;">\n <div class="ov-pipelines-headline">\n <div style="display:flex; flex-direction:column; gap:6px; min-width:0; flex:1;">\n <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <div style="font-size:15px; font-weight:600; color:var(--okdb-app-fg);">{{ pipelineSummaryText }}</div>\n <if :="pipelineHealthCounts.error > 0">\n <span class="ov-pipeline-pill" :style="\'color:var(--okdb-danger);border-color:var(--okdb-danger-deep-border);background:#450a0a33;\'">{{ pipelineHealthCounts.error }} error</span>\n </if>\n <if :="pipelineHealthCounts.degraded > 0">\n <span class="ov-pipeline-pill" :style="\'color:#fcd34d;border-color:#854d0e;background:#451a0333;\'">{{ pipelineHealthCounts.degraded }} degraded</span>\n </if>\n <if :="pipelineHealthCounts.online > 0">\n <span class="ov-pipeline-pill" :style="\'color:#86efac;border-color:#166534;background:#052e1633;\'">{{ pipelineHealthCounts.online }} online</span>\n </if>\n </div>\n <div style="font-size:12px; color:var(--okdb-app-fg-muted); line-height:1.6; max-width:640px;">\n The overview should only surface high-signal pipeline highlights. Open the dedicated workspace when you want the full grouped pipeline list and member details.\n </div>\n </div>\n <div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center; justify-content:flex-end;">\n <button @click="loadPipelines()" :disabled="pipelinesBusy" style="font-size:12px; padding:5px 12px; opacity:.75;">↻ Refresh</button>\n <button @click="openPipelinesWorkspace()" style="font-size:12px; padding:5px 12px; border-color:color-mix(in srgb, var(--okdb-indigo) 33%, transparent); color:var(--okdb-indigo);">Open pipelines workspace →</button>\n </div>\n </div>\n\n <if :="pipelinesError">\n <div class="ov-pipelines-error">{{ pipelinesError }}</div>\n </if>\n\n <if :="pipelinesBusy && pipelines.length === 0 && !pipelinesError">\n <div style="font-size:12px; opacity:.45;">Loading pipeline highlights…</div>\n </if>\n\n <if :="!pipelinesBusy && pipelines.length === 0 && !pipelinesError">\n <div class="ov-pipelines-empty">\n <div style="font-size:24px; opacity:.3;">⛓</div>\n <div style="font-size:14px; font-weight:600; opacity:.78;">No pipelines in {{ envName }}</div>\n <div style="font-size:12px; opacity:.5; max-width:520px; line-height:1.7; text-align:center;">\n Pipelines will appear here once created or synced. This frontpage intentionally stays compact and only shows a small highlight list.\n </div>\n </div>\n </if>\n\n <if :="pipelineHighlights.length > 0">\n <div class="ov-pipelines-list">\n <each :of="pipelineHighlights" let:item="pipeline">\n <button class="ov-pipeline-highlight" @click="openPipelinesWorkspace(pipeline)">\n <div style="display:flex; align-items:center; gap:9px; min-width:0;">\n <span :style="\'width:8px;height:8px;border-radius:50%;flex-shrink:0;background:\' + healthColor(pipeline.health)"></span>\n <span style="font-size:13px; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ pipelineLabel(pipeline) }}</span>\n <span :style="\'font-size:11px;color:\' + healthColor(pipeline.health) + \';opacity:.95;white-space:nowrap;\'">{{ pipeline.health || \'unknown\' }}</span>\n </div>\n <div style="display:flex; gap:12px; flex-wrap:wrap; margin-top:6px; font-size:11px; color:var(--okdb-app-fg-muted);">\n <span><span style="opacity:.6;">Members:</span> <span style="color:var(--okdb-app-fg);">{{ fmt(pipeline.engines?.length || 0) }}</span></span>\n <if :="pipeline.status">\n <span><span style="opacity:.6;">Status:</span> <span :style="\'color:\' + statusColor(pipeline.status)">{{ pipeline.status }}</span></span>\n </if>\n <span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ pipelineRoleSummary(pipeline) }}</span>\n </div>\n </button>\n </each>\n </div>\n </if>\n\n <if :="pipelines.length > pipelineHighlights.length">\n <div style="font-size:11px; color:var(--okdb-app-fg-muted); opacity:.75;">\n Showing {{ pipelineHighlights.length }} of {{ pipelines.length }} pipelines. Open the dedicated workspace for the full inventory.\n </div>\n </if>\n </div>\n </div>\n\n \x3c!-- Recent activity --\x3e\n <if :="recentChanges.length > 0">\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">Recent Activity</span>\n <span style="font-size:12px; opacity:.4; margin-left:auto;">last {{ recentChanges.length }} changes</span>\n </div>\n <table style="width:100%; border-collapse:collapse; font-size:12px;">\n <tbody>\n <tr as="repeat" :of="recentChanges" let:item="ch" class="ov-row ov-row--sm">\n <td class="ov-ch-td" style="text-align:right; width:60px; font-family:monospace; opacity:.4; padding-left:16px;">\n {{ ch.clock }}\n </td>\n <td class="ov-ch-td" style="width:100px;">\n <span :style="\'font-size:10px; padding:2px 6px; border-radius:3px; border:1px solid; color:\'+actionColor(ch.action)+\'; border-color:\'+actionColor(ch.action)+\'55;\'">\n {{ actionLabel(ch.action) }}\n </span>\n </td>\n <td class="ov-ch-td" style="font-family:monospace; font-size:11px; opacity:.7; max-width:110px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">\n {{ ch.type || \'—\' }}\n </td>\n <td class="ov-ch-td" style="font-family:monospace; font-size:11px; opacity:.45; max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">\n {{ ch.key || \'\' }}\n </td>\n <td class="ov-ch-td" style="text-align:right; opacity:.35; white-space:nowrap; font-size:11px; padding-right:16px;">\n {{ relTime(ch.timestamp) }}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </if>\n\n</div>\n',style:"\n db-overview .ov-stat-row {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 12px;\n }\n db-overview .ov-stat-card {\n background: var(--okdb-app-surface);\n border: 1px solid var(--okdb-app-border);\n border-radius: 12px;\n padding: 16px 18px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n box-shadow: 0 2px 8px #0003;\n }\n db-overview .ov-stat-label {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: .06em;\n opacity: .5;\n }\n db-overview .ov-stat-value {\n font-size: 22px;\n font-weight: 700;\n line-height: 1;\n }\n db-overview .ov-card {\n background: var(--okdb-app-surface);\n border: 1px solid var(--okdb-app-border);\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 2px 8px #0003;\n }\n db-overview .ov-card-head {\n padding: 13px 16px;\n border-bottom: 1px solid var(--okdb-app-border);\n display: flex;\n align-items: center;\n gap: 10px;\n }\n db-overview .ov-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n }\n db-overview .ov-table th {\n padding: 7px 16px;\n text-align: left;\n font-weight: 500;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: .05em;\n opacity: .4;\n border-bottom: 1px solid var(--okdb-app-border);\n background: var(--okdb-app-bg);\n }\n db-overview .ov-table td {\n padding: 9px 16px;\n border-bottom: 1px solid var(--okdb-app-surface-2);\n }\n db-overview .ov-row {\n cursor: pointer;\n transition: background .1s;\n }\n db-overview .ov-row:hover td {\n background: #ffffff07;\n }\n db-overview .ov-table tbody tr:last-child td {\n border-bottom: none;\n }\n db-overview .ov-ch-td {\n padding: 6px 8px;\n border-bottom: 1px solid var(--okdb-app-surface);\n }\n db-overview .ov-pipelines-headline {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n align-items: flex-start;\n flex-wrap: wrap;\n }\n db-overview .ov-pipeline-pill {\n font-size: 10px;\n padding: 2px 8px;\n border-radius: 999px;\n border: 1px solid;\n white-space: nowrap;\n }\n db-overview .ov-pipelines-error {\n color: var(--okdb-danger);\n font-size: 12px;\n padding: 10px 12px;\n border-radius: 10px;\n border: 1px solid color-mix(in srgb, var(--okdb-danger) 33%, transparent);\n background: color-mix(in srgb, var(--okdb-danger) 5%, transparent);\n }\n db-overview .ov-pipelines-empty {\n border: 1px dashed var(--okdb-app-border);\n border-radius: 12px;\n padding: 24px 20px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n align-items: center;\n }\n db-overview .ov-pipelines-list {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));\n gap: 10px;\n }\n db-overview .ov-pipeline-highlight {\n width: 100%;\n text-align: left;\n border-radius: 10px;\n border: 1px solid var(--okdb-app-border);\n background: var(--okdb-app-bg-deep);\n padding: 11px 12px;\n cursor: pointer;\n }\n db-overview .ov-pipeline-highlight:hover {\n border-color: var(--okdb-app-border-strong);\n background: var(--okdb-app-bg-deep);\n }\n db-overview tbody .ov-row--sm:last-child .ov-ch-td {\n border-bottom: none;\n }\n db-overview .ov-viol-group {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 9px 16px;\n border-bottom: 1px solid var(--okdb-app-surface-2);\n cursor: pointer;\n transition: background .1s;\n }\n db-overview .ov-viol-group:hover { background: #ffffff07; }\n db-overview .ov-viol-arrow {\n display: inline-block;\n font-size: 14px;\n opacity: .5;\n transition: transform .15s;\n width: 14px;\n flex-shrink: 0;\n }\n db-overview .ov-viol-list {\n background: var(--okdb-app-bg-deep);\n border-bottom: 1px solid var(--okdb-app-surface-2);\n }\n db-overview .ov-viol-row {\n display: flex;\n align-items: center;\n padding: 6px 16px 6px 38px;\n border-bottom: 1px solid var(--okdb-app-surface);\n font-size: 12px;\n }\n db-overview .ov-viol-row:last-child { border-bottom: none; }\n db-overview .ov-empty {\n border: 1px dashed var(--okdb-app-border);\n border-radius: 14px;\n padding: 30px 32px;\n text-align: center;\n opacity: .7;\n }\n /* ── Demo data card ── */\n db-overview .ov-demo-card {\n position: relative;\n border: 1px solid var(--okdb-app-border);\n border-radius: 16px;\n padding: 28px 24px 24px;\n text-align: center;\n cursor: pointer;\n overflow: hidden;\n transition: border-color .25s, box-shadow .25s, transform .2s;\n background: var(--okdb-app-bg-deep);\n }\n db-overview .ov-demo-card:hover {\n border-color: color-mix(in srgb, var(--okdb-accent) 53%, transparent);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--okdb-accent) 13%, transparent), 0 8px 40px color-mix(in srgb, var(--okdb-accent) 9%, transparent), 0 0 80px color-mix(in srgb, var(--okdb-purple) 4%, transparent);\n transform: translateY(-2px);\n }\n db-overview .ov-demo-card__aurora {\n position: absolute;\n inset: 0;\n background:\n radial-gradient(ellipse at 20% 50%, color-mix(in srgb, var(--okdb-accent) 4%, transparent), transparent 55%),\n radial-gradient(ellipse at 80% 30%, color-mix(in srgb, var(--okdb-purple) 4%, transparent), transparent 55%),\n radial-gradient(ellipse at 50% 90%, color-mix(in srgb, var(--okdb-success) 4%, transparent), transparent 55%);\n pointer-events: none;\n animation: ov-aurora 8s ease-in-out infinite alternate;\n }\n db-overview .ov-demo-card__sparkle {\n font-size: 26px;\n background: linear-gradient(135deg, var(--okdb-accent), var(--okdb-purple) 40%, var(--okdb-success));\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n animation: ov-hue 6s linear infinite;\n }\n db-overview .ov-demo-card__title {\n font-size: 17px;\n font-weight: 700;\n background: linear-gradient(135deg, var(--okdb-app-fg-strong), var(--okdb-accent));\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n }\n db-overview .ov-demo-card__desc {\n font-size: 13px;\n opacity: .55;\n max-width: 380px;\n line-height: 1.65;\n }\n db-overview .ov-demo-card__badges {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n justify-content: center;\n }\n db-overview .ov-demo-badge {\n padding: 3px 10px;\n border-radius: 999px;\n border: 1px solid;\n font-size: 11px;\n font-weight: 500;\n transition: transform .15s;\n }\n db-overview .ov-demo-card:hover .ov-demo-badge { transform: scale(1.05); }\n db-overview .ov-demo-card__cta {\n margin-top: 4px;\n font-size: 13px;\n font-weight: 600;\n color: var(--okdb-accent);\n opacity: .8;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: opacity .15s;\n }\n db-overview .ov-demo-card:hover .ov-demo-card__cta { opacity: 1; }\n @keyframes ov-hue {\n from { filter: hue-rotate(0deg); }\n to { filter: hue-rotate(360deg); }\n }\n @keyframes ov-aurora {\n from { opacity: .7; }\n to { opacity: 1.3; }\n }\n "};
|
|
@@ -1 +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)}`:"",
|
|
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)}`:"",r=await fetch(`${__ENV__.API}/locks/${encodeURIComponent(e.namespace)}/${encodeURIComponent(e.id)}${o}`,{method:"DELETE"}),n=await r.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 <if :="releaseConfirm">\n <div 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 <if :="releaseError">\n <span style="color:var(--okdb-danger);">{{ releaseError }}</span>\n </if>\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 </if>\n\n <if :="!locks.length && !busy">\n <div style="font-size:12px;color:var(--okdb-app-fg-subtle);padding:8px 0;">No active locks.</div>\n </if>\n\n <if :="locks.length"><div style="display:flex;flex-direction:column;font-size:12px;">\n \x3c!-- header row --\x3e\n <div style="display:grid;grid-template-columns:120px 120px 1fr 200px 60px 80px;gap:8px;padding:4px 8px;color:var(--okdb-app-fg-subtle);border-bottom:1px solid var(--okdb-app-border);font-weight:500;">\n <div>env</div>\n <div>namespace</div>\n <div>id</div>\n <div>holder</div>\n <div>age</div>\n <div></div>\n </div>\n <each :of="locks" let:item="lock">\n <div style="display:grid;grid-template-columns:120px 120px 1fr 200px 60px 80px;gap:8px;padding:4px 8px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));align-items:center;">\n <div style="color:var(--okdb-app-fg-subtle);">{{ lock.env }}</div>\n <div>{{ lock.namespace }}</div>\n <div style="font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" :title="lock.id">{{ lock.id }}</div>\n <div style="font-family:monospace;">{{ holderLabel(lock.entry) }}</div>\n <div style="color:var(--okdb-app-fg-subtle);">{{ ageLabel(lock.entry) }}</div>\n <div>\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 </div>\n </div>\n </each>\n </div></if>\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,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 refSections(){const e=this.sections?.refs??{};return Object.entries(e).map(([e,t])=>({name:e,...t}))},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 :="refSections" let:ref><div style="display:flex;align-items:center;gap:8px;font-size:12px;margin-bottom:4px;"><span style="min-width:80px">{{ ref.name }}:</span> <span :style="\'color:\' + stateColor(ref.state)">{{ ref.state }}</span><if :="ref.progress"><span>{{ Math.round(ref.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>'};
|
|
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 refSections(){const e=this.sections?.refs??{};return Object.entries(e).map(([e,t])=>({name:e,...t}))},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 :="state === \'ready\' || isInProgress"><button class="vd__btn vd__btn--warning" :disabled="actionLoading" @click="doAction(\'stop\')">Stop</button></if><if :="(state === \'stopped\' || state === \'halted\') && !isInProgress"><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><if :="!isInProgress"><button class="vd__btn vd__btn--danger" :disabled="actionLoading" @click="showDeleteConfirm = true">Delete</button></if><if :="isInProgress"><button class="vd__btn vd__btn--danger" disabled="disabled" title="Stop or Rebuild first">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 :="refSections" let:ref><div style="display:flex;align-items:center;gap:8px;font-size:12px;margin-bottom:4px;"><span style="min-width:80px">{{ ref.name }}:</span> <span :style="\'color:\' + stateColor(ref.state)">{{ ref.state }}</span><if :="ref.progress"><span>{{ Math.round(ref.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>'};
|