@kedem/okdb 1.6.4 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/okdb.js +1 -1
- package/docs/change-log.md +23 -0
- package/docs/pipelines.md +6 -1
- package/docs/time-machine.md +177 -0
- package/okdb.js +1 -1
- package/package.json +1 -1
- package/public/sections/db/parts/db-overview.ok.js +1 -1
- package/public/sections/db/parts/type-viewer.ok.js +1 -1
- package/public/sections/system/index.ok.html +1 -1
- package/public/sections/system/parts/system-locks-panel.ok.js +1 -0
- package/public/sections/views/parts/view-detail.ok.js +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"@toolkit/elements/ok-icon.ok.js";import n from"@toolkit/elements/ok-table.ok.js";import t from"@toolkit/core/ok-overlay.ok.js";import i from"../../../components/geo-map.ok.js";import o from"../../../components/ok-json-input.ok.js";import s from"../../../view-models/type-query-vm.js";import{buildSiftSchema as a}from"../../../util/sift-schema.js";import r from"../modals/add-item-modal.ok.js";import l from"../modals/create-fts-modal.ok.js";import d from"../modals/create-index-modal.ok.js";import c from"../modals/create-pipeline-modal.ok.js";import p from"./edit-row.ok.js";import u from"../../../stores/events-store.js";import h from"./type-changelog.ok.js";import g from"./type-pipelines-tab.ok.js";import f from"./type-schema-tab.ok.js";function y(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function x(e,n){y(e)&&e.__key!==n&&Object.defineProperty(e,"__key",{value:n,enumerable:!1,configurable:!1,writable:!1})}function m(e,n=0){try{return JSON.stringify(e,null,n)??String(e)}catch{return String(e)}}export default{tag:"type-viewer",dependencies:[e,n,t,p,o,h,g,f,r,d,l,c,i],attr:{type:{}},context(e){let n=null,t=null,i=null;const o=new s({type:e.$attr.type,apiBase:()=>e.$ok.stores.okdb.apiBase});return{tab:"data",get envApiBase(){return e.$ok.stores.okdb.apiBase},get type(){return e.$attr.type},violationCount:0,async loadViolations(){if(this.type)try{const[e,n]=await Promise.all([fetch(`${this.envApiBase}/violations/${encodeURIComponent(this.type)}`),fetch(`${this.envApiBase}/ref-violations/${encodeURIComponent(this.type)}`)]);let t=0;if(e.ok){const n=await e.json();t+=(n?.data?.violations??n?.result?.violations??[]).length}if(n.ok){const e=await n.json();t+=(e?.data?.violations??e?.result?.violations??[]).length}this.violationCount=t}catch{}},onViolationCountChanged(e){this.violationCount=e},query:o,possibleIndexes:[],indexStatuses:{},createIdxPossible:[],createIdxTypeLoaded:null,ftsList:[],vectorEngines:[],typeEngineCount:0,searchMode:"scan",geoIndexes:[],geoField:"",geoLat:"",geoLon:"",geoRadius:"",geoSortAsc:!0,geoDistanceMap:{},mapOpen:!1,geoViewMode:"table",geoHighlightedKey:null,_geoSelSource:null,get geoMapPoints(){const e=this.geoField;return e?(this.query.items||[]).map(n=>{const t=n.value||{},i=((e,n)=>{let t=e;for(const e of n.split(".")){if(null==t)return null;t=t[e]}return t})(t,e);return{key:n.key,title:t.name||t.title||t.label||n.key,lat:i?.lat,lon:i?.lon,distance:n.distance}}).filter(e=>null!=e.lat&&null!=e.lon):[]},setTab(n){if(this.tab=n,"engines"===n)return;const t=e.$vars.$router;if(!t?._search?.tab)return;const i={...t._search};delete i.tab,delete i.mode,delete i.pipeline,t.replace(t.resolve(t._path||"",i,t._anchor||""))},switchToPipelines(){this.openTypePipelines({mode:"pipelines",pipeline:this.currentRoutePipeline||null})},get currentRoutePipeline(){return String(e.$vars?.$router?._search?.pipeline||"")},get currentRouteMode(){return"pipelines"===String(e.$vars?.$router?._search?.mode||"").trim()?"pipelines":"engines"},openTypePipelines(n={}){const t=e.$vars?.$router;if(!t?.resolve||!t?.push)return;const i=e.$ok.stores.okdb.selectedEnv||"default",o={...t._search||{},env:i,type:this.type,tab:"engines",mode:"pipelines"===n.mode?"pipelines":"engines"};n.pipeline?o.pipeline=n.pipeline:delete o.pipeline,t.push(t.resolve("/db",o))},openEnginesWorkspace(n={}){if("create"===n.view){const t=e.$vars?.$router;if(!t?.resolve||!t?.push)return;const i={env:e.$ok.stores.okdb.selectedEnv||"default",view:"create",sourceType:this.type};return n.kind&&(i.kind=n.kind),void t.push(t.resolve("/engines",i))}this.openTypePipelines({mode:n.mode||"engines",pipeline:n.pipeline||null})},openTypePipelinesForSelectedVector(){const e=this.selectedVectorEngineMeta?.meta?.pipeline||null;this.openTypePipelines({mode:"pipelines",pipeline:e})},openGlobalEnginesWorkspace(n={}){const t=e.$vars?.$router;if(!t?.resolve||!t?.push)return;const i={env:e.$ok.stores.okdb.selectedEnv||"default",sourceType:this.type};n.view&&"overview"!==n.view||(i.mode="pipelines"),n.view&&"overview"!==n.view&&(i.view=n.view),n.kind&&(i.kind=n.kind),n.pipeline&&(i.pipeline=n.pipeline),t.push(t.resolve("/engines",i))},async openEnginesCreatePipeline(){this.type&&await e.$ok.shared.modal.open({owner:e,title:`Create Pipeline · ${this.type}`,canDrag:!0,content:'<create-pipeline-modal :modal="modal" :type="type" :env="envName" :on-done="onDone" />',context:n=>({modal:n,type:this.type,envName:e.$ok.stores.okdb.selectedEnv||"default",onDone:async()=>{await Promise.all([this.loadVectorEngines(!0),this.loadTypeEnginesCount()])}})})},openEnginesPipeline(e=null){this.openTypePipelines({mode:"pipelines",pipeline:e||null})},switchToEngines(){this.openTypePipelines({mode:"engines",pipeline:this.currentRoutePipeline||null})},setSearchMode(n){if(this.searchMode=n,this.query.selectedIndex=null,this.query.selectedFtsIndex=null,this.query.selectedVectorEngine=null,this.query.selectedPrefix=void 0,this.query.selectedIndexStart=void 0,this.query.selectedIndexEnd=void 0,this.geoDistanceMap={},this.geoHighlightedKey=null,"geo"!==n){this.geoViewMode="table";try{e.$el?.querySelector("ok-table")?.clearSelection?.()}catch(e){}}},_updateGeoIndexes(){this.geoIndexes=this.possibleIndexes.filter(e=>this.indexStatuses[e]?.geoConfig),this.geoIndexes.length&&!this.geoField&&(this.geoField=this.geoIndexes[0])},async loadGeoQuery(){if(!this.type||!this.geoField)return;const e=parseFloat(this.geoLat),n=parseFloat(this.geoLon);if(isNaN(e)||isNaN(n))return;const t=this.geoRadius?parseFloat(this.geoRadius):void 0;this.query.busy=!0,this.geoDistanceMap={};try{const i={...this.query.query},o={lat:e,lon:n};t&&!isNaN(t)&&(o.radius=t),i[this.geoField]={$near:o};const s=await fetch(`${this.envApiBase}/type/${this.type}/query`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filter:i,options:{limit:this.query.pageSize}})});if(!s.ok)throw new Error(s.statusText);const{data:a,meta:r}=await s.json();let l=a??[];this.geoSortAsc||(l=l.slice().reverse()),this.query.items=l;const d={};this.query.values=l.map(e=>(x(e.value,e.key),null!=e.distance&&(d[e.key]=e.distance),e.value)),this.geoDistanceMap=d,this.query.hasNext=!1,this.query.hasPrev=!1,this.query._current=null,this.query._backStack=[],this.query.time=r?.time??null,this.query.total=l.length}catch(e){console.error("Geo query error",e)}finally{this.query.busy=!1}},get selectedVectorEngineMeta(){return this.query.selectedVectorEngine?this.vectorEngines.find(e=>this.displayVectorName(e.name)===this.query.selectedVectorEngine)??null:null},helpOpen:!1,get typeFields(){const n=(e.$ok.stores.okdb.types||[]).find(e=>e.type===this.type);return n?.fields||{}},get typeCount(){const n=(e.$ok.stores.okdb.types||[]).find(e=>e.type===this.type);return n?.count??"?"},get typeFieldEntries(){return Object.entries(this.typeFields).map(([e,n])=>({name:e,types:n}))},get indexEntries(){return this.possibleIndexes.map(e=>({name:e,fields:e.split("~"),status:this.indexStatuses[e]?.status||"ready",isGeo:!!this.indexStatuses[e]?.geoConfig,geoPrecision:this.indexStatuses[e]?.geoConfig?.precision??null,isUnique:!!this.indexStatuses[e]?.unique,createdBy:this.indexStatuses[e]?.createdBy??null,usedBy:this.indexStatuses[e]?.usedBy??[]}))},get selectedIndexInfo(){const e=this.query.selectedIndex;if(!e)return null;const n=this.indexStatuses[e];return{fields:e.split("~"),isGeo:!!n?.geoConfig,geoPrecision:n?.geoConfig?.precision??null,isUnique:!!n?.unique}},get ftsEntries(){return this.ftsList.map(e=>({name:e.name,status:e.status||"ready",fields:(e.config&&e.config.fields?e.config.fields:[]).join(", "),sizeBytes:e.sizeBytes??null}))},fmtBytes(e){if(null==e||0===e)return"—";const n=["B","KB","MB","GB"];let t=e,i=0;for(;t>=1024&&i<n.length-1;)t/=1024,i++;return t.toFixed(1)+" "+n[i]},ttlStats:null,ttlStatsLoading:!1,ttlSweepBusy:!1,defaultTTL:null,defaultTTLInput:"",defaultTTLPreset:"",defaultTTLBusy:!1,async loadTTLStats(){if(this.type){this.ttlStatsLoading=!0;try{const e=await fetch(`${this.envApiBase}/ttl/stats?type=${encodeURIComponent(this.type)}`),n=await e.json();this.ttlStats=n.data??n.result??null}catch{this.ttlStats=null}finally{this.ttlStatsLoading=!1}}},async loadDefaultTTL(){if(this.type)try{const e=await fetch(`${this.envApiBase}/type/${encodeURIComponent(this.type)}/ttl/default`),n=await e.json(),t=n.data?.defaultTTL??n.result?.defaultTTL??null;this.defaultTTL=t,this.defaultTTLInput=t?String(t):"",this.defaultTTLPreset=t?String(t):""}catch{this.defaultTTL=null}},async setDefaultTTLAction(){const n=parseInt(this.defaultTTLInput);if(n&&!(n<=0)){this.defaultTTLBusy=!0;try{const t=await fetch(`${this.envApiBase}/type/${encodeURIComponent(this.type)}/ttl/default`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({ttl:n})});if(!t.ok){const e=await t.json().catch(()=>({}));throw new Error(e?.error?.message||t.statusText)}this.defaultTTL=n,e.$ok.shared?.toast?.info?.("Default TTL set ("+this.fmtDuration(n)+")")}catch(n){e.$ok.shared?.toast?.error?.(n.message||"Failed to set default TTL")}finally{this.defaultTTLBusy=!1}}},async clearDefaultTTLAction(){this.defaultTTLBusy=!0;try{const n=await fetch(`${this.envApiBase}/type/${encodeURIComponent(this.type)}/ttl/default`,{method:"DELETE"});if(!n.ok)throw new Error(n.statusText);this.defaultTTL=null,this.defaultTTLInput="",this.defaultTTLPreset="",e.$ok.shared?.toast?.info?.("Default TTL cleared")}catch(n){e.$ok.shared?.toast?.error?.(n.message||"Failed to clear default TTL")}finally{this.defaultTTLBusy=!1}},onDefaultTTLPresetChange(e){this.defaultTTLPreset=e,e&&(this.defaultTTLInput=e)},async triggerSweep(){this.ttlSweepBusy=!0;try{const n=await fetch(`${this.envApiBase}/ttl/sweep`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({batchSize:1e3})}),t=await n.json(),i=`Sweep done: ${(t.data??t.result??{}).removed??0} expired item(s) removed`;e.$ok.shared?.toast?.info?.(i),await this.loadTTLStats(),await this.query.load()}catch(n){e.$ok.shared?.toast?.error?.(n.message||"Sweep failed")}finally{this.ttlSweepBusy=!1}},fmtDuration(e){if(null==e)return"—";const n=Math.floor(e/1e3);return n<60?n+"s":n<3600?Math.floor(n/60)+"m "+n%60+"s":n<86400?Math.floor(n/3600)+"h "+Math.floor(n%3600/60)+"m":Math.floor(n/86400)+"d "+Math.floor(n%86400/3600)+"h"},get vectorEntries(){return this.vectorEngines.map(e=>({name:e.name,displayName:this.displayVectorName(e.name),status:e.isRunning?"running":e.status||"stopped",running:e.isRunning,pipeline:e.meta&&e.meta.pipeline?e.meta.pipeline:null}))},get siftSchema(){return a(this.typeFields)},async loadIndexes(e=!1){if(this.type&&(e||n!==this.type))try{const e=await fetch(`${this.envApiBase}/type/${this.type}/indexes`),t=await e.json(),i=t.data??t.result??{};this.possibleIndexes=Object.keys(i),this.indexStatuses=i,this._updateGeoIndexes(),n=this.type}catch(e){console.error("Error fetching indexes",e)}},async loadFtsIndexes(e=!1){if(this.type&&(e||t!==this.type))try{const e=await fetch(`${this.envApiBase}/type/${this.type}/fts`),{data:n}=await e.json();this.ftsList=n||[],t=this.type}catch(e){console.error("Error fetching FTS indexes",e)}},async loadVectorEngines(n=!1){if(this.type&&(n||i!==this.type))try{const n=e.$ok.stores.okdb.selectedEnv||"default",[t,o]=await Promise.allSettled([fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/engines`).then(e=>e.json()),fetch(`${__ENV__.API}/embeddings/embedders`).then(e=>e.json())]),s="fulfilled"===t.status?t.value?.result??t.value?.data??[]:[],a=[..."fulfilled"===o.status?o.value?.data??[]:[],...s],r={};for(const e of a)"indexer"===e.type&&e.config&&(r[e.name]={source_type:e.config.source_type,source_env:e.config.source_env??"default"});this.vectorEngines=a.filter(e=>{if("vector-search"!==e.type)return!1;const t=e.config?.source_type,i=e.config?.source_env??"default";if(t===this.type&&i===n)return!0;const o=r[e.name];return o?.source_type===this.type&&(o?.source_env??"default")===n}),i=this.type}catch(e){this.vectorEngines=[]}},async loadTypeEnginesCount(){if(this.type)try{const n=e.$ok.stores.okdb.selectedEnv||"default",t=await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/type/${encodeURIComponent(this.type)}/pipelines`),i=await t.json().catch(()=>({})),o=t.ok?i?.data??[]:[],s=new Set;for(const e of o.flatMap(e=>e?.engines??[])){const n=e?.key??(e?.type&&e?.name?`${e.type}@${e.name}`:e?.name??null);n&&s.add(n)}this.typeEngineCount=s.size}catch{this.typeEngineCount=0}else this.typeEngineCount=0},async recomputeFields(){if(this.type)try{await fetch(`${this.envApiBase}/type/${this.type}/recompute-fields`,{method:"POST"}),await e.$ok.stores.okdb.reloadTypes(),e.$ok.shared?.toast?.success?.("Fields recomputed")}catch(n){console.error("recomputeFields error",n),e.$ok.shared?.toast?.error?.("Recompute failed")}},indexStatusBadge(e){const n=this.indexStatuses?.[e]?.status;return n&&"ready"!==n?n:null},ftsStatusBadge(e){const n=e?.status;return n&&"ready"!==n?n:null},keyOf:e=>e?.__key??e?.key??null,get hasDirectValueRows(){return(this.query.items||[]).some(e=>!y(e?.value))},get displayRows(){return this.hasDirectValueRows?this.query.items:this.query.values},rowValueOf:e=>e&&"object"==typeof e&&"key"in e&&"value"in e?e.value:e,directValueKindOf(e){const n=this.rowValueOf(e);return null===n?"null":Array.isArray(n)?"array":y(n)?null:"scalar"},inlineValueOf(e){return m(this.rowValueOf(e))},prettyValueOf(e){return m(this.rowValueOf(e),2)},scoreOf(e){const n=this.keyOf(e);return n?this.query.ftsScoreMap?.[n]?.score??null:null},matchOf(e){const n=this.keyOf(e);if(!n)return null;const t=this.query.ftsScoreMap?.[n];if(!t||null==t.score)return null;const i=t.maxScore??2*t.numTerms;return`${t.score}/${i}`},vectorScoreOf(e){const n=this.keyOf(e);if(!n)return null;const t=this.query.vectorScoreMap?.[n];return null==t?null:`${Math.round(100*t)}%`},vectorChunkMetaOf(e){const n=this.keyOf(e);return n?this.query.vectorChunkMetaMap?.[n]??null:null},distanceOf(e){const n=this.keyOf(e);if(!n)return null;const t=this.geoDistanceMap?.[n];return null==t?null:t<1e3?`${Math.round(t)} m`:`${(t/1e3).toFixed(1)} km`},displayVectorName(n){const t=(e.$ok.stores.okdb.selectedEnv||"default")+":";return n.startsWith(t)?n.slice(t.length):n},selectVectorEngine(e,n){if("__create_pipeline__"===e)return n&&(n.target.value=this.query.selectedVectorEngine||""),void this.openCreatePipelineModal();this.query.selectedVectorEngine=e||null,this.query.selectedVectorEngine&&(this.query.selectedIndex=null,this.query.selectedFtsIndex=null,this.query.selectedPrefix=void 0,this.query.selectedIndexStart=void 0,this.query.selectedIndexEnd=void 0)},openCreatePipelineModal(){this.openEnginesCreatePipeline()},createMenuOpen:!1,onRowChanged(n){const t=n?.detail??null,i=t?.key??null;if(null!=i){const e=this.query.items.findIndex(e=>e?.key===i);if(e>=0){const n=t?.value;x(n,i),this.query.items.splice(e,1,{...this.query.items[e],value:n}),e<this.query.values.length&&this.query.values.splice(e,1,n)}}e.$ok.shared.toast.info("Item updated"),this.loadViolations()},onRemoveRow(e){const n=this.query.items.findIndex(n=>n?.key===e);n>=0&&(this.query.values.splice(n,1),this.query.items.splice(n,1))},async openAddModal(){await e.$ok.shared.modal.open({owner:e,title:"Add item",content:'<add-item-modal :modal="modal" :type="type" :on-done="onDone" />',context:e=>({modal:e,type:this.type,onDone:async({key:e,value:n})=>{x(n,e),this.query.values.unshift(n),this.query.items.unshift({key:e,value:n})}})})},async openCreateIndex(){let n={},t=[];try{const e=await fetch(`${this.envApiBase}/type/${this.type}/deep-properties`),i=await e.json();n=(i.data??i.result)?.paths??{},t=Object.keys(n).filter(e=>n[e].includes("geo"))}catch{const t=(e.$ok.stores.okdb.types||[]).find(e=>e.type===this.type);for(const e of Object.keys(t?.fields||{}))n[e]=t.fields[e]}await e.$ok.shared.modal.open({owner:e,title:"Create Index",content:'<create-index-modal :modal="modal" :type="type" :deep-paths="deepPaths" :geo-fields="geoFields" :on-done="onDone" />',context:e=>({modal:e,type:this.type,deepPaths:n,geoFields:t,onDone:async()=>{await this.loadIndexes(!0)}})})},async dropType(){if(this.type&&await e.$ok.shared.modal.confirm(`Drop type "${this.type}"? This cannot be undone.`))try{await fetch(`${this.envApiBase}/type/${this.type}`,{method:"DELETE"}),await e.$ok.stores.okdb.reloadTypes(),e.$el.dispatchEvent(new CustomEvent("dropped",{detail:this.type}))}catch(e){console.error("Drop type error",e)}},async removeIndex(n){const t=n||this.query.selectedIndex;if(t&&await e.$ok.shared.modal.confirm(`Remove index "${t}"?`)){try{const n=await fetch(`${this.envApiBase}/type/${this.type}/index/${t}`,{method:"DELETE"});if(!n.ok){const t=await n.json().catch(()=>({})),i=t?.error?.code;if("INDEX_HAS_CONSUMERS"===i){const n=(t?.error?.details?.usedBy??[]).map(e=>`${e.kind}:${e.name}`).join(", ");return void e.$ok.shared?.toast?.error?.(`Cannot remove: index is used by ${n||"a consumer"}`)}throw new Error(t?.error?.message??"Remove failed")}}catch(n){return console.error("Remove index error",n),void e.$ok.shared?.toast?.error?.(n.message)}this.query.selectedIndex===t&&(this.query.selectedIndex=null),this.query.resetPaging(),await this.loadIndexes(!0),await this.query.load()}},selectIndex(e){if("__create_index__"===e)return this.query.selectedIndex=null,void this.openCreateIndex();"__remove_index__"!==e?(this.query.selectedIndex=e||null,this.query.selectedIndex&&(this.query.selectedFtsIndex=null),this.query.selectedIndex||(this.query.selectedPrefix=void 0,this.query.selectedIndexStart=void 0,this.query.selectedIndexEnd=void 0)):this.removeIndex()},selectFtsIndex(e,n){if("__create_fts__"===e)return n&&(n.target.value=this.query.selectedFtsIndex||""),void this.openCreateFtsModal();"__drop_fts__"!==e?(this.query.selectedFtsIndex=e||null,this.query.selectedFtsIndex&&(this.query.selectedIndex=null,this.query.selectedPrefix=void 0,this.query.selectedIndexStart=void 0,this.query.selectedIndexEnd=void 0)):this.dropFtsIndex()},async openCreateFtsModal(){let n={};try{const e=await fetch(`${this.envApiBase}/type/${this.type}/deep-properties`),t=await e.json();n=(t.data??t.result)?.paths??{}}catch{const t=(e.$ok.stores.okdb.types||[]).find(e=>e.type===this.type);for(const e of Object.keys(t?.fields||{}))n[e]=t.fields[e]}await e.$ok.shared.modal.open({owner:e,title:"Create FTS Index",content:'<create-fts-modal :modal="modal" :type="type" :api-base="apiBase" :deep-paths="deepPaths" :on-done="onDone" />',context:e=>({modal:e,type:this.type,apiBase:this.envApiBase,deepPaths:n,onDone:async()=>{await this.loadFtsIndexes(!0)}})})},async dropFtsIndex(n){const t=n||this.query.selectedFtsIndex;if(t&&await e.$ok.shared.modal.confirm(`Drop FTS index "${t}"? This cannot be undone.`)){try{await fetch(`${this.envApiBase}/type/${this.type}/fts/${encodeURIComponent(t)}`,{method:"DELETE"})}catch(n){return console.error("Drop FTS error",n),void e.$ok.shared?.toast?.error?.("Drop FTS failed")}this.query.selectedFtsIndex===t&&(this.query.selectedFtsIndex=null),await this.loadFtsIndexes(!0),await this.query.load()}},async rebuildIndex(n){if(n&&await e.$ok.shared.modal.confirm(`Rebuild index "${n}"? This will re-scan all records.`))try{await fetch(`${this.envApiBase}/type/${this.type}/index/${encodeURIComponent(n)}/reset`,{method:"POST"}),e.$ok.shared?.toast?.info?.(`Rebuilding index "${n}"…`),await this.loadIndexes(!0)}catch(n){console.error("Rebuild index error",n),e.$ok.shared?.toast?.error?.("Rebuild failed")}},async rebuildFts(n){const t=n||this.query.selectedFtsIndex;if(t&&await e.$ok.shared.modal.confirm(`Rebuild FTS index "${t}"? This will re-index all records.`))try{await fetch(`${this.envApiBase}/type/${this.type}/fts/${encodeURIComponent(t)}/reset`,{method:"POST"}),e.$ok.shared?.toast?.info?.(`Rebuilding FTS index "${t}"…`),await this.loadFtsIndexes(!0)}catch(n){console.error("Rebuild FTS error",n),e.$ok.shared?.toast?.error?.("Rebuild failed")}},onGeoTableSelect(e){if("geo"!==this.searchMode)return;this._geoSelSource="table";const n=e.detail?.action;this.geoHighlightedKey="deselect"===n?null:e.detail?.row?.__key??null},onGeoMapSelect(e){const n=e.detail?.key??null;this._geoSelSource="map",this.geoHighlightedKey=this.geoHighlightedKey===n?null:n}}},_unsubs:[],async init(e){if(e.$watch(()=>this.type,async()=>{this.query.type=this.type,this.query.selectedFtsIndex=null,this.query.selectedVectorEngine=null,this.searchMode="scan",this.query.resetPaging(),await this.loadIndexes(!0),await this.loadFtsIndexes(!0),await this.loadVectorEngines(!0),await this.loadTypeEnginesCount(),await this.loadViolations(),await this.query.load()}),e.$watch(()=>[this.query.query,this.query.selectedIndex,this.query.selectedIndexStart,this.query.selectedIndexEnd,this.query.selectedDirection,this.query.queryMode,this.query.selectedPrefix,this.query.selectedFtsIndex,this.query.ftsText,this.query.ftsMode,this.query.ftsPrefix,this.query.selectedVectorEngine,this.query.vectorText,this.query.vectorLimit],async()=>{this.type&&(this.query.resetPaging(),await this.query.load())}),e.$watch(()=>[this.searchMode,this.geoField,this.geoLat,this.geoLon,this.geoRadius,this.geoSortAsc,this.query.query],async()=>{"geo"===this.searchMode&&this.type&&this.geoField&&this.geoLat&&this.geoLon&&await this.loadGeoQuery()}),this.type){const n=new Set(["data","changelog","manage","schema","pipelines","engines"]),t=e.$vars.$router?.search?.tab;t&&n.has(t)&&(this.tab="pipelines"===t?"engines":t),await Promise.all([this.loadIndexes(),this.loadFtsIndexes(),this.loadVectorEngines(),this.loadTypeEnginesCount(),this.loadViolations(),this.loadTTLStats(),this.loadDefaultTTL(),this.query.load()])}this._unsubs=[u.on("index:reset",()=>{this.loadIndexes(!0),this.loadFtsIndexes(!0)}),u.on("index:ready",n=>{this.loadIndexes(!0),this.loadFtsIndexes(!0);const t=n?.type;if(!t||t===this.type){const t=n?.index,i=Array.isArray(t)&&"fts"===t[0],o=i?t[1]:"string"==typeof t?t:"",s=i?`FTS index ready: ${o}`:o?`Index ready: ${o}`:"Index rebuild complete";e.$ok.shared?.toast?.success?.(s)}}),u.on("index:registered",()=>this.loadIndexes(!0)),u.on("index:drop",()=>this.loadIndexes(!0)),u.on("fts:registered",()=>this.loadFtsIndexes(!0)),u.on("fts:drop",()=>this.loadFtsIndexes(!0)),u.on("engine:started",()=>this.loadVectorEngines(!0)),u.on("engine:stopped",()=>this.loadVectorEngines(!0)),u.on("engine:error",()=>this.loadVectorEngines(!0)),u.on("engine:installed",()=>this.loadVectorEngines(!0)),u.on("engine:uninstalled",()=>this.loadVectorEngines(!0)),u.on("engine:started",()=>this.loadTypeEnginesCount()),u.on("engine:stopped",()=>this.loadTypeEnginesCount()),u.on("engine:error",()=>this.loadTypeEnginesCount()),u.on("engine:installed",()=>this.loadTypeEnginesCount()),u.on("engine:uninstalled",()=>this.loadTypeEnginesCount()),u.on("embeddings:pipeline:ready",()=>this.loadVectorEngines(!0)),u.on("embeddings:pipeline:ready",()=>this.loadTypeEnginesCount())];const n=new Set(["data","changelog","manage","schema","pipelines","engines"]);e.$watch(()=>e.$vars.$router?._search?.tab,()=>{const t=e.$vars.$router?._search?.tab;if(!t)return;const i=n.has(t)?"pipelines"===t?"engines":t:"data";this.tab!==i&&(this.tab=i)}),e.$watch(()=>this.geoHighlightedKey,n=>{const t=this._geoSelSource;if(this._geoSelSource=null,"geo"!==this.searchMode)return;if("table"===t)return;const i=e.$el?.querySelector("ok-table");if(!i)return;if(!n)return i.clearSelection?.(),void i.clearExpanded?.();const o=this.displayRows?.find(e=>this.keyOf(e)===n);o?(i.select?.(o),i.clearExpanded?.(),i.expand?.(o)):(i.clearSelection?.(),i.clearExpanded?.())})},unmount(){for(const e of this._unsubs)e();this._unsubs=[]},template:'\n<div style="display:flex; flex-direction:column; gap:0; height:100%;">\n\n \x3c!-- Tab bar --\x3e\n <div style="display:flex; gap:2px; border-bottom:1px solid var(--okdb-app-border); margin-bottom:12px; flex-shrink:0;">\n <button\n @click="setTab(\'data\')"\n :style="tab===\'data\' ? \'border-color:var(--okdb-accent); color:var(--okdb-accent); background:color-mix(in srgb, var(--okdb-accent) 7%, transparent); border-bottom-color:transparent;\' : \'border-color:transparent; background:transparent;\'"\n style="padding:6px 16px; border-radius:6px 6px 0 0; font-size:13px; cursor:pointer;">\n Data\n </button>\n <button\n @click="setTab(\'changelog\')"\n :style="tab===\'changelog\' ? \'border-color:var(--okdb-accent); color:var(--okdb-accent); background:color-mix(in srgb, var(--okdb-accent) 7%, transparent); border-bottom-color:transparent;\' : \'border-color:transparent; background:transparent;\'"\n style="padding:6px 16px; border-radius:6px 6px 0 0; font-size:13px; cursor:pointer;">\n Changelog\n </button>\n <button\n @click="setTab(\'manage\')"\n :style="tab===\'manage\' ? \'border-color:var(--okdb-accent); color:var(--okdb-accent); background:color-mix(in srgb, var(--okdb-accent) 7%, transparent); border-bottom-color:transparent;\' : \'border-color:transparent; background:transparent;\'"\n style="padding:6px 16px; border-radius:6px 6px 0 0; font-size:13px; cursor:pointer;">\n Manage\n </button>\n <button\n @click="setTab(\'schema\')"\n :style="tab===\'schema\' ? (\'border-color:\' + (violationCount > 0 ? \'var(--okdb-danger)\' : \'var(--okdb-success)\') + \'; color:\' + (violationCount > 0 ? \'var(--okdb-danger)\' : \'var(--okdb-success)\') + \'; background:\' + (violationCount > 0 ? \'#f8717111\' : \'color-mix(in srgb, var(--okdb-success) 7%, transparent)\') + \'; border-bottom-color:transparent;\') : (violationCount > 0 ? \'border-color:transparent; background:transparent; color:var(--okdb-danger);\' : \'border-color:transparent; background:transparent;\')"\n style="padding:6px 16px; border-radius:6px 6px 0 0; font-size:13px; cursor:pointer;">\n Schema<if :="violationCount > 0"><span style="margin-left:6px; font-size:10px; padding:2px 7px; border-radius:999px; background:color-mix(in srgb, var(--okdb-danger) 13%, transparent); color:var(--okdb-danger); font-weight:600;">{{ violationCount }}</span></if>\n </button>\n <button\n @click="switchToEngines()"\n :style="tab===\'engines\' ? \'border-color:var(--okdb-purple); color:var(--okdb-purple); background:color-mix(in srgb, var(--okdb-purple) 7%, transparent); border-bottom-color:transparent;\' : \'border-color:transparent; background:transparent;\'"\n style="padding:6px 16px; border-radius:6px 6px 0 0; font-size:13px; cursor:pointer;">\n Engines<if :="typeEngineCount > 0"><span class="mode-count">{{ typeEngineCount }}</span></if>\n </button>\n </div>\n\n \x3c!-- Data tab --\x3e\n <if :="tab === \'data\'">\n <div style="display:flex; flex-direction:column; gap:16px; flex:1; min-height:0;">\n\n <div class="card" style="display:flex; gap:16px;">\n\n \x3c!-- ── Left column: query controls ── --\x3e\n <div style="flex:1; display:flex; flex-direction:column; gap:10px; min-width:0;">\n\n \x3c!-- Row 1: filter label + accordion + JSON input --\x3e\n <div style="display:flex; flex-direction:column; gap:6px;">\n\n \x3c!-- Label row --\x3e\n <div style="display:flex; align-items:center; gap:6px;">\n <span style="font-size:12px; color:#6a8a9a; user-select:none;">\n {{ searchMode === \'vector\' ? \'Post-filter vector results with sift\' : searchMode === \'fts\' ? \'Post-filter FTS results with sift\' : searchMode === \'geo\' ? \'Post-filter geo results with sift\' : \'Filter with sift query\' }}\n </span>\n <span class="help-toggle" @click="helpOpen = !helpOpen" :class="helpOpen ? \'help-toggle--open\' : \'\'">i</span>\n </div>\n\n \x3c!-- Accordion: query reference --\x3e\n <if :="helpOpen">\n <div class="help-accordion">\n <div class="ht-row"><code>{}</code><span>match all records</span></div>\n <div class="ht-row"><code>{"field": "value"}</code><span>exact match</span></div>\n <div class="ht-row"><code>{"n": {"$gt": 5, "$lt": 50}}</code><span>numeric comparison</span></div>\n <div class="ht-row"><code>{"x": {"$in": ["a","b"]}}</code><span>set membership</span></div>\n <div class="ht-row"><code>{"x": {"$exists": true}}</code><span>field presence</span></div>\n <div class="ht-row ht-row--note">Nested fields use dot notation: <code>"a.b"</code></div>\n </div>\n </if>\n\n <ok-json-input\n :value="query.query"\n @change="query.query = $event.detail"\n :schema="siftSchema"\n style="min-width:220px; min-height:60px;">\n </ok-json-input>\n\n </div>\n\n \x3c!-- Mode bar: Scan / Index / FTS / Vector --\x3e\n <div style="display:flex; gap:4px; align-items:center; flex-wrap:wrap;">\n <div class="seg-toggle" style="flex-shrink:0;">\n <button :class="searchMode === \'scan\' ? \'active\' : \'\'" @click="setSearchMode(\'scan\')">Scan</button>\n <button :class="searchMode === \'index\' ? \'active\' : \'\'" @click="setSearchMode(\'index\')">\n Index<if :="possibleIndexes.length > 0"><span class="mode-count">{{ possibleIndexes.length }}</span></if>\n </button>\n <button :class="searchMode === \'fts\' ? \'active\' : \'\'" @click="setSearchMode(\'fts\')">\n FTS<if :="ftsList.length > 0"><span class="mode-count">{{ ftsList.length }}</span></if>\n </button>\n <button :class="searchMode === \'vector\' ? \'active\' : \'\'" @click="setSearchMode(\'vector\')">\n Vector<if :="vectorEngines.length > 0"><span class="mode-count">{{ vectorEngines.length }}</span></if>\n </button>\n <button :class="searchMode === \'geo\' ? \'active\' : \'\'" @click="setSearchMode(\'geo\')">\n Geo<if :="geoIndexes.length > 0"><span class="mode-count">{{ geoIndexes.length }}</span></if>\n </button>\n </div>\n </div>\n\n \x3c!-- ── Scan panel ── --\x3e\n <if :="searchMode === \'scan\'">\n <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">\n <button\n @click="query.selectedDirection = query.selectedDirection === \'desc\' ? \'asc\' : \'desc\'"\n style="min-width:78px;"\n title="Toggle key scan direction">\n {{ query.selectedDirection === \'desc\' ? \'↓ Desc\' : \'↑ Asc\' }}\n </button>\n <span style="font-size:12px; opacity:.4; user-select:none;">Full key-ordered scan · sift applied as filter</span>\n </div>\n </if>\n\n \x3c!-- ── Index panel ── --\x3e\n <if :="searchMode === \'index\'">\n <div style="display:flex; flex-direction:column; gap:8px;">\n\n <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">\n <select @change="selectIndex($event.target.value)" style="min-width:160px;">\n <option value="">— none —</option>\n <each :of="possibleIndexes" let:item="idx">\n <option :value="idx" :selected="query.selectedIndex === idx">\n {{ idx }}{{ indexStatuses[idx]?.unique ? \' ◆\' : \'\' }}{{ indexStatusBadge(idx) ? \' ⚠ \' + indexStatusBadge(idx) : \'\' }}\n </option>\n </each>\n <option value="__create_index__" style="color:var(--okdb-accent);">+ Create index…</option>\n <if :="query.selectedIndex">\n <option value="__remove_index__" style="color:var(--okdb-danger);">× Remove "{{ query.selectedIndex }}"</option>\n </if>\n </select>\n\n <button\n @click="query.selectedDirection = query.selectedDirection === \'desc\' ? \'asc\' : \'desc\'"\n style="min-width:78px;"\n title="Toggle sort direction">\n {{ query.selectedDirection === \'desc\' ? \'↓ Desc\' : \'↑ Asc\' }}\n </button>\n\n <if :="query.selectedIndex">\n <div class="seg-toggle">\n <button\n :class="query.queryMode === \'prefix\' ? \'active\' : \'\'"\n @click="query.queryMode = \'prefix\', query.selectedIndexStart = undefined, query.selectedIndexEnd = undefined">\n Prefix\n </button>\n <button\n :class="query.queryMode === \'range\' ? \'active\' : \'\'"\n @click="query.queryMode = \'range\', query.selectedPrefix = undefined">\n Range\n </button>\n </div>\n </if>\n </div>\n\n \x3c!-- ── Index detail strip ── --\x3e\n <if :="query.selectedIndex && selectedIndexInfo">\n <div class="idx-info-strip">\n <span class="idx-info-label">Fields:</span>\n <each :of="selectedIndexInfo.fields" let:item="f">\n <code class="idx-info-field">{{ f }}</code>\n </each>\n <if :="selectedIndexInfo.isUnique">\n <span class="idx-info-badge idx-info-badge--unique">◆ unique</span>\n </if>\n <if :="selectedIndexInfo.isGeo">\n <span class="idx-info-badge idx-info-badge--geo">◎ geo · precision {{ selectedIndexInfo.geoPrecision ?? 7 }}</span>\n </if>\n <if :="!selectedIndexInfo.isUnique && !selectedIndexInfo.isGeo">\n <span class="idx-info-badge idx-info-badge--regular">regular</span>\n </if>\n </div>\n </if>\n\n <if :="query.selectedIndex">\n <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">\n <if :="query.queryMode === \'prefix\'">\n <span style="opacity:.6; font-size:12px;">Prefix:</span>\n <ok-json-input mode="input" :value="query.selectedPrefix" @change="query.selectedPrefix = $event.detail" style="width:220px;" />\n <span style="opacity:.35; font-size:11px;">scalar or array for composite indexes</span>\n </if>\n <if :="query.queryMode === \'range\'">\n <span style="opacity:.6; font-size:12px;">Start:</span>\n <ok-json-input mode="input" :value="query.selectedIndexStart" @change="query.selectedIndexStart = $event.detail" style="width:180px;" />\n <span style="opacity:.6; font-size:12px;">End:</span>\n <ok-json-input mode="input" :value="query.selectedIndexEnd" @change="query.selectedIndexEnd = $event.detail" style="width:180px;" />\n </if>\n </div>\n </if>\n\n </div>\n </if>\n\n \x3c!-- ── FTS panel ── --\x3e\n <if :="searchMode === \'fts\'">\n <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">\n\n <select @change="selectFtsIndex($event.target.value, $event)" style="min-width:160px;">\n <option value="">— none —</option>\n <each :of="ftsList" let:item="fts">\n <option :value="fts.name" :selected="query.selectedFtsIndex === fts.name">\n {{ fts.name }}{{ ftsStatusBadge(fts) ? \' ⚠ \' + ftsStatusBadge(fts) : \'\' }}\n </option>\n </each>\n <option value="__create_fts__" style="color:var(--okdb-accent);">+ Create FTS index…</option>\n <if :="query.selectedFtsIndex">\n <option value="__drop_fts__" style="color:var(--okdb-danger);">× Drop "{{ query.selectedFtsIndex }}"</option>\n </if>\n </select>\n\n <if :="query.selectedFtsIndex">\n <input\n type="text"\n placeholder="Search…"\n :value="query.ftsText"\n @input="query.ftsText = $event.target.value"\n style="flex:1; min-width:140px; max-width:300px;"\n />\n <div class="seg-toggle" title="AND: all terms required · OR: any term matches, ranked by hit count">\n <button :class="query.ftsMode === \'and\' ? \'active\' : \'\'" @click="query.ftsMode = \'and\'">AND</button>\n <button :class="query.ftsMode === \'or\' ? \'active\' : \'\'" @click="query.ftsMode = \'or\'">OR</button>\n </div>\n <label style="font-size:12px; display:flex; align-items:center; gap:4px; cursor:pointer; white-space:nowrap; user-select:none;" title="Prefix: \'sta\' matches \'start\', \'stack\', etc.">\n <input type="checkbox" :checked="query.ftsPrefix" @change="query.ftsPrefix = $event.target.checked" />\n Prefix\n </label>\n </if>\n\n </div>\n </if>\n\n \x3c!-- ── Vector panel ── --\x3e\n <if :="searchMode === \'vector\'">\n <if :="vectorEngines.length === 0">\n <div style="font-size:12px; opacity:.7; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <ok-icon name="cpu" style="width:13px;height:13px;opacity:.5;"></ok-icon>\n <span>No vector search engines for <b>{{ type }}</b> yet.</span>\n <button\n @click="openEnginesCreatePipeline()"\n style="font-size:11px; padding:2px 10px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-indigo) 33%, transparent); color:var(--okdb-indigo); background:color-mix(in srgb, var(--okdb-indigo) 6%, transparent); cursor:pointer;"\n >⛓ Create Pipeline…</button>\n </div>\n </if>\n <if :="vectorEngines.length > 0">\n <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">\n\n <select @change="selectVectorEngine($event.target.value, $event)" style="min-width:160px;">\n <option value="">— none —</option>\n <each :of="vectorEngines" let:item="ve">\n <option :value="displayVectorName(ve.name)" :selected="query.selectedVectorEngine === displayVectorName(ve.name)">\n {{ displayVectorName(ve.name) }}{{ ve.isRunning ? \'\' : \' (stopped)\' }}\n </option>\n </each>\n <option value="__create_pipeline__" style="color:var(--okdb-indigo);">+ Create pipeline…</option>\n </select>\n\n <if :="query.selectedVectorEngine">\n <input\n type="text"\n placeholder="Semantic query…"\n :value="query.vectorText"\n @input:debounce.500="query.vectorText = $event.target.value"\n style="flex:1; min-width:160px; max-width:340px;"\n />\n <span style="font-size:12px; opacity:.55; white-space:nowrap; user-select:none;">top</span>\n <input\n type="number"\n :value="query.vectorLimit"\n @change="query.vectorLimit = Math.max(1, Math.min(200, Number($event.target.value) || 20))"\n min="1" max="200"\n style="width:56px; text-align:center;"\n title="Max results"\n />\n </if>\n\n \x3c!-- Pipeline badge — clickable, navigates to local Engines tab in pipeline mode --\x3e\n <if :="selectedVectorEngineMeta">\n <if :="selectedVectorEngineMeta.meta && selectedVectorEngineMeta.meta.pipeline">\n <button\n @click="openEnginesPipeline(selectedVectorEngineMeta.meta.pipeline)"\n style="font-size:11px; padding:2px 9px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-indigo) 33%, transparent); color:var(--okdb-indigo); background:color-mix(in srgb, var(--okdb-indigo) 6%, transparent); cursor:pointer; white-space:nowrap;"\n :title="\'Open the type pipeline view focused on: \' + selectedVectorEngineMeta.meta.pipeline"\n >⛓ {{ selectedVectorEngineMeta.meta.pipeline }} →</button>\n </if>\n </if>\n\n \x3c!-- Link to local type engines workspace --\x3e\n <button\n @click="openTypePipelinesForSelectedVector()"\n style="font-size:11px; padding:2px 9px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-indigo) 20%, transparent); color:color-mix(in srgb, var(--okdb-indigo) 60%, transparent); background:transparent; cursor:pointer; white-space:nowrap; margin-left:auto;"\n title="Open the type-scoped engines view for this vector search"\n >⛓ Type Engines →</button>\n\n </div>\n </if>\n </if>\n\n \x3c!-- ── Geo panel ── --\x3e\n <if :="searchMode === \'geo\'">\n <if :="geoIndexes.length === 0">\n <div style="font-size:12px; opacity:.7; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <span>◎</span>\n <span>No geo indexes for <b>{{ type }}</b> yet. Create one from the Manage tab.</span>\n </div>\n </if>\n <if :="geoIndexes.length > 0">\n <div style="display:flex; flex-direction:column; gap:8px;">\n <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">\n <select @change="geoField = $event.target.value" style="min-width:140px;">\n <each :of="geoIndexes" let:item="gf">\n <option :value="gf" :selected="geoField === gf">{{ gf }}</option>\n </each>\n </select>\n <span style="font-size:12px; opacity:.5;">Lat</span>\n <input\n type="number" step="any" placeholder="40.7484"\n :value="geoLat"\n @input:debounce.100="geoLat = $event.target.value"\n style="width:110px;"\n />\n <span style="font-size:12px; opacity:.5;">Lon</span>\n <input\n type="number" step="any" placeholder="-73.9856"\n :value="geoLon"\n @input:debounce.100="geoLon = $event.target.value"\n style="width:110px;"\n />\n <span style="font-size:12px; opacity:.5;">Radius (m)</span>\n <input\n type="number" step="any" placeholder="∞"\n :value="geoRadius"\n @input:debounce.100="geoRadius = $event.target.value"\n style="width:90px;"\n title="Leave empty for unlimited radius (sorted by distance)"\n />\n <button\n @click="geoSortAsc = !geoSortAsc"\n style="min-width:78px;"\n title="Toggle sort direction">\n {{ geoSortAsc ? \'↑ Nearest\' : \'↓ Farthest\' }}\n </button>\n </div>\n <div style="font-size:11px; opacity:.35; user-select:none;">\n Results sorted by distance · sift filter applied as post-filter · leave radius empty for no cutoff\n </div>\n </div>\n </if>\n </if>\n\n </div>\n\n </div>\n\n \x3c!-- ── Geo view toggle (only in geo mode) ── --\x3e\n <if :="searchMode === \'geo\' && geoIndexes.length > 0">\n <div style="display:flex; align-items:center; gap:4px; flex-shrink:0; padding:2px 0;">\n <div class="seg-toggle" style="flex-shrink:0;">\n <button :class="geoViewMode === \'table\' ? \'active\' : \'\'" @click="geoViewMode = \'table\'" title="Table only">\n <ok-icon name="table-2" style="width:13px;height:13px;vertical-align:middle;"></ok-icon>\n </button>\n <button :class="geoViewMode === \'map\' ? \'active\' : \'\'" @click="geoViewMode = \'map\'" title="Map only">\n <ok-icon name="map-pin" style="width:13px;height:13px;vertical-align:middle;"></ok-icon>\n </button>\n <button :class="geoViewMode === \'split\' ? \'active\' : \'\'" @click="geoViewMode = \'split\'" title="Table + Map">\n <ok-icon name="columns" style="width:13px;height:13px;vertical-align:middle;"></ok-icon>\n </button>\n </div>\n <span style="font-size:10px; opacity:.3; user-select:none;">view</span>\n </div>\n </if>\n\n \x3c!-- ── Results area ── --\x3e\n <div style="display:flex; flex-direction:column; position:relative; flex:1; min-height:0;">\n <if :="query.busy">\n <ok-overlay>\n <div class="loader-center">\n <ok-icon name="loader" class="spin"></ok-icon>\n </div>\n </ok-overlay>\n </if>\n\n \x3c!-- Results bar: timing + Add item --\x3e\n <div style="margin-bottom:8px; display:flex; align-items:center; gap:8px; flex-shrink:0;">\n <span style="font-size:12px; opacity:.45;">\n {{ query.time != null ? query.time + \'ms\' : \'\' }}\n <b style="opacity:.7;">{{ type }}</b>\n </span>\n <div style="margin-left:auto;">\n <button @click="openAddModal" class="btn-primary btn-icon" style="font-size:12px; padding:3px 12px;"\n :disabled="!$ok.stores.auth.can(\'data:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'data:write\') }"\n :title="$ok.stores.auth.permTooltip(\'data:write\')">\n <ok-icon name="plus"></ok-icon>Add item\n </button>\n </div>\n </div>\n\n \x3c!-- Content row — table + map as siblings, shown/hidden via display:none --\x3e\n <div style="display:flex; flex:1; min-height:0; gap:10px;">\n\n \x3c!-- TABLE col — hidden only in pure map mode --\x3e\n <div\n :style="searchMode === \'geo\' && geoViewMode === \'map\' ? \'display:none\' : \'flex:1; min-width:0; display:flex; flex-direction:column; min-height:0;\'"\n >\n <if :="hasDirectValueRows">\n <ok-table\n ref="table"\n :data="displayRows"\n select="single"\n no-scroll\n style="flex:1; min-height:0; overflow:auto;"\n @select-change="onGeoTableSelect($event)"\n >\n <columns>\n <column width="min-content" no-resize>\n <header>\n <span class="key__header">key</span>\n <if :="query.selectedFtsIndex">\n <span style="font-size:10px; color:var(--okdb-accent); opacity:.6; margin-left:6px; font-weight:normal;">score</span>\n </if>\n <if :="query.selectedVectorEngine">\n <span style="font-size:10px; color:var(--okdb-indigo); opacity:.6; margin-left:6px; font-weight:normal;">sim</span>\n </if>\n <if :="searchMode === \'geo\'">\n <span style="font-size:10px; color:var(--okdb-success); opacity:.6; margin-left:6px; font-weight:normal;">dist</span>\n </if>\n </header>\n <cell let:row>\n <span class="key__cell">{{ keyOf(row) }}</span>\n <if :="directValueKindOf(row)">\n <span :class="\'direct-value-badge direct-value-badge--\' + directValueKindOf(row)">\n {{ directValueKindOf(row) }}\n </span>\n </if>\n <if :="query.selectedFtsIndex">\n <span class="fts-score-badge">{{ matchOf(row) ?? \'—\' }}</span>\n </if>\n <if :="query.selectedVectorEngine">\n <span class="vec-score-badge">{{ vectorScoreOf(row) ?? \'—\' }}</span>\n <if :="vectorChunkMetaOf(row)">\n <span style="font-size:10px; color:var(--okdb-orange); opacity:.8; margin-left:4px; white-space:nowrap;">\n chunk {{ vectorChunkMetaOf(row).start }}–{{ vectorChunkMetaOf(row).end }}\n </span>\n </if>\n </if>\n <if :="searchMode === \'geo\' && distanceOf(row)">\n <span class="geo-dist-badge">{{ distanceOf(row) }}</span>\n </if>\n </cell>\n </column>\n\n <column>\n <header>value</header>\n <cell let:row>\n <code :title="prettyValueOf(row)" style="display:block; white-space:pre-wrap; word-break:break-word; font-size:12px;">{{ inlineValueOf(row) }}</code>\n </cell>\n </column>\n </columns>\n\n <expanded let:row>\n \x3c!-- Chunk info panel — shown only in vector mode for chunked pipeline results --\x3e\n <if :="query.selectedVectorEngine && vectorChunkMetaOf(row)">\n <div style="padding:8px 10px 0; display:flex; flex-direction:column; gap:6px;">\n <if :="vectorChunkMetaOf(row).text">\n <div style="font-size:11px; opacity:.5; margin-bottom:2px;">Matched chunk\n <span style="color:var(--okdb-orange); margin-left:6px;">chars {{ vectorChunkMetaOf(row).start }}–{{ vectorChunkMetaOf(row).end }}</span>\n </div>\n <div style="background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px; padding:8px; font-size:11px; max-height:140px; overflow:auto; white-space:pre-wrap; word-break:break-word; line-height:1.5;">{{ vectorChunkMetaOf(row).text }}</div>\n </if>\n\n <if :="!vectorChunkMetaOf(row).text && vectorChunkMetaOf(row).start != null">\n <div style="font-size:11px; opacity:.5; margin-bottom:2px;">Matched chunk\n <span style="color:var(--okdb-orange); margin-left:6px;">chars {{ vectorChunkMetaOf(row).start }}–{{ vectorChunkMetaOf(row).end }}</span>\n <span style="opacity:.55; margin-left:4px;">(highlighted in document below)</span>\n </div>\n </if>\n\n <div style="height:1px; background:var(--okdb-app-border); margin:2px 0;"></div>\n </div>\n </if>\n\n <edit-row\n :row="rowValueOf(row)"\n :key="keyOf(row)"\n :type="type"\n @remove="onRemoveRow(keyOf(row))"\n @change="onRowChanged($event)">\n </edit-row>\n </expanded>\n </ok-table>\n </if>\n\n <if :="!hasDirectValueRows">\n <ok-table\n ref="table"\n :data="displayRows"\n select="single"\n no-scroll\n style="flex:1; min-height:0; overflow:auto;"\n @select-change="onGeoTableSelect($event)"\n >\n <columns>\n <column width="min-content" no-resize>\n <header>\n <span class="key__header">key</span>\n <if :="query.selectedFtsIndex">\n <span style="font-size:10px; color:var(--okdb-accent); opacity:.6; margin-left:6px; font-weight:normal;">score</span>\n </if>\n <if :="query.selectedVectorEngine">\n <span style="font-size:10px; color:var(--okdb-indigo); opacity:.6; margin-left:6px; font-weight:normal;">sim</span>\n </if>\n <if :="searchMode === \'geo\'">\n <span style="font-size:10px; color:var(--okdb-success); opacity:.6; margin-left:6px; font-weight:normal;">dist</span>\n </if>\n </header>\n <cell let:row>\n <span class="key__cell">{{ keyOf(row) }}</span>\n <if :="query.selectedFtsIndex">\n <span class="fts-score-badge">{{ matchOf(row) ?? \'—\' }}</span>\n </if>\n <if :="query.selectedVectorEngine">\n <span class="vec-score-badge">{{ vectorScoreOf(row) ?? \'—\' }}</span>\n <if :="vectorChunkMetaOf(row)">\n <span style="font-size:10px; color:var(--okdb-orange); opacity:.8; margin-left:4px; white-space:nowrap;">\n chunk {{ vectorChunkMetaOf(row).start }}–{{ vectorChunkMetaOf(row).end }}\n </span>\n </if>\n </if>\n <if :="searchMode === \'geo\' && distanceOf(row)">\n <span class="geo-dist-badge">{{ distanceOf(row) }}</span>\n </if>\n </cell>\n </column>\n\n <auto max="10" :show-more-content="(n) => \'+\' + n + \' cols\'" show-more-title="Click to show all columns">\n <cell let:row let:value>\n <span :class="[typeof value, value === null ? \'null\' : \'\']">\n {{ value }}\n </span>\n </cell>\n </auto>\n </columns>\n\n <expanded let:row>\n \x3c!-- Chunk info panel — shown only in vector mode for chunked pipeline results --\x3e\n <if :="query.selectedVectorEngine && vectorChunkMetaOf(row)">\n <div style="padding:8px 10px 0; display:flex; flex-direction:column; gap:6px;">\n <if :="vectorChunkMetaOf(row).text">\n <div style="font-size:11px; opacity:.5; margin-bottom:2px;">Matched chunk\n <span style="color:var(--okdb-orange); margin-left:6px;">chars {{ vectorChunkMetaOf(row).start }}–{{ vectorChunkMetaOf(row).end }}</span>\n </div>\n <div style="background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px; padding:8px; font-size:11px; max-height:140px; overflow:auto; white-space:pre-wrap; word-break:break-word; line-height:1.5;">{{ vectorChunkMetaOf(row).text }}</div>\n </if>\n\n <if :="!vectorChunkMetaOf(row).text && vectorChunkMetaOf(row).start != null">\n <div style="font-size:11px; opacity:.5; margin-bottom:2px;">Matched chunk\n <span style="color:var(--okdb-orange); margin-left:6px;">chars {{ vectorChunkMetaOf(row).start }}–{{ vectorChunkMetaOf(row).end }}</span>\n <span style="opacity:.55; margin-left:4px;">(highlighted in document below)</span>\n </div>\n </if>\n\n <div style="height:1px; background:var(--okdb-app-border); margin:2px 0;"></div>\n </div>\n </if>\n\n <edit-row\n :row="rowValueOf(row)"\n :key="keyOf(row)"\n :type="type"\n @remove="onRemoveRow(keyOf(row))"\n @change="onRowChanged($event)">\n </edit-row>\n </expanded>\n </ok-table>\n </if>\n\n <div class="ok-pager">\n <button @click="query.prevPage()" :disabled="!query.hasPrev || query.busy">‹ Prev</button>\n <span>{{ query.total }} result{{ query.total === 1 ? \'\' : \'s\' }} · {{ query.pageSize }}/page</span>\n <button @click="query.nextPage()" :disabled="!query.hasNext || query.busy">Next ›</button>\n </div>\n </div>\n\n <if :="searchMode === \'geo\'">\n \x3c!-- MAP col — always mounted, shown only in geo map/split mode --\x3e\n <div\n style="flex:1; min-width:0; min-height:0; border-radius:8px; overflow:hidden; border:1px solid #23292e;"\n :style="{ \'display\': (geoViewMode === \'map\' || geoViewMode === \'split\')? \'block\' : \'none\' }"\n >\n <geo-map\n :points="geoMapPoints"\n :origin-lat="geoLat"\n :origin-lon="geoLon"\n :radius="geoRadius"\n :highlight-key="geoHighlightedKey"\n @move="geoLat = String($event.detail.lat.toFixed(6)), geoLon = String($event.detail.lon.toFixed(6))"\n @select="onGeoMapSelect($event)"\n style="width:100%; height:100%;"\n />\n </div>\n </if>\n\n </div>\n </div>\n\n\n</div>\n </if>\n\n \x3c!-- Changelog tab --\x3e\n <if :="tab === \'changelog\'">\n <type-changelog :type="type" style="flex:1; min-height:0;" />\n </if>\n\n \x3c!-- Manage tab --\x3e\n <if :="tab === \'manage\'">\n <div style="display:flex; flex-direction:column; gap:12px; flex:1; overflow:auto; padding-right:4px;">\n\n \x3c!-- Overview --\x3e\n <div class="card" style="display:flex; gap:24px; align-items:center;">\n <div>\n <div style="font-size:15px; font-weight:600;">{{ type }}</div>\n <div style="font-size:12px; opacity:.5; margin-top:2px;">type</div>\n </div>\n <div>\n <div style="font-size:22px; font-weight:600; color:var(--okdb-accent);">{{ typeCount }}</div>\n <div style="font-size:12px; opacity:.5; margin-top:2px;">records</div>\n </div>\n </div>\n\n \x3c!-- Fields --\x3e\n <div class="card">\n <div class="manage-section-title">Fields</div>\n <if :="typeFieldEntries.length === 0">\n <div style="opacity:.4; font-size:13px;">No fields tracked yet — add items or use Recompute.</div>\n </if>\n <if :="typeFieldEntries.length > 0">\n <div class="manage-grid manage-grid--2">\n <div class="manage-col-hd">Field</div>\n <div class="manage-col-hd">Observed types</div>\n <each :of="typeFieldEntries" let:item="tf">\n <div class="manage-cell"><code>{{ tf.name }}</code></div>\n <div class="manage-cell manage-cell--dim">{{ tf.types.join(\', \') }}</div>\n </each>\n </div>\n </if>\n </div>\n\n \x3c!-- Secondary Indexes --\x3e\n <div class="card">\n <div class="manage-section-header">\n <div class="manage-section-title">Secondary Indexes</div>\n <button @click="openCreateIndex()" class="btn-icon" style="font-size:11px; padding:3px 10px;"\n :disabled="!$ok.stores.auth.can(\'schema:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'schema:write\') }"\n :title="$ok.stores.auth.permTooltip(\'schema:write\')">\n <ok-icon name="plus"></ok-icon>Create index\n </button>\n </div>\n <if :="indexEntries.length === 0">\n <div style="opacity:.4; font-size:13px;">No indexes yet.</div>\n </if>\n <if :="indexEntries.length > 0">\n <div class="manage-grid manage-grid--idx">\n <div class="manage-col-hd">Fields</div>\n <div class="manage-col-hd">Kind</div>\n <div class="manage-col-hd">Ownership</div>\n <div class="manage-col-hd">Status</div>\n <div class="manage-col-hd"></div>\n <div class="manage-col-hd"></div>\n <each :of="indexEntries" let:item="ix">\n <div class="manage-cell" style="gap:4px; flex-wrap:wrap;">\n <each :of="ix.fields" let:item="f" let:index="fi">\n <code style="font-size:12px; color:var(--okdb-json-key);">{{ f }}</code><span style="opacity:.3; font-size:11px; margin-left:0;" :style="fi < ix.fields.length - 1 ? \'\' : \'display:none\'">·</span>\n </each>\n </div>\n <div class="manage-cell" style="gap:6px;">\n <if :="ix.isGeo">\n <span class="idx-kind-badge idx-kind-badge--geo">◎ geo</span>\n <span style="font-size:10px; opacity:.45; white-space:nowrap;">precision {{ ix.geoPrecision ?? 7 }}</span>\n </if>\n <if :="ix.isUnique && !ix.isGeo">\n <span class="idx-kind-badge idx-kind-badge--unique">◆ unique</span>\n </if>\n <if :="!ix.isGeo && !ix.isUnique">\n <span class="idx-kind-badge idx-kind-badge--regular">regular</span>\n </if>\n </div>\n <div class="manage-cell" style="gap:3px; flex-direction:column; align-items:flex-start;">\n <if :="ix.createdBy">\n <span class="idx-owned-badge">{{ ix.createdBy.kind }}:{{ ix.createdBy.name }}</span>\n </if>\n <each :of="ix.usedBy" let:item="consumer">\n <span class="idx-consumer-badge">{{ consumer }}</span>\n </each>\n </div>\n <div class="manage-cell">\n <span :class="\'manage-status manage-status--\' + ix.status">{{ ix.status }}</span>\n </div>\n <div class="manage-cell" style="justify-content:flex-end;">\n <button @click="rebuildIndex(ix.name)" class="btn-icon" style="font-size:11px; padding:2px 10px; opacity:.7;" title="Rebuild this index from all current records"\n :disabled="!$ok.stores.auth.can(\'schema:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'schema:write\') }"\n :title="$ok.stores.auth.can(\'schema:write\') ? \'Rebuild this index from all current records\' : $ok.stores.auth.permTooltip(\'schema:write\')">\n <ok-icon name="refresh-cw"></ok-icon>Rebuild\n </button>\n </div>\n <div class="manage-cell" style="justify-content:flex-end;">\n <button @click="removeIndex(ix.name)" class="btn-icon" style="font-size:11px; padding:2px 10px; color:var(--okdb-danger); opacity:.8;"\n :disabled="!$ok.stores.auth.can(\'schema:write\') || ix.usedBy.length > 0"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'schema:write\') }"\n :title="ix.usedBy.length > 0 ? \'Used by: \' + ix.usedBy.join(\', \') : ($ok.stores.auth.can(\'schema:write\') ? \'Remove this index\' : $ok.stores.auth.permTooltip(\'schema:write\'))">\n <ok-icon name="trash-2"></ok-icon>Remove\n </button>\n </div>\n </each>\n </div>\n </if>\n </div>\n\n \x3c!-- FTS Indexes --\x3e\n <div class="card">\n <div class="manage-section-header">\n <div class="manage-section-title">FTS Indexes</div>\n <button @click="openCreateFtsModal()" class="btn-icon" style="font-size:11px; padding:3px 10px;"\n :disabled="!$ok.stores.auth.can(\'schema:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'schema:write\') }"\n :title="$ok.stores.auth.permTooltip(\'schema:write\')">\n <ok-icon name="plus"></ok-icon>Create FTS index\n </button>\n </div>\n <if :="ftsEntries.length === 0">\n <div style="opacity:.4; font-size:13px;">No FTS indexes yet.</div>\n </if>\n <if :="ftsEntries.length > 0">\n <div class="manage-grid manage-grid--fts">\n <div class="manage-col-hd">Name</div>\n <div class="manage-col-hd">Fields</div>\n <div class="manage-col-hd">Status</div>\n <div class="manage-col-hd">Size</div>\n <div class="manage-col-hd"></div>\n <div class="manage-col-hd"></div>\n <each :of="ftsEntries" let:item="fe">\n <div class="manage-cell"><code>{{ fe.name }}</code></div>\n <div class="manage-cell manage-cell--dim">{{ fe.fields }}</div>\n <div class="manage-cell">\n <span :class="\'manage-status manage-status--\' + fe.status">{{ fe.status }}</span>\n </div>\n <div class="manage-cell manage-cell--dim" style="font-size:11px; font-family:monospace;">{{ fmtBytes(fe.sizeBytes) }}</div>\n <div class="manage-cell" style="justify-content:flex-end;">\n <button @click="rebuildFts(fe.name)" class="btn-icon" style="font-size:11px; padding:2px 10px; opacity:.7;" title="Rebuild this FTS index from all current records"\n :disabled="!$ok.stores.auth.can(\'schema:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'schema:write\') }"\n :title="$ok.stores.auth.can(\'schema:write\') ? \'Rebuild this FTS index from all current records\' : $ok.stores.auth.permTooltip(\'schema:write\')">\n <ok-icon name="refresh-cw"></ok-icon>Rebuild\n </button>\n </div>\n <div class="manage-cell" style="justify-content:flex-end;">\n <button @click="dropFtsIndex(fe.name)" class="btn-icon" style="font-size:11px; padding:2px 10px; color:var(--okdb-danger); opacity:.8;" title="Drop this FTS index"\n :disabled="!$ok.stores.auth.can(\'schema:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'schema:write\') }"\n :title="$ok.stores.auth.can(\'schema:write\') ? \'Drop this FTS index\' : $ok.stores.auth.permTooltip(\'schema:write\')">\n <ok-icon name="trash-2"></ok-icon>Drop\n </button>\n </div>\n </each>\n </div>\n </if>\n </div>\n\n \x3c!-- Vector Search Engines --\x3e\n <div class="card">\n <div class="manage-section-header">\n <div class="manage-section-title">Vector Search Engines</div>\n <button\n @click="openEnginesWorkspace()"\n class="btn-icon"\n style="font-size:11px; padding:3px 10px; color:var(--okdb-indigo); border-color:color-mix(in srgb, var(--okdb-indigo) 27%, transparent);"\n title="Open the type-scoped engines view for this type"\n >\n ⛓ Type Engines →\n </button>\n </div>\n <if :="vectorEntries.length === 0">\n <div style="opacity:.4; font-size:13px;">\n No vector search engines for this type yet.\n <button\n @click="openEnginesCreatePipeline()"\n style="margin-left:6px; font-size:11px; padding:1px 8px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-indigo) 27%, transparent); color:var(--okdb-indigo); background:color-mix(in srgb, var(--okdb-indigo) 5%, transparent); cursor:pointer;"\n >Create Pipeline…</button>\n </div>\n </if>\n <if :="vectorEntries.length > 0">\n <div class="manage-grid manage-grid--vec">\n <div class="manage-col-hd">Engine</div>\n <div class="manage-col-hd">Status</div>\n <div class="manage-col-hd">Pipeline</div>\n <div class="manage-col-hd"></div>\n <each :of="vectorEntries" let:item="ve">\n <div class="manage-cell"><code>{{ ve.displayName }}</code></div>\n <div class="manage-cell">\n <span :class="\'manage-status manage-status--\' + (ve.running ? \'ready\' : \'creating\')">{{ ve.status }}</span>\n </div>\n <div class="manage-cell manage-cell--dim">{{ ve.pipeline ?? \'—\' }}</div>\n <div class="manage-cell" style="justify-content:flex-end;">\n <button\n @click="openEnginesPipeline(ve.pipeline)"\n class="btn-icon"\n style="font-size:11px; padding:2px 10px; color:var(--okdb-indigo); opacity:.85;"\n title="Open this type-scoped pipeline view"\n >\n ⛓ View local →\n </button>\n </div>\n </each>\n </div>\n </if>\n </div>\n\n \x3c!-- TTL --\x3e\n <div class="card">\n <div class="manage-section-header">\n <div class="manage-section-title">Time-to-Live (TTL)</div>\n <button @click="(loadTTLStats(), loadDefaultTTL())" class="btn-icon" style="font-size:11px; padding:3px 10px; opacity:.6;" :disabled="ttlStatsLoading" title="Refresh TTL stats">\n ↻\n </button>\n </div>\n\n \x3c!-- Default TTL --\x3e\n <div style="margin-bottom:12px; padding:10px; background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px;">\n <div style="font-size:12px; font-weight:600; opacity:.7; margin-bottom:8px;">Default TTL for new items</div>\n <if :="defaultTTL">\n <div style="font-size:13px; color:var(--okdb-success); margin-bottom:8px;">\n Currently: <span style="font-family:monospace; font-weight:600;">{{ fmtDuration(defaultTTL) }}</span>\n <span style="opacity:.4; font-size:11px;"> ({{ defaultTTL }}ms)</span>\n </div>\n </if>\n <if :="!defaultTTL">\n <div style="font-size:12px; opacity:.4; margin-bottom:8px;">No default — items don\'t expire unless TTL is set explicitly.</div>\n </if>\n <div style="display:flex; gap:6px; align-items:center; flex-wrap:wrap;">\n <select\n @change="onDefaultTTLPresetChange($event.target.value)"\n style="font-size:11px; padding:2px 6px; background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:4px; color:var(--okdb-app-fg);"\n >\n <option value="">Custom…</option>\n <option value="60000">1 min</option>\n <option value="300000">5 min</option>\n <option value="900000">15 min</option>\n <option value="3600000">1 hr</option>\n <option value="21600000">6 hr</option>\n <option value="86400000">24 hr</option>\n <option value="604800000">7 days</option>\n <option value="2592000000">30 days</option>\n </select>\n <input\n type="number"\n !value="defaultTTLInput"\n @input="(defaultTTLInput = $el.value, defaultTTLPreset = \'\')"\n placeholder="ms"\n style="width:110px; font-size:11px; padding:2px 6px; background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:4px; color:var(--okdb-app-fg);"\n />\n <button @click="setDefaultTTLAction()" :disabled="defaultTTLBusy || !defaultTTLInput || !$ok.stores.auth.can(\'data:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'data:write\') }"\n :title="$ok.stores.auth.permTooltip(\'data:write\')"\n style="font-size:11px; padding:2px 10px; color:var(--okdb-success); border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);">Set</button>\n <if :="defaultTTL">\n <button @click="clearDefaultTTLAction()" :disabled="defaultTTLBusy || !$ok.stores.auth.can(\'data:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'data:write\') }"\n :title="$ok.stores.auth.permTooltip(\'data:write\')"\n style="font-size:11px; padding:2px 10px; color:var(--okdb-danger); border-color:color-mix(in srgb, var(--okdb-danger) 33%, transparent);">Clear</button>\n </if>\n </div>\n </div>\n\n \x3c!-- TTL stats --\x3e\n <if :="!ttlStats || !ttlStats.enabled">\n <div style="opacity:.4; font-size:13px;">No TTL entries for this type — TTL sub-databases are created lazily on first use.</div>\n </if>\n <if :="ttlStats && ttlStats.enabled">\n <div style="display:flex; gap:24px; align-items:center; margin-bottom:10px;">\n <div>\n <div style="font-size:18px; font-weight:600; color:var(--okdb-success);">{{ ttlStats.totalEntries }}</div>\n <div style="font-size:11px; opacity:.5;">entries</div>\n </div>\n <div>\n <div :style="\'font-size:18px; font-weight:600; color:\' + (ttlStats.expiredEntries > 0 ? \'var(--okdb-danger)\' : \'var(--okdb-app-fg-muted)\') + \';\'">{{ ttlStats.expiredEntries }}</div>\n <div style="font-size:11px; opacity:.5;">expired</div>\n </div>\n <div>\n <div style="font-size:13px; font-family:monospace; color:var(--okdb-app-fg);">{{ ttlStats.nextExpiry ? fmtDuration(ttlStats.nextExpiry - Date.now()) : \'—\' }}</div>\n <div style="font-size:11px; opacity:.5;">next expiry</div>\n </div>\n </div>\n <div style="display:flex; gap:8px; align-items:center;">\n <button @click="triggerSweep()" :disabled="ttlSweepBusy || !$ok.stores.auth.can(\'data:write\')" class="btn-icon" style="font-size:11px; padding:3px 10px;"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'data:write\') }"\n :title="$ok.stores.auth.permTooltip(\'data:write\')">\n <ok-icon name="refresh-cw"></ok-icon>Sweep expired now\n </button>\n <if :="ttlSweepBusy"><span style="font-size:11px; opacity:.5;">Sweeping…</span></if>\n </div>\n </if>\n </div>\n\n \x3c!-- Danger zone --\x3e\n <div class="card card--danger"> <div class="manage-section-title" style="color:var(--okdb-danger);">⚠ Danger Zone</div>\n <div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">\n <button @click="recomputeFields" class="btn-icon"\n :disabled="!$ok.stores.auth.can(\'data:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'data:write\') }"\n :title="$ok.stores.auth.can(\'data:write\') ? \'Rescan all records to rebuild the field schema\' : $ok.stores.auth.permTooltip(\'data:write\')">\n <ok-icon name="refresh-cw"></ok-icon>Recompute fields\n </button>\n <button @click="dropType" class="btn-danger btn-icon"\n :disabled="!$ok.stores.auth.can(\'data:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'data:write\') }"\n :title="$ok.stores.auth.permTooltip(\'data:write\')">\n <ok-icon name="trash-2"></ok-icon>Drop type\n </button>\n </div>\n </div>\n\n </div>\n </if>\n\n \x3c!-- Schema tab --\x3e\n <if :="tab === \'schema\'">\n <type-schema-tab\n :type="type"\n :on-view-key="(key) => setTab(\'data\')"\n :on-schema-changed="() => loadViolations()"\n :on-violation-count-changed="(count) => onViolationCountChanged(count)"\n style="flex:1; min-height:0;"\n />\n </if>\n\n <if :="tab === \'engines\'">\n <type-pipelines-tab :type="type" :focusedpipeline="currentRoutePipeline" :currentmode="currentRouteMode" style="flex:1; min-height:0;" />\n </if>\n\n</div>',style:"[tag]{display:flex;flex-direction:column;height:100%;min-width:0;min-height:0}.ok-table__cell__inner>span.object{color:#a6da95}.ok-table__cell__inner>span.number{color:var(--okdb-accent)}.ok-table__cell__inner>span.boolean{color:#ff6c6b}.ok-table__cell__inner>span.null,.ok-table__cell__inner>span.undefined{color:#c792ea}.ok-table__cell__inner>.key__header{font-weight:700;color:var(--okdb-json-key)}.ok-table__cell__inner>.key__cell{color:var(--okdb-json-key)}.fts-score-badge,.mode-count{display:inline-block;border-radius:999px;font-size:10px;vertical-align:middle}.mode-count{margin-left:4px;padding:0 5px;background:#ffffff18;line-height:1.6}.fts-score-badge{margin-left:5px;padding:1px 5px;border:1px solid color-mix(in srgb,var(--okdb-accent) 33%,transparent);color:var(--okdb-accent);opacity:.8}.vec-score-badge{border:1px solid color-mix(in srgb,var(--okdb-indigo) 33%,transparent);color:var(--okdb-indigo)}.direct-value-badge,.geo-dist-badge,.vec-score-badge{display:inline-block;margin-left:5px;padding:1px 5px;border-radius:999px;font-size:10px;vertical-align:middle;opacity:.8}.geo-dist-badge{border:1px solid color-mix(in srgb,var(--okdb-success) 33%,transparent);color:var(--okdb-success)}.direct-value-badge{border:1px solid}.direct-value-badge--scalar{border-color:color-mix(in srgb,var(--okdb-app-fg-muted) 33%,transparent);color:var(--okdb-app-fg-muted)}.direct-value-badge--array{border-color:#2dd4bf55;color:#2dd4bf}.direct-value-badge--null{border-color:color-mix(in srgb,var(--okdb-purple) 33%,transparent);color:var(--okdb-purple)}.manage-section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.manage-section-title{font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.06em;opacity:.5;margin-bottom:0}.manage-grid{display:grid;gap:0}.manage-grid--2{grid-template-columns:1fr 1fr}.manage-grid--3{grid-template-columns:1fr auto auto}.manage-grid--4{grid-template-columns:1fr auto auto auto}.manage-grid--idx{grid-template-columns:minmax(80px,2fr) minmax(90px,1fr) minmax(80px,1fr) auto auto auto}.idx-consumer-badge,.idx-owned-badge{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;white-space:nowrap}.idx-owned-badge{background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);border:1px solid color-mix(in srgb,var(--okdb-accent) 25%,transparent)}.idx-consumer-badge{background:color-mix(in srgb,var(--okdb-sky) 12%,transparent);color:var(--okdb-sky);border:1px solid color-mix(in srgb,var(--okdb-sky) 25%,transparent)}.manage-grid--fts{grid-template-columns:minmax(80px,1fr) 2fr minmax(60px,auto) minmax(50px,auto) auto auto}.manage-grid--vec{grid-template-columns:minmax(80px,1fr) minmax(60px,auto) minmax(80px,1fr) auto}.idx-kind-badge{display:inline-block;padding:1px 7px;border-radius:999px;font-size:10px;border:1px solid;white-space:nowrap}.idx-kind-badge--geo{color:var(--okdb-success);border-color:color-mix(in srgb,var(--okdb-success) 33%,transparent)}.idx-kind-badge--unique{color:var(--okdb-warning);border-color:color-mix(in srgb,var(--okdb-warning) 33%,transparent)}.idx-kind-badge--regular{color:var(--okdb-app-fg-muted);border-color:color-mix(in srgb,var(--okdb-app-fg-muted) 20%,transparent)}.idx-info-strip{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:5px 8px;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-surface-2);border-radius:4px;font-size:11px}.idx-info-label{opacity:.4;white-space:nowrap}.idx-info-field{font-size:11px;color:var(--okdb-json-key)}.idx-info-badge{padding:1px 7px;border-radius:999px;font-size:10px;border:1px solid;white-space:nowrap}.idx-info-badge--geo{color:var(--okdb-success);border-color:color-mix(in srgb,var(--okdb-success) 33%,transparent)}.idx-info-badge--unique{color:var(--okdb-warning);border-color:color-mix(in srgb,var(--okdb-warning) 33%,transparent)}.idx-info-badge--regular{color:var(--okdb-app-fg-muted);border-color:color-mix(in srgb,var(--okdb-app-fg-muted) 20%,transparent)}.manage-col-hd{font-size:11px;opacity:.4;padding:0 12px 6px 0;border-bottom:1px solid var(--okdb-app-border);font-weight:400}.manage-cell{padding:6px 12px 6px 0;border-bottom:1px solid var(--okdb-app-surface);font-size:13px;display:flex;align-items:center;min-width:0}.manage-cell--dim{opacity:.5;font-size:12px}.manage-status{display:inline-block;padding:1px 7px;border-radius:999px;font-size:10px;border:1px solid}.manage-status--ready{color:var(--okdb-success);border-color:color-mix(in srgb,var(--okdb-success) 33%,transparent)}.manage-status--creating,.manage-status--resetting{color:var(--okdb-warning);border-color:color-mix(in srgb,var(--okdb-warning) 33%,transparent)}.card--danger{border:1px solid color-mix(in srgb,var(--okdb-danger) 13%,transparent)!important}"};
|
|
1
|
+
import e from"@toolkit/elements/ok-icon.ok.js";import n from"@toolkit/elements/ok-table.ok.js";import t from"@toolkit/core/ok-overlay.ok.js";import i from"../../../components/geo-map.ok.js";import o from"../../../components/ok-json-input.ok.js";import s from"../../../view-models/type-query-vm.js";import{buildSiftSchema as a}from"../../../util/sift-schema.js";import r from"../modals/add-item-modal.ok.js";import l from"../modals/create-fts-modal.ok.js";import d from"../modals/create-index-modal.ok.js";import c from"../modals/create-pipeline-modal.ok.js";import p from"./edit-row.ok.js";import u from"../../../stores/events-store.js";import h from"./type-changelog.ok.js";import g from"./type-pipelines-tab.ok.js";import f from"./type-schema-tab.ok.js";function y(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function x(e,n){y(e)&&e.__key!==n&&Object.defineProperty(e,"__key",{value:n,enumerable:!1,configurable:!1,writable:!1})}function v(e,n=0){try{return JSON.stringify(e,null,n)??String(e)}catch{return String(e)}}export default{tag:"type-viewer",dependencies:[e,n,t,p,o,h,g,f,r,d,l,c,i],attr:{type:{}},context(e){let n=null,t=null,i=null;const o=new s({type:e.$attr.type,apiBase:()=>e.$ok.stores.okdb.apiBase});return{tab:"data",get envApiBase(){return e.$ok.stores.okdb.apiBase},get type(){return e.$attr.type},violationCount:0,async loadViolations(){if(this.type)try{const[e,n]=await Promise.all([fetch(`${this.envApiBase}/violations/${encodeURIComponent(this.type)}`),fetch(`${this.envApiBase}/ref-violations/${encodeURIComponent(this.type)}`)]);let t=0;if(e.ok){const n=await e.json();t+=(n?.data?.violations??n?.result?.violations??[]).length}if(n.ok){const e=await n.json();t+=(e?.data?.violations??e?.result?.violations??[]).length}this.violationCount=t}catch{}},onViolationCountChanged(e){this.violationCount=e},query:o,possibleIndexes:[],indexStatuses:{},createIdxPossible:[],createIdxTypeLoaded:null,ftsList:[],vectorEngines:[],typeEngineCount:0,searchMode:"scan",geoIndexes:[],geoField:"",geoLat:"",geoLon:"",geoRadius:"",geoSortAsc:!0,geoDistanceMap:{},mapOpen:!1,geoViewMode:"table",geoHighlightedKey:null,_geoSelSource:null,get geoMapPoints(){const e=this.geoField;return e?(this.query.items||[]).map(n=>{const t=n.value||{},i=((e,n)=>{let t=e;for(const e of n.split(".")){if(null==t)return null;t=t[e]}return t})(t,e);return{key:n.key,title:t.name||t.title||t.label||n.key,lat:i?.lat,lon:i?.lon,distance:n.distance}}).filter(e=>null!=e.lat&&null!=e.lon):[]},setTab(n){if(this.tab=n,"engines"===n)return;const t=e.$vars.$router;if(!t?._search?.tab)return;const i={...t._search};delete i.tab,delete i.mode,delete i.pipeline,t.replace(t.resolve(t._path||"",i,t._anchor||""))},switchToPipelines(){this.openTypePipelines({mode:"pipelines",pipeline:this.currentRoutePipeline||null})},get currentRoutePipeline(){return String(e.$vars?.$router?._search?.pipeline||"")},get currentRouteMode(){return"pipelines"===String(e.$vars?.$router?._search?.mode||"").trim()?"pipelines":"engines"},openTypePipelines(n={}){const t=e.$vars?.$router;if(!t?.resolve||!t?.push)return;const i=e.$ok.stores.okdb.selectedEnv||"default",o={...t._search||{},env:i,type:this.type,tab:"engines",mode:"pipelines"===n.mode?"pipelines":"engines"};n.pipeline?o.pipeline=n.pipeline:delete o.pipeline,t.push(t.resolve("/db",o))},openEnginesWorkspace(n={}){if("create"===n.view){const t=e.$vars?.$router;if(!t?.resolve||!t?.push)return;const i={env:e.$ok.stores.okdb.selectedEnv||"default",view:"create",sourceType:this.type};return n.kind&&(i.kind=n.kind),void t.push(t.resolve("/engines",i))}this.openTypePipelines({mode:n.mode||"engines",pipeline:n.pipeline||null})},openTypePipelinesForSelectedVector(){const e=this.selectedVectorEngineMeta?.meta?.pipeline||null;this.openTypePipelines({mode:"pipelines",pipeline:e})},openGlobalEnginesWorkspace(n={}){const t=e.$vars?.$router;if(!t?.resolve||!t?.push)return;const i={env:e.$ok.stores.okdb.selectedEnv||"default",sourceType:this.type};n.view&&"overview"!==n.view||(i.mode="pipelines"),n.view&&"overview"!==n.view&&(i.view=n.view),n.kind&&(i.kind=n.kind),n.pipeline&&(i.pipeline=n.pipeline),t.push(t.resolve("/engines",i))},async openEnginesCreatePipeline(){this.type&&await e.$ok.shared.modal.open({owner:e,title:`Create Pipeline · ${this.type}`,canDrag:!0,content:'<create-pipeline-modal :modal="modal" :type="type" :env="envName" :on-done="onDone" />',context:n=>({modal:n,type:this.type,envName:e.$ok.stores.okdb.selectedEnv||"default",onDone:async()=>{await Promise.all([this.loadVectorEngines(!0),this.loadTypeEnginesCount()])}})})},openEnginesPipeline(e=null){this.openTypePipelines({mode:"pipelines",pipeline:e||null})},switchToEngines(){this.openTypePipelines({mode:"engines",pipeline:this.currentRoutePipeline||null})},setSearchMode(n){if(this.searchMode=n,this.query.selectedIndex=null,this.query.selectedFtsIndex=null,this.query.selectedVectorEngine=null,this.query.selectedPrefix=void 0,this.query.selectedIndexStart=void 0,this.query.selectedIndexEnd=void 0,this.geoDistanceMap={},this.geoHighlightedKey=null,"geo"!==n){this.geoViewMode="table";try{e.$el?.querySelector("ok-table")?.clearSelection?.()}catch(e){}}},_updateGeoIndexes(){this.geoIndexes=this.possibleIndexes.filter(e=>this.indexStatuses[e]?.geoConfig),this.geoIndexes.length&&!this.geoField&&(this.geoField=this.geoIndexes[0])},async loadGeoQuery(){if(!this.type||!this.geoField)return;const e=parseFloat(this.geoLat),n=parseFloat(this.geoLon);if(isNaN(e)||isNaN(n))return;const t=this.geoRadius?parseFloat(this.geoRadius):void 0;this.query.busy=!0,this.geoDistanceMap={};try{const i={...this.query.query},o={lat:e,lon:n};t&&!isNaN(t)&&(o.radius=t),i[this.geoField]={$near:o};const s=await fetch(`${this.envApiBase}/type/${this.type}/query`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filter:i,options:{limit:this.query.pageSize}})});if(!s.ok)throw new Error(s.statusText);const{data:a,meta:r}=await s.json();let l=a??[];this.geoSortAsc||(l=l.slice().reverse()),this.query.items=l;const d={};this.query.values=l.map(e=>(x(e.value,e.key),null!=e.distance&&(d[e.key]=e.distance),e.value)),this.geoDistanceMap=d,this.query.hasNext=!1,this.query.hasPrev=!1,this.query._current=null,this.query._backStack=[],this.query.time=r?.time??null,this.query.total=l.length}catch(e){console.error("Geo query error",e)}finally{this.query.busy=!1}},get selectedVectorEngineMeta(){return this.query.selectedVectorEngine?this.vectorEngines.find(e=>this.displayVectorName(e.name)===this.query.selectedVectorEngine)??null:null},helpOpen:!1,get typeFields(){const n=(e.$ok.stores.okdb.types||[]).find(e=>e.type===this.type);return n?.fields||{}},get typeCount(){const n=(e.$ok.stores.okdb.types||[]).find(e=>e.type===this.type);return n?.count??"?"},get typeFieldEntries(){return Object.entries(this.typeFields).map(([e,n])=>({name:e,types:n}))},get indexEntries(){return this.possibleIndexes.map(e=>({name:e,fields:e.split("~"),status:this.indexStatuses[e]?.status||"ready",procState:this.indexStatuses[e]?.procState??null,progress:this.indexStatuses[e]?.progress??null,isGeo:!!this.indexStatuses[e]?.geoConfig,geoPrecision:this.indexStatuses[e]?.geoConfig?.precision??null,isUnique:!!this.indexStatuses[e]?.unique,createdBy:this.indexStatuses[e]?.createdBy??null,usedBy:this.indexStatuses[e]?.usedBy??[]}))},get selectedIndexInfo(){const e=this.query.selectedIndex;if(!e)return null;const n=this.indexStatuses[e];return{fields:e.split("~"),isGeo:!!n?.geoConfig,geoPrecision:n?.geoConfig?.precision??null,isUnique:!!n?.unique}},get ftsEntries(){return this.ftsList.map(e=>({name:e.name,status:e.status||"ready",fields:(e.config&&e.config.fields?e.config.fields:[]).join(", "),sizeBytes:e.sizeBytes??null}))},fmtBytes(e){if(null==e||0===e)return"—";const n=["B","KB","MB","GB"];let t=e,i=0;for(;t>=1024&&i<n.length-1;)t/=1024,i++;return t.toFixed(1)+" "+n[i]},ttlStats:null,ttlStatsLoading:!1,ttlSweepBusy:!1,defaultTTL:null,defaultTTLInput:"",defaultTTLPreset:"",defaultTTLBusy:!1,async loadTTLStats(){if(this.type){this.ttlStatsLoading=!0;try{const e=await fetch(`${this.envApiBase}/ttl/stats?type=${encodeURIComponent(this.type)}`),n=await e.json();this.ttlStats=n.data??n.result??null}catch{this.ttlStats=null}finally{this.ttlStatsLoading=!1}}},async loadDefaultTTL(){if(this.type)try{const e=await fetch(`${this.envApiBase}/type/${encodeURIComponent(this.type)}/ttl/default`),n=await e.json(),t=n.data?.defaultTTL??n.result?.defaultTTL??null;this.defaultTTL=t,this.defaultTTLInput=t?String(t):"",this.defaultTTLPreset=t?String(t):""}catch{this.defaultTTL=null}},async setDefaultTTLAction(){const n=parseInt(this.defaultTTLInput);if(n&&!(n<=0)){this.defaultTTLBusy=!0;try{const t=await fetch(`${this.envApiBase}/type/${encodeURIComponent(this.type)}/ttl/default`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({ttl:n})});if(!t.ok){const e=await t.json().catch(()=>({}));throw new Error(e?.error?.message||t.statusText)}this.defaultTTL=n,e.$ok.shared?.toast?.info?.("Default TTL set ("+this.fmtDuration(n)+")")}catch(n){e.$ok.shared?.toast?.error?.(n.message||"Failed to set default TTL")}finally{this.defaultTTLBusy=!1}}},async clearDefaultTTLAction(){this.defaultTTLBusy=!0;try{const n=await fetch(`${this.envApiBase}/type/${encodeURIComponent(this.type)}/ttl/default`,{method:"DELETE"});if(!n.ok)throw new Error(n.statusText);this.defaultTTL=null,this.defaultTTLInput="",this.defaultTTLPreset="",e.$ok.shared?.toast?.info?.("Default TTL cleared")}catch(n){e.$ok.shared?.toast?.error?.(n.message||"Failed to clear default TTL")}finally{this.defaultTTLBusy=!1}},onDefaultTTLPresetChange(e){this.defaultTTLPreset=e,e&&(this.defaultTTLInput=e)},async triggerSweep(){this.ttlSweepBusy=!0;try{const n=await fetch(`${this.envApiBase}/ttl/sweep`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({batchSize:1e3})}),t=await n.json(),i=`Sweep done: ${(t.data??t.result??{}).removed??0} expired item(s) removed`;e.$ok.shared?.toast?.info?.(i),await this.loadTTLStats(),await this.query.load()}catch(n){e.$ok.shared?.toast?.error?.(n.message||"Sweep failed")}finally{this.ttlSweepBusy=!1}},fmtDuration(e){if(null==e)return"—";const n=Math.floor(e/1e3);return n<60?n+"s":n<3600?Math.floor(n/60)+"m "+n%60+"s":n<86400?Math.floor(n/3600)+"h "+Math.floor(n%3600/60)+"m":Math.floor(n/86400)+"d "+Math.floor(n%86400/3600)+"h"},get vectorEntries(){return this.vectorEngines.map(e=>({name:e.name,displayName:this.displayVectorName(e.name),status:e.isRunning?"running":e.status||"stopped",running:e.isRunning,pipeline:e.meta&&e.meta.pipeline?e.meta.pipeline:null}))},get siftSchema(){return a(this.typeFields)},async loadIndexes(e=!1){if(this.type&&(e||n!==this.type))try{const e=await fetch(`${this.envApiBase}/type/${this.type}/indexes`),t=await e.json(),i=t.data??t.result??{};this.possibleIndexes=Object.keys(i),this.indexStatuses=i,this._updateGeoIndexes(),n=this.type}catch(e){console.error("Error fetching indexes",e)}},async loadFtsIndexes(e=!1){if(this.type&&(e||t!==this.type))try{const e=await fetch(`${this.envApiBase}/type/${this.type}/fts`),{data:n}=await e.json();this.ftsList=n||[],t=this.type}catch(e){console.error("Error fetching FTS indexes",e)}},async loadVectorEngines(n=!1){if(this.type&&(n||i!==this.type))try{const n=e.$ok.stores.okdb.selectedEnv||"default",[t,o]=await Promise.allSettled([fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/engines`).then(e=>e.json()),fetch(`${__ENV__.API}/embeddings/embedders`).then(e=>e.json())]),s="fulfilled"===t.status?t.value?.result??t.value?.data??[]:[],a=[..."fulfilled"===o.status?o.value?.data??[]:[],...s],r={};for(const e of a)"indexer"===e.type&&e.config&&(r[e.name]={source_type:e.config.source_type,source_env:e.config.source_env??"default"});this.vectorEngines=a.filter(e=>{if("vector-search"!==e.type)return!1;const t=e.config?.source_type,i=e.config?.source_env??"default";if(t===this.type&&i===n)return!0;const o=r[e.name];return o?.source_type===this.type&&(o?.source_env??"default")===n}),i=this.type}catch(e){this.vectorEngines=[]}},async loadTypeEnginesCount(){if(this.type)try{const n=e.$ok.stores.okdb.selectedEnv||"default",t=await fetch(`${__ENV__.API}/env/${encodeURIComponent(n)}/type/${encodeURIComponent(this.type)}/pipelines`),i=await t.json().catch(()=>({})),o=t.ok?i?.data??[]:[],s=new Set;for(const e of o.flatMap(e=>e?.engines??[])){const n=e?.key??(e?.type&&e?.name?`${e.type}@${e.name}`:e?.name??null);n&&s.add(n)}this.typeEngineCount=s.size}catch{this.typeEngineCount=0}else this.typeEngineCount=0},async recomputeFields(){if(this.type)try{await fetch(`${this.envApiBase}/type/${this.type}/recompute-fields`,{method:"POST"}),await e.$ok.stores.okdb.reloadTypes(),e.$ok.shared?.toast?.success?.("Fields recomputed")}catch(n){console.error("recomputeFields error",n),e.$ok.shared?.toast?.error?.("Recompute failed")}},indexStatusBadge(e){const n=this.indexStatuses?.[e]?.status;return n&&"ready"!==n?n:null},ftsStatusBadge(e){const n=e?.status;return n&&"ready"!==n?n:null},keyOf:e=>e?.__key??e?.key??null,get hasDirectValueRows(){return(this.query.items||[]).some(e=>!y(e?.value))},get displayRows(){return this.hasDirectValueRows?this.query.items:this.query.values},rowValueOf:e=>e&&"object"==typeof e&&"key"in e&&"value"in e?e.value:e,directValueKindOf(e){const n=this.rowValueOf(e);return null===n?"null":Array.isArray(n)?"array":y(n)?null:"scalar"},inlineValueOf(e){return v(this.rowValueOf(e))},prettyValueOf(e){return v(this.rowValueOf(e),2)},scoreOf(e){const n=this.keyOf(e);return n?this.query.ftsScoreMap?.[n]?.score??null:null},matchOf(e){const n=this.keyOf(e);if(!n)return null;const t=this.query.ftsScoreMap?.[n];if(!t||null==t.score)return null;const i=t.maxScore??2*t.numTerms;return`${t.score}/${i}`},vectorScoreOf(e){const n=this.keyOf(e);if(!n)return null;const t=this.query.vectorScoreMap?.[n];return null==t?null:`${Math.round(100*t)}%`},vectorChunkMetaOf(e){const n=this.keyOf(e);return n?this.query.vectorChunkMetaMap?.[n]??null:null},distanceOf(e){const n=this.keyOf(e);if(!n)return null;const t=this.geoDistanceMap?.[n];return null==t?null:t<1e3?`${Math.round(t)} m`:`${(t/1e3).toFixed(1)} km`},displayVectorName(n){const t=(e.$ok.stores.okdb.selectedEnv||"default")+":";return n.startsWith(t)?n.slice(t.length):n},selectVectorEngine(e,n){if("__create_pipeline__"===e)return n&&(n.target.value=this.query.selectedVectorEngine||""),void this.openCreatePipelineModal();this.query.selectedVectorEngine=e||null,this.query.selectedVectorEngine&&(this.query.selectedIndex=null,this.query.selectedFtsIndex=null,this.query.selectedPrefix=void 0,this.query.selectedIndexStart=void 0,this.query.selectedIndexEnd=void 0)},openCreatePipelineModal(){this.openEnginesCreatePipeline()},createMenuOpen:!1,onRowChanged(n){const t=n?.detail??null,i=t?.key??null;if(null!=i){const e=this.query.items.findIndex(e=>e?.key===i);if(e>=0){const n=t?.value;x(n,i),this.query.items.splice(e,1,{...this.query.items[e],value:n}),e<this.query.values.length&&this.query.values.splice(e,1,n)}}e.$ok.shared.toast.info("Item updated"),this.loadViolations()},onRemoveRow(e){const n=this.query.items.findIndex(n=>n?.key===e);n>=0&&(this.query.values.splice(n,1),this.query.items.splice(n,1))},async openAddModal(){await e.$ok.shared.modal.open({owner:e,title:"Add item",content:'<add-item-modal :modal="modal" :type="type" :on-done="onDone" />',context:e=>({modal:e,type:this.type,onDone:async({key:e,value:n})=>{x(n,e),this.query.values.unshift(n),this.query.items.unshift({key:e,value:n})}})})},async openCreateIndex(){let n={},t=[];try{const e=await fetch(`${this.envApiBase}/type/${this.type}/deep-properties`),i=await e.json();n=(i.data??i.result)?.paths??{},t=Object.keys(n).filter(e=>n[e].includes("geo"))}catch{const t=(e.$ok.stores.okdb.types||[]).find(e=>e.type===this.type);for(const e of Object.keys(t?.fields||{}))n[e]=t.fields[e]}await e.$ok.shared.modal.open({owner:e,title:"Create Index",content:'<create-index-modal :modal="modal" :type="type" :deep-paths="deepPaths" :geo-fields="geoFields" :on-done="onDone" />',context:e=>({modal:e,type:this.type,deepPaths:n,geoFields:t,onDone:async()=>{await this.loadIndexes(!0)}})})},async dropType(){if(this.type&&await e.$ok.shared.modal.confirm(`Drop type "${this.type}"? This cannot be undone.`))try{await fetch(`${this.envApiBase}/type/${this.type}`,{method:"DELETE"}),await e.$ok.stores.okdb.reloadTypes(),e.$el.dispatchEvent(new CustomEvent("dropped",{detail:this.type}))}catch(e){console.error("Drop type error",e)}},async removeIndex(n){const t=n||this.query.selectedIndex;if(t&&await e.$ok.shared.modal.confirm(`Remove index "${t}"?`)){try{const n=await fetch(`${this.envApiBase}/type/${this.type}/index/${t}`,{method:"DELETE"});if(!n.ok){const t=await n.json().catch(()=>({})),i=t?.error?.code;if("INDEX_HAS_CONSUMERS"===i){const n=(t?.error?.details?.usedBy??[]).map(e=>`${e.kind}:${e.name}`).join(", ");return void e.$ok.shared?.toast?.error?.(`Cannot remove: index is used by ${n||"a consumer"}`)}throw new Error(t?.error?.message??"Remove failed")}}catch(n){return console.error("Remove index error",n),void e.$ok.shared?.toast?.error?.(n.message)}this.query.selectedIndex===t&&(this.query.selectedIndex=null),this.query.resetPaging(),await this.loadIndexes(!0),await this.query.load()}},selectIndex(e){if("__create_index__"===e)return this.query.selectedIndex=null,void this.openCreateIndex();"__remove_index__"!==e?(this.query.selectedIndex=e||null,this.query.selectedIndex&&(this.query.selectedFtsIndex=null),this.query.selectedIndex||(this.query.selectedPrefix=void 0,this.query.selectedIndexStart=void 0,this.query.selectedIndexEnd=void 0)):this.removeIndex()},selectFtsIndex(e,n){if("__create_fts__"===e)return n&&(n.target.value=this.query.selectedFtsIndex||""),void this.openCreateFtsModal();"__drop_fts__"!==e?(this.query.selectedFtsIndex=e||null,this.query.selectedFtsIndex&&(this.query.selectedIndex=null,this.query.selectedPrefix=void 0,this.query.selectedIndexStart=void 0,this.query.selectedIndexEnd=void 0)):this.dropFtsIndex()},async openCreateFtsModal(){let n={};try{const e=await fetch(`${this.envApiBase}/type/${this.type}/deep-properties`),t=await e.json();n=(t.data??t.result)?.paths??{}}catch{const t=(e.$ok.stores.okdb.types||[]).find(e=>e.type===this.type);for(const e of Object.keys(t?.fields||{}))n[e]=t.fields[e]}await e.$ok.shared.modal.open({owner:e,title:"Create FTS Index",content:'<create-fts-modal :modal="modal" :type="type" :api-base="apiBase" :deep-paths="deepPaths" :on-done="onDone" />',context:e=>({modal:e,type:this.type,apiBase:this.envApiBase,deepPaths:n,onDone:async()=>{await this.loadFtsIndexes(!0)}})})},async dropFtsIndex(n){const t=n||this.query.selectedFtsIndex;if(t&&await e.$ok.shared.modal.confirm(`Drop FTS index "${t}"? This cannot be undone.`)){try{await fetch(`${this.envApiBase}/type/${this.type}/fts/${encodeURIComponent(t)}`,{method:"DELETE"})}catch(n){return console.error("Drop FTS error",n),void e.$ok.shared?.toast?.error?.("Drop FTS failed")}this.query.selectedFtsIndex===t&&(this.query.selectedFtsIndex=null),await this.loadFtsIndexes(!0),await this.query.load()}},async rebuildIndex(n){if(n&&await e.$ok.shared.modal.confirm(`Rebuild index "${n}"? This will re-scan all records.`))try{await fetch(`${this.envApiBase}/type/${this.type}/index/${encodeURIComponent(n)}/reset`,{method:"POST"}),e.$ok.shared?.toast?.info?.(`Rebuilding index "${n}"…`),await this.loadIndexes(!0)}catch(n){console.error("Rebuild index error",n),e.$ok.shared?.toast?.error?.("Rebuild failed")}},async rebuildFts(n){const t=n||this.query.selectedFtsIndex;if(t&&await e.$ok.shared.modal.confirm(`Rebuild FTS index "${t}"? This will re-index all records.`))try{await fetch(`${this.envApiBase}/type/${this.type}/fts/${encodeURIComponent(t)}/reset`,{method:"POST"}),e.$ok.shared?.toast?.info?.(`Rebuilding FTS index "${t}"…`),await this.loadFtsIndexes(!0)}catch(n){console.error("Rebuild FTS error",n),e.$ok.shared?.toast?.error?.("Rebuild failed")}},onGeoTableSelect(e){if("geo"!==this.searchMode)return;this._geoSelSource="table";const n=e.detail?.action;this.geoHighlightedKey="deselect"===n?null:e.detail?.row?.__key??null},onGeoMapSelect(e){const n=e.detail?.key??null;this._geoSelSource="map",this.geoHighlightedKey=this.geoHighlightedKey===n?null:n}}},_unsubs:[],async init(e){if(e.$watch(()=>this.type,async()=>{this.query.type=this.type,this.query.selectedFtsIndex=null,this.query.selectedVectorEngine=null,this.searchMode="scan",this.query.resetPaging(),await this.loadIndexes(!0),await this.loadFtsIndexes(!0),await this.loadVectorEngines(!0),await this.loadTypeEnginesCount(),await this.loadViolations(),await this.query.load()}),e.$watch(()=>[this.query.query,this.query.selectedIndex,this.query.selectedIndexStart,this.query.selectedIndexEnd,this.query.selectedDirection,this.query.queryMode,this.query.selectedPrefix,this.query.selectedFtsIndex,this.query.ftsText,this.query.ftsMode,this.query.ftsPrefix,this.query.selectedVectorEngine,this.query.vectorText,this.query.vectorLimit],async()=>{this.type&&(this.query.resetPaging(),await this.query.load())}),e.$watch(()=>[this.searchMode,this.geoField,this.geoLat,this.geoLon,this.geoRadius,this.geoSortAsc,this.query.query],async()=>{"geo"===this.searchMode&&this.type&&this.geoField&&this.geoLat&&this.geoLon&&await this.loadGeoQuery()}),this.type){const n=new Set(["data","changelog","manage","schema","pipelines","engines"]),t=e.$vars.$router?.search?.tab;t&&n.has(t)&&(this.tab="pipelines"===t?"engines":t),await Promise.all([this.loadIndexes(),this.loadFtsIndexes(),this.loadVectorEngines(),this.loadTypeEnginesCount(),this.loadViolations(),this.loadTTLStats(),this.loadDefaultTTL(),this.query.load()])}this._unsubs=[u.on("index:reset",()=>{this.loadIndexes(!0),this.loadFtsIndexes(!0)}),u.on("processor:progress",e=>{e?.meta?.name?.startsWith("index:"+this.type+":")&&this.loadIndexes(!0)}),u.on("index:ready",n=>{this.loadIndexes(!0),this.loadFtsIndexes(!0);const t=n?.type;if(!t||t===this.type){const t=n?.index,i=Array.isArray(t)&&"fts"===t[0],o=i?t[1]:"string"==typeof t?t:"",s=i?`FTS index ready: ${o}`:o?`Index ready: ${o}`:"Index rebuild complete";e.$ok.shared?.toast?.success?.(s)}}),u.on("index:registered",()=>this.loadIndexes(!0)),u.on("index:drop",()=>this.loadIndexes(!0)),u.on("fts:registered",()=>this.loadFtsIndexes(!0)),u.on("fts:drop",()=>this.loadFtsIndexes(!0)),u.on("engine:started",()=>this.loadVectorEngines(!0)),u.on("engine:stopped",()=>this.loadVectorEngines(!0)),u.on("engine:error",()=>this.loadVectorEngines(!0)),u.on("engine:installed",()=>this.loadVectorEngines(!0)),u.on("engine:uninstalled",()=>this.loadVectorEngines(!0)),u.on("engine:started",()=>this.loadTypeEnginesCount()),u.on("engine:stopped",()=>this.loadTypeEnginesCount()),u.on("engine:error",()=>this.loadTypeEnginesCount()),u.on("engine:installed",()=>this.loadTypeEnginesCount()),u.on("engine:uninstalled",()=>this.loadTypeEnginesCount()),u.on("embeddings:pipeline:ready",()=>this.loadVectorEngines(!0)),u.on("embeddings:pipeline:ready",()=>this.loadTypeEnginesCount())];const n=new Set(["data","changelog","manage","schema","pipelines","engines"]);e.$watch(()=>e.$vars.$router?._search?.tab,()=>{const t=e.$vars.$router?._search?.tab;if(!t)return;const i=n.has(t)?"pipelines"===t?"engines":t:"data";this.tab!==i&&(this.tab=i)}),e.$watch(()=>this.geoHighlightedKey,n=>{const t=this._geoSelSource;if(this._geoSelSource=null,"geo"!==this.searchMode)return;if("table"===t)return;const i=e.$el?.querySelector("ok-table");if(!i)return;if(!n)return i.clearSelection?.(),void i.clearExpanded?.();const o=this.displayRows?.find(e=>this.keyOf(e)===n);o?(i.select?.(o),i.clearExpanded?.(),i.expand?.(o)):(i.clearSelection?.(),i.clearExpanded?.())})},unmount(){for(const e of this._unsubs)e();this._unsubs=[]},template:'\n<div style="display:flex; flex-direction:column; gap:0; height:100%;">\n\n \x3c!-- Tab bar --\x3e\n <div style="display:flex; gap:2px; border-bottom:1px solid var(--okdb-app-border); margin-bottom:12px; flex-shrink:0;">\n <button\n @click="setTab(\'data\')"\n :style="tab===\'data\' ? \'border-color:var(--okdb-accent); color:var(--okdb-accent); background:color-mix(in srgb, var(--okdb-accent) 7%, transparent); border-bottom-color:transparent;\' : \'border-color:transparent; background:transparent;\'"\n style="padding:6px 16px; border-radius:6px 6px 0 0; font-size:13px; cursor:pointer;">\n Data\n </button>\n <button\n @click="setTab(\'changelog\')"\n :style="tab===\'changelog\' ? \'border-color:var(--okdb-accent); color:var(--okdb-accent); background:color-mix(in srgb, var(--okdb-accent) 7%, transparent); border-bottom-color:transparent;\' : \'border-color:transparent; background:transparent;\'"\n style="padding:6px 16px; border-radius:6px 6px 0 0; font-size:13px; cursor:pointer;">\n Changelog\n </button>\n <button\n @click="setTab(\'manage\')"\n :style="tab===\'manage\' ? \'border-color:var(--okdb-accent); color:var(--okdb-accent); background:color-mix(in srgb, var(--okdb-accent) 7%, transparent); border-bottom-color:transparent;\' : \'border-color:transparent; background:transparent;\'"\n style="padding:6px 16px; border-radius:6px 6px 0 0; font-size:13px; cursor:pointer;">\n Manage\n </button>\n <button\n @click="setTab(\'schema\')"\n :style="tab===\'schema\' ? (\'border-color:\' + (violationCount > 0 ? \'var(--okdb-danger)\' : \'var(--okdb-success)\') + \'; color:\' + (violationCount > 0 ? \'var(--okdb-danger)\' : \'var(--okdb-success)\') + \'; background:\' + (violationCount > 0 ? \'#f8717111\' : \'color-mix(in srgb, var(--okdb-success) 7%, transparent)\') + \'; border-bottom-color:transparent;\') : (violationCount > 0 ? \'border-color:transparent; background:transparent; color:var(--okdb-danger);\' : \'border-color:transparent; background:transparent;\')"\n style="padding:6px 16px; border-radius:6px 6px 0 0; font-size:13px; cursor:pointer;">\n Schema<if :="violationCount > 0"><span style="margin-left:6px; font-size:10px; padding:2px 7px; border-radius:999px; background:color-mix(in srgb, var(--okdb-danger) 13%, transparent); color:var(--okdb-danger); font-weight:600;">{{ violationCount }}</span></if>\n </button>\n <button\n @click="switchToEngines()"\n :style="tab===\'engines\' ? \'border-color:var(--okdb-purple); color:var(--okdb-purple); background:color-mix(in srgb, var(--okdb-purple) 7%, transparent); border-bottom-color:transparent;\' : \'border-color:transparent; background:transparent;\'"\n style="padding:6px 16px; border-radius:6px 6px 0 0; font-size:13px; cursor:pointer;">\n Engines<if :="typeEngineCount > 0"><span class="mode-count">{{ typeEngineCount }}</span></if>\n </button>\n </div>\n\n \x3c!-- Data tab --\x3e\n <if :="tab === \'data\'">\n <div style="display:flex; flex-direction:column; gap:16px; flex:1; min-height:0;">\n\n <div class="card" style="display:flex; gap:16px;">\n\n \x3c!-- ── Left column: query controls ── --\x3e\n <div style="flex:1; display:flex; flex-direction:column; gap:10px; min-width:0;">\n\n \x3c!-- Row 1: filter label + accordion + JSON input --\x3e\n <div style="display:flex; flex-direction:column; gap:6px;">\n\n \x3c!-- Label row --\x3e\n <div style="display:flex; align-items:center; gap:6px;">\n <span style="font-size:12px; color:#6a8a9a; user-select:none;">\n {{ searchMode === \'vector\' ? \'Post-filter vector results with sift\' : searchMode === \'fts\' ? \'Post-filter FTS results with sift\' : searchMode === \'geo\' ? \'Post-filter geo results with sift\' : \'Filter with sift query\' }}\n </span>\n <span class="help-toggle" @click="helpOpen = !helpOpen" :class="helpOpen ? \'help-toggle--open\' : \'\'">i</span>\n </div>\n\n \x3c!-- Accordion: query reference --\x3e\n <if :="helpOpen">\n <div class="help-accordion">\n <div class="ht-row"><code>{}</code><span>match all records</span></div>\n <div class="ht-row"><code>{"field": "value"}</code><span>exact match</span></div>\n <div class="ht-row"><code>{"n": {"$gt": 5, "$lt": 50}}</code><span>numeric comparison</span></div>\n <div class="ht-row"><code>{"x": {"$in": ["a","b"]}}</code><span>set membership</span></div>\n <div class="ht-row"><code>{"x": {"$exists": true}}</code><span>field presence</span></div>\n <div class="ht-row ht-row--note">Nested fields use dot notation: <code>"a.b"</code></div>\n </div>\n </if>\n\n <ok-json-input\n :value="query.query"\n @change="query.query = $event.detail"\n :schema="siftSchema"\n style="min-width:220px; min-height:60px;">\n </ok-json-input>\n\n </div>\n\n \x3c!-- Mode bar: Scan / Index / FTS / Vector --\x3e\n <div style="display:flex; gap:4px; align-items:center; flex-wrap:wrap;">\n <div class="seg-toggle" style="flex-shrink:0;">\n <button :class="searchMode === \'scan\' ? \'active\' : \'\'" @click="setSearchMode(\'scan\')">Scan</button>\n <button :class="searchMode === \'index\' ? \'active\' : \'\'" @click="setSearchMode(\'index\')">\n Index<if :="possibleIndexes.length > 0"><span class="mode-count">{{ possibleIndexes.length }}</span></if>\n </button>\n <button :class="searchMode === \'fts\' ? \'active\' : \'\'" @click="setSearchMode(\'fts\')">\n FTS<if :="ftsList.length > 0"><span class="mode-count">{{ ftsList.length }}</span></if>\n </button>\n <button :class="searchMode === \'vector\' ? \'active\' : \'\'" @click="setSearchMode(\'vector\')">\n Vector<if :="vectorEngines.length > 0"><span class="mode-count">{{ vectorEngines.length }}</span></if>\n </button>\n <button :class="searchMode === \'geo\' ? \'active\' : \'\'" @click="setSearchMode(\'geo\')">\n Geo<if :="geoIndexes.length > 0"><span class="mode-count">{{ geoIndexes.length }}</span></if>\n </button>\n </div>\n </div>\n\n \x3c!-- ── Scan panel ── --\x3e\n <if :="searchMode === \'scan\'">\n <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">\n <button\n @click="query.selectedDirection = query.selectedDirection === \'desc\' ? \'asc\' : \'desc\'"\n style="min-width:78px;"\n title="Toggle key scan direction">\n {{ query.selectedDirection === \'desc\' ? \'↓ Desc\' : \'↑ Asc\' }}\n </button>\n <span style="font-size:12px; opacity:.4; user-select:none;">Full key-ordered scan · sift applied as filter</span>\n </div>\n </if>\n\n \x3c!-- ── Index panel ── --\x3e\n <if :="searchMode === \'index\'">\n <div style="display:flex; flex-direction:column; gap:8px;">\n\n <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">\n <select @change="selectIndex($event.target.value)" style="min-width:160px;">\n <option value="">— none —</option>\n <each :of="possibleIndexes" let:item="idx">\n <option :value="idx" :selected="query.selectedIndex === idx">\n {{ idx }}{{ indexStatuses[idx]?.unique ? \' ◆\' : \'\' }}{{ indexStatusBadge(idx) ? \' ⚠ \' + indexStatusBadge(idx) : \'\' }}\n </option>\n </each>\n <option value="__create_index__" style="color:var(--okdb-accent);">+ Create index…</option>\n <if :="query.selectedIndex">\n <option value="__remove_index__" style="color:var(--okdb-danger);">× Remove "{{ query.selectedIndex }}"</option>\n </if>\n </select>\n\n <button\n @click="query.selectedDirection = query.selectedDirection === \'desc\' ? \'asc\' : \'desc\'"\n style="min-width:78px;"\n title="Toggle sort direction">\n {{ query.selectedDirection === \'desc\' ? \'↓ Desc\' : \'↑ Asc\' }}\n </button>\n\n <if :="query.selectedIndex">\n <div class="seg-toggle">\n <button\n :class="query.queryMode === \'prefix\' ? \'active\' : \'\'"\n @click="query.queryMode = \'prefix\', query.selectedIndexStart = undefined, query.selectedIndexEnd = undefined">\n Prefix\n </button>\n <button\n :class="query.queryMode === \'range\' ? \'active\' : \'\'"\n @click="query.queryMode = \'range\', query.selectedPrefix = undefined">\n Range\n </button>\n </div>\n </if>\n </div>\n\n \x3c!-- ── Index detail strip ── --\x3e\n <if :="query.selectedIndex && selectedIndexInfo">\n <div class="idx-info-strip">\n <span class="idx-info-label">Fields:</span>\n <each :of="selectedIndexInfo.fields" let:item="f">\n <code class="idx-info-field">{{ f }}</code>\n </each>\n <if :="selectedIndexInfo.isUnique">\n <span class="idx-info-badge idx-info-badge--unique">◆ unique</span>\n </if>\n <if :="selectedIndexInfo.isGeo">\n <span class="idx-info-badge idx-info-badge--geo">◎ geo · precision {{ selectedIndexInfo.geoPrecision ?? 7 }}</span>\n </if>\n <if :="!selectedIndexInfo.isUnique && !selectedIndexInfo.isGeo">\n <span class="idx-info-badge idx-info-badge--regular">regular</span>\n </if>\n </div>\n </if>\n\n <if :="query.selectedIndex">\n <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">\n <if :="query.queryMode === \'prefix\'">\n <span style="opacity:.6; font-size:12px;">Prefix:</span>\n <ok-json-input mode="input" :value="query.selectedPrefix" @change="query.selectedPrefix = $event.detail" style="width:220px;" />\n <span style="opacity:.35; font-size:11px;">scalar or array for composite indexes</span>\n </if>\n <if :="query.queryMode === \'range\'">\n <span style="opacity:.6; font-size:12px;">Start:</span>\n <ok-json-input mode="input" :value="query.selectedIndexStart" @change="query.selectedIndexStart = $event.detail" style="width:180px;" />\n <span style="opacity:.6; font-size:12px;">End:</span>\n <ok-json-input mode="input" :value="query.selectedIndexEnd" @change="query.selectedIndexEnd = $event.detail" style="width:180px;" />\n </if>\n </div>\n </if>\n\n </div>\n </if>\n\n \x3c!-- ── FTS panel ── --\x3e\n <if :="searchMode === \'fts\'">\n <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">\n\n <select @change="selectFtsIndex($event.target.value, $event)" style="min-width:160px;">\n <option value="">— none —</option>\n <each :of="ftsList" let:item="fts">\n <option :value="fts.name" :selected="query.selectedFtsIndex === fts.name">\n {{ fts.name }}{{ ftsStatusBadge(fts) ? \' ⚠ \' + ftsStatusBadge(fts) : \'\' }}\n </option>\n </each>\n <option value="__create_fts__" style="color:var(--okdb-accent);">+ Create FTS index…</option>\n <if :="query.selectedFtsIndex">\n <option value="__drop_fts__" style="color:var(--okdb-danger);">× Drop "{{ query.selectedFtsIndex }}"</option>\n </if>\n </select>\n\n <if :="query.selectedFtsIndex">\n <input\n type="text"\n placeholder="Search…"\n :value="query.ftsText"\n @input="query.ftsText = $event.target.value"\n style="flex:1; min-width:140px; max-width:300px;"\n />\n <div class="seg-toggle" title="AND: all terms required · OR: any term matches, ranked by hit count">\n <button :class="query.ftsMode === \'and\' ? \'active\' : \'\'" @click="query.ftsMode = \'and\'">AND</button>\n <button :class="query.ftsMode === \'or\' ? \'active\' : \'\'" @click="query.ftsMode = \'or\'">OR</button>\n </div>\n <label style="font-size:12px; display:flex; align-items:center; gap:4px; cursor:pointer; white-space:nowrap; user-select:none;" title="Prefix: \'sta\' matches \'start\', \'stack\', etc.">\n <input type="checkbox" :checked="query.ftsPrefix" @change="query.ftsPrefix = $event.target.checked" />\n Prefix\n </label>\n </if>\n\n </div>\n </if>\n\n \x3c!-- ── Vector panel ── --\x3e\n <if :="searchMode === \'vector\'">\n <if :="vectorEngines.length === 0">\n <div style="font-size:12px; opacity:.7; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <ok-icon name="cpu" style="width:13px;height:13px;opacity:.5;"></ok-icon>\n <span>No vector search engines for <b>{{ type }}</b> yet.</span>\n <button\n @click="openEnginesCreatePipeline()"\n style="font-size:11px; padding:2px 10px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-indigo) 33%, transparent); color:var(--okdb-indigo); background:color-mix(in srgb, var(--okdb-indigo) 6%, transparent); cursor:pointer;"\n >⛓ Create Pipeline…</button>\n </div>\n </if>\n <if :="vectorEngines.length > 0">\n <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">\n\n <select @change="selectVectorEngine($event.target.value, $event)" style="min-width:160px;">\n <option value="">— none —</option>\n <each :of="vectorEngines" let:item="ve">\n <option :value="displayVectorName(ve.name)" :selected="query.selectedVectorEngine === displayVectorName(ve.name)">\n {{ displayVectorName(ve.name) }}{{ ve.isRunning ? \'\' : \' (stopped)\' }}\n </option>\n </each>\n <option value="__create_pipeline__" style="color:var(--okdb-indigo);">+ Create pipeline…</option>\n </select>\n\n <if :="query.selectedVectorEngine">\n <input\n type="text"\n placeholder="Semantic query…"\n :value="query.vectorText"\n @input:debounce.500="query.vectorText = $event.target.value"\n style="flex:1; min-width:160px; max-width:340px;"\n />\n <span style="font-size:12px; opacity:.55; white-space:nowrap; user-select:none;">top</span>\n <input\n type="number"\n :value="query.vectorLimit"\n @change="query.vectorLimit = Math.max(1, Math.min(200, Number($event.target.value) || 20))"\n min="1" max="200"\n style="width:56px; text-align:center;"\n title="Max results"\n />\n </if>\n\n \x3c!-- Pipeline badge — clickable, navigates to local Engines tab in pipeline mode --\x3e\n <if :="selectedVectorEngineMeta">\n <if :="selectedVectorEngineMeta.meta && selectedVectorEngineMeta.meta.pipeline">\n <button\n @click="openEnginesPipeline(selectedVectorEngineMeta.meta.pipeline)"\n style="font-size:11px; padding:2px 9px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-indigo) 33%, transparent); color:var(--okdb-indigo); background:color-mix(in srgb, var(--okdb-indigo) 6%, transparent); cursor:pointer; white-space:nowrap;"\n :title="\'Open the type pipeline view focused on: \' + selectedVectorEngineMeta.meta.pipeline"\n >⛓ {{ selectedVectorEngineMeta.meta.pipeline }} →</button>\n </if>\n </if>\n\n \x3c!-- Link to local type engines workspace --\x3e\n <button\n @click="openTypePipelinesForSelectedVector()"\n style="font-size:11px; padding:2px 9px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-indigo) 20%, transparent); color:color-mix(in srgb, var(--okdb-indigo) 60%, transparent); background:transparent; cursor:pointer; white-space:nowrap; margin-left:auto;"\n title="Open the type-scoped engines view for this vector search"\n >⛓ Type Engines →</button>\n\n </div>\n </if>\n </if>\n\n \x3c!-- ── Geo panel ── --\x3e\n <if :="searchMode === \'geo\'">\n <if :="geoIndexes.length === 0">\n <div style="font-size:12px; opacity:.7; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">\n <span>◎</span>\n <span>No geo indexes for <b>{{ type }}</b> yet. Create one from the Manage tab.</span>\n </div>\n </if>\n <if :="geoIndexes.length > 0">\n <div style="display:flex; flex-direction:column; gap:8px;">\n <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">\n <select @change="geoField = $event.target.value" style="min-width:140px;">\n <each :of="geoIndexes" let:item="gf">\n <option :value="gf" :selected="geoField === gf">{{ gf }}</option>\n </each>\n </select>\n <span style="font-size:12px; opacity:.5;">Lat</span>\n <input\n type="number" step="any" placeholder="40.7484"\n :value="geoLat"\n @input:debounce.100="geoLat = $event.target.value"\n style="width:110px;"\n />\n <span style="font-size:12px; opacity:.5;">Lon</span>\n <input\n type="number" step="any" placeholder="-73.9856"\n :value="geoLon"\n @input:debounce.100="geoLon = $event.target.value"\n style="width:110px;"\n />\n <span style="font-size:12px; opacity:.5;">Radius (m)</span>\n <input\n type="number" step="any" placeholder="∞"\n :value="geoRadius"\n @input:debounce.100="geoRadius = $event.target.value"\n style="width:90px;"\n title="Leave empty for unlimited radius (sorted by distance)"\n />\n <button\n @click="geoSortAsc = !geoSortAsc"\n style="min-width:78px;"\n title="Toggle sort direction">\n {{ geoSortAsc ? \'↑ Nearest\' : \'↓ Farthest\' }}\n </button>\n </div>\n <div style="font-size:11px; opacity:.35; user-select:none;">\n Results sorted by distance · sift filter applied as post-filter · leave radius empty for no cutoff\n </div>\n </div>\n </if>\n </if>\n\n </div>\n\n </div>\n\n \x3c!-- ── Geo view toggle (only in geo mode) ── --\x3e\n <if :="searchMode === \'geo\' && geoIndexes.length > 0">\n <div style="display:flex; align-items:center; gap:4px; flex-shrink:0; padding:2px 0;">\n <div class="seg-toggle" style="flex-shrink:0;">\n <button :class="geoViewMode === \'table\' ? \'active\' : \'\'" @click="geoViewMode = \'table\'" title="Table only">\n <ok-icon name="table-2" style="width:13px;height:13px;vertical-align:middle;"></ok-icon>\n </button>\n <button :class="geoViewMode === \'map\' ? \'active\' : \'\'" @click="geoViewMode = \'map\'" title="Map only">\n <ok-icon name="map-pin" style="width:13px;height:13px;vertical-align:middle;"></ok-icon>\n </button>\n <button :class="geoViewMode === \'split\' ? \'active\' : \'\'" @click="geoViewMode = \'split\'" title="Table + Map">\n <ok-icon name="columns" style="width:13px;height:13px;vertical-align:middle;"></ok-icon>\n </button>\n </div>\n <span style="font-size:10px; opacity:.3; user-select:none;">view</span>\n </div>\n </if>\n\n \x3c!-- ── Results area ── --\x3e\n <div style="display:flex; flex-direction:column; position:relative; flex:1; min-height:0;">\n <if :="query.busy">\n <ok-overlay>\n <div class="loader-center">\n <ok-icon name="loader" class="spin"></ok-icon>\n </div>\n </ok-overlay>\n </if>\n\n \x3c!-- Results bar: timing + Add item --\x3e\n <div style="margin-bottom:8px; display:flex; align-items:center; gap:8px; flex-shrink:0;">\n <span style="font-size:12px; opacity:.45;">\n {{ query.time != null ? query.time + \'ms\' : \'\' }}\n <b style="opacity:.7;">{{ type }}</b>\n </span>\n <div style="margin-left:auto;">\n <button @click="openAddModal" class="btn-primary btn-icon" style="font-size:12px; padding:3px 12px;"\n :disabled="!$ok.stores.auth.can(\'data:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'data:write\') }"\n :title="$ok.stores.auth.permTooltip(\'data:write\')">\n <ok-icon name="plus"></ok-icon>Add item\n </button>\n </div>\n </div>\n\n \x3c!-- Content row — table + map as siblings, shown/hidden via display:none --\x3e\n <div style="display:flex; flex:1; min-height:0; gap:10px;">\n\n \x3c!-- TABLE col — hidden only in pure map mode --\x3e\n <div\n :style="searchMode === \'geo\' && geoViewMode === \'map\' ? \'display:none\' : \'flex:1; min-width:0; display:flex; flex-direction:column; min-height:0;\'"\n >\n <if :="hasDirectValueRows">\n <ok-table\n ref="table"\n :data="displayRows"\n select="single"\n no-scroll\n style="flex:1; min-height:0; overflow:auto;"\n @select-change="onGeoTableSelect($event)"\n >\n <columns>\n <column width="min-content" no-resize>\n <header>\n <span class="key__header">key</span>\n <if :="query.selectedFtsIndex">\n <span style="font-size:10px; color:var(--okdb-accent); opacity:.6; margin-left:6px; font-weight:normal;">score</span>\n </if>\n <if :="query.selectedVectorEngine">\n <span style="font-size:10px; color:var(--okdb-indigo); opacity:.6; margin-left:6px; font-weight:normal;">sim</span>\n </if>\n <if :="searchMode === \'geo\'">\n <span style="font-size:10px; color:var(--okdb-success); opacity:.6; margin-left:6px; font-weight:normal;">dist</span>\n </if>\n </header>\n <cell let:row>\n <span class="key__cell">{{ keyOf(row) }}</span>\n <if :="directValueKindOf(row)">\n <span :class="\'direct-value-badge direct-value-badge--\' + directValueKindOf(row)">\n {{ directValueKindOf(row) }}\n </span>\n </if>\n <if :="query.selectedFtsIndex">\n <span class="fts-score-badge">{{ matchOf(row) ?? \'—\' }}</span>\n </if>\n <if :="query.selectedVectorEngine">\n <span class="vec-score-badge">{{ vectorScoreOf(row) ?? \'—\' }}</span>\n <if :="vectorChunkMetaOf(row)">\n <span style="font-size:10px; color:var(--okdb-orange); opacity:.8; margin-left:4px; white-space:nowrap;">\n chunk {{ vectorChunkMetaOf(row).start }}–{{ vectorChunkMetaOf(row).end }}\n </span>\n </if>\n </if>\n <if :="searchMode === \'geo\' && distanceOf(row)">\n <span class="geo-dist-badge">{{ distanceOf(row) }}</span>\n </if>\n </cell>\n </column>\n\n <column>\n <header>value</header>\n <cell let:row>\n <code :title="prettyValueOf(row)" style="display:block; white-space:pre-wrap; word-break:break-word; font-size:12px;">{{ inlineValueOf(row) }}</code>\n </cell>\n </column>\n </columns>\n\n <expanded let:row>\n \x3c!-- Chunk info panel — shown only in vector mode for chunked pipeline results --\x3e\n <if :="query.selectedVectorEngine && vectorChunkMetaOf(row)">\n <div style="padding:8px 10px 0; display:flex; flex-direction:column; gap:6px;">\n <if :="vectorChunkMetaOf(row).text">\n <div style="font-size:11px; opacity:.5; margin-bottom:2px;">Matched chunk\n <span style="color:var(--okdb-orange); margin-left:6px;">chars {{ vectorChunkMetaOf(row).start }}–{{ vectorChunkMetaOf(row).end }}</span>\n </div>\n <div style="background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px; padding:8px; font-size:11px; max-height:140px; overflow:auto; white-space:pre-wrap; word-break:break-word; line-height:1.5;">{{ vectorChunkMetaOf(row).text }}</div>\n </if>\n\n <if :="!vectorChunkMetaOf(row).text && vectorChunkMetaOf(row).start != null">\n <div style="font-size:11px; opacity:.5; margin-bottom:2px;">Matched chunk\n <span style="color:var(--okdb-orange); margin-left:6px;">chars {{ vectorChunkMetaOf(row).start }}–{{ vectorChunkMetaOf(row).end }}</span>\n <span style="opacity:.55; margin-left:4px;">(highlighted in document below)</span>\n </div>\n </if>\n\n <div style="height:1px; background:var(--okdb-app-border); margin:2px 0;"></div>\n </div>\n </if>\n\n <edit-row\n :row="rowValueOf(row)"\n :key="keyOf(row)"\n :type="type"\n @remove="onRemoveRow(keyOf(row))"\n @change="onRowChanged($event)">\n </edit-row>\n </expanded>\n </ok-table>\n </if>\n\n <if :="!hasDirectValueRows">\n <ok-table\n ref="table"\n :data="displayRows"\n select="single"\n no-scroll\n style="flex:1; min-height:0; overflow:auto;"\n @select-change="onGeoTableSelect($event)"\n >\n <columns>\n <column width="min-content" no-resize>\n <header>\n <span class="key__header">key</span>\n <if :="query.selectedFtsIndex">\n <span style="font-size:10px; color:var(--okdb-accent); opacity:.6; margin-left:6px; font-weight:normal;">score</span>\n </if>\n <if :="query.selectedVectorEngine">\n <span style="font-size:10px; color:var(--okdb-indigo); opacity:.6; margin-left:6px; font-weight:normal;">sim</span>\n </if>\n <if :="searchMode === \'geo\'">\n <span style="font-size:10px; color:var(--okdb-success); opacity:.6; margin-left:6px; font-weight:normal;">dist</span>\n </if>\n </header>\n <cell let:row>\n <span class="key__cell">{{ keyOf(row) }}</span>\n <if :="query.selectedFtsIndex">\n <span class="fts-score-badge">{{ matchOf(row) ?? \'—\' }}</span>\n </if>\n <if :="query.selectedVectorEngine">\n <span class="vec-score-badge">{{ vectorScoreOf(row) ?? \'—\' }}</span>\n <if :="vectorChunkMetaOf(row)">\n <span style="font-size:10px; color:var(--okdb-orange); opacity:.8; margin-left:4px; white-space:nowrap;">\n chunk {{ vectorChunkMetaOf(row).start }}–{{ vectorChunkMetaOf(row).end }}\n </span>\n </if>\n </if>\n <if :="searchMode === \'geo\' && distanceOf(row)">\n <span class="geo-dist-badge">{{ distanceOf(row) }}</span>\n </if>\n </cell>\n </column>\n\n <auto max="10" :show-more-content="(n) => \'+\' + n + \' cols\'" show-more-title="Click to show all columns">\n <cell let:row let:value>\n <span :class="[typeof value, value === null ? \'null\' : \'\']">\n {{ value }}\n </span>\n </cell>\n </auto>\n </columns>\n\n <expanded let:row>\n \x3c!-- Chunk info panel — shown only in vector mode for chunked pipeline results --\x3e\n <if :="query.selectedVectorEngine && vectorChunkMetaOf(row)">\n <div style="padding:8px 10px 0; display:flex; flex-direction:column; gap:6px;">\n <if :="vectorChunkMetaOf(row).text">\n <div style="font-size:11px; opacity:.5; margin-bottom:2px;">Matched chunk\n <span style="color:var(--okdb-orange); margin-left:6px;">chars {{ vectorChunkMetaOf(row).start }}–{{ vectorChunkMetaOf(row).end }}</span>\n </div>\n <div style="background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px; padding:8px; font-size:11px; max-height:140px; overflow:auto; white-space:pre-wrap; word-break:break-word; line-height:1.5;">{{ vectorChunkMetaOf(row).text }}</div>\n </if>\n\n <if :="!vectorChunkMetaOf(row).text && vectorChunkMetaOf(row).start != null">\n <div style="font-size:11px; opacity:.5; margin-bottom:2px;">Matched chunk\n <span style="color:var(--okdb-orange); margin-left:6px;">chars {{ vectorChunkMetaOf(row).start }}–{{ vectorChunkMetaOf(row).end }}</span>\n <span style="opacity:.55; margin-left:4px;">(highlighted in document below)</span>\n </div>\n </if>\n\n <div style="height:1px; background:var(--okdb-app-border); margin:2px 0;"></div>\n </div>\n </if>\n\n <edit-row\n :row="rowValueOf(row)"\n :key="keyOf(row)"\n :type="type"\n @remove="onRemoveRow(keyOf(row))"\n @change="onRowChanged($event)">\n </edit-row>\n </expanded>\n </ok-table>\n </if>\n\n <div class="ok-pager">\n <button @click="query.prevPage()" :disabled="!query.hasPrev || query.busy">‹ Prev</button>\n <span>{{ query.total }} result{{ query.total === 1 ? \'\' : \'s\' }} · {{ query.pageSize }}/page</span>\n <button @click="query.nextPage()" :disabled="!query.hasNext || query.busy">Next ›</button>\n </div>\n </div>\n\n <if :="searchMode === \'geo\'">\n \x3c!-- MAP col — always mounted, shown only in geo map/split mode --\x3e\n <div\n style="flex:1; min-width:0; min-height:0; border-radius:8px; overflow:hidden; border:1px solid #23292e;"\n :style="{ \'display\': (geoViewMode === \'map\' || geoViewMode === \'split\')? \'block\' : \'none\' }"\n >\n <geo-map\n :points="geoMapPoints"\n :origin-lat="geoLat"\n :origin-lon="geoLon"\n :radius="geoRadius"\n :highlight-key="geoHighlightedKey"\n @move="geoLat = String($event.detail.lat.toFixed(6)), geoLon = String($event.detail.lon.toFixed(6))"\n @select="onGeoMapSelect($event)"\n style="width:100%; height:100%;"\n />\n </div>\n </if>\n\n </div>\n </div>\n\n\n</div>\n </if>\n\n \x3c!-- Changelog tab --\x3e\n <if :="tab === \'changelog\'">\n <type-changelog :type="type" style="flex:1; min-height:0;" />\n </if>\n\n \x3c!-- Manage tab --\x3e\n <if :="tab === \'manage\'">\n <div style="display:flex; flex-direction:column; gap:12px; flex:1; overflow:auto; padding-right:4px;">\n\n \x3c!-- Overview --\x3e\n <div class="card" style="display:flex; gap:24px; align-items:center;">\n <div>\n <div style="font-size:15px; font-weight:600;">{{ type }}</div>\n <div style="font-size:12px; opacity:.5; margin-top:2px;">type</div>\n </div>\n <div>\n <div style="font-size:22px; font-weight:600; color:var(--okdb-accent);">{{ typeCount }}</div>\n <div style="font-size:12px; opacity:.5; margin-top:2px;">records</div>\n </div>\n </div>\n\n \x3c!-- Fields --\x3e\n <div class="card">\n <div class="manage-section-title">Fields</div>\n <if :="typeFieldEntries.length === 0">\n <div style="opacity:.4; font-size:13px;">No fields tracked yet — add items or use Recompute.</div>\n </if>\n <if :="typeFieldEntries.length > 0">\n <div class="manage-grid manage-grid--2">\n <div class="manage-col-hd">Field</div>\n <div class="manage-col-hd">Observed types</div>\n <each :of="typeFieldEntries" let:item="tf">\n <div class="manage-cell"><code>{{ tf.name }}</code></div>\n <div class="manage-cell manage-cell--dim">{{ tf.types.join(\', \') }}</div>\n </each>\n </div>\n </if>\n </div>\n\n \x3c!-- Secondary Indexes --\x3e\n <div class="card">\n <div class="manage-section-header">\n <div class="manage-section-title">Secondary Indexes</div>\n <button @click="openCreateIndex()" class="btn-icon" style="font-size:11px; padding:3px 10px;"\n :disabled="!$ok.stores.auth.can(\'schema:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'schema:write\') }"\n :title="$ok.stores.auth.permTooltip(\'schema:write\')">\n <ok-icon name="plus"></ok-icon>Create index\n </button>\n </div>\n <if :="indexEntries.length === 0">\n <div style="opacity:.4; font-size:13px;">No indexes yet.</div>\n </if>\n <if :="indexEntries.length > 0">\n <div class="manage-grid manage-grid--idx">\n <div class="manage-col-hd">Fields</div>\n <div class="manage-col-hd">Kind</div>\n <div class="manage-col-hd">Ownership</div>\n <div class="manage-col-hd">Status</div>\n <div class="manage-col-hd"></div>\n <div class="manage-col-hd"></div>\n <each :of="indexEntries" let:item="ix">\n <div class="manage-cell" style="gap:4px; flex-wrap:wrap;">\n <each :of="ix.fields" let:item="f" let:index="fi">\n <code style="font-size:12px; color:var(--okdb-json-key);">{{ f }}</code><span style="opacity:.3; font-size:11px; margin-left:0;" :style="fi < ix.fields.length - 1 ? \'\' : \'display:none\'">·</span>\n </each>\n </div>\n <div class="manage-cell" style="gap:6px;">\n <if :="ix.isGeo">\n <span class="idx-kind-badge idx-kind-badge--geo">◎ geo</span>\n <span style="font-size:10px; opacity:.45; white-space:nowrap;">precision {{ ix.geoPrecision ?? 7 }}</span>\n </if>\n <if :="ix.isUnique && !ix.isGeo">\n <span class="idx-kind-badge idx-kind-badge--unique">◆ unique</span>\n </if>\n <if :="!ix.isGeo && !ix.isUnique">\n <span class="idx-kind-badge idx-kind-badge--regular">regular</span>\n </if>\n </div>\n <div class="manage-cell" style="gap:3px; flex-direction:column; align-items:flex-start;">\n <if :="ix.createdBy">\n <span class="idx-owned-badge">{{ ix.createdBy.kind }}:{{ ix.createdBy.name }}</span>\n </if>\n <each :of="ix.usedBy" let:item="consumer">\n <span class="idx-consumer-badge">{{ consumer }}</span>\n </each>\n </div>\n <div class="manage-cell" style="flex-direction:column; align-items:flex-start; gap:4px;">\n <span :class="\'manage-status manage-status--\' + ix.status">{{ ix.status }}</span>\n <if :="ix.procState === \'building\' && ix.progress">\n <div class="proc-card__progress" style="width:100%; min-width:60px;">\n <div class="proc-card__progress-bar" :style="\'width:\' + Math.round((ix.progress.percent ?? 0) * 100) + \'%\'"></div>\n </div>\n <span style="font-size:10px; opacity:.55;">{{ Math.round((ix.progress.percent ?? 0) * 100) }}%</span>\n </if>\n </div>\n <div class="manage-cell" style="justify-content:flex-end;">\n <button @click="rebuildIndex(ix.name)" class="btn-icon" style="font-size:11px; padding:2px 10px; opacity:.7;" title="Rebuild this index from all current records"\n :disabled="!$ok.stores.auth.can(\'schema:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'schema:write\') }"\n :title="$ok.stores.auth.can(\'schema:write\') ? \'Rebuild this index from all current records\' : $ok.stores.auth.permTooltip(\'schema:write\')">\n <ok-icon name="refresh-cw"></ok-icon>Rebuild\n </button>\n </div>\n <div class="manage-cell" style="justify-content:flex-end;">\n <button @click="removeIndex(ix.name)" class="btn-icon" style="font-size:11px; padding:2px 10px; color:var(--okdb-danger); opacity:.8;"\n :disabled="!$ok.stores.auth.can(\'schema:write\') || ix.usedBy.length > 0"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'schema:write\') }"\n :title="ix.usedBy.length > 0 ? \'Used by: \' + ix.usedBy.join(\', \') : ($ok.stores.auth.can(\'schema:write\') ? \'Remove this index\' : $ok.stores.auth.permTooltip(\'schema:write\'))">\n <ok-icon name="trash-2"></ok-icon>Remove\n </button>\n </div>\n </each>\n </div>\n </if>\n </div>\n\n \x3c!-- FTS Indexes --\x3e\n <div class="card">\n <div class="manage-section-header">\n <div class="manage-section-title">FTS Indexes</div>\n <button @click="openCreateFtsModal()" class="btn-icon" style="font-size:11px; padding:3px 10px;"\n :disabled="!$ok.stores.auth.can(\'schema:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'schema:write\') }"\n :title="$ok.stores.auth.permTooltip(\'schema:write\')">\n <ok-icon name="plus"></ok-icon>Create FTS index\n </button>\n </div>\n <if :="ftsEntries.length === 0">\n <div style="opacity:.4; font-size:13px;">No FTS indexes yet.</div>\n </if>\n <if :="ftsEntries.length > 0">\n <div class="manage-grid manage-grid--fts">\n <div class="manage-col-hd">Name</div>\n <div class="manage-col-hd">Fields</div>\n <div class="manage-col-hd">Status</div>\n <div class="manage-col-hd">Size</div>\n <div class="manage-col-hd"></div>\n <div class="manage-col-hd"></div>\n <each :of="ftsEntries" let:item="fe">\n <div class="manage-cell"><code>{{ fe.name }}</code></div>\n <div class="manage-cell manage-cell--dim">{{ fe.fields }}</div>\n <div class="manage-cell">\n <span :class="\'manage-status manage-status--\' + fe.status">{{ fe.status }}</span>\n </div>\n <div class="manage-cell manage-cell--dim" style="font-size:11px; font-family:monospace;">{{ fmtBytes(fe.sizeBytes) }}</div>\n <div class="manage-cell" style="justify-content:flex-end;">\n <button @click="rebuildFts(fe.name)" class="btn-icon" style="font-size:11px; padding:2px 10px; opacity:.7;" title="Rebuild this FTS index from all current records"\n :disabled="!$ok.stores.auth.can(\'schema:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'schema:write\') }"\n :title="$ok.stores.auth.can(\'schema:write\') ? \'Rebuild this FTS index from all current records\' : $ok.stores.auth.permTooltip(\'schema:write\')">\n <ok-icon name="refresh-cw"></ok-icon>Rebuild\n </button>\n </div>\n <div class="manage-cell" style="justify-content:flex-end;">\n <button @click="dropFtsIndex(fe.name)" class="btn-icon" style="font-size:11px; padding:2px 10px; color:var(--okdb-danger); opacity:.8;" title="Drop this FTS index"\n :disabled="!$ok.stores.auth.can(\'schema:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'schema:write\') }"\n :title="$ok.stores.auth.can(\'schema:write\') ? \'Drop this FTS index\' : $ok.stores.auth.permTooltip(\'schema:write\')">\n <ok-icon name="trash-2"></ok-icon>Drop\n </button>\n </div>\n </each>\n </div>\n </if>\n </div>\n\n \x3c!-- Vector Search Engines --\x3e\n <div class="card">\n <div class="manage-section-header">\n <div class="manage-section-title">Vector Search Engines</div>\n <button\n @click="openEnginesWorkspace()"\n class="btn-icon"\n style="font-size:11px; padding:3px 10px; color:var(--okdb-indigo); border-color:color-mix(in srgb, var(--okdb-indigo) 27%, transparent);"\n title="Open the type-scoped engines view for this type"\n >\n ⛓ Type Engines →\n </button>\n </div>\n <if :="vectorEntries.length === 0">\n <div style="opacity:.4; font-size:13px;">\n No vector search engines for this type yet.\n <button\n @click="openEnginesCreatePipeline()"\n style="margin-left:6px; font-size:11px; padding:1px 8px; border-radius:999px; border:1px solid color-mix(in srgb, var(--okdb-indigo) 27%, transparent); color:var(--okdb-indigo); background:color-mix(in srgb, var(--okdb-indigo) 5%, transparent); cursor:pointer;"\n >Create Pipeline…</button>\n </div>\n </if>\n <if :="vectorEntries.length > 0">\n <div class="manage-grid manage-grid--vec">\n <div class="manage-col-hd">Engine</div>\n <div class="manage-col-hd">Status</div>\n <div class="manage-col-hd">Pipeline</div>\n <div class="manage-col-hd"></div>\n <each :of="vectorEntries" let:item="ve">\n <div class="manage-cell"><code>{{ ve.displayName }}</code></div>\n <div class="manage-cell">\n <span :class="\'manage-status manage-status--\' + (ve.running ? \'ready\' : \'creating\')">{{ ve.status }}</span>\n </div>\n <div class="manage-cell manage-cell--dim">{{ ve.pipeline ?? \'—\' }}</div>\n <div class="manage-cell" style="justify-content:flex-end;">\n <button\n @click="openEnginesPipeline(ve.pipeline)"\n class="btn-icon"\n style="font-size:11px; padding:2px 10px; color:var(--okdb-indigo); opacity:.85;"\n title="Open this type-scoped pipeline view"\n >\n ⛓ View local →\n </button>\n </div>\n </each>\n </div>\n </if>\n </div>\n\n \x3c!-- TTL --\x3e\n <div class="card">\n <div class="manage-section-header">\n <div class="manage-section-title">Time-to-Live (TTL)</div>\n <button @click="(loadTTLStats(), loadDefaultTTL())" class="btn-icon" style="font-size:11px; padding:3px 10px; opacity:.6;" :disabled="ttlStatsLoading" title="Refresh TTL stats">\n ↻\n </button>\n </div>\n\n \x3c!-- Default TTL --\x3e\n <div style="margin-bottom:12px; padding:10px; background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px;">\n <div style="font-size:12px; font-weight:600; opacity:.7; margin-bottom:8px;">Default TTL for new items</div>\n <if :="defaultTTL">\n <div style="font-size:13px; color:var(--okdb-success); margin-bottom:8px;">\n Currently: <span style="font-family:monospace; font-weight:600;">{{ fmtDuration(defaultTTL) }}</span>\n <span style="opacity:.4; font-size:11px;"> ({{ defaultTTL }}ms)</span>\n </div>\n </if>\n <if :="!defaultTTL">\n <div style="font-size:12px; opacity:.4; margin-bottom:8px;">No default — items don\'t expire unless TTL is set explicitly.</div>\n </if>\n <div style="display:flex; gap:6px; align-items:center; flex-wrap:wrap;">\n <select\n @change="onDefaultTTLPresetChange($event.target.value)"\n style="font-size:11px; padding:2px 6px; background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:4px; color:var(--okdb-app-fg);"\n >\n <option value="">Custom…</option>\n <option value="60000">1 min</option>\n <option value="300000">5 min</option>\n <option value="900000">15 min</option>\n <option value="3600000">1 hr</option>\n <option value="21600000">6 hr</option>\n <option value="86400000">24 hr</option>\n <option value="604800000">7 days</option>\n <option value="2592000000">30 days</option>\n </select>\n <input\n type="number"\n !value="defaultTTLInput"\n @input="(defaultTTLInput = $el.value, defaultTTLPreset = \'\')"\n placeholder="ms"\n style="width:110px; font-size:11px; padding:2px 6px; background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:4px; color:var(--okdb-app-fg);"\n />\n <button @click="setDefaultTTLAction()" :disabled="defaultTTLBusy || !defaultTTLInput || !$ok.stores.auth.can(\'data:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'data:write\') }"\n :title="$ok.stores.auth.permTooltip(\'data:write\')"\n style="font-size:11px; padding:2px 10px; color:var(--okdb-success); border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);">Set</button>\n <if :="defaultTTL">\n <button @click="clearDefaultTTLAction()" :disabled="defaultTTLBusy || !$ok.stores.auth.can(\'data:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'data:write\') }"\n :title="$ok.stores.auth.permTooltip(\'data:write\')"\n style="font-size:11px; padding:2px 10px; color:var(--okdb-danger); border-color:color-mix(in srgb, var(--okdb-danger) 33%, transparent);">Clear</button>\n </if>\n </div>\n </div>\n\n \x3c!-- TTL stats --\x3e\n <if :="!ttlStats || !ttlStats.enabled">\n <div style="opacity:.4; font-size:13px;">No TTL entries for this type — TTL sub-databases are created lazily on first use.</div>\n </if>\n <if :="ttlStats && ttlStats.enabled">\n <div style="display:flex; gap:24px; align-items:center; margin-bottom:10px;">\n <div>\n <div style="font-size:18px; font-weight:600; color:var(--okdb-success);">{{ ttlStats.totalEntries }}</div>\n <div style="font-size:11px; opacity:.5;">entries</div>\n </div>\n <div>\n <div :style="\'font-size:18px; font-weight:600; color:\' + (ttlStats.expiredEntries > 0 ? \'var(--okdb-danger)\' : \'var(--okdb-app-fg-muted)\') + \';\'">{{ ttlStats.expiredEntries }}</div>\n <div style="font-size:11px; opacity:.5;">expired</div>\n </div>\n <div>\n <div style="font-size:13px; font-family:monospace; color:var(--okdb-app-fg);">{{ ttlStats.nextExpiry ? fmtDuration(ttlStats.nextExpiry - Date.now()) : \'—\' }}</div>\n <div style="font-size:11px; opacity:.5;">next expiry</div>\n </div>\n </div>\n <div style="display:flex; gap:8px; align-items:center;">\n <button @click="triggerSweep()" :disabled="ttlSweepBusy || !$ok.stores.auth.can(\'data:write\')" class="btn-icon" style="font-size:11px; padding:3px 10px;"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'data:write\') }"\n :title="$ok.stores.auth.permTooltip(\'data:write\')">\n <ok-icon name="refresh-cw"></ok-icon>Sweep expired now\n </button>\n <if :="ttlSweepBusy"><span style="font-size:11px; opacity:.5;">Sweeping…</span></if>\n </div>\n </if>\n </div>\n\n \x3c!-- Danger zone --\x3e\n <div class="card card--danger"> <div class="manage-section-title" style="color:var(--okdb-danger);">⚠ Danger Zone</div>\n <div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">\n <button @click="recomputeFields" class="btn-icon"\n :disabled="!$ok.stores.auth.can(\'data:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'data:write\') }"\n :title="$ok.stores.auth.can(\'data:write\') ? \'Rescan all records to rebuild the field schema\' : $ok.stores.auth.permTooltip(\'data:write\')">\n <ok-icon name="refresh-cw"></ok-icon>Recompute fields\n </button>\n <button @click="dropType" class="btn-danger btn-icon"\n :disabled="!$ok.stores.auth.can(\'data:write\')"\n :class="{ \'perm-denied\': !$ok.stores.auth.can(\'data:write\') }"\n :title="$ok.stores.auth.permTooltip(\'data:write\')">\n <ok-icon name="trash-2"></ok-icon>Drop type\n </button>\n </div>\n </div>\n\n </div>\n </if>\n\n \x3c!-- Schema tab --\x3e\n <if :="tab === \'schema\'">\n <type-schema-tab\n :type="type"\n :on-view-key="(key) => setTab(\'data\')"\n :on-schema-changed="() => loadViolations()"\n :on-violation-count-changed="(count) => onViolationCountChanged(count)"\n style="flex:1; min-height:0;"\n />\n </if>\n\n <if :="tab === \'engines\'">\n <type-pipelines-tab :type="type" :focusedpipeline="currentRoutePipeline" :currentmode="currentRouteMode" style="flex:1; min-height:0;" />\n </if>\n\n</div>',style:"[tag]{display:flex;flex-direction:column;height:100%;min-width:0;min-height:0}.ok-table__cell__inner>span.object{color:#a6da95}.ok-table__cell__inner>span.number{color:var(--okdb-accent)}.ok-table__cell__inner>span.boolean{color:#ff6c6b}.ok-table__cell__inner>span.null,.ok-table__cell__inner>span.undefined{color:#c792ea}.ok-table__cell__inner>.key__header{font-weight:700;color:var(--okdb-json-key)}.ok-table__cell__inner>.key__cell{color:var(--okdb-json-key)}.fts-score-badge,.mode-count{display:inline-block;border-radius:999px;font-size:10px;vertical-align:middle}.mode-count{margin-left:4px;padding:0 5px;background:#ffffff18;line-height:1.6}.fts-score-badge{margin-left:5px;padding:1px 5px;border:1px solid color-mix(in srgb,var(--okdb-accent) 33%,transparent);color:var(--okdb-accent);opacity:.8}.vec-score-badge{border:1px solid color-mix(in srgb,var(--okdb-indigo) 33%,transparent);color:var(--okdb-indigo)}.direct-value-badge,.geo-dist-badge,.vec-score-badge{display:inline-block;margin-left:5px;padding:1px 5px;border-radius:999px;font-size:10px;vertical-align:middle;opacity:.8}.geo-dist-badge{border:1px solid color-mix(in srgb,var(--okdb-success) 33%,transparent);color:var(--okdb-success)}.direct-value-badge{border:1px solid}.direct-value-badge--scalar{border-color:color-mix(in srgb,var(--okdb-app-fg-muted) 33%,transparent);color:var(--okdb-app-fg-muted)}.direct-value-badge--array{border-color:#2dd4bf55;color:#2dd4bf}.direct-value-badge--null{border-color:color-mix(in srgb,var(--okdb-purple) 33%,transparent);color:var(--okdb-purple)}.manage-section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.manage-section-title{font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.06em;opacity:.5;margin-bottom:0}.manage-grid{display:grid;gap:0}.manage-grid--2{grid-template-columns:1fr 1fr}.manage-grid--3{grid-template-columns:1fr auto auto}.manage-grid--4{grid-template-columns:1fr auto auto auto}.manage-grid--idx{grid-template-columns:minmax(80px,2fr) minmax(90px,1fr) minmax(80px,1fr) auto auto auto}.idx-consumer-badge,.idx-owned-badge{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;white-space:nowrap}.idx-owned-badge{background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);border:1px solid color-mix(in srgb,var(--okdb-accent) 25%,transparent)}.idx-consumer-badge{background:color-mix(in srgb,var(--okdb-sky) 12%,transparent);color:var(--okdb-sky);border:1px solid color-mix(in srgb,var(--okdb-sky) 25%,transparent)}.manage-grid--fts{grid-template-columns:minmax(80px,1fr) 2fr minmax(60px,auto) minmax(50px,auto) auto auto}.manage-grid--vec{grid-template-columns:minmax(80px,1fr) minmax(60px,auto) minmax(80px,1fr) auto}.idx-kind-badge{display:inline-block;padding:1px 7px;border-radius:999px;font-size:10px;border:1px solid;white-space:nowrap}.idx-kind-badge--geo{color:var(--okdb-success);border-color:color-mix(in srgb,var(--okdb-success) 33%,transparent)}.idx-kind-badge--unique{color:var(--okdb-warning);border-color:color-mix(in srgb,var(--okdb-warning) 33%,transparent)}.idx-kind-badge--regular{color:var(--okdb-app-fg-muted);border-color:color-mix(in srgb,var(--okdb-app-fg-muted) 20%,transparent)}.idx-info-strip{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:5px 8px;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-surface-2);border-radius:4px;font-size:11px}.idx-info-label{opacity:.4;white-space:nowrap}.idx-info-field{font-size:11px;color:var(--okdb-json-key)}.idx-info-badge{padding:1px 7px;border-radius:999px;font-size:10px;border:1px solid;white-space:nowrap}.idx-info-badge--geo{color:var(--okdb-success);border-color:color-mix(in srgb,var(--okdb-success) 33%,transparent)}.idx-info-badge--unique{color:var(--okdb-warning);border-color:color-mix(in srgb,var(--okdb-warning) 33%,transparent)}.idx-info-badge--regular{color:var(--okdb-app-fg-muted);border-color:color-mix(in srgb,var(--okdb-app-fg-muted) 20%,transparent)}.manage-col-hd{font-size:11px;opacity:.4;padding:0 12px 6px 0;border-bottom:1px solid var(--okdb-app-border);font-weight:400}.manage-cell{padding:6px 12px 6px 0;border-bottom:1px solid var(--okdb-app-surface);font-size:13px;display:flex;align-items:center;min-width:0}.manage-cell--dim{opacity:.5;font-size:12px}.manage-status{display:inline-block;padding:1px 7px;border-radius:999px;font-size:10px;border:1px solid}.manage-status--ready{color:var(--okdb-success);border-color:color-mix(in srgb,var(--okdb-success) 33%,transparent)}.manage-status--creating,.manage-status--resetting{color:var(--okdb-warning);border-color:color-mix(in srgb,var(--okdb-warning) 33%,transparent)}.card--danger{border:1px solid color-mix(in srgb,var(--okdb-danger) 13%,transparent)!important}.proc-card__progress{background:var(--okdb-app-border);border-radius:999px;height:4px;overflow:hidden}.proc-card__progress-bar{height:100%;background:var(--okdb-warning);border-radius:999px;transition:width .3s}"};
|