@kedem/okdb 1.8.14 → 1.8.16

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.
@@ -1 +1 @@
1
- import e from"@toolkit/components/ok-chart.ok.js";export default{tag:"view-detail",dependencies:[e],attr:{name:{}},context:e=>({output:null,meta:null,definition:null,loading:!0,error:null,actionLoading:!1,showDeleteConfirm:!1,showOrphanedIndexConfirm:!1,orphanedIndexes:[],rangeData:[],rangeLoading:!1,rangeError:null,rangeFrom:"",rangeTo:"",rangeIncludePartial:!1,rangePreset:"30d",bucketsData:[],bucketsLoading:!1,bucketsError:null,async load(){this.loading=!0,this.error=null,this.output=null,this.meta=null,this.definition=null;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,[r,o,i]=await Promise.all([fetch(`${t}/views/${a}`),fetch(`${t}/views/${a}/meta`),fetch(`${t}/views/${a}/definition`)]),n=r.ok?await r.json():null,s=o.ok?await o.json():null,d=i.ok?await i.json():null;this.output=n?.data??n?.result??null,this.meta=s?.data??s?.result??null,this.definition=d?.data??d?.result??null,this.isBucketed&&(this.rangePreset="30d",this.rangeFrom=new Date(Date.now()-2592e6).toISOString().split("T")[0],this.rangeTo="",this.loadRange(),this.loadBuckets())}catch(e){this.error=e.message}finally{this.loading=!1}},async doAction(t){this.actionLoading=!0;try{const a=e.$ok.stores.okdb.apiBase,r=e.$attr.name,o=await fetch(`${a}/views/${r}/${t}`,{method:"POST"});if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message??`${t} failed`)}await this.load(),e.$dispatch("state-changed")}catch(e){this.error=e.message}finally{this.actionLoading=!1}},async deleteView(){this.actionLoading=!0;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,r=await fetch(`${t}/views/${a}`,{method:"DELETE"});if(!r.ok){const e=await r.json().catch(()=>({})),t=e?.error?.code;if("VIEW_HAS_ORPHANED_INDEXES"===t)return this.orphanedIndexes=e?.error?.details?.orphanedIndexes??[],this.showDeleteConfirm=!1,void(this.showOrphanedIndexConfirm=!0);throw new Error(e?.error?.message??"Delete failed")}e.$dispatch("removed")}catch(e){this.error=e.message}finally{this.actionLoading=!1,this.showOrphanedIndexConfirm||(this.showDeleteConfirm=!1)}},async deleteViewWithIndexes(t){this.actionLoading=!0;try{const a=e.$ok.stores.okdb.apiBase,r=e.$attr.name,o=await fetch(`${a}/views/${r}`,{method:"DELETE",headers:{"Content-Type":"application/json"},body:JSON.stringify({managedIndexes:t})});if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message??"Delete failed")}e.$dispatch("removed")}catch(e){this.error=e.message}finally{this.actionLoading=!1,this.showOrphanedIndexConfirm=!1,this.orphanedIndexes=[]}},formatJson(e){if(null==e)return"";try{return JSON.stringify(e,null,2)}catch{return String(e)}},stateColor(e){switch(e){case"ready":return"#4caf50";case"creating":case"resetting":case"partially-ready":return"#4FC1FF";case"halted":return"#f44336";case"stopped":return"#fbbf24";default:return"#666"}},get state(){return this.meta?.state??"unknown"},get isInProgress(){return"creating"===this.state||"resetting"===this.state||"partially-ready"===this.state},get sections(){return this.meta?.sections??null},get refSections(){const e=this.sections?.refs??{};return Object.entries(e).map(([e,t])=>({name:e,...t}))},get isBucketed(){return this.definition&&this.definition.bucket},get chartType(){return this.isBucketed?"time"===this.definition.bucket.preset?"line":"bar":null},get chartData(){const e=this.rangeData||[];if(e.length<2)return null;if(!this.isBucketed)return null;const t=e.map(e=>e.bucketKey),a=e[0]?.reducers||{};return{labels:t,datasets:Object.entries(a).filter(([,e])=>e&&"number"==typeof e.value).map(([e])=>e).map(t=>({label:t,data:e.map(e=>{const a=e.reducers[t];return a&&"number"==typeof a.value?a.value:null})}))}},setQuickRange(e){const t=new Date,a=e=>new Date(t-864e5*e).toISOString().split("T")[0];switch(this.rangePreset=e,e){case"7d":this.rangeFrom=a(7),this.rangeTo="";break;case"30d":this.rangeFrom=a(30),this.rangeTo="";break;case"90d":this.rangeFrom=a(90),this.rangeTo="";break;case"1y":this.rangeFrom=a(365),this.rangeTo="";break;case"all":this.rangeFrom="",this.rangeTo=""}this.loadRange()},shiftRange(e){const t=e=>e?new Date(e+"T00:00:00Z"):null,a=e=>e.toISOString().split("T")[0],r=new Date;let o=t(this.rangeFrom),i=t(this.rangeTo);o||i?o?i||(i=new Date(r)):o=new Date(i-2592e6):(i=r,o=new Date(r-2592e6));const n=i-o;this.rangeFrom=a(new Date(o.getTime()+e*n)),this.rangeTo=a(new Date(i.getTime()+e*n)),this.rangePreset="",this.loadRange()},async loadRange(){if(this.isBucketed){this.rangeLoading=!0,this.rangeError=null;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,r=new URLSearchParams;this.rangeFrom&&r.set("from",this.rangeFrom),this.rangeTo&&r.set("to",this.rangeTo),this.rangeIncludePartial&&r.set("includePartial","true");const o=this.definition.bucket.granularity;r.set("granularity",o);const i=await fetch(`${t}/views/${a}/range?${r}`);if(!i.ok){const e=await i.json().catch(()=>({}));throw new Error(e?.error?.message??"Range fetch failed")}const n=await i.json();this.rangeData=n.data??n.result??[]}catch(e){this.rangeError=e.message,this.rangeData=[]}finally{this.rangeLoading=!1}}},async loadBuckets(){if(this.isBucketed){this.bucketsLoading=!0,this.bucketsError=null;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,r=encodeURIComponent(this.definition.bucket.granularity),o=await fetch(`${t}/views/${a}/buckets?granularity=${r}&limit=10000`);if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message??"Bucket list failed")}const i=await o.json();this.bucketsData=i.data??i.result??[]}catch(e){this.bucketsError=e.message,this.bucketsData=[]}finally{this.bucketsLoading=!1}}},get rangeColumns(){return this.rangeData.length?Object.keys(this.rangeData[0].reducers||{}):[]},reducerDisplayValue(e,t){const a=e.reducers?.[t];if(null==a)return"";if("object"==typeof a&&"number"==typeof a.value)return a.value;if("object"==typeof a&&!Array.isArray(a)){const e=Object.entries(a);if(!e.length)return"—";const t=e[0][1];return"object"==typeof t&&"number"==typeof t.value?e.sort((e,t)=>(t[1].value??0)-(e[1].value??0)).slice(0,3).map(([e,t])=>`${e}: ${t.value}`).join(" · ")+(e.length>3?" +"+(e.length-3):""):e.map(([e,t])=>{const a=Object.values(t||{})[0];return`${e}: ${"number"==typeof a?.value?a.value:"?"}`}).slice(0,3).join(" · ")+(e.length>3?" +"+(e.length-3):"")}return String(a)},get rangeTotals(){if(!this.rangeData.length)return null;const e=this.rangeColumns;if(!e.length)return null;const t=this.definition?.reduce||{},a={};for(const r of e){const e=t[r],o=e?Object.keys(e).find(e=>"items"!==e):null,i=this.rangeData.map(e=>e.reducers?.[r]).filter(e=>null!=e&&"number"==typeof e.value).map(e=>e.value);if(!i.length){a[r]=null;continue}const n=e=>"number"==typeof e&&isFinite(e)?parseFloat(e.toPrecision(10)):e;a[r]="$min"===o?n(Math.min(...i)):"$max"===o?n(Math.max(...i)):"$avg"===o?{approx:n(i.reduce((e,t)=>e+t,0)/i.length)}:n(i.reduce((e,t)=>e+t,0))}return a},rangeTotalDisplay(e){const t=this.rangeTotals;if(!t)return"—";const a=t[e];return null==a?"—":"object"==typeof a&&null!=a.approx?"~"+Math.round(1e3*a.approx)/1e3:a}}),mount(e){e.$watch(()=>e.$attr.name,()=>{this.loading=!0,this.output=null,this.meta=null,this.definition=null,this.showDeleteConfirm=!1,this.showOrphanedIndexConfirm=!1,this.orphanedIndexes=[],this.load()}),this.load()},style:"view-detail{gap:0;height:100%;overflow-y:auto}.vd,.vd__header,view-detail{display:flex;flex-direction:column}.vd{gap:20px;padding:20px 24px}.vd__header{gap:10px;padding-bottom:16px;border-bottom:1px solid var(--okdb-app-border)}.vd__title-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.vd__name,.vd__state-badge{font-weight:600;white-space:nowrap}.vd__name{font-size:20px;color:var(--okdb-app-fg-strong);margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;flex-shrink:1}.vd__state-badge{display:inline-flex;align-items:center;gap:6px;padding:3px 8px;border-radius:999px;border:1px solid var(--okdb-app-border);font-size:11px;text-transform:uppercase;letter-spacing:.04em}.vd__state-dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0}.vd__type-label{font-size:11px;color:var(--okdb-app-fg-subtle);font-family:monospace}.vd__actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.vd__btn{padding:5px 12px;border-radius:4px;font-size:12px;font-weight:600;border:1px solid;background:0 0;cursor:pointer;transition:all .15s ease}.vd__btn--primary{border-color:var(--okdb-accent);color:var(--okdb-accent)}.vd__btn--primary:hover{background:color-mix(in srgb,var(--okdb-accent) 10%,transparent);transform:none;box-shadow:none}.vd__btn--warning{border-color:var(--okdb-warning);color:var(--okdb-warning)}.vd__btn--warning:hover{background:color-mix(in srgb,var(--okdb-warning) 10%,transparent);transform:none;box-shadow:none}.vd__btn--danger{border-color:var(--okdb-danger);color:var(--okdb-danger)}.vd__btn--danger:hover{background:color-mix(in srgb,var(--okdb-danger) 10%,transparent);transform:none;box-shadow:none}.vd__btn:disabled{opacity:.4;cursor:not-allowed}.vd__delete-confirm{display:flex;align-items:center;gap:10px;padding:8px 12px;background:color-mix(in srgb,var(--okdb-danger) 7%,transparent);border:1px solid color-mix(in srgb,var(--okdb-danger) 25%,transparent);border-radius:5px;font-size:12px;color:var(--okdb-danger);flex-wrap:wrap}.vd__delete-confirm-text{flex:1;min-width:0}.vd__delete-confirm-actions{display:flex;gap:6px}.vd__section{display:flex;flex-direction:column;gap:10px}.vd__section-title{font-size:11px;font-weight:600;color:var(--okdb-app-fg-muted);text-transform:uppercase;letter-spacing:.05em}.vd__pre{font-family:monospace;font-size:12px;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:5px;padding:12px 14px;overflow-x:auto;color:var(--okdb-app-fg);white-space:pre;margin:0;line-height:1.5}.vd__meta-grid{display:flex;flex-direction:column;gap:6px;font-size:12px}.vd__meta-row{display:flex;gap:8px}.vd__meta-key{color:var(--okdb-app-fg-subtle);width:80px;flex-shrink:0}.vd__meta-val{color:var(--okdb-app-fg);font-family:monospace;word-break:break-all}.vd__meta-val--error{color:var(--okdb-danger)}.vd__empty,.vd__error{font-size:12px;color:var(--okdb-app-fg-subtle)}.vd__error{color:var(--okdb-danger);padding:8px;background:color-mix(in srgb,var(--okdb-danger) 7%,transparent);border:1px solid color-mix(in srgb,var(--okdb-danger) 20%,transparent);border-radius:4px}.vd__loading{font-size:12px;opacity:.5;padding:40px;text-align:center}.vd__refs{display:flex;flex-direction:column;gap:4px;margin-top:4px}.vd__ref-row{display:flex;gap:8px;font-size:11px;padding:4px 8px;background:var(--okdb-app-bg-deep);border-radius:3px;border:1px solid var(--okdb-app-border)}.vd__ref-name{color:var(--okdb-sky);font-family:monospace;flex:1}.vd__ref-state{color:var(--okdb-app-fg-subtle)}",template:'<div class="vd"><if :="loading"><div class="vd__loading">Loading…</div></if><if :="!loading"><if :="error"><div class="vd__error">{{ error }}</div></if><div class="vd__header"><div class="vd__title-row"><div style="flex:1; min-width:0; display:flex; align-items:baseline; gap:8px; overflow:hidden;"><h2 class="vd__name">{{ $attr.name }}</h2><if :="definition != null && definition.type != null && definition.type !== \'\'"><span class="vd__type-label">{{ definition.type }}</span></if></div><span class="vd__state-badge" :style="\'color:\' + stateColor(state)"><span class="vd__state-dot" :style="\'background:\' + stateColor(state)"/> {{ state }}</span></div><div class="vd__actions"><if :="state === \'ready\' || isInProgress"><button class="vd__btn vd__btn--warning" :disabled="actionLoading" @click="doAction(\'stop\')">Stop</button></if><if :="(state === \'stopped\' || state === \'halted\') &amp;&amp; !isInProgress"><button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="doAction(\'start\')">Start</button></if><button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="doAction(\'rebuild\')">Rebuild</button><if :="!isInProgress"><button class="vd__btn vd__btn--danger" :disabled="actionLoading" @click="showDeleteConfirm = true">Delete</button></if><if :="isInProgress"><button class="vd__btn vd__btn--danger" disabled="disabled" title="Stop or Rebuild first">Delete</button></if></div><if :="isInProgress && sections"><div class="vd__section" style="margin-top:10px;"><div class="vd__section-title">Bootstrap progress</div><if :="sections.primary"><div style="display:flex;align-items:center;gap:8px;font-size:12px;margin-bottom:4px;"><span style="min-width:80px">Primary:</span> <span :style="\'color:\' + stateColor(sections.primary.state)">{{ sections.primary.state }}</span><if :="sections.primary.progress"><span>{{ Math.round(sections.primary.progress.percent * 100) }}%</span></if></div></if><for :="refSections" let:ref><div style="display:flex;align-items:center;gap:8px;font-size:12px;margin-bottom:4px;"><span style="min-width:80px">{{ ref.name }}:</span> <span :style="\'color:\' + stateColor(ref.state)">{{ ref.state }}</span><if :="ref.progress"><span>{{ Math.round(ref.progress.percent * 100) }}%</span></if></div></for></div></if><if :="showDeleteConfirm"><div class="vd__delete-confirm"><span class="vd__delete-confirm-text">Delete view <strong>{{ $attr.name }}</strong>? This cannot be undone.</span><div class="vd__delete-confirm-actions"><button class="vd__btn vd__btn--danger" :disabled="actionLoading" @click="deleteView()">Confirm</button> <button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="showDeleteConfirm = false">Cancel</button></div></div></if><if :="showOrphanedIndexConfirm"><div class="vd__delete-confirm"><span class="vd__delete-confirm-text">Deleting this view will orphan: <strong>{{ orphanedIndexes.join(\', \') }}</strong>. Drop or keep these indexes?</span><div class="vd__delete-confirm-actions"><button class="vd__btn vd__btn--danger" :disabled="actionLoading" @click="deleteViewWithIndexes(\'drop\')">Drop</button> <button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="deleteViewWithIndexes(\'keep\')">Keep</button> <button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="showOrphanedIndexConfirm = false">Cancel</button></div></div></if></div><if :="isBucketed"><div class="vd__section"><div class="vd__section-title">Bucketing</div><div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:12px;"><span style="font-family:monospace;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:4px;padding:2px 8px;white-space:nowrap;">{{ definition.bucket.field }} · {{ definition.bucket.granularity }} · {{ definition.bucket.timezone || \'UTC\' }} · {{ definition.bucket.preset || \'custom\' }}</span><if :="meta && meta.bucketing && meta.bucketing.unbucketedCount > 0"><span style="font-size:11px;color:var(--okdb-warning);background:color-mix(in srgb,var(--okdb-warning) 10%,transparent);border:1px solid color-mix(in srgb,var(--okdb-warning) 30%,transparent);border-radius:4px;padding:2px 8px;">{{ meta.bucketing.unbucketedCount }} records have no bucket (missing or null bucket field)</span></if></div><div><if :="bucketsLoading"><span style="font-size:11px;color:var(--okdb-app-fg-subtle);">Loading buckets…</span></if><if :="!bucketsLoading && bucketsError"><span style="font-size:11px;color:var(--okdb-danger);">{{ bucketsError }}</span></if><if :="!bucketsLoading && !bucketsError && bucketsData.length === 0"><span style="font-size:11px;color:var(--okdb-app-fg-subtle);">No buckets yet</span></if><if :="!bucketsLoading && !bucketsError && bucketsData.length > 0"><span style="font-size:11px;color:var(--okdb-app-fg-muted);font-family:monospace;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:4px;padding:2px 8px;">{{ bucketsData.length }} bucket{{ bucketsData.length === 1 ? \'\' : \'s\' }} · earliest {{ bucketsData[0].bucketKey }} · latest {{ bucketsData[bucketsData.length - 1].bucketKey }}{{ bucketsData.length >= 10000 ? \' (showing first 10000)\' : \'\' }}</span></if></div></div><div class="vd__section"><div class="vd__section-title">Per-bucket aggregates</div><if :="definition.bucket.preset === \'time\'"><div style="display:flex;align-items:center;gap:4px;flex-wrap:wrap;"><button style="padding:4px 8px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;line-height:1;" @click="shiftRange(-1)" :disabled="rangeLoading" title="Previous period">←</button> <button :style="rangePreset === \'7d\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'7d\')" :disabled="rangeLoading">7d</button> <button :style="rangePreset === \'30d\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'30d\')" :disabled="rangeLoading">30d</button> <button :style="rangePreset === \'90d\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'90d\')" :disabled="rangeLoading">90d</button> <button :style="rangePreset === \'1y\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'1y\')" :disabled="rangeLoading">1y</button> <button :style="rangePreset === \'all\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'all\')" :disabled="rangeLoading">All</button> <button style="padding:4px 8px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;line-height:1;" @click="shiftRange(1)" :disabled="rangeLoading" title="Next period">→</button></div></if><div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;"><div style="display:flex;flex-direction:column;gap:3px;"><span style="font-size:10px;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:0.05em;">From</span><if :="definition.bucket.preset === \'time\'"><input type="date" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeFrom" @input="rangeFrom = $el.value"></if><if :="definition.bucket.preset !== \'time\'"><input type="text" placeholder="start key" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeFrom" @input="rangeFrom = $el.value"></if></div><div style="display:flex;flex-direction:column;gap:3px;"><span style="font-size:10px;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:0.05em;">To</span><if :="definition.bucket.preset === \'time\'"><input type="date" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeTo" @input="rangeTo = $el.value"></if><if :="definition.bucket.preset !== \'time\'"><input type="text" placeholder="end key" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeTo" @input="rangeTo = $el.value"></if></div><label style="display:flex;align-items:center;gap:5px;font-size:12px;cursor:pointer;color:var(--okdb-app-fg-muted);padding-bottom:4px;"><input type="checkbox" :checked="rangeIncludePartial" @change="rangeIncludePartial = $event.target.checked"> Include partial</label> <button style="padding:5px 12px;border-radius:5px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;" @click="loadRange()" :disabled="rangeLoading">Reload</button></div><if :="rangeError"><div class="vd__error">{{ rangeError }}</div></if><if :="rangeLoading"><div class="vd__empty">Loading…</div></if><if :="!rangeLoading && !rangeError && rangeData.length === 0"><div class="vd__empty">No data for the selected range</div></if><if :="!rangeLoading && !rangeError && rangeData.length > 0"><div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:8px 12px;background:color-mix(in srgb,var(--okdb-accent) 6%,var(--okdb-app-bg-deep));border:1px solid color-mix(in srgb,var(--okdb-accent) 20%,var(--okdb-app-border));border-radius:5px;font-size:12px;"><span style="font-size:10px;text-transform:uppercase;letter-spacing:0.06em;color:var(--okdb-app-fg-subtle);white-space:nowrap;">Range totals</span><each :of="rangeColumns" let:item="col"><span style="display:flex;align-items:center;gap:5px;"><span style="font-size:10px;color:var(--okdb-app-fg-subtle);font-family:monospace;">{{ col }}</span> <span style="font-family:monospace;font-weight:600;color:var(--okdb-app-fg);">{{ rangeTotalDisplay(col) }}</span></span></each></div><div style="overflow-x:auto;border:1px solid var(--okdb-app-border);border-radius:5px;font-size:12px;"><div style="display:flex;border-bottom:1px solid var(--okdb-app-border);background:var(--okdb-app-bg-deep);"><div style="flex:0 0 160px;padding:6px 10px;font-size:10px;text-transform:uppercase;letter-spacing:0.05em;color:var(--okdb-app-fg-subtle);white-space:nowrap;">Bucket</div><each :of="rangeColumns" let:item="col"><div style="flex:1;min-width:80px;padding:6px 10px;font-size:10px;text-transform:uppercase;letter-spacing:0.05em;color:var(--okdb-app-fg-subtle);white-space:nowrap;font-family:monospace;text-align:right;">{{ col }}</div></each></div><each :of="rangeData" let:item="row"><div style="display:flex;border-bottom:1px solid var(--okdb-app-border);"><div style="flex:0 0 160px;padding:5px 10px;font-family:monospace;color:var(--okdb-app-fg-muted);">{{ row.bucketKey }}</div><each :of="rangeColumns" let:item="col"><div style="flex:1;min-width:80px;padding:5px 10px;font-family:monospace;text-align:right;">{{ reducerDisplayValue(row, col) }}</div></each></div></each><div style="display:flex;border-top:2px solid var(--okdb-app-border);background:var(--okdb-app-bg-deep);"><div style="flex:0 0 160px;padding:5px 10px;font-family:monospace;font-size:11px;font-weight:600;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:0.04em;">Total</div><each :of="rangeColumns" let:item="col"><div style="flex:1;min-width:80px;padding:5px 10px;font-family:monospace;font-weight:600;text-align:right;">{{ rangeTotalDisplay(col) }}</div></each></div></div></if></div><if :="isBucketed && chartData != null && !rangeLoading"><div class="vd__section"><div class="vd__section-title">Trend</div><div style="height:300px;"><ok-chart :type="chartType" :data="chartData"></div></div></if><if :="isBucketed && chartData == null && rangeData != null && !rangeLoading"><div class="vd__section"><div class="vd__section-title">Trend</div><div class="vd__empty">Not enough data to chart yet (need at least 2 buckets).</div></div></if></if><div class="vd__section"><div class="vd__section-title">Output<if :="isBucketed"><span style="font-size:10px;font-weight:400;color:var(--okdb-app-fg-subtle);text-transform:none;letter-spacing:0;margin-left:6px;">global totals · all time</span></if></div><if :="output != null"><pre class="vd__pre">{{ formatJson(output) }}</pre></if><if :="output == null"><div class="vd__empty">No output available</div></if></div><div class="vd__section"><div class="vd__section-title">Meta</div><if :="meta"><div class="vd__meta-grid"><div class="vd__meta-row"><span class="vd__meta-key">state</span> <span class="vd__meta-val" :style="\'color:\' + stateColor(meta.state)">{{ meta.state ?? \'—\' }}</span></div><div class="vd__meta-row"><span class="vd__meta-key">clock</span> <span class="vd__meta-val">{{ meta.clock ?? \'—\' }}</span></div><if :="meta.error"><div class="vd__meta-row"><span class="vd__meta-key">error</span> <span class="vd__meta-val vd__meta-val--error">{{ meta.error }}</span></div></if></div><if :="meta.refs && Object.keys(meta.refs).length > 0"><div class="vd__section-title" style="margin-top: 8px;">Refs</div><div class="vd__refs"><each :of="Object.entries(meta.refs)" let:item="entry"><div class="vd__ref-row"><span class="vd__ref-name">{{ entry[0] }}</span> <span class="vd__ref-state">{{ typeof entry[1] === \'object\' ? (entry[1]?.state ?? JSON.stringify(entry[1])) : entry[1] }}</span></div></each></div></if></if><if :="!meta"><div class="vd__empty">No meta available</div></if></div><div class="vd__section"><div class="vd__section-title">Definition</div><if :="definition != null"><pre class="vd__pre">{{ formatJson(definition) }}</pre></if><if :="definition == null"><div class="vd__empty">No definition available</div></if></div></if></div>'};
1
+ import e from"@toolkit/components/ok-chart.ok.js";export default{tag:"view-detail",dependencies:[e],attr:{name:{}},context:e=>({output:null,meta:null,definition:null,loading:!0,error:null,actionLoading:!1,showDeleteConfirm:!1,showOrphanedIndexConfirm:!1,orphanedIndexes:[],rangeData:[],rangeLoading:!1,rangeError:null,rangeFrom:"",rangeTo:"",rangeIncludePartial:!1,rangePreset:"30d",bucketsData:[],bucketsLoading:!1,bucketsError:null,async load(){this.loading=!0,this.error=null,this.output=null,this.meta=null,this.definition=null;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,[r,o,i]=await Promise.all([fetch(`${t}/views/${a}`),fetch(`${t}/views/${a}/meta`),fetch(`${t}/views/${a}/definition`)]),n=r.ok?await r.json():null,s=o.ok?await o.json():null,d=i.ok?await i.json():null;this.output=n?.data??n?.result??null,this.meta=s?.data??s?.result??null,this.definition=d?.data??d?.result??null,this.isBucketed&&(this.rangePreset="30d",this.rangeFrom=new Date(Date.now()-2592e6).toISOString().split("T")[0],this.rangeTo="",this.loadRange(),this.loadBuckets())}catch(e){this.error=e.message}finally{this.loading=!1}},async doAction(t){this.actionLoading=!0;try{const a=e.$ok.stores.okdb.apiBase,r=e.$attr.name,o=await fetch(`${a}/views/${r}/${t}`,{method:"POST"});if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message??`${t} failed`)}await this.load(),e.$dispatch("state-changed")}catch(e){this.error=e.message}finally{this.actionLoading=!1}},async deleteView(){this.actionLoading=!0;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,r=await fetch(`${t}/views/${a}`,{method:"DELETE"});if(!r.ok){const e=await r.json().catch(()=>({})),t=e?.error?.code;if("VIEW_HAS_ORPHANED_INDEXES"===t)return this.orphanedIndexes=e?.error?.details?.orphanedIndexes??[],this.showDeleteConfirm=!1,void(this.showOrphanedIndexConfirm=!0);throw new Error(e?.error?.message??"Delete failed")}e.$dispatch("removed")}catch(e){this.error=e.message}finally{this.actionLoading=!1,this.showOrphanedIndexConfirm||(this.showDeleteConfirm=!1)}},async deleteViewWithIndexes(t){this.actionLoading=!0;try{const a=e.$ok.stores.okdb.apiBase,r=e.$attr.name,o=await fetch(`${a}/views/${r}`,{method:"DELETE",headers:{"Content-Type":"application/json"},body:JSON.stringify({managedIndexes:t})});if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message??"Delete failed")}e.$dispatch("removed")}catch(e){this.error=e.message}finally{this.actionLoading=!1,this.showOrphanedIndexConfirm=!1,this.orphanedIndexes=[]}},formatJson(e){if(null==e)return"";try{return JSON.stringify(e,null,2)}catch{return String(e)}},stateColor(e){switch(e){case"ready":return"#4caf50";case"creating":case"resetting":case"partially-ready":return"#4FC1FF";case"halted":return"#f44336";case"stopped":return"#fbbf24";default:return"#666"}},get state(){return this.meta?.state??"unknown"},get isInProgress(){return"creating"===this.state||"resetting"===this.state||"partially-ready"===this.state},get sections(){return this.meta?.sections??null},get refSections(){const e=this.sections?.refs??{};return Object.entries(e).map(([e,t])=>({name:e,...t}))},get isBucketed(){return this.definition&&this.definition.bucket},get chartType(){return this.isBucketed?"time"===this.definition.bucket.preset?"line":"bar":null},get chartData(){const e=this.rangeData||[];if(e.length<2)return null;if(!this.isBucketed)return null;const t=e.map(e=>e.bucketKey),a=e[0]?.reducers||{};return{labels:t,datasets:Object.entries(a).filter(([,e])=>e&&"number"==typeof e.value).map(([e])=>e).map(t=>({label:t,data:e.map(e=>{const a=e.reducers[t];return a&&"number"==typeof a.value?a.value:null})}))}},setQuickRange(e){const t=new Date,a=e=>new Date(t-864e5*e).toISOString().split("T")[0];switch(this.rangePreset=e,e){case"7d":this.rangeFrom=a(7),this.rangeTo="";break;case"30d":this.rangeFrom=a(30),this.rangeTo="";break;case"90d":this.rangeFrom=a(90),this.rangeTo="";break;case"1y":this.rangeFrom=a(365),this.rangeTo="";break;case"all":this.rangeFrom="",this.rangeTo=""}this.loadRange()},shiftRange(e){const t=e=>e?new Date(e+"T00:00:00Z"):null,a=e=>e.toISOString().split("T")[0],r=new Date;let o=t(this.rangeFrom),i=t(this.rangeTo);o||i?o?i||(i=new Date(r)):o=new Date(i-2592e6):(i=r,o=new Date(r-2592e6));const n=i-o;this.rangeFrom=a(new Date(o.getTime()+e*n)),this.rangeTo=a(new Date(i.getTime()+e*n)),this.rangePreset="",this.loadRange()},async loadRange(){if(this.isBucketed){this.rangeLoading=!0,this.rangeError=null;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,r=new URLSearchParams;this.rangeFrom&&r.set("from",this.rangeFrom),this.rangeTo&&r.set("to",this.rangeTo),this.rangeIncludePartial&&r.set("includePartial","true");const o=this.definition.bucket.granularity;r.set("granularity",o);const i=await fetch(`${t}/views/${a}/range?${r}`);if(!i.ok){const e=await i.json().catch(()=>({}));throw new Error(e?.error?.message??"Range fetch failed")}const n=await i.json();this.rangeData=n.data??n.result??[]}catch(e){this.rangeError=e.message,this.rangeData=[]}finally{this.rangeLoading=!1}}},async loadBuckets(){if(this.isBucketed){this.bucketsLoading=!0,this.bucketsError=null;try{const t=e.$ok.stores.okdb.apiBase,a=e.$attr.name,r=encodeURIComponent(this.definition.bucket.granularity),o=await fetch(`${t}/views/${a}/buckets?granularity=${r}&limit=10000`);if(!o.ok){const e=await o.json().catch(()=>({}));throw new Error(e?.error?.message??"Bucket list failed")}const i=await o.json();this.bucketsData=i.data??i.result??[]}catch(e){this.bucketsError=e.message,this.bucketsData=[]}finally{this.bucketsLoading=!1}}},get rangeColumns(){return this.rangeData.length?Object.keys(this.rangeData[0].reducers||{}):[]},reducerDisplayValue(e,t){const a=e.reducers?.[t];if(null==a)return"";if("object"==typeof a&&"number"==typeof a.value)return a.value;if("object"==typeof a&&!Array.isArray(a)){const e=Object.entries(a);if(!e.length)return"—";const t=e[0][1];return"object"==typeof t&&"number"==typeof t.value?e.sort((e,t)=>(t[1].value??0)-(e[1].value??0)).slice(0,3).map(([e,t])=>`${e}: ${t.value}`).join(" · ")+(e.length>3?" +"+(e.length-3):""):e.map(([e,t])=>{const a=Object.values(t||{})[0];return`${e}: ${"number"==typeof a?.value?a.value:"?"}`}).slice(0,3).join(" · ")+(e.length>3?" +"+(e.length-3):"")}return String(a)},get rangeTotals(){if(!this.rangeData.length)return null;const e=this.rangeColumns;if(!e.length)return null;const t=this.definition?.reduce||{},a={};for(const r of e){const e=t[r],o=e?Object.keys(e).find(e=>"items"!==e):null,i=this.rangeData.map(e=>e.reducers?.[r]).filter(e=>null!=e&&"number"==typeof e.value).map(e=>e.value);if(!i.length){a[r]=null;continue}const n=e=>"number"==typeof e&&isFinite(e)?parseFloat(e.toPrecision(10)):e;a[r]="$min"===o?n(Math.min(...i)):"$max"===o?n(Math.max(...i)):"$avg"===o?{approx:n(i.reduce((e,t)=>e+t,0)/i.length)}:n(i.reduce((e,t)=>e+t,0))}return a},rangeTotalDisplay(e){const t=this.rangeTotals;if(!t)return"—";const a=t[e];return null==a?"—":"object"==typeof a&&null!=a.approx?"~"+Math.round(1e3*a.approx)/1e3:a}}),mount(e){e.$watch(()=>e.$attr.name,()=>{this.loading=!0,this.output=null,this.meta=null,this.definition=null,this.showDeleteConfirm=!1,this.showOrphanedIndexConfirm=!1,this.orphanedIndexes=[],this.load()}),this.load()},style:"view-detail{gap:0;height:100%;overflow-y:auto}.vd,.vd__header,view-detail{display:flex;flex-direction:column}.vd{gap:20px;padding:20px 24px}.vd__header{gap:10px;padding-bottom:16px;border-bottom:1px solid var(--okdb-app-border)}.vd__title-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.vd__name,.vd__state-badge{font-weight:600;white-space:nowrap}.vd__name{font-size:20px;color:var(--okdb-app-fg-strong);margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;flex-shrink:1}.vd__state-badge{display:inline-flex;align-items:center;gap:6px;padding:3px 8px;border-radius:999px;border:1px solid var(--okdb-app-border);font-size:11px;text-transform:uppercase;letter-spacing:.04em}.vd__state-dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0}.vd__type-label{font-size:11px;color:var(--okdb-app-fg-subtle);font-family:monospace}.vd__actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.vd__btn{padding:5px 12px;border-radius:4px;font-size:12px;font-weight:600;border:1px solid;background:0 0;cursor:pointer;transition:all .15s ease}.vd__btn--primary{border-color:var(--okdb-accent);color:var(--okdb-accent)}.vd__btn--primary:hover{background:color-mix(in srgb,var(--okdb-accent) 10%,transparent);transform:none;box-shadow:none}.vd__btn--warning{border-color:var(--okdb-warning);color:var(--okdb-warning)}.vd__btn--warning:hover{background:color-mix(in srgb,var(--okdb-warning) 10%,transparent);transform:none;box-shadow:none}.vd__btn--danger{border-color:var(--okdb-danger);color:var(--okdb-danger)}.vd__btn--danger:hover{background:color-mix(in srgb,var(--okdb-danger) 10%,transparent);transform:none;box-shadow:none}.vd__btn:disabled{opacity:.4;cursor:not-allowed}.vd__delete-confirm{display:flex;align-items:center;gap:10px;padding:8px 12px;background:color-mix(in srgb,var(--okdb-danger) 7%,transparent);border:1px solid color-mix(in srgb,var(--okdb-danger) 25%,transparent);border-radius:5px;font-size:12px;color:var(--okdb-danger);flex-wrap:wrap}.vd__delete-confirm-text{flex:1;min-width:0}.vd__delete-confirm-actions{display:flex;gap:6px}.vd__section{display:flex;flex-direction:column;gap:10px}.vd__section-title{font-size:11px;font-weight:600;color:var(--okdb-app-fg-muted);text-transform:uppercase;letter-spacing:.05em}.vd__pre{font-family:monospace;font-size:12px;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:5px;padding:12px 14px;overflow-x:auto;color:var(--okdb-app-fg);white-space:pre;margin:0;line-height:1.5}.vd__meta-grid{display:flex;flex-direction:column;gap:6px;font-size:12px}.vd__meta-row{display:flex;gap:8px}.vd__meta-key{color:var(--okdb-app-fg-subtle);width:80px;flex-shrink:0}.vd__meta-val{color:var(--okdb-app-fg);font-family:monospace;word-break:break-all}.vd__meta-val--error{color:var(--okdb-danger)}.vd__empty,.vd__error{font-size:12px;color:var(--okdb-app-fg-subtle)}.vd__error{color:var(--okdb-danger);padding:8px;background:color-mix(in srgb,var(--okdb-danger) 7%,transparent);border:1px solid color-mix(in srgb,var(--okdb-danger) 20%,transparent);border-radius:4px}.vd__loading{font-size:12px;opacity:.5;padding:40px;text-align:center}.vd__refs{display:flex;flex-direction:column;gap:4px;margin-top:4px}.vd__ref-row{display:flex;gap:8px;font-size:11px;padding:4px 8px;background:var(--okdb-app-bg-deep);border-radius:3px;border:1px solid var(--okdb-app-border)}.vd__ref-name{color:var(--okdb-sky);font-family:monospace;flex:1}.vd__ref-state{color:var(--okdb-app-fg-subtle)}",template:'<div class="vd"><if :="loading"><div class="vd__loading">Loading…</div></if><if :="!loading"><if :="error"><div class="vd__error">{{ error }}</div></if><div class="vd__header"><div class="vd__title-row"><div style="flex:1; min-width:0; display:flex; align-items:baseline; gap:8px; overflow:hidden;"><h2 class="vd__name">{{ definition?.name ?? \'\' }}</h2><if :="definition != null && definition.type != null && definition.type !== \'\'"><span class="vd__type-label">{{ definition.type }}</span></if></div><span class="vd__state-badge" :style="\'color:\' + stateColor(state)"><span class="vd__state-dot" :style="\'background:\' + stateColor(state)"/> {{ state }}</span></div><div class="vd__actions"><if :="state === \'ready\' || isInProgress"><button class="vd__btn vd__btn--warning" :disabled="actionLoading" @click="doAction(\'stop\')">Stop</button></if><if :="(state === \'stopped\' || state === \'halted\') &amp;&amp; !isInProgress"><button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="doAction(\'start\')">Start</button></if><button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="doAction(\'rebuild\')">Rebuild</button><if :="!isInProgress"><button class="vd__btn vd__btn--danger" :disabled="actionLoading" @click="showDeleteConfirm = true">Delete</button></if><if :="isInProgress"><button class="vd__btn vd__btn--danger" disabled="disabled" title="Stop or Rebuild first">Delete</button></if></div><if :="isInProgress && sections"><div class="vd__section" style="margin-top:10px;"><div class="vd__section-title">Bootstrap progress</div><if :="sections.primary"><div style="display:flex;align-items:center;gap:8px;font-size:12px;margin-bottom:4px;"><span style="min-width:80px">Primary:</span> <span :style="\'color:\' + stateColor(sections.primary.state)">{{ sections.primary.state }}</span><if :="sections.primary.progress"><span>{{ Math.round(sections.primary.progress.percent * 100) }}%</span></if></div></if><for :="refSections" let:ref><div style="display:flex;align-items:center;gap:8px;font-size:12px;margin-bottom:4px;"><span style="min-width:80px">{{ ref.name }}:</span> <span :style="\'color:\' + stateColor(ref.state)">{{ ref.state }}</span><if :="ref.progress"><span>{{ Math.round(ref.progress.percent * 100) }}%</span></if></div></for></div></if><if :="showDeleteConfirm"><div class="vd__delete-confirm"><span class="vd__delete-confirm-text">Delete view <strong>{{ definition?.name ?? \'\' }}</strong>? This cannot be undone.</span><div class="vd__delete-confirm-actions"><button class="vd__btn vd__btn--danger" :disabled="actionLoading" @click="deleteView()">Confirm</button> <button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="showDeleteConfirm = false">Cancel</button></div></div></if><if :="showOrphanedIndexConfirm"><div class="vd__delete-confirm"><span class="vd__delete-confirm-text">Deleting this view will orphan: <strong>{{ orphanedIndexes.join(\', \') }}</strong>. Drop or keep these indexes?</span><div class="vd__delete-confirm-actions"><button class="vd__btn vd__btn--danger" :disabled="actionLoading" @click="deleteViewWithIndexes(\'drop\')">Drop</button> <button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="deleteViewWithIndexes(\'keep\')">Keep</button> <button class="vd__btn vd__btn--primary" :disabled="actionLoading" @click="showOrphanedIndexConfirm = false">Cancel</button></div></div></if></div><if :="isBucketed"><div class="vd__section"><div class="vd__section-title">Bucketing</div><div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:12px;"><span style="font-family:monospace;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:4px;padding:2px 8px;white-space:nowrap;">{{ definition.bucket.field }} · {{ definition.bucket.granularity }} · {{ definition.bucket.timezone || \'UTC\' }} · {{ definition.bucket.preset || \'custom\' }}</span><if :="meta && meta.bucketing && meta.bucketing.unbucketedCount > 0"><span style="font-size:11px;color:var(--okdb-warning);background:color-mix(in srgb,var(--okdb-warning) 10%,transparent);border:1px solid color-mix(in srgb,var(--okdb-warning) 30%,transparent);border-radius:4px;padding:2px 8px;">{{ meta.bucketing.unbucketedCount }} records have no bucket (missing or null bucket field)</span></if></div><div><if :="bucketsLoading"><span style="font-size:11px;color:var(--okdb-app-fg-subtle);">Loading buckets…</span></if><if :="!bucketsLoading && bucketsError"><span style="font-size:11px;color:var(--okdb-danger);">{{ bucketsError }}</span></if><if :="!bucketsLoading && !bucketsError && bucketsData.length === 0"><span style="font-size:11px;color:var(--okdb-app-fg-subtle);">No buckets yet</span></if><if :="!bucketsLoading && !bucketsError && bucketsData.length > 0"><span style="font-size:11px;color:var(--okdb-app-fg-muted);font-family:monospace;background:var(--okdb-app-bg-deep);border:1px solid var(--okdb-app-border);border-radius:4px;padding:2px 8px;">{{ bucketsData.length }} bucket{{ bucketsData.length === 1 ? \'\' : \'s\' }} · earliest {{ bucketsData[0].bucketKey }} · latest {{ bucketsData[bucketsData.length - 1].bucketKey }}{{ bucketsData.length >= 10000 ? \' (showing first 10000)\' : \'\' }}</span></if></div></div><div class="vd__section"><div class="vd__section-title">Per-bucket aggregates</div><if :="definition.bucket.preset === \'time\'"><div style="display:flex;align-items:center;gap:4px;flex-wrap:wrap;"><button style="padding:4px 8px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;line-height:1;" @click="shiftRange(-1)" :disabled="rangeLoading" title="Previous period">←</button> <button :style="rangePreset === \'7d\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'7d\')" :disabled="rangeLoading">7d</button> <button :style="rangePreset === \'30d\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'30d\')" :disabled="rangeLoading">30d</button> <button :style="rangePreset === \'90d\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'90d\')" :disabled="rangeLoading">90d</button> <button :style="rangePreset === \'1y\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'1y\')" :disabled="rangeLoading">1y</button> <button :style="rangePreset === \'all\' ? \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-accent);background:color-mix(in srgb,var(--okdb-accent) 12%,transparent);color:var(--okdb-accent);font-size:11px;font-weight:600;cursor:pointer;\' : \'padding:4px 9px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:11px;cursor:pointer;\'" @click="setQuickRange(\'all\')" :disabled="rangeLoading">All</button> <button style="padding:4px 8px;border-radius:4px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;line-height:1;" @click="shiftRange(1)" :disabled="rangeLoading" title="Next period">→</button></div></if><div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;"><div style="display:flex;flex-direction:column;gap:3px;"><span style="font-size:10px;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:0.05em;">From</span><if :="definition.bucket.preset === \'time\'"><input type="date" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeFrom" @input="rangeFrom = $el.value"></if><if :="definition.bucket.preset !== \'time\'"><input type="text" placeholder="start key" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeFrom" @input="rangeFrom = $el.value"></if></div><div style="display:flex;flex-direction:column;gap:3px;"><span style="font-size:10px;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:0.05em;">To</span><if :="definition.bucket.preset === \'time\'"><input type="date" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeTo" @input="rangeTo = $el.value"></if><if :="definition.bucket.preset !== \'time\'"><input type="text" placeholder="end key" style="padding:5px 8px;border:1px solid var(--okdb-app-border);border-radius:5px;background:var(--okdb-app-bg-deep);color:var(--okdb-app-fg);font-size:12px;" :value="rangeTo" @input="rangeTo = $el.value"></if></div><label style="display:flex;align-items:center;gap:5px;font-size:12px;cursor:pointer;color:var(--okdb-app-fg-muted);padding-bottom:4px;"><input type="checkbox" :checked="rangeIncludePartial" @change="rangeIncludePartial = $event.target.checked"> Include partial</label> <button style="padding:5px 12px;border-radius:5px;border:1px solid var(--okdb-app-border);background:transparent;color:var(--okdb-app-fg-muted);font-size:12px;cursor:pointer;" @click="loadRange()" :disabled="rangeLoading">Reload</button></div><if :="rangeError"><div class="vd__error">{{ rangeError }}</div></if><if :="rangeLoading"><div class="vd__empty">Loading…</div></if><if :="!rangeLoading && !rangeError && rangeData.length === 0"><div class="vd__empty">No data for the selected range</div></if><if :="!rangeLoading && !rangeError && rangeData.length > 0"><div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:8px 12px;background:color-mix(in srgb,var(--okdb-accent) 6%,var(--okdb-app-bg-deep));border:1px solid color-mix(in srgb,var(--okdb-accent) 20%,var(--okdb-app-border));border-radius:5px;font-size:12px;"><span style="font-size:10px;text-transform:uppercase;letter-spacing:0.06em;color:var(--okdb-app-fg-subtle);white-space:nowrap;">Range totals</span><each :of="rangeColumns" let:item="col"><span style="display:flex;align-items:center;gap:5px;"><span style="font-size:10px;color:var(--okdb-app-fg-subtle);font-family:monospace;">{{ col }}</span> <span style="font-family:monospace;font-weight:600;color:var(--okdb-app-fg);">{{ rangeTotalDisplay(col) }}</span></span></each></div><div style="overflow-x:auto;border:1px solid var(--okdb-app-border);border-radius:5px;font-size:12px;"><div style="display:flex;border-bottom:1px solid var(--okdb-app-border);background:var(--okdb-app-bg-deep);"><div style="flex:0 0 160px;padding:6px 10px;font-size:10px;text-transform:uppercase;letter-spacing:0.05em;color:var(--okdb-app-fg-subtle);white-space:nowrap;">Bucket</div><each :of="rangeColumns" let:item="col"><div style="flex:1;min-width:80px;padding:6px 10px;font-size:10px;text-transform:uppercase;letter-spacing:0.05em;color:var(--okdb-app-fg-subtle);white-space:nowrap;font-family:monospace;text-align:right;">{{ col }}</div></each></div><each :of="rangeData" let:item="row"><div style="display:flex;border-bottom:1px solid var(--okdb-app-border);"><div style="flex:0 0 160px;padding:5px 10px;font-family:monospace;color:var(--okdb-app-fg-muted);">{{ row.bucketKey }}</div><each :of="rangeColumns" let:item="col"><div style="flex:1;min-width:80px;padding:5px 10px;font-family:monospace;text-align:right;">{{ reducerDisplayValue(row, col) }}</div></each></div></each><div style="display:flex;border-top:2px solid var(--okdb-app-border);background:var(--okdb-app-bg-deep);"><div style="flex:0 0 160px;padding:5px 10px;font-family:monospace;font-size:11px;font-weight:600;color:var(--okdb-app-fg-subtle);text-transform:uppercase;letter-spacing:0.04em;">Total</div><each :of="rangeColumns" let:item="col"><div style="flex:1;min-width:80px;padding:5px 10px;font-family:monospace;font-weight:600;text-align:right;">{{ rangeTotalDisplay(col) }}</div></each></div></div></if></div><if :="isBucketed && chartData != null && !rangeLoading"><div class="vd__section"><div class="vd__section-title">Trend</div><div style="height:300px;"><ok-chart :type="chartType" :data="chartData"></div></div></if><if :="isBucketed && chartData == null && rangeData != null && !rangeLoading"><div class="vd__section"><div class="vd__section-title">Trend</div><div class="vd__empty">Not enough data to chart yet (need at least 2 buckets).</div></div></if></if><div class="vd__section"><div class="vd__section-title">Output<if :="isBucketed"><span style="font-size:10px;font-weight:400;color:var(--okdb-app-fg-subtle);text-transform:none;letter-spacing:0;margin-left:6px;">global totals · all time</span></if></div><if :="output != null"><pre class="vd__pre">{{ formatJson(output) }}</pre></if><if :="output == null"><div class="vd__empty">No output available</div></if></div><div class="vd__section"><div class="vd__section-title">Meta</div><if :="meta"><div class="vd__meta-grid"><div class="vd__meta-row"><span class="vd__meta-key">state</span> <span class="vd__meta-val" :style="\'color:\' + stateColor(meta.state)">{{ meta.state ?? \'—\' }}</span></div><div class="vd__meta-row"><span class="vd__meta-key">clock</span> <span class="vd__meta-val">{{ meta.clock ?? \'—\' }}</span></div><if :="meta.error"><div class="vd__meta-row"><span class="vd__meta-key">error</span> <span class="vd__meta-val vd__meta-val--error">{{ meta.error }}</span></div></if></div><if :="meta.refs && Object.keys(meta.refs).length > 0"><div class="vd__section-title" style="margin-top: 8px;">Refs</div><div class="vd__refs"><each :of="Object.entries(meta.refs)" let:item="entry"><div class="vd__ref-row"><span class="vd__ref-name">{{ entry[0] }}</span> <span class="vd__ref-state">{{ typeof entry[1] === \'object\' ? (entry[1]?.state ?? JSON.stringify(entry[1])) : entry[1] }}</span></div></each></div></if></if><if :="!meta"><div class="vd__empty">No meta available</div></if></div><div class="vd__section"><div class="vd__section-title">Definition</div><if :="definition != null"><pre class="vd__pre">{{ formatJson(definition) }}</pre></if><if :="definition == null"><div class="vd__empty">No definition available</div></if></div></if></div>'};
@@ -1 +1 @@
1
- import{ok as e}from"../modules/ok.js";const t=["system:ready","system:stopped","system:clock_change","system:poke","system:drain","system:bus_state_change","item:create","item:update","item:remove","index:reset","index:ready","index:progress","index:drop","index:registered","fts:registered","fts:drop","fts:reset","sync:apply","txn:start","txn:end","txn:rollback","type:registered","type:drop","env:opened","env:removed","engine:started","engine:stopped","engine:error","engine:installed","engine:uninstalled","embeddings:indexer:ready","embeddings:indexer:flushed","embeddings:indexer:doc:done","embeddings:indexer:doc:failed","embeddings:indexer:doc:queued","embeddings:doc:status_change","embeddings:pipeline:ready","queue:enqueued","queue:done","queue:failed","queue:retry","queue:removed","queue:reconciled","processor:ready","processor:progress","processor:error","processor:paused","processor:resumed","processor:retry","relay:error"];export{t as ALL_EVENTS};let n=null;const r={};function s(e){try{return JSON.parse(e)}catch{return null}}function o(e,t){const n=r[e];if(n)for(const e of n)try{e(t)}catch(e){console.error("events-store handler error",e)}}function i(e,t){return[...e.length>=500?e.slice(-499):e,t]}const d=e.proxy({connected:!1,connecting:!1,error:null,activity:[],get logs(){return this.activity.filter(e=>"log"===e._kind)},connect(e=null){if(n||this.connecting)return;this.connecting=!0,this.error=null;let r=__ENV__.API+"/events";e?.length&&(r+="?types="+encodeURIComponent(e.join(",")));const d=new EventSource(r);n=d,d.addEventListener("open",()=>{this.connected=!0,this.connecting=!1,this.error=null}),d.addEventListener("error",()=>{this.connected=!1,this.connecting=!1,this.error="Stream disconnected — reconnecting…"});const c=e=>{const t=s(e.data);if(!t)return;const n={_kind:"log",_ts:t._ts??t.ts??Date.now(),...t};this.activity=i(this.activity,n),o("log",t)};d.addEventListener("log",c);for(const e of t)d.addEventListener(e,t=>{try{const n=s(t.data);if(!n)return;const r={_kind:"event",_event:e,_ts:n._ts??Date.now(),...n};this.activity=i(this.activity,r),o(e,n)}catch{}});d.onmessage=c},disconnect(){n?.close(),n=null,this.connected=!1,this.connecting=!1},on:(e,t)=>(r[e]||(r[e]=new Set),r[e].add(t),()=>r[e]?.delete(t)),clearActivity(){this.activity=[]},clearLogs(){this.clearActivity()}});export default d;
1
+ import{ok as e}from"../modules/ok.js";const t=["system:ready","system:stopped","system:clock_change","system:poke","system:drain","system:bus_state_change","item:create","item:update","item:remove","index:reset","index:ready","index:progress","index:drop","index:registered","fts:registered","fts:drop","fts:reset","sync:apply","txn:start","txn:end","txn:rollback","type:registered","type:drop","env:opened","env:removed","engine:started","engine:stopped","engine:error","engine:installed","engine:uninstalled","embeddings:indexer:ready","embeddings:indexer:flushed","embeddings:indexer:doc:done","embeddings:indexer:doc:failed","embeddings:indexer:doc:queued","embeddings:doc:status_change","embeddings:pipeline:ready","queue:enqueued","queue:done","queue:failed","queue:retry","queue:removed","queue:reconciled","queue:log","processor:ready","processor:progress","processor:error","processor:paused","processor:resumed","processor:retry","relay:error"];export{t as ALL_EVENTS};let n=null;const r={};function s(e){try{return JSON.parse(e)}catch{return null}}function o(e,t){const n=r[e];if(n)for(const e of n)try{e(t)}catch(e){console.error("events-store handler error",e)}}function i(e,t){return[...e.length>=500?e.slice(-499):e,t]}const d=e.proxy({connected:!1,connecting:!1,error:null,activity:[],get logs(){return this.activity.filter(e=>"log"===e._kind)},connect(e=null){if(n||this.connecting)return;this.connecting=!0,this.error=null;let r=__ENV__.API+"/events";e?.length&&(r+="?types="+encodeURIComponent(e.join(",")));const d=new EventSource(r);n=d,d.addEventListener("open",()=>{this.connected=!0,this.connecting=!1,this.error=null}),d.addEventListener("error",()=>{this.connected=!1,this.connecting=!1,this.error="Stream disconnected — reconnecting…"});const c=e=>{const t=s(e.data);if(!t)return;const n={_kind:"log",_ts:t._ts??t.ts??Date.now(),...t};this.activity=i(this.activity,n),o("log",t)};d.addEventListener("log",c);for(const e of t)d.addEventListener(e,t=>{try{const n=s(t.data);if(!n)return;const r={_kind:"event",_event:e,_ts:n._ts??Date.now(),...n};this.activity=i(this.activity,r),o(e,n)}catch{}});d.onmessage=c},disconnect(){n?.close(),n=null,this.connected=!1,this.connecting=!1},on:(e,t)=>(r[e]||(r[e]=new Set),r[e].add(t),()=>r[e]?.delete(t)),clearActivity(){this.activity=[]},clearLogs(){this.clearActivity()}});export default d;