@kedem/okdb 1.8.18 → 1.9.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.
Files changed (39) hide show
  1. package/bin/okdb.js +1 -1
  2. package/docs/change-log.md +16 -16
  3. package/docs/embeddings.md +1 -1
  4. package/docs/functions.md +26 -25
  5. package/docs/http-cluster.md +157 -0
  6. package/docs/index.md +1 -0
  7. package/docs/manifest.json +5 -1
  8. package/docs/process-registry.md +185 -0
  9. package/docs/processors.md +61 -65
  10. package/docs/queue.md +23 -13
  11. package/docs/subscriptions.md +230 -0
  12. package/docs/worker-fleet.md +139 -0
  13. package/okdb-functions-runner-child.js +1 -1
  14. package/okdb-functions-sandbox-worker.js +1 -0
  15. package/okdb-http-worker-child.js +1 -0
  16. package/okdb-queue-load-handler.js +1 -0
  17. package/okdb-queue-spawn-child.js +1 -1
  18. package/okdb-views-bootstrap-worker.js +1 -1
  19. package/okdb-worker-child.js +1 -0
  20. package/okdb.js +1 -1
  21. package/package.json +1 -1
  22. package/public/sections/db/parts/db-overview.ok.js +1 -1
  23. package/public/sections/embeddings/parts/pipeline-create-panel.ok.js +1 -1
  24. package/public/sections/queue/parts/queue-jobs.ok.js +1 -1
  25. package/public/sections/system/index.ok.html +1 -1
  26. package/public/sections/system/modals/data-ops-modal.ok.js +1 -1
  27. package/public/sections/system/parts/system-process-panel.ok.js +1 -0
  28. package/public/sections/system/parts/system-processing-panel.ok.js +1 -0
  29. package/public/sections/system/parts/system-runtime-overview.ok.js +1 -0
  30. package/public/sections/system/parts/system-workers-panel.ok.js +1 -0
  31. package/types/environment.d.ts +21 -4
  32. package/types/features/embeddings.d.ts +11 -2
  33. package/types/features/queue.d.ts +27 -5
  34. package/types/features/views.d.ts +10 -10
  35. package/types/index.d.ts +54 -6
  36. package/types/options.d.ts +12 -0
  37. package/okdb-fts-handler.js +0 -1
  38. package/okdb-processor-worker.js +0 -1
  39. package/public/sections/processors/index.ok.html +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kedem/okdb",
3
- "version": "1.8.18",
3
+ "version": "1.9.0",
4
4
  "description": "A fast, type-oriented database — strong consistency and rich indexing at the core, with sync, vector embeddings, full-text search, and AI tooling built in. Designed for the AI era.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "engines": {
@@ -1 +1 @@
1
- import e from"../../../stores/events-store.js";import n from"../modals/demo-env-modal.ok.js";export default{tag:"db-overview",dependencies:[n],attr:{stats:{}},context:e=>({recentChanges:[],recentBusy:!1,recentError:null,pipelines:[],pipelinesBusy:!1,pipelinesError:null,violations:[],violationsBusy:!1,violationsError:null,expandedViolationType:null,tmStatus:null,tmBusy:!1,tmTypeBusy:new Set,ftsSize:null,ftsSizeBusy:!1,get info(){return e.$ok.stores.okdb.info},get types(){return(e.$ok.stores.okdb.types||[]).filter(e=>!e.type.startsWith("~")).sort((e,n)=>e.type.localeCompare(n.type))},get systemTypes(){return(e.$ok.stores.okdb.types||[]).filter(e=>e.type.startsWith("~"))},get envName(){return e.$ok.stores.okdb.selectedEnv||"default"},get totalRecords(){return(e.$ok.stores.okdb.types||[]).filter(e=>!e.type.startsWith("~")).reduce((e,n)=>e+(n.count||0),0)},get approxSize(){const e=this.info?.lastPageNumber,n=this.info?.pageSize;if(!e||!n)return"--";let t=e*n;const i=["B","KB","MB","GB","TB"];let o=0;for(;t>=1024&&o<i.length-1;)t/=1024,o++;return t.toFixed(2)+" "+i[o]},typeIndexCount(e){const n=this.info?.types?.[e]?.indexes;return n?Object.keys(n).length:0},typePages(e){return this.info?.types?.[e]?.data?.pages??null},typeBytes(e){const n=this.info?.types?.[e];if(!n)return null;const t=this.info?.pageSize||4096;let i=n.data?.pages||0;if(n.indexes)for(const e of Object.values(n.indexes))i+=e?.pages||0;return i*t},typeBytesTooltip(e){const n=this.info?.types?.[e];if(!n)return"";const t=this.info?.pageSize||4096,i=n.data?.pages||0;let o=0;const s=[];if(n.indexes)for(const[e,i]of Object.entries(n.indexes)){const n=i?.pages||0;o+=n,s.push(` · ${e}: ${this.fmtSizeBytes(n*t)}`)}const a=[`Data: ${this.fmtSizeBytes(i*t)}`];return s.length&&(a.push(`Indexes: ${this.fmtSizeBytes(o*t)}`),a.push(...s)),a.push("(Page-based estimate — includes LMDB B-tree overhead.)"),a.join("\n")},select(n){e.$dispatch("select",n)},async loadRecent(){this.recentBusy=!0,this.recentError=null;try{const n=await fetch(`${e.$ok.stores.okdb.apiBase}/changelog?limit=10`);if(n.ok){const e=await n.json();this.recentChanges=e.data??[]}}catch(e){this.recentError=e?.message||"Failed to load changes"}finally{this.recentBusy=!1}},actionColor:e=>({put:"var(--okdb-success)",remove:"var(--okdb-danger)",type_register:"var(--okdb-accent)",type_drop:"var(--okdb-danger)",index_register:"var(--okdb-purple)",index_drop:"var(--okdb-warning)"}[e]||"var(--okdb-app-fg-muted)"),actionLabel:e=>(e||"").replace(/_/g," "),relTime(e){if(!e)return"—";const n=Date.now()-e;return n<5e3?"just now":n<6e4?`${Math.round(n/1e3)}s ago`:n<36e5?`${Math.round(n/6e4)}m ago`:n<864e5?`${Math.round(n/36e5)}h ago`:new Date(e).toLocaleDateString()},fmt:e=>null==e?"--":Number(e).toLocaleString(),healthColor:e=>"online"===e?"var(--okdb-success)":"degraded"===e?"var(--okdb-warning)":"error"===e?"var(--okdb-danger)":"stopped"===e?"var(--okdb-app-fg-muted)":"var(--okdb-sky)",statusColor:e=>"running"===e||"online"===e?"var(--okdb-success)":"building"===e||"degraded"===e?"var(--okdb-warning)":"error"===e?"var(--okdb-danger)":"stopped"===e||"idle"===e?"var(--okdb-app-fg-muted)":"var(--okdb-sky)",pipelineKey(e){return`${e?.env||this.envName}::${e?.name||""}`},pipelineLabel:e=>e?.meta?.label||e?.name||"Unnamed pipeline",pipelineRoleSummary(e){const n=Array.isArray(e?.engines)?e.engines:[];if(0===n.length)return"No members";const t=[];for(const e of n){const n=e?.role||e?.type;if(n&&!t.includes(n)&&(t.push(n),t.length>=3))break}return t.join(" · ")},pipelineSeverity(e){const n=String(e?.health||"").toLowerCase(),t=String(e?.status||"").toLowerCase();return"error"===n||"error"===t?0:"degraded"===n||"degraded"===t||"building"===t?1:"stopped"===n||"stopped"===t||"idle"===t?2:"online"===n||"running"===t||"online"===t?3:4},get pipelineEngineCount(){const e=new Set;for(const n of this.pipelines)for(const t of n?.engines||[]){const i=t?.key||(t?.type&&t?.name?`${t.type}@${t.name}`:t?.name)||null;i&&e.add(`${n?.env||this.envName}::${i}`)}return e.size},get pipelineHealthCounts(){const e={online:0,degraded:0,error:0,stopped:0};for(const n of this.pipelines){const t=String(n?.health||"").toLowerCase();null!=e[t]&&(e[t]+=1)}return e},get pipelineSummaryText(){const e=this.pipelines.length,n=this.pipelineEngineCount;return`${e} pipeline${1===e?"":"s"} · ${n} engine${1===n?"":"s"}`},get pipelineHighlights(){return[...this.pipelines].sort((e,n)=>{const t=this.pipelineSeverity(e)-this.pipelineSeverity(n);return 0!==t?t:this.pipelineLabel(e).localeCompare(this.pipelineLabel(n))}).slice(0,4)},async loadPipelines(){this.pipelinesBusy=!0,this.pipelinesError=null;try{const n=await fetch(`${e.$ok.stores.okdb.apiBase}/pipelines`),t=await n.json().catch(()=>({}));if(!n.ok)throw new Error(t?.error?.message||"Failed to load pipelines");this.pipelines=t?.data??t?.result??[]}catch(e){this.pipelinesError=e?.message||"Failed to load pipelines",this.pipelines=[]}finally{this.pipelinesBusy=!1}},openPipelinesWorkspace(n=null){const t=e.$vars?.$router;if(!t?.resolve||!t?.push)return;const i={env:this.envName,mode:"pipelines"};n?.name&&(i.pipeline=n.name),t.push(t.resolve("/engines",i))},async openDemoModal(){await e.$ok.shared.modal.open({owner:e,title:'<span style="background:linear-gradient(135deg,var(--okdb-accent),var(--okdb-purple) 50%,var(--okdb-success));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;font-weight:700;font-size:15px;">✦ Demo Environment</span>',canDrag:!0,content:'<demo-env-modal :modal="modal" :on-done="onDone" />',context:n=>({modal:n,onDone:async n=>{e.$ok.stores.okdb.selectEnv(n),await e.$ok.stores.okdb.reloadEnvs(),e.$ok.shared?.toast?.success?.(`✦ Demo environment "${n}" is ready!`)}})})},_unsubs:[],_reloadTimer:null,_scheduleReload(){clearTimeout(this._reloadTimer),this._reloadTimer=setTimeout(()=>this.loadRecent(),600)},async _reloadTypes(){await Promise.all([e.$ok.stores.okdb.reloadTypes(),e.$ok.stores.okdb.reloadInfo()])},async loadViolations(){this.violationsBusy=!0,this.violationsError=null;try{const n=await fetch(`${e.$ok.stores.okdb.apiBase}/violations`);if(404===n.status)return void(this.violations=[]);if(!n.ok){const e=await n.json().catch(()=>({}));throw new Error(e?.error?.message||"Failed to load violations")}const t=await n.json();this.violations=t?.data?.violations??t?.result?.violations??[]}catch(e){this.violationsError=e?.message||"Failed to load violations",this.violations=[]}finally{this.violationsBusy=!1}},get violationsByType(){const e=new Map;for(const n of this.violations){const t=n.type||"?";e.has(t)||e.set(t,[]),e.get(t).push(n)}return[...e.entries()].map(([e,n])=>({type:e,count:n.length,items:n})).sort((e,n)=>n.count-e.count)},get totalViolations(){return this.violations.length},toggleViolationGroup(e){this.expandedViolationType=this.expandedViolationType===e?null:e},navigateToType(n){e.$dispatch("select",{type:n})},async loadFtsSize(){this.ftsSizeBusy=!0;try{const n=await fetch(`${e.$ok.stores.okdb.apiBase}/fts/size`);if(n.ok){const e=await n.json();this.ftsSize=e?.result??e?.data??null}else this.ftsSize=null}catch{this.ftsSize=null}finally{this.ftsSizeBusy=!1}},fmtSizeBytes(e){if(null==e)return"--";if(0===e)return"0";const n=["B","KB","MB","GB","TB"];let t=e,i=0;for(;t>=1024&&i<n.length-1;)t/=1024,i++;return t.toFixed(t>=100?0:t>=10?1:2)+" "+n[i]},get ftsSizeTooltip(){const e=this.ftsSize;return e?[`Disk file: ${this.fmtSizeBytes(e.diskBytes)}`,`Actual payload: ${this.fmtSizeBytes(e.payloadBytes)}`,` · indexes: ${this.fmtSizeBytes(e.indexPayloadBytes)}`,` · shared dicts: ${this.fmtSizeBytes(e.sharedDictBytes)}`,`Reclaimable slack: ${this.fmtSizeBytes(e.slackBytes)}`,`Indexes: ${(e.indexes||[]).length}`].join("\n"):""},async reloadOverviewData(){await Promise.all([this.loadRecent(),this.loadPipelines(),this.loadViolations(),this.loadFtsSize()])},async loadTMStatus(){try{const n=e.$ok.stores.okdb.selectedEnv||"default",t=await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/time-machine`);if(!t.ok)return void(this.tmStatus={enabled:!1,autoEnable:!1,types:[]});const i=await t.json();this.tmStatus=i.result??i.data??{enabled:!1,autoEnable:!1,types:[]}}catch{this.tmStatus={enabled:!1,autoEnable:!1,types:[]}}},async enableAllTM(){if(!this.tmBusy){this.tmBusy=!0;try{const n=e.$ok.stores.okdb.selectedEnv||"default";await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/time-machine/enable-all`,{method:"POST"}),await this.loadTMStatus()}catch{}this.tmBusy=!1}},async disableAllTM(){if(!this.tmBusy){this.tmBusy=!0;try{const n=e.$ok.stores.okdb.selectedEnv||"default";await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/time-machine/disable-all`,{method:"POST"}),await this.loadTMStatus()}catch{}this.tmBusy=!1}},async _toggleTMType(n,t){if(this.tmTypeBusy.has(n))return;this.tmTypeBusy.add(n),this.tmTypeBusy=new Set(this.tmTypeBusy);const i=this.tmStatus?.types?.find(e=>e.type===n),o=i?i.enabled:null;i&&(i.enabled="enable"===t);try{const s=e.$ok.stores.okdb.selectedEnv||"default";!(await fetch(`${__ENV__.API}/env/${encodeURIComponent(s)}/time-machine/types/${encodeURIComponent(n)}/${t}`,{method:"POST"})).ok&&i&&(i.enabled=o)}catch{i&&(i.enabled=o)}this.tmTypeBusy.delete(n),this.tmTypeBusy=new Set(this.tmTypeBusy),0===this.tmTypeBusy.size&&await this.loadTMStatus()},async enableTMType(e){return this._toggleTMType(e,"enable")},async disableTMType(e){return this._toggleTMType(e,"disable")}}),init(e){this.loadTMStatus(),e.$watch(()=>e.$ok.stores.okdb.selectedEnv,()=>{this.reloadOverviewData(),this.loadTMStatus()})},mount(){this.reloadOverviewData();const n=()=>this.loadPipelines();this._unsubs=[e.on("type:registered",()=>this._reloadTypes()),e.on("type:drop",()=>this._reloadTypes()),e.on("index:registered",()=>this._reloadTypes()),e.on("index:drop",()=>this._reloadTypes()),e.on("engine:started",n),e.on("engine:stopped",n),e.on("engine:error",n),e.on("engine:installed",n),e.on("engine:uninstalled",n),e.on("pipeline:start",n),e.on("pipeline:stop",n),e.on("pipeline:delete",n),e.on("item:create",()=>this._scheduleReload()),e.on("item:update",()=>this._scheduleReload()),e.on("item:remove",()=>this._scheduleReload()),e.on("sync:apply",()=>this._scheduleReload()),e.on("system:clock_change",()=>this._scheduleReload()),e.on("system:poke",()=>this._scheduleReload())]},unmount(){clearTimeout(this._reloadTimer);for(const e of this._unsubs)e();this._unsubs=[]},template:'\n<div style="display:flex; flex-direction:column; gap:20px; max-width:860px;">\n\n \x3c!-- Header --\x3e\n <div style="display:flex; align-items:center; gap:10px;">\n <div style="font-weight:600; font-size:18px;">Database Overview</div>\n <if :="recentBusy">\n <span style="font-size:12px; opacity:.4;">updating…</span>\n </if>\n <div style="margin-left:auto;">\n <button @click="loadRecent()" :disabled="recentBusy"\n style="font-size:12px; padding:5px 12px; opacity:.75;">\n ↻ Refresh\n </button>\n </div>\n </div>\n\n \x3c!-- Stat cards row --\x3e\n <div class="ov-stat-row">\n <div class="ov-stat-card">\n <div class="ov-stat-label">DB Size</div>\n <div class="ov-stat-value" style="color:var(--accent);">{{ approxSize }}</div>\n </div>\n <div class="ov-stat-card">\n <div class="ov-stat-label">Clock</div>\n <div class="ov-stat-value" style="font-family:monospace; font-size:18px;">{{ fmt(info?.clock) }}</div>\n </div>\n <div class="ov-stat-card">\n <div class="ov-stat-label">User Types</div>\n <div class="ov-stat-value" style="color:var(--okdb-purple);">{{ types.length }}</div>\n </div>\n <div class="ov-stat-card">\n <div class="ov-stat-label">Total Records</div>\n <div class="ov-stat-value" style="color:var(--okdb-success);">{{ fmt(totalRecords) }}</div>\n </div>\n <if :="ftsSize && ftsSize.indexes && ftsSize.indexes.length > 0">\n <div class="ov-stat-card" :title="ftsSizeTooltip">\n <div class="ov-stat-label">FTS Storage</div>\n <div class="ov-stat-value" style="color:var(--okdb-sky);">{{ fmtSizeBytes(ftsSize.payloadBytes) }}</div>\n <div style="font-size:10px; opacity:.5; margin-top:2px;">\n {{ ftsSize.indexes.length }} idx · disk {{ fmtSizeBytes(ftsSize.diskBytes) }}\n </div>\n </div>\n </if>\n </div>\n\n \x3c!-- Empty state --\x3e\n <if :="types.length === 0">\n <div style="display:flex; flex-direction:column; gap:14px;">\n\n \x3c!-- Standard empty hint --\x3e\n <div class="ov-empty">\n <div style="font-size:30px; margin-bottom:12px;">🗄️</div>\n <div style="font-weight:600; font-size:15px; margin-bottom:5px;">No types registered yet</div>\n <div style="font-size:13px; opacity:.6; line-height:1.7;">\n Use <b>Register Type +</b> in the sidebar to create your first collection.<br>\n Types are strongly-typed document collections stored in LMDB.\n </div>\n </div>\n\n \x3c!-- Demo data card --\x3e\n <div class="ov-demo-card" @click="openDemoModal()">\n <div class="ov-demo-card__aurora"></div>\n <div style="position:relative; z-index:1; display:flex; flex-direction:column; align-items:center; gap:10px;">\n <div class="ov-demo-card__sparkle">✦</div>\n <div class="ov-demo-card__title">Generate Demo Environment</div>\n <div class="ov-demo-card__desc">\n Instantly populate a new environment with realistic data, indexes,\n and full-text search — ready to explore in seconds.\n </div>\n <div class="ov-demo-card__badges">\n <span class="ov-demo-badge" style="color:var(--okdb-success); border-color:color-mix(in srgb, var(--okdb-success) 20%, transparent); background:color-mix(in srgb, var(--okdb-success) 7%, transparent);">🛒 E-Commerce</span>\n <span class="ov-demo-badge" style="color:var(--okdb-accent); border-color:color-mix(in srgb, var(--okdb-accent) 20%, transparent); background:color-mix(in srgb, var(--okdb-accent) 7%, transparent);">📊 Analytics</span>\n <span class="ov-demo-badge" style="color:var(--okdb-purple); border-color:color-mix(in srgb, var(--okdb-purple) 20%, transparent); background:color-mix(in srgb, var(--okdb-purple) 7%, transparent);">📝 Content Hub</span>\n <span class="ov-demo-badge" style="color:var(--okdb-warning); border-color:color-mix(in srgb, var(--okdb-warning) 20%, transparent); background:color-mix(in srgb, var(--okdb-warning) 7%, transparent);">👥 Team Directory</span>\n <span class="ov-demo-badge" style="color:var(--okdb-orange); border-color:color-mix(in srgb, var(--okdb-orange) 20%, transparent); background:color-mix(in srgb, var(--okdb-orange) 7%, transparent);">🗺️ City Explorer</span>\n </div>\n <div class="ov-demo-card__cta">\n <span>Choose a preset</span>\n <span style="margin-left:6px; font-size:14px;">→</span>\n </div>\n </div>\n </div>\n\n </div>\n </if>\n\n \x3c!-- Types table --\x3e\n <if :="types.length > 0">\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">Types</span>\n <span style="font-size:12px; opacity:.45; margin-left:auto;">\n {{ types.length }} user&nbsp;·&nbsp;{{ systemTypes.length }} system\n </span>\n </div>\n <table class="ov-table">\n <thead>\n <tr>\n <th>Name</th>\n <th style="text-align:right;">Records</th>\n <th style="text-align:right;">Size</th>\n <th style="text-align:right;">Indexes</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr as="repeat" :of="types" let:item="t" class="ov-row" @click="select(t)" title="Open in type viewer">\n <td>\n <span style="font-family:monospace; font-size:13px;">{{ t.type }}</span>\n </td>\n <td style="text-align:right; color:var(--accent); font-weight:600;">{{ fmt(t.count) }}</td>\n <td style="text-align:right; font-size:12px; font-family:monospace; opacity:.7;" :title="typeBytesTooltip(t.type)">{{ fmtSizeBytes(typeBytes(t.type)) }}</td>\n <td style="text-align:right; font-size:12px;">\n <if :="typeIndexCount(t.type) > 0">\n <span style="\n padding:1px 7px; border-radius:999px;\n border:1px solid color-mix(in srgb, var(--okdb-purple) 33%, transparent); color:var(--okdb-purple);\n font-size:11px;\n ">{{ typeIndexCount(t.type) }}</span>\n </if>\n <if :="typeIndexCount(t.type) === 0">\n <span style="opacity:.3;">—</span>\n </if>\n </td>\n <td style="text-align:right; opacity:.3; font-size:11px; padding-right:16px;">→</td>\n </tr>\n </tbody>\n </table>\n </div>\n </if>\n\n \x3c!-- Violations summary --\x3e\n <if :="totalViolations > 0 || violationsBusy || violationsError">\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">Schema Violations</span>\n <if :="totalViolations > 0">\n <span style="font-size:11px; padding:2px 10px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-danger) 33%, transparent); color:var(--okdb-danger); background:color-mix(in srgb, var(--okdb-danger) 5%, transparent); font-weight:600;">{{ totalViolations }}</span>\n </if>\n <span style="font-size:12px; opacity:.45; margin-left:auto;">across all types in {{ envName }}</span>\n <button @click="loadViolations()" :disabled="violationsBusy" style="font-size:12px; padding:5px 12px; opacity:.75;">↻</button>\n </div>\n <if :="violationsBusy && violations.length === 0">\n <div style="padding:14px 16px; font-size:12px; opacity:.45;">Loading…</div>\n </if>\n <if :="violationsError">\n <div style="padding:10px 16px; color:var(--okdb-danger); font-size:12px;">{{ violationsError }}</div>\n </if>\n <if :="!violationsBusy && !violationsError && violationsByType.length === 0">\n <div style="padding:14px 16px; font-size:12px; opacity:.4;">No violations.</div>\n </if>\n <if :="violationsByType.length > 0">\n <div>\n <each :of="violationsByType" let:item="grp">\n \x3c!-- type group header --\x3e\n <div class="ov-viol-group" @click="toggleViolationGroup(grp.type)">\n <span class="ov-viol-arrow" :style="expandedViolationType === grp.type ? \'transform:rotate(90deg)\' : \'\'">›</span>\n <span style="font-family:monospace; font-size:13px; flex:1;">{{ grp.type }}</span>\n <span style="font-size:11px; padding:2px 8px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-danger) 33%, transparent); color:var(--okdb-danger); background:color-mix(in srgb, var(--okdb-danger) 5%, transparent); margin-right:8px;">{{ grp.count }}</span>\n <button\n @click:stop="navigateToType(grp.type)"\n style="font-size:11px; padding:2px 8px; border-radius:5px; border:1px solid var(--okdb-app-border); background:transparent; color:var(--okdb-sky); cursor:pointer; opacity:.8;"\n title="Open Schema tab for this type"\n >Schema →</button>\n </div>\n \x3c!-- expanded violations for this type --\x3e\n <if :="expandedViolationType === grp.type">\n <div class="ov-viol-list">\n <each :of="grp.items" let:item="v">\n <div class="ov-viol-row">\n <span style="font-family:monospace; font-size:11px; color:var(--okdb-json-key); flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ v.key }}</span>\n <span style="font-size:11px; opacity:.45; white-space:nowrap; margin-left:8px;">{{ (v.errors ?? []).length || (v.errorCount ?? 0) }} error(s)</span>\n </div>\n </each>\n </div>\n </if>\n </each>\n </div>\n </if>\n </div>\n </if>\n\n \x3c!-- Time Machine card --\x3e\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">⏱ Time Machine</span>\n <if :="tmStatus && tmStatus.autoEnable">\n <span style="font-size:10px; padding:2px 8px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-success) 33%, transparent); color:var(--okdb-success); background:color-mix(in srgb, var(--okdb-success) 7%, transparent);">auto-enable</span>\n </if>\n <if :="!tmStatus">\n <span style="font-size:11px; opacity:.4; margin-left:auto;">loading…</span>\n </if>\n <if :="tmStatus">\n <div style="margin-left:auto; display:flex; gap:6px;">\n <button @click="loadTMStatus()" :disabled="tmBusy" style="font-size:11px; padding:2px 9px; opacity:.65;">↻</button>\n <button @click="enableAllTM()" :disabled="tmBusy" style="font-size:11px; padding:2px 10px; color:var(--okdb-success); border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);">{{ tmBusy ? \'…\' : \'Enable All\' }}</button>\n <button @click="disableAllTM()" :disabled="tmBusy" style="font-size:11px; padding:2px 10px; color:var(--okdb-danger); border-color:color-mix(in srgb, var(--okdb-danger) 33%, transparent);">{{ tmBusy ? \'…\' : \'Disable All\' }}</button>\n </div>\n </if>\n </div>\n <if :="tmStatus && tmStatus.types && tmStatus.types.length > 0">\n \x3c!-- CSS-grid rows instead of <table>: HTML\'s parser would hoist okjs\'s\n <each> directive out of <tbody> (only <tr> is valid there), breaking\n iteration. All other admin sections use this grid+each pattern. --\x3e\n <div style="display:flex; flex-direction:column; font-size:13px;">\n <div style="display:grid; grid-template-columns:1fr 80px 120px 100px; gap:8px; padding:6px 12px; font-size:11px; opacity:.55; border-bottom:1px solid var(--okdb-app-border);">\n <div>Type</div>\n <div style="text-align:right;">Heads</div>\n <div style="text-align:right;">Last Clock</div>\n <div></div>\n </div>\n <each :of="tmStatus.types" let:item="t">\n <div style="display:grid; grid-template-columns:1fr 80px 120px 100px; gap:8px; padding:6px 12px; border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05)); align-items:center;">\n <div>\n <span style="font-family:monospace;">{{ t.type }}</span>\n <if :="!t.enabled">\n <span style="font-size:10px; opacity:.4; margin-left:6px;">disabled</span>\n </if>\n </div>\n <div style="text-align:right; font-size:12px; font-family:monospace; opacity:.7;">{{ t.headCount ?? \'—\' }}</div>\n <div style="text-align:right; font-size:11px; font-family:monospace; opacity:.5;">{{ t.lastClock ?? \'—\' }}</div>\n <div style="text-align:right;">\n <if :="t.enabled">\n <button @click="disableTMType(t.type)" :disabled="tmTypeBusy.has(t.type)" style="font-size:10px; padding:1px 8px; color:var(--okdb-danger); border-color:color-mix(in srgb, var(--okdb-danger) 33%, transparent);">{{ tmTypeBusy.has(t.type) ? \'…\' : \'Disable\' }}</button>\n </if>\n <if :="!t.enabled">\n <button @click="enableTMType(t.type)" :disabled="tmTypeBusy.has(t.type)" style="font-size:10px; padding:1px 8px; color:var(--okdb-success); border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);">{{ tmTypeBusy.has(t.type) ? \'…\' : \'Enable\' }}</button>\n </if>\n </div>\n </div>\n </each>\n </div>\n </if>\n <if :="tmStatus && (!tmStatus.types || tmStatus.types.length === 0)">\n <div style="padding:14px 16px; font-size:12px; opacity:.4;">\n No types registered in this environment yet.\n </div>\n </if>\n </div>\n\n <div class="ov-card" style="overflow:visible;">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">Pipelines</span>\n <span style="font-size:12px; opacity:.45; margin-left:auto;">overview highlights for the current environment</span>\n </div>\n <div style="padding:16px; display:flex; flex-direction:column; gap:14px;">\n <div class="ov-pipelines-headline">\n <div style="display:flex; flex-direction:column; gap:6px; min-width:0; flex:1;">\n <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <div style="font-size:15px; font-weight:600; color:var(--okdb-app-fg);">{{ pipelineSummaryText }}</div>\n <if :="pipelineHealthCounts.error > 0">\n <span class="ov-pipeline-pill" :style="\'color:var(--okdb-danger);border-color:var(--okdb-danger-deep-border);background:#450a0a33;\'">{{ pipelineHealthCounts.error }} error</span>\n </if>\n <if :="pipelineHealthCounts.degraded > 0">\n <span class="ov-pipeline-pill" :style="\'color:#fcd34d;border-color:#854d0e;background:#451a0333;\'">{{ pipelineHealthCounts.degraded }} degraded</span>\n </if>\n <if :="pipelineHealthCounts.online > 0">\n <span class="ov-pipeline-pill" :style="\'color:#86efac;border-color:#166534;background:#052e1633;\'">{{ pipelineHealthCounts.online }} online</span>\n </if>\n </div>\n <div style="font-size:12px; color:var(--okdb-app-fg-muted); line-height:1.6; max-width:640px;">\n The overview should only surface high-signal pipeline highlights. Open the dedicated workspace when you want the full grouped pipeline list and member details.\n </div>\n </div>\n <div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center; justify-content:flex-end;">\n <button @click="loadPipelines()" :disabled="pipelinesBusy" style="font-size:12px; padding:5px 12px; opacity:.75;">↻ Refresh</button>\n <button @click="openPipelinesWorkspace()" style="font-size:12px; padding:5px 12px; border-color:color-mix(in srgb, var(--okdb-indigo) 33%, transparent); color:var(--okdb-indigo);">Open pipelines workspace →</button>\n </div>\n </div>\n\n <if :="pipelinesError">\n <div class="ov-pipelines-error">{{ pipelinesError }}</div>\n </if>\n\n <if :="pipelinesBusy && pipelines.length === 0 && !pipelinesError">\n <div style="font-size:12px; opacity:.45;">Loading pipeline highlights…</div>\n </if>\n\n <if :="!pipelinesBusy && pipelines.length === 0 && !pipelinesError">\n <div class="ov-pipelines-empty">\n <div style="font-size:24px; opacity:.3;">⛓</div>\n <div style="font-size:14px; font-weight:600; opacity:.78;">No pipelines in {{ envName }}</div>\n <div style="font-size:12px; opacity:.5; max-width:520px; line-height:1.7; text-align:center;">\n Pipelines will appear here once created or synced. This frontpage intentionally stays compact and only shows a small highlight list.\n </div>\n </div>\n </if>\n\n <if :="pipelineHighlights.length > 0">\n <div class="ov-pipelines-list">\n <each :of="pipelineHighlights" let:item="pipeline">\n <button class="ov-pipeline-highlight" @click="openPipelinesWorkspace(pipeline)">\n <div style="display:flex; align-items:center; gap:9px; min-width:0;">\n <span :style="\'width:8px;height:8px;border-radius:50%;flex-shrink:0;background:\' + healthColor(pipeline.health)"></span>\n <span style="font-size:13px; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ pipelineLabel(pipeline) }}</span>\n <span :style="\'font-size:11px;color:\' + healthColor(pipeline.health) + \';opacity:.95;white-space:nowrap;\'">{{ pipeline.health || \'unknown\' }}</span>\n </div>\n <div style="display:flex; gap:12px; flex-wrap:wrap; margin-top:6px; font-size:11px; color:var(--okdb-app-fg-muted);">\n <span><span style="opacity:.6;">Members:</span> <span style="color:var(--okdb-app-fg);">{{ fmt(pipeline.engines?.length || 0) }}</span></span>\n <if :="pipeline.status">\n <span><span style="opacity:.6;">Status:</span> <span :style="\'color:\' + statusColor(pipeline.status)">{{ pipeline.status }}</span></span>\n </if>\n <span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ pipelineRoleSummary(pipeline) }}</span>\n </div>\n </button>\n </each>\n </div>\n </if>\n\n <if :="pipelines.length > pipelineHighlights.length">\n <div style="font-size:11px; color:var(--okdb-app-fg-muted); opacity:.75;">\n Showing {{ pipelineHighlights.length }} of {{ pipelines.length }} pipelines. Open the dedicated workspace for the full inventory.\n </div>\n </if>\n </div>\n </div>\n\n \x3c!-- Recent activity --\x3e\n <if :="recentChanges.length > 0">\n <div class="ov-card">\n <div class="ov-card-head">\n <span style="font-weight:600; font-size:14px;">Recent Activity</span>\n <span style="font-size:12px; opacity:.4; margin-left:auto;">last {{ recentChanges.length }} changes</span>\n </div>\n <table style="width:100%; border-collapse:collapse; font-size:12px;">\n <tbody>\n <tr as="repeat" :of="recentChanges" let:item="ch" class="ov-row ov-row--sm">\n <td class="ov-ch-td" style="text-align:right; width:60px; font-family:monospace; opacity:.4; padding-left:16px;">\n {{ ch.clock }}\n </td>\n <td class="ov-ch-td" style="width:100px;">\n <span :style="\'font-size:10px; padding:2px 6px; border-radius:3px; border:1px solid; color:\'+actionColor(ch.action)+\'; border-color:\'+actionColor(ch.action)+\'55;\'">\n {{ actionLabel(ch.action) }}\n </span>\n </td>\n <td class="ov-ch-td" style="font-family:monospace; font-size:11px; opacity:.7; max-width:110px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">\n {{ ch.type || \'—\' }}\n </td>\n <td class="ov-ch-td" style="font-family:monospace; font-size:11px; opacity:.45; max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">\n {{ ch.key || \'\' }}\n </td>\n <td class="ov-ch-td" style="text-align:right; opacity:.35; white-space:nowrap; font-size:11px; padding-right:16px;">\n {{ relTime(ch.timestamp) }}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </if>\n\n</div>\n',style:"\n db-overview .ov-stat-row {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 12px;\n }\n db-overview .ov-stat-card {\n background: var(--okdb-app-surface);\n border: 1px solid var(--okdb-app-border);\n border-radius: 12px;\n padding: 16px 18px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n box-shadow: 0 2px 8px #0003;\n }\n db-overview .ov-stat-label {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: .06em;\n opacity: .5;\n }\n db-overview .ov-stat-value {\n font-size: 22px;\n font-weight: 700;\n line-height: 1;\n }\n db-overview .ov-card {\n background: var(--okdb-app-surface);\n border: 1px solid var(--okdb-app-border);\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 2px 8px #0003;\n }\n db-overview .ov-card-head {\n padding: 13px 16px;\n border-bottom: 1px solid var(--okdb-app-border);\n display: flex;\n align-items: center;\n gap: 10px;\n }\n db-overview .ov-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n }\n db-overview .ov-table th {\n padding: 7px 16px;\n text-align: left;\n font-weight: 500;\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: .05em;\n opacity: .4;\n border-bottom: 1px solid var(--okdb-app-border);\n background: var(--okdb-app-bg);\n }\n db-overview .ov-table td {\n padding: 9px 16px;\n border-bottom: 1px solid var(--okdb-app-surface-2);\n }\n db-overview .ov-row {\n cursor: pointer;\n transition: background .1s;\n }\n db-overview .ov-row:hover td {\n background: #ffffff07;\n }\n db-overview .ov-table tbody tr:last-child td {\n border-bottom: none;\n }\n db-overview .ov-ch-td {\n padding: 6px 8px;\n border-bottom: 1px solid var(--okdb-app-surface);\n }\n db-overview .ov-pipelines-headline {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n align-items: flex-start;\n flex-wrap: wrap;\n }\n db-overview .ov-pipeline-pill {\n font-size: 10px;\n padding: 2px 8px;\n border-radius: 999px;\n border: 1px solid;\n white-space: nowrap;\n }\n db-overview .ov-pipelines-error {\n color: var(--okdb-danger);\n font-size: 12px;\n padding: 10px 12px;\n border-radius: 10px;\n border: 1px solid color-mix(in srgb, var(--okdb-danger) 33%, transparent);\n background: color-mix(in srgb, var(--okdb-danger) 5%, transparent);\n }\n db-overview .ov-pipelines-empty {\n border: 1px dashed var(--okdb-app-border);\n border-radius: 12px;\n padding: 24px 20px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n align-items: center;\n }\n db-overview .ov-pipelines-list {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));\n gap: 10px;\n }\n db-overview .ov-pipeline-highlight {\n width: 100%;\n text-align: left;\n border-radius: 10px;\n border: 1px solid var(--okdb-app-border);\n background: var(--okdb-app-bg-deep);\n padding: 11px 12px;\n cursor: pointer;\n }\n db-overview .ov-pipeline-highlight:hover {\n border-color: var(--okdb-app-border-strong);\n background: var(--okdb-app-bg-deep);\n }\n db-overview tbody .ov-row--sm:last-child .ov-ch-td {\n border-bottom: none;\n }\n db-overview .ov-viol-group {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 9px 16px;\n border-bottom: 1px solid var(--okdb-app-surface-2);\n cursor: pointer;\n transition: background .1s;\n }\n db-overview .ov-viol-group:hover { background: #ffffff07; }\n db-overview .ov-viol-arrow {\n display: inline-block;\n font-size: 14px;\n opacity: .5;\n transition: transform .15s;\n width: 14px;\n flex-shrink: 0;\n }\n db-overview .ov-viol-list {\n background: var(--okdb-app-bg-deep);\n border-bottom: 1px solid var(--okdb-app-surface-2);\n }\n db-overview .ov-viol-row {\n display: flex;\n align-items: center;\n padding: 6px 16px 6px 38px;\n border-bottom: 1px solid var(--okdb-app-surface);\n font-size: 12px;\n }\n db-overview .ov-viol-row:last-child { border-bottom: none; }\n db-overview .ov-empty {\n border: 1px dashed var(--okdb-app-border);\n border-radius: 14px;\n padding: 30px 32px;\n text-align: center;\n opacity: .7;\n }\n /* ── Demo data card ── */\n db-overview .ov-demo-card {\n position: relative;\n border: 1px solid var(--okdb-app-border);\n border-radius: 16px;\n padding: 28px 24px 24px;\n text-align: center;\n cursor: pointer;\n overflow: hidden;\n transition: border-color .25s, box-shadow .25s, transform .2s;\n background: var(--okdb-app-bg-deep);\n }\n db-overview .ov-demo-card:hover {\n border-color: color-mix(in srgb, var(--okdb-accent) 53%, transparent);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--okdb-accent) 13%, transparent), 0 8px 40px color-mix(in srgb, var(--okdb-accent) 9%, transparent), 0 0 80px color-mix(in srgb, var(--okdb-purple) 4%, transparent);\n transform: translateY(-2px);\n }\n db-overview .ov-demo-card__aurora {\n position: absolute;\n inset: 0;\n background:\n radial-gradient(ellipse at 20% 50%, color-mix(in srgb, var(--okdb-accent) 4%, transparent), transparent 55%),\n radial-gradient(ellipse at 80% 30%, color-mix(in srgb, var(--okdb-purple) 4%, transparent), transparent 55%),\n radial-gradient(ellipse at 50% 90%, color-mix(in srgb, var(--okdb-success) 4%, transparent), transparent 55%);\n pointer-events: none;\n animation: ov-aurora 8s ease-in-out infinite alternate;\n }\n db-overview .ov-demo-card__sparkle {\n font-size: 26px;\n background: linear-gradient(135deg, var(--okdb-accent), var(--okdb-purple) 40%, var(--okdb-success));\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n animation: ov-hue 6s linear infinite;\n }\n db-overview .ov-demo-card__title {\n font-size: 17px;\n font-weight: 700;\n background: linear-gradient(135deg, var(--okdb-app-fg-strong), var(--okdb-accent));\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n }\n db-overview .ov-demo-card__desc {\n font-size: 13px;\n opacity: .55;\n max-width: 380px;\n line-height: 1.65;\n }\n db-overview .ov-demo-card__badges {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n justify-content: center;\n }\n db-overview .ov-demo-badge {\n padding: 3px 10px;\n border-radius: 999px;\n border: 1px solid;\n font-size: 11px;\n font-weight: 500;\n transition: transform .15s;\n }\n db-overview .ov-demo-card:hover .ov-demo-badge { transform: scale(1.05); }\n db-overview .ov-demo-card__cta {\n margin-top: 4px;\n font-size: 13px;\n font-weight: 600;\n color: var(--okdb-accent);\n opacity: .8;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: opacity .15s;\n }\n db-overview .ov-demo-card:hover .ov-demo-card__cta { opacity: 1; }\n @keyframes ov-hue {\n from { filter: hue-rotate(0deg); }\n to { filter: hue-rotate(360deg); }\n }\n @keyframes ov-aurora {\n from { opacity: .7; }\n to { opacity: 1.3; }\n }\n "};
1
+ 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&nbsp;·&nbsp;{{ 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
- import e from"../../../components/ok-js-input.ok.js";import n from"./embed-create-panel.ok.js";const t=[{symbol:"require",pattern:/(^|[^\w$.])require\s*\(/},{symbol:"import",pattern:/(^|[^\w$.])import\b/},{symbol:"console",pattern:/(^|[^\w$.])console\b/},{symbol:"module",pattern:/(^|[^\w$.])module\b/},{symbol:"exports",pattern:/(^|[^\w$.])exports\b/},{symbol:"process",pattern:/(^|[^\w$.])process\b/},{symbol:"child_process",pattern:/child_process/},{symbol:"worker_threads",pattern:/worker_threads/}];function i(e){const n="string"==typeof e?e.replace(/\r\n/g,"\n").trim():"";if(!n)return{ok:!1,message:"Source is required"};if((new TextEncoder).encode(n).length>65536)return{ok:!1,message:"Source exceeds 64 KB"};if(!/^(async\s+)?\(\s*[A-Za-z_$][\w$]*\s*\)\s*=>/.test(n))return{ok:!1,message:"Must be (ctx) => { … } or async (ctx) => { … }"};for(const{symbol:e,pattern:i}of t)if(i.test(n))return{ok:!1,message:`Cannot use "${e}"`};try{new Function(`return (${n});`)()}catch(e){return{ok:!1,message:e?.message||"Compile error"}}return{ok:!0,normalizedSource:n}}function a(e){return!0===e||""===e||1===e||"1"===e||"true"===e}export default{tag:"pipeline-create-panel",attr:{locktype:{},lockenv:{},initialkind:{},locktypestrict:{},hidequeueworker:{}},dependencies:[e,n],context:e=>({step:1,busy:!1,error:null,success:null,engineKind:"",proc_sourceType:"",mat_sourceType:"",mat_targetType:"",qw_jobType:"",qw_jobTypeMode:"existing",handlerMode:"existing",handlerName:"",newFnName:"",newFnSource:"",newFnValidation:{ok:!1,message:"Source is required"},newFnDryRunning:!1,newFnDryResult:null,newFnDryError:null,newFnDryPayload:null,showAdvanced:!1,proc_mode:"async",proc_bootstrap:"snapshot",proc_batchSize:256,proc_originMode:"all",mat_bootstrap:"snapshot",mat_batchSize:128,mat_originMode:"all",mat_clearTargetOnRebuild:!0,qw_concurrency:1,handler_recordRuns:"errors",types:[],functions:[],queueJobTypes:[],get lockedType(){return e.$attr.locktype||""},get lockedEnv(){return e.$attr.lockenv||""},get initialKind(){const n=e.$attr.initialkind||"";return this.availableKinds.includes(n)?n:""},get lockTypeStrict(){return a(e.$attr.locktypestrict)},get hideQueueWorker(){return a(e.$attr.hidequeueworker)},get availableKinds(){return["processor","materializer","queue-worker","embedding"].filter(e=>!(this.hideQueueWorker&&"queue-worker"===e))},get typeSelectionLocked(){return!!this.lockedType&&this.lockTypeStrict},get kindLocked(){return!!this.initialKind},get selectedEnv(){return this.lockedEnv||e.$ok.stores.okdb.selectedEnv||"default"},async loadMeta(){const e=this.selectedEnv;try{const[n,t,i]=await Promise.all([fetch(`${__ENV__.API}/env/${encodeURIComponent(e)}/types`),fetch(`${__ENV__.API}/env/${encodeURIComponent(e)}/functions`),fetch(`${__ENV__.API}/env/${encodeURIComponent(e)}/queue/stats`).catch(()=>null)]),a=await n.json().catch(()=>({})),s=await t.json().catch(()=>({})),r=a.data??a.result??[];this.types=(Array.isArray(r)?r.map(e=>e.type??e.name??e):Object.keys(r)).filter(e=>"string"==typeof e&&!e.startsWith("~")).sort();const o=s.data??s.result??[];if(this.functions=Array.isArray(o)?o.map(e=>e.key??e.name??e).filter(Boolean).sort():[],i&&i.ok){const e=await i.json().catch(()=>({})),n=e?.result??e?.data??null,t=n?.byType??{};this.queueJobTypes=Object.keys(t).sort()}else this.queueJobTypes=[]}catch{this.types=[],this.functions=[],this.queueJobTypes=[]}},get effectiveJobType(){return"custom"===this.qw_jobTypeMode?this.qw_jobType.trim():this.qw_jobType},get effectiveHandlerName(){return"new"===this.handlerMode?this.newFnName.trim():this.handlerName},get autoName(){return"processor"===this.engineKind&&this.proc_sourceType&&this.effectiveHandlerName?`${this.proc_sourceType}-${this.effectiveHandlerName}`:"materializer"===this.engineKind&&this.mat_sourceType&&this.mat_targetType&&this.effectiveHandlerName?`${this.mat_sourceType}-to-${this.mat_targetType}-${this.effectiveHandlerName}`:"queue-worker"===this.engineKind&&this.effectiveJobType&&this.effectiveHandlerName?`${this.effectiveJobType}-${this.effectiveHandlerName}`:""},get pipelineName(){return this.autoName},get engineName(){return this.autoName?`${this.autoName}:${this.engineKind}`:""},get step2Valid(){return"embedding"===this.engineKind||!("processor"===this.engineKind&&!this.proc_sourceType)&&!!("materializer"!==this.engineKind||this.mat_sourceType&&this.mat_targetType)&&!("queue-worker"===this.engineKind&&!this.effectiveJobType)&&("existing"===this.handlerMode?!!this.handlerName:!!this.newFnName.trim()&&this.newFnValidation.ok)},resetWizard(){this.step=this.kindLocked?2:1,this.engineKind=this.kindLocked?this.initialKind:"",this.proc_sourceType=this.lockedType||"",this.mat_sourceType=this.lockedType||"",this.mat_targetType="",this.qw_jobType="",this.qw_jobTypeMode="existing",this.handlerMode="existing",this.handlerName="",this.newFnName="",this.newFnSource="",this.newFnValidation={ok:!1,message:"Source is required"},this.newFnDryResult=null,this.newFnDryError=null,this.showAdvanced=!1,this.handler_recordRuns="errors",this.proc_mode="async",this.proc_bootstrap="snapshot",this.proc_batchSize=256,this.proc_originMode="all",this.mat_bootstrap="snapshot",this.mat_batchSize=128,this.mat_originMode="all",this.mat_clearTargetOnRebuild=!0,this.qw_concurrency=1},applyScopeDefaults(){this.typeSelectionLocked?(this.proc_sourceType=this.lockedType,this.mat_sourceType=this.lockedType):this.lockedType&&!this.proc_sourceType&&(this.proc_sourceType=this.lockedType),this.typeSelectionLocked||!this.lockedType||this.mat_sourceType||(this.mat_sourceType=this.lockedType),this.kindLocked&&this.engineKind!==this.initialKind&&this.pickType(this.initialKind)},get dedupedDryLogs(){const e=this.newFnDryResult?.logs;if(!Array.isArray(e)||!e.length)return[];const n=[],t=new Map;for(const i of e){const e=`${i.level??""}::${i.msg??i.message??JSON.stringify(i)}`;if(t.has(e))t.get(e).count++;else{const a={...i,count:1,_key:e};t.set(e,a),n.push(a)}}return n},get dryRunDuration(){const e=this.newFnDryResult;return e?e.execution?.durationMs??null:null},get dryRunStatus(){const e=this.newFnDryResult;return e?"error"===e.status||e.error?"error":e.status||"success":null},pickType(e){this.availableKinds.includes(e)&&(this.engineKind=e,this.step=2,this.typeSelectionLocked&&(this.proc_sourceType=this.lockedType,this.mat_sourceType=this.lockedType),"embedding"!==e&&(this.newFnSource||(this.newFnSource="processor"===e?'async (ctx) => {\n // ctx.payload — array of { key, value, action, clock }\n // ctx.processor — processor trigger metadata (only for processor engines)\n // ctx.processor.mode — "snapshot" | "live"\n // ctx.processor.sourceType — the type being watched\n // ctx.processor.sourceEnv — the environment of the source type\n // ctx.env — env facade: put, get, query, txn, ...\n // ctx.log — structured logger: ctx.log.info(msg, data)\n //\n // Example: materialised view\n // for (const change of ctx.payload) {\n // await ctx.env.put(\'derived\', change.key, { processed: true, ...change.value });\n // }\n\n return { processed: ctx.payload.length };\n}':"materializer"===e?"async (ctx) => {\n // ctx.payload — array of change records\n // change.action — \"put\" | \"remove\"\n // change.key — source record key\n // change.value — hydrated source value for puts\n // change.clock — log clock during live replay; null during snapshot bootstrap\n // ctx.materializer — materializer trigger metadata\n // ctx.materializer.sourceType — the type being watched\n // ctx.materializer.targetType — the derived target type\n // ctx.materializer.sourceEnv — the environment of the source type\n // ctx.materializer.targetEnv — the environment of the target type\n // ctx.env — env facade (read-only for deterministic materializers)\n // ctx.log — structured logger: ctx.log.info(msg, data)\n //\n // Return ops for the engine to apply to the target type.\n const ops = [];\n\n for (const change of ctx.payload) {\n if (change.action === 'put') {\n const doc = change.value ?? change.newValue;\n if (!doc) continue;\n\n ops.push({\n action: 'put',\n key: change.key,\n value: {\n projected: true,\n sourceKey: change.key,\n ...doc,\n },\n });\n }\n\n if (change.action === 'remove') {\n ops.push({ action: 'remove', key: change.key });\n }\n }\n\n return ops;\n}":"async (ctx) => {\n // ctx.payload — the raw job payload (whatever was enqueued)\n // ctx.job — queue job context (only for queue-worker pipelines)\n // ctx.job.id, ctx.job.type, ctx.job.tries, ctx.job.tags, ...\n // ctx.job.heartbeat() — keep the job alive\n // ctx.job.markProgress(msg) — report progress\n // ctx.env — env facade: put, get, query, txn, ...\n // ctx.log — structured logger: ctx.log.info(msg, data)\n //\n // Example: handle a queued task\n // const { id, action } = ctx.payload;\n // ctx.log.info('handling job', { id, action, attempt: ctx.job.tries });\n // await ctx.env.put('results', id, { done: true });\n\n return { ok: true };\n}",this.newFnValidation=i(this.newFnSource))))},goBack(){"embedding"!==this.engineKind?this.step>1&&this.step--:this.resetWizard()},goNext(){2===this.step&&this.step2Valid&&(this.step=3)},onEmbeddingCreated(n){this.success="Embedding pipeline created",this.error=null,this.resetWizard(),e.$el.dispatchEvent(new CustomEvent("created",{detail:n,bubbles:!0}))},updateSource(e){this.newFnSource=e??"",this.newFnValidation=i(this.newFnSource)},async buildDryRunPayload(){if("processor"===this.engineKind)return[{key:"test-1",value:{sample:!0,name:"Example record"},action:"put",clock:1}];if("materializer"===this.engineKind)return[{key:"test-1",value:{sample:!0,name:"Example record"},action:"put",clock:null},{key:"test-2",action:"remove",clock:2}];const e=this.effectiveJobType;if(e)try{const n=this.selectedEnv,t=`${__ENV__.API}/env/${encodeURIComponent(n)}/queue`,i=await fetch(`${t}/jobs?${new URLSearchParams({type:e,limit:"1"})}`);if(i.ok){const e=await i.json(),n=e?.result?.items??e?.data?.items??[],a=n[0]?.key??n[0]?.id??n[0]?.value?.id;if(a){const e=await fetch(`${t}/job/${encodeURIComponent(a)}`);if(e.ok){const n=await e.json(),t=n?.result??n?.data;if(void 0!==t?.payload)return t.payload}}}}catch{}return{}},async dryRunFunction(){if(this.newFnValidation.ok&&!this.newFnDryRunning){this.newFnDryRunning=!0,this.newFnDryResult=null,this.newFnDryError=null,this.newFnDryPayload=null;try{const e=this.selectedEnv,n=await this.buildDryRunPayload();this.newFnDryPayload=n;const t={name:this.newFnName.trim()||"__preview__",source:this.newFnValidation.normalizedSource||this.newFnSource.trim(),metadata:{description:"Pipeline handler preview"},enabled:!0,payload:n};"queue-worker"===this.engineKind&&(t.jobContext={id:"__preview-job__",type:this.effectiveJobType||"preview",tries:1,created:Date.now(),tags:null,priority:0,bucket:null,cron:null}),"processor"===this.engineKind&&(t.processorContext={mode:"live",sourceType:this.proc_sourceType||"example",sourceEnv:this.selectedEnv,processor:"__preview__",engineKey:"__preview__",cursorKey:null}),"materializer"===this.engineKind&&(t.materializerContext={mode:"live",sourceType:this.mat_sourceType||"example",targetType:this.mat_targetType||"example_projection",sourceEnv:this.selectedEnv,targetEnv:this.selectedEnv,engineKey:"__preview__",cursorKey:null});const i=await fetch(`${__ENV__.API}/env/${encodeURIComponent(e)}/functions/preview`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),a=await i.json().catch(()=>({})),s=a?.data??a?.result??a;if(!i.ok&&!s?.status)throw new Error(a?.error?.message||`Preview failed (${i.status})`);this.newFnDryResult=s}catch(e){this.newFnDryError=e?.message||String(e)}finally{this.newFnDryRunning=!1}}},buildEngineConfig(){const e={kind:"function",name:this.effectiveHandlerName};return"all"!==this.handler_recordRuns&&(e.recordRuns=this.handler_recordRuns),"processor"===this.engineKind?{source_type:this.proc_sourceType,source_env:this.selectedEnv,handler:e,mode:this.proc_mode,bootstrap:"inline"===this.proc_mode?null:this.proc_bootstrap,batchSize:Number(this.proc_batchSize)||256,originMode:this.proc_originMode}:"materializer"===this.engineKind?{source_type:this.mat_sourceType,target_type:this.mat_targetType,source_env:this.selectedEnv,target_env:this.selectedEnv,handler:e,bootstrap:this.mat_bootstrap,batchSize:Number(this.mat_batchSize)||128,originMode:this.mat_originMode,clearTargetOnRebuild:!1!==this.mat_clearTargetOnRebuild}:"queue-worker"===this.engineKind?{job_type:this.effectiveJobType,queue_env:this.selectedEnv,handler:e,concurrency:Number(this.qw_concurrency)||1}:{}},async submit(){if(this.step2Valid&&!this.busy){this.busy=!0,this.error=null,this.success=null;try{const n=this.selectedEnv;if("new"===this.handlerMode){const e={name:this.newFnName.trim(),source:this.newFnValidation.normalizedSource||this.newFnSource.trim(),metadata:{description:`Handler for pipeline ${this.pipelineName}`,owner_engine:this.engineName,pipeline:this.pipelineName},enabled:!0},t=await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/functions`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),i=await t.json().catch(()=>({}));if(!t.ok)throw new Error(i?.error?.message||`Failed to create function (${t.status})`)}const t={name:this.pipelineName,meta:{label:this.pipelineName},members:[{engineType:this.engineKind,engineName:this.engineName,role:this.engineKind,engineConfig:this.buildEngineConfig()}]},i=await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/pipelines/scaffold`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),a=await i.json().catch(()=>({}));if(!i.ok)throw new Error(a?.error?.message||`Failed (${i.status})`);this.success=`Pipeline "${this.pipelineName}" created and started`,e.$el.dispatchEvent(new CustomEvent("created",{detail:a.result??a.data,bubbles:!0})),this.resetWizard()}catch(e){this.error=e?.message||String(e)}finally{this.busy=!1}}},formatJson(e){try{return JSON.stringify(e,null,2)}catch{return String(e??"")}},setJobTypeMode(e){this.qw_jobTypeMode=e,this.qw_jobType=""},logLabel(e){const n=e?.msg??e?.message??"";return e?.count>1?`${n} (×${e.count})`:n},logColor(e){const n=e?.level??"";return"error"===n||"fatal"===n?"var(--okdb-danger)":"warn"===n?"var(--okdb-warning)":"debug"===n?"var(--okdb-app-fg-muted)":"var(--okdb-app-fg-subtle)"}}),async init(e){await this.loadMeta(),this.applyScopeDefaults(),e.$watch(()=>e.$ok.stores.okdb.selectedEnv,async()=>{await this.loadMeta(),this.applyScopeDefaults()})},style:"\n pipeline-create-panel {\n display: block;\n }\n pipeline-create-panel .pw-wrap {\n display: flex;\n flex-direction: column;\n gap: 18px;\n max-width: 820px;\n }\n pipeline-create-panel .pw-title {\n font-size: 18px;\n font-weight: 600;\n }\n pipeline-create-panel .pw-sub {\n font-size: 12px;\n opacity: .55;\n line-height: 1.7;\n max-width: 680px;\n }\n\n /* ── steps indicator ── */\n pipeline-create-panel .pw-steps {\n display: flex;\n align-items: center;\n gap: 0;\n font-size: 12px;\n margin-bottom: 4px;\n }\n pipeline-create-panel .pw-step-dot {\n width: 26px; height: 26px;\n border-radius: 50%;\n display: flex; align-items: center; justify-content: center;\n font-weight: 700; font-size: 12px;\n border: 2px solid var(--okdb-app-border);\n color: var(--okdb-app-fg-muted);\n background: transparent;\n flex-shrink: 0;\n }\n pipeline-create-panel .pw-step-dot--active {\n border-color: var(--okdb-accent);\n color: var(--okdb-accent);\n background: color-mix(in srgb, var(--okdb-accent) 7%, transparent);\n }\n pipeline-create-panel .pw-step-dot--done {\n border-color: var(--okdb-success);\n color: var(--okdb-success);\n background: color-mix(in srgb, var(--okdb-success) 7%, transparent);\n }\n pipeline-create-panel .pw-step-line {\n flex: 1;\n height: 2px;\n background: var(--okdb-app-border);\n margin: 0 6px;\n max-width: 60px;\n }\n pipeline-create-panel .pw-step-line--done {\n background: color-mix(in srgb, var(--okdb-success) 40%, transparent);\n }\n pipeline-create-panel .pw-step-label {\n margin-left: 8px;\n opacity: .45;\n }\n pipeline-create-panel .pw-step-label--active {\n opacity: 1;\n color: var(--okdb-accent);\n }\n\n /* ── type-choice cards ── */\n pipeline-create-panel .pw-choice-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\n gap: 14px;\n }\n pipeline-create-panel .pw-choice {\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-border);\n border-radius: 12px;\n padding: 20px 18px;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n gap: 8px;\n transition: border-color .15s, background .15s;\n }\n pipeline-create-panel .pw-choice:hover {\n border-color: color-mix(in srgb, var(--okdb-accent) 33%, transparent);\n background: var(--okdb-app-surface);\n }\n pipeline-create-panel .pw-choice-icon {\n font-size: 28px;\n opacity: .8;\n }\n pipeline-create-panel .pw-choice-name {\n font-size: 15px;\n font-weight: 600;\n }\n pipeline-create-panel .pw-choice-desc {\n font-size: 12px;\n opacity: .55;\n line-height: 1.7;\n }\n\n /* ── card / form ── */\n pipeline-create-panel .pw-card {\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-border);\n border-radius: 12px;\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n }\n pipeline-create-panel .pw-section {\n font-size: 11px;\n text-transform: uppercase;\n opacity: .45;\n letter-spacing: .06em;\n }\n pipeline-create-panel .pw-row {\n display: grid;\n grid-template-columns: 130px 1fr;\n gap: 8px 12px;\n align-items: center;\n font-size: 13px;\n }\n pipeline-create-panel .pw-label {\n opacity: .7;\n text-align: right;\n font-size: 12px;\n }\n pipeline-create-panel .pw-input {\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-border);\n border-radius: 6px;\n padding: 6px 8px;\n color: inherit;\n font-size: 13px;\n width: 100%;\n box-sizing: border-box;\n }\n pipeline-create-panel .pw-input:focus {\n outline: none;\n border-color: var(--okdb-accent);\n }\n pipeline-create-panel .pw-select {\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-border);\n border-radius: 6px;\n padding: 6px 8px;\n color: inherit;\n font-size: 13px;\n width: 100%;\n box-sizing: border-box;\n }\n pipeline-create-panel .pw-hint {\n font-size: 11px;\n opacity: .4;\n font-style: italic;\n grid-column: 2;\n line-height: 1.5;\n }\n pipeline-create-panel .pw-pills {\n display: flex;\n gap: 4px;\n }\n pipeline-create-panel .pw-pill {\n padding: 4px 12px;\n border-radius: 999px;\n font-size: 12px;\n border: 1px solid var(--okdb-app-border);\n background: transparent;\n cursor: pointer;\n color: inherit;\n opacity: .7;\n }\n pipeline-create-panel .pw-pill:hover {\n opacity: 1;\n border-color: color-mix(in srgb, var(--okdb-accent) 27%, transparent);\n }\n pipeline-create-panel .pw-pill--on {\n border-color: var(--okdb-accent);\n color: var(--okdb-accent);\n opacity: 1;\n background: color-mix(in srgb, var(--okdb-accent) 7%, transparent);\n }\n\n /* ── handler toggle ── */\n pipeline-create-panel .pw-handler-toggle {\n display: flex;\n gap: 0;\n border: 1px solid var(--okdb-app-border);\n border-radius: 8px;\n overflow: hidden;\n }\n pipeline-create-panel .pw-handler-toggle-btn {\n flex: 1;\n padding: 7px 14px;\n font-size: 12px;\n border: none;\n background: transparent;\n color: inherit;\n cursor: pointer;\n opacity: .6;\n text-align: center;\n }\n pipeline-create-panel .pw-handler-toggle-btn:first-child {\n border-right: 1px solid var(--okdb-app-border);\n }\n pipeline-create-panel .pw-handler-toggle-btn--on {\n opacity: 1;\n background: color-mix(in srgb, var(--okdb-accent) 7%, transparent);\n color: var(--okdb-accent);\n font-weight: 600;\n }\n\n /* ── code editor area ── */\n pipeline-create-panel .pw-editor-wrap {\n border: 1px solid var(--okdb-app-border);\n border-radius: 10px;\n overflow: hidden;\n background: var(--okdb-app-bg-deep);\n height: 260px;\n min-height: 260px;\n }\n\n /* ── dry-run result ── */\n pipeline-create-panel .pw-dry-card {\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-border);\n border-radius: 10px;\n padding: 12px 14px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n pipeline-create-panel .pw-dry-ok {\n color: var(--okdb-success);\n font-size: 12px;\n }\n pipeline-create-panel .pw-dry-err {\n color: var(--okdb-danger);\n font-size: 12px;\n }\n pipeline-create-panel .pw-dry-pre {\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n font-size: 11px;\n line-height: 1.5;\n color: var(--okdb-app-fg-subtle);\n max-height: 160px;\n overflow: auto;\n }\n pipeline-create-panel .pw-dry-meta {\n display: flex;\n gap: 12px;\n font-size: 11px;\n opacity: .5;\n margin-top: 2px;\n }\n\n /* ── dry-run logs ── */\n pipeline-create-panel .pw-dry-logs {\n display: flex;\n flex-direction: column;\n gap: 2px;\n margin-top: 4px;\n }\n pipeline-create-panel .pw-dry-log-entry {\n font-size: 11px;\n font-family: monospace;\n line-height: 1.5;\n padding: 2px 8px;\n border-radius: 4px;\n background: var(--okdb-app-bg-deep);\n }\n pipeline-create-panel .pw-dry-log-level {\n display: inline-block;\n min-width: 40px;\n font-weight: 600;\n text-transform: uppercase;\n font-size: 10px;\n margin-right: 6px;\n }\n\n /* ── review summary ── */\n pipeline-create-panel .pw-summary-row {\n display: flex;\n justify-content: space-between;\n font-size: 13px;\n padding: 6px 0;\n border-bottom: 1px solid var(--okdb-app-surface);\n }\n pipeline-create-panel .pw-summary-label {\n opacity: .55;\n }\n pipeline-create-panel .pw-summary-val {\n font-family: monospace;\n color: var(--okdb-accent);\n }\n\n /* ── buttons ── */\n pipeline-create-panel .pw-actions {\n display: flex;\n gap: 8px;\n align-items: center;\n }\n pipeline-create-panel .pw-btn {\n padding: 8px 20px;\n border-radius: 8px;\n border: 1px solid var(--okdb-accent);\n background: color-mix(in srgb, var(--okdb-accent) 9%, transparent);\n color: var(--okdb-accent);\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n }\n pipeline-create-panel .pw-btn:hover { background: color-mix(in srgb, var(--okdb-accent) 16%, transparent); }\n pipeline-create-panel .pw-btn:disabled { opacity: .4; cursor: default; }\n pipeline-create-panel .pw-btn--ghost {\n border-color: var(--okdb-app-border);\n background: transparent;\n color: inherit;\n font-weight: 400;\n opacity: .7;\n }\n pipeline-create-panel .pw-btn--ghost:hover { opacity: 1; background: var(--okdb-app-surface); }\n pipeline-create-panel .pw-btn--green {\n border-color: var(--okdb-success);\n background: color-mix(in srgb, var(--okdb-success) 9%, transparent);\n color: var(--okdb-success);\n }\n pipeline-create-panel .pw-btn--green:hover { background: color-mix(in srgb, var(--okdb-success) 16%, transparent); }\n pipeline-create-panel .pw-btn--small {\n padding: 5px 14px;\n font-size: 12px;\n font-weight: 500;\n }\n\n /* ── feedback ── */\n pipeline-create-panel .pw-error {\n color: var(--okdb-danger);\n font-size: 13px;\n padding: 8px 12px;\n border: 1px solid var(--okdb-danger);\n border-radius: 6px;\n }\n pipeline-create-panel .pw-success {\n color: var(--okdb-success);\n font-size: 13px;\n padding: 8px 12px;\n border: 1px solid color-mix(in srgb, var(--okdb-success) 33%, transparent);\n border-radius: 6px;\n background: color-mix(in srgb, var(--okdb-success) 5%, transparent);\n }\n pipeline-create-panel .pw-warn {\n font-size: 11px;\n color: var(--okdb-warning);\n opacity: .75;\n padding: 6px 10px;\n border: 1px solid color-mix(in srgb, var(--okdb-warning) 20%, transparent);\n border-radius: 6px;\n background: color-mix(in srgb, var(--okdb-warning) 4%, transparent);\n line-height: 1.5;\n }\n\n /* ── advanced toggle ── */\n pipeline-create-panel .pw-adv-toggle {\n font-size: 12px;\n opacity: .5;\n cursor: pointer;\n user-select: none;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n }\n pipeline-create-panel .pw-adv-toggle:hover { opacity: .8; }\n ",template:'\n<div class="pw-wrap">\n <div>\n <div class="pw-title">{{ engineKind === \'embedding\' || initialKind === \'embedding\' ? \'Create Embedding Pipeline\' : engineKind === \'materializer\' || initialKind === \'materializer\' ? \'Create Materializer Pipeline\' : \'Create Pipeline\' }}</div>\n <div class="pw-sub">\n Set up automated processing in <b>{{ selectedEnv }}</b>.\n A pipeline connects a data source to a handler function and runs it automatically.\n </div>\n </div>\n\n \x3c!-- ── Steps indicator ── --\x3e\n <if :="engineKind !== \'embedding\'">\n <div class="pw-steps">\n <div :class="step >= 1 ? (step > 1 ? \'pw-step-dot pw-step-dot--done\' : \'pw-step-dot pw-step-dot--active\') : \'pw-step-dot\'">\n {{ step > 1 ? \'✓\' : \'1\' }}\n </div>\n <span :class="step >= 1 ? \'pw-step-label pw-step-label--active\' : \'pw-step-label\'">Type</span>\n\n <div :class="step > 1 ? \'pw-step-line pw-step-line--done\' : \'pw-step-line\'"></div>\n\n <div :class="step >= 2 ? (step > 2 ? \'pw-step-dot pw-step-dot--done\' : \'pw-step-dot pw-step-dot--active\') : \'pw-step-dot\'">\n {{ step > 2 ? \'✓\' : \'2\' }}\n </div>\n <span :class="step >= 2 ? \'pw-step-label pw-step-label--active\' : \'pw-step-label\'">Configure</span>\n\n <div :class="step > 2 ? \'pw-step-line pw-step-line--done\' : \'pw-step-line\'"></div>\n\n <div :class="step >= 3 ? \'pw-step-dot pw-step-dot--active\' : \'pw-step-dot\'">3</div>\n <span :class="step >= 3 ? \'pw-step-label pw-step-label--active\' : \'pw-step-label\'">Review</span>\n </div>\n </if>\n\n \x3c!-- ════════════ STEP 1: Choose type ════════════ --\x3e\n <if :="step === 1 && !kindLocked">\n <div class="pw-choice-grid">\n <div class="pw-choice" @click="pickType(\'processor\')">\n <div class="pw-choice-icon">⚡</div>\n <div class="pw-choice-name">Change Processor</div>\n <div class="pw-choice-desc">\n Watches a data type for changes and runs your function for every batch.\n Use for projections, computed values, webhooks, or side-effects that should trigger\n whenever records are created, updated, or deleted.\n </div>\n </div>\n <div class="pw-choice" @click="pickType(\'materializer\')">\n <div class="pw-choice-icon">⇄</div>\n <div class="pw-choice-name">Materializer</div>\n <div class="pw-choice-desc">\n Watches a source type and maintains a deterministic derived target type by returning ops for the engine to apply.\n Use for rebuildable projections, mirrors, and materialized views.\n </div>\n </div>\n <if :="!hideQueueWorker">\n <div class="pw-choice" @click="pickType(\'queue-worker\')">\n <div class="pw-choice-icon">⇶</div>\n <div class="pw-choice-name">Queue Worker</div>\n <div class="pw-choice-desc">\n Processes jobs from a queue. Use for background tasks like sending emails,\n generating reports, or any work that can be enqueued and processed asynchronously\n with automatic retries.\n </div>\n </div>\n </if>\n <div class="pw-choice" @click="pickType(\'embedding\')">\n <div class="pw-choice-icon">⊛</div>\n <div class="pw-choice-name">Embedding Pipeline</div>\n <div class="pw-choice-desc">\n Creates an embedder, indexer, search engine, and optional worker as one grouped pipeline.\n Use this for semantic search, RAG, and other vector workflows.\n </div>\n </div>\n </div>\n </if>\n\n \x3c!-- ════════════ STEP 2: Configure ════════════ --\x3e\n <if :="step === 2">\n <if :="engineKind === \'embedding\'">\n <div class="pw-card">\n <div class="pw-section">Embeddings</div>\n <div style="font-size:12px; opacity:.58; line-height:1.7; margin-bottom:2px;">\n Embedding pipelines use the specialized creator because they can bundle multiple engines and optional search/worker settings in one pass.\n </div>\n <div class="pw-actions" style="margin-top:4px;" :style="kindLocked ? \'display:none;\' : \'\'">\n <button class="pw-btn pw-btn--ghost" @click="goBack()">← Back</button>\n </div>\n </div>\n <embed-create-panel initialtab="pipeline" :locktype="lockedType" :lockenv="selectedEnv" @created="onEmbeddingCreated($event.detail)" />\n </if>\n\n <if :="engineKind !== \'embedding\'">\n \x3c!-- Source / job type --\x3e\n <div class="pw-card">\n <div class="pw-section">\n {{ engineKind === \'queue-worker\' ? \'What to process\' : \'What to watch\' }}\n </div>\n\n <if :="engineKind === \'processor\'">\n <if :="typeSelectionLocked">\n <div class="pw-row">\n <span class="pw-label">Data type</span>\n <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <span style="display:inline-flex; align-items:center; gap:6px; padding:5px 10px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-accent) 33%, transparent); color:var(--okdb-accent); background:color-mix(in srgb, var(--okdb-accent) 6%, transparent); font-size:12px;">{{ proc_sourceType }}</span>\n <span style="font-size:11px; opacity:.45;">Locked to the current DB type</span>\n </div>\n </div>\n </if>\n <if :="!typeSelectionLocked">\n <div class="pw-row">\n <span class="pw-label">Data type</span>\n <select class="pw-select" :value="proc_sourceType" @change="proc_sourceType = $event.target.value">\n <option value="">— select a type —</option>\n <each :of="types" let:item="t">\n <option :value="t">{{ t }}</option>\n </each>\n </select>\n </div>\n </if>\n <div class="pw-hint">\n The processor will receive a batch of changes every time records of this type are created, updated, or deleted.\n </div>\n </if>\n\n <if :="engineKind === \'materializer\'">\n <if :="typeSelectionLocked">\n <div class="pw-row">\n <span class="pw-label">Source type</span>\n <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <span style="display:inline-flex; align-items:center; gap:6px; padding:5px 10px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-accent) 33%, transparent); color:var(--okdb-accent); background:color-mix(in srgb, var(--okdb-accent) 6%, transparent); font-size:12px;">{{ mat_sourceType }}</span>\n <span style="font-size:11px; opacity:.45;">Locked to the current DB type</span>\n </div>\n </div>\n </if>\n <if :="!typeSelectionLocked">\n <div class="pw-row">\n <span class="pw-label">Source type</span>\n <select class="pw-select" :value="mat_sourceType" @change="mat_sourceType = $event.target.value">\n <option value="">— select a type —</option>\n <each :of="types" let:item="t">\n <option :value="t">{{ t }}</option>\n </each>\n </select>\n </div>\n </if>\n <div class="pw-row">\n <span class="pw-label">Target type</span>\n <input class="pw-input" type="text" placeholder="e.g. order_summaries"\n :value="mat_targetType" @input="mat_targetType = $event.target.value" />\n </div>\n <div class="pw-hint">\n The materializer watches the source type and applies returned ops to the target type, making rebuilds deterministic.\n </div>\n </if>\n\n <if :="engineKind === \'queue-worker\'">\n <div class="pw-row">\n <span class="pw-label">Job type</span>\n <div style="display:flex; flex-direction:column; gap:6px;">\n <if :="queueJobTypes.length > 0">\n <div class="pw-handler-toggle" style="max-width:300px;">\n <button\n :class="qw_jobTypeMode === \'existing\' ? \'pw-handler-toggle-btn pw-handler-toggle-btn--on\' : \'pw-handler-toggle-btn\'"\n @click="setJobTypeMode(\'existing\')"\n >Known type</button>\n <button\n :class="qw_jobTypeMode === \'custom\' ? \'pw-handler-toggle-btn pw-handler-toggle-btn--on\' : \'pw-handler-toggle-btn\'"\n @click="setJobTypeMode(\'custom\')"\n >Custom</button>\n </div>\n </if>\n <if :="qw_jobTypeMode === \'existing\' && queueJobTypes.length > 0">\n <select class="pw-select" :value="qw_jobType" @change="qw_jobType = $event.target.value">\n <option value="">— select job type —</option>\n <each :of="queueJobTypes" let:item="jt">\n <option :value="jt">{{ jt }}</option>\n </each>\n </select>\n </if>\n <if :="qw_jobTypeMode === \'custom\' || queueJobTypes.length === 0">\n <input class="pw-input" type="text" placeholder="e.g. email.send"\n :value="qw_jobType" @input="qw_jobType = $event.target.value" />\n </if>\n </div>\n </div>\n <div class="pw-hint" style="grid-column: 1 / -1;">\n Jobs enqueued with this type will be routed to this worker. You can enqueue jobs from functions, the API, or other pipelines.\n </div>\n </if>\n </div>\n\n \x3c!-- Handler function --\x3e\n <div class="pw-card">\n <div class="pw-section">Handler function</div>\n <div style="font-size:12px; opacity:.5; line-height:1.6; margin-bottom:4px;">\n The function that runs for each\n {{ engineKind === \'queue-worker\' ? \'job\' : \'batch of changes\' }}.\n Pick an existing function or write one now.\n </div>\n\n <div class="pw-handler-toggle">\n <button\n :class="handlerMode === \'existing\' ? \'pw-handler-toggle-btn pw-handler-toggle-btn--on\' : \'pw-handler-toggle-btn\'"\n @click="handlerMode = \'existing\'"\n >Use existing function</button>\n <button\n :class="handlerMode === \'new\' ? \'pw-handler-toggle-btn pw-handler-toggle-btn--on\' : \'pw-handler-toggle-btn\'"\n @click="handlerMode = \'new\'"\n >Write new function</button>\n </div>\n\n \x3c!-- Existing function picker --\x3e\n <if :="handlerMode === \'existing\'">\n <div class="pw-row">\n <span class="pw-label">Function</span>\n <select class="pw-select" :value="handlerName" @change="handlerName = $event.target.value">\n <option value="">— select function —</option>\n <each :of="functions" let:item="fn">\n <option :value="fn">{{ fn }}</option>\n </each>\n </select>\n </div>\n <if :="functions.length === 0">\n <div class="pw-hint" style="grid-column: 1 / -1;">\n No functions found in <b>{{ selectedEnv }}</b>.\n Switch to "Write new function" to create one inline.\n </div>\n </if>\n </if>\n\n \x3c!-- New function inline authoring --\x3e\n <if :="handlerMode === \'new\'">\n <div class="pw-row">\n <span class="pw-label">Function name</span>\n <input class="pw-input" type="text"\n placeholder="e.g. processArticle"\n :value="newFnName"\n @input="newFnName = $event.target.value" />\n </div>\n\n <div style="font-size:11px; text-transform:uppercase; letter-spacing:.06em; opacity:.45; margin-top:4px;">\n Implementation\n </div>\n <div class="pw-editor-wrap">\n <ok-js-input\n style="height:100%;"\n :value="newFnSource"\n @edit="updateSource($event.detail)"\n ></ok-js-input>\n </div>\n\n \x3c!-- Validation --\x3e\n <if :="newFnValidation.ok">\n <div style="font-size:12px; color:var(--okdb-success);">✓ Valid function source</div>\n </if>\n <if :="!newFnValidation.ok && newFnSource">\n <div style="font-size:12px; color:var(--okdb-danger);">{{ newFnValidation.message }}</div>\n </if>\n\n \x3c!-- Dry run --\x3e\n <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <button class="pw-btn pw-btn--small pw-btn--ghost"\n :disabled="!newFnValidation.ok || newFnDryRunning"\n @click="dryRunFunction()">\n {{ newFnDryRunning ? \'Running…\' : \'▶ Test with sample data\' }}\n </button>\n <span style="font-size:11px; opacity:.4;">\n {{ engineKind === \'queue-worker\' && effectiveJobType\n ? \'Uses a real job payload from the queue if available.\'\n : \'Runs the function draft against synthetic test data without saving it.\' }}\n </span>\n </div>\n <div class="pw-warn">\n ⚠ <strong>Test with sample data</strong> runs your function for real — the function itself is not saved,\n but any side-effects (e.g.&nbsp;<code>ctx.env.put</code>) <em>will</em> write to live data.\n </div>\n\n \x3c!-- Dry-run result --\x3e\n <if :="newFnDryResult">\n <div class="pw-dry-card">\n <div :class="dryRunStatus === \'error\' ? \'pw-dry-err\' : \'pw-dry-ok\'">\n {{ dryRunStatus === \'error\' ? \'✕ Function returned an error\' : \'✓ Function executed successfully\' }}\n </div>\n <div class="pw-dry-meta">\n <if :="dryRunDuration != null">\n <span>{{ dryRunDuration }}ms</span>\n </if>\n <span>status: {{ newFnDryResult.status || \'ok\' }}</span>\n </div>\n <if :="dryRunStatus === \'error\'">\n <pre class="pw-dry-pre" style="color:var(--okdb-danger);">{{ formatJson(newFnDryResult.error ?? newFnDryResult.result?.value ?? newFnDryResult) }}</pre>\n </if>\n <if :="dryRunStatus !== \'error\'">\n <pre class="pw-dry-pre">{{ formatJson(newFnDryResult.result?.value ?? newFnDryResult) }}</pre>\n </if>\n\n \x3c!-- Dry-run logs --\x3e\n <if :="dedupedDryLogs.length > 0">\n <div style="font-size:10px; text-transform:uppercase; letter-spacing:.06em; opacity:.4; margin-top:6px;">\n Logs ({{ newFnDryResult.logs.length }})\n </div>\n <div class="pw-dry-logs">\n <each :of="dedupedDryLogs" let:item="entry">\n <div class="pw-dry-log-entry">\n <span class="pw-dry-log-level" :style="\'color:\' + logColor(entry)">{{ entry.level || \'log\' }}</span>\n <span>{{ logLabel(entry) }}</span>\n </div>\n </each>\n </div>\n </if>\n\n \x3c!-- Input payload used --\x3e\n <if :="newFnDryPayload != null">\n <details style="margin-top:6px;">\n <summary style="font-size:10px; text-transform:uppercase; letter-spacing:.06em; opacity:.4; cursor:pointer; user-select:none;">\n Input payload\n </summary>\n <pre class="pw-dry-pre" style="margin-top:4px; max-height:180px;">{{ formatJson(newFnDryPayload) }}</pre>\n </details>\n </if>\n </div>\n </if>\n <if :="newFnDryError">\n <div class="pw-dry-card">\n <div class="pw-dry-err">✕ {{ newFnDryError }}</div>\n </div>\n </if>\n </if>\n </div>\n\n \x3c!-- Advanced options (collapsed) --\x3e\n <div>\n <span class="pw-adv-toggle" @click="showAdvanced = !showAdvanced">\n <span>{{ showAdvanced ? \'▾\' : \'▸\' }}</span>\n <span>Advanced options</span>\n </span>\n </div>\n <if :="showAdvanced">\n <div class="pw-card">\n <if :="engineKind === \'processor\'">\n <div class="pw-row">\n <span class="pw-label">Mode</span>\n <div class="pw-pills">\n <button :class="proc_mode === \'async\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_mode = \'async\'">async</button>\n <button :class="proc_mode === \'inline\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_mode = \'inline\'">inline</button>\n </div>\n </div>\n <div class="pw-hint">\n <b>async</b> processes changes in background batches (recommended).\n <b>inline</b> runs synchronously inside the write path.\n </div>\n <if :="proc_mode === \'async\'">\n <div class="pw-row">\n <span class="pw-label">Bootstrap</span>\n <div class="pw-pills">\n <button :class="proc_bootstrap === \'snapshot\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_bootstrap = \'snapshot\'">snapshot</button>\n <button :class="proc_bootstrap === \'log\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_bootstrap = \'log\'">log</button>\n </div>\n </div>\n <div class="pw-hint">\n <b>snapshot</b> processes all existing records first, then switches to live.\n <b>log</b> replays the change log from the beginning.\n </div>\n </if>\n <div class="pw-row">\n <span class="pw-label">Batch size</span>\n <input class="pw-input" type="number" min="1" max="10000"\n :value="proc_batchSize" @input="proc_batchSize = $event.target.value" />\n </div>\n <div class="pw-row">\n <span class="pw-label">Origin mode</span>\n <div class="pw-pills">\n <button :class="proc_originMode === \'all\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_originMode = \'all\'">all</button>\n <button :class="proc_originMode === \'self\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_originMode = \'self\'">self</button>\n <button :class="proc_originMode === \'remote\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_originMode = \'remote\'">remote</button>\n </div>\n </div>\n <div class="pw-hint">\n <b>all</b> = local + synced changes. <b>self</b> = local only. <b>remote</b> = synced only.\n </div>\n </if>\n <if :="engineKind === \'materializer\'">\n <div class="pw-row">\n <span class="pw-label">Bootstrap</span>\n <div class="pw-pills">\n <button :class="mat_bootstrap === \'snapshot\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="mat_bootstrap = \'snapshot\'">snapshot</button>\n <button :class="mat_bootstrap === \'log\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="mat_bootstrap = \'log\'">log</button>\n </div>\n </div>\n <div class="pw-hint">\n <b>snapshot</b> replays all current source rows first. <b>log</b> replays the full change log.\n </div>\n <div class="pw-row">\n <span class="pw-label">Batch size</span>\n <input class="pw-input" type="number" min="1" max="10000"\n :value="mat_batchSize" @input="mat_batchSize = $event.target.value" />\n </div>\n <div class="pw-row">\n <span class="pw-label">Origin mode</span>\n <div class="pw-pills">\n <button :class="mat_originMode === \'all\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="mat_originMode = \'all\'">all</button>\n <button :class="mat_originMode === \'self\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="mat_originMode = \'self\'">self</button>\n <button :class="mat_originMode === \'remote\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="mat_originMode = \'remote\'">remote</button>\n </div>\n </div>\n <div class="pw-hint">\n Choose whether the materializer reacts to local changes, synced changes, or both.\n </div>\n <div class="pw-row">\n <span class="pw-label">Rebuild</span>\n <label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--okdb-app-fg);">\n <input type="checkbox" :checked="mat_clearTargetOnRebuild" @change="mat_clearTargetOnRebuild = $event.target.checked" />\n Clear target before rebuild\n </label>\n </div>\n </if>\n <if :="engineKind === \'queue-worker\'">\n <div class="pw-row">\n <span class="pw-label">Concurrency</span>\n <input class="pw-input" type="number" min="1" max="64"\n :value="qw_concurrency" @input="qw_concurrency = $event.target.value" />\n </div>\n <div class="pw-hint">How many jobs to process in parallel.</div>\n </if>\n\n \x3c!-- Run recording (shared by both engine types) --\x3e\n <div style="border-top:1px solid var(--okdb-app-border); margin-top:4px; padding-top:12px;">\n <div class="pw-row">\n <span class="pw-label">Run recording</span>\n <div class="pw-pills">\n <button :class="handler_recordRuns === \'all\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="handler_recordRuns = \'all\'">all runs</button>\n <button :class="handler_recordRuns === \'errors\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="handler_recordRuns = \'errors\'">errors only</button>\n <button :class="handler_recordRuns === \'none\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="handler_recordRuns = \'none\'">none</button>\n </div>\n </div>\n <div class="pw-hint">\n Controls whether each function invocation is recorded in the run ledger.\n <b>errors only</b> (recommended) avoids creating hundreds of run records for busy pipelines while still capturing failures.\n <b>none</b> fully suppresses recording — useful for high-throughput processors where logging isn\'t needed.\n </div>\n </div>\n </div>\n </if>\n\n \x3c!-- Navigation --\x3e\n <div class="pw-actions">\n <button class="pw-btn pw-btn--ghost" @click="goBack()">← Back</button>\n <button class="pw-btn" :disabled="!step2Valid" @click="goNext()">Review →</button>\n </div>\n </if>\n </if>\n\n \x3c!-- ════════════ STEP 3: Review & Create ════════════ --\x3e\n <if :="step === 3">\n <div class="pw-card">\n <div class="pw-section">Summary</div>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Pipeline</span>\n <span class="pw-summary-val">{{ pipelineName }}</span>\n </div>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Environment</span>\n <span class="pw-summary-val">{{ selectedEnv }}</span>\n </div>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Type</span>\n <span class="pw-summary-val">{{ engineKind === \'processor\' ? \'Change Processor\' : engineKind === \'materializer\' ? \'Materializer\' : \'Queue Worker\' }}</span>\n </div>\n <if :="engineKind === \'processor\'">\n <div class="pw-summary-row">\n <span class="pw-summary-label">Watches</span>\n <span class="pw-summary-val">{{ proc_sourceType }}</span>\n </div>\n </if>\n <if :="engineKind === \'materializer\'">\n <div class="pw-summary-row">\n <span class="pw-summary-label">Source</span>\n <span class="pw-summary-val">{{ mat_sourceType }}</span>\n </div>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Target</span>\n <span class="pw-summary-val">{{ mat_targetType }}</span>\n </div>\n </if>\n <if :="engineKind === \'queue-worker\'">\n <div class="pw-summary-row">\n <span class="pw-summary-label">Job type</span>\n <span class="pw-summary-val">{{ effectiveJobType }}</span>\n </div>\n </if>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Handler</span>\n <span class="pw-summary-val">\n {{ effectiveHandlerName }}\n <span style="opacity:.4; font-family:sans-serif; font-size:11px;">\n {{ handlerMode === \'new\' ? \'(will be created)\' : \'\' }}\n </span>\n </span>\n </div>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Engine</span>\n <span class="pw-summary-val">{{ engineName }}</span>\n </div>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Run recording</span>\n <span class="pw-summary-val" style="font-size:11px;">\n {{ handler_recordRuns === \'all\' ? \'all runs\' : handler_recordRuns === \'errors\' ? \'errors only\' : \'none\' }}\n </span>\n </div>\n <if :="showAdvanced || proc_mode !== \'async\' || proc_bootstrap !== \'snapshot\' || proc_batchSize != 256 || proc_originMode !== \'all\' || mat_bootstrap !== \'snapshot\' || mat_batchSize != 128 || mat_originMode !== \'all\' || !mat_clearTargetOnRebuild || qw_concurrency != 1">\n <div class="pw-summary-row" style="border-bottom:none;">\n <span class="pw-summary-label">Options</span>\n <span class="pw-summary-val" style="font-size:11px;">\n <if :="engineKind === \'processor\'">\n mode={{ proc_mode }} · bootstrap={{ proc_mode === \'inline\' ? \'n/a\' : proc_bootstrap }} · batch={{ proc_batchSize }} · origin={{ proc_originMode }}\n </if>\n <if :="engineKind === \'materializer\'">\n bootstrap={{ mat_bootstrap }} · batch={{ mat_batchSize }} · origin={{ mat_originMode }} · clearOnRebuild={{ mat_clearTargetOnRebuild ? \'yes\' : \'no\' }}\n </if>\n <if :="engineKind === \'queue-worker\'">\n concurrency={{ qw_concurrency }}\n </if>\n </span>\n </div>\n </if>\n </div>\n\n <div style="font-size:12px; opacity:.45; line-height:1.6;">\n This will{{ handlerMode === \'new\' ? \' create the function, then\' : \'\' }} create the engine and pipeline record, and start it immediately.\n </div>\n\n \x3c!-- Feedback --\x3e\n <if :="error">\n <div class="pw-error">{{ error }}</div>\n </if>\n <if :="success">\n <div class="pw-success">{{ success }}</div>\n </if>\n\n <div class="pw-actions">\n <button class="pw-btn pw-btn--ghost" @click="goBack()">← Back</button>\n <button class="pw-btn pw-btn--green" :disabled="busy" @click="submit()">\n {{ busy ? \'Creating…\' : \'Create & Start Pipeline\' }}\n </button>\n </div>\n </if>\n</div>'};
1
+ import e from"../../../components/ok-js-input.ok.js";import n from"./embed-create-panel.ok.js";const t=[{symbol:"require",pattern:/(^|[^\w$.])require\s*\(/},{symbol:"import",pattern:/(^|[^\w$.])import\b/},{symbol:"console",pattern:/(^|[^\w$.])console\b/},{symbol:"module",pattern:/(^|[^\w$.])module\b/},{symbol:"exports",pattern:/(^|[^\w$.])exports\b/},{symbol:"process",pattern:/(^|[^\w$.])process\b/},{symbol:"child_process",pattern:/child_process/},{symbol:"worker_threads",pattern:/worker_threads/}];function i(e){const n="string"==typeof e?e.replace(/\r\n/g,"\n").trim():"";if(!n)return{ok:!1,message:"Source is required"};if((new TextEncoder).encode(n).length>65536)return{ok:!1,message:"Source exceeds 64 KB"};if(!/^(async\s+)?\(\s*[A-Za-z_$][\w$]*\s*\)\s*=>/.test(n))return{ok:!1,message:"Must be (ctx) => { … } or async (ctx) => { … }"};for(const{symbol:e,pattern:i}of t)if(i.test(n))return{ok:!1,message:`Cannot use "${e}"`};try{new Function(`return (${n});`)()}catch(e){return{ok:!1,message:e?.message||"Compile error"}}return{ok:!0,normalizedSource:n}}function a(e){return!0===e||""===e||1===e||"1"===e||"true"===e}export default{tag:"pipeline-create-panel",attr:{locktype:{},lockenv:{},initialkind:{},locktypestrict:{},hidequeueworker:{}},dependencies:[e,n],context:e=>({step:1,busy:!1,error:null,success:null,engineKind:"",proc_sourceType:"",mat_sourceType:"",mat_targetType:"",qw_jobType:"",qw_jobTypeMode:"existing",handlerMode:"existing",handlerName:"",newFnName:"",newFnSource:"",newFnValidation:{ok:!1,message:"Source is required"},newFnDryRunning:!1,newFnDryResult:null,newFnDryError:null,newFnDryPayload:null,showAdvanced:!1,proc_mode:"worker",proc_bootstrap:"snapshot",proc_batchSize:256,proc_originMode:"all",mat_bootstrap:"snapshot",mat_batchSize:128,mat_originMode:"all",mat_clearTargetOnRebuild:!0,qw_concurrency:1,handler_recordRuns:"errors",types:[],functions:[],queueJobTypes:[],get lockedType(){return e.$attr.locktype||""},get lockedEnv(){return e.$attr.lockenv||""},get initialKind(){const n=e.$attr.initialkind||"";return this.availableKinds.includes(n)?n:""},get lockTypeStrict(){return a(e.$attr.locktypestrict)},get hideQueueWorker(){return a(e.$attr.hidequeueworker)},get availableKinds(){return["processor","materializer","queue-worker","embedding"].filter(e=>!(this.hideQueueWorker&&"queue-worker"===e))},get typeSelectionLocked(){return!!this.lockedType&&this.lockTypeStrict},get kindLocked(){return!!this.initialKind},get selectedEnv(){return this.lockedEnv||e.$ok.stores.okdb.selectedEnv||"default"},async loadMeta(){const e=this.selectedEnv;try{const[n,t,i]=await Promise.all([fetch(`${__ENV__.API}/env/${encodeURIComponent(e)}/types`),fetch(`${__ENV__.API}/env/${encodeURIComponent(e)}/functions`),fetch(`${__ENV__.API}/env/${encodeURIComponent(e)}/queue/stats`).catch(()=>null)]),a=await n.json().catch(()=>({})),r=await t.json().catch(()=>({})),s=a.data??a.result??[];this.types=(Array.isArray(s)?s.map(e=>e.type??e.name??e):Object.keys(s)).filter(e=>"string"==typeof e&&!e.startsWith("~")).sort();const o=r.data??r.result??[];if(this.functions=Array.isArray(o)?o.map(e=>e.key??e.name??e).filter(Boolean).sort():[],i&&i.ok){const e=await i.json().catch(()=>({})),n=e?.result??e?.data??null,t=n?.byType??{};this.queueJobTypes=Object.keys(t).sort()}else this.queueJobTypes=[]}catch{this.types=[],this.functions=[],this.queueJobTypes=[]}},get effectiveJobType(){return"custom"===this.qw_jobTypeMode?this.qw_jobType.trim():this.qw_jobType},get effectiveHandlerName(){return"new"===this.handlerMode?this.newFnName.trim():this.handlerName},get autoName(){return"processor"===this.engineKind&&this.proc_sourceType&&this.effectiveHandlerName?`${this.proc_sourceType}-${this.effectiveHandlerName}`:"materializer"===this.engineKind&&this.mat_sourceType&&this.mat_targetType&&this.effectiveHandlerName?`${this.mat_sourceType}-to-${this.mat_targetType}-${this.effectiveHandlerName}`:"queue-worker"===this.engineKind&&this.effectiveJobType&&this.effectiveHandlerName?`${this.effectiveJobType}-${this.effectiveHandlerName}`:""},get pipelineName(){return this.autoName},get engineName(){return this.autoName?`${this.autoName}:${this.engineKind}`:""},get step2Valid(){return"embedding"===this.engineKind||!("processor"===this.engineKind&&!this.proc_sourceType)&&!!("materializer"!==this.engineKind||this.mat_sourceType&&this.mat_targetType)&&!("queue-worker"===this.engineKind&&!this.effectiveJobType)&&("existing"===this.handlerMode?!!this.handlerName:!!this.newFnName.trim()&&this.newFnValidation.ok)},resetWizard(){this.step=this.kindLocked?2:1,this.engineKind=this.kindLocked?this.initialKind:"",this.proc_sourceType=this.lockedType||"",this.mat_sourceType=this.lockedType||"",this.mat_targetType="",this.qw_jobType="",this.qw_jobTypeMode="existing",this.handlerMode="existing",this.handlerName="",this.newFnName="",this.newFnSource="",this.newFnValidation={ok:!1,message:"Source is required"},this.newFnDryResult=null,this.newFnDryError=null,this.showAdvanced=!1,this.handler_recordRuns="errors",this.proc_mode="worker",this.proc_bootstrap="snapshot",this.proc_batchSize=256,this.proc_originMode="all",this.mat_bootstrap="snapshot",this.mat_batchSize=128,this.mat_originMode="all",this.mat_clearTargetOnRebuild=!0,this.qw_concurrency=1},applyScopeDefaults(){this.typeSelectionLocked?(this.proc_sourceType=this.lockedType,this.mat_sourceType=this.lockedType):this.lockedType&&!this.proc_sourceType&&(this.proc_sourceType=this.lockedType),this.typeSelectionLocked||!this.lockedType||this.mat_sourceType||(this.mat_sourceType=this.lockedType),this.kindLocked&&this.engineKind!==this.initialKind&&this.pickType(this.initialKind)},get dedupedDryLogs(){const e=this.newFnDryResult?.logs;if(!Array.isArray(e)||!e.length)return[];const n=[],t=new Map;for(const i of e){const e=`${i.level??""}::${i.msg??i.message??JSON.stringify(i)}`;if(t.has(e))t.get(e).count++;else{const a={...i,count:1,_key:e};t.set(e,a),n.push(a)}}return n},get dryRunDuration(){const e=this.newFnDryResult;return e?e.execution?.durationMs??null:null},get dryRunStatus(){const e=this.newFnDryResult;return e?"error"===e.status||e.error?"error":e.status||"success":null},pickType(e){this.availableKinds.includes(e)&&(this.engineKind=e,this.step=2,this.typeSelectionLocked&&(this.proc_sourceType=this.lockedType,this.mat_sourceType=this.lockedType),"embedding"!==e&&(this.newFnSource||(this.newFnSource="processor"===e?'async (ctx) => {\n // ctx.payload — array of { key, value, action, clock }\n // ctx.processor — processor trigger metadata (only for processor engines)\n // ctx.processor.mode — "snapshot" | "live"\n // ctx.processor.sourceType — the type being watched\n // ctx.processor.sourceEnv — the environment of the source type\n // ctx.env — env facade: put, get, query, txn, ...\n // ctx.log — structured logger: ctx.log.info(msg, data)\n //\n // Example: materialised view\n // for (const change of ctx.payload) {\n // await ctx.env.put(\'derived\', change.key, { processed: true, ...change.value });\n // }\n\n return { processed: ctx.payload.length };\n}':"materializer"===e?"async (ctx) => {\n // ctx.payload — array of change records\n // change.action — \"put\" | \"remove\"\n // change.key — source record key\n // change.value — hydrated source value for puts\n // change.clock — log clock during live replay; null during snapshot bootstrap\n // ctx.materializer — materializer trigger metadata\n // ctx.materializer.sourceType — the type being watched\n // ctx.materializer.targetType — the derived target type\n // ctx.materializer.sourceEnv — the environment of the source type\n // ctx.materializer.targetEnv — the environment of the target type\n // ctx.env — env facade (read-only for deterministic materializers)\n // ctx.log — structured logger: ctx.log.info(msg, data)\n //\n // Return ops for the engine to apply to the target type.\n const ops = [];\n\n for (const change of ctx.payload) {\n if (change.action === 'put') {\n const doc = change.value ?? change.newValue;\n if (!doc) continue;\n\n ops.push({\n action: 'put',\n key: change.key,\n value: {\n projected: true,\n sourceKey: change.key,\n ...doc,\n },\n });\n }\n\n if (change.action === 'remove') {\n ops.push({ action: 'remove', key: change.key });\n }\n }\n\n return ops;\n}":"async (ctx) => {\n // ctx.payload — the raw job payload (whatever was enqueued)\n // ctx.job — queue job context (only for queue-worker pipelines)\n // ctx.job.id, ctx.job.type, ctx.job.tries, ctx.job.tags, ...\n // ctx.job.heartbeat() — keep the job alive\n // ctx.job.markProgress(msg) — report progress\n // ctx.env — env facade: put, get, query, txn, ...\n // ctx.log — structured logger: ctx.log.info(msg, data)\n //\n // Example: handle a queued task\n // const { id, action } = ctx.payload;\n // ctx.log.info('handling job', { id, action, attempt: ctx.job.tries });\n // await ctx.env.put('results', id, { done: true });\n\n return { ok: true };\n}",this.newFnValidation=i(this.newFnSource))))},goBack(){"embedding"!==this.engineKind?this.step>1&&this.step--:this.resetWizard()},goNext(){2===this.step&&this.step2Valid&&(this.step=3)},onEmbeddingCreated(n){this.success="Embedding pipeline created",this.error=null,this.resetWizard(),e.$el.dispatchEvent(new CustomEvent("created",{detail:n,bubbles:!0}))},updateSource(e){this.newFnSource=e??"",this.newFnValidation=i(this.newFnSource)},async buildDryRunPayload(){if("processor"===this.engineKind)return[{key:"test-1",value:{sample:!0,name:"Example record"},action:"put",clock:1}];if("materializer"===this.engineKind)return[{key:"test-1",value:{sample:!0,name:"Example record"},action:"put",clock:null},{key:"test-2",action:"remove",clock:2}];const e=this.effectiveJobType;if(e)try{const n=this.selectedEnv,t=`${__ENV__.API}/env/${encodeURIComponent(n)}/queue`,i=await fetch(`${t}/jobs?${new URLSearchParams({type:e,limit:"1"})}`);if(i.ok){const e=await i.json(),n=e?.result?.items??e?.data?.items??[],a=n[0]?.key??n[0]?.id??n[0]?.value?.id;if(a){const e=await fetch(`${t}/job/${encodeURIComponent(a)}`);if(e.ok){const n=await e.json(),t=n?.result??n?.data;if(void 0!==t?.payload)return t.payload}}}}catch{}return{}},async dryRunFunction(){if(this.newFnValidation.ok&&!this.newFnDryRunning){this.newFnDryRunning=!0,this.newFnDryResult=null,this.newFnDryError=null,this.newFnDryPayload=null;try{const e=this.selectedEnv,n=await this.buildDryRunPayload();this.newFnDryPayload=n;const t={name:this.newFnName.trim()||"__preview__",source:this.newFnValidation.normalizedSource||this.newFnSource.trim(),metadata:{description:"Pipeline handler preview"},enabled:!0,payload:n};"queue-worker"===this.engineKind&&(t.jobContext={id:"__preview-job__",type:this.effectiveJobType||"preview",tries:1,created:Date.now(),tags:null,priority:0,bucket:null,cron:null}),"processor"===this.engineKind&&(t.processorContext={mode:"live",sourceType:this.proc_sourceType||"example",sourceEnv:this.selectedEnv,processor:"__preview__",engineKey:"__preview__",cursorKey:null}),"materializer"===this.engineKind&&(t.materializerContext={mode:"live",sourceType:this.mat_sourceType||"example",targetType:this.mat_targetType||"example_projection",sourceEnv:this.selectedEnv,targetEnv:this.selectedEnv,engineKey:"__preview__",cursorKey:null});const i=await fetch(`${__ENV__.API}/env/${encodeURIComponent(e)}/functions/preview`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),a=await i.json().catch(()=>({})),r=a?.data??a?.result??a;if(!i.ok&&!r?.status)throw new Error(a?.error?.message||`Preview failed (${i.status})`);this.newFnDryResult=r}catch(e){this.newFnDryError=e?.message||String(e)}finally{this.newFnDryRunning=!1}}},buildEngineConfig(){const e={kind:"function",name:this.effectiveHandlerName};return"all"!==this.handler_recordRuns&&(e.recordRuns=this.handler_recordRuns),"processor"===this.engineKind?{source_type:this.proc_sourceType,source_env:this.selectedEnv,handler:e,mode:this.proc_mode,bootstrap:"inline"===this.proc_mode?null:this.proc_bootstrap,batchSize:Number(this.proc_batchSize)||256,originMode:this.proc_originMode}:"materializer"===this.engineKind?{source_type:this.mat_sourceType,target_type:this.mat_targetType,source_env:this.selectedEnv,target_env:this.selectedEnv,handler:e,bootstrap:this.mat_bootstrap,batchSize:Number(this.mat_batchSize)||128,originMode:this.mat_originMode,clearTargetOnRebuild:!1!==this.mat_clearTargetOnRebuild}:"queue-worker"===this.engineKind?{job_type:this.effectiveJobType,queue_env:this.selectedEnv,handler:e,concurrency:Number(this.qw_concurrency)||1}:{}},async submit(){if(this.step2Valid&&!this.busy){this.busy=!0,this.error=null,this.success=null;try{const n=this.selectedEnv;if("new"===this.handlerMode){const e={name:this.newFnName.trim(),source:this.newFnValidation.normalizedSource||this.newFnSource.trim(),metadata:{description:`Handler for pipeline ${this.pipelineName}`,owner_engine:this.engineName,pipeline:this.pipelineName},enabled:!0},t=await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/functions`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),i=await t.json().catch(()=>({}));if(!t.ok)throw new Error(i?.error?.message||`Failed to create function (${t.status})`)}const t={name:this.pipelineName,meta:{label:this.pipelineName},members:[{engineType:this.engineKind,engineName:this.engineName,role:this.engineKind,engineConfig:this.buildEngineConfig()}]},i=await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/pipelines/scaffold`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),a=await i.json().catch(()=>({}));if(!i.ok)throw new Error(a?.error?.message||`Failed (${i.status})`);this.success=`Pipeline "${this.pipelineName}" created and started`,e.$el.dispatchEvent(new CustomEvent("created",{detail:a.result??a.data,bubbles:!0})),this.resetWizard()}catch(e){this.error=e?.message||String(e)}finally{this.busy=!1}}},formatJson(e){try{return JSON.stringify(e,null,2)}catch{return String(e??"")}},setJobTypeMode(e){this.qw_jobTypeMode=e,this.qw_jobType=""},logLabel(e){const n=e?.msg??e?.message??"";return e?.count>1?`${n} (×${e.count})`:n},logColor(e){const n=e?.level??"";return"error"===n||"fatal"===n?"var(--okdb-danger)":"warn"===n?"var(--okdb-warning)":"debug"===n?"var(--okdb-app-fg-muted)":"var(--okdb-app-fg-subtle)"}}),async init(e){await this.loadMeta(),this.applyScopeDefaults(),e.$watch(()=>e.$ok.stores.okdb.selectedEnv,async()=>{await this.loadMeta(),this.applyScopeDefaults()})},style:"\n pipeline-create-panel {\n display: block;\n }\n pipeline-create-panel .pw-wrap {\n display: flex;\n flex-direction: column;\n gap: 18px;\n max-width: 820px;\n }\n pipeline-create-panel .pw-title {\n font-size: 18px;\n font-weight: 600;\n }\n pipeline-create-panel .pw-sub {\n font-size: 12px;\n opacity: .55;\n line-height: 1.7;\n max-width: 680px;\n }\n\n /* ── steps indicator ── */\n pipeline-create-panel .pw-steps {\n display: flex;\n align-items: center;\n gap: 0;\n font-size: 12px;\n margin-bottom: 4px;\n }\n pipeline-create-panel .pw-step-dot {\n width: 26px; height: 26px;\n border-radius: 50%;\n display: flex; align-items: center; justify-content: center;\n font-weight: 700; font-size: 12px;\n border: 2px solid var(--okdb-app-border);\n color: var(--okdb-app-fg-muted);\n background: transparent;\n flex-shrink: 0;\n }\n pipeline-create-panel .pw-step-dot--active {\n border-color: var(--okdb-accent);\n color: var(--okdb-accent);\n background: color-mix(in srgb, var(--okdb-accent) 7%, transparent);\n }\n pipeline-create-panel .pw-step-dot--done {\n border-color: var(--okdb-success);\n color: var(--okdb-success);\n background: color-mix(in srgb, var(--okdb-success) 7%, transparent);\n }\n pipeline-create-panel .pw-step-line {\n flex: 1;\n height: 2px;\n background: var(--okdb-app-border);\n margin: 0 6px;\n max-width: 60px;\n }\n pipeline-create-panel .pw-step-line--done {\n background: color-mix(in srgb, var(--okdb-success) 40%, transparent);\n }\n pipeline-create-panel .pw-step-label {\n margin-left: 8px;\n opacity: .45;\n }\n pipeline-create-panel .pw-step-label--active {\n opacity: 1;\n color: var(--okdb-accent);\n }\n\n /* ── type-choice cards ── */\n pipeline-create-panel .pw-choice-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\n gap: 14px;\n }\n pipeline-create-panel .pw-choice {\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-border);\n border-radius: 12px;\n padding: 20px 18px;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n gap: 8px;\n transition: border-color .15s, background .15s;\n }\n pipeline-create-panel .pw-choice:hover {\n border-color: color-mix(in srgb, var(--okdb-accent) 33%, transparent);\n background: var(--okdb-app-surface);\n }\n pipeline-create-panel .pw-choice-icon {\n font-size: 28px;\n opacity: .8;\n }\n pipeline-create-panel .pw-choice-name {\n font-size: 15px;\n font-weight: 600;\n }\n pipeline-create-panel .pw-choice-desc {\n font-size: 12px;\n opacity: .55;\n line-height: 1.7;\n }\n\n /* ── card / form ── */\n pipeline-create-panel .pw-card {\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-border);\n border-radius: 12px;\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n }\n pipeline-create-panel .pw-section {\n font-size: 11px;\n text-transform: uppercase;\n opacity: .45;\n letter-spacing: .06em;\n }\n pipeline-create-panel .pw-row {\n display: grid;\n grid-template-columns: 130px 1fr;\n gap: 8px 12px;\n align-items: center;\n font-size: 13px;\n }\n pipeline-create-panel .pw-label {\n opacity: .7;\n text-align: right;\n font-size: 12px;\n }\n pipeline-create-panel .pw-input {\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-border);\n border-radius: 6px;\n padding: 6px 8px;\n color: inherit;\n font-size: 13px;\n width: 100%;\n box-sizing: border-box;\n }\n pipeline-create-panel .pw-input:focus {\n outline: none;\n border-color: var(--okdb-accent);\n }\n pipeline-create-panel .pw-select {\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-border);\n border-radius: 6px;\n padding: 6px 8px;\n color: inherit;\n font-size: 13px;\n width: 100%;\n box-sizing: border-box;\n }\n pipeline-create-panel .pw-hint {\n font-size: 11px;\n opacity: .4;\n font-style: italic;\n grid-column: 2;\n line-height: 1.5;\n }\n pipeline-create-panel .pw-pills {\n display: flex;\n gap: 4px;\n }\n pipeline-create-panel .pw-pill {\n padding: 4px 12px;\n border-radius: 999px;\n font-size: 12px;\n border: 1px solid var(--okdb-app-border);\n background: transparent;\n cursor: pointer;\n color: inherit;\n opacity: .7;\n }\n pipeline-create-panel .pw-pill:hover {\n opacity: 1;\n border-color: color-mix(in srgb, var(--okdb-accent) 27%, transparent);\n }\n pipeline-create-panel .pw-pill--on {\n border-color: var(--okdb-accent);\n color: var(--okdb-accent);\n opacity: 1;\n background: color-mix(in srgb, var(--okdb-accent) 7%, transparent);\n }\n\n /* ── handler toggle ── */\n pipeline-create-panel .pw-handler-toggle {\n display: flex;\n gap: 0;\n border: 1px solid var(--okdb-app-border);\n border-radius: 8px;\n overflow: hidden;\n }\n pipeline-create-panel .pw-handler-toggle-btn {\n flex: 1;\n padding: 7px 14px;\n font-size: 12px;\n border: none;\n background: transparent;\n color: inherit;\n cursor: pointer;\n opacity: .6;\n text-align: center;\n }\n pipeline-create-panel .pw-handler-toggle-btn:first-child {\n border-right: 1px solid var(--okdb-app-border);\n }\n pipeline-create-panel .pw-handler-toggle-btn--on {\n opacity: 1;\n background: color-mix(in srgb, var(--okdb-accent) 7%, transparent);\n color: var(--okdb-accent);\n font-weight: 600;\n }\n\n /* ── code editor area ── */\n pipeline-create-panel .pw-editor-wrap {\n border: 1px solid var(--okdb-app-border);\n border-radius: 10px;\n overflow: hidden;\n background: var(--okdb-app-bg-deep);\n height: 260px;\n min-height: 260px;\n }\n\n /* ── dry-run result ── */\n pipeline-create-panel .pw-dry-card {\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-border);\n border-radius: 10px;\n padding: 12px 14px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n pipeline-create-panel .pw-dry-ok {\n color: var(--okdb-success);\n font-size: 12px;\n }\n pipeline-create-panel .pw-dry-err {\n color: var(--okdb-danger);\n font-size: 12px;\n }\n pipeline-create-panel .pw-dry-pre {\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n font-size: 11px;\n line-height: 1.5;\n color: var(--okdb-app-fg-subtle);\n max-height: 160px;\n overflow: auto;\n }\n pipeline-create-panel .pw-dry-meta {\n display: flex;\n gap: 12px;\n font-size: 11px;\n opacity: .5;\n margin-top: 2px;\n }\n\n /* ── dry-run logs ── */\n pipeline-create-panel .pw-dry-logs {\n display: flex;\n flex-direction: column;\n gap: 2px;\n margin-top: 4px;\n }\n pipeline-create-panel .pw-dry-log-entry {\n font-size: 11px;\n font-family: monospace;\n line-height: 1.5;\n padding: 2px 8px;\n border-radius: 4px;\n background: var(--okdb-app-bg-deep);\n }\n pipeline-create-panel .pw-dry-log-level {\n display: inline-block;\n min-width: 40px;\n font-weight: 600;\n text-transform: uppercase;\n font-size: 10px;\n margin-right: 6px;\n }\n\n /* ── review summary ── */\n pipeline-create-panel .pw-summary-row {\n display: flex;\n justify-content: space-between;\n font-size: 13px;\n padding: 6px 0;\n border-bottom: 1px solid var(--okdb-app-surface);\n }\n pipeline-create-panel .pw-summary-label {\n opacity: .55;\n }\n pipeline-create-panel .pw-summary-val {\n font-family: monospace;\n color: var(--okdb-accent);\n }\n\n /* ── buttons ── */\n pipeline-create-panel .pw-actions {\n display: flex;\n gap: 8px;\n align-items: center;\n }\n pipeline-create-panel .pw-btn {\n padding: 8px 20px;\n border-radius: 8px;\n border: 1px solid var(--okdb-accent);\n background: color-mix(in srgb, var(--okdb-accent) 9%, transparent);\n color: var(--okdb-accent);\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n }\n pipeline-create-panel .pw-btn:hover { background: color-mix(in srgb, var(--okdb-accent) 16%, transparent); }\n pipeline-create-panel .pw-btn:disabled { opacity: .4; cursor: default; }\n pipeline-create-panel .pw-btn--ghost {\n border-color: var(--okdb-app-border);\n background: transparent;\n color: inherit;\n font-weight: 400;\n opacity: .7;\n }\n pipeline-create-panel .pw-btn--ghost:hover { opacity: 1; background: var(--okdb-app-surface); }\n pipeline-create-panel .pw-btn--green {\n border-color: var(--okdb-success);\n background: color-mix(in srgb, var(--okdb-success) 9%, transparent);\n color: var(--okdb-success);\n }\n pipeline-create-panel .pw-btn--green:hover { background: color-mix(in srgb, var(--okdb-success) 16%, transparent); }\n pipeline-create-panel .pw-btn--small {\n padding: 5px 14px;\n font-size: 12px;\n font-weight: 500;\n }\n\n /* ── feedback ── */\n pipeline-create-panel .pw-error {\n color: var(--okdb-danger);\n font-size: 13px;\n padding: 8px 12px;\n border: 1px solid var(--okdb-danger);\n border-radius: 6px;\n }\n pipeline-create-panel .pw-success {\n color: var(--okdb-success);\n font-size: 13px;\n padding: 8px 12px;\n border: 1px solid color-mix(in srgb, var(--okdb-success) 33%, transparent);\n border-radius: 6px;\n background: color-mix(in srgb, var(--okdb-success) 5%, transparent);\n }\n pipeline-create-panel .pw-warn {\n font-size: 11px;\n color: var(--okdb-warning);\n opacity: .75;\n padding: 6px 10px;\n border: 1px solid color-mix(in srgb, var(--okdb-warning) 20%, transparent);\n border-radius: 6px;\n background: color-mix(in srgb, var(--okdb-warning) 4%, transparent);\n line-height: 1.5;\n }\n\n /* ── advanced toggle ── */\n pipeline-create-panel .pw-adv-toggle {\n font-size: 12px;\n opacity: .5;\n cursor: pointer;\n user-select: none;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n }\n pipeline-create-panel .pw-adv-toggle:hover { opacity: .8; }\n ",template:'\n<div class="pw-wrap">\n <div>\n <div class="pw-title">{{ engineKind === \'embedding\' || initialKind === \'embedding\' ? \'Create Embedding Pipeline\' : engineKind === \'materializer\' || initialKind === \'materializer\' ? \'Create Materializer Pipeline\' : \'Create Pipeline\' }}</div>\n <div class="pw-sub">\n Set up automated processing in <b>{{ selectedEnv }}</b>.\n A pipeline connects a data source to a handler function and runs it automatically.\n </div>\n </div>\n\n \x3c!-- ── Steps indicator ── --\x3e\n <if :="engineKind !== \'embedding\'">\n <div class="pw-steps">\n <div :class="step >= 1 ? (step > 1 ? \'pw-step-dot pw-step-dot--done\' : \'pw-step-dot pw-step-dot--active\') : \'pw-step-dot\'">\n {{ step > 1 ? \'✓\' : \'1\' }}\n </div>\n <span :class="step >= 1 ? \'pw-step-label pw-step-label--active\' : \'pw-step-label\'">Type</span>\n\n <div :class="step > 1 ? \'pw-step-line pw-step-line--done\' : \'pw-step-line\'"></div>\n\n <div :class="step >= 2 ? (step > 2 ? \'pw-step-dot pw-step-dot--done\' : \'pw-step-dot pw-step-dot--active\') : \'pw-step-dot\'">\n {{ step > 2 ? \'✓\' : \'2\' }}\n </div>\n <span :class="step >= 2 ? \'pw-step-label pw-step-label--active\' : \'pw-step-label\'">Configure</span>\n\n <div :class="step > 2 ? \'pw-step-line pw-step-line--done\' : \'pw-step-line\'"></div>\n\n <div :class="step >= 3 ? \'pw-step-dot pw-step-dot--active\' : \'pw-step-dot\'">3</div>\n <span :class="step >= 3 ? \'pw-step-label pw-step-label--active\' : \'pw-step-label\'">Review</span>\n </div>\n </if>\n\n \x3c!-- ════════════ STEP 1: Choose type ════════════ --\x3e\n <if :="step === 1 && !kindLocked">\n <div class="pw-choice-grid">\n <div class="pw-choice" @click="pickType(\'processor\')">\n <div class="pw-choice-icon">⚡</div>\n <div class="pw-choice-name">Change Processor</div>\n <div class="pw-choice-desc">\n Watches a data type for changes and runs your function for every batch.\n Use for projections, computed values, webhooks, or side-effects that should trigger\n whenever records are created, updated, or deleted.\n </div>\n </div>\n <div class="pw-choice" @click="pickType(\'materializer\')">\n <div class="pw-choice-icon">⇄</div>\n <div class="pw-choice-name">Materializer</div>\n <div class="pw-choice-desc">\n Watches a source type and maintains a deterministic derived target type by returning ops for the engine to apply.\n Use for rebuildable projections, mirrors, and materialized views.\n </div>\n </div>\n <if :="!hideQueueWorker">\n <div class="pw-choice" @click="pickType(\'queue-worker\')">\n <div class="pw-choice-icon">⇶</div>\n <div class="pw-choice-name">Queue Worker</div>\n <div class="pw-choice-desc">\n Processes jobs from a queue. Use for background tasks like sending emails,\n generating reports, or any work that can be enqueued and processed asynchronously\n with automatic retries.\n </div>\n </div>\n </if>\n <div class="pw-choice" @click="pickType(\'embedding\')">\n <div class="pw-choice-icon">⊛</div>\n <div class="pw-choice-name">Embedding Pipeline</div>\n <div class="pw-choice-desc">\n Creates an embedder, indexer, search engine, and optional worker as one grouped pipeline.\n Use this for semantic search, RAG, and other vector workflows.\n </div>\n </div>\n </div>\n </if>\n\n \x3c!-- ════════════ STEP 2: Configure ════════════ --\x3e\n <if :="step === 2">\n <if :="engineKind === \'embedding\'">\n <div class="pw-card">\n <div class="pw-section">Embeddings</div>\n <div style="font-size:12px; opacity:.58; line-height:1.7; margin-bottom:2px;">\n Embedding pipelines use the specialized creator because they can bundle multiple engines and optional search/worker settings in one pass.\n </div>\n <div class="pw-actions" style="margin-top:4px;" :style="kindLocked ? \'display:none;\' : \'\'">\n <button class="pw-btn pw-btn--ghost" @click="goBack()">← Back</button>\n </div>\n </div>\n <embed-create-panel initialtab="pipeline" :locktype="lockedType" :lockenv="selectedEnv" @created="onEmbeddingCreated($event.detail)" />\n </if>\n\n <if :="engineKind !== \'embedding\'">\n \x3c!-- Source / job type --\x3e\n <div class="pw-card">\n <div class="pw-section">\n {{ engineKind === \'queue-worker\' ? \'What to process\' : \'What to watch\' }}\n </div>\n\n <if :="engineKind === \'processor\'">\n <if :="typeSelectionLocked">\n <div class="pw-row">\n <span class="pw-label">Data type</span>\n <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <span style="display:inline-flex; align-items:center; gap:6px; padding:5px 10px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-accent) 33%, transparent); color:var(--okdb-accent); background:color-mix(in srgb, var(--okdb-accent) 6%, transparent); font-size:12px;">{{ proc_sourceType }}</span>\n <span style="font-size:11px; opacity:.45;">Locked to the current DB type</span>\n </div>\n </div>\n </if>\n <if :="!typeSelectionLocked">\n <div class="pw-row">\n <span class="pw-label">Data type</span>\n <select class="pw-select" :value="proc_sourceType" @change="proc_sourceType = $event.target.value">\n <option value="">— select a type —</option>\n <each :of="types" let:item="t">\n <option :value="t">{{ t }}</option>\n </each>\n </select>\n </div>\n </if>\n <div class="pw-hint">\n The processor will receive a batch of changes every time records of this type are created, updated, or deleted.\n </div>\n </if>\n\n <if :="engineKind === \'materializer\'">\n <if :="typeSelectionLocked">\n <div class="pw-row">\n <span class="pw-label">Source type</span>\n <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <span style="display:inline-flex; align-items:center; gap:6px; padding:5px 10px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-accent) 33%, transparent); color:var(--okdb-accent); background:color-mix(in srgb, var(--okdb-accent) 6%, transparent); font-size:12px;">{{ mat_sourceType }}</span>\n <span style="font-size:11px; opacity:.45;">Locked to the current DB type</span>\n </div>\n </div>\n </if>\n <if :="!typeSelectionLocked">\n <div class="pw-row">\n <span class="pw-label">Source type</span>\n <select class="pw-select" :value="mat_sourceType" @change="mat_sourceType = $event.target.value">\n <option value="">— select a type —</option>\n <each :of="types" let:item="t">\n <option :value="t">{{ t }}</option>\n </each>\n </select>\n </div>\n </if>\n <div class="pw-row">\n <span class="pw-label">Target type</span>\n <input class="pw-input" type="text" placeholder="e.g. order_summaries"\n :value="mat_targetType" @input="mat_targetType = $event.target.value" />\n </div>\n <div class="pw-hint">\n The materializer watches the source type and applies returned ops to the target type, making rebuilds deterministic.\n </div>\n </if>\n\n <if :="engineKind === \'queue-worker\'">\n <div class="pw-row">\n <span class="pw-label">Job type</span>\n <div style="display:flex; flex-direction:column; gap:6px;">\n <if :="queueJobTypes.length > 0">\n <div class="pw-handler-toggle" style="max-width:300px;">\n <button\n :class="qw_jobTypeMode === \'existing\' ? \'pw-handler-toggle-btn pw-handler-toggle-btn--on\' : \'pw-handler-toggle-btn\'"\n @click="setJobTypeMode(\'existing\')"\n >Known type</button>\n <button\n :class="qw_jobTypeMode === \'custom\' ? \'pw-handler-toggle-btn pw-handler-toggle-btn--on\' : \'pw-handler-toggle-btn\'"\n @click="setJobTypeMode(\'custom\')"\n >Custom</button>\n </div>\n </if>\n <if :="qw_jobTypeMode === \'existing\' && queueJobTypes.length > 0">\n <select class="pw-select" :value="qw_jobType" @change="qw_jobType = $event.target.value">\n <option value="">— select job type —</option>\n <each :of="queueJobTypes" let:item="jt">\n <option :value="jt">{{ jt }}</option>\n </each>\n </select>\n </if>\n <if :="qw_jobTypeMode === \'custom\' || queueJobTypes.length === 0">\n <input class="pw-input" type="text" placeholder="e.g. email.send"\n :value="qw_jobType" @input="qw_jobType = $event.target.value" />\n </if>\n </div>\n </div>\n <div class="pw-hint" style="grid-column: 1 / -1;">\n Jobs enqueued with this type will be routed to this worker. You can enqueue jobs from functions, the API, or other pipelines.\n </div>\n </if>\n </div>\n\n \x3c!-- Handler function --\x3e\n <div class="pw-card">\n <div class="pw-section">Handler function</div>\n <div style="font-size:12px; opacity:.5; line-height:1.6; margin-bottom:4px;">\n The function that runs for each\n {{ engineKind === \'queue-worker\' ? \'job\' : \'batch of changes\' }}.\n Pick an existing function or write one now.\n </div>\n\n <div class="pw-handler-toggle">\n <button\n :class="handlerMode === \'existing\' ? \'pw-handler-toggle-btn pw-handler-toggle-btn--on\' : \'pw-handler-toggle-btn\'"\n @click="handlerMode = \'existing\'"\n >Use existing function</button>\n <button\n :class="handlerMode === \'new\' ? \'pw-handler-toggle-btn pw-handler-toggle-btn--on\' : \'pw-handler-toggle-btn\'"\n @click="handlerMode = \'new\'"\n >Write new function</button>\n </div>\n\n \x3c!-- Existing function picker --\x3e\n <if :="handlerMode === \'existing\'">\n <div class="pw-row">\n <span class="pw-label">Function</span>\n <select class="pw-select" :value="handlerName" @change="handlerName = $event.target.value">\n <option value="">— select function —</option>\n <each :of="functions" let:item="fn">\n <option :value="fn">{{ fn }}</option>\n </each>\n </select>\n </div>\n <if :="functions.length === 0">\n <div class="pw-hint" style="grid-column: 1 / -1;">\n No functions found in <b>{{ selectedEnv }}</b>.\n Switch to "Write new function" to create one inline.\n </div>\n </if>\n </if>\n\n \x3c!-- New function inline authoring --\x3e\n <if :="handlerMode === \'new\'">\n <div class="pw-row">\n <span class="pw-label">Function name</span>\n <input class="pw-input" type="text"\n placeholder="e.g. processArticle"\n :value="newFnName"\n @input="newFnName = $event.target.value" />\n </div>\n\n <div style="font-size:11px; text-transform:uppercase; letter-spacing:.06em; opacity:.45; margin-top:4px;">\n Implementation\n </div>\n <div class="pw-editor-wrap">\n <ok-js-input\n style="height:100%;"\n :value="newFnSource"\n @edit="updateSource($event.detail)"\n ></ok-js-input>\n </div>\n\n \x3c!-- Validation --\x3e\n <if :="newFnValidation.ok">\n <div style="font-size:12px; color:var(--okdb-success);">✓ Valid function source</div>\n </if>\n <if :="!newFnValidation.ok && newFnSource">\n <div style="font-size:12px; color:var(--okdb-danger);">{{ newFnValidation.message }}</div>\n </if>\n\n \x3c!-- Dry run --\x3e\n <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <button class="pw-btn pw-btn--small pw-btn--ghost"\n :disabled="!newFnValidation.ok || newFnDryRunning"\n @click="dryRunFunction()">\n {{ newFnDryRunning ? \'Running…\' : \'▶ Test with sample data\' }}\n </button>\n <span style="font-size:11px; opacity:.4;">\n {{ engineKind === \'queue-worker\' && effectiveJobType\n ? \'Uses a real job payload from the queue if available.\'\n : \'Runs the function draft against synthetic test data without saving it.\' }}\n </span>\n </div>\n <div class="pw-warn">\n ⚠ <strong>Test with sample data</strong> runs your function for real — the function itself is not saved,\n but any side-effects (e.g.&nbsp;<code>ctx.env.put</code>) <em>will</em> write to live data.\n </div>\n\n \x3c!-- Dry-run result --\x3e\n <if :="newFnDryResult">\n <div class="pw-dry-card">\n <div :class="dryRunStatus === \'error\' ? \'pw-dry-err\' : \'pw-dry-ok\'">\n {{ dryRunStatus === \'error\' ? \'✕ Function returned an error\' : \'✓ Function executed successfully\' }}\n </div>\n <div class="pw-dry-meta">\n <if :="dryRunDuration != null">\n <span>{{ dryRunDuration }}ms</span>\n </if>\n <span>status: {{ newFnDryResult.status || \'ok\' }}</span>\n </div>\n <if :="dryRunStatus === \'error\'">\n <pre class="pw-dry-pre" style="color:var(--okdb-danger);">{{ formatJson(newFnDryResult.error ?? newFnDryResult.result?.value ?? newFnDryResult) }}</pre>\n </if>\n <if :="dryRunStatus !== \'error\'">\n <pre class="pw-dry-pre">{{ formatJson(newFnDryResult.result?.value ?? newFnDryResult) }}</pre>\n </if>\n\n \x3c!-- Dry-run logs --\x3e\n <if :="dedupedDryLogs.length > 0">\n <div style="font-size:10px; text-transform:uppercase; letter-spacing:.06em; opacity:.4; margin-top:6px;">\n Logs ({{ newFnDryResult.logs.length }})\n </div>\n <div class="pw-dry-logs">\n <each :of="dedupedDryLogs" let:item="entry">\n <div class="pw-dry-log-entry">\n <span class="pw-dry-log-level" :style="\'color:\' + logColor(entry)">{{ entry.level || \'log\' }}</span>\n <span>{{ logLabel(entry) }}</span>\n </div>\n </each>\n </div>\n </if>\n\n \x3c!-- Input payload used --\x3e\n <if :="newFnDryPayload != null">\n <details style="margin-top:6px;">\n <summary style="font-size:10px; text-transform:uppercase; letter-spacing:.06em; opacity:.4; cursor:pointer; user-select:none;">\n Input payload\n </summary>\n <pre class="pw-dry-pre" style="margin-top:4px; max-height:180px;">{{ formatJson(newFnDryPayload) }}</pre>\n </details>\n </if>\n </div>\n </if>\n <if :="newFnDryError">\n <div class="pw-dry-card">\n <div class="pw-dry-err">✕ {{ newFnDryError }}</div>\n </div>\n </if>\n </if>\n </div>\n\n \x3c!-- Advanced options (collapsed) --\x3e\n <div>\n <span class="pw-adv-toggle" @click="showAdvanced = !showAdvanced">\n <span>{{ showAdvanced ? \'▾\' : \'▸\' }}</span>\n <span>Advanced options</span>\n </span>\n </div>\n <if :="showAdvanced">\n <div class="pw-card">\n <if :="engineKind === \'processor\'">\n <div class="pw-row">\n <span class="pw-label">Mode</span>\n <div class="pw-pills">\n <button :class="proc_mode === \'worker\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_mode = \'worker\'">worker (background)</button>\n <button :class="proc_mode === \'inline\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_mode = \'inline\'">inline</button>\n </div>\n </div>\n <div class="pw-hint">\n <b>worker</b> processes changes in background batches (recommended).\n <b>inline</b> runs synchronously inside the write path.\n </div>\n <if :="proc_mode === \'worker\'">\n <div class="pw-row">\n <span class="pw-label">Bootstrap</span>\n <div class="pw-pills">\n <button :class="proc_bootstrap === \'snapshot\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_bootstrap = \'snapshot\'">snapshot</button>\n <button :class="proc_bootstrap === \'log\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_bootstrap = \'log\'">log</button>\n </div>\n </div>\n <div class="pw-hint">\n <b>snapshot</b> processes all existing records first, then switches to live.\n <b>log</b> replays the change log from the beginning.\n </div>\n </if>\n <div class="pw-row">\n <span class="pw-label">Batch size</span>\n <input class="pw-input" type="number" min="1" max="10000"\n :value="proc_batchSize" @input="proc_batchSize = $event.target.value" />\n </div>\n <div class="pw-row">\n <span class="pw-label">Origin mode</span>\n <div class="pw-pills">\n <button :class="proc_originMode === \'all\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_originMode = \'all\'">all</button>\n <button :class="proc_originMode === \'self\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_originMode = \'self\'">self</button>\n <button :class="proc_originMode === \'remote\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="proc_originMode = \'remote\'">remote</button>\n </div>\n </div>\n <div class="pw-hint">\n <b>all</b> = local + synced changes. <b>self</b> = local only. <b>remote</b> = synced only.\n </div>\n </if>\n <if :="engineKind === \'materializer\'">\n <div class="pw-row">\n <span class="pw-label">Bootstrap</span>\n <div class="pw-pills">\n <button :class="mat_bootstrap === \'snapshot\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="mat_bootstrap = \'snapshot\'">snapshot</button>\n <button :class="mat_bootstrap === \'log\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="mat_bootstrap = \'log\'">log</button>\n </div>\n </div>\n <div class="pw-hint">\n <b>snapshot</b> replays all current source rows first. <b>log</b> replays the full change log.\n </div>\n <div class="pw-row">\n <span class="pw-label">Batch size</span>\n <input class="pw-input" type="number" min="1" max="10000"\n :value="mat_batchSize" @input="mat_batchSize = $event.target.value" />\n </div>\n <div class="pw-row">\n <span class="pw-label">Origin mode</span>\n <div class="pw-pills">\n <button :class="mat_originMode === \'all\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="mat_originMode = \'all\'">all</button>\n <button :class="mat_originMode === \'self\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="mat_originMode = \'self\'">self</button>\n <button :class="mat_originMode === \'remote\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="mat_originMode = \'remote\'">remote</button>\n </div>\n </div>\n <div class="pw-hint">\n Choose whether the materializer reacts to local changes, synced changes, or both.\n </div>\n <div class="pw-row">\n <span class="pw-label">Rebuild</span>\n <label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--okdb-app-fg);">\n <input type="checkbox" :checked="mat_clearTargetOnRebuild" @change="mat_clearTargetOnRebuild = $event.target.checked" />\n Clear target before rebuild\n </label>\n </div>\n </if>\n <if :="engineKind === \'queue-worker\'">\n <div class="pw-row">\n <span class="pw-label">Concurrency</span>\n <input class="pw-input" type="number" min="1" max="64"\n :value="qw_concurrency" @input="qw_concurrency = $event.target.value" />\n </div>\n <div class="pw-hint">How many jobs to process in parallel.</div>\n </if>\n\n \x3c!-- Run recording (shared by both engine types) --\x3e\n <div style="border-top:1px solid var(--okdb-app-border); margin-top:4px; padding-top:12px;">\n <div class="pw-row">\n <span class="pw-label">Run recording</span>\n <div class="pw-pills">\n <button :class="handler_recordRuns === \'all\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="handler_recordRuns = \'all\'">all runs</button>\n <button :class="handler_recordRuns === \'errors\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="handler_recordRuns = \'errors\'">errors only</button>\n <button :class="handler_recordRuns === \'none\' ? \'pw-pill pw-pill--on\' : \'pw-pill\'" @click="handler_recordRuns = \'none\'">none</button>\n </div>\n </div>\n <div class="pw-hint">\n Controls whether each function invocation is recorded in the run ledger.\n <b>errors only</b> (recommended) avoids creating hundreds of run records for busy pipelines while still capturing failures.\n <b>none</b> fully suppresses recording — useful for high-throughput processors where logging isn\'t needed.\n </div>\n </div>\n </div>\n </if>\n\n \x3c!-- Navigation --\x3e\n <div class="pw-actions">\n <button class="pw-btn pw-btn--ghost" @click="goBack()">← Back</button>\n <button class="pw-btn" :disabled="!step2Valid" @click="goNext()">Review →</button>\n </div>\n </if>\n </if>\n\n \x3c!-- ════════════ STEP 3: Review & Create ════════════ --\x3e\n <if :="step === 3">\n <div class="pw-card">\n <div class="pw-section">Summary</div>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Pipeline</span>\n <span class="pw-summary-val">{{ pipelineName }}</span>\n </div>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Environment</span>\n <span class="pw-summary-val">{{ selectedEnv }}</span>\n </div>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Type</span>\n <span class="pw-summary-val">{{ engineKind === \'processor\' ? \'Change Processor\' : engineKind === \'materializer\' ? \'Materializer\' : \'Queue Worker\' }}</span>\n </div>\n <if :="engineKind === \'processor\'">\n <div class="pw-summary-row">\n <span class="pw-summary-label">Watches</span>\n <span class="pw-summary-val">{{ proc_sourceType }}</span>\n </div>\n </if>\n <if :="engineKind === \'materializer\'">\n <div class="pw-summary-row">\n <span class="pw-summary-label">Source</span>\n <span class="pw-summary-val">{{ mat_sourceType }}</span>\n </div>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Target</span>\n <span class="pw-summary-val">{{ mat_targetType }}</span>\n </div>\n </if>\n <if :="engineKind === \'queue-worker\'">\n <div class="pw-summary-row">\n <span class="pw-summary-label">Job type</span>\n <span class="pw-summary-val">{{ effectiveJobType }}</span>\n </div>\n </if>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Handler</span>\n <span class="pw-summary-val">\n {{ effectiveHandlerName }}\n <span style="opacity:.4; font-family:sans-serif; font-size:11px;">\n {{ handlerMode === \'new\' ? \'(will be created)\' : \'\' }}\n </span>\n </span>\n </div>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Engine</span>\n <span class="pw-summary-val">{{ engineName }}</span>\n </div>\n <div class="pw-summary-row">\n <span class="pw-summary-label">Run recording</span>\n <span class="pw-summary-val" style="font-size:11px;">\n {{ handler_recordRuns === \'all\' ? \'all runs\' : handler_recordRuns === \'errors\' ? \'errors only\' : \'none\' }}\n </span>\n </div>\n <if :="showAdvanced || proc_mode !== \'worker\' || proc_bootstrap !== \'snapshot\' || proc_batchSize != 256 || proc_originMode !== \'all\' || mat_bootstrap !== \'snapshot\' || mat_batchSize != 128 || mat_originMode !== \'all\' || !mat_clearTargetOnRebuild || qw_concurrency != 1">\n <div class="pw-summary-row" style="border-bottom:none;">\n <span class="pw-summary-label">Options</span>\n <span class="pw-summary-val" style="font-size:11px;">\n <if :="engineKind === \'processor\'">\n mode={{ proc_mode }} · bootstrap={{ proc_mode === \'inline\' ? \'n/a\' : proc_bootstrap }} · batch={{ proc_batchSize }} · origin={{ proc_originMode }}\n </if>\n <if :="engineKind === \'materializer\'">\n bootstrap={{ mat_bootstrap }} · batch={{ mat_batchSize }} · origin={{ mat_originMode }} · clearOnRebuild={{ mat_clearTargetOnRebuild ? \'yes\' : \'no\' }}\n </if>\n <if :="engineKind === \'queue-worker\'">\n concurrency={{ qw_concurrency }}\n </if>\n </span>\n </div>\n </if>\n </div>\n\n <div style="font-size:12px; opacity:.45; line-height:1.6;">\n This will{{ handlerMode === \'new\' ? \' create the function, then\' : \'\' }} create the engine and pipeline record, and start it immediately.\n </div>\n\n \x3c!-- Feedback --\x3e\n <if :="error">\n <div class="pw-error">{{ error }}</div>\n </if>\n <if :="success">\n <div class="pw-success">{{ success }}</div>\n </if>\n\n <div class="pw-actions">\n <button class="pw-btn pw-btn--ghost" @click="goBack()">← Back</button>\n <button class="pw-btn pw-btn--green" :disabled="busy" @click="submit()">\n {{ busy ? \'Creating…\' : \'Create & Start Pipeline\' }}\n </button>\n </div>\n </if>\n</div>'};