@kedem/okdb 1.8.17 → 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.
- package/bin/okdb.js +1 -1
- package/docs/change-log.md +16 -16
- package/docs/embeddings.md +1 -1
- package/docs/functions.md +26 -25
- package/docs/http-cluster.md +157 -0
- package/docs/index.md +1 -0
- package/docs/manifest.json +5 -1
- package/docs/process-registry.md +185 -0
- package/docs/processors.md +61 -65
- package/docs/queue.md +23 -13
- package/docs/subscriptions.md +230 -0
- package/docs/worker-fleet.md +139 -0
- package/okdb-functions-runner-child.js +1 -1
- package/okdb-functions-sandbox-worker.js +1 -0
- package/okdb-http-worker-child.js +1 -0
- package/okdb-queue-load-handler.js +1 -0
- package/okdb-queue-spawn-child.js +1 -1
- package/okdb-views-bootstrap-worker.js +1 -1
- package/okdb-worker-child.js +1 -0
- package/okdb.js +1 -1
- package/package.json +1 -1
- package/public/sections/db/parts/db-overview.ok.js +1 -1
- package/public/sections/embeddings/parts/pipeline-create-panel.ok.js +1 -1
- package/public/sections/queue/parts/queue-jobs.ok.js +1 -1
- package/public/sections/system/index.ok.html +1 -1
- package/public/sections/system/modals/data-ops-modal.ok.js +1 -1
- package/public/sections/system/parts/system-process-panel.ok.js +1 -0
- package/public/sections/system/parts/system-processing-panel.ok.js +1 -0
- package/public/sections/system/parts/system-runtime-overview.ok.js +1 -0
- package/public/sections/system/parts/system-workers-panel.ok.js +1 -0
- package/types/environment.d.ts +21 -4
- package/types/features/embeddings.d.ts +11 -2
- package/types/features/queue.d.ts +27 -5
- package/types/features/views.d.ts +10 -10
- package/types/index.d.ts +54 -6
- package/types/options.d.ts +12 -0
- package/okdb-fts-handler.js +0 -1
- package/okdb-processor-worker.js +0 -1
- package/public/sections/processors/index.ok.html +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
export default{tag:"data-ops-modal",attr:{modal:{},envs:{default:()=>[]},onDone:{},initialTab:{default:"export"}},context(e){function n(){const e=new Date,n=e=>String(e).padStart(2,"0");return`${e.getFullYear()}-${n(e.getMonth()+1)}-${n(e.getDate())}`}return{tab:e.$attr.initialTab||"export",formatBytes:function(e){if(null==e)return"--";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(0===o?0:2)+" "+n[o]},get userEnvs(){return(e.$attr.envs||[]).filter(e=>!e.name.startsWith("~"))},typesForEnv(n){const t=(e.$attr.envs||[]).find(e=>e.name===n);return(t?.types||[]).filter(e=>!e.name.startsWith("~")).map(e=>e.name)},exportSelEnvs:{},exportIncludeChanges:!1,exportBusy:!1,exportError:null,exportDone:null,exportSelTypes:{},exportAllTypes:!0,get exportSelectedEnvNames(){return Object.keys(this.exportSelEnvs).filter(e=>this.exportSelEnvs[e])},get exportIsSingleEnv(){return 1===this.exportSelectedEnvNames.length},get exportSingleEnvTypes(){return this.exportIsSingleEnv?this.typesForEnv(this.exportSelectedEnvNames[0]):[]},get exportSelectedTypeNames(){return!this.exportIsSingleEnv||this.exportAllTypes?null:Object.keys(this.exportSelTypes).filter(e=>this.exportSelTypes[e])},get exportCanRun(){return this.exportSelectedEnvNames.length>0&&!this.exportBusy},exportToggleAllEnvs(e){const n={};for(const t of this.userEnvs)n[t.name]=e;this.exportSelEnvs=n},exportToggleAllTypes(e){const n={};for(const t of this.exportSingleEnvTypes)n[t]=e;this.exportSelTypes=n},exportInit(){0===Object.keys(this.exportSelEnvs).length&&this.userEnvs.length&&(this.exportSelEnvs={[this.userEnvs[0].name]:!0})},async runExport(){if(this.exportCanRun){this.exportBusy=!0,this.exportError=null,this.exportDone=null;try{const e=this.exportSelectedEnvNames,n=e.length>1,t=new URLSearchParams;n?t.set("envs",e.join(",")):t.set("env",e[0]),this.exportIncludeChanges&&t.set("includeChanges","true");const o=this.exportSelectedTypeNames;o&&o.length&&t.set("types",o.join(","));const r=await fetch(`${__ENV__.API}/system/export?${t}`);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e?.error?.message||`HTTP ${r.status}`)}const s=await r.blob(),a=URL.createObjectURL(s),p=document.createElement("a"),i=r.headers.get("content-disposition")||"",l=i.match(/filename="?([^"]+)"?/)?.[1]||`okdb-export-${e.join("+")}-${Date.now()}.ndjson`;p.href=a,p.download=l,document.body.appendChild(p),p.click(),document.body.removeChild(p),URL.revokeObjectURL(a);const d=(await s.text()).split("\n").filter(e=>e.trim()).map(e=>{try{return JSON.parse(e)}catch{return null}}).filter(Boolean);this.exportDone={filename:l,bytes:s.size,envCount:n?e.length:1,types:d.filter(e=>"schema"===e.r).length,records:d.filter(e=>"data"===e.r).length,changes:d.filter(e=>"change"===e.r).length}}catch(e){this.exportError=e?.message||String(e)}finally{this.exportBusy=!1}}},importFile:null,importEnvOverride:"",importSelTypes:{},importAllTypes:!0,importDropExisting:!1,importIncludeChanges:!1,importBatchSize:500,importShowAdvanced:!1,importBusy:!1,importError:null,importDone:null,importDragOver:!1,importFileTypes:[],get importSelectedTypeNames(){return this.importAllTypes?null:Object.keys(this.importSelTypes).filter(e=>this.importSelTypes[e])},get canImport(){return!!this.importFile&&!this.importBusy},onDrop(e){e.preventDefault(),this.importDragOver=!1;const n=e.dataTransfer?.files?.[0];n&&this._setImportFile(n)},onDragOver(e){e.preventDefault(),this.importDragOver=!0},onDragLeave(){this.importDragOver=!1},pickFile(){const e=document.createElement("input");e.type="file",e.accept=".ndjson,.jsonl,.json,.txt",e.onchange=e=>{const n=e.target.files?.[0];n&&this._setImportFile(n)},e.click()},async _setImportFile(e){this.importFile=e,this.importError=null,this.importDone=null,this.importFileTypes=[];try{const n=(await e.slice(0,32768).text()).split("\n").filter(e=>e.trim()),t=n.map(e=>{try{return JSON.parse(e)}catch{return null}}).filter(e=>"schema"===e?.r);this.importFileTypes=[...new Set(t.map(e=>e.type))];const o=n.map(e=>{try{return JSON.parse(e)}catch{return null}}).find(e=>"header"===e?.r);o?.envName&&this.importEnvOverride}catch{}},importToggleAllTypes(e){const n={};for(const t of this.importFileTypes)n[t]=e;this.importSelTypes=n},async runImport(){if(this.importFile){this.importBusy=!0,this.importError=null,this.importDone=null;try{const n=new URLSearchParams({dropExisting:String(this.importDropExisting),includeChanges:String(this.importIncludeChanges),batchSize:String(this.importBatchSize||500)});this.importEnvOverride&&n.set("env",this.importEnvOverride);const t=this.importSelectedTypeNames;t&&t.length&&n.set("types",t.join(","));const o=await fetch(`${__ENV__.API}/system/import?${n}`,{method:"POST",headers:{"content-type":"application/x-ndjson"},body:this.importFile}),r=await o.json().catch(()=>({}));if(!o.ok)throw new Error(r?.error?.message||`HTTP ${o.status}`);const s=r.data??r;this.importDone={types:s.types??0,records:s.records??0,changes:s.changes??0},e.$attr.onDone?.()}catch(e){this.importError=e?.message||String(e)}finally{this.importBusy=!1}}},backupMode:"server",backupDestDir:`./okdb-backup-${n()}`,backupSelEnvs:{},backupEnvMode:"all",backupCompact:!0,backupBusy:!1,backupError:null,backupResult:null,backupDlBusy:!1,backupDlError:null,backupDlDone:null,get backupEnvNames(){return"all"===this.backupEnvMode?null:Object.keys(this.backupSelEnvs).filter(e=>this.backupSelEnvs[e])},get canBackup(){return this.backupDestDir.trim().length>0&&!this.backupBusy},get canBackupDl(){return!this.backupDlBusy},async runBackup(){if(this.canBackup){this.backupBusy=!0,this.backupError=null,this.backupResult=null;try{const e=await fetch(`${__ENV__.API}/system/backup`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({destDir:this.backupDestDir.trim(),envNames:this.backupEnvNames,compact:this.backupCompact})}),n=await e.json().catch(()=>({}));if(!e.ok)throw new Error(n?.error?.message||`HTTP ${e.status}`);this.backupResult=n.data??n}catch(e){this.backupError=e?.message||String(e)}finally{this.backupBusy=!1}}},async runDownloadBackup(){this.backupDlBusy=!0,this.backupDlError=null,this.backupDlDone=null;try{const e=this.backupEnvNames,t=new URLSearchParams;e?.length?t.set("envs",e.join(",")):t.set("envs",this.userEnvs.map(e=>e.name).join(","));const o=await fetch(`${__ENV__.API}/system/export?${t}`);if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error||`HTTP ${o.status}`)}const r=await o.blob(),s=URL.createObjectURL(r),a=document.createElement("a"),p=o.headers.get("content-disposition")||"",i=p.match(/filename="?([^"]+)"?/)?.[1]||`okdb-backup-${n()}.ndjson`;a.href=s,a.download=i,document.body.appendChild(a),a.click(),document.body.removeChild(a),URL.revokeObjectURL(s),this.backupDlDone={filename:i,bytes:r.size}}catch(e){this.backupDlError=e?.message||String(e)}finally{this.backupDlBusy=!1}},close(){e.$attr.modal.close()},mount(){this.exportInit()}}},template:'\n<div style="min-width:520px; max-width:640px; display:flex; flex-direction:column; gap:0;">\n\n \x3c!-- Tab bar --\x3e\n <div style="display:flex; gap:2px; padding:3px; background:var(--okdb-app-bg-deep); border-radius:9px; margin-bottom:22px; border:1px solid var(--okdb-app-border);">\n <button @click="tab=\'export\'"\n :style="tab===\'export\' ? \'flex:1;background:var(--okdb-app-surface-2);color:var(--okdb-accent);border-color:color-mix(in srgb, var(--okdb-accent) 27%, transparent);font-weight:600;\' : \'flex:1;background:transparent;border-color:transparent;opacity:.5;\'"\n style="padding:7px 0;border-radius:7px;font-size:13px;transition:all .15s;cursor:pointer;">\n ⬇ Export\n </button>\n <button @click="tab=\'import\'"\n :style="tab===\'import\' ? \'flex:1;background:var(--okdb-app-surface-2);color:var(--okdb-accent);border-color:color-mix(in srgb, var(--okdb-accent) 27%, transparent);font-weight:600;\' : \'flex:1;background:transparent;border-color:transparent;opacity:.5;\'"\n style="padding:7px 0;border-radius:7px;font-size:13px;transition:all .15s;cursor:pointer;">\n ⬆ Import\n </button>\n <button @click="tab=\'backup\'"\n :style="tab===\'backup\' ? \'flex:1;background:var(--okdb-app-surface-2);color:var(--okdb-purple);border-color:color-mix(in srgb, var(--okdb-purple) 27%, transparent);font-weight:600;\' : \'flex:1;background:transparent;border-color:transparent;opacity:.5;\'"\n style="padding:7px 0;border-radius:7px;font-size:13px;transition:all .15s;cursor:pointer;">\n 🗄 Backup\n </button>\n </div>\n\n \x3c!-- ═══ EXPORT ═══════════════════════════════════════════════════════════ --\x3e\n <if :="tab === \'export\'">\n <div style="display:flex;flex-direction:column;gap:18px;">\n\n <div style="font-size:12px;opacity:.5;line-height:1.5;">\n Streaming NDJSON export — portable across OKDB versions, importable into any instance.\n </div>\n\n \x3c!-- Environments --\x3e\n <div class="dops-field">\n <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">\n <label class="dops-label" style="flex:1;margin:0;">Environments</label>\n <button @click="exportToggleAllEnvs(true)" class="dops-micro">all</button>\n <button @click="exportToggleAllEnvs(false)" class="dops-micro">none</button>\n </div>\n <div style="display:flex;flex-wrap:wrap;gap:6px;">\n <for :="userEnvs" let:e>\n <label\n :style="exportSelEnvs[e.name] ? \'background:var(--okdb-app-surface-2);border-color:color-mix(in srgb, var(--okdb-accent) 33%, transparent);color:#e2eaf4;\' : \'background:var(--okdb-app-surface);border-color:var(--okdb-app-border);opacity:.55;\'"\n style="display:flex;align-items:center;gap:5px;padding:5px 11px;border-radius:999px;font-size:12px;cursor:pointer;border:1px solid;transition:all .12s;user-select:none;">\n <input type="checkbox"\n :checked="!!exportSelEnvs[e.name]"\n @change="exportSelEnvs = {...exportSelEnvs, [e.name]: $el.checked}"\n style="display:none;" />\n <span :style="exportSelEnvs[e.name] ? \'color:var(--okdb-accent);font-size:10px;\' : \'display:none;\'">✓</span>\n {{e.name}}\n <span style="opacity:.4;font-size:10px;">{{e.types ? e.types.filter(t=>!t.name.startsWith(\'~\')).length : 0}} types</span>\n </label>\n </for>\n </div>\n </div>\n\n \x3c!-- Types (only for single env) --\x3e\n <if :="exportIsSingleEnv && exportSingleEnvTypes.length > 0">\n <div class="dops-field">\n <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">\n <label class="dops-label" style="flex:1;margin:0;">Types</label>\n <label class="dops-check" style="font-size:11px;gap:4px;">\n <input type="checkbox" :checked="exportAllTypes" @change="exportAllTypes = $el.checked" />\n <span style="opacity:.6;">All types</span>\n </label>\n </div>\n <if :="!exportAllTypes">\n <div style="display:flex;flex-direction:column;gap:0;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:8px;overflow:hidden;">\n <div style="display:flex;align-items:center;padding:5px 10px;border-bottom:1px solid var(--okdb-app-border);gap:8px;">\n <button @click="exportToggleAllTypes(true)" class="dops-micro">all</button>\n <button @click="exportToggleAllTypes(false)" class="dops-micro">none</button>\n </div>\n <div style="display:flex;flex-direction:column;max-height:140px;overflow-y:auto;">\n <for :="exportSingleEnvTypes" let:t>\n <label style="display:flex;align-items:center;gap:8px;padding:6px 10px;cursor:pointer;transition:background .1s;"\n :style="exportSelTypes[t] ? \'background:#1a2a1a;\' : \'\'"\n @mouseenter="$el.style.background=exportSelTypes[t]?\'#1a2a1a\':\'var(--okdb-app-surface)\'"\n @mouseleave="$el.style.background=exportSelTypes[t]?\'#1a2a1a\':\'\'">\n <input type="checkbox"\n :checked="!!exportSelTypes[t]"\n @change="exportSelTypes = {...exportSelTypes, [t]: $el.checked}" />\n <span style="font-family:monospace;font-size:12px;">{{t}}</span>\n </label>\n </for>\n </div>\n </div>\n </if>\n </div>\n </if>\n <if :="!exportIsSingleEnv && exportSelectedEnvNames.length > 1">\n <div style="font-size:11px;opacity:.4;font-style:italic;">All types will be exported from each selected environment.</div>\n </if>\n\n \x3c!-- Options --\x3e\n <label class="dops-check">\n <input type="checkbox" :checked="exportIncludeChanges" @change="exportIncludeChanges = $el.checked" />\n <span>Include changelog <span style="opacity:.4;font-size:11px;">(sync history)</span></span>\n </label>\n\n <if :="exportError"><div class="dops-error">{{exportError}}</div></if>\n\n \x3c!-- Success --\x3e\n <if :="exportDone">\n <div class="dops-success">\n <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">\n <span>✓</span><span style="font-weight:600;">Downloaded</span>\n <span style="opacity:.4;font-size:11px;margin-left:auto;">{{formatBytes(exportDone.bytes)}}</span>\n </div>\n <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;font-size:12px;">\n <if :="exportDone.envCount > 1">\n <div class="dops-stat"><div class="dops-stat-n">{{exportDone.envCount}}</div><div>envs</div></div>\n </if>\n <div class="dops-stat"><div class="dops-stat-n">{{exportDone.types}}</div><div>types</div></div>\n <div class="dops-stat"><div class="dops-stat-n">{{exportDone.records}}</div><div>records</div></div>\n <div class="dops-stat"><div class="dops-stat-n">{{exportDone.changes}}</div><div>changes</div></div>\n </div>\n <div style="font-size:10px;opacity:.35;margin-top:6px;font-family:monospace;word-break:break-all;">{{exportDone.filename}}</div>\n </div>\n </if>\n\n <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:4px;">\n <button @click="close()" class="dops-btn-ghost">Cancel</button>\n <button @click="runExport()" :disabled="!exportCanRun" class="dops-btn-primary">\n <if :="exportBusy"><span class="dops-spinner"></span> Exporting…</if>\n <if :="!exportBusy">⬇ Download NDJSON</if>\n </button>\n </div>\n </div>\n </if>\n\n \x3c!-- ═══ IMPORT ═══════════════════════════════════════════════════════════ --\x3e\n <if :="tab === \'import\'">\n <div style="display:flex;flex-direction:column;gap:18px;">\n\n <div style="font-size:12px;opacity:.5;line-height:1.5;">\n Upload a OKDB NDJSON export. Types, indexes, and records are restored automatically.\n Multi-env files restore each environment separately.\n </div>\n\n \x3c!-- Drop zone --\x3e\n <div @click="pickFile()" @drop="onDrop($event)" @dragover="onDragOver($event)" @dragleave="onDragLeave()"\n :style="importDragOver ? \'border-color:var(--okdb-accent);background:color-mix(in srgb, var(--okdb-accent) 5%, transparent);\' : importFile ? \'border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);background:#0a1a10;\' : \'border-color:var(--okdb-app-border);background:var(--okdb-app-bg-deep);\'"\n style="border:2px dashed;border-radius:10px;padding:24px 16px;text-align:center;cursor:pointer;transition:all .15s;">\n <if :="!importFile">\n <div style="font-size:22px;opacity:.4;margin-bottom:8px;">📂</div>\n <div style="font-size:13px;opacity:.7;">Drop .ndjson file here or <span style="color:var(--okdb-accent);">click to browse</span></div>\n <div style="font-size:11px;opacity:.3;margin-top:4px;">.ndjson · .jsonl · .json</div>\n </if>\n <if :="importFile">\n <div style="font-size:20px;margin-bottom:6px;">📄</div>\n <div style="font-size:13px;font-weight:600;color:var(--okdb-success);font-family:monospace;word-break:break-all;">{{importFile.name}}</div>\n <div style="font-size:11px;opacity:.45;margin-top:3px;">{{formatBytes(importFile.size)}}\n <if :="importFileTypes.length"> · {{importFileTypes.length}} type{{importFileTypes.length!==1?\'s\':\'\'}} detected</if>\n · click to change</div>\n </if>\n </div>\n\n \x3c!-- Types filter (from file) --\x3e\n <if :="importFileTypes.length > 0">\n <div class="dops-field">\n <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">\n <label class="dops-label" style="flex:1;margin:0;">Types to import</label>\n <label class="dops-check" style="font-size:11px;gap:4px;">\n <input type="checkbox" :checked="importAllTypes" @change="importAllTypes = $el.checked" />\n <span style="opacity:.6;">All</span>\n </label>\n </div>\n <if :="!importAllTypes">\n <div style="display:flex;flex-direction:column;gap:0;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:8px;overflow:hidden;">\n <div style="display:flex;padding:5px 10px;border-bottom:1px solid var(--okdb-app-border);gap:8px;">\n <button @click="importToggleAllTypes(true)" class="dops-micro">all</button>\n <button @click="importToggleAllTypes(false)" class="dops-micro">none</button>\n </div>\n <div style="display:flex;flex-direction:column;max-height:130px;overflow-y:auto;">\n <for :="importFileTypes" let:t>\n <label style="display:flex;align-items:center;gap:8px;padding:6px 10px;cursor:pointer;"\n :style="importSelTypes[t] ? \'background:#1a2a1a;\' : \'\'">\n <input type="checkbox"\n :checked="!!importSelTypes[t]"\n @change="importSelTypes = {...importSelTypes, [t]: $el.checked}" />\n <span style="font-family:monospace;font-size:12px;">{{t}}</span>\n </label>\n </for>\n </div>\n </div>\n </if>\n </div>\n </if>\n\n \x3c!-- Target env override --\x3e\n <div class="dops-field">\n <label class="dops-label">Target environment\n <span style="opacity:.35;font-size:11px;font-weight:400;">(leave empty to use the file\'s source env)</span>\n </label>\n <select :value="importEnvOverride" @change="importEnvOverride = $el.value" class="dops-select">\n <option value="">— from file header —</option>\n <for :="userEnvs" let:e>\n <option :value="e.name">{{e.name}}</option>\n </for>\n </select>\n </div>\n\n \x3c!-- Options --\x3e\n <div style="display:flex;flex-direction:column;gap:7px;">\n <label class="dops-check">\n <input type="checkbox" :checked="importDropExisting" @change="importDropExisting = $el.checked" />\n <span>Drop & recreate existing types <span style="opacity:.4;font-size:11px;">(replaces current data)</span></span>\n </label>\n <label class="dops-check">\n <input type="checkbox" :checked="importIncludeChanges" @change="importIncludeChanges = $el.checked" />\n <span>Restore changelog rows</span>\n </label>\n </div>\n\n \x3c!-- Advanced --\x3e\n <div>\n <button @click="importShowAdvanced=!importShowAdvanced"\n style="font-size:11px;opacity:.4;background:transparent;border:none;padding:0;cursor:pointer;">\n {{importShowAdvanced?\'▴ hide\':\'▾\'}} advanced\n </button>\n <if :="importShowAdvanced">\n <div class="dops-field" style="margin-top:10px;">\n <label class="dops-label">Batch size <span style="opacity:.35;font-size:11px;">(records per write-txn)</span></label>\n <input type="number" min="10" max="10000" step="10"\n :value="importBatchSize" @input="importBatchSize=Number($el.value)||500"\n class="dops-input" style="max-width:120px;" />\n </div>\n </if>\n </div>\n\n <if :="importError"><div class="dops-error">{{importError}}</div></if>\n\n <if :="importDone">\n <div class="dops-success">\n <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">\n <span>✓</span><span style="font-weight:600;">Import complete</span>\n </div>\n <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;font-size:12px;">\n <div class="dops-stat"><div class="dops-stat-n">{{importDone.types}}</div><div>types</div></div>\n <div class="dops-stat"><div class="dops-stat-n">{{importDone.records}}</div><div>records</div></div>\n <div class="dops-stat"><div class="dops-stat-n">{{importDone.changes}}</div><div>changes</div></div>\n </div>\n </div>\n </if>\n\n <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:4px;">\n <button @click="close()" class="dops-btn-ghost">Cancel</button>\n <button @click="runImport()" :disabled="!canImport" class="dops-btn-primary">\n <if :="importBusy"><span class="dops-spinner"></span> Importing…</if>\n <if :="!importBusy">⬆ Import File</if>\n </button>\n </div>\n </div>\n </if>\n\n \x3c!-- ═══ BACKUP ════════════════════════════════════════════════════════════ --\x3e\n <if :="tab === \'backup\'">\n <div style="display:flex;flex-direction:column;gap:0;">\n\n \x3c!-- Mode tabs --\x3e\n <div style="display:flex;gap:0;margin-bottom:18px;border-bottom:1px solid var(--okdb-app-border);">\n <button @click="backupMode=\'server\'"\n :style="backupMode===\'server\' ? \'color:var(--okdb-purple);border-bottom:2px solid var(--okdb-purple);opacity:1;\' : \'border-bottom:2px solid transparent;opacity:.45;\'"\n style="padding:6px 16px;background:transparent;border:none;border-radius:0;font-size:13px;cursor:pointer;transition:all .15s;margin-bottom:-1px;">\n 🗄 Server backup\n </button>\n <button @click="backupMode=\'download\'"\n :style="backupMode===\'download\' ? \'color:var(--okdb-purple);border-bottom:2px solid var(--okdb-purple);opacity:1;\' : \'border-bottom:2px solid transparent;opacity:.45;\'"\n style="padding:6px 16px;background:transparent;border:none;border-radius:0;font-size:13px;cursor:pointer;transition:all .15s;margin-bottom:-1px;">\n ⬇ Download to PC\n </button>\n </div>\n\n \x3c!-- Server backup mode --\x3e\n <if :="backupMode === \'server\'">\n <div style="display:flex;flex-direction:column;gap:16px;">\n <div style="font-size:12px;opacity:.5;line-height:1.5;">\n Hot LMDB page-copy backup — atomic, exact snapshot, no serialisation.\n Writes to a directory <em>on the server</em>. The output can be used\n directly as a OKDB data path.\n </div>\n\n \x3c!-- Dest dir --\x3e\n <div class="dops-field">\n <label class="dops-label">Destination directory\n <span style="opacity:.35;font-size:11px;font-weight:400;">(server-side path)</span>\n </label>\n <input type="text" :value="backupDestDir" @input="backupDestDir=$el.value"\n class="dops-input" style="font-family:monospace;" spellcheck="false" />\n </div>\n\n \x3c!-- Env selection --\x3e\n <div class="dops-field">\n <label class="dops-label" style="margin-bottom:8px;">Environments</label>\n <div style="display:flex;gap:12px;margin-bottom:8px;">\n <label class="dops-check">\n <input type="radio" name="bkEnvMode" value="all"\n :checked="backupEnvMode===\'all\'" @change="backupEnvMode=\'all\'" />\n <span>All</span>\n </label>\n <label class="dops-check">\n <input type="radio" name="bkEnvMode" value="selected"\n :checked="backupEnvMode===\'selected\'" @change="backupEnvMode=\'selected\'" />\n <span>Select…</span>\n </label>\n </div>\n <if :="backupEnvMode===\'selected\'">\n <div style="display:flex;flex-wrap:wrap;gap:6px;padding:10px;background:var(--okdb-app-bg-deep);border-radius:8px;border:1px solid var(--okdb-app-border);">\n <for :="userEnvs" let:e>\n <label\n :style="backupSelEnvs[e.name] ? \'background:var(--okdb-app-surface);border-color:color-mix(in srgb, var(--okdb-purple) 27%, transparent);color:#e2d8f4;\' : \'background:var(--okdb-app-surface);border-color:var(--okdb-app-border);opacity:.55;\'"\n style="display:flex;align-items:center;gap:5px;padding:5px 11px;border-radius:999px;font-size:12px;cursor:pointer;border:1px solid;transition:all .12s;user-select:none;">\n <input type="checkbox"\n :checked="!!backupSelEnvs[e.name]"\n @change="backupSelEnvs={...backupSelEnvs,[e.name]:$el.checked}"\n style="display:none;" />\n <span :style="backupSelEnvs[e.name]?\'color:var(--okdb-purple);font-size:10px;\':\'display:none;\'">✓</span>\n {{e.name}}\n </label>\n </for>\n </div>\n </if>\n </div>\n\n <label class="dops-check">\n <input type="checkbox" :checked="backupCompact" @change="backupCompact=$el.checked" />\n <span>Compact while copying <span style="opacity:.4;font-size:11px;">(smaller files, slightly slower)</span></span>\n </label>\n\n <if :="backupError"><div class="dops-error">{{backupError}}</div></if>\n\n <if :="backupResult">\n <div class="dops-success" style="border-color:color-mix(in srgb, var(--okdb-purple) 20%, transparent);">\n <div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">\n <span>✓</span>\n <span style="font-weight:600;color:var(--okdb-app-fg);">Server backup complete</span>\n <span style="opacity:.4;font-size:11px;margin-left:auto;">{{backupResult.durationMs}}ms</span>\n </div>\n <div style="display:flex;flex-direction:column;gap:4px;">\n <for :="backupResult.envs||[]" let:e>\n <div style="background:#1a1228;border-radius:6px;padding:7px 10px;font-size:12px;">\n <div style="display:flex;gap:8px;align-items:center;">\n <span style="font-family:monospace;font-weight:600;color:var(--okdb-purple);">{{e.name}}</span>\n <span style="opacity:.35;font-size:11px;">{{formatBytes(e.sizeBefore)}} → {{formatBytes(e.sizeAfter)}}</span>\n </div>\n <div style="font-family:monospace;font-size:10px;opacity:.4;margin-top:3px;word-break:break-all;">{{e.dest}}</div>\n </div>\n </for>\n </div>\n </div>\n </if>\n\n <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:4px;">\n <button @click="close()" class="dops-btn-ghost">Cancel</button>\n <button @click="runBackup()" :disabled="!canBackup"\n style="padding:8px 20px;border-radius:8px;font-size:13px;font-weight:600;background:linear-gradient(135deg,#1e1530,#150f22);border:1px solid color-mix(in srgb, var(--okdb-purple) 33%, transparent);color:var(--okdb-purple);cursor:pointer;display:flex;align-items:center;gap:6px;transition:all .15s;">\n <if :="backupBusy"><span class="dops-spinner" style="border-color:color-mix(in srgb, var(--okdb-purple) 27%, transparent);border-top-color:var(--okdb-purple);"></span> Backing up…</if>\n <if :="!backupBusy">🗄 Run Server Backup</if>\n </button>\n </div>\n </div>\n </if>\n\n \x3c!-- Download backup mode --\x3e\n <if :="backupMode === \'download\'">\n <div style="display:flex;flex-direction:column;gap:16px;">\n <div style="font-size:12px;opacity:.5;line-height:1.5;">\n Export all environments as a single portable NDJSON file and download it to your computer.\n This file can be imported back into any OKDB instance.\n </div>\n\n \x3c!-- Env selection --\x3e\n <div class="dops-field">\n <label class="dops-label" style="margin-bottom:8px;">Environments to include</label>\n <div style="display:flex;gap:12px;margin-bottom:8px;">\n <label class="dops-check">\n <input type="radio" name="dlEnvMode" value="all"\n :checked="backupEnvMode===\'all\'" @change="backupEnvMode=\'all\'" />\n <span>All environments</span>\n </label>\n <label class="dops-check">\n <input type="radio" name="dlEnvMode" value="selected"\n :checked="backupEnvMode===\'selected\'" @change="backupEnvMode=\'selected\'" />\n <span>Select…</span>\n </label>\n </div>\n <if :="backupEnvMode===\'selected\'">\n <div style="display:flex;flex-wrap:wrap;gap:6px;padding:10px;background:var(--okdb-app-bg-deep);border-radius:8px;border:1px solid var(--okdb-app-border);">\n <for :="userEnvs" let:e>\n <label\n :style="backupSelEnvs[e.name] ? \'background:var(--okdb-app-surface);border-color:color-mix(in srgb, var(--okdb-purple) 27%, transparent);\' : \'background:var(--okdb-app-surface);border-color:var(--okdb-app-border);opacity:.55;\'"\n style="display:flex;align-items:center;gap:5px;padding:5px 11px;border-radius:999px;font-size:12px;cursor:pointer;border:1px solid;transition:all .12s;user-select:none;">\n <input type="checkbox"\n :checked="!!backupSelEnvs[e.name]"\n @change="backupSelEnvs={...backupSelEnvs,[e.name]:$el.checked}"\n style="display:none;" />\n <span :style="backupSelEnvs[e.name]?\'color:var(--okdb-purple);font-size:10px;\':\'display:none;\'">✓</span>\n {{e.name}}\n </label>\n </for>\n </div>\n </if>\n </div>\n\n <if :="backupDlError"><div class="dops-error">{{backupDlError}}</div></if>\n\n <if :="backupDlDone">\n <div class="dops-success" style="border-color:color-mix(in srgb, var(--okdb-purple) 20%, transparent);">\n <div style="display:flex;align-items:center;gap:8px;">\n <span>✓</span>\n <span style="font-weight:600;color:var(--okdb-app-fg);">Downloaded</span>\n <span style="opacity:.4;font-size:11px;">{{formatBytes(backupDlDone.bytes)}}</span>\n </div>\n <div style="font-family:monospace;font-size:10px;opacity:.35;margin-top:5px;word-break:break-all;">{{backupDlDone.filename}}</div>\n </div>\n </if>\n\n <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:4px;">\n <button @click="close()" class="dops-btn-ghost">Cancel</button>\n <button @click="runDownloadBackup()" :disabled="!canBackupDl"\n style="padding:8px 20px;border-radius:8px;font-size:13px;font-weight:600;background:linear-gradient(135deg,#1e1530,#150f22);border:1px solid color-mix(in srgb, var(--okdb-purple) 33%, transparent);color:var(--okdb-purple);cursor:pointer;display:flex;align-items:center;gap:6px;transition:all .15s;">\n <if :="backupDlBusy"><span class="dops-spinner" style="border-color:color-mix(in srgb, var(--okdb-purple) 27%, transparent);border-top-color:var(--okdb-purple);"></span> Downloading…</if>\n <if :="!backupDlBusy">⬇ Download Backup</if>\n </button>\n </div>\n </div>\n </if>\n\n </div>\n </if>\n\n</div>\n\n<style>\n .dops-field { display:flex; flex-direction:column; gap:5px; }\n .dops-label { font-size:12px; opacity:.6; font-weight:500; }\n .dops-input {\n background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px;\n color:inherit; padding:7px 10px; font-size:13px; width:100%;\n box-sizing:border-box; transition:border-color .15s;\n }\n .dops-input:focus { outline:none; border-color:color-mix(in srgb, var(--okdb-accent) 33%, transparent); }\n .dops-select {\n background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px;\n color:inherit; padding:7px 10px; font-size:13px; width:100%; box-sizing:border-box; cursor:pointer;\n }\n .dops-select:focus { outline:none; border-color:color-mix(in srgb, var(--okdb-accent) 33%, transparent); }\n .dops-check { display:flex; align-items:center; gap:8px; font-size:13px; cursor:pointer; user-select:none; }\n .dops-micro {\n padding:2px 8px; border-radius:4px; font-size:10px; background:transparent;\n border:1px solid var(--okdb-app-border); opacity:.5; cursor:pointer; transition:opacity .1s;\n }\n .dops-micro:hover { opacity:.85; }\n .dops-error {\n background:#2a1010; border:1px solid color-mix(in srgb, var(--okdb-danger) 27%, transparent); border-radius:8px;\n padding:10px 12px; font-size:12px; color:var(--okdb-danger); line-height:1.5;\n }\n .dops-success {\n background:#0d1a14; border:1px solid color-mix(in srgb, var(--okdb-success) 20%, transparent); border-radius:8px;\n padding:12px 14px; font-size:13px; color:#d1fae5;\n }\n .dops-stat {\n background:#071410; border-radius:6px; padding:8px; text-align:center; font-size:11px; opacity:.8;\n }\n .dops-stat-n { font-size:18px; font-weight:700; color:var(--okdb-success); }\n .dops-btn-primary {\n padding:8px 20px; border-radius:8px; font-size:13px; font-weight:600;\n background:linear-gradient(135deg,#1e4060,#1a3050);\n border:1px solid color-mix(in srgb, var(--okdb-accent) 33%, transparent); color:var(--okdb-accent);\n cursor:pointer; display:flex; align-items:center; gap:6px; transition:all .15s;\n }\n .dops-btn-primary:hover:not(:disabled) { border-color:color-mix(in srgb, var(--okdb-accent) 53%, transparent); }\n .dops-btn-primary:disabled { opacity:.35; cursor:not-allowed; }\n .dops-btn-ghost {\n padding:8px 16px; border-radius:8px; font-size:13px;\n background:transparent; border:1px solid var(--okdb-app-border); opacity:.55; cursor:pointer;\n }\n .dops-btn-ghost:hover { opacity:.85; }\n .dops-spinner {\n display:inline-block; width:12px; height:12px;\n border:2px solid color-mix(in srgb, var(--okdb-accent) 20%, transparent); border-top-color:var(--okdb-accent);\n border-radius:50%; animation:dops-spin .6s linear infinite;\n }\n @keyframes dops-spin { to { transform:rotate(360deg); } }\n</style>\n'};
|
|
1
|
+
export default{tag:"data-ops-modal",attr:{modal:{},envs:{default:()=>[]},onDone:{},initialTab:{default:"export"}},context(e){function n(){const e=new Date,n=e=>String(e).padStart(2,"0");return`${e.getFullYear()}-${n(e.getMonth()+1)}-${n(e.getDate())}`}return{tab:e.$attr.initialTab||"export",formatBytes:function(e){if(null==e)return"--";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(0===o?0:2)+" "+n[o]},get userEnvs(){return(e.$attr.envs||[]).filter(e=>!e.name.startsWith("~"))},typesForEnv(n){const t=(e.$attr.envs||[]).find(e=>e.name===n);return(t?.types||[]).filter(e=>!e.name.startsWith("~")).map(e=>e.name)},exportSelEnvs:{},exportIncludeChanges:!1,exportBusy:!1,exportError:null,exportDone:null,exportSelTypes:{},exportAllTypes:!0,get exportSelectedEnvNames(){return Object.keys(this.exportSelEnvs).filter(e=>this.exportSelEnvs[e])},get exportIsSingleEnv(){return 1===this.exportSelectedEnvNames.length},get exportSingleEnvTypes(){return this.exportIsSingleEnv?this.typesForEnv(this.exportSelectedEnvNames[0]):[]},get exportSelectedTypeNames(){return!this.exportIsSingleEnv||this.exportAllTypes?null:Object.keys(this.exportSelTypes).filter(e=>this.exportSelTypes[e])},get exportCanRun(){return this.exportSelectedEnvNames.length>0&&!this.exportBusy},exportToggleAllEnvs(e){const n={};for(const t of this.userEnvs)n[t.name]=e;this.exportSelEnvs=n},exportToggleAllTypes(e){const n={};for(const t of this.exportSingleEnvTypes)n[t]=e;this.exportSelTypes=n},exportInit(){0===Object.keys(this.exportSelEnvs).length&&this.userEnvs.length&&(this.exportSelEnvs={[this.userEnvs[0].name]:!0})},async runExport(){if(this.exportCanRun){this.exportBusy=!0,this.exportError=null,this.exportDone=null;try{const e=this.exportSelectedEnvNames,n=e.length>1,t=new URLSearchParams;n?t.set("envs",e.join(",")):t.set("env",e[0]),this.exportIncludeChanges&&t.set("includeChanges","true");const o=this.exportSelectedTypeNames;o&&o.length&&t.set("types",o.join(","));const r=await fetch(`${__ENV__.API}/system/export?${t}`);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e?.error?.message||`HTTP ${r.status}`)}const a=await r.blob(),s=URL.createObjectURL(a),i=document.createElement("a"),p=r.headers.get("content-disposition")||"",l=p.match(/filename="?([^"]+)"?/)?.[1]||`okdb-export-${e.join("+")}-${Date.now()}.ndjson`;i.href=s,i.download=l,document.body.appendChild(i),i.click(),document.body.removeChild(i),URL.revokeObjectURL(s);const c=(await a.text()).split("\n").filter(e=>e.trim()).map(e=>{try{return JSON.parse(e)}catch{return null}}).filter(Boolean);this.exportDone={filename:l,bytes:a.size,envCount:n?e.length:1,types:c.filter(e=>"schema"===e.r).length,records:c.filter(e=>"data"===e.r).length,changes:c.filter(e=>"change"===e.r).length}}catch(e){this.exportError=e?.message||String(e)}finally{this.exportBusy=!1}}},importFile:null,importEnvOverride:"",importSelTypes:{},importAllTypes:!0,importDropExisting:!1,importIncludeChanges:!1,importBatchSize:500,importShowAdvanced:!1,importBusy:!1,importError:null,importDone:null,importDragOver:!1,importFileTypes:[],get importSelectedTypeNames(){return this.importAllTypes?null:Object.keys(this.importSelTypes).filter(e=>this.importSelTypes[e])},get canImport(){return!!this.importFile&&!this.importBusy},onDrop(e){e.preventDefault(),this.importDragOver=!1;const n=e.dataTransfer?.files?.[0];n&&this._setImportFile(n)},onDragOver(e){e.preventDefault(),this.importDragOver=!0},onDragLeave(){this.importDragOver=!1},pickFile(){const e=document.createElement("input");e.type="file",e.accept=".ndjson,.jsonl,.json,.txt",e.onchange=e=>{const n=e.target.files?.[0];n&&this._setImportFile(n)},e.click()},async _setImportFile(e){this.importFile=e,this.importError=null,this.importDone=null,this.importFileTypes=[];try{const n=(await e.slice(0,32768).text()).split("\n").filter(e=>e.trim()),t=n.map(e=>{try{return JSON.parse(e)}catch{return null}}).filter(e=>"schema"===e?.r);this.importFileTypes=[...new Set(t.map(e=>e.type))];const o=n.map(e=>{try{return JSON.parse(e)}catch{return null}}).find(e=>"header"===e?.r);o?.envName&&this.importEnvOverride}catch{}},importToggleAllTypes(e){const n={};for(const t of this.importFileTypes)n[t]=e;this.importSelTypes=n},async runImport(){if(this.importFile){this.importBusy=!0,this.importError=null,this.importDone=null;try{const n=new URLSearchParams({dropExisting:String(this.importDropExisting),includeChanges:String(this.importIncludeChanges),batchSize:String(this.importBatchSize||500)});this.importEnvOverride&&n.set("env",this.importEnvOverride);const t=this.importSelectedTypeNames;t&&t.length&&n.set("types",t.join(","));const o=await fetch(`${__ENV__.API}/system/import?${n}`,{method:"POST",headers:{"content-type":"application/x-ndjson"},body:this.importFile}),r=await o.json().catch(()=>({}));if(!o.ok)throw new Error(r?.error?.message||`HTTP ${o.status}`);const a=r.data??r;this.importDone={types:a.types??0,records:a.records??0,changes:a.changes??0},e.$attr.onDone?.()}catch(e){this.importError=e?.message||String(e)}finally{this.importBusy=!1}}},backupMode:"server",backupDestDir:`./okdb-backup-${n()}`,backupSelEnvs:{},backupEnvMode:"all",backupCompact:!0,backupBusy:!1,backupError:null,backupResult:null,backupDlBusy:!1,backupDlError:null,backupDlDone:null,get backupEnvNames(){return"all"===this.backupEnvMode?null:Object.keys(this.backupSelEnvs).filter(e=>this.backupSelEnvs[e])},get canBackup(){return this.backupDestDir.trim().length>0&&!this.backupBusy},get canBackupDl(){return!this.backupDlBusy},async runBackup(){if(!this.canBackup)return;const n=(this.backupDestDir||"").trim();if(!n)return void(this.backupError="Backup destination directory is required.");const t=`Write a server-side backup of ${this.backupEnvNames?.length||"all"} env(s) to "${n}" on the SERVER? Make sure this path is correct.`;if(e.$ok?.shared?.confirm?await e.$ok.shared.confirm(t):"function"!=typeof confirm||confirm(t)){this.backupBusy=!0,this.backupError=null,this.backupResult=null;try{const e=await fetch(`${__ENV__.API}/system/backup`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({destDir:n,envNames:this.backupEnvNames,compact:this.backupCompact})}),t=await e.json().catch(()=>({}));if(!e.ok)throw new Error(t?.error?.message||`HTTP ${e.status}`);this.backupResult=t.data??t}catch(e){this.backupError=e?.message||String(e)}finally{this.backupBusy=!1}}},async runDownloadBackup(){this.backupDlBusy=!0,this.backupDlError=null,this.backupDlDone=null;try{const e=this.backupEnvNames,t=new URLSearchParams;e?.length?t.set("envs",e.join(",")):t.set("envs",this.userEnvs.map(e=>e.name).join(","));const o=await fetch(`${__ENV__.API}/system/export?${t}`);if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error||`HTTP ${o.status}`)}const r=await o.blob(),a=URL.createObjectURL(r),s=document.createElement("a"),i=o.headers.get("content-disposition")||"",p=i.match(/filename="?([^"]+)"?/)?.[1]||`okdb-backup-${n()}.ndjson`;s.href=a,s.download=p,document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(a),this.backupDlDone={filename:p,bytes:r.size}}catch(e){this.backupDlError=e?.message||String(e)}finally{this.backupDlBusy=!1}},compactEnv:e.$attr.initialEnv||"default",compactBusy:!1,compactError:null,compactResult:null,async runCompact(n){const t=(this.compactEnv||"default").trim();if(!t||this.compactBusy)return;const o=n?`Logical rebuild reads "${t}" into a fresh, leak-free file and parks writers for the duration (longer than a normal compact). Intended for page-leaked/unhealthy envs or for testing the rebuild path. Continue?`:`Compact "${t}"? It reclaims free pages and briefly parks writers.`;if(e.$ok?.shared?.confirm?await e.$ok.shared.confirm(o):"function"!=typeof confirm||confirm(o)){this.compactBusy=!0,this.compactError=null,this.compactResult=null;try{const e=await fetch(`${__ENV__.API}/env/${encodeURIComponent(t)}/compact`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({repair:!!n})}),o=await e.json().catch(()=>({}));if(!e.ok)throw new Error(o?.error?.message||o?.error||`HTTP ${e.status}`);this.compactResult=o.result??o.data??o}catch(e){this.compactError=e?.message||String(e)}finally{this.compactBusy=!1}}},close(){e.$attr.modal.close()},mount(){this.exportInit()}}},template:'\n<div style="min-width:520px; max-width:640px; display:flex; flex-direction:column; gap:0;">\n\n \x3c!-- Tab bar --\x3e\n <div style="display:flex; gap:2px; padding:3px; background:var(--okdb-app-bg-deep); border-radius:9px; margin-bottom:22px; border:1px solid var(--okdb-app-border);">\n <button @click="tab=\'export\'"\n :style="tab===\'export\' ? \'flex:1;background:var(--okdb-app-surface-2);color:var(--okdb-accent);border-color:color-mix(in srgb, var(--okdb-accent) 27%, transparent);font-weight:600;\' : \'flex:1;background:transparent;border-color:transparent;opacity:.5;\'"\n style="padding:7px 0;border-radius:7px;font-size:13px;transition:all .15s;cursor:pointer;">\n ⬇ Export\n </button>\n <button @click="tab=\'import\'"\n :style="tab===\'import\' ? \'flex:1;background:var(--okdb-app-surface-2);color:var(--okdb-accent);border-color:color-mix(in srgb, var(--okdb-accent) 27%, transparent);font-weight:600;\' : \'flex:1;background:transparent;border-color:transparent;opacity:.5;\'"\n style="padding:7px 0;border-radius:7px;font-size:13px;transition:all .15s;cursor:pointer;">\n ⬆ Import\n </button>\n <button @click="tab=\'backup\'"\n :style="tab===\'backup\' ? \'flex:1;background:var(--okdb-app-surface-2);color:var(--okdb-purple);border-color:color-mix(in srgb, var(--okdb-purple) 27%, transparent);font-weight:600;\' : \'flex:1;background:transparent;border-color:transparent;opacity:.5;\'"\n style="padding:7px 0;border-radius:7px;font-size:13px;transition:all .15s;cursor:pointer;">\n 🗄 Backup\n </button>\n <button @click="tab=\'compact\'"\n :style="tab===\'compact\' ? \'flex:1;background:var(--okdb-app-surface-2);color:var(--okdb-purple);border-color:color-mix(in srgb, var(--okdb-purple) 27%, transparent);font-weight:600;\' : \'flex:1;background:transparent;border-color:transparent;opacity:.5;\'"\n style="padding:7px 0;border-radius:7px;font-size:13px;transition:all .15s;cursor:pointer;">\n 🧹 Compact\n </button>\n </div>\n\n \x3c!-- ═══ EXPORT ═══════════════════════════════════════════════════════════ --\x3e\n <if :="tab === \'export\'">\n <div style="display:flex;flex-direction:column;gap:18px;">\n\n <div style="font-size:12px;opacity:.5;line-height:1.5;">\n Streaming NDJSON export — portable across OKDB versions, importable into any instance.\n </div>\n\n \x3c!-- Environments --\x3e\n <div class="dops-field">\n <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">\n <label class="dops-label" style="flex:1;margin:0;">Environments</label>\n <button @click="exportToggleAllEnvs(true)" class="dops-micro">all</button>\n <button @click="exportToggleAllEnvs(false)" class="dops-micro">none</button>\n </div>\n <div style="display:flex;flex-wrap:wrap;gap:6px;">\n <for :="userEnvs" let:e>\n <label\n :style="exportSelEnvs[e.name] ? \'background:var(--okdb-app-surface-2);border-color:color-mix(in srgb, var(--okdb-accent) 33%, transparent);color:#e2eaf4;\' : \'background:var(--okdb-app-surface);border-color:var(--okdb-app-border);opacity:.55;\'"\n style="display:flex;align-items:center;gap:5px;padding:5px 11px;border-radius:999px;font-size:12px;cursor:pointer;border:1px solid;transition:all .12s;user-select:none;">\n <input type="checkbox"\n :checked="!!exportSelEnvs[e.name]"\n @change="exportSelEnvs = {...exportSelEnvs, [e.name]: $el.checked}"\n style="display:none;" />\n <span :style="exportSelEnvs[e.name] ? \'color:var(--okdb-accent);font-size:10px;\' : \'display:none;\'">✓</span>\n {{e.name}}\n <span style="opacity:.4;font-size:10px;">{{e.types ? e.types.filter(t=>!t.name.startsWith(\'~\')).length : 0}} types</span>\n </label>\n </for>\n </div>\n </div>\n\n \x3c!-- Types (only for single env) --\x3e\n <if :="exportIsSingleEnv && exportSingleEnvTypes.length > 0">\n <div class="dops-field">\n <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">\n <label class="dops-label" style="flex:1;margin:0;">Types</label>\n <label class="dops-check" style="font-size:11px;gap:4px;">\n <input type="checkbox" :checked="exportAllTypes" @change="exportAllTypes = $el.checked" />\n <span style="opacity:.6;">All types</span>\n </label>\n </div>\n <if :="!exportAllTypes">\n <div style="display:flex;flex-direction:column;gap:0;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:8px;overflow:hidden;">\n <div style="display:flex;align-items:center;padding:5px 10px;border-bottom:1px solid var(--okdb-app-border);gap:8px;">\n <button @click="exportToggleAllTypes(true)" class="dops-micro">all</button>\n <button @click="exportToggleAllTypes(false)" class="dops-micro">none</button>\n </div>\n <div style="display:flex;flex-direction:column;max-height:140px;overflow-y:auto;">\n <for :="exportSingleEnvTypes" let:t>\n <label style="display:flex;align-items:center;gap:8px;padding:6px 10px;cursor:pointer;transition:background .1s;"\n :style="exportSelTypes[t] ? \'background:#1a2a1a;\' : \'\'"\n @mouseenter="$el.style.background=exportSelTypes[t]?\'#1a2a1a\':\'var(--okdb-app-surface)\'"\n @mouseleave="$el.style.background=exportSelTypes[t]?\'#1a2a1a\':\'\'">\n <input type="checkbox"\n :checked="!!exportSelTypes[t]"\n @change="exportSelTypes = {...exportSelTypes, [t]: $el.checked}" />\n <span style="font-family:monospace;font-size:12px;">{{t}}</span>\n </label>\n </for>\n </div>\n </div>\n </if>\n </div>\n </if>\n <if :="!exportIsSingleEnv && exportSelectedEnvNames.length > 1">\n <div style="font-size:11px;opacity:.4;font-style:italic;">All types will be exported from each selected environment.</div>\n </if>\n\n \x3c!-- Options --\x3e\n <label class="dops-check">\n <input type="checkbox" :checked="exportIncludeChanges" @change="exportIncludeChanges = $el.checked" />\n <span>Include changelog <span style="opacity:.4;font-size:11px;">(sync history)</span></span>\n </label>\n\n <if :="exportError"><div class="dops-error">{{exportError}}</div></if>\n\n \x3c!-- Success --\x3e\n <if :="exportDone">\n <div class="dops-success">\n <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">\n <span>✓</span><span style="font-weight:600;">Downloaded</span>\n <span style="opacity:.4;font-size:11px;margin-left:auto;">{{formatBytes(exportDone.bytes)}}</span>\n </div>\n <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;font-size:12px;">\n <if :="exportDone.envCount > 1">\n <div class="dops-stat"><div class="dops-stat-n">{{exportDone.envCount}}</div><div>envs</div></div>\n </if>\n <div class="dops-stat"><div class="dops-stat-n">{{exportDone.types}}</div><div>types</div></div>\n <div class="dops-stat"><div class="dops-stat-n">{{exportDone.records}}</div><div>records</div></div>\n <div class="dops-stat"><div class="dops-stat-n">{{exportDone.changes}}</div><div>changes</div></div>\n </div>\n <div style="font-size:10px;opacity:.35;margin-top:6px;font-family:monospace;word-break:break-all;">{{exportDone.filename}}</div>\n </div>\n </if>\n\n <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:4px;">\n <button @click="close()" class="dops-btn-ghost">Cancel</button>\n <button @click="runExport()" :disabled="!exportCanRun" class="dops-btn-primary">\n <if :="exportBusy"><span class="dops-spinner"></span> Exporting…</if>\n <if :="!exportBusy">⬇ Download NDJSON</if>\n </button>\n </div>\n </div>\n </if>\n\n \x3c!-- ═══ IMPORT ═══════════════════════════════════════════════════════════ --\x3e\n <if :="tab === \'import\'">\n <div style="display:flex;flex-direction:column;gap:18px;">\n\n <div style="font-size:12px;opacity:.5;line-height:1.5;">\n Upload a OKDB NDJSON export. Types, indexes, and records are restored automatically.\n Multi-env files restore each environment separately.\n </div>\n\n \x3c!-- Drop zone --\x3e\n <div @click="pickFile()" @drop="onDrop($event)" @dragover="onDragOver($event)" @dragleave="onDragLeave()"\n :style="importDragOver ? \'border-color:var(--okdb-accent);background:color-mix(in srgb, var(--okdb-accent) 5%, transparent);\' : importFile ? \'border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);background:#0a1a10;\' : \'border-color:var(--okdb-app-border);background:var(--okdb-app-bg-deep);\'"\n style="border:2px dashed;border-radius:10px;padding:24px 16px;text-align:center;cursor:pointer;transition:all .15s;">\n <if :="!importFile">\n <div style="font-size:22px;opacity:.4;margin-bottom:8px;">📂</div>\n <div style="font-size:13px;opacity:.7;">Drop .ndjson file here or <span style="color:var(--okdb-accent);">click to browse</span></div>\n <div style="font-size:11px;opacity:.3;margin-top:4px;">.ndjson · .jsonl · .json</div>\n </if>\n <if :="importFile">\n <div style="font-size:20px;margin-bottom:6px;">📄</div>\n <div style="font-size:13px;font-weight:600;color:var(--okdb-success);font-family:monospace;word-break:break-all;">{{importFile.name}}</div>\n <div style="font-size:11px;opacity:.45;margin-top:3px;">{{formatBytes(importFile.size)}}\n <if :="importFileTypes.length"> · {{importFileTypes.length}} type{{importFileTypes.length!==1?\'s\':\'\'}} detected</if>\n · click to change</div>\n </if>\n </div>\n\n \x3c!-- Types filter (from file) --\x3e\n <if :="importFileTypes.length > 0">\n <div class="dops-field">\n <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">\n <label class="dops-label" style="flex:1;margin:0;">Types to import</label>\n <label class="dops-check" style="font-size:11px;gap:4px;">\n <input type="checkbox" :checked="importAllTypes" @change="importAllTypes = $el.checked" />\n <span style="opacity:.6;">All</span>\n </label>\n </div>\n <if :="!importAllTypes">\n <div style="display:flex;flex-direction:column;gap:0;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:8px;overflow:hidden;">\n <div style="display:flex;padding:5px 10px;border-bottom:1px solid var(--okdb-app-border);gap:8px;">\n <button @click="importToggleAllTypes(true)" class="dops-micro">all</button>\n <button @click="importToggleAllTypes(false)" class="dops-micro">none</button>\n </div>\n <div style="display:flex;flex-direction:column;max-height:130px;overflow-y:auto;">\n <for :="importFileTypes" let:t>\n <label style="display:flex;align-items:center;gap:8px;padding:6px 10px;cursor:pointer;"\n :style="importSelTypes[t] ? \'background:#1a2a1a;\' : \'\'">\n <input type="checkbox"\n :checked="!!importSelTypes[t]"\n @change="importSelTypes = {...importSelTypes, [t]: $el.checked}" />\n <span style="font-family:monospace;font-size:12px;">{{t}}</span>\n </label>\n </for>\n </div>\n </div>\n </if>\n </div>\n </if>\n\n \x3c!-- Target env override --\x3e\n <div class="dops-field">\n <label class="dops-label">Target environment\n <span style="opacity:.35;font-size:11px;font-weight:400;">(leave empty to use the file\'s source env)</span>\n </label>\n <select :value="importEnvOverride" @change="importEnvOverride = $el.value" class="dops-select">\n <option value="">— from file header —</option>\n <for :="userEnvs" let:e>\n <option :value="e.name">{{e.name}}</option>\n </for>\n </select>\n </div>\n\n \x3c!-- Options --\x3e\n <div style="display:flex;flex-direction:column;gap:7px;">\n <label class="dops-check">\n <input type="checkbox" :checked="importDropExisting" @change="importDropExisting = $el.checked" />\n <span>Drop & recreate existing types <span style="opacity:.4;font-size:11px;">(replaces current data)</span></span>\n </label>\n <label class="dops-check">\n <input type="checkbox" :checked="importIncludeChanges" @change="importIncludeChanges = $el.checked" />\n <span>Restore changelog rows</span>\n </label>\n </div>\n\n \x3c!-- Advanced --\x3e\n <div>\n <button @click="importShowAdvanced=!importShowAdvanced"\n style="font-size:11px;opacity:.4;background:transparent;border:none;padding:0;cursor:pointer;">\n {{importShowAdvanced?\'▴ hide\':\'▾\'}} advanced\n </button>\n <if :="importShowAdvanced">\n <div class="dops-field" style="margin-top:10px;">\n <label class="dops-label">Batch size <span style="opacity:.35;font-size:11px;">(records per write-txn)</span></label>\n <input type="number" min="10" max="10000" step="10"\n :value="importBatchSize" @input="importBatchSize=Number($el.value)||500"\n class="dops-input" style="max-width:120px;" />\n </div>\n </if>\n </div>\n\n <if :="importError"><div class="dops-error">{{importError}}</div></if>\n\n <if :="importDone">\n <div class="dops-success">\n <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">\n <span>✓</span><span style="font-weight:600;">Import complete</span>\n </div>\n <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;font-size:12px;">\n <div class="dops-stat"><div class="dops-stat-n">{{importDone.types}}</div><div>types</div></div>\n <div class="dops-stat"><div class="dops-stat-n">{{importDone.records}}</div><div>records</div></div>\n <div class="dops-stat"><div class="dops-stat-n">{{importDone.changes}}</div><div>changes</div></div>\n </div>\n </div>\n </if>\n\n <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:4px;">\n <button @click="close()" class="dops-btn-ghost">Cancel</button>\n <button @click="runImport()" :disabled="!canImport" class="dops-btn-primary">\n <if :="importBusy"><span class="dops-spinner"></span> Importing…</if>\n <if :="!importBusy">⬆ Import File</if>\n </button>\n </div>\n </div>\n </if>\n\n \x3c!-- ═══ BACKUP ════════════════════════════════════════════════════════════ --\x3e\n <if :="tab === \'backup\'">\n <div style="display:flex;flex-direction:column;gap:0;">\n\n \x3c!-- Mode tabs --\x3e\n <div style="display:flex;gap:0;margin-bottom:18px;border-bottom:1px solid var(--okdb-app-border);">\n <button @click="backupMode=\'server\'"\n :style="backupMode===\'server\' ? \'color:var(--okdb-purple);border-bottom:2px solid var(--okdb-purple);opacity:1;\' : \'border-bottom:2px solid transparent;opacity:.45;\'"\n style="padding:6px 16px;background:transparent;border:none;border-radius:0;font-size:13px;cursor:pointer;transition:all .15s;margin-bottom:-1px;">\n 🗄 Server backup\n </button>\n <button @click="backupMode=\'download\'"\n :style="backupMode===\'download\' ? \'color:var(--okdb-purple);border-bottom:2px solid var(--okdb-purple);opacity:1;\' : \'border-bottom:2px solid transparent;opacity:.45;\'"\n style="padding:6px 16px;background:transparent;border:none;border-radius:0;font-size:13px;cursor:pointer;transition:all .15s;margin-bottom:-1px;">\n ⬇ Download to PC\n </button>\n </div>\n\n \x3c!-- Server backup mode --\x3e\n <if :="backupMode === \'server\'">\n <div style="display:flex;flex-direction:column;gap:16px;">\n <div style="font-size:12px;opacity:.5;line-height:1.5;">\n Hot LMDB page-copy backup — atomic, exact snapshot, no serialisation.\n Writes to a directory <em>on the server</em>. The output can be used\n directly as a OKDB data path.\n </div>\n\n \x3c!-- Dest dir --\x3e\n <div class="dops-field">\n <label class="dops-label">Destination directory\n <span style="opacity:.35;font-size:11px;font-weight:400;">(server-side path)</span>\n </label>\n <input type="text" :value="backupDestDir" @input="backupDestDir=$el.value"\n class="dops-input" style="font-family:monospace;" spellcheck="false" />\n </div>\n\n \x3c!-- Env selection --\x3e\n <div class="dops-field">\n <label class="dops-label" style="margin-bottom:8px;">Environments</label>\n <div style="display:flex;gap:12px;margin-bottom:8px;">\n <label class="dops-check">\n <input type="radio" name="bkEnvMode" value="all"\n :checked="backupEnvMode===\'all\'" @change="backupEnvMode=\'all\'" />\n <span>All</span>\n </label>\n <label class="dops-check">\n <input type="radio" name="bkEnvMode" value="selected"\n :checked="backupEnvMode===\'selected\'" @change="backupEnvMode=\'selected\'" />\n <span>Select…</span>\n </label>\n </div>\n <if :="backupEnvMode===\'selected\'">\n <div style="display:flex;flex-wrap:wrap;gap:6px;padding:10px;background:var(--okdb-app-bg-deep);border-radius:8px;border:1px solid var(--okdb-app-border);">\n <for :="userEnvs" let:e>\n <label\n :style="backupSelEnvs[e.name] ? \'background:var(--okdb-app-surface);border-color:color-mix(in srgb, var(--okdb-purple) 27%, transparent);color:#e2d8f4;\' : \'background:var(--okdb-app-surface);border-color:var(--okdb-app-border);opacity:.55;\'"\n style="display:flex;align-items:center;gap:5px;padding:5px 11px;border-radius:999px;font-size:12px;cursor:pointer;border:1px solid;transition:all .12s;user-select:none;">\n <input type="checkbox"\n :checked="!!backupSelEnvs[e.name]"\n @change="backupSelEnvs={...backupSelEnvs,[e.name]:$el.checked}"\n style="display:none;" />\n <span :style="backupSelEnvs[e.name]?\'color:var(--okdb-purple);font-size:10px;\':\'display:none;\'">✓</span>\n {{e.name}}\n </label>\n </for>\n </div>\n </if>\n </div>\n\n <label class="dops-check">\n <input type="checkbox" :checked="backupCompact" @change="backupCompact=$el.checked" />\n <span>Compact while copying <span style="opacity:.4;font-size:11px;">(smaller files, slightly slower)</span></span>\n </label>\n\n <if :="backupError"><div class="dops-error">{{backupError}}</div></if>\n\n <if :="backupResult">\n <div class="dops-success" style="border-color:color-mix(in srgb, var(--okdb-purple) 20%, transparent);">\n <div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">\n <span>✓</span>\n <span style="font-weight:600;color:var(--okdb-app-fg);">Server backup complete</span>\n <span style="opacity:.4;font-size:11px;margin-left:auto;">{{backupResult.durationMs}}ms</span>\n </div>\n <div style="display:flex;flex-direction:column;gap:4px;">\n <for :="backupResult.envs||[]" let:e>\n <div style="background:#1a1228;border-radius:6px;padding:7px 10px;font-size:12px;">\n <div style="display:flex;gap:8px;align-items:center;">\n <span style="font-family:monospace;font-weight:600;color:var(--okdb-purple);">{{e.name}}</span>\n <span style="opacity:.35;font-size:11px;">{{formatBytes(e.sizeBefore)}} → {{formatBytes(e.sizeAfter)}}</span>\n </div>\n <div style="font-family:monospace;font-size:10px;opacity:.4;margin-top:3px;word-break:break-all;">{{e.dest}}</div>\n </div>\n </for>\n </div>\n </div>\n </if>\n\n <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:4px;">\n <button @click="close()" class="dops-btn-ghost">Cancel</button>\n <button @click="runBackup()" :disabled="!canBackup"\n style="padding:8px 20px;border-radius:8px;font-size:13px;font-weight:600;background:linear-gradient(135deg,#1e1530,#150f22);border:1px solid color-mix(in srgb, var(--okdb-purple) 33%, transparent);color:var(--okdb-purple);cursor:pointer;display:flex;align-items:center;gap:6px;transition:all .15s;">\n <if :="backupBusy"><span class="dops-spinner" style="border-color:color-mix(in srgb, var(--okdb-purple) 27%, transparent);border-top-color:var(--okdb-purple);"></span> Backing up…</if>\n <if :="!backupBusy">🗄 Run Server Backup</if>\n </button>\n </div>\n </div>\n </if>\n\n \x3c!-- Download backup mode --\x3e\n <if :="backupMode === \'download\'">\n <div style="display:flex;flex-direction:column;gap:16px;">\n <div style="font-size:12px;opacity:.5;line-height:1.5;">\n Export all environments as a single portable NDJSON file and download it to your computer.\n This file can be imported back into any OKDB instance.\n </div>\n\n \x3c!-- Env selection --\x3e\n <div class="dops-field">\n <label class="dops-label" style="margin-bottom:8px;">Environments to include</label>\n <div style="display:flex;gap:12px;margin-bottom:8px;">\n <label class="dops-check">\n <input type="radio" name="dlEnvMode" value="all"\n :checked="backupEnvMode===\'all\'" @change="backupEnvMode=\'all\'" />\n <span>All environments</span>\n </label>\n <label class="dops-check">\n <input type="radio" name="dlEnvMode" value="selected"\n :checked="backupEnvMode===\'selected\'" @change="backupEnvMode=\'selected\'" />\n <span>Select…</span>\n </label>\n </div>\n <if :="backupEnvMode===\'selected\'">\n <div style="display:flex;flex-wrap:wrap;gap:6px;padding:10px;background:var(--okdb-app-bg-deep);border-radius:8px;border:1px solid var(--okdb-app-border);">\n <for :="userEnvs" let:e>\n <label\n :style="backupSelEnvs[e.name] ? \'background:var(--okdb-app-surface);border-color:color-mix(in srgb, var(--okdb-purple) 27%, transparent);\' : \'background:var(--okdb-app-surface);border-color:var(--okdb-app-border);opacity:.55;\'"\n style="display:flex;align-items:center;gap:5px;padding:5px 11px;border-radius:999px;font-size:12px;cursor:pointer;border:1px solid;transition:all .12s;user-select:none;">\n <input type="checkbox"\n :checked="!!backupSelEnvs[e.name]"\n @change="backupSelEnvs={...backupSelEnvs,[e.name]:$el.checked}"\n style="display:none;" />\n <span :style="backupSelEnvs[e.name]?\'color:var(--okdb-purple);font-size:10px;\':\'display:none;\'">✓</span>\n {{e.name}}\n </label>\n </for>\n </div>\n </if>\n </div>\n\n <if :="backupDlError"><div class="dops-error">{{backupDlError}}</div></if>\n\n <if :="backupDlDone">\n <div class="dops-success" style="border-color:color-mix(in srgb, var(--okdb-purple) 20%, transparent);">\n <div style="display:flex;align-items:center;gap:8px;">\n <span>✓</span>\n <span style="font-weight:600;color:var(--okdb-app-fg);">Downloaded</span>\n <span style="opacity:.4;font-size:11px;">{{formatBytes(backupDlDone.bytes)}}</span>\n </div>\n <div style="font-family:monospace;font-size:10px;opacity:.35;margin-top:5px;word-break:break-all;">{{backupDlDone.filename}}</div>\n </div>\n </if>\n\n <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:4px;">\n <button @click="close()" class="dops-btn-ghost">Cancel</button>\n <button @click="runDownloadBackup()" :disabled="!canBackupDl"\n style="padding:8px 20px;border-radius:8px;font-size:13px;font-weight:600;background:linear-gradient(135deg,#1e1530,#150f22);border:1px solid color-mix(in srgb, var(--okdb-purple) 33%, transparent);color:var(--okdb-purple);cursor:pointer;display:flex;align-items:center;gap:6px;transition:all .15s;">\n <if :="backupDlBusy"><span class="dops-spinner" style="border-color:color-mix(in srgb, var(--okdb-purple) 27%, transparent);border-top-color:var(--okdb-purple);"></span> Downloading…</if>\n <if :="!backupDlBusy">⬇ Download Backup</if>\n </button>\n </div>\n </div>\n </if>\n\n </div>\n </if>\n\n \x3c!-- ═══ COMPACT / REPAIR ════════════════════════════════════════════════ --\x3e\n <if :="tab === \'compact\'">\n <div style="display:flex;flex-direction:column;gap:18px;">\n\n <div style="font-size:12px;opacity:.5;line-height:1.5;">\n Reclaim free pages by compacting an environment\'s data file. <b>Logical rebuild</b> reads\n every sub-DB into a fresh, leak-free file — use it to repair a page-leaked environment\n (it is also the automatic fallback) or to exercise the rebuild path. It parks writers for\n the duration, so it takes longer than a normal compact.\n </div>\n\n \x3c!-- Env selection --\x3e\n <div class="dops-field">\n <label class="dops-label" style="margin-bottom:8px;">Environment</label>\n <div style="display:flex;flex-wrap:wrap;gap:6px;">\n <for :="userEnvs" let:e>\n <label\n :style="compactEnv === e.name ? \'background:var(--okdb-app-surface-2);border-color:color-mix(in srgb, var(--okdb-purple) 33%, transparent);color:var(--okdb-purple);\' : \'background:var(--okdb-app-surface);border-color:var(--okdb-app-border);opacity:.55;\'"\n style="display:flex;align-items:center;gap:5px;padding:5px 11px;border-radius:999px;font-size:12px;cursor:pointer;border:1px solid;transition:all .12s;user-select:none;"\n @click="compactEnv = e.name">\n <span :style="compactEnv === e.name ? \'color:var(--okdb-purple);font-size:10px;\' : \'display:none;\'">✓</span>\n {{e.name}}\n </label>\n </for>\n </div>\n </div>\n\n <if :="compactError"><div class="dops-error">{{compactError}}</div></if>\n\n <if :="compactResult">\n <div class="dops-success" style="border-color:color-mix(in srgb, var(--okdb-purple) 20%, transparent);">\n <div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">\n <span>✓</span>\n <span style="font-weight:600;color:var(--okdb-app-fg);">\n {{compactResult.usedLogicalRebuild ? \'Logical rebuild complete\' : \'Compaction complete\'}}\n </span>\n <span style="opacity:.4;font-size:11px;margin-left:auto;">{{compactResult.savedPct}}% reclaimed</span>\n </div>\n <div style="display:flex;gap:8px;">\n <div class="dops-stat" style="flex:1;background:#120e1e;">\n <div class="dops-stat-n" style="color:var(--okdb-purple);">{{formatBytes(compactResult.sizeBefore)}}</div>\n before\n </div>\n <div class="dops-stat" style="flex:1;background:#120e1e;">\n <div class="dops-stat-n" style="color:var(--okdb-purple);">{{formatBytes(compactResult.sizeAfter)}}</div>\n after\n </div>\n <div class="dops-stat" style="flex:1;background:#120e1e;">\n <div class="dops-stat-n" style="color:var(--okdb-purple);">{{formatBytes(compactResult.saved)}}</div>\n saved\n </div>\n </div>\n <if :="compactResult.usedLogicalRebuild">\n <div style="font-size:11px;opacity:.5;margin-top:8px;">\n Rebuilt {{compactResult.subDbsCopied}} sub-databases into a fresh leak-free file.\n </div>\n </if>\n </div>\n </if>\n\n <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:4px;">\n <button @click="close()" class="dops-btn-ghost">Cancel</button>\n <button @click="runCompact(false)" :disabled="compactBusy" class="dops-btn-ghost" style="opacity:.85;">\n <if :="compactBusy"><span class="dops-spinner"></span> Working…</if>\n <if :="!compactBusy">🧹 Compact</if>\n </button>\n <button @click="runCompact(true)" :disabled="compactBusy"\n style="padding:8px 20px;border-radius:8px;font-size:13px;font-weight:600;background:linear-gradient(135deg,#1e1530,#150f22);border:1px solid color-mix(in srgb, var(--okdb-purple) 33%, transparent);color:var(--okdb-purple);cursor:pointer;display:flex;align-items:center;gap:6px;transition:all .15s;">\n <if :="compactBusy"><span class="dops-spinner" style="border-color:color-mix(in srgb, var(--okdb-purple) 27%, transparent);border-top-color:var(--okdb-purple);"></span> Rebuilding…</if>\n <if :="!compactBusy">🔧 Logical rebuild (repair)</if>\n </button>\n </div>\n </div>\n </if>\n\n</div>\n\n<style>\n .dops-field { display:flex; flex-direction:column; gap:5px; }\n .dops-label { font-size:12px; opacity:.6; font-weight:500; }\n .dops-input {\n background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px;\n color:inherit; padding:7px 10px; font-size:13px; width:100%;\n box-sizing:border-box; transition:border-color .15s;\n }\n .dops-input:focus { outline:none; border-color:color-mix(in srgb, var(--okdb-accent) 33%, transparent); }\n .dops-select {\n background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px;\n color:inherit; padding:7px 10px; font-size:13px; width:100%; box-sizing:border-box; cursor:pointer;\n }\n .dops-select:focus { outline:none; border-color:color-mix(in srgb, var(--okdb-accent) 33%, transparent); }\n .dops-check { display:flex; align-items:center; gap:8px; font-size:13px; cursor:pointer; user-select:none; }\n .dops-micro {\n padding:2px 8px; border-radius:4px; font-size:10px; background:transparent;\n border:1px solid var(--okdb-app-border); opacity:.5; cursor:pointer; transition:opacity .1s;\n }\n .dops-micro:hover { opacity:.85; }\n .dops-error {\n background:#2a1010; border:1px solid color-mix(in srgb, var(--okdb-danger) 27%, transparent); border-radius:8px;\n padding:10px 12px; font-size:12px; color:var(--okdb-danger); line-height:1.5;\n }\n .dops-success {\n background:#0d1a14; border:1px solid color-mix(in srgb, var(--okdb-success) 20%, transparent); border-radius:8px;\n padding:12px 14px; font-size:13px; color:#d1fae5;\n }\n .dops-stat {\n background:#071410; border-radius:6px; padding:8px; text-align:center; font-size:11px; opacity:.8;\n }\n .dops-stat-n { font-size:18px; font-weight:700; color:var(--okdb-success); }\n .dops-btn-primary {\n padding:8px 20px; border-radius:8px; font-size:13px; font-weight:600;\n background:linear-gradient(135deg,#1e4060,#1a3050);\n border:1px solid color-mix(in srgb, var(--okdb-accent) 33%, transparent); color:var(--okdb-accent);\n cursor:pointer; display:flex; align-items:center; gap:6px; transition:all .15s;\n }\n .dops-btn-primary:hover:not(:disabled) { border-color:color-mix(in srgb, var(--okdb-accent) 53%, transparent); }\n .dops-btn-primary:disabled { opacity:.35; cursor:not-allowed; }\n .dops-btn-ghost {\n padding:8px 16px; border-radius:8px; font-size:13px;\n background:transparent; border:1px solid var(--okdb-app-border); opacity:.55; cursor:pointer;\n }\n .dops-btn-ghost:hover { opacity:.85; }\n .dops-spinner {\n display:inline-block; width:12px; height:12px;\n border:2px solid color-mix(in srgb, var(--okdb-accent) 20%, transparent); border-top-color:var(--okdb-accent);\n border-radius:50%; animation:dops-spin .6s linear infinite;\n }\n @keyframes dops-spin { to { transform:rotate(360deg); } }\n</style>\n'};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default{tag:"system-process-panel",context:n=>({tree:[],busy:!1,error:null,expanded:{},_timer:null,get rows(){const n=[],e=(o,s)=>{const r=new Map;for(const n of o||[])r.has(n.kind)||r.set(n.kind,[]),r.get(n.kind).push(n);for(const[o,i]of r){if(i.length>8){const e=`g:${s}:${o}:${i[0]?.processKey}`;if(n.push({group:!0,kind:o,count:i.length,depth:s,key:e,open:!!this.expanded[e]}),!this.expanded[e])continue}for(const o of i){if(n.push({node:o,depth:s}),o.instances&&o.instances.length>1&&this.expanded[`i:${o.processKey}`])for(const e of o.instances)n.push({instance:e,node:o,depth:s+1});o.children&&o.children.length&&e(o.children,s+1)}}};return e(this.tree,0),n},get nodeCount(){let n=0;const e=o=>{for(const s of o||[])n++,s.children&&e(s.children)};return e(this.tree),n},toggle(n){this.expanded={...this.expanded,[n]:!this.expanded[n]}},toggleInstances(n){n?.instances&&n.instances.length>1&&this.toggle(`i:${n.processKey}`)},async load(){this.busy=!0;try{const n=await fetch(`${__ENV__.API}/processes/tree`);if(n.ok){const e=await n.json();this.tree=e.data??e.result??[],this.error=null}}catch(n){this.error=n?.message||String(n)}finally{this.busy=!1}},async doDecommission(e){e?.caps?.decommissionable&&(n.$ok?.shared?.confirm?await n.$ok.shared.confirm(`Decommission ${this.kindLabel(e.kind)} (pid ${e.pid})? It will drain and stay down until recommissioned.`):window.confirm(`Decommission ${this.kindLabel(e.kind)} (pid ${e.pid})?`))&&(e.lifecycle={state:"decommissioning"},await fetch(`${__ENV__.API}/processes/${encodeURIComponent(e.nodeKey)}/decommission`,{method:"POST"}),await this.load())},async doRecommission(e){const o=e?.slotKey||e?.nodeKey;if(!o)return;const s=`Recommission ${this.kindLabel(e.kind)}? The supervisor will fork it again.`;(n.$ok?.shared?.confirm?await n.$ok.shared.confirm(s):window.confirm(s))&&(await fetch(`${__ENV__.API}/processes/${encodeURIComponent(o)}/recommission`,{method:"POST"}),await this.load())},isDecommissioning:n=>"decommissioning"===n?.lifecycle?.state,async doKill(e){if(!e?.caps?.killable)return;const o="primary"===e.kind,s=o?`HARD-KILL the primary (pid ${e.pid})? This tears down the ENTIRE cluster — all workers exit. This cannot be undone.`:`HARD-KILL ${this.kindLabel(e.kind)} (pid ${e.pid})? It is terminated immediately (no drain).`;if(!(n.$ok?.shared?.confirm?await n.$ok.shared.confirm(s):window.confirm(s)))return;const r="force=1"+(o?"&confirm=1":"");await fetch(`${__ENV__.API}/processes/${encodeURIComponent(e.nodeKey)}/kill?${r}`,{method:"POST"}),await this.load()},kindLabel:n=>({primary:"Primary","http-server":"HTTP server","queue-worker":"Queue worker","fn-runner":"Function runner",cli:"CLI",standalone:"Standalone",embedded:"Embedded"}[n]||n||"process"),kindColor(n){switch(n){case"primary":case"standalone":case"cli":return"var(--okdb-accent)";case"http-server":return"var(--okdb-success)";case"queue-worker":case"fn-runner":return"var(--okdb-purple)";case"embedded":return"var(--okdb-app-fg-muted)";default:return"var(--okdb-app-fg-subtle)"}},capabilities(n){const e=n?.role||{},o=[];return e.asyncProcessors&&o.push("process"),e.http&&o.push("serve"),e.engines&&o.push("queue"),"active"===e.compaction&&o.push("compact"),"fn-runner"===n?.kind&&o.push("functions"),o.unshift("ingest"),o},capStrip(n){return this.capabilities(n).join(" · ")},indentPx:n=>22*n,formatUptime(n){if(null==n)return"--";const e=Math.floor(n),o=Math.floor(e/86400),s=Math.floor(e%86400/3600),r=Math.floor(e%3600/60),i=e%60;return o>0?`${o}d ${s}h ${r}m`:s>0?`${s}h ${r}m ${i}s`:r>0?`${r}m ${i}s`:`${i}s`},facetSummary(n){const e=n.processing;return e?{procs:(e.processors||[]).filter(n=>"single"===n.mode||"fanout"===n.mode).length}:null},maxLag(n){const e=n.processing;if(!e)return 0;let o=0;for(const n of e.processors||[])(n.lag||0)>o&&(o=n.lag);return o},lagColor:n=>n?n<1e3?"var(--okdb-warning)":"var(--okdb-danger)":"var(--okdb-success)"}),init(n){this.load(),this._timer=setInterval(()=>this.load(),3e3),this._timer.unref&&this._timer.unref()},unmount(){this._timer&&clearInterval(this._timer),this._timer=null},template:'\n <div style="display:flex;flex-direction:column;gap:16px;">\n\n <div style="display:flex;align-items:center;gap:10px;font-size:12px;color:var(--okdb-method-default);">\n <span>Live processes sharing this root: <strong style="color:var(--okdb-app-fg-strong);">{{ nodeCount }}</strong></span>\n <button @click="load()" style="margin-left:auto;font-size:11px;padding:2px 8px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:4px;cursor:pointer;color:var(--okdb-app-fg);" :disabled="busy">Refresh</button>\n </div>\n\n <if :="error">\n <div style="color:var(--okdb-danger);font-size:12px;padding:8px;border:1px solid var(--okdb-danger);border-radius:6px;">{{ error }}</div>\n </if>\n\n <if :="rows.length === 0">\n <div style="font-size:12px;color:var(--okdb-app-fg-subtle);padding:8px 0;">No processes reporting. The registry is enabled on clustered / supervisor processes.</div>\n </if>\n\n <div style="display:flex;flex-direction:column;font-size:12px;">\n <div style="display:grid;grid-template-columns:2.4fr 1fr 0.8fr 1.2fr 1fr 1.4fr 1.1fr;gap:10px;padding:5px 10px;color:var(--okdb-app-fg-subtle);border-bottom:1px solid var(--okdb-app-border);font-weight:500;">\n <div>Process</div><div>Capabilities</div><div>PID</div><div>Listen</div><div>Uptime</div><div>Processors</div><div></div>\n </div>\n <each :of="rows" let:item="r">\n \x3c!-- collapsed same-kind sibling group (expand to reveal) --\x3e\n <if :="r.group">\n <div @click="toggle(r.key)" :style="\'padding:6px 10px;padding-left:\' + (indentPx(r.depth)+10) + \'px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));cursor:pointer;color:var(--okdb-method-default);display:flex;align-items:center;gap:7px;\'">\n <span style="opacity:.6;">{{ r.open ? \'▾\' : \'▸\' }}</span>\n <span :style="\'font-size:10px;padding:2px 7px;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;color:\' + kindColor(r.kind)">{{ kindLabel(r.kind) }}</span>\n <span style="opacity:.7;">{{ r.count }} processes</span>\n </div>\n </if>\n \x3c!-- instance sub-row of a multi-instance process --\x3e\n <if :="r.instance">\n <div :style="\'display:grid;grid-template-columns:2.4fr 1fr 0.8fr 1.2fr 1fr 1.4fr 1.1fr;gap:10px;padding:4px 10px;padding-left:\' + (indentPx(r.depth)+10) + \'px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));align-items:center;color:var(--okdb-app-fg-subtle);font-size:11px;\'">\n <span style="display:flex;align-items:center;gap:6px;"><span style="opacity:.35;">▪</span> instance <span style="font-family:monospace;">{{ r.instance.instanceId }}</span></span>\n <span>{{ capStrip({ role: r.instance.role, kind: r.node.kind }) }}</span>\n <span></span><span style="font-family:monospace;">{{ r.instance.listenAddr || \'—\' }}</span><span></span>\n <span>\n <if :="r.instance.processing"><span>{{ (r.instance.processing.processors || []).length }} proc</span></if>\n <if :="!r.instance.processing"><span style="opacity:.4;">—</span></if>\n </span>\n <span></span>\n </div>\n </if>\n \x3c!-- a process --\x3e\n <if :="r.node && !r.instance">\n <div style="display:grid;grid-template-columns:2.4fr 1fr 0.8fr 1.2fr 1fr 1.4fr 1.1fr;gap:10px;padding:7px 10px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));align-items:center;color:var(--okdb-method-default);">\n <span :style="\'padding-left:\' + indentPx(r.depth) + \'px;display:flex;align-items:center;gap:7px;\'">\n <if :="r.depth > 0"><span style="opacity:.35;">↳</span></if>\n <span :style="\'font-size:10px;padding:2px 7px;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;color:\' + kindColor(r.node.kind)">{{ kindLabel(r.node.kind) }}</span>\n <span style="font-family:monospace;opacity:.6;font-size:11px;">{{ r.node.host }}</span>\n <if :="r.node.meta?.env"><span :title="\'serves env \' + r.node.meta.env + (r.node.meta.queueType ? \' · \' + r.node.meta.queueType : \'\')" style="font-size:10px;padding:1px 6px;border-radius:4px;background:var(--okdb-app-bg-2);color:var(--okdb-accent);font-family:monospace;">{{ r.node.meta.env }}</span></if>\n <if :="r.node.instances && r.node.instances.length > 1"><span @click="toggleInstances(r.node)" style="font-size:10px;padding:1px 6px;border-radius:4px;background:var(--okdb-app-bg-2);color:var(--okdb-method-default);cursor:pointer;">{{ r.node.instances.length }} instances</span></if>\n <if :="isDecommissioning(r.node)"><span style="font-size:10px;color:var(--okdb-warning);">⏻ decommissioning</span></if>\n </span>\n <span :style="\'font-size:11px;opacity:.85;\'">{{ capStrip(r.node) }}</span>\n <span style="font-family:monospace;">{{ r.node.pid }}</span>\n <span style="font-family:monospace;font-size:11px;opacity:.8;">{{ r.node.listenAddr || \'—\' }}</span>\n <span>{{ formatUptime(r.node.uptime) }}</span>\n <span>\n <if :="facetSummary(r.node)">\n <span>{{ facetSummary(r.node).procs }} proc</span>\n <span :style="\'margin-left:6px;color:\' + lagColor(maxLag(r.node))">lag {{ maxLag(r.node) }}</span>\n </if>\n <if :="!facetSummary(r.node)"><span style="opacity:.4;">—</span></if>\n </span>\n <span style="display:flex;gap:5px;justify-content:flex-end;">\n <if :="r.node.caps?.decommissionable && !isDecommissioning(r.node)">\n <button @click="doDecommission(r.node)" title="Decommission — drain & stay down" style="font-size:10px;padding:2px 8px;border-radius:3px;border:1px solid color-mix(in srgb, var(--okdb-warning) 30%, transparent);background:var(--okdb-app-surface);color:var(--okdb-warning);cursor:pointer;">⏻ Decommission</button>\n </if>\n <if :="isDecommissioning(r.node)">\n <button @click="doRecommission(r.node)" title="Recommission — re-fork this slot" style="font-size:10px;padding:2px 8px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">▶ Recommission</button>\n </if>\n <if :="r.node.caps?.killable">\n <button @click="doKill(r.node)" :title="r.node.kind === \'primary\' ? \'Hard-kill — tears down the whole cluster\' : \'Hard-kill — immediate, no drain\'" style="font-size:10px;padding:2px 8px;border-radius:3px;border:1px solid color-mix(in srgb, var(--okdb-danger) 35%, transparent);background:var(--okdb-app-surface);color:var(--okdb-danger);cursor:pointer;">⨯ Kill</button>\n </if>\n </span>\n </div>\n </if>\n </each>\n </div>\n\n </div>\n '};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"../../../stores/events-store.js";export default{tag:"system-processing-panel",context:e=>({status:{policy:""},processors:[],busy:!1,actionBusy:null,_timer:null,_unsubs:[],get tierProcessors(){return this.processors.filter(e=>"single"===e.mode||"fanout"===e.mode)},get inlineCount(){return this.processors.filter(e=>"inline"===e.mode).length},async load(){this.busy=!0;try{const[e,o]=await Promise.all([fetch(`${__ENV__.API}/processing/status`),fetch(`${__ENV__.API}/processors`)]);if(e.ok){const o=await e.json();this.status=o.data??o.result??{policy:""}}if(o.ok){const e=await o.json();this.processors=e.data??e.result??[]}}catch{}finally{this.busy=!1}},async _action(o,r,t){if(!this.actionBusy){this.actionBusy=o;try{const o=await r();if(o&&!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message||e?.error||`HTTP ${o.status}`)}t&&e.$ok.shared?.toast?.success?.(t),await this.load()}catch(o){e.$ok.shared?.toast?.error?.(o?.message||"action failed")}finally{this.actionBusy=null}}},btnIcon(e,o){return this.actionBusy===e?"⏳":o},doPause(e){return this._action(`pause:${e}`,()=>fetch(`${__ENV__.API}/processors/${encodeURIComponent(e)}/pause`,{method:"POST"}),"processor paused")},doResume(e){return this._action(`resume:${e}`,()=>fetch(`${__ENV__.API}/processors/${encodeURIComponent(e)}/resume`,{method:"POST"}),"processor resumed")},doRetry(e){return this._action(`retry:${e}`,()=>fetch(`${__ENV__.API}/processors/${encodeURIComponent(e)}/retry`,{method:"POST"}),"retry requested")},async doResetCursor(o,r){if(!o)return;const t=`Reset cursor "${o}"${r?` in ${r}`:""}? This reprocesses every document of this type from the beginning — a full rebuild.`;return(e.$ok?.shared?.confirm?await e.$ok.shared.confirm(t):window.confirm(t))?this._action(`reset:${o}`,()=>fetch(`${__ENV__.API}/processors/cursor/reset`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({cursorKey:o,env:r})}),"cursor reset requested — full reprocess will start"):void 0},lagColor:e=>0===e?"var(--okdb-success)":e<1e3?"var(--okdb-warning)":"var(--okdb-danger)",stateColor(e){switch(e){case"online":return"var(--okdb-success)";case"building":return"var(--okdb-warning)";case"error":return"var(--okdb-danger)";case"waiting":return"var(--okdb-method-default)";case"paused":return"#818cf8";case"delegated":return"var(--okdb-accent)";default:return"var(--okdb-app-fg-subtle)"}},procStateLabel:e=>e.claimGated?"delegated":e.paused?"paused":e.state,isOnline:e=>!e.paused&&"online"===e.state,isUserPaused:e=>e.paused&&!e.claimGated,isError:e=>"error"===e.state,stateDetail:e=>e.claimGated?e.heldBy?`draining on worker pid ${e.heldBy.pid}${e.heldBy.hostname?" on "+e.heldBy.hostname:""}`:"delegated to workers — not yet claimed":"building"===e.state&&"worker"===e.progress?.placement?"running"===e.progress.state&&e.progress.claimedBy?`bootstrap running on worker pid ${e.progress.claimedBy}`:"bootstrap queued on workers":"online"!==e.state&&"building"!==e.state||"worker"!==e.owner?.kind?"waiting"!==e.state&&"error"!==e.state||!e.error?"":e.error:`on worker pid ${e.owner.pid}`,_logHeadKnown:e=>e.logHead>=e.lastClock&&(e.logHead>0||0===e.envClock),clockLabel(e){return null==e.logHead||null==e.envClock?`${e.lastClock} / ${e.headClock}`:this._logHeadKnown(e)?`${e.lastClock} / ${e.logHead}`:`${e.lastClock} / —`},clockTitle(e){return null==e.logHead||null==e.envClock||this._logHeadKnown(e)?"":`head unknown: the change log for this type has no entries at/after the cursor (old entries are deleted by compaction/sync-GC once processed). Nothing is pending; the head shows again on the next write to this type. Cursor ${e.lastClock} · env clock ${e.envClock}.`}}),init(o){this.load(),this._timer=setInterval(()=>this.load(),3e3),this._timer.unref&&this._timer.unref();const r=()=>this.load();this._unsubs=[e.on("processor:ready",r),e.on("processor:error",r),e.on("processor:paused",r),e.on("processor:resumed",r),e.on("processor:retry",r)]},unmount(){this._timer&&clearInterval(this._timer),this._timer=null;for(const e of this._unsubs)e();this._unsubs=[]},template:'\n <div style="display:flex;flex-direction:column;gap:20px;">\n\n <div style="display:flex;align-items:center;gap:10px;font-size:12px;color:var(--okdb-method-default);">\n <span>Policy: <strong style="color:var(--okdb-app-fg-strong);">{{ status.policy || \'unknown\' }}</strong></span>\n <button @click="load()" style="margin-left:auto;font-size:11px;padding:2px 8px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:4px;cursor:pointer;color:var(--okdb-app-fg);" :disabled="busy">Refresh</button>\n </div>\n\n \x3c!-- ── Async/worker processors (the tier; inline excluded) ── --\x3e\n <div>\n <div style="font-size:12px;font-weight:600;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px;">Async & Worker Processors</div>\n <if :="tierProcessors.length === 0">\n <div style="font-size:12px;color:var(--okdb-app-fg-subtle);padding:8px 0;">No async or worker processors running.</div>\n </if>\n <if :="tierProcessors.length > 0">\n <div style="display:flex;flex-direction:column;font-size:12px;">\n <div style="display:grid;grid-template-columns:1fr 1.4fr 0.9fr 0.9fr 1.3fr 0.7fr 1.8fr;gap:10px;padding:5px 10px;color:var(--okdb-app-fg-subtle);border-bottom:1px solid var(--okdb-app-border);font-weight:500;">\n <div>Env</div><div>Processor</div><div>Mode</div><div>State</div><div>Clock</div><div>Lag</div><div></div>\n </div>\n <each :of="tierProcessors" let:item="p">\n <div style="display:grid;grid-template-columns:1fr 1.4fr 0.9fr 0.9fr 1.3fr 0.7fr 1.8fr;gap:10px;padding:7px 10px;border-bottom:1px solid var(--okdb-app-border-faint,rgba(255,255,255,.05));align-items:center;color:var(--okdb-method-default);">\n <span style="color:var(--okdb-accent);">{{ p.env }}</span>\n <span><strong style="color:var(--okdb-app-fg);">{{ p.meta?.name || p.type }}</strong></span>\n <span>{{ p.mode }}</span>\n <span :title="stateDetail(p)">\n <span :style="\'color:\' + stateColor(procStateLabel(p))">{{ procStateLabel(p) }}</span>\n <if :="stateDetail(p)">\n <span style="display:block;font-size:10px;opacity:.75;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ stateDetail(p) }}</span>\n </if>\n </span>\n <span style="font-family:monospace;" :title="clockTitle(p)">{{ clockLabel(p) }}</span>\n <span :style="\'color:\' + lagColor(p.lag)">{{ p.lag }}</span>\n <span style="display:flex;gap:5px;justify-content:flex-end;">\n <if :="isOnline(p)">\n <button @click="doPause(p.id)" :disabled="!!actionBusy" title="Pause" style="font-size:10px;padding:2px 7px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">{{ btnIcon(\'pause:\' + p.id, \'⏸\') }}</button>\n </if>\n <if :="isUserPaused(p)">\n <button @click="doResume(p.id)" :disabled="!!actionBusy" title="Resume" style="font-size:10px;padding:2px 7px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">{{ btnIcon(\'resume:\' + p.id, \'▶\') }}</button>\n </if>\n <if :="isError(p)">\n <button @click="doRetry(p.id)" :disabled="!!actionBusy" title="Retry" style="font-size:10px;padding:2px 7px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">{{ btnIcon(\'retry:\' + p.id, \'↻\') }}</button>\n </if>\n <if :="p.cursorKey">\n <button @click="doResetCursor(p.cursorKey, p.env)" :disabled="!!actionBusy" title="Reset cursor — full reprocess" style="font-size:10px;padding:2px 7px;border-radius:3px;border:1px solid var(--okdb-app-border-strong);background:var(--okdb-app-surface);color:var(--okdb-method-default);cursor:pointer;">{{ btnIcon(\'reset:\' + p.cursorKey, \'⟲\') }}</button>\n </if>\n </span>\n </div>\n </each>\n </div>\n </if>\n <if :="inlineCount > 0">\n <div style="font-size:11px;color:var(--okdb-app-fg-subtle);padding:8px 2px 0;opacity:.7;">\n + {{ inlineCount }} inline processor(s) (index/view maintenance) — synchronous & always consistent, not scheduled by the tier.\n </div>\n </if>\n </div>\n\n </div>\n '};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default{tag:"system-runtime-overview",context:()=>({procCount:null,workers:null,procs:null,busy:!1,_timer:null,countTree(r){let e=0;const t=r=>{for(const o of r||[])e++,o.children&&t(o.children)};return t(r),e},async load(){this.busy=!0;try{const[r,e,t]=await Promise.all([fetch(`${__ENV__.API}/processes/tree`).catch(()=>null),fetch(`${__ENV__.API}/workers`).catch(()=>null),fetch(`${__ENV__.API}/processors/status`).catch(()=>null)]);if(r?.ok){const e=await r.json();this.procCount=this.countTree(e.data??e.result??[])}if(e?.ok){const r=await e.json();this.workers=r.data??r.result??null}if(t?.ok){const r=await t.json(),e=r.data??r.result??{},o=e.envs??e,s=[];for(const r of Object.keys(o||{}))if(Array.isArray(o[r]))for(const e of o[r])s.push(e);this.procs=s}}catch{}finally{this.busy=!1}},get workerLine(){const r=this.workers;if(!r)return"—";if(0===r.scale)return"off (self-processing)";const e="auto"===r.scale?"auto":null!=r.scale?.max?`${r.scale.min}–${r.scale.max}`:`min ${r.scale?.min}`;return`${r.live} live · desired ${e}`},get procHealth(){if(!this.procs)return{text:"—",color:"var(--okdb-app-fg-subtle)"};if(0===this.procs.length)return{text:"none registered",color:"var(--okdb-app-fg-subtle)"};const r=this.procs.filter(r=>"error"===r.state).length,e=this.procs.filter(r=>(r.lag||0)>1e3).length;return r?{text:`${r} error`,color:"var(--okdb-danger)"}:e?{text:`${e} lagging`,color:"var(--okdb-warning)"}:{text:"all caught up",color:"var(--okdb-success)"}}}),init(){this.load(),this._timer=setInterval(()=>this.load(),4e3),this._timer.unref&&this._timer.unref()},unmount(){this._timer&&clearInterval(this._timer),this._timer=null},template:'\n <div style="display:flex;flex-direction:column;gap:14px;">\n <div style="font-size:12px;color:var(--okdb-method-default);">A snapshot of everything running under this root. Open a tab for detail and controls.</div>\n <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">\n\n \x3c!-- Processes --\x3e\n <div style="padding:14px 16px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:8px;">\n <div style="font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--okdb-app-fg-subtle);">Processes</div>\n <div style="font-size:26px;font-weight:600;color:var(--okdb-app-fg-strong);margin-top:4px;">{{ procCount == null ? \'—\' : procCount }}</div>\n <div style="font-size:11px;color:var(--okdb-app-fg-subtle);">live processes under this root</div>\n </div>\n\n \x3c!-- Workers --\x3e\n <div style="padding:14px 16px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:8px;">\n <div style="font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--okdb-app-fg-subtle);">Workers</div>\n <div style="font-size:26px;font-weight:600;color:var(--okdb-app-fg-strong);margin-top:4px;">{{ workers ? workers.live : \'—\' }}</div>\n <div style="font-size:11px;color:var(--okdb-app-fg-subtle);">{{ workerLine }}</div>\n <if :="workers && workers.why"><div style="font-size:10px;color:var(--okdb-app-fg-muted);margin-top:3px;">{{ workers.why }}</div></if>\n </div>\n\n \x3c!-- Processors --\x3e\n <div style="padding:14px 16px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:8px;">\n <div style="font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--okdb-app-fg-subtle);">Processors</div>\n <div style="font-size:26px;font-weight:600;color:var(--okdb-app-fg-strong);margin-top:4px;">{{ procs == null ? \'—\' : procs.length }}</div>\n <div :style="\'font-size:11px;color:\' + procHealth.color + \';\'">{{ procHealth.text }}</div>\n </div>\n </div>\n </div>\n '};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default{tag:"system-workers-panel",context:e=>({s:null,busy:!1,actionBusy:null,error:null,_timer:null,async load(){this.busy=!0;try{const e=await fetch(`${__ENV__.API}/workers`);if(e.ok){const s=await e.json();this.s=s.data??s.result??null,this.error=null}}catch{}finally{this.busy=!1}},async _action(s,t,a){if(!this.actionBusy){this.actionBusy=s;try{const s=await fetch(`${__ENV__.API}/workers/scale`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({scale:t})}),o=await s.json().catch(()=>({}));if(!s.ok||o?.error)throw new Error(o?.error?.message||o?.error||`HTTP ${s.status}`);e.$ok?.shared?.toast?.success?.(a),await this.load()}catch(s){e.$ok?.shared?.toast?.error?.(s?.message||"scale failed")}finally{this.actionBusy=null}}},setAuto(){return this._action("auto","auto","workers set to auto (elastic)")},async setFixed(){const s=e.$ok?.shared?.prompt?await e.$ok.shared.prompt("Fixed worker count (pins min = max):",String(this.s?.live??1)):window.prompt("Fixed worker count (pins min = max):",String(this.s?.live??1));if(null==s)return;const t=Number(s);if(Number.isFinite(t)&&!(t<0))return this._action("fixed",{min:t,max:t},`workers pinned to ${t}`);e.$ok?.shared?.toast?.error?.("enter a non-negative number")},async setOff(){const s="Turn workers OFF (scale 0)? Background work falls back to this instance self-processing.";if(e.$ok?.shared?.confirm?await e.$ok.shared.confirm(s):window.confirm(s))return this._action("off",0,"workers turned off")},scaleLabel(){const e=this.s;return e?0===e.scale?"off":"auto"===e.scale?"auto":e.scale&&"object"==typeof e.scale?null!=e.scale.max?`fixed ${e.scale.min}–${e.scale.max}`:`min ${e.scale.min}`:String(e.scale):"—"},capLabel(){const e=this.s;return e&&null!=e.cap?`cap ${e.cap} · ${e.capReason}`:"uncapped"},gaugePct:(e,s)=>!s||s<=0?e>0?100:0:Math.min(100,Math.round(e/s*100)),gaugeColor:(e,s)=>e?e>=s?"var(--okdb-danger)":"var(--okdb-warning)":"var(--okdb-success)",whyColor(){const e=this.s?.why||"";return e.startsWith("at cap")?"var(--okdb-danger)":e.startsWith("scaling")?"var(--okdb-warning)":"var(--okdb-app-fg-muted)"},btnDisabled(e){return null!=this.actionBusy},btnLabel(e,s){return this.actionBusy===e?"⏳":s},lastScaleLabel(){const e=this.s?.lastScale;return e?`${"up"===e.dir?"▲ up":"▼ down"} to ${e.to} — ${e.reason}`:"none this session"},get signals(){if(!this.s)return[];const e=this.s.backlog||{},s=this.s.thresholds||{};return[{label:"unprocessed changes",value:e.totalLag??0,threshold:s.lagHigh??0,title:"Changelog entries not yet processed by derived-state processors (FTS / views / materializer / etc.), summed across envs."},{label:"queue (ready)",value:e.pendingJobs??0,threshold:s.queueHigh??0,title:"Queue jobs a fleet worker can claim RIGHT NOW: status pending, DUE (scheduled `when` ≤ now), and of a type with a registered queue.worker() runner. Excluded (so the count means what it says): future-scheduled jobs, and types served by a custom user runner or an HTTP handler — a fleet worker never claims those, so they must not drive scaling."},{label:"fn dispatch",value:e.fnPending??0,threshold:s.fnHigh??0,title:"Pending WORKER-DISPATCHED function requests (the durable ~fn:requests queue) awaiting a fleet worker. This is NOT every function call: in-process and HTTP-handler invocations run where they are called and never enter this queue, so they are not counted here. An unexpected non-zero on an idle system suggests orphaned rows from a caller that exited before any worker claimed (these are now reaped after OKDB_FN_STALE_MS)."},{label:"index / FTS builds",value:e.bootstrapPending??0,threshold:1,title:"Pending routed bootstrap (index / FTS) build jobs awaiting a worker."}]},signalNote:e=>0===e.value?"idle":e.threshold>1?`scales up above ${e.threshold}`:"scales up while > 0"}),init(e){this.load(),this._timer=setInterval(()=>this.load(),3e3),this._timer.unref&&this._timer.unref()},unmount(){this._timer&&clearInterval(this._timer),this._timer=null},template:'\n <div style="display:flex;flex-direction:column;gap:16px;">\n\n <div style="display:flex;align-items:center;gap:10px;font-size:12px;color:var(--okdb-method-default);">\n <span>The elastic worker population under this root — backlog-driven, capped, shared by all instances.</span>\n <button @click="load()" :disabled="busy" style="margin-left:auto;font-size:11px;padding:2px 8px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:4px;cursor:pointer;color:var(--okdb-app-fg);">Refresh</button>\n </div>\n\n <if :="!s">\n <div style="font-size:12px;color:var(--okdb-app-fg-subtle);padding:8px 0;">Loading worker status…</div>\n </if>\n\n <if :="s">\n \x3c!-- Population: ACTUAL (live now) vs DESIRED (the persisted root-wide setting) --\x3e\n <div style="display:flex;align-items:baseline;gap:14px;padding:14px 16px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:8px;">\n <span style="font-size:30px;font-weight:600;color:var(--okdb-app-fg-strong);">{{ s.live }}</span>\n <span style="font-size:12px;color:var(--okdb-app-fg-subtle);">live now</span>\n <span style="font-size:12px;color:var(--okdb-app-fg-subtle);">· desired</span>\n <span style="font-size:11px;padding:3px 9px;border-radius:5px;background:var(--okdb-app-bg);color:var(--okdb-accent);font-family:monospace;">{{ scaleLabel() }}</span>\n <span style="font-size:11px;padding:3px 9px;border-radius:5px;background:var(--okdb-app-bg);color:var(--okdb-app-fg-muted);font-family:monospace;">{{ capLabel() }}</span>\n <if :="s.supervisor && s.supervisor.isSelf"><span title="This instance currently holds the per-root supervisor lease and reconciles the population." style="font-size:10px;padding:2px 7px;border-radius:4px;background:var(--okdb-app-bg);color:var(--okdb-success);">supervisor</span></if>\n <span :title="s.source === \'operator\' ? \'Set from the admin UI/API — takes precedence over instances re-asserting their boot config.\' : \'Set by an instance at boot (workers.ensure). An operator change here would take precedence.\'" style="font-size:10px;padding:2px 7px;border-radius:4px;background:var(--okdb-app-bg);color:var(--okdb-app-fg-subtle);margin-left:auto;">{{ s.source === \'operator\' ? \'set by operator\' : \'from config\' }}</span>\n </div>\n\n \x3c!-- The setting is ONE persisted, root-wide record — not a per-instance toggle. --\x3e\n <div style="font-size:11px;color:var(--okdb-app-fg-subtle);line-height:1.5;padding:0 2px;">\n <strong>Desired</strong> is a single <strong>persisted, root-wide</strong> setting (durable in <code>~system</code>, keyed by the root path): every instance sharing this database shares <strong>one</strong> worker pool, reconciled by the <strong>elected supervisor</strong>. Changing it below overwrites that one setting for the whole deployment (last write wins) — instances don\'t run competing pools; they converge on this value. <strong>Actual</strong> catches up to desired as the supervisor forks/retires children.\n </div>\n\n \x3c!-- WHY this many — the headline operator answer --\x3e\n <div :style="\'font-size:13px;padding:10px 14px;border-left:3px solid \' + whyColor() + \';background:var(--okdb-app-bg-2);border-radius:0 6px 6px 0;\'">\n <span style="opacity:.6;font-size:11px;text-transform:uppercase;letter-spacing:.5px;">Why this many</span><br/>\n <span :style="\'color:\' + whyColor() + \';\'">{{ s.why }}</span>\n </div>\n\n \x3c!-- Backlog signals that drive scale-up. The number is PENDING WORK; the threshold\n is the LINE above which the scaler adds a worker — not a capacity/utilization. --\x3e\n <div style="font-size:12px;">\n <div style="color:var(--okdb-app-fg-subtle);margin-bottom:6px;">Pending work <span style="opacity:.6;">(hover a row for what it counts; the scaler adds a worker when any rises above its line)</span></div>\n <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:10px;">\n <each :of="signals" let:item="sig">\n <div :title="sig.title">\n <div style="display:flex;justify-content:space-between;align-items:baseline;">\n <span>{{ sig.label }}</span>\n <span :style="\'font-weight:600;color:\' + gaugeColor(sig.value, sig.threshold) + \';\'">{{ sig.value }}</span>\n </div>\n <div style="height:5px;background:var(--okdb-app-bg);border-radius:3px;margin-top:3px;overflow:hidden;"><div :style="\'height:100%;width:\' + gaugePct(sig.value, sig.threshold) + \'%;background:\' + gaugeColor(sig.value, sig.threshold) + \';\'"></div></div>\n <div style="font-size:10px;opacity:.55;margin-top:2px;">{{ signalNote(sig) }}</div>\n </div>\n </each>\n </div>\n </div>\n\n \x3c!-- Last scale event --\x3e\n <div style="font-size:12px;color:var(--okdb-app-fg-subtle);">\n Last scale event: <span style="color:var(--okdb-app-fg);">{{ lastScaleLabel() }}</span>\n <if :="s.scaling && s.scaling.awaitingOnline"><span style="color:var(--okdb-warning);"> · waiting for the last worker to come online</span></if>\n </div>\n\n \x3c!-- Controls --\x3e\n <div style="display:flex;align-items:center;gap:8px;padding-top:6px;border-top:1px solid var(--okdb-app-border);">\n <span style="font-size:11px;color:var(--okdb-app-fg-subtle);margin-right:4px;">Scale:</span>\n <button @click="setAuto()" :disabled="btnDisabled(\'auto\')" style="font-size:11px;padding:4px 12px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:4px;cursor:pointer;color:var(--okdb-app-fg);">{{ btnLabel(\'auto\', \'Auto\') }}</button>\n <button @click="setFixed()" :disabled="btnDisabled(\'fixed\')" style="font-size:11px;padding:4px 12px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-app-border);border-radius:4px;cursor:pointer;color:var(--okdb-app-fg);">{{ btnLabel(\'fixed\', \'Fixed…\') }}</button>\n <button @click="setOff()" :disabled="btnDisabled(\'off\')" style="font-size:11px;padding:4px 12px;background:var(--okdb-app-bg-2);border:1px solid var(--okdb-danger);border-radius:4px;cursor:pointer;color:var(--okdb-danger);">{{ btnLabel(\'off\', \'Off\') }}</button>\n </div>\n </if>\n </div>\n '};
|
package/types/environment.d.ts
CHANGED
|
@@ -69,7 +69,11 @@ export interface OKDBEnvironment {
|
|
|
69
69
|
// ── Read ──────────────────────────────────────────────────────────────────
|
|
70
70
|
get<T = unknown>(type: string, key: OKDBPrimaryKey, options?: Record<string, unknown>): T | undefined;
|
|
71
71
|
getMany<T = unknown>(type: string, keys: OKDBPrimaryKey[], options?: Record<string, unknown>): (T | undefined)[];
|
|
72
|
-
getEntry<T = unknown>(
|
|
72
|
+
getEntry<T = unknown>(
|
|
73
|
+
type: string,
|
|
74
|
+
key: OKDBPrimaryKey,
|
|
75
|
+
options?: Record<string, unknown>,
|
|
76
|
+
): OKDBEntry<T> | undefined;
|
|
73
77
|
getRange<T = unknown>(type: string, options?: OKDBRangeOptions): Iterable<OKDBEntry<T>>;
|
|
74
78
|
getValues<T = unknown>(type: string, options?: OKDBRangeOptions): Iterable<T>;
|
|
75
79
|
getKeys(type: string, options?: OKDBRangeOptions): Iterable<OKDBPrimaryKey>;
|
|
@@ -81,11 +85,20 @@ export interface OKDBEnvironment {
|
|
|
81
85
|
byIndex<T = unknown>(type: string, index: string[], options?: OKDBQueryOptions): Iterable<OKDBIndexEntry<T>>;
|
|
82
86
|
query<T = unknown>(type: string, filter: OKDBFilter, options?: OKDBQueryOptions): Iterable<OKDBIndexEntry<T>>;
|
|
83
87
|
geoQuery<T = unknown>(type: string, field: string, options: OKDBGeoQueryOptions): Iterable<OKDBEntry<T>>;
|
|
84
|
-
ftsQuery<T = unknown>(
|
|
88
|
+
ftsQuery<T = unknown>(
|
|
89
|
+
type: string,
|
|
90
|
+
name: string,
|
|
91
|
+
text: string,
|
|
92
|
+
filter?: OKDBFilter,
|
|
93
|
+
options?: OKDBFtsQueryOptions,
|
|
94
|
+
): Iterable<OKDBEntry<T>>;
|
|
85
95
|
|
|
86
96
|
// ── Type management ───────────────────────────────────────────────────────
|
|
87
97
|
registerType(type: string, timestamp?: number): Promise<void>;
|
|
88
|
-
ensureType(
|
|
98
|
+
ensureType(
|
|
99
|
+
type: string,
|
|
100
|
+
options?: { timestamp?: number; indexes?: Array<string[] | OKDBIndexDefinition> },
|
|
101
|
+
): Promise<void>;
|
|
89
102
|
hasType(type: string): boolean;
|
|
90
103
|
dropType(type: string, timestamp?: number): Promise<void>;
|
|
91
104
|
|
|
@@ -95,7 +108,11 @@ export interface OKDBEnvironment {
|
|
|
95
108
|
dropSchema(type: string): Promise<void>;
|
|
96
109
|
|
|
97
110
|
// ── Index management ──────────────────────────────────────────────────────
|
|
98
|
-
registerIndex(
|
|
111
|
+
registerIndex(
|
|
112
|
+
type: string,
|
|
113
|
+
index: string[],
|
|
114
|
+
options?: { type?: 'geo'; precision?: number; unique?: boolean; timestamp?: number },
|
|
115
|
+
): Promise<void>;
|
|
99
116
|
hasIndex(type: string, index: string[]): boolean;
|
|
100
117
|
dropIndex(type: string, index: string[], timestamp?: number): Promise<void>;
|
|
101
118
|
resetIndex(type: string, index: string[], clear?: boolean): Promise<void>;
|
|
@@ -28,11 +28,20 @@ export interface OKDBEmbeddingsAddPipelineOptions {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export declare class OKDBEmbeddings {
|
|
31
|
-
addPipeline(
|
|
31
|
+
addPipeline(
|
|
32
|
+
name: string,
|
|
33
|
+
sourceType: string,
|
|
34
|
+
options: OKDBEmbeddingsAddPipelineOptions,
|
|
35
|
+
): Promise<OKDBEmbeddingsPipeline>;
|
|
32
36
|
removePipeline(name: string, sourceType: string): Promise<void>;
|
|
33
37
|
getPipeline(name: string, sourceType: string): OKDBEmbeddingsPipeline | null;
|
|
34
38
|
listPipelines(sourceType?: string): OKDBEmbeddingsPipeline[];
|
|
35
|
-
search(
|
|
39
|
+
search(
|
|
40
|
+
name: string,
|
|
41
|
+
sourceType: string,
|
|
42
|
+
query: string | number[],
|
|
43
|
+
options?: OKDBEmbeddingsSearchOptions,
|
|
44
|
+
): Promise<OKDBEmbeddingsSearchResult[]>;
|
|
36
45
|
embed(model: string, text: string | string[]): Promise<number[] | number[][]>;
|
|
37
46
|
start(): Promise<void>;
|
|
38
47
|
stop(): Promise<void>;
|
|
@@ -140,13 +140,21 @@ export declare class OKDBQueue {
|
|
|
140
140
|
// ── Core job lifecycle ────────────────────────────────────────────────────
|
|
141
141
|
enqueue(type: string, payload: unknown, options?: OKDBEnqueueOptions): Promise<string>;
|
|
142
142
|
claim(type: string, options?: OKDBClaimOptions): Promise<OKDBJob | null>;
|
|
143
|
-
worker(
|
|
143
|
+
worker(
|
|
144
|
+
type: string,
|
|
145
|
+
handler: (job: OKDBJob) => Promise<unknown> | unknown,
|
|
146
|
+
options?: Record<string, unknown>,
|
|
147
|
+
): OKDBQueueWorker;
|
|
144
148
|
spawn(type: string, modulePath: string, options?: Record<string, unknown>): Promise<OKDBQueueSpawnedWorker>;
|
|
145
149
|
stopAll(timeout?: number): Promise<void>;
|
|
146
150
|
|
|
147
151
|
// ── Job retrieval / management ────────────────────────────────────────────
|
|
148
152
|
getJob(id: string): Promise<OKDBJob | null>;
|
|
149
|
-
updateJob(
|
|
153
|
+
updateJob(
|
|
154
|
+
id: string,
|
|
155
|
+
patch: Partial<OKDBJob & { payload: unknown }>,
|
|
156
|
+
options?: { txn?: OKDBQueueTransaction },
|
|
157
|
+
): Promise<OKDBJob>;
|
|
150
158
|
removeJob(id: string, options?: { txn?: OKDBQueueTransaction }): Promise<boolean>;
|
|
151
159
|
retryJob(id: string): Promise<OKDBJob>;
|
|
152
160
|
cancelJob(id: string): Promise<OKDBJob>;
|
|
@@ -158,9 +166,23 @@ export declare class OKDBQueue {
|
|
|
158
166
|
markJobProgress(jobId: string, claimId: string, message: string): Promise<OKDBJob>;
|
|
159
167
|
markJobComplete(jobId: string, claimId: string, result?: unknown): Promise<OKDBJob>;
|
|
160
168
|
completeJob(jobId: string, claimId: string, result?: unknown): Promise<OKDBJob>;
|
|
161
|
-
markJobFail(
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
markJobFail(
|
|
170
|
+
jobId: string,
|
|
171
|
+
claimId: string,
|
|
172
|
+
err: Error | unknown,
|
|
173
|
+
code?: string,
|
|
174
|
+
): Promise<{ permanentlyFailed: boolean; job: OKDBJob }>;
|
|
175
|
+
failJob(
|
|
176
|
+
jobId: string,
|
|
177
|
+
claimId: string,
|
|
178
|
+
err: Error | unknown,
|
|
179
|
+
code?: string,
|
|
180
|
+
): Promise<{ permanentlyFailed: boolean; job: OKDBJob }>;
|
|
181
|
+
releaseClaim(
|
|
182
|
+
jobId: string,
|
|
183
|
+
claimId: string,
|
|
184
|
+
options?: { when?: number; statusCode?: string; keepTry?: boolean },
|
|
185
|
+
): Promise<boolean>;
|
|
164
186
|
|
|
165
187
|
// ── Bulk operations ───────────────────────────────────────────────────────
|
|
166
188
|
clearDone(type?: string | null, limit?: number): Promise<number>;
|
|
@@ -11,20 +11,20 @@ export type OKDBMapFieldSpec = string | OKDBMapRefSpec | OKDBMapConcatSpec | OKD
|
|
|
11
11
|
export type OKDBViewMap = Record<string, OKDBMapFieldSpec>;
|
|
12
12
|
|
|
13
13
|
/** Built-in reducer specs */
|
|
14
|
-
export type OKDBCountSpec
|
|
15
|
-
export type OKDBSumSpec
|
|
16
|
-
export type OKDBAvgSpec
|
|
17
|
-
export type OKDBMinSpec
|
|
18
|
-
export type OKDBMaxSpec
|
|
19
|
-
export type OKDBCountBySpec
|
|
20
|
-
export type OKDBCustomSpec
|
|
14
|
+
export type OKDBCountSpec = { $count: true; items?: boolean };
|
|
15
|
+
export type OKDBSumSpec = { $sum: string; items?: boolean };
|
|
16
|
+
export type OKDBAvgSpec = { $avg: string; items?: boolean };
|
|
17
|
+
export type OKDBMinSpec = { $min: string; items?: boolean };
|
|
18
|
+
export type OKDBMaxSpec = { $max: string; items?: boolean };
|
|
19
|
+
export type OKDBCountBySpec = { $countBy: string; items?: boolean };
|
|
20
|
+
export type OKDBCustomSpec = { [reducer: string]: unknown; items?: boolean };
|
|
21
21
|
|
|
22
22
|
/** $ref sub-view aggregation */
|
|
23
23
|
export interface OKDBRefSpec {
|
|
24
|
-
type:
|
|
25
|
-
key:
|
|
24
|
+
type: string;
|
|
25
|
+
key: string;
|
|
26
26
|
filter?: OKDBFilter;
|
|
27
|
-
reduce:
|
|
27
|
+
reduce: Record<string, OKDBReducerSpec>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export type OKDBReducerSpec =
|
package/types/index.d.ts
CHANGED
|
@@ -4,7 +4,17 @@ export type * from './errors';
|
|
|
4
4
|
export type { OKDBTransaction, OKDBEnvironment } from './environment';
|
|
5
5
|
|
|
6
6
|
// Features
|
|
7
|
-
export type {
|
|
7
|
+
export type {
|
|
8
|
+
OKDBQueue,
|
|
9
|
+
OKDBJob,
|
|
10
|
+
OKDBJobStatus,
|
|
11
|
+
OKDBJobType,
|
|
12
|
+
OKDBJobBucket,
|
|
13
|
+
OKDBJobBucketRecord,
|
|
14
|
+
OKDBEnqueueOptions,
|
|
15
|
+
OKDBClaimOptions,
|
|
16
|
+
OKDBListJobsOptions,
|
|
17
|
+
} from './features/queue';
|
|
8
18
|
export type { OKDBFiles, OKDBFileRecord, OKDBUploadOptions } from './features/files';
|
|
9
19
|
export type { OKDBSync, OKDBSyncNode, OKDBSyncInfo } from './features/sync';
|
|
10
20
|
export type { OKDBAuth, OKDBToken, OKDBAuthContext, OKDBLoginResult } from './features/auth';
|
|
@@ -20,7 +30,25 @@ export type { OKDBAdmin } from './features/admin';
|
|
|
20
30
|
export type { OKDBApi } from './features/api';
|
|
21
31
|
|
|
22
32
|
// ── Main OKDB class ───────────────────────────────────────────────────────────
|
|
23
|
-
import type {
|
|
33
|
+
import type {
|
|
34
|
+
OKDBOptions,
|
|
35
|
+
OKDBEnvironmentConfig,
|
|
36
|
+
OKDBPrimaryKey,
|
|
37
|
+
OKDBIndexKey,
|
|
38
|
+
OKDBFilter,
|
|
39
|
+
OKDBRangeOptions,
|
|
40
|
+
OKDBQueryOptions,
|
|
41
|
+
OKDBGeoQueryOptions,
|
|
42
|
+
OKDBFtsQueryOptions,
|
|
43
|
+
OKDBWriteOptions,
|
|
44
|
+
OKDBEntry,
|
|
45
|
+
OKDBIndexEntry,
|
|
46
|
+
OKDBChangeEntry,
|
|
47
|
+
OKDBIndexDefinition,
|
|
48
|
+
OKDBRemoveEnvironmentResult,
|
|
49
|
+
OKDBTtlListOptions,
|
|
50
|
+
OKDBInfoResult,
|
|
51
|
+
} from './options';
|
|
24
52
|
import type { OKDBEnvironment, OKDBTransaction } from './environment';
|
|
25
53
|
import type { OKDBQueue } from './features/queue';
|
|
26
54
|
import type { OKDBFiles } from './features/files';
|
|
@@ -76,6 +104,9 @@ export declare class OKDB {
|
|
|
76
104
|
// ── Environment management ────────────────────────────────────────────────
|
|
77
105
|
readonly default: OKDBEnvironment;
|
|
78
106
|
env(name?: string): OKDBEnvironment;
|
|
107
|
+
/** Open a registered env on demand (companion to the `envs` constructor
|
|
108
|
+
* option). Idempotent; throws ENV_NOT_FOUND for an unregistered name. */
|
|
109
|
+
openEnv(name: string): Promise<OKDBEnvironment>;
|
|
79
110
|
createEnvironment(name: string, config?: OKDBEnvironmentConfig): Promise<OKDBEnvironment>;
|
|
80
111
|
removeEnvironment(name: string, options?: { keepSubEnvs?: string[] }): Promise<OKDBRemoveEnvironmentResult>;
|
|
81
112
|
|
|
@@ -96,7 +127,11 @@ export declare class OKDB {
|
|
|
96
127
|
// ── Read ──────────────────────────────────────────────────────────────────
|
|
97
128
|
get<T = unknown>(type: string, key: OKDBPrimaryKey, options?: Record<string, unknown>): T | undefined;
|
|
98
129
|
getMany<T = unknown>(type: string, keys: OKDBPrimaryKey[], options?: Record<string, unknown>): (T | undefined)[];
|
|
99
|
-
getEntry<T = unknown>(
|
|
130
|
+
getEntry<T = unknown>(
|
|
131
|
+
type: string,
|
|
132
|
+
key: OKDBPrimaryKey,
|
|
133
|
+
options?: Record<string, unknown>,
|
|
134
|
+
): OKDBEntry<T> | undefined;
|
|
100
135
|
getRange<T = unknown>(type: string, options?: OKDBRangeOptions): Iterable<OKDBEntry<T>>;
|
|
101
136
|
getValues<T = unknown>(type: string, options?: OKDBRangeOptions): Iterable<T>;
|
|
102
137
|
getKeys(type: string, options?: OKDBRangeOptions): Iterable<OKDBPrimaryKey>;
|
|
@@ -108,16 +143,29 @@ export declare class OKDB {
|
|
|
108
143
|
byIndex<T = unknown>(type: string, index: string[], options?: OKDBQueryOptions): Iterable<OKDBIndexEntry<T>>;
|
|
109
144
|
query<T = unknown>(type: string, filter: OKDBFilter, options?: OKDBQueryOptions): Iterable<OKDBIndexEntry<T>>;
|
|
110
145
|
geoQuery<T = unknown>(type: string, field: string, options: OKDBGeoQueryOptions): Iterable<OKDBEntry<T>>;
|
|
111
|
-
ftsQuery<T = unknown>(
|
|
146
|
+
ftsQuery<T = unknown>(
|
|
147
|
+
type: string,
|
|
148
|
+
name: string,
|
|
149
|
+
text: string,
|
|
150
|
+
filter?: OKDBFilter,
|
|
151
|
+
options?: OKDBFtsQueryOptions,
|
|
152
|
+
): Iterable<OKDBEntry<T>>;
|
|
112
153
|
|
|
113
154
|
// ── Type management ───────────────────────────────────────────────────────
|
|
114
155
|
registerType(type: string, timestamp?: number): Promise<void>;
|
|
115
|
-
ensureType(
|
|
156
|
+
ensureType(
|
|
157
|
+
type: string,
|
|
158
|
+
options?: { timestamp?: number; indexes?: Array<string[] | OKDBIndexDefinition> },
|
|
159
|
+
): Promise<void>;
|
|
116
160
|
hasType(type: string): boolean;
|
|
117
161
|
dropType(type: string, timestamp?: number): Promise<void>;
|
|
118
162
|
|
|
119
163
|
// ── Index management ──────────────────────────────────────────────────────
|
|
120
|
-
registerIndex(
|
|
164
|
+
registerIndex(
|
|
165
|
+
type: string,
|
|
166
|
+
index: string[],
|
|
167
|
+
options?: { type?: 'geo'; precision?: number; unique?: boolean; timestamp?: number },
|
|
168
|
+
): Promise<void>;
|
|
121
169
|
hasIndex(type: string, index: string[]): boolean;
|
|
122
170
|
dropIndex(type: string, index: string[], timestamp?: number): Promise<void>;
|
|
123
171
|
resetIndex(type: string, index: string[], clear?: boolean): Promise<void>;
|