@kedem/okdb 1.8.8 → 1.8.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kedem/okdb",
3
- "version": "1.8.8",
3
+ "version": "1.8.11",
4
4
  "description": "A fast, type-oriented database — strong consistency and rich indexing at the core, with sync, vector embeddings, full-text search, and AI tooling built in. Designed for the AI era.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "engines": {
@@ -1 +1 @@
1
- import e from"@toolkit/elements/ok-icon.ok.js";import t from"@toolkit/core/ok-overlay.ok.js";import n from"../../../components/ok-json-input.ok.js";export default{tag:"add-item-modal",dependencies:[n,t,e],attr:{modal:{},type:{},onDone:{}},context:e=>({busy:!1,key:"",valueInit:{},value:{},async save(){const t=String(this.key||"").trim();if(t){this.busy=!0;try{const n=await fetch(`${e.$ok.stores.okdb.apiBase}/type/${e.$attr.type}/item/${encodeURIComponent(t)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(this.value)});if(!n.ok)throw new Error((await n.json())?.error?.message||n.statusText);await e.$ok.stores.okdb.reloadTypes(),e.$ok.shared?.toast?.success?.("Item added"),await e.$attr.modal.close(),await(e.$attr.onDone?.({key:t,value:this.value}))}catch(t){e.$ok.shared?.toast?.error?.(t?.message||"Add failed"),console.error("Add item error",t)}finally{this.busy=!1}}},cancel(){e.$attr.modal.close()}}),template:'\n<div style="position:relative; min-width:360px; display:flex; flex-direction:column; gap:10px;">\n\n <if :="busy">\n <ok-overlay>\n <div class="loader-center">\n <ok-icon name="loader" class="spin" style="width:32px;height:32px;"></ok-icon>\n </div>\n </ok-overlay>\n </if>\n\n <input type="text" placeholder="Key" :value="key" @input="key = $el.value" />\n\n <div style="font-size:11px; opacity:.65; line-height:1.45;">\n Value may be any JSON value: object, array, string, number, boolean, or <code>null</code>.\n Editing in the data view replaces the full stored value.\n </div>\n\n <ok-json-input\n mode="textarea"\n style="height:300px;"\n :value="valueInit"\n @edit="value = $event.detail"\n />\n\n <div style="display:flex; gap:10px; justify-content:flex-end;">\n <button @click="cancel()">Cancel</button>\n <button @click="save()" :disabled="!key || value === undefined || busy">Save</button>\n </div>\n\n</div>\n'};
1
+ import e from"@toolkit/elements/ok-icon.ok.js";import t from"@toolkit/core/ok-overlay.ok.js";import o from"../../../components/ok-json-input.ok.js";export default{tag:"add-item-modal",dependencies:[o,t,e],attr:{modal:{},type:{},onDone:{}},context:e=>({busy:!1,key:"",valueInit:{},value:{},async save(){const t=String(this.key||"").trim();if(!t)return;const o=e.$attr.type;if("string"!=typeof o||!o.startsWith("~")||await e.$ok.shared.modal.confirm(`Adding "${t}" to "${o}" can corrupt the system. The queue, license checker, and other features rely on the exact shape of these records. Proceed?`,{title:"Add system record",confirmText:"Add anyway",confirmClass:"destructive"})){this.busy=!0;try{const o=await fetch(`${e.$ok.stores.okdb.apiBase}/type/${e.$attr.type}/item/${encodeURIComponent(t)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(this.value)});if(!o.ok)throw new Error((await o.json())?.error?.message||o.statusText);await e.$ok.stores.okdb.reloadTypes(),e.$ok.shared?.toast?.success?.("Item added"),await e.$attr.modal.close(),await(e.$attr.onDone?.({key:t,value:this.value}))}catch(t){e.$ok.shared?.toast?.error?.(t?.message||"Add failed"),console.error("Add item error",t)}finally{this.busy=!1}}},cancel(){e.$attr.modal.close()}}),template:'\n<div style="position:relative; min-width:360px; display:flex; flex-direction:column; gap:10px;">\n\n <if :="busy">\n <ok-overlay>\n <div class="loader-center">\n <ok-icon name="loader" class="spin" style="width:32px;height:32px;"></ok-icon>\n </div>\n </ok-overlay>\n </if>\n\n <input type="text" placeholder="Key" :value="key" @input="key = $el.value" />\n\n <div style="font-size:11px; opacity:.65; line-height:1.45;">\n Value may be any JSON value: object, array, string, number, boolean, or <code>null</code>.\n Editing in the data view replaces the full stored value.\n </div>\n\n <ok-json-input\n mode="textarea"\n style="height:300px;"\n :value="valueInit"\n @edit="value = $event.detail"\n />\n\n <div style="display:flex; gap:10px; justify-content:flex-end;">\n <button @click="cancel()">Cancel</button>\n <button @click="save()" :disabled="!key || value === undefined || busy">Save</button>\n </div>\n\n</div>\n'};
@@ -1 +1 @@
1
- import t from"../../../components/ok-json-input.ok.js";import e from"./tm-timeline.ok.js";function o(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}export default{tag:"edit-row",dependencies:[t,e],attr:{type:{},key:{},row:{}},context:t=>({get api(){return t.$ok.stores.okdb.apiBase},get type(){return t.$attr.type},get key(){return t.$attr.key},get row(){return t.$attr.row},get hasEditedValue(){return void 0!==this.edited&&!this.jsonError},edited:void 0,jsonError:!1,busySave:!1,busyRemove:!1,tmSelectedClock:null,tmFetchBusy:!1,tmValue:null,tmDiffs:[],get tmDiffClocks(){return this.tmDiffs.map(t=>t.clock)},get tmSelectedDiff(){return null===this.tmSelectedClock?null:this.tmDiffs.find(t=>t.clock===this.tmSelectedClock)??null},get tmSelectedTime(){const t=this.tmSelectedDiff?.timestamp;return t?new Date(t).toLocaleString("en-GB",{day:"numeric",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"}):null},get tmCurrentIdx(){return null===this.tmSelectedClock?-1:this.tmDiffClocks.indexOf(this.tmSelectedClock)},get canGoPrev(){return this.tmCurrentIdx>0},get canGoNext(){return-1!==this.tmCurrentIdx&&this.tmCurrentIdx<this.tmDiffClocks.length-1},goPrev(){this.canGoPrev&&this.onTMClockSelect({clock:this.tmDiffClocks[this.tmCurrentIdx-1]})},goNext(){this.canGoNext&&this.onTMClockSelect({clock:this.tmDiffClocks[this.tmCurrentIdx+1]})},clockOpen:!1,clockBusy:!1,clockEntries:null,ttlInfo:null,ttlBusy:!1,ttlOpen:!1,ttlInputMs:"",ttlPreset:"",ttlPresets:[{label:"1 min",ms:6e4},{label:"5 min",ms:3e5},{label:"15 min",ms:9e5},{label:"1 hr",ms:36e5},{label:"6 hr",ms:216e5},{label:"24 hr",ms:864e5},{label:"7 days",ms:6048e5},{label:"30 days",ms:2592e6}],async loadTTL(){if(!this.ttlBusy){this.ttlBusy=!0;try{const t=await fetch(`${this.api}/type/${encodeURIComponent(this.type)}/item/${encodeURIComponent(this.key)}/ttl`),e=await t.json(),o=e.data??e.result??null;this.ttlInfo=o||!1}catch{this.ttlInfo=!1}finally{this.ttlBusy=!1}}},async setItemTTL(){const e=parseInt(this.ttlInputMs);if(e&&!(e<=0)){this.ttlBusy=!0;try{const o=await fetch(`${this.api}/type/${encodeURIComponent(this.type)}/item/${encodeURIComponent(this.key)}/ttl`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({ttl:e})});if(!o.ok){const t=await o.json().catch(()=>({}));throw new Error(t?.error?.message||o.statusText)}t.$ok.shared?.toast?.info?.(`TTL set (${this.fmtDuration(e)})`),await this.loadTTL()}catch(e){t.$ok.shared?.toast?.error?.(e.message||"Failed to set TTL")}finally{this.ttlBusy=!1}}},async clearItemTTL(){this.ttlBusy=!0;try{const e=await fetch(`${this.api}/type/${encodeURIComponent(this.type)}/item/${encodeURIComponent(this.key)}/ttl`,{method:"DELETE"});if(!e.ok)throw new Error(e.statusText);this.ttlInfo=!1,this.ttlInputMs="",this.ttlPreset="",t.$ok.shared?.toast?.info?.("TTL cleared")}catch(e){t.$ok.shared?.toast?.error?.(e.message||"Failed to clear TTL")}finally{this.ttlBusy=!1}},onTTLPresetChange(t){this.ttlPreset=t,t&&(this.ttlInputMs=t)},fmtDuration(t){if(null==t)return"—";const e=Math.floor(t/1e3);return e<60?e+"s":e<3600?Math.floor(e/60)+"m "+e%60+"s":e<86400?Math.floor(e/3600)+"h "+Math.floor(e%3600/60)+"m":Math.floor(e/86400)+"d "+Math.floor(e%86400/3600)+"h"},get ttlBadgeText(){return this.ttlBusy?"ttl …":this.ttlInfo?this.ttlInfo.remainingMs<=0?"ttl: expired":"ttl: "+this.fmtDuration(this.ttlInfo.remainingMs):"no ttl"},get ttlBadgeColor(){return this.ttlInfo?this.ttlInfo.remainingMs<=0?"var(--okdb-danger)":this.ttlInfo.remainingMs<6e4?"var(--okdb-orange)":"var(--okdb-success)":"var(--okdb-app-fg-muted)"},hlcToDate(t){const e=Number(t)||0,o=(e-e%1024)/1024;return o?new Date(o).toLocaleString():"—"},get latestClock(){return this.clockEntries?.length?this.clockEntries[0].clock??null:null},get latestTime(){if(!this.clockEntries?.length)return null;const t=this.clockEntries[0].timestamp;return t?this.hlcToDate(t):null},async toggleClock(){this.clockOpen=!this.clockOpen},async loadClock(){if(!this.clockBusy){this.clockBusy=!0;try{const t=await fetch(`${this.api}/type/${encodeURIComponent(this.type)}/item/${encodeURIComponent(this.key)}/changelog?limit=20`),e=await t.json();this.clockEntries=e.data??[]}catch{this.clockEntries=[]}finally{this.clockBusy=!1}}},async onTMClockSelect({clock:t}){if(this.tmSelectedClock=t,null===t)return this.tmValue=null,this.tmFetchBusy=!1,void(this.edited=void 0);this.tmFetchBusy=!0;try{const e=await fetch(`${this.api}/time-machine/${encodeURIComponent(this.type)}/${encodeURIComponent(this.key)}/at/${t}`),o=await e.json();if(!e.ok)throw new Error(o?.error?.message||"Failed");this.tmValue=o.result?.value??o.data?.value??null,this.edited=JSON.parse(JSON.stringify(this.tmValue))}catch{this.tmSelectedClock=null,this.tmValue=null}finally{this.tmFetchBusy=!1}},reset(){this.edited=void 0,this.jsonError=!1,t.$refs.editor.reset?.()},async save(){if(void 0!==this.edited&&!this.jsonError){this.busySave=!0;try{const e=this.edited,n=await fetch(`${this.api}/type/${this.type}/item/${this.key}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok){const t=await n.json().catch(()=>({})),e=t?.error?.message||t?.message||t?.error||n.statusText;throw new Error("string"==typeof e?e:n.statusText)}o(this.row)&&o(e)&&(Object.keys(this.row).forEach(t=>delete this.row[t]),Object.assign(this.row,e)),t.$el.dispatchEvent(new CustomEvent("change",{detail:{key:this.key,value:e}})),this.clockEntries=null,await this.loadClock()}catch(e){console.error("Save row error",e),t.$ok.shared?.toast?.error?.(e.message||"Save failed")}finally{this.busySave=!1}}},async remove(){this.busyRemove=!0;try{const e=await fetch(`${this.api}/type/${this.type}/item/${this.key}`,{method:"DELETE"});if(!e.ok)throw new Error(e.statusText);t.$ok.shared.toast?.info(`${this.key} deleted`),t.$el.dispatchEvent(new CustomEvent("remove",{detail:{key:this.key,value:this.row}})),await t.$ok.stores.okdb.reloadTypes()}catch(t){console.error("Remove row error",t)}finally{this.busyRemove=!1}}}),async init(t){t.$watch(()=>`${t.$attr.type}|${t.$attr.key}`,()=>{this.tmSelectedClock=null,this.tmValue=null,this.tmFetchBusy=!1,this.tmDiffs=[]}),await Promise.all([this.loadClock(),this.loadTTL()])},template:'\n <transition name="grow">\n <div>\n \x3c!-- Clock + TTL badges row --\x3e\n <div style="display:flex; align-items:center; gap:6px; margin-bottom:8px;">\n <button\n @click="toggleClock()"\n style="font-size:10px; padding:1px 7px; border-radius:4px; border:1px solid var(--okdb-app-border); background:var(--okdb-app-bg-deep); color:var(--okdb-app-fg-muted); cursor:pointer; font-family:monospace; display:flex; align-items:center; gap:5px;"\n title="Show change history for this record"\n >\n <if :="clockBusy">\n <span>clock …</span>\n </if>\n <if :="!clockBusy && latestClock != null">\n <span>clock: {{ latestClock }}</span>\n <if :="latestTime">\n <span style="opacity:.55; font-family:sans-serif; font-size:10px;">· {{ latestTime }}</span>\n </if>\n <span style="opacity:.4;">{{ clockOpen ? \'▲\' : \'▼\' }}</span>\n </if>\n <if :="!clockBusy && latestClock == null">\n <span style="opacity:.5;">no clock</span>\n </if>\n </button>\n\n <button\n @click="ttlOpen = !ttlOpen"\n :style="\'font-size:10px; padding:1px 7px; border-radius:4px; border:1px solid \' + (ttlInfo ? ttlBadgeColor + \'55\' : \'var(--okdb-app-border)\') + \'; background:var(--okdb-app-bg-deep); color:\' + ttlBadgeColor + \'; cursor:pointer; font-family:monospace; display:flex; align-items:center; gap:5px;\'"\n title="TTL (time-to-live) for this record"\n >\n <span>{{ ttlBadgeText }}</span>\n <if :="ttlInfo">\n <span style="opacity:.4;">{{ ttlOpen ? \'▲\' : \'▼\' }}</span>\n </if>\n </button>\n </div>\n\n \x3c!-- Changelog + time-travel panel (opens when clock badge clicked) --\x3e\n <if :="clockOpen">\n <div style="margin-bottom:10px;">\n \x3c!-- Changelog table --\x3e\n <if :="clockEntries && clockEntries.length > 0">\n <div style="margin-bottom:8px; background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px; overflow:hidden; font-size:11px;">\n <div style="display:grid; grid-template-columns:auto auto 1fr auto; gap:0;">\n <div style="padding:4px 10px; opacity:.4; border-bottom:1px solid var(--okdb-app-border); font-weight:600;">clock</div>\n <div style="padding:4px 10px; opacity:.4; border-bottom:1px solid var(--okdb-app-border); font-weight:600;">action</div>\n <div style="padding:4px 10px; opacity:.4; border-bottom:1px solid var(--okdb-app-border); font-weight:600;">wall time</div>\n <div style="padding:4px 10px; opacity:.4; border-bottom:1px solid var(--okdb-app-border); font-weight:600;">origin</div>\n <each :of="clockEntries" let:item="entry">\n <div style="padding:3px 10px; font-family:monospace; opacity:.8; border-bottom:1px solid var(--okdb-app-surface);">{{ entry.clock ?? \'—\' }}</div>\n <div style="padding:3px 10px; border-bottom:1px solid var(--okdb-app-surface);">\n <span :style="\'color:\' + (entry.action === \'put\' ? \'var(--okdb-success)\' : entry.action === \'remove\' ? \'var(--okdb-danger)\' : \'var(--okdb-warning)\')">{{ entry.action }}</span>\n </div>\n <div style="padding:3px 10px; opacity:.6; border-bottom:1px solid var(--okdb-app-surface); white-space:nowrap;">{{ hlcToDate(entry.timestamp) }}</div>\n <div style="padding:3px 10px; font-family:monospace; opacity:.35; font-size:10px; border-bottom:1px solid var(--okdb-app-surface); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:120px;" :title="entry.origin">{{ (entry.origin || \'—\').slice(0, 8) }}</div>\n </each>\n </div>\n </div>\n </if>\n \x3c!-- Time-travel scrubber (sits outside overflow:hidden so tooltips aren\'t clipped) --\x3e\n <tm-timeline\n :type="type"\n :item-key="key"\n :env="$ok.stores.okdb.selectedEnv || \'default\'"\n :selected-clock="tmSelectedClock"\n @tm-clock-select="onTMClockSelect($event.detail)"\n @tm-diffs-loaded="tmDiffs = $event.detail.diffs"\n />\n </div>\n </if>\n\n \x3c!-- TTL detail panel --\x3e\n <if :="ttlOpen">\n <div style="margin-bottom:10px; background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px; padding:10px; font-size:11px;">\n <div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">\n <span style="font-weight:600; opacity:.6;">Time-to-Live</span>\n <if :="ttlInfo && ttlInfo.expiresAt">\n <span :style="\'color:\' + ttlBadgeColor + \'; font-family:monospace;\'">{{ fmtDuration(ttlInfo.remainingMs) }} remaining</span>\n <span style="opacity:.35; font-size:10px;">· expires {{ new Date(ttlInfo.expiresAt).toLocaleString() }}</span>\n </if>\n <if :="!ttlInfo">\n <span style="opacity:.4;">No TTL set — this item will not expire.</span>\n </if>\n </div>\n <div style="display:flex; gap:6px; align-items:center; flex-wrap:wrap;">\n <select\n @change="onTTLPresetChange($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 <each :of="ttlPresets" let:item="p">\n <option :value="String(p.ms)" :selected="ttlPreset === String(p.ms)">{{ p.label }}</option>\n </each>\n </select>\n <input\n type="number"\n !value="ttlInputMs"\n @input="(ttlInputMs = $el.value, ttlPreset = \'\')"\n placeholder="ms"\n style="width:100px; 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="setItemTTL()" :disabled="ttlBusy || !ttlInputMs" style="font-size:11px; padding:2px 10px; color:var(--okdb-success); border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);">Set TTL</button>\n <if :="ttlInfo">\n <button @click="clearItemTTL()" :disabled="ttlBusy" style="font-size:11px; padding:2px 10px; color:var(--okdb-danger); border-color:color-mix(in srgb, var(--okdb-danger) 33%, transparent);">Clear TTL</button>\n </if>\n <button @click="loadTTL()" :disabled="ttlBusy" style="font-size:11px; padding:2px 8px; opacity:.5;" title="Refresh TTL info">↻</button>\n </div>\n </div>\n </if>\n\n <div>\n \x3c!-- JSON header with historical clock badge + prev/next navigation --\x3e\n <div style="display:flex; align-items:center; gap:5px; margin-bottom:4px;">\n <if :="tmSelectedClock !== null">\n <button\n @click="goPrev()"\n :disabled="!canGoPrev"\n :style="\'font-size:12px; padding:0 6px; height:20px; line-height:1; border-radius:4px; border:1px solid var(--okdb-app-border); background:var(--okdb-app-bg-deep); color:var(--okdb-app-fg-muted); cursor:pointer; font-family:monospace; opacity:\' + (canGoPrev ? \'1\' : \'.3\') + \'; transition:opacity 120ms;\'"\n title="Previous snapshot"\n >&#8592;</button>\n <span style="display:inline-flex; align-items:center; gap:6px; font-size:10px; padding:1px 8px; border-radius:4px; background:color-mix(in srgb, var(--okdb-accent) 12%, transparent); color:var(--okdb-accent); font-family:monospace;">\n <span>clock {{ tmSelectedClock }}</span>\n <if :="tmSelectedTime">\n <span style="opacity:.6; font-family:sans-serif; font-weight:400;">{{ tmSelectedTime }}</span>\n </if>\n </span>\n <button\n @click="goNext()"\n :disabled="!canGoNext"\n :style="\'font-size:12px; padding:0 6px; height:20px; line-height:1; border-radius:4px; border:1px solid var(--okdb-app-border); background:var(--okdb-app-bg-deep); color:var(--okdb-app-fg-muted); cursor:pointer; font-family:monospace; opacity:\' + (canGoNext ? \'1\' : \'.3\') + \'; transition:opacity 120ms;\'"\n title="Next snapshot"\n >&#8594;</button>\n </if>\n </div>\n\n <div style="position:relative;">\n <ok-json-input ref="editor" :value="tmSelectedClock !== null ? tmValue : row" @edit="edited=$event.detail" @parse-error="jsonError=$event.detail" style="min-height: 300px"></ok-json-input>\n <if :="tmFetchBusy">\n <div\n style="position:absolute; inset:0; background:var(--okdb-app-bg)88; display:flex; align-items:center; justify-content:center; border-radius:6px; z-index:2;"\n >\n <span style="font-size:12px; opacity:.6;">loading…</span>\n </div>\n </if>\n </div>\n\n <div style="margin-top:8px; display:flex; gap:8px;">\n <button @click="reset" :disabled="!hasEditedValue">Reset</button>\n <button @click="save" :disabled="!hasEditedValue || busySave">Save</button>\n <button @click="remove" :disabled="busyRemove">Delete</button>\n </div>\n </div>\n </div>\n </transition>\n '};
1
+ import t from"../../../components/ok-json-input.ok.js";import e from"./tm-timeline.ok.js";function o(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}export default{tag:"edit-row",dependencies:[t,e],attr:{type:{},key:{},row:{}},context:t=>({get api(){return t.$ok.stores.okdb.apiBase},get type(){return t.$attr.type},get key(){return t.$attr.key},get row(){return t.$attr.row},get hasEditedValue(){return void 0!==this.edited&&!this.jsonError},edited:void 0,jsonError:!1,busySave:!1,busyRemove:!1,tmSelectedClock:null,tmFetchBusy:!1,tmValue:null,tmDiffs:[],get tmDiffClocks(){return this.tmDiffs.map(t=>t.clock)},get tmSelectedDiff(){return null===this.tmSelectedClock?null:this.tmDiffs.find(t=>t.clock===this.tmSelectedClock)??null},get tmSelectedTime(){const t=this.tmSelectedDiff?.timestamp;return t?new Date(t).toLocaleString("en-GB",{day:"numeric",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"}):null},get tmCurrentIdx(){return null===this.tmSelectedClock?-1:this.tmDiffClocks.indexOf(this.tmSelectedClock)},get canGoPrev(){return this.tmCurrentIdx>0},get canGoNext(){return-1!==this.tmCurrentIdx&&this.tmCurrentIdx<this.tmDiffClocks.length-1},goPrev(){this.canGoPrev&&this.onTMClockSelect({clock:this.tmDiffClocks[this.tmCurrentIdx-1]})},goNext(){this.canGoNext&&this.onTMClockSelect({clock:this.tmDiffClocks[this.tmCurrentIdx+1]})},clockOpen:!1,clockBusy:!1,clockEntries:null,ttlInfo:null,ttlBusy:!1,ttlOpen:!1,ttlInputMs:"",ttlPreset:"",ttlPresets:[{label:"1 min",ms:6e4},{label:"5 min",ms:3e5},{label:"15 min",ms:9e5},{label:"1 hr",ms:36e5},{label:"6 hr",ms:216e5},{label:"24 hr",ms:864e5},{label:"7 days",ms:6048e5},{label:"30 days",ms:2592e6}],async loadTTL(){if(!this.ttlBusy){this.ttlBusy=!0;try{const t=await fetch(`${this.api}/type/${encodeURIComponent(this.type)}/item/${encodeURIComponent(this.key)}/ttl`),e=await t.json(),o=e.data??e.result??null;this.ttlInfo=o||!1}catch{this.ttlInfo=!1}finally{this.ttlBusy=!1}}},async setItemTTL(){const e=parseInt(this.ttlInputMs);if(e&&!(e<=0)){this.ttlBusy=!0;try{const o=await fetch(`${this.api}/type/${encodeURIComponent(this.type)}/item/${encodeURIComponent(this.key)}/ttl`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({ttl:e})});if(!o.ok){const t=await o.json().catch(()=>({}));throw new Error(t?.error?.message||o.statusText)}t.$ok.shared?.toast?.info?.(`TTL set (${this.fmtDuration(e)})`),await this.loadTTL()}catch(e){t.$ok.shared?.toast?.error?.(e.message||"Failed to set TTL")}finally{this.ttlBusy=!1}}},async clearItemTTL(){this.ttlBusy=!0;try{const e=await fetch(`${this.api}/type/${encodeURIComponent(this.type)}/item/${encodeURIComponent(this.key)}/ttl`,{method:"DELETE"});if(!e.ok)throw new Error(e.statusText);this.ttlInfo=!1,this.ttlInputMs="",this.ttlPreset="",t.$ok.shared?.toast?.info?.("TTL cleared")}catch(e){t.$ok.shared?.toast?.error?.(e.message||"Failed to clear TTL")}finally{this.ttlBusy=!1}},onTTLPresetChange(t){this.ttlPreset=t,t&&(this.ttlInputMs=t)},fmtDuration(t){if(null==t)return"—";const e=Math.floor(t/1e3);return e<60?e+"s":e<3600?Math.floor(e/60)+"m "+e%60+"s":e<86400?Math.floor(e/3600)+"h "+Math.floor(e%3600/60)+"m":Math.floor(e/86400)+"d "+Math.floor(e%86400/3600)+"h"},get ttlBadgeText(){return this.ttlBusy?"ttl …":this.ttlInfo?this.ttlInfo.remainingMs<=0?"ttl: expired":"ttl: "+this.fmtDuration(this.ttlInfo.remainingMs):"no ttl"},get ttlBadgeColor(){return this.ttlInfo?this.ttlInfo.remainingMs<=0?"var(--okdb-danger)":this.ttlInfo.remainingMs<6e4?"var(--okdb-orange)":"var(--okdb-success)":"var(--okdb-app-fg-muted)"},hlcToDate(t){const e=Number(t)||0,o=(e-e%1024)/1024;return o?new Date(o).toLocaleString():"—"},get latestClock(){return this.clockEntries?.length?this.clockEntries[0].clock??null:null},get latestTime(){if(!this.clockEntries?.length)return null;const t=this.clockEntries[0].timestamp;return t?this.hlcToDate(t):null},async toggleClock(){this.clockOpen=!this.clockOpen},async loadClock(){if(!this.clockBusy){this.clockBusy=!0;try{const t=await fetch(`${this.api}/type/${encodeURIComponent(this.type)}/item/${encodeURIComponent(this.key)}/changelog?limit=20`),e=await t.json();this.clockEntries=e.data??[]}catch{this.clockEntries=[]}finally{this.clockBusy=!1}}},async onTMClockSelect({clock:t}){if(this.tmSelectedClock=t,null===t)return this.tmValue=null,this.tmFetchBusy=!1,void(this.edited=void 0);this.tmFetchBusy=!0;try{const e=await fetch(`${this.api}/time-machine/${encodeURIComponent(this.type)}/${encodeURIComponent(this.key)}/at/${t}`),o=await e.json();if(!e.ok)throw new Error(o?.error?.message||"Failed");this.tmValue=o.result?.value??o.data?.value??null,this.edited=JSON.parse(JSON.stringify(this.tmValue))}catch{this.tmSelectedClock=null,this.tmValue=null}finally{this.tmFetchBusy=!1}},reset(){this.edited=void 0,this.jsonError=!1,t.$refs.editor.reset?.()},async save(){if(void 0!==this.edited&&!this.jsonError){if(this.type.startsWith("~")&&!await t.$ok.shared.modal.confirm(`Editing "${this.type}" can corrupt the system. The queue, license checker, and other features rely on the exact shape of these records. Proceed?`,{title:"Edit system record",confirmText:"Save anyway",confirmClass:"destructive"}))return;this.busySave=!0;try{const e=this.edited,n=await fetch(`${this.api}/type/${this.type}/item/${this.key}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok){const t=await n.json().catch(()=>({})),e=t?.error?.message||t?.message||t?.error||n.statusText;throw new Error("string"==typeof e?e:n.statusText)}o(this.row)&&o(e)&&(Object.keys(this.row).forEach(t=>delete this.row[t]),Object.assign(this.row,e)),t.$el.dispatchEvent(new CustomEvent("change",{detail:{key:this.key,value:e}})),this.clockEntries=null,await this.loadClock()}catch(e){console.error("Save row error",e),t.$ok.shared?.toast?.error?.(e.message||"Save failed")}finally{this.busySave=!1}}},async remove(){if(!this.type.startsWith("~")||await t.$ok.shared.modal.confirm(`Deleting "${this.key}" from "${this.type}" can corrupt the system. Proceed?`,{title:"Delete system record",confirmText:"Delete",confirmClass:"destructive"})){this.busyRemove=!0;try{const e=await fetch(`${this.api}/type/${this.type}/item/${this.key}`,{method:"DELETE"});if(!e.ok)throw new Error(e.statusText);t.$ok.shared.toast?.info(`${this.key} deleted`),t.$el.dispatchEvent(new CustomEvent("remove",{detail:{key:this.key,value:this.row}})),await t.$ok.stores.okdb.reloadTypes()}catch(t){console.error("Remove row error",t)}finally{this.busyRemove=!1}}}}),async init(t){t.$watch(()=>`${t.$attr.type}|${t.$attr.key}`,()=>{this.tmSelectedClock=null,this.tmValue=null,this.tmFetchBusy=!1,this.tmDiffs=[]}),await Promise.all([this.loadClock(),this.loadTTL()])},template:'\n <transition name="grow">\n <div>\n \x3c!-- Clock + TTL badges row --\x3e\n <div style="display:flex; align-items:center; gap:6px; margin-bottom:8px;">\n <button\n @click="toggleClock()"\n style="font-size:10px; padding:1px 7px; border-radius:4px; border:1px solid var(--okdb-app-border); background:var(--okdb-app-bg-deep); color:var(--okdb-app-fg-muted); cursor:pointer; font-family:monospace; display:flex; align-items:center; gap:5px;"\n title="Show change history for this record"\n >\n <if :="clockBusy">\n <span>clock …</span>\n </if>\n <if :="!clockBusy && latestClock != null">\n <span>clock: {{ latestClock }}</span>\n <if :="latestTime">\n <span style="opacity:.55; font-family:sans-serif; font-size:10px;">· {{ latestTime }}</span>\n </if>\n <span style="opacity:.4;">{{ clockOpen ? \'▲\' : \'▼\' }}</span>\n </if>\n <if :="!clockBusy && latestClock == null">\n <span style="opacity:.5;">no clock</span>\n </if>\n </button>\n\n <button\n @click="ttlOpen = !ttlOpen"\n :style="\'font-size:10px; padding:1px 7px; border-radius:4px; border:1px solid \' + (ttlInfo ? ttlBadgeColor + \'55\' : \'var(--okdb-app-border)\') + \'; background:var(--okdb-app-bg-deep); color:\' + ttlBadgeColor + \'; cursor:pointer; font-family:monospace; display:flex; align-items:center; gap:5px;\'"\n title="TTL (time-to-live) for this record"\n >\n <span>{{ ttlBadgeText }}</span>\n <if :="ttlInfo">\n <span style="opacity:.4;">{{ ttlOpen ? \'▲\' : \'▼\' }}</span>\n </if>\n </button>\n </div>\n\n \x3c!-- Changelog + time-travel panel (opens when clock badge clicked) --\x3e\n <if :="clockOpen">\n <div style="margin-bottom:10px;">\n \x3c!-- Changelog table --\x3e\n <if :="clockEntries && clockEntries.length > 0">\n <div style="margin-bottom:8px; background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px; overflow:hidden; font-size:11px;">\n <div style="display:grid; grid-template-columns:auto auto 1fr auto; gap:0;">\n <div style="padding:4px 10px; opacity:.4; border-bottom:1px solid var(--okdb-app-border); font-weight:600;">clock</div>\n <div style="padding:4px 10px; opacity:.4; border-bottom:1px solid var(--okdb-app-border); font-weight:600;">action</div>\n <div style="padding:4px 10px; opacity:.4; border-bottom:1px solid var(--okdb-app-border); font-weight:600;">wall time</div>\n <div style="padding:4px 10px; opacity:.4; border-bottom:1px solid var(--okdb-app-border); font-weight:600;">origin</div>\n <each :of="clockEntries" let:item="entry">\n <div style="padding:3px 10px; font-family:monospace; opacity:.8; border-bottom:1px solid var(--okdb-app-surface);">{{ entry.clock ?? \'—\' }}</div>\n <div style="padding:3px 10px; border-bottom:1px solid var(--okdb-app-surface);">\n <span :style="\'color:\' + (entry.action === \'put\' ? \'var(--okdb-success)\' : entry.action === \'remove\' ? \'var(--okdb-danger)\' : \'var(--okdb-warning)\')">{{ entry.action }}</span>\n </div>\n <div style="padding:3px 10px; opacity:.6; border-bottom:1px solid var(--okdb-app-surface); white-space:nowrap;">{{ hlcToDate(entry.timestamp) }}</div>\n <div style="padding:3px 10px; font-family:monospace; opacity:.35; font-size:10px; border-bottom:1px solid var(--okdb-app-surface); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:120px;" :title="entry.origin">{{ (entry.origin || \'—\').slice(0, 8) }}</div>\n </each>\n </div>\n </div>\n </if>\n \x3c!-- Time-travel scrubber (sits outside overflow:hidden so tooltips aren\'t clipped) --\x3e\n <tm-timeline\n :type="type"\n :item-key="key"\n :env="$ok.stores.okdb.selectedEnv || \'default\'"\n :selected-clock="tmSelectedClock"\n @tm-clock-select="onTMClockSelect($event.detail)"\n @tm-diffs-loaded="tmDiffs = $event.detail.diffs"\n />\n </div>\n </if>\n\n \x3c!-- TTL detail panel --\x3e\n <if :="ttlOpen">\n <div style="margin-bottom:10px; background:var(--okdb-app-bg-deep); border:1px solid var(--okdb-app-border); border-radius:6px; padding:10px; font-size:11px;">\n <div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">\n <span style="font-weight:600; opacity:.6;">Time-to-Live</span>\n <if :="ttlInfo && ttlInfo.expiresAt">\n <span :style="\'color:\' + ttlBadgeColor + \'; font-family:monospace;\'">{{ fmtDuration(ttlInfo.remainingMs) }} remaining</span>\n <span style="opacity:.35; font-size:10px;">· expires {{ new Date(ttlInfo.expiresAt).toLocaleString() }}</span>\n </if>\n <if :="!ttlInfo">\n <span style="opacity:.4;">No TTL set — this item will not expire.</span>\n </if>\n </div>\n <div style="display:flex; gap:6px; align-items:center; flex-wrap:wrap;">\n <select\n @change="onTTLPresetChange($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 <each :of="ttlPresets" let:item="p">\n <option :value="String(p.ms)" :selected="ttlPreset === String(p.ms)">{{ p.label }}</option>\n </each>\n </select>\n <input\n type="number"\n !value="ttlInputMs"\n @input="(ttlInputMs = $el.value, ttlPreset = \'\')"\n placeholder="ms"\n style="width:100px; 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="setItemTTL()" :disabled="ttlBusy || !ttlInputMs" style="font-size:11px; padding:2px 10px; color:var(--okdb-success); border-color:color-mix(in srgb, var(--okdb-success) 33%, transparent);">Set TTL</button>\n <if :="ttlInfo">\n <button @click="clearItemTTL()" :disabled="ttlBusy" style="font-size:11px; padding:2px 10px; color:var(--okdb-danger); border-color:color-mix(in srgb, var(--okdb-danger) 33%, transparent);">Clear TTL</button>\n </if>\n <button @click="loadTTL()" :disabled="ttlBusy" style="font-size:11px; padding:2px 8px; opacity:.5;" title="Refresh TTL info">↻</button>\n </div>\n </div>\n </if>\n\n <div>\n \x3c!-- JSON header with historical clock badge + prev/next navigation --\x3e\n <div style="display:flex; align-items:center; gap:5px; margin-bottom:4px;">\n <if :="tmSelectedClock !== null">\n <button\n @click="goPrev()"\n :disabled="!canGoPrev"\n :style="\'font-size:12px; padding:0 6px; height:20px; line-height:1; border-radius:4px; border:1px solid var(--okdb-app-border); background:var(--okdb-app-bg-deep); color:var(--okdb-app-fg-muted); cursor:pointer; font-family:monospace; opacity:\' + (canGoPrev ? \'1\' : \'.3\') + \'; transition:opacity 120ms;\'"\n title="Previous snapshot"\n >&#8592;</button>\n <span style="display:inline-flex; align-items:center; gap:6px; font-size:10px; padding:1px 8px; border-radius:4px; background:color-mix(in srgb, var(--okdb-accent) 12%, transparent); color:var(--okdb-accent); font-family:monospace;">\n <span>clock {{ tmSelectedClock }}</span>\n <if :="tmSelectedTime">\n <span style="opacity:.6; font-family:sans-serif; font-weight:400;">{{ tmSelectedTime }}</span>\n </if>\n </span>\n <button\n @click="goNext()"\n :disabled="!canGoNext"\n :style="\'font-size:12px; padding:0 6px; height:20px; line-height:1; border-radius:4px; border:1px solid var(--okdb-app-border); background:var(--okdb-app-bg-deep); color:var(--okdb-app-fg-muted); cursor:pointer; font-family:monospace; opacity:\' + (canGoNext ? \'1\' : \'.3\') + \'; transition:opacity 120ms;\'"\n title="Next snapshot"\n >&#8594;</button>\n </if>\n </div>\n\n <div style="position:relative;">\n <ok-json-input ref="editor" :value="tmSelectedClock !== null ? tmValue : row" @edit="edited=$event.detail" @parse-error="jsonError=$event.detail" style="min-height: 300px"></ok-json-input>\n <if :="tmFetchBusy">\n <div\n style="position:absolute; inset:0; background:var(--okdb-app-bg)88; display:flex; align-items:center; justify-content:center; border-radius:6px; z-index:2;"\n >\n <span style="font-size:12px; opacity:.6;">loading…</span>\n </div>\n </if>\n </div>\n\n <div style="margin-top:8px; display:flex; gap:8px;">\n <button @click="reset" :disabled="!hasEditedValue">Reset</button>\n <button @click="save" :disabled="!hasEditedValue || busySave">Save</button>\n <button @click="remove" :disabled="busyRemove">Delete</button>\n </div>\n </div>\n </div>\n </transition>\n '};