@kedem/okdb 1.9.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/bin/okdb.js +1 -1
- package/docs/deployment.md +155 -0
- package/docs/diagnostics.md +115 -0
- package/docs/embeddings.md +1 -1
- package/docs/functions.md +30 -27
- package/docs/getting-started.md +14 -3
- package/docs/http-api.md +3 -3
- package/docs/http-cluster.md +93 -60
- package/docs/index.md +13 -1
- package/docs/manifest.json +6 -3
- package/docs/pipelines.md +1 -1
- package/docs/process-registry.md +5 -5
- package/docs/processors.md +35 -18
- package/docs/queue.md +112 -42
- package/docs/sync.md +10 -7
- package/docs/ttl.md +1 -1
- package/docs/upgrade-2.0.md +290 -0
- package/okdb-functions-sandbox-worker.js +1 -1
- package/okdb-http-worker-child.js +1 -1
- package/okdb-views-bootstrap-worker.js +1 -1
- package/okdb.js +1 -1
- package/package.json +1 -1
- package/public/layouts/_default.ok.html +1 -1
- package/public/sections/db/modals/create-env-modal.ok.js +1 -1
- package/public/sections/db/modals/demo-env-modal.ok.js +1 -1
- package/public/sections/db/parts/db-overview.ok.js +1 -1
- package/public/sections/engines/engine-ui-utils.js +1 -1
- package/public/sections/engines/parts/engine-declaration-editor.ok.js +1 -1
- package/public/sections/engines/parts/generic-engine-panel.ok.js +1 -1
- package/public/sections/queue/parts/code-panel.ok.js +1 -1
- package/public/sections/queue/parts/job-log-panel.ok.js +1 -1
- package/public/sections/queue/parts/queue-jobs.ok.js +1 -1
- package/public/sections/sync/parts/sync-topology.ok.js +1 -1
- package/public/sections/system/index.ok.html +1 -1
- package/public/sections/system/parts/system-process-panel.ok.js +1 -1
- package/public/sections/system/parts/system-processing-panel.ok.js +1 -1
- package/public/sections/system/parts/system-runtime-overview.ok.js +1 -1
- package/public/setup-app.js +1 -1
- package/public/setup.html +116 -55
- package/types/index.d.ts +33 -3
- package/types/options.d.ts +19 -10
- package/docs/worker-fleet.md +0 -139
- package/okdb-functions-runner-child.js +0 -1
- package/okdb-queue-load-handler.js +0 -1
- package/okdb-queue-spawn-child.js +0 -1
- package/okdb-worker-child.js +0 -1
- package/public/sections/system/parts/system-workers-panel.ok.js +0 -1
|
@@ -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:new Set,ftsSize:null,ftsSizeBusy:!1,procRows:[],get procMaxLag(){return this.procRows.reduce((e,n)=>Math.max(e,n.lag||0),0)},get procCaughtUp(){return this.procRows.length>0&&0===this.procMaxLag},procLagColor:e=>0===e?"var(--okdb-success)":e<1e3?"var(--okdb-warning)":"var(--okdb-danger)",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 s=[];if(n.indexes)for(const[e,o]of Object.entries(n.indexes)){const n=o?.pages||0;i+=n,s.push(` · ${e}: ${this.fmtSizeBytes(n*t)}`)}const a=[`Data: ${this.fmtSizeBytes(o*t)}`];return s.length&&(a.push(`Indexes: ${this.fmtSizeBytes(i*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 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(),this.loadProcStatus()])},async loadProcStatus(){try{const n=e.$ok.stores.okdb.selectedEnv||"default",t=await fetch(`${__ENV__.API}/processing/status`);if(!t.ok)return void(this.procRows=[]);const o=await t.json(),i=o.data??o.result??{};this.procRows=i.envs&&i.envs[n]||[]}catch{this.procRows=[]}},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 _toggleTMType(n,t){if(this.tmTypeBusy.has(n))return;this.tmTypeBusy.add(n),this.tmTypeBusy=new Set(this.tmTypeBusy);const o=this.tmStatus?.types?.find(e=>e.type===n),i=o?o.enabled:null;o&&(o.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&&o&&(o.enabled=i)}catch{o&&(o.enabled=i)}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!-- Processing / derived-state card (this env\'s processor lag). The\n process-wide pool view lives under System → Processing; this is the\n "is THIS env\'s data indexed?" lens. --\x3e\n <if :="procRows.length > 0">\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">⚇ Processing</span>\n <if :="procCaughtUp">\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);">caught up</span>\n </if>\n <if :="!procCaughtUp">\n <span :style="\'font-size:10px; padding:2px 8px; border-radius:999px; border:1px solid color-mix(in srgb, \' + procLagColor(procMaxLag) + \' 33%, transparent); color:\' + procLagColor(procMaxLag) + \'; background:color-mix(in srgb, \' + procLagColor(procMaxLag) + \' 7%, transparent);\'">{{ procMaxLag }} behind</span>\n </if>\n <a href="#/system/processing" style="margin-left:auto; font-size:11px; opacity:.7; text-decoration:none; color:var(--okdb-accent);">View pool →</a>\n </div>\n <div style="display:flex; flex-direction:column; font-size:13px;">\n <div style="display:grid; grid-template-columns:1fr 90px 100px 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>Mode</div>\n <div>State</div>\n <div style="text-align:right;">Lag</div>\n </div>\n <each :of="procRows" let:item="p">\n <div style="display:grid; grid-template-columns:1fr 90px 100px 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 style="font-family:monospace;">{{ p.type }}</div>\n <div style="font-size:12px; opacity:.7;">{{ p.mode }}</div>\n <div style="font-size:12px;">{{ p.state }}</div>\n <div style="text-align:right; font-family:monospace;" :style="\'color:\' + procLagColor(p.lag)">{{ p.lag }}</div>\n </div>\n </each>\n </div>\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
|
+
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,procRows:[],get procMaxLag(){return this.procRows.reduce((e,n)=>Math.max(e,n.lag||0),0)},get procCaughtUp(){return this.procRows.length>0&&0===this.procMaxLag},procLagColor:e=>0===e?"var(--okdb-success)":e<1e3?"var(--okdb-warning)":"var(--okdb-danger)",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 s=[];if(n.indexes)for(const[e,o]of Object.entries(n.indexes)){const n=o?.pages||0;i+=n,s.push(` · ${e}: ${this.fmtSizeBytes(n*t)}`)}const a=[`Data: ${this.fmtSizeBytes(o*t)}`];return s.length&&(a.push(`Indexes: ${this.fmtSizeBytes(i*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 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(),this.loadProcStatus()])},async loadProcStatus(){try{const n=e.$ok.stores.okdb.selectedEnv||"default",t=await fetch(`${__ENV__.API}/processors/status`);if(!t.ok)return void(this.procRows=[]);const o=await t.json(),i=o.data??o.result??{};this.procRows=i.envs&&i.envs[n]||[]}catch{this.procRows=[]}},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 _toggleTMType(n,t){if(this.tmTypeBusy.has(n))return;this.tmTypeBusy.add(n),this.tmTypeBusy=new Set(this.tmTypeBusy);const o=this.tmStatus?.types?.find(e=>e.type===n),i=o?o.enabled:null;o&&(o.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&&o&&(o.enabled=i)}catch{o&&(o.enabled=i)}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!-- Processing / derived-state card (this env\'s processor lag). The\n process-wide pool view lives under System → Processing; this is the\n "is THIS env\'s data indexed?" lens. --\x3e\n <if :="procRows.length > 0">\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">⚇ Processing</span>\n <if :="procCaughtUp">\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);">caught up</span>\n </if>\n <if :="!procCaughtUp">\n <span :style="\'font-size:10px; padding:2px 8px; border-radius:999px; border:1px solid color-mix(in srgb, \' + procLagColor(procMaxLag) + \' 33%, transparent); color:\' + procLagColor(procMaxLag) + \'; background:color-mix(in srgb, \' + procLagColor(procMaxLag) + \' 7%, transparent);\'">{{ procMaxLag }} behind</span>\n </if>\n <a href="#/system/processing" style="margin-left:auto; font-size:11px; opacity:.7; text-decoration:none; color:var(--okdb-accent);">View pool →</a>\n </div>\n <div style="display:flex; flex-direction:column; font-size:13px;">\n <div style="display:grid; grid-template-columns:1fr 90px 100px 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>Mode</div>\n <div>State</div>\n <div style="text-align:right;">Lag</div>\n </div>\n <each :of="procRows" let:item="p">\n <div style="display:grid; grid-template-columns:1fr 90px 100px 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 style="font-family:monospace;">{{ p.type }}</div>\n <div style="font-size:12px; opacity:.7;">{{ p.mode }}</div>\n <div style="font-size:12px;">{{ p.state }}</div>\n <div style="text-align:right; font-family:monospace;" :style="\'color:\' + procLagColor(p.lag)">{{ p.lag }}</div>\n </div>\n </each>\n </div>\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 const EMBEDDINGS_ENGINE_TYPES=new Set(["embedder","indexer","embed-worker","vector-search"]);export function isEmbeddingsEngineType(e){return EMBEDDINGS_ENGINE_TYPES.has(e)}export function isEmbeddingsEngine(e){return isEmbeddingsEngineType(e?.type)}export function cloneValue(e,n={}){try{return JSON.parse(JSON.stringify(e??n))}catch{return JSON.parse(JSON.stringify(n))}}export function parseTagList(e){const n=String(e??"").split(",").map(e=>e.trim()).filter(Boolean);return n.length?[...new Set(n)]:null}export function statusTone(e=null){return e?.isRunning?"var(--okdb-success)":"inactive"===e?.status?"var(--okdb-warning)":"error"===e?.status?"var(--okdb-danger)":"var(--okdb-app-fg-muted)"}export function readableReason(e){return e?String(e).replace(/_/g," "):null}export function formatJsonValue(e){try{return JSON.stringify(e,null,2)}catch{return String(e??"")}}export function resolveEngineEnvName(e=null,n=null){return n??e?.env??e?._uiEnv??e?.storeEnv??e?.config?.source_env??null}export function getEnginePipelineName(e=null){return e?.meta?.pipeline??null}export function buildFunctionsRoute(e,n=null){const t=new URLSearchParams,r=e?.$ok?.stores?.okdb?.selectedEnv??null;r&&t.set("env",r),n&&t.set("name",n);const o=t.toString();return"/functions"+(o?`?${o}`:"")}export function openFunctionRoute(e,n,{beforeNavigate:t}={}){if(!n)return!1;const r=e?.$vars?.$router;if("function"==typeof t&&t(),r?.push){const t={...r._search||{}},o=e?.$ok?.stores?.okdb?.selectedEnv??null;o?t.env=o:delete t.env,t.name=n;const u="function"==typeof r.resolve?r.resolve("/functions",t):buildFunctionsRoute(e,n);return r.push(u),!0}return!!globalThis.location&&(globalThis.location.hash=`#${buildFunctionsRoute(e,n)}`,!0)}export function dispatchEnginePipelineSelection(e,n=null){const t=getEnginePipelineName(n);return!!t&&(e.$dispatch("select-pipeline",t),!0)}function matchesLinkedEngine(e=null,n=null){return!!n&&(e?.name===n||e?.key===n)}function matchesLinkedFunction(e=null,n=null){return!!n&&(e?.config?.handler?.name===n||e?.runtime?.handler?.name===n)}function linkedMemberSourceType(e=null){return e?.config?.source_type??e?.runtime?.source_type??null}function linkedMemberJobType(e=null){return e?.config?.job_type??e?.runtime?.job_type??null}function linkedMemberQueueEnv(e=null){return e?.config?.queue_env??e?.runtime?.queue_env??null}export async function resolveLinkedPipeline(e,{envName:n=null,pipelineName:t=null,ownerEngine:r=null,functionName:o=null}={}){const u=n??e?.$ok?.stores?.okdb?.selectedEnv??null;if(!u||!t&&!r&&!o)return null;let c=[],i=null,s=null,a=null,l="pipelines";try{const e=await fetch(`${__ENV__.API}/env/${encodeURIComponent(u)}/pipelines`);if(e.ok){const n=await e.json().catch(()=>({}));c=n?.data??n?.result??[],i=c.find(e=>!!e&&(!(!t||e.name!==t)||(e.engines??[]).some(e=>matchesLinkedEngine(e,r)||matchesLinkedFunction(e,o))))??null,s=(i?.engines??[]).find(e=>matchesLinkedEngine(e,r))??(i?.engines??[]).find(e=>matchesLinkedFunction(e,o))??(i?.engines??[]).find(e=>linkedMemberSourceType(e)&&["processor","materializer","queue-worker"].includes(e?.type))??(i?.engines??[]).find(e=>linkedMemberSourceType(e))??(i?.engines??[]).find(e=>e?.config?.job_type)??null,["processor","materializer","queue-worker"].includes(s?.type)&&(l="engines")}}catch{}if(!s&&(r||o))try{const e=await fetch(`${__ENV__.API}/env/${encodeURIComponent(u)}/engines`);if(e.ok){const n=await e.json().catch(()=>({})),t=n?.data??n?.result??[];a=t.find(e=>e?.name===r||e?.key===r)??t.find(e=>e?.config?.handler?.name===o)??null,!i&&a&&(i=c.find(e=>(e?.engines??[]).some(e=>matchesLinkedEngine(e,a?.name)||matchesLinkedEngine(e,a?.key)||matchesLinkedFunction(e,o)))??null),!s&&i&&(s=(i?.engines??[]).find(e=>matchesLinkedEngine(e,a?.name))??(i?.engines??[]).find(e=>matchesLinkedEngine(e,a?.key))??(i?.engines??[]).find(e=>matchesLinkedFunction(e,o))??null),["processor","materializer","queue-worker"].includes((s??a)?.type)&&(l="engines")}}catch{}const p=linkedMemberSourceType(s)??a?.config?.source_type??a?.runtime?.source_type??(i?.engines??[]).map(linkedMemberSourceType).find(Boolean)??null,d=linkedMemberJobType(s)??a?.config?.job_type??a?.runtime?.job_type??(i?.engines??[]).map(linkedMemberJobType).find(Boolean)??null,m=linkedMemberQueueEnv(s)??a?.config?.queue_env??a?.runtime?.queue_env??u;return"queue-worker"===(s??a)?.type||d?{envName:m,pipelineName:t??i?.name??null,ownerEngine:r??s?.name??s?.key??a?.name??a?.key??null,sourceType:null,jobType:d,targetTab:"workers",targetSection:"queue"}:p?{envName:u,pipelineName:t??i?.name??null,ownerEngine:r??s?.name??s?.key??a?.name??a?.key??null,sourceType:p,jobType:null,targetTab:l,targetSection:"db"}:null}export async function openLinkedPipelineRoute(e,{envName:n=null,pipelineName:t=null,ownerEngine:r=null,functionName:o=null,beforeNavigate:u}={}){const c=e?.$vars?.$router,i=await resolveLinkedPipeline(e,{envName:n,pipelineName:t,ownerEngine:r,functionName:o});if(!i)return!1;if("function"==typeof u&&u(),e?.$ok?.stores?.okdb?.selectedEnv&&e.$ok.stores.okdb.selectedEnv!==i.envName&&e.$ok.stores.okdb.selectEnv(i.envName),c?.push&&"function"==typeof c.resolve){if("queue"===i.targetSection){const e={env:i.envName,view:i.targetTab??"workers"};return i.jobType&&(e.type=i.jobType),c.push(c.resolve("/queue",e)),!0}if(i.pipelineName){const e={env:i.envName,pipeline:i.pipelineName};return i.sourceType&&(e.sourceType=i.sourceType),c.push(c.resolve("/engines",e)),!0}return!!i.sourceType&&(c.push(c.resolve("/db",{env:i.envName,type:i.sourceType,tab:i.targetTab})),!0)}if(globalThis.location){if("queue"===i.targetSection){const e=new URLSearchParams({env:i.envName,view:i.targetTab??"workers"});return i.jobType&&e.set("type",i.jobType),globalThis.location.hash=`#/queue?${e.toString()}`,!0}if(i.pipelineName){const e=new URLSearchParams({env:i.envName,pipeline:i.pipelineName});return i.sourceType&&e.set("sourceType",i.sourceType),globalThis.location.hash=`#/engines?${e.toString()}`,!0}if(!i.sourceType)return!1;const e=new URLSearchParams({env:i.envName,type:i.sourceType,tab:i.targetTab});return globalThis.location.hash=`#/db?${e.toString()}`,!0}return!1}export function buildEmbeddingsPipelineUrl(e=null,n=null){const t=e?.config??{},r=encodeURIComponent(t.source_env??"default"),o=encodeURIComponent(t.source_type??""),u=encodeURIComponent(t.storage_key??n??e?.name??"");return`${__ENV__.API}/env/${r}/type/${o}/pipelines/${u}`}export function maskSensitiveConfig(e={}){if(Array.isArray(e))return e.map(e=>maskSensitiveConfig(e));if(!e||"object"!=typeof e)return e;const n={};for(const[t,r]of Object.entries(e))n[t]=/key|secret|pass|token/i.test(t)?"***":maskSensitiveConfig(r);return n}export function summarizeValidationDetails(e=[]){if(!Array.isArray(e)||0===e.length)return null;const n=e.slice(0,3).map((e={})=>`${e.field?`${e.field} `:""}${e.message||"is invalid"}`.trim()).join("; ");return e.length>3?`${n} (+${e.length-3} more)`:n}export function getApiErrorMessage(e=null,n="Request failed"){const t=e?.error??null,r=summarizeValidationDetails(t?.details??e?.details??null);return"string"==typeof t?e?.message?e.message:r&&/^[A-Z0-9_]+$/.test(t)?`Validation failed: ${r}`:t||n:t?.message?r&&"VALIDATION_ERROR"===t.code&&!t.message.includes(r)?`${t.message} — ${r}`:t.message:e?.message?r&&!e.message.includes(r)?`${e.message} — ${r}`:e.message:r?`Validation failed: ${r}`:n}export function buildEngineBasePath(e=null,n=null){if(!e)return null;if(isEmbeddingsEngine(e))return e.key?`${__ENV__.API}/embeddings/engines/${encodeURIComponent(e.key)}`:null;const t=resolveEngineEnvName(e,n);return t&&e.type&&e.name?`${__ENV__.API}/env/${encodeURIComponent(t)}/engines/${encodeURIComponent(e.type)}/${encodeURIComponent(e.name)}`:null}function compactCount(e){if(null==e||""===e)return null;const n=Number(e);return Number.isFinite(n)&&Number.isInteger(n)?n.toLocaleString():String(e)}function firstDefined(...e){for(const n of e)if(null!=n&&""!==n)return n;return null}function pushBadge(e,n,t="var(--okdb-app-fg-muted)",r=null){null!=n&&""!==n&&e.push({text:String(n),color:t,title:r??String(n)})}function pushMetric(e,n,t,r={}){null!=n&&""!==n&&null!=t&&""!==t&&"—"!==t&&e.push({label:String(n),value:String(t),tone:r.tone??"neutral",title:r.title??`${n}: ${t}`})}function buildStatusMeta(e,n,t=null){return{label:e,color:n,title:t??`Status: ${e}`}}function formatClockBadge(e,n){const t=compactCount(e),r=compactCount(n);return null==t&&null==r?null:`clock ${t??"—"} / ${r??"—"}`}function inferLag(e,n,t){if(null!=e&&""!==e)return e;const r=Number(n),o=Number(t);return Number.isFinite(r)&&Number.isFinite(o)?Math.max(0,o-r):null}function formatBool(e,n="yes",t="no"){return null==e||""===e?null:e?n:t}function formatChunkConfig(e=null){if(!e||"object"!=typeof e)return null;const n=[e.strategy??e.type??"chunked"];return null!=e.size&&n.push(`${compactCount(e.size)} chars`),null!=e.overlap&&n.push(`${compactCount(e.overlap)} overlap`),null!=e.storeText&&n.push(`store text ${formatBool(e.storeText,"on","off")}`),n.filter(Boolean).join(" / ")}function formatIndexerProgress(e=null){if(!e||"object"!=typeof e)return null;const n=Number(e.done??0),t=Number(e.pending??0),r=Number(e.failed??0),o=Number(e.total??n+t+r+Number(e.deleted??0));if(!Number.isFinite(o)||o<=0)return null;const u=Math.round(Math.max(0,n)/o*100);return{value:`${compactCount(n)}/${compactCount(o)} (${u}%)`,tone:r>0?"danger":t>0?"warning":"success"}}function formatClockProgress(e,n){const t=Number(e),r=Number(n);if(!Number.isFinite(t)||!Number.isFinite(r)||r<=0)return null;const o=Math.max(0,Math.min(t,r)),u=Math.round(o/r*100);return{value:`${compactCount(o)}/${compactCount(r)} (${u}%)`,tone:o>=r?"success":"warning"}}export function compactEngineErrorText(e=null,n=160){const t=firstDefined(e?.stats?.error,e?.runtime?.error,e?.error);if(null==t)return null;const r=String(t).replace(/\s+/g," ").trim();return r?r.length>n?`${r.slice(0,Math.max(0,n-1)).trimEnd()}…`:r:null}export function compactEngineStatusMeta(e=null){const n=e?.stats??{},t=e?.runtime??{},r=firstDefined(n?.state,t?.state,e?.state,e?.status);return!0===firstDefined(n?.paused,t?.paused,e?.paused)?buildStatusMeta("paused","var(--okdb-warning)"):"error"===r||"error"===e?.status?buildStatusMeta("error","var(--okdb-danger)"):"inactive"===r||"inactive"===e?.status?buildStatusMeta("inactive","var(--okdb-warning)"):"building"===r?buildStatusMeta("building","var(--okdb-info)"):e?.isRunning||"online"===r||"running"===r?buildStatusMeta("running","var(--okdb-success)"):"degraded"===r?buildStatusMeta("degraded","var(--okdb-warning)"):buildStatusMeta("stopped"===r||"idle"===r?"stopped":r?String(r):"stopped","var(--okdb-app-fg-muted)")}export function compactEngineRowStatusBadges(e=null){const n=e?.stats??{},t=[],r=compactEngineStatusMeta(e),o=readableReason(e?.reason);return pushBadge(t,r.label,r.color,r.title),"embedder"===e?.type&&(!0===n?.ok&&pushBadge(t,"healthy","var(--okdb-success)","Provider health check is healthy"),!1===n?.ok&&pushBadge(t,"unhealthy","var(--okdb-danger)","Provider health check failed")),!1===e?.enabled&&pushBadge(t,"disabled","var(--okdb-app-fg-muted)","Engine is disabled"),o&&o!==r.label&&pushBadge(t,o,"var(--okdb-app-fg-muted)",`Reason: ${o}`),t}export function compactEngineRowSummary(e=null){const n=e?.config??{},t=e?.stats??{},r=t?.indexer??t??{},o=t?.search??t??{},u=t?.handler?.name??n?.handler?.name??null;switch(e?.type){case"queue-worker":return`Consumes ${t?.job_type??n?.job_type??"queue jobs"}`;case"processor":return`Watches ${t?.source_type??n?.source_type??"source changes"}`;case"materializer":return`${t?.source_type??n?.source_type??"source"} → ${t?.target_type??n?.target_type??"target"}`;case"embedder":return`${t?.model??n?.model??e?.name??"Embedder"} on ${t?.type??n?.type??n?.provider??"provider"}`;case"indexer":return`Indexes ${r?.source_type??n?.source_type??"source docs"}`;case"vector-search":return`Searches ${o?.storage_key??n?.storage_key??e?.name??"vector index"}`;case"embed-worker":return"Runs queued embedding jobs"+(t?.indexer??n?.indexer?` for ${t?.indexer??n.indexer}`:"");default:return u?`Uses ƒ ${u}`:e?.name??e?.key??"Engine"}}export function compactEngineRowMetrics(e=null){const n=e?.config??{},t=e?.stats??{},r=t?.indexer??t??{},o=t?.search??t??{},u=[],c=firstDefined(t?.lastClock,e?.runtime?.lastClock,e?.lastClock,r?.last_seen_clock,o?.last_seen_clock),i=firstDefined(t?.headClock,e?.runtime?.headClock,e?.headClock,r?.head_clock,o?.head_clock),s=inferLag(firstDefined(t?.lag,e?.runtime?.lag,e?.lag,r?.lag,o?.lag),c,i),a=formatClockBadge(c,i);switch(e?.type){case"queue-worker":pushMetric(u,"Done",compactCount(t?.processed??0),{tone:"success"}),pushMetric(u,"Error",compactCount(t?.failed??0),{tone:(t?.failed??0)>0?"danger":"neutral"}),pushMetric(u,"Queue",t?.queue_env??n?.queue_env??e?.env??"default"),pushMetric(u,"Handler",t?.handler?.name??n?.handler?.name,{title:"Queue worker handler"});break;case"processor":null!=t?.processedChanges&&pushMetric(u,"Changes",compactCount(t.processedChanges)),null!=t?.failedBatches&&pushMetric(u,"Error",compactCount(t.failedBatches),{tone:(t?.failedBatches??0)>0?"danger":"neutral",title:"Failed batches"}),null!=t?.runningBatches&&pushMetric(u,"Active",compactCount(t.runningBatches),{tone:(t?.runningBatches??0)>0?"accent":"neutral",title:"Running batches"}),null!=s&&pushMetric(u,"Lag",0===s?"caught up":compactCount(s),{tone:Number(s)>0?"warning":"success"}),a&&pushMetric(u,"Clock",a.replace(/^clock\s+/i,"")),pushMetric(u,"Mode",t?.mode??n?.mode??"async");break;case"materializer":null!=t?.targetCount&&pushMetric(u,"Rows",compactCount(t.targetCount),{tone:"success"}),null!=t?.appliedOps&&pushMetric(u,"Ops",compactCount(t.appliedOps)),null!=t?.failedBatches&&pushMetric(u,"Error",compactCount(t.failedBatches),{tone:(t?.failedBatches??0)>0?"danger":"neutral",title:"Failed batches"}),null!=t?.runningBatches&&pushMetric(u,"Active",compactCount(t.runningBatches),{tone:(t?.runningBatches??0)>0?"accent":"neutral",title:"Running batches"}),null!=s&&pushMetric(u,"Lag",0===s?"caught up":compactCount(s),{tone:Number(s)>0?"warning":"success"}),a&&pushMetric(u,"Clock",a.replace(/^clock\s+/i,""));break;case"embedder":pushMetric(u,"Provider",t?.type??n?.type??n?.provider),pushMetric(u,"Model",t?.model??n?.model),null!=t?.latency&&pushMetric(u,"Latency",`${compactCount(t.latency)} ms`,{tone:"accent"});break;case"indexer":if(r.doc_counts&&pushMetric(u,"Done",compactCount(r.doc_counts.done??0),{tone:"success"}),r.doc_counts){const e=formatIndexerProgress(r.doc_counts);e&&pushMetric(u,"Progress",e.value,{tone:e.tone,title:"Indexed documents / total tracked documents"})}else{const e=formatClockProgress(c,i);e&&pushMetric(u,"Progress",e.value,{tone:e.tone,title:"Indexer cursor / source head clock"})}r.doc_counts&&pushMetric(u,"Pending",compactCount(r.doc_counts.pending??0),{tone:(r.doc_counts.pending??0)>0?"warning":"neutral"}),r.doc_counts&&pushMetric(u,"Error",compactCount(r.doc_counts.failed??0),{tone:(r.doc_counts.failed??0)>0?"danger":"neutral"}),null!=r?.vector_count&&pushMetric(u,"Vectors",compactCount(r.vector_count),{tone:"success"}),a&&pushMetric(u,"Clock",a.replace(/^clock\s+/i,"")),null!=s&&pushMetric(u,"Lag",0===s?"caught up":compactCount(s),{tone:Number(s)>0?"warning":"success"}),pushMetric(u,"Field",r.field??n?.field??"whole record"),pushMetric(u,"Mode",r.mode??n?.mode),pushMetric(u,"Prepare",r.prepare??n?.prepare),pushMetric(u,"Chunk",formatChunkConfig(r.chunk??n?.chunk)),pushMetric(u,"Embedder",r.embedder??n?.embedder);break;case"vector-search":null==o.count&&null==o.vector_count||pushMetric(u,"Vectors",compactCount(o.count??o.vector_count),{tone:"success"}),null!=o.dims&&pushMetric(u,"Dims",compactCount(o.dims)),pushMetric(u,"Algorithm",o.algorithm??n?.algorithm);break;case"embed-worker":null!=t?.processed&&pushMetric(u,"Done",compactCount(t.processed),{tone:"success"}),null!=t?.failed&&pushMetric(u,"Error",compactCount(t.failed),{tone:(t?.failed??0)>0?"danger":"neutral"}),null!=t?.in_flight&&pushMetric(u,"Active",compactCount(t.in_flight),{tone:(t?.in_flight??0)>0?"accent":"neutral"}),null!=t?.concurrency&&pushMetric(u,"Workers",compactCount(t.concurrency),{tone:"accent"}),pushMetric(u,"Indexer",t?.indexer??n?.indexer);break;default:null!=s&&pushMetric(u,"Lag",0===s?"caught up":compactCount(s),{tone:Number(s)>0?"warning":"success"}),pushMetric(u,"Type",n?.source_type)}return Array.isArray(e?.affinity)&&e.affinity.length&&pushMetric(u,"Affinity",e.affinity.join(", ")),u}export function compactEngineRowPreview(e=null){const n=compactEngineRowMetrics(e),t=new Map(n.map(e=>[e.label,e])),r=[],o=e=>{const n=t.get(e);n&&(r.push(n),t.delete(e))};switch(e?.type){case"indexer":["Progress","Lag","Pending","Error","Vectors","Chunk"].forEach(o);break;case"embed-worker":case"queue-worker":["Done","Error","Active","Workers"].forEach(o);break;case"processor":["Changes","Error","Active","Lag"].forEach(o);break;case"materializer":["Rows","Ops","Error","Active","Lag"].forEach(o);break;case"vector-search":["Vectors","Dims","Algorithm"].forEach(o);break;case"embedder":["Model","Provider","Latency"].forEach(o);break;default:["Error","Lag","Type"].forEach(o)}return r.slice(0,3)}export function compactEngineConfigMetrics(e=null){const n=e?.config??{},t=e?.stats??{},r=e?.runtime??{},o=[],u=n?.handler??r?.handler??t?.handler??null;switch(e?.type){case"processor":pushMetric(o,"Source",n?.source_type??t?.source_type),pushMetric(o,"Source env",n?.source_env??r?.source_env),pushMetric(o,"Mode",n?.mode??t?.mode??"async"),pushMetric(o,"Bootstrap",n?.bootstrap),pushMetric(o,"Batch",compactCount(n?.batchSize)),pushMetric(o,"Origin",n?.originMode);break;case"materializer":pushMetric(o,"Source",n?.source_type??t?.source_type),pushMetric(o,"Target",n?.target_type??t?.target_type),pushMetric(o,"Source env",n?.source_env??r?.source_env),pushMetric(o,"Target env",n?.target_env??r?.target_env),pushMetric(o,"Bootstrap",n?.bootstrap),pushMetric(o,"Batch",compactCount(n?.batchSize)),pushMetric(o,"Origin",n?.originMode),pushMetric(o,"Clear target",formatBool(n?.clearTargetOnRebuild,"on","off"));break;case"queue-worker":pushMetric(o,"Job type",n?.job_type??t?.job_type),pushMetric(o,"Queue env",n?.queue_env??t?.queue_env),pushMetric(o,"Concurrency",compactCount(n?.concurrency??t?.concurrency));break;case"embedder":pushMetric(o,"Provider",n?.type??n?.provider??t?.type),pushMetric(o,"Model",n?.model??t?.model),pushMetric(o,"Dims",compactCount(n?.dims??t?.dims));break;case"indexer":pushMetric(o,"Source",n?.source_type??t?.source_type),pushMetric(o,"Source env",n?.source_env??r?.source_env),pushMetric(o,"Field",n?.field??t?.field??"whole record"),pushMetric(o,"Mode",n?.mode??t?.mode),pushMetric(o,"Storage",n?.storage_key??t?.storage_key),pushMetric(o,"Dims",compactCount(n?.dims??t?.dims)),pushMetric(o,"Prepare",n?.prepare??t?.prepare),pushMetric(o,"Chunk",formatChunkConfig(n?.chunk??t?.chunk)),pushMetric(o,"Embedder",n?.embedder??t?.embedder);break;case"vector-search":if(pushMetric(o,"Source",n?.source_type??t?.source_type),pushMetric(o,"Source env",n?.source_env??r?.source_env),pushMetric(o,"Storage",n?.storage_key??t?.storage_key),pushMetric(o,"Dims",compactCount(n?.dims??t?.dims)),pushMetric(o,"Algorithm",n?.algorithm??t?.algorithm??"flat"),n?.algorithm_config&&"object"==typeof n.algorithm_config)for(const[e,t]of Object.entries(n.algorithm_config))pushMetric(o,e,compactCount(t));pushMetric(o,"Embedder",n?.embedder??t?.embedder);break;case"embed-worker":pushMetric(o,"Job type",n?.job_type??t?.job_type),pushMetric(o,"Indexer",n?.indexer??t?.indexer),pushMetric(o,"Embedder",n?.embedder??t?.embedder),pushMetric(o,"Concurrency",compactCount(n?.concurrency??t?.concurrency)),pushMetric(o,"Poll",null!=n?.pollInterval?`${compactCount(n.pollInterval)} ms`:null),pushMetric(o,"TTL",null!=n?.ttl?`${compactCount(n.ttl)} ms`:null);break;default:pushMetric(o,"Source",n?.source_type??t?.source_type),pushMetric(o,"Target",n?.target_type??t?.target_type),pushMetric(o,"Mode",n?.mode??t?.mode)}return u?.name&&pushMetric(o,"Handler",u.name,{title:"Configured handler function"}),u?.recordRuns&&pushMetric(o,"Run log",u.recordRuns,{title:"Function run recording policy"}),o}
|
|
1
|
+
export const EMBEDDINGS_ENGINE_TYPES=new Set(["embedder","indexer","embed-worker","vector-search"]);export function isEmbeddingsEngineType(e){return EMBEDDINGS_ENGINE_TYPES.has(e)}export function isEmbeddingsEngine(e){return isEmbeddingsEngineType(e?.type)}export function cloneValue(e,n={}){try{return JSON.parse(JSON.stringify(e??n))}catch{return JSON.parse(JSON.stringify(n))}}export function statusTone(e=null){return e?.isRunning?"var(--okdb-success)":"inactive"===e?.status?"var(--okdb-warning)":"error"===e?.status?"var(--okdb-danger)":"var(--okdb-app-fg-muted)"}export function readableReason(e){return e?String(e).replace(/_/g," "):null}export function formatJsonValue(e){try{return JSON.stringify(e,null,2)}catch{return String(e??"")}}export function resolveEngineEnvName(e=null,n=null){return n??e?.env??e?._uiEnv??e?.storeEnv??e?.config?.source_env??null}export function getEnginePipelineName(e=null){return e?.meta?.pipeline??null}export function buildFunctionsRoute(e,n=null){const t=new URLSearchParams,r=e?.$ok?.stores?.okdb?.selectedEnv??null;r&&t.set("env",r),n&&t.set("name",n);const o=t.toString();return"/functions"+(o?`?${o}`:"")}export function openFunctionRoute(e,n,{beforeNavigate:t}={}){if(!n)return!1;const r=e?.$vars?.$router;if("function"==typeof t&&t(),r?.push){const t={...r._search||{}},o=e?.$ok?.stores?.okdb?.selectedEnv??null;o?t.env=o:delete t.env,t.name=n;const c="function"==typeof r.resolve?r.resolve("/functions",t):buildFunctionsRoute(e,n);return r.push(c),!0}return!!globalThis.location&&(globalThis.location.hash=`#${buildFunctionsRoute(e,n)}`,!0)}export function dispatchEnginePipelineSelection(e,n=null){const t=getEnginePipelineName(n);return!!t&&(e.$dispatch("select-pipeline",t),!0)}function matchesLinkedEngine(e=null,n=null){return!!n&&(e?.name===n||e?.key===n)}function matchesLinkedFunction(e=null,n=null){return!!n&&(e?.config?.handler?.name===n||e?.runtime?.handler?.name===n)}function linkedMemberSourceType(e=null){return e?.config?.source_type??e?.runtime?.source_type??null}function linkedMemberJobType(e=null){return e?.config?.job_type??e?.runtime?.job_type??null}function linkedMemberQueueEnv(e=null){return e?.config?.queue_env??e?.runtime?.queue_env??null}export async function resolveLinkedPipeline(e,{envName:n=null,pipelineName:t=null,ownerEngine:r=null,functionName:o=null}={}){const c=n??e?.$ok?.stores?.okdb?.selectedEnv??null;if(!c||!t&&!r&&!o)return null;let u=[],i=null,s=null,a=null,l="pipelines";try{const e=await fetch(`${__ENV__.API}/env/${encodeURIComponent(c)}/pipelines`);if(e.ok){const n=await e.json().catch(()=>({}));u=n?.data??n?.result??[],i=u.find(e=>!!e&&(!(!t||e.name!==t)||(e.engines??[]).some(e=>matchesLinkedEngine(e,r)||matchesLinkedFunction(e,o))))??null,s=(i?.engines??[]).find(e=>matchesLinkedEngine(e,r))??(i?.engines??[]).find(e=>matchesLinkedFunction(e,o))??(i?.engines??[]).find(e=>linkedMemberSourceType(e)&&["processor","materializer","queue-worker"].includes(e?.type))??(i?.engines??[]).find(e=>linkedMemberSourceType(e))??(i?.engines??[]).find(e=>e?.config?.job_type)??null,["processor","materializer","queue-worker"].includes(s?.type)&&(l="engines")}}catch{}if(!s&&(r||o))try{const e=await fetch(`${__ENV__.API}/env/${encodeURIComponent(c)}/engines`);if(e.ok){const n=await e.json().catch(()=>({})),t=n?.data??n?.result??[];a=t.find(e=>e?.name===r||e?.key===r)??t.find(e=>e?.config?.handler?.name===o)??null,!i&&a&&(i=u.find(e=>(e?.engines??[]).some(e=>matchesLinkedEngine(e,a?.name)||matchesLinkedEngine(e,a?.key)||matchesLinkedFunction(e,o)))??null),!s&&i&&(s=(i?.engines??[]).find(e=>matchesLinkedEngine(e,a?.name))??(i?.engines??[]).find(e=>matchesLinkedEngine(e,a?.key))??(i?.engines??[]).find(e=>matchesLinkedFunction(e,o))??null),["processor","materializer","queue-worker"].includes((s??a)?.type)&&(l="engines")}}catch{}const p=linkedMemberSourceType(s)??a?.config?.source_type??a?.runtime?.source_type??(i?.engines??[]).map(linkedMemberSourceType).find(Boolean)??null,d=linkedMemberJobType(s)??a?.config?.job_type??a?.runtime?.job_type??(i?.engines??[]).map(linkedMemberJobType).find(Boolean)??null,m=linkedMemberQueueEnv(s)??a?.config?.queue_env??a?.runtime?.queue_env??c;return"queue-worker"===(s??a)?.type||d?{envName:m,pipelineName:t??i?.name??null,ownerEngine:r??s?.name??s?.key??a?.name??a?.key??null,sourceType:null,jobType:d,targetTab:"workers",targetSection:"queue"}:p?{envName:c,pipelineName:t??i?.name??null,ownerEngine:r??s?.name??s?.key??a?.name??a?.key??null,sourceType:p,jobType:null,targetTab:l,targetSection:"db"}:null}export async function openLinkedPipelineRoute(e,{envName:n=null,pipelineName:t=null,ownerEngine:r=null,functionName:o=null,beforeNavigate:c}={}){const u=e?.$vars?.$router,i=await resolveLinkedPipeline(e,{envName:n,pipelineName:t,ownerEngine:r,functionName:o});if(!i)return!1;if("function"==typeof c&&c(),e?.$ok?.stores?.okdb?.selectedEnv&&e.$ok.stores.okdb.selectedEnv!==i.envName&&e.$ok.stores.okdb.selectEnv(i.envName),u?.push&&"function"==typeof u.resolve){if("queue"===i.targetSection){const e={env:i.envName,view:i.targetTab??"workers"};return i.jobType&&(e.type=i.jobType),u.push(u.resolve("/queue",e)),!0}if(i.pipelineName){const e={env:i.envName,pipeline:i.pipelineName};return i.sourceType&&(e.sourceType=i.sourceType),u.push(u.resolve("/engines",e)),!0}return!!i.sourceType&&(u.push(u.resolve("/db",{env:i.envName,type:i.sourceType,tab:i.targetTab})),!0)}if(globalThis.location){if("queue"===i.targetSection){const e=new URLSearchParams({env:i.envName,view:i.targetTab??"workers"});return i.jobType&&e.set("type",i.jobType),globalThis.location.hash=`#/queue?${e.toString()}`,!0}if(i.pipelineName){const e=new URLSearchParams({env:i.envName,pipeline:i.pipelineName});return i.sourceType&&e.set("sourceType",i.sourceType),globalThis.location.hash=`#/engines?${e.toString()}`,!0}if(!i.sourceType)return!1;const e=new URLSearchParams({env:i.envName,type:i.sourceType,tab:i.targetTab});return globalThis.location.hash=`#/db?${e.toString()}`,!0}return!1}export function buildEmbeddingsPipelineUrl(e=null,n=null){const t=e?.config??{},r=encodeURIComponent(t.source_env??"default"),o=encodeURIComponent(t.source_type??""),c=encodeURIComponent(t.storage_key??n??e?.name??"");return`${__ENV__.API}/env/${r}/type/${o}/pipelines/${c}`}export function maskSensitiveConfig(e={}){if(Array.isArray(e))return e.map(e=>maskSensitiveConfig(e));if(!e||"object"!=typeof e)return e;const n={};for(const[t,r]of Object.entries(e))n[t]=/key|secret|pass|token/i.test(t)?"***":maskSensitiveConfig(r);return n}export function summarizeValidationDetails(e=[]){if(!Array.isArray(e)||0===e.length)return null;const n=e.slice(0,3).map((e={})=>`${e.field?`${e.field} `:""}${e.message||"is invalid"}`.trim()).join("; ");return e.length>3?`${n} (+${e.length-3} more)`:n}export function getApiErrorMessage(e=null,n="Request failed"){const t=e?.error??null,r=summarizeValidationDetails(t?.details??e?.details??null);return"string"==typeof t?e?.message?e.message:r&&/^[A-Z0-9_]+$/.test(t)?`Validation failed: ${r}`:t||n:t?.message?r&&"VALIDATION_ERROR"===t.code&&!t.message.includes(r)?`${t.message} — ${r}`:t.message:e?.message?r&&!e.message.includes(r)?`${e.message} — ${r}`:e.message:r?`Validation failed: ${r}`:n}export function buildEngineBasePath(e=null,n=null){if(!e)return null;if(isEmbeddingsEngine(e))return e.key?`${__ENV__.API}/embeddings/engines/${encodeURIComponent(e.key)}`:null;const t=resolveEngineEnvName(e,n);return t&&e.type&&e.name?`${__ENV__.API}/env/${encodeURIComponent(t)}/engines/${encodeURIComponent(e.type)}/${encodeURIComponent(e.name)}`:null}function compactCount(e){if(null==e||""===e)return null;const n=Number(e);return Number.isFinite(n)&&Number.isInteger(n)?n.toLocaleString():String(e)}function firstDefined(...e){for(const n of e)if(null!=n&&""!==n)return n;return null}function pushBadge(e,n,t="var(--okdb-app-fg-muted)",r=null){null!=n&&""!==n&&e.push({text:String(n),color:t,title:r??String(n)})}function pushMetric(e,n,t,r={}){null!=n&&""!==n&&null!=t&&""!==t&&"—"!==t&&e.push({label:String(n),value:String(t),tone:r.tone??"neutral",title:r.title??`${n}: ${t}`})}function buildStatusMeta(e,n,t=null){return{label:e,color:n,title:t??`Status: ${e}`}}function formatClockBadge(e,n){const t=compactCount(e),r=compactCount(n);return null==t&&null==r?null:`clock ${t??"—"} / ${r??"—"}`}function inferLag(e,n,t){if(null!=e&&""!==e)return e;const r=Number(n),o=Number(t);return Number.isFinite(r)&&Number.isFinite(o)?Math.max(0,o-r):null}function formatBool(e,n="yes",t="no"){return null==e||""===e?null:e?n:t}function formatChunkConfig(e=null){if(!e||"object"!=typeof e)return null;const n=[e.strategy??e.type??"chunked"];return null!=e.size&&n.push(`${compactCount(e.size)} chars`),null!=e.overlap&&n.push(`${compactCount(e.overlap)} overlap`),null!=e.storeText&&n.push(`store text ${formatBool(e.storeText,"on","off")}`),n.filter(Boolean).join(" / ")}function formatIndexerProgress(e=null){if(!e||"object"!=typeof e)return null;const n=Number(e.done??0),t=Number(e.pending??0),r=Number(e.failed??0),o=Number(e.total??n+t+r+Number(e.deleted??0));if(!Number.isFinite(o)||o<=0)return null;const c=Math.round(Math.max(0,n)/o*100);return{value:`${compactCount(n)}/${compactCount(o)} (${c}%)`,tone:r>0?"danger":t>0?"warning":"success"}}function formatClockProgress(e,n){const t=Number(e),r=Number(n);if(!Number.isFinite(t)||!Number.isFinite(r)||r<=0)return null;const o=Math.max(0,Math.min(t,r)),c=Math.round(o/r*100);return{value:`${compactCount(o)}/${compactCount(r)} (${c}%)`,tone:o>=r?"success":"warning"}}export function compactEngineErrorText(e=null,n=160){const t=firstDefined(e?.stats?.error,e?.runtime?.error,e?.error);if(null==t)return null;const r=String(t).replace(/\s+/g," ").trim();return r?r.length>n?`${r.slice(0,Math.max(0,n-1)).trimEnd()}…`:r:null}export function compactEngineStatusMeta(e=null){const n=e?.stats??{},t=e?.runtime??{},r=firstDefined(n?.state,t?.state,e?.state,e?.status);return!0===firstDefined(n?.paused,t?.paused,e?.paused)?buildStatusMeta("paused","var(--okdb-warning)"):"error"===r||"error"===e?.status?buildStatusMeta("error","var(--okdb-danger)"):"inactive"===r||"inactive"===e?.status?buildStatusMeta("inactive","var(--okdb-warning)"):"building"===r?buildStatusMeta("building","var(--okdb-info)"):e?.isRunning||"online"===r||"running"===r?buildStatusMeta("running","var(--okdb-success)"):"degraded"===r?buildStatusMeta("degraded","var(--okdb-warning)"):buildStatusMeta("stopped"===r||"idle"===r?"stopped":r?String(r):"stopped","var(--okdb-app-fg-muted)")}export function compactEngineRowStatusBadges(e=null){const n=e?.stats??{},t=[],r=compactEngineStatusMeta(e),o=readableReason(e?.reason);return pushBadge(t,r.label,r.color,r.title),"embedder"===e?.type&&(!0===n?.ok&&pushBadge(t,"healthy","var(--okdb-success)","Provider health check is healthy"),!1===n?.ok&&pushBadge(t,"unhealthy","var(--okdb-danger)","Provider health check failed")),!1===e?.enabled&&pushBadge(t,"disabled","var(--okdb-app-fg-muted)","Engine is disabled"),o&&o!==r.label&&pushBadge(t,o,"var(--okdb-app-fg-muted)",`Reason: ${o}`),t}export function compactEngineRowSummary(e=null){const n=e?.config??{},t=e?.stats??{},r=t?.indexer??t??{},o=t?.search??t??{},c=t?.handler?.name??n?.handler?.name??null;switch(e?.type){case"queue-worker":return`Consumes ${t?.job_type??n?.job_type??"queue jobs"}`;case"processor":return`Watches ${t?.source_type??n?.source_type??"source changes"}`;case"materializer":return`${t?.source_type??n?.source_type??"source"} → ${t?.target_type??n?.target_type??"target"}`;case"embedder":return`${t?.model??n?.model??e?.name??"Embedder"} on ${t?.type??n?.type??n?.provider??"provider"}`;case"indexer":return`Indexes ${r?.source_type??n?.source_type??"source docs"}`;case"vector-search":return`Searches ${o?.storage_key??n?.storage_key??e?.name??"vector index"}`;case"embed-worker":return"Runs queued embedding jobs"+(t?.indexer??n?.indexer?` for ${t?.indexer??n.indexer}`:"");default:return c?`Uses ƒ ${c}`:e?.name??e?.key??"Engine"}}export function compactEngineRowMetrics(e=null){const n=e?.config??{},t=e?.stats??{},r=t?.indexer??t??{},o=t?.search??t??{},c=[],u=firstDefined(t?.lastClock,e?.runtime?.lastClock,e?.lastClock,r?.last_seen_clock,o?.last_seen_clock),i=firstDefined(t?.headClock,e?.runtime?.headClock,e?.headClock,r?.head_clock,o?.head_clock),s=inferLag(firstDefined(t?.lag,e?.runtime?.lag,e?.lag,r?.lag,o?.lag),u,i),a=formatClockBadge(u,i);switch(e?.type){case"queue-worker":pushMetric(c,"Done",compactCount(t?.processed??0),{tone:"success"}),pushMetric(c,"Error",compactCount(t?.failed??0),{tone:(t?.failed??0)>0?"danger":"neutral"}),pushMetric(c,"Queue",t?.queue_env??n?.queue_env??e?.env??"default"),pushMetric(c,"Handler",t?.handler?.name??n?.handler?.name,{title:"Queue worker handler"});break;case"processor":null!=t?.processedChanges&&pushMetric(c,"Changes",compactCount(t.processedChanges)),null!=t?.failedBatches&&pushMetric(c,"Error",compactCount(t.failedBatches),{tone:(t?.failedBatches??0)>0?"danger":"neutral",title:"Failed batches"}),null!=t?.runningBatches&&pushMetric(c,"Active",compactCount(t.runningBatches),{tone:(t?.runningBatches??0)>0?"accent":"neutral",title:"Running batches"}),null!=s&&pushMetric(c,"Lag",0===s?"caught up":compactCount(s),{tone:Number(s)>0?"warning":"success"}),a&&pushMetric(c,"Clock",a.replace(/^clock\s+/i,"")),pushMetric(c,"Mode",t?.mode??n?.mode??"async");break;case"materializer":null!=t?.targetCount&&pushMetric(c,"Rows",compactCount(t.targetCount),{tone:"success"}),null!=t?.appliedOps&&pushMetric(c,"Ops",compactCount(t.appliedOps)),null!=t?.failedBatches&&pushMetric(c,"Error",compactCount(t.failedBatches),{tone:(t?.failedBatches??0)>0?"danger":"neutral",title:"Failed batches"}),null!=t?.runningBatches&&pushMetric(c,"Active",compactCount(t.runningBatches),{tone:(t?.runningBatches??0)>0?"accent":"neutral",title:"Running batches"}),null!=s&&pushMetric(c,"Lag",0===s?"caught up":compactCount(s),{tone:Number(s)>0?"warning":"success"}),a&&pushMetric(c,"Clock",a.replace(/^clock\s+/i,""));break;case"embedder":pushMetric(c,"Provider",t?.type??n?.type??n?.provider),pushMetric(c,"Model",t?.model??n?.model),null!=t?.latency&&pushMetric(c,"Latency",`${compactCount(t.latency)} ms`,{tone:"accent"});break;case"indexer":if(r.doc_counts&&pushMetric(c,"Done",compactCount(r.doc_counts.done??0),{tone:"success"}),r.doc_counts){const e=formatIndexerProgress(r.doc_counts);e&&pushMetric(c,"Progress",e.value,{tone:e.tone,title:"Indexed documents / total tracked documents"})}else{const e=formatClockProgress(u,i);e&&pushMetric(c,"Progress",e.value,{tone:e.tone,title:"Indexer cursor / source head clock"})}r.doc_counts&&pushMetric(c,"Pending",compactCount(r.doc_counts.pending??0),{tone:(r.doc_counts.pending??0)>0?"warning":"neutral"}),r.doc_counts&&pushMetric(c,"Error",compactCount(r.doc_counts.failed??0),{tone:(r.doc_counts.failed??0)>0?"danger":"neutral"}),null!=r?.vector_count&&pushMetric(c,"Vectors",compactCount(r.vector_count),{tone:"success"}),a&&pushMetric(c,"Clock",a.replace(/^clock\s+/i,"")),null!=s&&pushMetric(c,"Lag",0===s?"caught up":compactCount(s),{tone:Number(s)>0?"warning":"success"}),pushMetric(c,"Field",r.field??n?.field??"whole record"),pushMetric(c,"Mode",r.mode??n?.mode),pushMetric(c,"Prepare",r.prepare??n?.prepare),pushMetric(c,"Chunk",formatChunkConfig(r.chunk??n?.chunk)),pushMetric(c,"Embedder",r.embedder??n?.embedder);break;case"vector-search":null==o.count&&null==o.vector_count||pushMetric(c,"Vectors",compactCount(o.count??o.vector_count),{tone:"success"}),null!=o.dims&&pushMetric(c,"Dims",compactCount(o.dims)),pushMetric(c,"Algorithm",o.algorithm??n?.algorithm);break;case"embed-worker":null!=t?.processed&&pushMetric(c,"Done",compactCount(t.processed),{tone:"success"}),null!=t?.failed&&pushMetric(c,"Error",compactCount(t.failed),{tone:(t?.failed??0)>0?"danger":"neutral"}),null!=t?.in_flight&&pushMetric(c,"Active",compactCount(t.in_flight),{tone:(t?.in_flight??0)>0?"accent":"neutral"}),null!=t?.concurrency&&pushMetric(c,"Workers",compactCount(t.concurrency),{tone:"accent"}),pushMetric(c,"Indexer",t?.indexer??n?.indexer);break;default:null!=s&&pushMetric(c,"Lag",0===s?"caught up":compactCount(s),{tone:Number(s)>0?"warning":"success"}),pushMetric(c,"Type",n?.source_type)}return c}export function compactEngineRowPreview(e=null){const n=compactEngineRowMetrics(e),t=new Map(n.map(e=>[e.label,e])),r=[],o=e=>{const n=t.get(e);n&&(r.push(n),t.delete(e))};switch(e?.type){case"indexer":["Progress","Lag","Pending","Error","Vectors","Chunk"].forEach(o);break;case"embed-worker":case"queue-worker":["Done","Error","Active","Workers"].forEach(o);break;case"processor":["Changes","Error","Active","Lag"].forEach(o);break;case"materializer":["Rows","Ops","Error","Active","Lag"].forEach(o);break;case"vector-search":["Vectors","Dims","Algorithm"].forEach(o);break;case"embedder":["Model","Provider","Latency"].forEach(o);break;default:["Error","Lag","Type"].forEach(o)}return r.slice(0,3)}export function compactEngineConfigMetrics(e=null){const n=e?.config??{},t=e?.stats??{},r=e?.runtime??{},o=[],c=n?.handler??r?.handler??t?.handler??null;switch(e?.type){case"processor":pushMetric(o,"Source",n?.source_type??t?.source_type),pushMetric(o,"Source env",n?.source_env??r?.source_env),pushMetric(o,"Mode",n?.mode??t?.mode??"async"),pushMetric(o,"Bootstrap",n?.bootstrap),pushMetric(o,"Batch",compactCount(n?.batchSize)),pushMetric(o,"Origin",n?.originMode);break;case"materializer":pushMetric(o,"Source",n?.source_type??t?.source_type),pushMetric(o,"Target",n?.target_type??t?.target_type),pushMetric(o,"Source env",n?.source_env??r?.source_env),pushMetric(o,"Target env",n?.target_env??r?.target_env),pushMetric(o,"Bootstrap",n?.bootstrap),pushMetric(o,"Batch",compactCount(n?.batchSize)),pushMetric(o,"Origin",n?.originMode),pushMetric(o,"Clear target",formatBool(n?.clearTargetOnRebuild,"on","off"));break;case"queue-worker":pushMetric(o,"Job type",n?.job_type??t?.job_type),pushMetric(o,"Queue env",n?.queue_env??t?.queue_env),pushMetric(o,"Concurrency",compactCount(n?.concurrency??t?.concurrency));break;case"embedder":pushMetric(o,"Provider",n?.type??n?.provider??t?.type),pushMetric(o,"Model",n?.model??t?.model),pushMetric(o,"Dims",compactCount(n?.dims??t?.dims));break;case"indexer":pushMetric(o,"Source",n?.source_type??t?.source_type),pushMetric(o,"Source env",n?.source_env??r?.source_env),pushMetric(o,"Field",n?.field??t?.field??"whole record"),pushMetric(o,"Mode",n?.mode??t?.mode),pushMetric(o,"Storage",n?.storage_key??t?.storage_key),pushMetric(o,"Dims",compactCount(n?.dims??t?.dims)),pushMetric(o,"Prepare",n?.prepare??t?.prepare),pushMetric(o,"Chunk",formatChunkConfig(n?.chunk??t?.chunk)),pushMetric(o,"Embedder",n?.embedder??t?.embedder);break;case"vector-search":if(pushMetric(o,"Source",n?.source_type??t?.source_type),pushMetric(o,"Source env",n?.source_env??r?.source_env),pushMetric(o,"Storage",n?.storage_key??t?.storage_key),pushMetric(o,"Dims",compactCount(n?.dims??t?.dims)),pushMetric(o,"Algorithm",n?.algorithm??t?.algorithm??"flat"),n?.algorithm_config&&"object"==typeof n.algorithm_config)for(const[e,t]of Object.entries(n.algorithm_config))pushMetric(o,e,compactCount(t));pushMetric(o,"Embedder",n?.embedder??t?.embedder);break;case"embed-worker":pushMetric(o,"Job type",n?.job_type??t?.job_type),pushMetric(o,"Indexer",n?.indexer??t?.indexer),pushMetric(o,"Embedder",n?.embedder??t?.embedder),pushMetric(o,"Concurrency",compactCount(n?.concurrency??t?.concurrency)),pushMetric(o,"Poll",null!=n?.pollInterval?`${compactCount(n.pollInterval)} ms`:null),pushMetric(o,"TTL",null!=n?.ttl?`${compactCount(n.ttl)} ms`:null);break;default:pushMetric(o,"Source",n?.source_type??t?.source_type),pushMetric(o,"Target",n?.target_type??t?.target_type),pushMetric(o,"Mode",n?.mode??t?.mode)}return c?.name&&pushMetric(o,"Handler",c.name,{title:"Configured handler function"}),c?.recordRuns&&pushMetric(o,"Run log",c.recordRuns,{title:"Function run recording policy"}),o}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"../../../components/ok-json-input.ok.js";import{buildEngineBasePath as
|
|
1
|
+
import e from"../../../components/ok-json-input.ok.js";import{buildEngineBasePath as t,cloneValue as n,getApiErrorMessage as a}from"../engine-ui-utils.js";export default{tag:"engine-declaration-editor",dependencies:[e],attr:{engine:{},env:{}},context:e=>({saving:!1,error:null,open:!1,enabledDraft:!0,metaDraft:{},get eng(){return e.$attr.engine??{}},get basePath(){return t(this.eng,e.$attr.env??null)},get canEdit(){return!!this.basePath},get metaSummary(){const e=this.eng?.meta??{},t="object"==typeof e&&e&&!Array.isArray(e)?Object.keys(e).length:0;return t?`${t} field${1===t?"":"s"}`:"empty"},syncDrafts(e=null){const t=e??this.eng??{};this.enabledDraft=!1!==t.enabled,this.metaDraft=n(t.meta,{}),this.error=null},cloneMeta:e=>n(e,{}),toggleOpen(){this.open=!this.open},async save(){if(this.canEdit&&!this.saving){this.saving=!0,this.error=null;try{const t=await fetch(this.basePath,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:this.enabledDraft,meta:n(this.metaDraft,{})})}),i=await t.json().catch(()=>({}));if(!t.ok)throw new Error(a(i,`Failed (${t.status})`));const s=i?.data??i?.result??null;s&&(this.syncDrafts(s),e.$dispatch("engine-updated",s)),this.open=!1,e.$dispatch("refresh"),e.$ok.shared?.toast?.success?.("Engine declaration saved")}catch(t){this.error=t?.message||String(t),this.open=!0,e.$ok.shared?.toast?.error?.(this.error)}finally{this.saving=!1}}}}),init(e){this.syncDrafts(e.$attr.engine),e.$watch(()=>e.$attr.engine,()=>this.syncDrafts(e.$attr.engine))},template:'\n<div style="display:flex; flex-direction:column; gap:12px; padding:14px; border:1px solid var(--okdb-app-border); border-radius:12px; background:var(--okdb-app-bg-deep);">\n <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <div style="font-weight:600; font-size:13px;">Advanced declaration</div>\n <div style="font-size:11px; opacity:.45;">enablement and synced meta</div>\n <div style="flex:1;"></div>\n <button @click="toggleOpen()" style="font-size:11px; padding:4px 10px; opacity:.8;">\n {{ open ? \'Hide\' : \'Edit declaration\' }}\n </button>\n </div>\n\n <div class="kv-grid kv-grid--compact">\n <span class="kv-key">enabled</span><span class="kv-val">{{ eng.enabled === false ? \'no\' : \'yes\' }}</span>\n <span class="kv-key">meta</span><span class="kv-val">{{ metaSummary }}</span>\n </div>\n\n <if :="open">\n <div style="display:flex; flex-direction:column; gap:12px; padding-top:4px; border-top:1px solid var(--okdb-app-border);">\n <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <div style="font-size:11px; opacity:.5;">These values are persisted and synced.</div>\n <div style="flex:1;"></div>\n <button @click="save()" :disabled="saving || !canEdit" style="font-size:11px; padding:4px 10px; color:var(--okdb-accent); border-color:color-mix(in srgb, var(--okdb-accent) 33%, transparent);">\n {{ saving ? \'Saving…\' : \'Save declaration\' }}\n </button>\n </div>\n\n <label style="display:flex; align-items:center; gap:8px; font-size:12px;">\n <input type="checkbox" :checked="enabledDraft" @change="enabledDraft = $event.target.checked" />\n <span>Enabled on eligible nodes</span>\n </label>\n\n <div style="display:flex; flex-direction:column; gap:6px; min-height:0;">\n <div style="font-size:10px; opacity:.45; text-transform:uppercase; letter-spacing:.05em;">Meta</div>\n <div style="min-height:160px; border:1px solid var(--okdb-app-border); border-radius:8px; overflow:hidden; background:var(--okdb-app-bg-deep);">\n <ok-json-input mode="textbox" :value="metaDraft" @edit="metaDraft = cloneMeta($event.detail)" style="height:100%; display:flex; min-height:160px;" />\n </div>\n </div>\n\n <if :="error">\n <div style="font-size:12px; color:var(--okdb-danger);">{{ error }}</div>\n </if>\n </div>\n </if>\n</div>\n'};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"./engine-panel-shell.ok.js";import n from"../../functions/modals/function-modal.ok.js";import{ENGINE_PANEL_SHARED_STYLE as t}from"../engine-panel-shared-style.js";import{buildEngineBasePath as i,formatJsonValue as r,getApiErrorMessage as a,openFunctionRoute as s,resolveEngineEnvName as l}from"../engine-ui-utils.js";function o(e,n="—"){return null==e||""===e?n:Array.isArray(e)?e.length?e.join(", "):n:"boolean"==typeof e?e?"yes":"no":String(e)}function c(e,n=[]){return function(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}(e)?Object.entries(e).filter(([e,t])=>!n.includes(e)&&(null==t||["string","number","boolean"].includes(typeof t))):[]}function d(e,n){if(""===e||null==e)return null;const t=Math.trunc(Number(e));if(!Number.isFinite(t)||t<1)throw new Error(`${n} must be a positive integer`);return t}function p(e){if(null==e||""===e)return"—";const n=Number(e);if(!Number.isFinite(n))return String(e);const t=n<=1?100*n:n;return`${Math.round(t)}%`}export default{tag:"generic-engine-panel",dependencies:[e,n],attr:{engine:{},env:{},modal:{}},context:e=>({saveConfigBusy:!1,saveConfigError:null,materializerRepairBusy:!1,materializerRepairError:null,showRawRuntime:!1,showRawStats:!1,showRawConfig:!1,workerConfigDraft:{concurrency:"",pollInterval:"",ttl:""},get eng(){return e.$attr.engine??{}},get envName(){return l(this.eng,e.$attr.env??null)},get basePath(){return i(this.eng,this.envName)},get isQueueWorker(){return"queue-worker"===this.eng.type},get isProcessor(){return"processor"===this.eng.type},get isMaterializer(){return"materializer"===this.eng.type},get hasSpecializedPipelineView(){return this.isQueueWorker||this.isProcessor||this.isMaterializer},get stateText(){return"error"===this.eng.status?"error":"inactive"===this.eng.status?"inactive":this.eng.isRunning||"online"===this.eng.status?"running":this.eng.status||"stopped"},get reasonText(){const e=this.eng.reason??null;return e&&e!==this.stateText?String(e).replace(/_/g," "):"—"},get affinityText(){return o(this.eng.affinity,"all nodes")},get updatedText(){return function(e){if(!e)return"—";try{return new Date(e).toLocaleString()}catch{return String(e)}}(this.eng.runtime?.updated)},get runtimePreviewEntries(){return c(this.eng.runtime,["state","reason","error","running","updated"])},get statsPreviewEntries(){return c(this.eng.stats,["engine_key","job_type","queue_env","handler","concurrency","processed","failed","running"])},get configPreviewEntries(){return c(this.eng.config,["job_type","queue_env","handler","concurrency","pollInterval","ttl"])},get workerStats(){return this.eng.stats??{}},get pipelineStats(){return this.eng.stats??{}},get workerHandler(){return this.workerStats.handler??this.eng.config?.handler??null},get pipelineHandler(){return this.pipelineStats.handler??this.eng.config?.handler??null},get handlerName(){return this.pipelineHandler?.name??null},get handlerEnv(){return this.pipelineHandler?.env??this.envName??"—"},get canOpenHandlerFunction(){return!!this.handlerName},get workerJobType(){return this.workerStats.job_type??this.eng.config?.job_type??"—"},get workerQueueEnv(){return this.workerStats.queue_env??this.eng.config?.queue_env??this.envName??"—"},get workerFunctionEnv(){return this.workerHandler?.env??this.envName??"—"},get workerRecordRuns(){return this.eng.config?.handler?.recordRuns??"all"},get workerProcessed(){return this.workerStats.processed??0},get workerFailed(){return this.workerStats.failed??0},get workerRunning(){return this.workerStats.running??0},get workerConcurrency(){return this.workerStats.concurrency??this.eng.config?.concurrency??1},get workerPollInterval(){return this.eng.config?.pollInterval??1e3},get workerTtl(){return this.eng.config?.ttl??3e4},get canOpenWorkerFunction(){return!!this.workerHandler?.name},get showWorkerReason(){return"—"!==this.reasonText&&"running"!==this.reasonText},get showWorkerAffinity(){return"all nodes"!==this.affinityText},get processorSourceType(){return this.pipelineStats.source_type??this.eng.config?.source_type??"—"},get processorSourceEnv(){return this.pipelineStats.source_env??this.eng.config?.source_env??this.envName??"—"},get processorMode(){return this.pipelineStats.mode??this.eng.config?.mode??"async"},get processorBootstrap(){return this.pipelineStats.bootstrap??this.eng.config?.bootstrap??"snapshot"},get processorOriginMode(){return this.pipelineStats.originMode??this.eng.config?.originMode??"all"},get processorBatchSize(){return this.pipelineStats.batchSize??this.eng.config?.batchSize??256},get processorCursorKey(){return this.pipelineStats.cursorKey??this.eng.config?.cursorKey??this.eng.key??"—"},get processorLockMode(){return this.pipelineStats.lockMode??this.eng.config?.lockMode??"exclusive"},get processorHydrateValues(){return o(this.pipelineStats.hydrateValues??this.eng.config?.hydrateValues??!0)},get processorFailOnHandlerError(){return o(this.pipelineStats.failOnHandlerError??this.eng.config?.failOnHandlerError??!0)},get processorInvocations(){return this.pipelineStats.invocations??0},get processorProcessedChanges(){return this.pipelineStats.processedChanges??0},get processorFailedBatches(){return this.pipelineStats.failedBatches??0},get processorRunningBatches(){return this.pipelineStats.runningBatches??0},get processorLag(){return this.pipelineStats.lag??"—"},get processorProgress(){return p(this.pipelineStats.progress)},get processorPaused(){return!0===this.pipelineStats.paused},get materializerSourceType(){return this.pipelineStats.source_type??this.eng.config?.source_type??"—"},get materializerTargetType(){return this.pipelineStats.target_type??this.eng.config?.target_type??"—"},get materializerSourceEnv(){return this.pipelineStats.source_env??this.eng.config?.source_env??this.envName??"—"},get materializerTargetEnv(){return this.pipelineStats.target_env??this.eng.config?.target_env??this.envName??"—"},get materializerBootstrap(){return this.pipelineStats.bootstrap??this.eng.config?.bootstrap??"snapshot"},get materializerOriginMode(){return this.pipelineStats.originMode??this.eng.config?.originMode??"all"},get materializerBatchSize(){return this.pipelineStats.batchSize??this.eng.config?.batchSize??128},get materializerCursorKey(){return this.pipelineStats.cursorKey??this.eng.config?.cursorKey??this.eng.key??"—"},get materializerClearTargetOnRebuild(){return o(this.pipelineStats.clearTargetOnRebuild??this.eng.config?.clearTargetOnRebuild??!0)},get materializerProcessedChanges(){return this.pipelineStats.processedChanges??0},get materializerAppliedOps(){return this.pipelineStats.appliedOps??0},get materializerFailedBatches(){return this.pipelineStats.failedBatches??0},get materializerRunningBatches(){return this.pipelineStats.runningBatches??0},get materializerTargetCount(){return this.pipelineStats.targetCount??"—"},get materializerLag(){return this.pipelineStats.lag??"—"},get materializerProgress(){return p(this.pipelineStats.progress)},get materializerPaused(){return!0===this.pipelineStats.paused},get materializerErrorText(){return this.pipelineStats.error??this.eng.error??null},get materializerPreviewPayload(){return[{action:"put",key:"example-1",value:{sample:!0,title:"Example source row",sourceType:this.materializerSourceType},clock:null},{action:"remove",key:"example-2",clock:123}]},get materializerPreviewContext(){return{mode:"online"===this.pipelineStats.state?"live":"build",sourceType:this.materializerSourceType,targetType:this.materializerTargetType,sourceEnv:this.materializerSourceEnv,targetEnv:this.materializerTargetEnv,engineKey:this.eng.key??null,cursorKey:this.materializerCursorKey}},formatJson:r,labelize:e=>String(e||"").replace(/_/g," "),humanValue:o,rawToggleStyle:e=>e?"border-color:color-mix(in srgb, var(--okdb-accent) 40%, transparent);color:var(--okdb-accent);background:color-mix(in srgb, var(--okdb-accent) 7%, transparent);":"",syncWorkerConfig(e=null){const n=e?.config??{};this.workerConfigDraft={concurrency:null==n.concurrency?"":String(n.concurrency),pollInterval:null==n.pollInterval?"":String(n.pollInterval),ttl:null==n.ttl?"":String(n.ttl)},this.saveConfigError=null},buildWorkerConfigPatch(){const e=this.eng.config??{},n={concurrency:d(this.workerConfigDraft.concurrency,"Concurrency"),pollInterval:d(this.workerConfigDraft.pollInterval,"Poll interval"),ttl:d(this.workerConfigDraft.ttl,"Job TTL")},t={};for(const[i,r]of Object.entries(n))null==r?void 0!==e[i]&&(t[i]=null):e[i]!==r&&(t[i]=r);return t},resetWorkerConfig(){this.syncWorkerConfig(this.eng)},inspectWorkerFunction(){this.openHandlerFunction(this.workerHandler?.name??null)},openHandlerFunction(n=null){const t=n??this.handlerName;t&&s(e,t,{beforeNavigate:()=>e.$attr.modal?.close?.()})},async openHandlerEditor(){if(!this.handlerName)return;const n=e.$ok.stores.okdb,t=e.$ok.stores.functions,i=this.handlerEnv&&"—"!==this.handlerEnv?this.handlerEnv:this.envName;i?(n.selectedEnv!==i&&n.selectEnv(i),await t.onEnvChanged(),await t.select(this.handlerName),t.beginEditDraft(),t.setModalPayloadDraft(this.formatJson(this.materializerPreviewPayload)),t.setModalPreviewContext({materializerContext:this.materializerPreviewContext}),await e.$ok.shared.modal.open({owner:e,title:`Edit Function · ${this.handlerName}`,canDrag:!0,content:'<function-modal :modal="modal" mode="edit" :on-done="onDone" />',context:n=>({modal:n,onDone:async()=>{await t.select(this.handlerName),await t.loadOverviewRuns(),e.$dispatch("refresh")}})})):this.openHandlerFunction()},async runMaterializerRepairAction(n,t,i=null){if(this.isMaterializer&&this.basePath&&!this.materializerRepairBusy){if(i&&!await e.$ok.shared.modal.confirm(i))return;this.materializerRepairBusy=!0,this.materializerRepairError=null;try{const i=await fetch(`${this.basePath}${n}`,{method:"POST"}),r=await i.json().catch(()=>({}));if(!i.ok)throw new Error(a(r,`Failed (${i.status})`));const s=r?.data??r?.result??null;s&&e.$dispatch("engine-updated",s),e.$dispatch("refresh"),e.$ok.shared?.toast?.success?.(t)}catch(n){this.materializerRepairError=n?.message||String(n),e.$ok.shared?.toast?.error?.(this.materializerRepairError)}finally{this.materializerRepairBusy=!1}}},retryMaterializer(){return this.runMaterializerRepairAction("/retry","Materializer retry started")},resetMaterializerCursor(){return this.runMaterializerRepairAction("/reset-cursor","Materializer cursor reset",`Reset cursor for materializer "${this.eng.name}"?`)},rebuildMaterializer(){return this.runMaterializerRepairAction("/rebuild","Materializer rebuild started",`Rebuild materializer "${this.eng.name}"?`)},async saveWorkerConfig(){if(this.isQueueWorker&&this.basePath&&!this.saveConfigBusy){this.saveConfigBusy=!0,this.saveConfigError=null;try{const n=this.buildWorkerConfigPatch();if(!Object.keys(n).length)return void e.$ok.shared?.toast?.info?.("No worker setting changes to save");const t=await fetch(this.basePath,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({config:n})}),i=await t.json().catch(()=>({}));if(!t.ok)throw new Error(a(i,`Failed (${t.status})`));const r=i?.data??i?.result??null;r&&(this.syncWorkerConfig(r),e.$dispatch("engine-updated",r)),e.$dispatch("refresh"),e.$ok.shared?.toast?.success?.("Worker settings saved")}catch(n){this.saveConfigError=n?.message||String(n),e.$ok.shared?.toast?.error?.(this.saveConfigError)}finally{this.saveConfigBusy=!1}}}}),init(e){this.syncWorkerConfig(e.$attr.engine),e.$watch(()=>e.$attr.engine,()=>this.syncWorkerConfig(e.$attr.engine))},template:'\n<engine-panel-shell\n :engine="eng"\n :env="envName"\n @engine-updated="$dispatch(\'engine-updated\', $event.detail)"\n @refresh="$dispatch(\'refresh\')"\n @deleted="$dispatch(\'deleted\', $event.detail)"\n>\n <if :="isQueueWorker">\n <div class="stack-compact">\n <div class="card stack-compact" style="padding:14px;gap:12px;">\n <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;">\n <div style="display:flex;flex-direction:column;gap:8px;min-width:0;flex:1;">\n <div class="pill-row">\n <span class="info-pill" title="Queue job type this worker consumes.">{{ workerJobType }}</span>\n <span class="info-pill" title="Environment whose queue is consumed.">queue: {{ workerQueueEnv }}</span>\n <span class="info-pill" title="Function environment.">fn env: {{ workerFunctionEnv }}</span>\n <if :="showWorkerReason">\n <span class="info-pill" title="Why the worker is in its current state.">{{ reasonText }}</span>\n </if>\n <if :="showWorkerAffinity">\n <span class="info-pill" title="Node tags required for this worker to run.">affinity: {{ affinityText }}</span>\n </if>\n </div>\n <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0;">\n <span class="field-label" style="margin:0;">Function</span>\n <if :="canOpenWorkerFunction">\n <button @click="inspectWorkerFunction()" class="inline-link" title="View or edit this function in the Functions section.">\n ƒ {{ workerHandler?.name || \'—\' }} ↗\n </button>\n </if>\n <if :="!canOpenWorkerFunction">\n <span class="muted-code" style="color:var(--okdb-app-fg);">{{ workerHandler?.name || \'—\' }}</span>\n </if>\n </div>\n </div>\n\n <div class="metric-grid metric-grid--compact" style="min-width:min(100%, 360px);">\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Done</div>\n <div class="metric-value" style="color:var(--okdb-success);">{{ workerProcessed }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Error</div>\n <div class="metric-value" :style="\'color:\' + (workerFailed > 0 ? \'var(--okdb-danger)\' : \'var(--okdb-app-fg-strong)\')">{{ workerFailed }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Active</div>\n <div class="metric-value" :style="\'color:\' + (workerRunning > 0 ? \'var(--okdb-warning)\' : \'var(--okdb-app-fg-strong)\')">{{ workerRunning }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Concurrency</div>\n <div class="metric-value" style="color:var(--okdb-accent);">{{ workerConcurrency }}</div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="card stack-compact" style="padding:14px;gap:12px;">\n <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">\n <div class="field-label" style="margin:0;">Settings</div>\n <div style="flex:1;"></div>\n <span class="story-note" style="margin-right:4px;">updated {{ updatedText }}</span>\n <button @click="resetWorkerConfig()" :disabled="saveConfigBusy || !basePath" title="Discard unsaved changes." style="font-size:11px;padding:4px 10px;opacity:.75;">Reset</button>\n <button @click="saveWorkerConfig()" :disabled="saveConfigBusy || !basePath" title="Save and restart the worker with these values." style="font-size:11px;padding:4px 10px;color:var(--okdb-accent);border-color:color-mix(in srgb, var(--okdb-accent) 33%, transparent);">\n {{ saveConfigBusy ? \'Saving…\' : \'Save\' }}\n </button>\n </div>\n\n <div class="form-grid">\n <label class="field-stack">\n <span class="field-label" title="Parallel worker lanes.">Concurrency</span>\n <input class="field-input" type="number" min="1" :value="workerConfigDraft.concurrency" @input="workerConfigDraft.concurrency = $event.target.value" placeholder="1" />\n </label>\n <label class="field-stack">\n <span class="field-label" title="Milliseconds to wait before polling again when the queue is empty.">Poll</span>\n <input class="field-input" type="number" min="1" :value="workerConfigDraft.pollInterval" @input="workerConfigDraft.pollInterval = $event.target.value" placeholder="1000" />\n </label>\n <label class="field-stack">\n <span class="field-label" title="Milliseconds an in-flight claim stays reserved before it can be reclaimed.">TTL</span>\n <input class="field-input" type="number" min="1" :value="workerConfigDraft.ttl" @input="workerConfigDraft.ttl = $event.target.value" placeholder="30000" />\n </label>\n <label class="field-stack">\n <span class="field-label" title="Whether function runs are persisted.">Runs</span>\n <div class="field-input" style="display:flex;align-items:center;min-height:36px;background:var(--okdb-app-bg);">{{ workerRecordRuns }}</div>\n </label>\n </div>\n\n <if :="saveConfigError">\n <div class="alert alert--error">{{ saveConfigError }}</div>\n </if>\n </div>\n </div>\n </if>\n\n <if :="isProcessor">\n <div class="stack-compact">\n <div class="card stack-compact" style="padding:14px;gap:12px;">\n <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;">\n <div style="display:flex;flex-direction:column;gap:8px;min-width:0;flex:1;">\n <div class="pill-row">\n <span class="info-pill" title="Source type this processor watches.">{{ processorSourceType }}</span>\n <span class="info-pill" title="Environment whose changelog is consumed.">source: {{ processorSourceEnv }}</span>\n <span class="info-pill" title="Function environment.">fn env: {{ handlerEnv }}</span>\n <span class="info-pill" title="How source changes are consumed.">mode: {{ processorMode }}</span>\n <if :="processorPaused">\n <span class="info-pill" title="Processor registration is currently paused.">paused</span>\n </if>\n <if :="showWorkerReason">\n <span class="info-pill" title="Why the processor is in its current state.">{{ reasonText }}</span>\n </if>\n </div>\n <div class="story-lead">\n Watches <strong>{{ processorSourceType }}</strong> in <strong>{{ processorSourceEnv }}</strong> and runs\n <if :="canOpenHandlerFunction">\n <button @click="openHandlerFunction()" class="inline-link" title="View or edit this function in the Functions section.">ƒ {{ handlerName }} ↗</button>\n </if>\n <if :="!canOpenHandlerFunction">\n <span class="muted-code" style="color:var(--okdb-app-fg);">{{ handlerName || \'—\' }}</span>\n </if>\n for each change batch.\n </div>\n <div class="story-note">Use this engine for side effects and projections driven by change batches without owning a dedicated target type.</div>\n </div>\n\n <div class="metric-grid metric-grid--compact" style="min-width:min(100%, 420px);">\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Invocations</div>\n <div class="metric-value" style="color:var(--okdb-accent);">{{ processorInvocations }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Changes</div>\n <div class="metric-value" style="color:var(--okdb-success);">{{ processorProcessedChanges }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Failures</div>\n <div class="metric-value" :style="\'color:\' + (processorFailedBatches > 0 ? \'var(--okdb-danger)\' : \'var(--okdb-app-fg-strong)\')">{{ processorFailedBatches }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Lag</div>\n <div class="metric-value" :style="\'color:\' + (processorLag === 0 ? \'var(--okdb-success)\' : \'var(--okdb-warning)\')">{{ processorLag }}</div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="card stack-compact" style="padding:14px;gap:12px;">\n <div class="card-title" style="margin:0;">Processing settings</div>\n <div class="kv-grid kv-grid--compact">\n <span class="kv-key">bootstrap</span><span class="kv-val">{{ processorBootstrap }}</span>\n <span class="kv-key">origin mode</span><span class="kv-val">{{ processorOriginMode }}</span>\n <span class="kv-key">batch size</span><span class="kv-val">{{ processorBatchSize }}</span>\n <span class="kv-key">running batches</span><span class="kv-val">{{ processorRunningBatches }}</span>\n <span class="kv-key">progress</span><span class="kv-val">{{ processorProgress }}</span>\n <span class="kv-key">cursor key</span><span class="kv-val">{{ processorCursorKey }}</span>\n <span class="kv-key">lock mode</span><span class="kv-val">{{ processorLockMode }}</span>\n <span class="kv-key">hydrate values</span><span class="kv-val">{{ processorHydrateValues }}</span>\n <span class="kv-key">fail on fn error</span><span class="kv-val">{{ processorFailOnHandlerError }}</span>\n </div>\n </div>\n </div>\n </if>\n\n <if :="isMaterializer">\n <div class="stack-compact">\n <div class="card stack-compact" style="padding:14px;gap:12px;">\n <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;">\n <div style="display:flex;flex-direction:column;gap:8px;min-width:0;flex:1;">\n <div class="pill-row">\n <span class="info-pill" title="Source type this materializer watches.">{{ materializerSourceType }}</span>\n <span class="info-pill" title="Target type updated by returned ops.">target: {{ materializerTargetType }}</span>\n <span class="info-pill" title="Source environment.">source env: {{ materializerSourceEnv }}</span>\n <span class="info-pill" title="Target environment.">target env: {{ materializerTargetEnv }}</span>\n <span class="info-pill" title="Function environment.">fn env: {{ handlerEnv }}</span>\n <if :="materializerPaused">\n <span class="info-pill" title="Materializer registration is currently paused.">paused</span>\n </if>\n </div>\n <div class="story-lead">\n Projects <strong>{{ materializerSourceType }}</strong> into <strong>{{ materializerTargetType }}</strong> using\n <if :="canOpenHandlerFunction">\n <button @click="openHandlerFunction()" class="inline-link" title="View or edit this function in the Functions section.">ƒ {{ handlerName }} ↗</button>\n </if>\n <if :="!canOpenHandlerFunction">\n <span class="muted-code" style="color:var(--okdb-app-fg);">{{ handlerName || \'—\' }}</span>\n </if>\n and applies the returned ops to the target collection.\n </div>\n <div class="story-note">Use this engine when you want deterministic, rebuildable derived records instead of free-form side effects.</div>\n </div>\n\n <div class="metric-grid metric-grid--compact" style="min-width:min(100%, 420px);">\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Applied ops</div>\n <div class="metric-value" style="color:var(--okdb-success);">{{ materializerAppliedOps }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Target rows</div>\n <div class="metric-value" style="color:var(--okdb-accent);">{{ materializerTargetCount }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Changes</div>\n <div class="metric-value" style="color:var(--okdb-success);">{{ materializerProcessedChanges }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Failures</div>\n <div class="metric-value" :style="\'color:\' + (materializerFailedBatches > 0 ? \'var(--okdb-danger)\' : \'var(--okdb-app-fg-strong)\')">{{ materializerFailedBatches }}</div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="card stack-compact" style="padding:14px;gap:12px;">\n <div class="card-title" style="margin:0;">Materialization settings</div>\n <div class="kv-grid kv-grid--compact">\n <span class="kv-key">bootstrap</span><span class="kv-val">{{ materializerBootstrap }}</span>\n <span class="kv-key">origin mode</span><span class="kv-val">{{ materializerOriginMode }}</span>\n <span class="kv-key">batch size</span><span class="kv-val">{{ materializerBatchSize }}</span>\n <span class="kv-key">running batches</span><span class="kv-val">{{ materializerRunningBatches }}</span>\n <span class="kv-key">progress</span><span class="kv-val">{{ materializerProgress }}</span>\n <span class="kv-key">lag</span><span class="kv-val">{{ materializerLag }}</span>\n <span class="kv-key">cursor key</span><span class="kv-val">{{ materializerCursorKey }}</span>\n <span class="kv-key">clear on rebuild</span><span class="kv-val">{{ materializerClearTargetOnRebuild }}</span>\n </div>\n </div>\n\n <div class="card stack-compact" style="padding:14px;gap:12px;border-color:#244057;">\n <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;">\n <div style="display:flex;flex-direction:column;gap:6px;min-width:0;flex:1;">\n <div class="card-title" style="margin:0;">Repair workflow</div>\n <div class="story-note" style="margin:0;">Edit the handler with seeded sample input, verify the draft run, then retry, rewind, or rebuild from here.</div>\n </div>\n <div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;">\n <button @click="openHandlerEditor()" :disabled="!canOpenHandlerFunction || materializerRepairBusy" style="font-size:12px;padding:4px 10px;color:var(--okdb-accent);border-color:color-mix(in srgb, var(--okdb-accent) 33%, transparent);">ƒ Edit handler</button>\n <button @click="retryMaterializer()" :disabled="materializerRepairBusy || !basePath" style="font-size:12px;padding:4px 10px;color:var(--okdb-orange);border-color:color-mix(in srgb, var(--okdb-orange) 33%, transparent);">↻ Retry</button>\n <button @click="resetMaterializerCursor()" :disabled="materializerRepairBusy || !basePath" style="font-size:12px;padding:4px 10px;color:var(--okdb-info);border-color:color-mix(in srgb, var(--okdb-info) 33%, transparent);">⇤ Reset cursor</button>\n <button @click="rebuildMaterializer()" :disabled="materializerRepairBusy || !basePath" style="font-size:12px;padding:4px 10px;color:var(--okdb-success);border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);">⟳ Rebuild</button>\n </div>\n </div>\n\n <if :="materializerErrorText">\n <div class="alert alert--error">\n <div style="font-weight:600;margin-bottom:4px;">Last materializer error</div>\n <div>{{ materializerErrorText }}</div>\n </div>\n </if>\n\n <if :="materializerRepairError">\n <div class="alert alert--error">{{ materializerRepairError }}</div>\n </if>\n\n <div style="display:grid;grid-template-columns:minmax(0,1fr) minmax(280px,.9fr);gap:12px;">\n <div style="display:flex;flex-direction:column;gap:8px;min-width:0;">\n <div class="field-label">Suggested <code>ctx.payload</code></div>\n <pre class="json-block" style="margin:0;max-height:220px;overflow:auto;">{{ formatJson(materializerPreviewPayload) }}</pre>\n </div>\n <div style="display:flex;flex-direction:column;gap:8px;min-width:0;">\n <div class="field-label">Suggested <code>ctx.materializer</code></div>\n <pre class="json-block" style="margin:0;max-height:220px;overflow:auto;">{{ formatJson(materializerPreviewContext) }}</pre>\n </div>\n </div>\n\n <div class="story-note" style="margin:0;">Recommended order: <strong>Edit handler</strong> → run the seeded draft preview → <strong>Retry</strong>. Use <strong>Reset cursor</strong> to replay without clearing the target, or <strong>Rebuild</strong> to clear and rematerialize from scratch.</div>\n </div>\n </div>\n </if>\n\n <if :="!hasSpecializedPipelineView">\n <div class="panel-grid panel-grid--two">\n <div class="card" style="display:flex;flex-direction:column;gap:12px;">\n <div class="card-title" style="margin:0;">Overview</div>\n <div class="kv-grid kv-grid--compact">\n <span class="kv-key">state</span><span class="kv-val">{{ stateText }}</span>\n <span class="kv-key">reason</span><span class="kv-val">{{ reasonText }}</span>\n <span class="kv-key">enabled</span><span class="kv-val">{{ eng.enabled === false ? \'no\' : \'yes\' }}</span>\n <span class="kv-key">affinity</span><span class="kv-val">{{ affinityText }}</span>\n <span class="kv-key">updated</span><span class="kv-val">{{ updatedText }}</span>\n </div>\n <if :="runtimePreviewEntries.length > 0">\n <div style="display:flex;flex-direction:column;gap:8px;">\n <div class="field-label">Runtime details</div>\n <div class="kv-grid kv-grid--compact">\n <each :of="runtimePreviewEntries" let:item="entry">\n <span class="kv-key">{{ labelize(entry[0]) }}</span><span class="kv-val">{{ humanValue(entry[1]) }}</span>\n </each>\n </div>\n </div>\n </if>\n </div>\n\n <div class="card" style="display:flex;flex-direction:column;gap:12px;">\n <div class="card-title" style="margin:0;">Configuration</div>\n <if :="configPreviewEntries.length > 0">\n <div class="kv-grid kv-grid--compact">\n <each :of="configPreviewEntries" let:item="entry">\n <span class="kv-key">{{ labelize(entry[0]) }}</span><span class="kv-val">{{ humanValue(entry[1]) }}</span>\n </each>\n </div>\n </if>\n <if :="configPreviewEntries.length === 0">\n <div class="helper-text">This engine mostly uses structured config. Open the raw JSON below if you need the full payload.</div>\n </if>\n <if :="statsPreviewEntries.length > 0">\n <div style="display:flex;flex-direction:column;gap:8px;">\n <div class="field-label">Stats snapshot</div>\n <div class="kv-grid kv-grid--compact">\n <each :of="statsPreviewEntries" let:item="entry">\n <span class="kv-key">{{ labelize(entry[0]) }}</span><span class="kv-val">{{ humanValue(entry[1]) }}</span>\n </each>\n </div>\n </div>\n </if>\n </div>\n </div>\n </if>\n\n <div class="card" style="display:flex;flex-direction:column;gap:12px;">\n <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">\n <div class="card-title" style="margin:0;">Raw data</div>\n <div class="card-subtitle">Keep the default view visual first. Open these only when you need the underlying JSON.</div>\n </div>\n <div class="raw-toggle-row">\n <if :="eng.runtime">\n <button class="raw-toggle" :style="rawToggleStyle(showRawRuntime)" @click="showRawRuntime = !showRawRuntime">Runtime JSON</button>\n </if>\n <if :="eng.stats">\n <button class="raw-toggle" :style="rawToggleStyle(showRawStats)" @click="showRawStats = !showRawStats">Stats JSON</button>\n </if>\n <if :="eng.config">\n <button class="raw-toggle" :style="rawToggleStyle(showRawConfig)" @click="showRawConfig = !showRawConfig">Config JSON</button>\n </if>\n </div>\n\n <if :="showRawRuntime && eng.runtime">\n <div style="display:flex;flex-direction:column;gap:6px;">\n <div class="field-label">Runtime payload</div>\n <pre class="json-block">{{ formatJson(eng.runtime) }}</pre>\n </div>\n </if>\n <if :="showRawStats && eng.stats">\n <div style="display:flex;flex-direction:column;gap:6px;">\n <div class="field-label">Stats payload</div>\n <pre class="json-block">{{ formatJson(eng.stats) }}</pre>\n </div>\n </if>\n <if :="showRawConfig && eng.config">\n <div style="display:flex;flex-direction:column;gap:6px;">\n <div class="field-label">Config payload</div>\n <pre class="json-block">{{ formatJson(eng.config) }}</pre>\n </div>\n </if>\n </div>\n</engine-panel-shell>\n',style:t};
|
|
1
|
+
import e from"./engine-panel-shell.ok.js";import n from"../../functions/modals/function-modal.ok.js";import{ENGINE_PANEL_SHARED_STYLE as t}from"../engine-panel-shared-style.js";import{buildEngineBasePath as i,formatJsonValue as r,getApiErrorMessage as a,openFunctionRoute as s,resolveEngineEnvName as l}from"../engine-ui-utils.js";function o(e,n="—"){return null==e||""===e?n:Array.isArray(e)?e.length?e.join(", "):n:"boolean"==typeof e?e?"yes":"no":String(e)}function c(e,n=[]){return function(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}(e)?Object.entries(e).filter(([e,t])=>!n.includes(e)&&(null==t||["string","number","boolean"].includes(typeof t))):[]}function d(e,n){if(""===e||null==e)return null;const t=Math.trunc(Number(e));if(!Number.isFinite(t)||t<1)throw new Error(`${n} must be a positive integer`);return t}function p(e){if(null==e||""===e)return"—";const n=Number(e);if(!Number.isFinite(n))return String(e);const t=n<=1?100*n:n;return`${Math.round(t)}%`}export default{tag:"generic-engine-panel",dependencies:[e,n],attr:{engine:{},env:{},modal:{}},context:e=>({saveConfigBusy:!1,saveConfigError:null,materializerRepairBusy:!1,materializerRepairError:null,showRawRuntime:!1,showRawStats:!1,showRawConfig:!1,workerConfigDraft:{concurrency:"",pollInterval:"",ttl:""},get eng(){return e.$attr.engine??{}},get envName(){return l(this.eng,e.$attr.env??null)},get basePath(){return i(this.eng,this.envName)},get isQueueWorker(){return"queue-worker"===this.eng.type},get isProcessor(){return"processor"===this.eng.type},get isMaterializer(){return"materializer"===this.eng.type},get hasSpecializedPipelineView(){return this.isQueueWorker||this.isProcessor||this.isMaterializer},get stateText(){return"error"===this.eng.status?"error":"inactive"===this.eng.status?"inactive":this.eng.isRunning||"online"===this.eng.status?"running":this.eng.status||"stopped"},get reasonText(){const e=this.eng.reason??null;return e&&e!==this.stateText?String(e).replace(/_/g," "):"—"},get updatedText(){return function(e){if(!e)return"—";try{return new Date(e).toLocaleString()}catch{return String(e)}}(this.eng.runtime?.updated)},get runtimePreviewEntries(){return c(this.eng.runtime,["state","reason","error","running","updated"])},get statsPreviewEntries(){return c(this.eng.stats,["engine_key","job_type","queue_env","handler","concurrency","processed","failed","running"])},get configPreviewEntries(){return c(this.eng.config,["job_type","queue_env","handler","concurrency","pollInterval","ttl"])},get workerStats(){return this.eng.stats??{}},get pipelineStats(){return this.eng.stats??{}},get workerHandler(){return this.workerStats.handler??this.eng.config?.handler??null},get pipelineHandler(){return this.pipelineStats.handler??this.eng.config?.handler??null},get handlerName(){return this.pipelineHandler?.name??null},get handlerEnv(){return this.pipelineHandler?.env??this.envName??"—"},get canOpenHandlerFunction(){return!!this.handlerName},get workerJobType(){return this.workerStats.job_type??this.eng.config?.job_type??"—"},get workerQueueEnv(){return this.workerStats.queue_env??this.eng.config?.queue_env??this.envName??"—"},get workerFunctionEnv(){return this.workerHandler?.env??this.envName??"—"},get workerRecordRuns(){return this.eng.config?.handler?.recordRuns??"all"},get workerProcessed(){return this.workerStats.processed??0},get workerFailed(){return this.workerStats.failed??0},get workerRunning(){return this.workerStats.running??0},get workerConcurrency(){return this.workerStats.concurrency??this.eng.config?.concurrency??1},get workerPollInterval(){return this.eng.config?.pollInterval??1e3},get workerTtl(){return this.eng.config?.ttl??3e4},get canOpenWorkerFunction(){return!!this.workerHandler?.name},get showWorkerReason(){return"—"!==this.reasonText&&"running"!==this.reasonText},get processorSourceType(){return this.pipelineStats.source_type??this.eng.config?.source_type??"—"},get processorSourceEnv(){return this.pipelineStats.source_env??this.eng.config?.source_env??this.envName??"—"},get processorMode(){return this.pipelineStats.mode??this.eng.config?.mode??"async"},get processorBootstrap(){return this.pipelineStats.bootstrap??this.eng.config?.bootstrap??"snapshot"},get processorOriginMode(){return this.pipelineStats.originMode??this.eng.config?.originMode??"all"},get processorBatchSize(){return this.pipelineStats.batchSize??this.eng.config?.batchSize??256},get processorCursorKey(){return this.pipelineStats.cursorKey??this.eng.config?.cursorKey??this.eng.key??"—"},get processorLockMode(){return this.pipelineStats.lockMode??this.eng.config?.lockMode??"exclusive"},get processorHydrateValues(){return o(this.pipelineStats.hydrateValues??this.eng.config?.hydrateValues??!0)},get processorFailOnHandlerError(){return o(this.pipelineStats.failOnHandlerError??this.eng.config?.failOnHandlerError??!0)},get processorInvocations(){return this.pipelineStats.invocations??0},get processorProcessedChanges(){return this.pipelineStats.processedChanges??0},get processorFailedBatches(){return this.pipelineStats.failedBatches??0},get processorRunningBatches(){return this.pipelineStats.runningBatches??0},get processorLag(){return this.pipelineStats.lag??"—"},get processorProgress(){return p(this.pipelineStats.progress)},get processorPaused(){return!0===this.pipelineStats.paused},get materializerSourceType(){return this.pipelineStats.source_type??this.eng.config?.source_type??"—"},get materializerTargetType(){return this.pipelineStats.target_type??this.eng.config?.target_type??"—"},get materializerSourceEnv(){return this.pipelineStats.source_env??this.eng.config?.source_env??this.envName??"—"},get materializerTargetEnv(){return this.pipelineStats.target_env??this.eng.config?.target_env??this.envName??"—"},get materializerBootstrap(){return this.pipelineStats.bootstrap??this.eng.config?.bootstrap??"snapshot"},get materializerOriginMode(){return this.pipelineStats.originMode??this.eng.config?.originMode??"all"},get materializerBatchSize(){return this.pipelineStats.batchSize??this.eng.config?.batchSize??128},get materializerCursorKey(){return this.pipelineStats.cursorKey??this.eng.config?.cursorKey??this.eng.key??"—"},get materializerClearTargetOnRebuild(){return o(this.pipelineStats.clearTargetOnRebuild??this.eng.config?.clearTargetOnRebuild??!0)},get materializerProcessedChanges(){return this.pipelineStats.processedChanges??0},get materializerAppliedOps(){return this.pipelineStats.appliedOps??0},get materializerFailedBatches(){return this.pipelineStats.failedBatches??0},get materializerRunningBatches(){return this.pipelineStats.runningBatches??0},get materializerTargetCount(){return this.pipelineStats.targetCount??"—"},get materializerLag(){return this.pipelineStats.lag??"—"},get materializerProgress(){return p(this.pipelineStats.progress)},get materializerPaused(){return!0===this.pipelineStats.paused},get materializerErrorText(){return this.pipelineStats.error??this.eng.error??null},get materializerPreviewPayload(){return[{action:"put",key:"example-1",value:{sample:!0,title:"Example source row",sourceType:this.materializerSourceType},clock:null},{action:"remove",key:"example-2",clock:123}]},get materializerPreviewContext(){return{mode:"online"===this.pipelineStats.state?"live":"build",sourceType:this.materializerSourceType,targetType:this.materializerTargetType,sourceEnv:this.materializerSourceEnv,targetEnv:this.materializerTargetEnv,engineKey:this.eng.key??null,cursorKey:this.materializerCursorKey}},formatJson:r,labelize:e=>String(e||"").replace(/_/g," "),humanValue:o,rawToggleStyle:e=>e?"border-color:color-mix(in srgb, var(--okdb-accent) 40%, transparent);color:var(--okdb-accent);background:color-mix(in srgb, var(--okdb-accent) 7%, transparent);":"",syncWorkerConfig(e=null){const n=e?.config??{};this.workerConfigDraft={concurrency:null==n.concurrency?"":String(n.concurrency),pollInterval:null==n.pollInterval?"":String(n.pollInterval),ttl:null==n.ttl?"":String(n.ttl)},this.saveConfigError=null},buildWorkerConfigPatch(){const e=this.eng.config??{},n={concurrency:d(this.workerConfigDraft.concurrency,"Concurrency"),pollInterval:d(this.workerConfigDraft.pollInterval,"Poll interval"),ttl:d(this.workerConfigDraft.ttl,"Job TTL")},t={};for(const[i,r]of Object.entries(n))null==r?void 0!==e[i]&&(t[i]=null):e[i]!==r&&(t[i]=r);return t},resetWorkerConfig(){this.syncWorkerConfig(this.eng)},inspectWorkerFunction(){this.openHandlerFunction(this.workerHandler?.name??null)},openHandlerFunction(n=null){const t=n??this.handlerName;t&&s(e,t,{beforeNavigate:()=>e.$attr.modal?.close?.()})},async openHandlerEditor(){if(!this.handlerName)return;const n=e.$ok.stores.okdb,t=e.$ok.stores.functions,i=this.handlerEnv&&"—"!==this.handlerEnv?this.handlerEnv:this.envName;i?(n.selectedEnv!==i&&n.selectEnv(i),await t.onEnvChanged(),await t.select(this.handlerName),t.beginEditDraft(),t.setModalPayloadDraft(this.formatJson(this.materializerPreviewPayload)),t.setModalPreviewContext({materializerContext:this.materializerPreviewContext}),await e.$ok.shared.modal.open({owner:e,title:`Edit Function · ${this.handlerName}`,canDrag:!0,content:'<function-modal :modal="modal" mode="edit" :on-done="onDone" />',context:n=>({modal:n,onDone:async()=>{await t.select(this.handlerName),await t.loadOverviewRuns(),e.$dispatch("refresh")}})})):this.openHandlerFunction()},async runMaterializerRepairAction(n,t,i=null){if(this.isMaterializer&&this.basePath&&!this.materializerRepairBusy){if(i&&!await e.$ok.shared.modal.confirm(i))return;this.materializerRepairBusy=!0,this.materializerRepairError=null;try{const i=await fetch(`${this.basePath}${n}`,{method:"POST"}),r=await i.json().catch(()=>({}));if(!i.ok)throw new Error(a(r,`Failed (${i.status})`));const s=r?.data??r?.result??null;s&&e.$dispatch("engine-updated",s),e.$dispatch("refresh"),e.$ok.shared?.toast?.success?.(t)}catch(n){this.materializerRepairError=n?.message||String(n),e.$ok.shared?.toast?.error?.(this.materializerRepairError)}finally{this.materializerRepairBusy=!1}}},retryMaterializer(){return this.runMaterializerRepairAction("/retry","Materializer retry started")},resetMaterializerCursor(){return this.runMaterializerRepairAction("/reset-cursor","Materializer cursor reset",`Reset cursor for materializer "${this.eng.name}"?`)},rebuildMaterializer(){return this.runMaterializerRepairAction("/rebuild","Materializer rebuild started",`Rebuild materializer "${this.eng.name}"?`)},async saveWorkerConfig(){if(this.isQueueWorker&&this.basePath&&!this.saveConfigBusy){this.saveConfigBusy=!0,this.saveConfigError=null;try{const n=this.buildWorkerConfigPatch();if(!Object.keys(n).length)return void e.$ok.shared?.toast?.info?.("No worker setting changes to save");const t=await fetch(this.basePath,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({config:n})}),i=await t.json().catch(()=>({}));if(!t.ok)throw new Error(a(i,`Failed (${t.status})`));const r=i?.data??i?.result??null;r&&(this.syncWorkerConfig(r),e.$dispatch("engine-updated",r)),e.$dispatch("refresh"),e.$ok.shared?.toast?.success?.("Worker settings saved")}catch(n){this.saveConfigError=n?.message||String(n),e.$ok.shared?.toast?.error?.(this.saveConfigError)}finally{this.saveConfigBusy=!1}}}}),init(e){this.syncWorkerConfig(e.$attr.engine),e.$watch(()=>e.$attr.engine,()=>this.syncWorkerConfig(e.$attr.engine))},template:'\n<engine-panel-shell\n :engine="eng"\n :env="envName"\n @engine-updated="$dispatch(\'engine-updated\', $event.detail)"\n @refresh="$dispatch(\'refresh\')"\n @deleted="$dispatch(\'deleted\', $event.detail)"\n>\n <if :="isQueueWorker">\n <div class="stack-compact">\n <div class="card stack-compact" style="padding:14px;gap:12px;">\n <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;">\n <div style="display:flex;flex-direction:column;gap:8px;min-width:0;flex:1;">\n <div class="pill-row">\n <span class="info-pill" title="Queue job type this worker consumes.">{{ workerJobType }}</span>\n <span class="info-pill" title="Environment whose queue is consumed.">queue: {{ workerQueueEnv }}</span>\n <span class="info-pill" title="Function environment.">fn env: {{ workerFunctionEnv }}</span>\n <if :="showWorkerReason">\n <span class="info-pill" title="Why the worker is in its current state.">{{ reasonText }}</span>\n </if>\n </div>\n <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0;">\n <span class="field-label" style="margin:0;">Function</span>\n <if :="canOpenWorkerFunction">\n <button @click="inspectWorkerFunction()" class="inline-link" title="View or edit this function in the Functions section.">\n ƒ {{ workerHandler?.name || \'—\' }} ↗\n </button>\n </if>\n <if :="!canOpenWorkerFunction">\n <span class="muted-code" style="color:var(--okdb-app-fg);">{{ workerHandler?.name || \'—\' }}</span>\n </if>\n </div>\n </div>\n\n <div class="metric-grid metric-grid--compact" style="min-width:min(100%, 360px);">\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Done</div>\n <div class="metric-value" style="color:var(--okdb-success);">{{ workerProcessed }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Error</div>\n <div class="metric-value" :style="\'color:\' + (workerFailed > 0 ? \'var(--okdb-danger)\' : \'var(--okdb-app-fg-strong)\')">{{ workerFailed }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Active</div>\n <div class="metric-value" :style="\'color:\' + (workerRunning > 0 ? \'var(--okdb-warning)\' : \'var(--okdb-app-fg-strong)\')">{{ workerRunning }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Concurrency</div>\n <div class="metric-value" style="color:var(--okdb-accent);">{{ workerConcurrency }}</div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="card stack-compact" style="padding:14px;gap:12px;">\n <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">\n <div class="field-label" style="margin:0;">Settings</div>\n <div style="flex:1;"></div>\n <span class="story-note" style="margin-right:4px;">updated {{ updatedText }}</span>\n <button @click="resetWorkerConfig()" :disabled="saveConfigBusy || !basePath" title="Discard unsaved changes." style="font-size:11px;padding:4px 10px;opacity:.75;">Reset</button>\n <button @click="saveWorkerConfig()" :disabled="saveConfigBusy || !basePath" title="Save and restart the worker with these values." style="font-size:11px;padding:4px 10px;color:var(--okdb-accent);border-color:color-mix(in srgb, var(--okdb-accent) 33%, transparent);">\n {{ saveConfigBusy ? \'Saving…\' : \'Save\' }}\n </button>\n </div>\n\n <div class="form-grid">\n <label class="field-stack">\n <span class="field-label" title="Parallel worker lanes.">Concurrency</span>\n <input class="field-input" type="number" min="1" :value="workerConfigDraft.concurrency" @input="workerConfigDraft.concurrency = $event.target.value" placeholder="1" />\n </label>\n <label class="field-stack">\n <span class="field-label" title="Milliseconds to wait before polling again when the queue is empty.">Poll</span>\n <input class="field-input" type="number" min="1" :value="workerConfigDraft.pollInterval" @input="workerConfigDraft.pollInterval = $event.target.value" placeholder="1000" />\n </label>\n <label class="field-stack">\n <span class="field-label" title="Milliseconds an in-flight claim stays reserved before it can be reclaimed.">TTL</span>\n <input class="field-input" type="number" min="1" :value="workerConfigDraft.ttl" @input="workerConfigDraft.ttl = $event.target.value" placeholder="30000" />\n </label>\n <label class="field-stack">\n <span class="field-label" title="Whether function runs are persisted.">Runs</span>\n <div class="field-input" style="display:flex;align-items:center;min-height:36px;background:var(--okdb-app-bg);">{{ workerRecordRuns }}</div>\n </label>\n </div>\n\n <if :="saveConfigError">\n <div class="alert alert--error">{{ saveConfigError }}</div>\n </if>\n </div>\n </div>\n </if>\n\n <if :="isProcessor">\n <div class="stack-compact">\n <div class="card stack-compact" style="padding:14px;gap:12px;">\n <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;">\n <div style="display:flex;flex-direction:column;gap:8px;min-width:0;flex:1;">\n <div class="pill-row">\n <span class="info-pill" title="Source type this processor watches.">{{ processorSourceType }}</span>\n <span class="info-pill" title="Environment whose changelog is consumed.">source: {{ processorSourceEnv }}</span>\n <span class="info-pill" title="Function environment.">fn env: {{ handlerEnv }}</span>\n <span class="info-pill" title="How source changes are consumed.">mode: {{ processorMode }}</span>\n <if :="processorPaused">\n <span class="info-pill" title="Processor registration is currently paused.">paused</span>\n </if>\n <if :="showWorkerReason">\n <span class="info-pill" title="Why the processor is in its current state.">{{ reasonText }}</span>\n </if>\n </div>\n <div class="story-lead">\n Watches <strong>{{ processorSourceType }}</strong> in <strong>{{ processorSourceEnv }}</strong> and runs\n <if :="canOpenHandlerFunction">\n <button @click="openHandlerFunction()" class="inline-link" title="View or edit this function in the Functions section.">ƒ {{ handlerName }} ↗</button>\n </if>\n <if :="!canOpenHandlerFunction">\n <span class="muted-code" style="color:var(--okdb-app-fg);">{{ handlerName || \'—\' }}</span>\n </if>\n for each change batch.\n </div>\n <div class="story-note">Use this engine for side effects and projections driven by change batches without owning a dedicated target type.</div>\n </div>\n\n <div class="metric-grid metric-grid--compact" style="min-width:min(100%, 420px);">\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Invocations</div>\n <div class="metric-value" style="color:var(--okdb-accent);">{{ processorInvocations }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Changes</div>\n <div class="metric-value" style="color:var(--okdb-success);">{{ processorProcessedChanges }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Failures</div>\n <div class="metric-value" :style="\'color:\' + (processorFailedBatches > 0 ? \'var(--okdb-danger)\' : \'var(--okdb-app-fg-strong)\')">{{ processorFailedBatches }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Lag</div>\n <div class="metric-value" :style="\'color:\' + (processorLag === 0 ? \'var(--okdb-success)\' : \'var(--okdb-warning)\')">{{ processorLag }}</div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="card stack-compact" style="padding:14px;gap:12px;">\n <div class="card-title" style="margin:0;">Processing settings</div>\n <div class="kv-grid kv-grid--compact">\n <span class="kv-key">bootstrap</span><span class="kv-val">{{ processorBootstrap }}</span>\n <span class="kv-key">origin mode</span><span class="kv-val">{{ processorOriginMode }}</span>\n <span class="kv-key">batch size</span><span class="kv-val">{{ processorBatchSize }}</span>\n <span class="kv-key">running batches</span><span class="kv-val">{{ processorRunningBatches }}</span>\n <span class="kv-key">progress</span><span class="kv-val">{{ processorProgress }}</span>\n <span class="kv-key">cursor key</span><span class="kv-val">{{ processorCursorKey }}</span>\n <span class="kv-key">lock mode</span><span class="kv-val">{{ processorLockMode }}</span>\n <span class="kv-key">hydrate values</span><span class="kv-val">{{ processorHydrateValues }}</span>\n <span class="kv-key">fail on fn error</span><span class="kv-val">{{ processorFailOnHandlerError }}</span>\n </div>\n </div>\n </div>\n </if>\n\n <if :="isMaterializer">\n <div class="stack-compact">\n <div class="card stack-compact" style="padding:14px;gap:12px;">\n <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;">\n <div style="display:flex;flex-direction:column;gap:8px;min-width:0;flex:1;">\n <div class="pill-row">\n <span class="info-pill" title="Source type this materializer watches.">{{ materializerSourceType }}</span>\n <span class="info-pill" title="Target type updated by returned ops.">target: {{ materializerTargetType }}</span>\n <span class="info-pill" title="Source environment.">source env: {{ materializerSourceEnv }}</span>\n <span class="info-pill" title="Target environment.">target env: {{ materializerTargetEnv }}</span>\n <span class="info-pill" title="Function environment.">fn env: {{ handlerEnv }}</span>\n <if :="materializerPaused">\n <span class="info-pill" title="Materializer registration is currently paused.">paused</span>\n </if>\n </div>\n <div class="story-lead">\n Projects <strong>{{ materializerSourceType }}</strong> into <strong>{{ materializerTargetType }}</strong> using\n <if :="canOpenHandlerFunction">\n <button @click="openHandlerFunction()" class="inline-link" title="View or edit this function in the Functions section.">ƒ {{ handlerName }} ↗</button>\n </if>\n <if :="!canOpenHandlerFunction">\n <span class="muted-code" style="color:var(--okdb-app-fg);">{{ handlerName || \'—\' }}</span>\n </if>\n and applies the returned ops to the target collection.\n </div>\n <div class="story-note">Use this engine when you want deterministic, rebuildable derived records instead of free-form side effects.</div>\n </div>\n\n <div class="metric-grid metric-grid--compact" style="min-width:min(100%, 420px);">\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Applied ops</div>\n <div class="metric-value" style="color:var(--okdb-success);">{{ materializerAppliedOps }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Target rows</div>\n <div class="metric-value" style="color:var(--okdb-accent);">{{ materializerTargetCount }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Changes</div>\n <div class="metric-value" style="color:var(--okdb-success);">{{ materializerProcessedChanges }}</div>\n </div>\n <div class="metric-card metric-card--compact">\n <div class="metric-title">Failures</div>\n <div class="metric-value" :style="\'color:\' + (materializerFailedBatches > 0 ? \'var(--okdb-danger)\' : \'var(--okdb-app-fg-strong)\')">{{ materializerFailedBatches }}</div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="card stack-compact" style="padding:14px;gap:12px;">\n <div class="card-title" style="margin:0;">Materialization settings</div>\n <div class="kv-grid kv-grid--compact">\n <span class="kv-key">bootstrap</span><span class="kv-val">{{ materializerBootstrap }}</span>\n <span class="kv-key">origin mode</span><span class="kv-val">{{ materializerOriginMode }}</span>\n <span class="kv-key">batch size</span><span class="kv-val">{{ materializerBatchSize }}</span>\n <span class="kv-key">running batches</span><span class="kv-val">{{ materializerRunningBatches }}</span>\n <span class="kv-key">progress</span><span class="kv-val">{{ materializerProgress }}</span>\n <span class="kv-key">lag</span><span class="kv-val">{{ materializerLag }}</span>\n <span class="kv-key">cursor key</span><span class="kv-val">{{ materializerCursorKey }}</span>\n <span class="kv-key">clear on rebuild</span><span class="kv-val">{{ materializerClearTargetOnRebuild }}</span>\n </div>\n </div>\n\n <div class="card stack-compact" style="padding:14px;gap:12px;border-color:#244057;">\n <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;">\n <div style="display:flex;flex-direction:column;gap:6px;min-width:0;flex:1;">\n <div class="card-title" style="margin:0;">Repair workflow</div>\n <div class="story-note" style="margin:0;">Edit the handler with seeded sample input, verify the draft run, then retry, rewind, or rebuild from here.</div>\n </div>\n <div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;">\n <button @click="openHandlerEditor()" :disabled="!canOpenHandlerFunction || materializerRepairBusy" style="font-size:12px;padding:4px 10px;color:var(--okdb-accent);border-color:color-mix(in srgb, var(--okdb-accent) 33%, transparent);">ƒ Edit handler</button>\n <button @click="retryMaterializer()" :disabled="materializerRepairBusy || !basePath" style="font-size:12px;padding:4px 10px;color:var(--okdb-orange);border-color:color-mix(in srgb, var(--okdb-orange) 33%, transparent);">↻ Retry</button>\n <button @click="resetMaterializerCursor()" :disabled="materializerRepairBusy || !basePath" style="font-size:12px;padding:4px 10px;color:var(--okdb-info);border-color:color-mix(in srgb, var(--okdb-info) 33%, transparent);">⇤ Reset cursor</button>\n <button @click="rebuildMaterializer()" :disabled="materializerRepairBusy || !basePath" style="font-size:12px;padding:4px 10px;color:var(--okdb-success);border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);">⟳ Rebuild</button>\n </div>\n </div>\n\n <if :="materializerErrorText">\n <div class="alert alert--error">\n <div style="font-weight:600;margin-bottom:4px;">Last materializer error</div>\n <div>{{ materializerErrorText }}</div>\n </div>\n </if>\n\n <if :="materializerRepairError">\n <div class="alert alert--error">{{ materializerRepairError }}</div>\n </if>\n\n <div style="display:grid;grid-template-columns:minmax(0,1fr) minmax(280px,.9fr);gap:12px;">\n <div style="display:flex;flex-direction:column;gap:8px;min-width:0;">\n <div class="field-label">Suggested <code>ctx.payload</code></div>\n <pre class="json-block" style="margin:0;max-height:220px;overflow:auto;">{{ formatJson(materializerPreviewPayload) }}</pre>\n </div>\n <div style="display:flex;flex-direction:column;gap:8px;min-width:0;">\n <div class="field-label">Suggested <code>ctx.materializer</code></div>\n <pre class="json-block" style="margin:0;max-height:220px;overflow:auto;">{{ formatJson(materializerPreviewContext) }}</pre>\n </div>\n </div>\n\n <div class="story-note" style="margin:0;">Recommended order: <strong>Edit handler</strong> → run the seeded draft preview → <strong>Retry</strong>. Use <strong>Reset cursor</strong> to replay without clearing the target, or <strong>Rebuild</strong> to clear and rematerialize from scratch.</div>\n </div>\n </div>\n </if>\n\n <if :="!hasSpecializedPipelineView">\n <div class="panel-grid panel-grid--two">\n <div class="card" style="display:flex;flex-direction:column;gap:12px;">\n <div class="card-title" style="margin:0;">Overview</div>\n <div class="kv-grid kv-grid--compact">\n <span class="kv-key">state</span><span class="kv-val">{{ stateText }}</span>\n <span class="kv-key">reason</span><span class="kv-val">{{ reasonText }}</span>\n <span class="kv-key">enabled</span><span class="kv-val">{{ eng.enabled === false ? \'no\' : \'yes\' }}</span>\n <span class="kv-key">updated</span><span class="kv-val">{{ updatedText }}</span>\n </div>\n <if :="runtimePreviewEntries.length > 0">\n <div style="display:flex;flex-direction:column;gap:8px;">\n <div class="field-label">Runtime details</div>\n <div class="kv-grid kv-grid--compact">\n <each :of="runtimePreviewEntries" let:item="entry">\n <span class="kv-key">{{ labelize(entry[0]) }}</span><span class="kv-val">{{ humanValue(entry[1]) }}</span>\n </each>\n </div>\n </div>\n </if>\n </div>\n\n <div class="card" style="display:flex;flex-direction:column;gap:12px;">\n <div class="card-title" style="margin:0;">Configuration</div>\n <if :="configPreviewEntries.length > 0">\n <div class="kv-grid kv-grid--compact">\n <each :of="configPreviewEntries" let:item="entry">\n <span class="kv-key">{{ labelize(entry[0]) }}</span><span class="kv-val">{{ humanValue(entry[1]) }}</span>\n </each>\n </div>\n </if>\n <if :="configPreviewEntries.length === 0">\n <div class="helper-text">This engine mostly uses structured config. Open the raw JSON below if you need the full payload.</div>\n </if>\n <if :="statsPreviewEntries.length > 0">\n <div style="display:flex;flex-direction:column;gap:8px;">\n <div class="field-label">Stats snapshot</div>\n <div class="kv-grid kv-grid--compact">\n <each :of="statsPreviewEntries" let:item="entry">\n <span class="kv-key">{{ labelize(entry[0]) }}</span><span class="kv-val">{{ humanValue(entry[1]) }}</span>\n </each>\n </div>\n </div>\n </if>\n </div>\n </div>\n </if>\n\n <div class="card" style="display:flex;flex-direction:column;gap:12px;">\n <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">\n <div class="card-title" style="margin:0;">Raw data</div>\n <div class="card-subtitle">Keep the default view visual first. Open these only when you need the underlying JSON.</div>\n </div>\n <div class="raw-toggle-row">\n <if :="eng.runtime">\n <button class="raw-toggle" :style="rawToggleStyle(showRawRuntime)" @click="showRawRuntime = !showRawRuntime">Runtime JSON</button>\n </if>\n <if :="eng.stats">\n <button class="raw-toggle" :style="rawToggleStyle(showRawStats)" @click="showRawStats = !showRawStats">Stats JSON</button>\n </if>\n <if :="eng.config">\n <button class="raw-toggle" :style="rawToggleStyle(showRawConfig)" @click="showRawConfig = !showRawConfig">Config JSON</button>\n </if>\n </div>\n\n <if :="showRawRuntime && eng.runtime">\n <div style="display:flex;flex-direction:column;gap:6px;">\n <div class="field-label">Runtime payload</div>\n <pre class="json-block">{{ formatJson(eng.runtime) }}</pre>\n </div>\n </if>\n <if :="showRawStats && eng.stats">\n <div style="display:flex;flex-direction:column;gap:6px;">\n <div class="field-label">Stats payload</div>\n <pre class="json-block">{{ formatJson(eng.stats) }}</pre>\n </div>\n </if>\n <if :="showRawConfig && eng.config">\n <div style="display:flex;flex-direction:column;gap:6px;">\n <div class="field-label">Config payload</div>\n <pre class="json-block">{{ formatJson(eng.config) }}</pre>\n </div>\n </if>\n </div>\n</engine-panel-shell>\n',style:t};
|