Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>ElideDB</title> | |
| <style> | |
| /* ElideDB console: a precision instrument, not a demo. | |
| Monospace-forward identity, paper and graphite surfaces, one | |
| working accent. Chart hues are the validated categorical set. */ | |
| :root { | |
| --ground:#f3f2ee; --panel:#fcfbf9; --panel-2:#eceae4; --rail:#eae8e2; | |
| --ink:#191d21; --muted:#565e66; --faint:#8a9098; | |
| --line:#d8d5cd; --hairline:#e5e2da; --grid:#e9e6df; | |
| --accent:#0f6f63; --accent-ink:#0b5c52; --accent-soft:rgba(15,111,99,.12); | |
| --signal:#b4500f; --code-bg:#efede7; | |
| --shadow:0 14px 40px rgba(24,28,32,.14); | |
| } | |
| @media (prefers-color-scheme: dark) { :root { | |
| --ground:#131518; --panel:#1a1d21; --panel-2:#22262b; --rail:#0e1013; | |
| --ink:#e7e9ea; --muted:#9aa3ab; --faint:#697077; | |
| --line:#2c3238; --hairline:#24292e; --grid:#22272c; | |
| --accent:#3fb9a5; --accent-ink:#5accb9; --accent-soft:rgba(63,185,165,.14); | |
| --signal:#e8763d; --code-bg:#101317; | |
| --shadow:0 18px 48px rgba(0,0,0,.55); | |
| }} | |
| * { box-sizing:border-box; } | |
| html,body { margin:0; height:100%; } | |
| body { | |
| display:flex; background:var(--ground); color:var(--ink); | |
| font:13px/1.55 ui-monospace, "SF Mono", Menlo, Consolas, monospace; | |
| overflow:hidden; | |
| -webkit-font-smoothing:antialiased; | |
| } | |
| .tnum { font-variant-numeric:tabular-nums; } | |
| ::selection { background:var(--accent-soft); } | |
| /* micro label: the one recurring signature */ | |
| .lbl { font-size:9.5px; letter-spacing:.16em; text-transform:uppercase; | |
| color:var(--faint); font-weight:600; } | |
| /* left rail */ | |
| aside { | |
| width:252px; flex:none; background:var(--rail); | |
| border-right:1px solid var(--line); | |
| display:flex; flex-direction:column; | |
| } | |
| .brand { padding:18px 16px 14px; border-bottom:1px solid var(--line); } | |
| .brand b { letter-spacing:.22em; font-size:13px; font-weight:700; } | |
| .brand b i { font-style:normal; color:var(--accent); } | |
| .brand .sub { color:var(--faint); font-size:10px; margin-top:3px; | |
| letter-spacing:.05em; } | |
| .rail-h { padding:14px 16px 6px; } | |
| .stores { flex:1; overflow-y:auto; padding:4px 10px 10px; } | |
| .store-card { | |
| padding:10px 12px; border-radius:6px; cursor:pointer; | |
| border:1px solid transparent; margin-bottom:5px; | |
| } | |
| .store-card:hover { background:var(--panel); } | |
| .store-card.on { background:var(--panel); border-color:var(--line); | |
| box-shadow:inset 3px 0 0 var(--accent); } | |
| .store-card .n { font-weight:700; font-size:12.5px; letter-spacing:.02em; } | |
| .store-card .m { color:var(--faint); font-size:10.5px; margin-top:3px; } | |
| .railfoot { padding:12px 16px; color:var(--faint); font-size:10px; | |
| border-top:1px solid var(--line); line-height:1.7; } | |
| /* main column */ | |
| .main { flex:1; display:flex; flex-direction:column; min-width:0; } | |
| .topbar { | |
| display:flex; align-items:center; gap:18px; padding:0 22px; | |
| border-bottom:1px solid var(--line); background:var(--ground); | |
| min-height:52px; flex-wrap:wrap; | |
| } | |
| .topbar h1 { font-size:14px; margin:0; font-weight:700; | |
| letter-spacing:.03em; } | |
| nav#views { display:flex; gap:2px; align-self:stretch; } | |
| nav#views button { | |
| border:0; background:transparent; color:var(--muted); cursor:pointer; | |
| font:600 10.5px/1 ui-monospace, Menlo, monospace; | |
| letter-spacing:.14em; padding:0 13px; border-bottom:2px solid transparent; | |
| text-transform:uppercase; | |
| } | |
| nav#views button:hover { color:var(--ink); } | |
| nav#views button.on { color:var(--ink); border-bottom-color:var(--accent); } | |
| nav#views button:focus-visible { outline:2px solid var(--accent); } | |
| .topstat { margin-left:auto; color:var(--faint); font-size:11px; } | |
| .content { flex:1; overflow-y:auto; padding:22px 24px 40px; } | |
| .view { display:none; } .view.on { display:block; } | |
| /* stat tiles */ | |
| .tiles { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); | |
| gap:10px; margin-bottom:18px; } | |
| .tile { background:var(--panel); border:1px solid var(--line); | |
| border-radius:6px; padding:12px 14px 10px; } | |
| .tile .k { font-size:9.5px; letter-spacing:.16em; text-transform:uppercase; | |
| color:var(--faint); font-weight:600; margin-bottom:6px; } | |
| .tile .v { font-size:21px; font-weight:700; letter-spacing:-.01em; } | |
| .tile .v small { font-size:11px; color:var(--muted); font-weight:500; } | |
| .tile .d { font-size:10.5px; color:var(--muted); margin-top:3px; } | |
| .tile.hero { border-color:var(--accent); } | |
| .tile.hero .v { color:var(--accent-ink); } | |
| /* panels: engineering sheet header, hairline body */ | |
| .panel { background:var(--panel); border:1px solid var(--line); | |
| border-radius:6px; padding:0; margin-bottom:16px; overflow:hidden; } | |
| .panel > .hd { display:flex; align-items:baseline; gap:12px; | |
| padding:11px 16px 9px; border-bottom:1px solid var(--hairline); } | |
| .panel > .hd h2 { margin:0; font-size:11px; letter-spacing:.16em; | |
| text-transform:uppercase; font-weight:700; } | |
| .panel > .hd .note { color:var(--faint); font-size:10.5px; } | |
| .panel > .bd { padding:14px 16px 16px; } | |
| table { border-collapse:collapse; width:100%; font-size:12px; } | |
| th { text-align:left; font-size:9.5px; letter-spacing:.14em; | |
| text-transform:uppercase; color:var(--faint); font-weight:600; | |
| padding:5px 14px 5px 0; border-bottom:1px solid var(--line); } | |
| td { padding:5.5px 14px 5.5px 0; border-bottom:1px solid var(--grid); | |
| color:var(--muted); } | |
| td:first-child { color:var(--ink); } | |
| td.r,th.r { text-align:right; } | |
| tr:last-child td { border-bottom:0; } | |
| .tablewrap { overflow-x:auto; } | |
| .badge { display:inline-block; font-size:9px; padding:1px 7px; | |
| border-radius:3px; border:1px solid var(--line); | |
| color:var(--muted); letter-spacing:.08em; text-transform:uppercase; } | |
| .pill { font-size:9.5px; padding:1.5px 8px; border-radius:3px; | |
| border:1px solid var(--line); color:var(--muted); } | |
| .pill.ok { color:var(--accent-ink); border-color:var(--accent); } | |
| .pill.warn { color:var(--signal); border-color:var(--signal); } | |
| /* charts */ | |
| .chart-tip { position:fixed; z-index:60; pointer-events:none; display:none; | |
| background:var(--panel); border:1px solid var(--line); border-radius:4px; | |
| box-shadow:var(--shadow); padding:6px 9px; font-size:10.5px; | |
| color:var(--muted); max-width:280px; } | |
| .chart-tip b { color:var(--ink); display:block; font-size:11px; } | |
| .bar-row { display:grid; grid-template-columns:170px 1fr 92px; gap:12px; | |
| align-items:center; padding:3px 0; } | |
| .bar-row .id { font-size:11.5px; overflow:hidden; text-overflow:ellipsis; | |
| white-space:nowrap; } | |
| .bar-track { height:14px; background:var(--panel-2); border-radius:3px; | |
| position:relative; } | |
| .bar-fill { position:absolute; top:2px; bottom:2px; left:0; | |
| background:var(--accent); border-radius:2px; min-width:2px; } | |
| .bar-fill.dim { opacity:.35; } | |
| .bar-val { font-size:10.5px; color:var(--faint); text-align:right; } | |
| .heat-row { display:grid; grid-template-columns:170px 1fr 92px; gap:12px; | |
| align-items:center; padding:2.5px 0; } | |
| .heat-strip { display:flex; height:13px; border-radius:3px; overflow:hidden; | |
| background:var(--panel-2); } | |
| .heat-cell { flex:1; } | |
| .axis { display:flex; justify-content:space-between; color:var(--faint); | |
| font-size:9.5px; margin-top:6px; } | |
| .axis.indent { padding-left:182px; padding-right:104px; } | |
| /* timeline */ | |
| .tl-bar { position:absolute; top:2px; bottom:2px; border-radius:2px; | |
| min-width:3px; } | |
| /* forms */ | |
| .seg { display:inline-flex; gap:2px; background:var(--panel-2); | |
| padding:3px; border-radius:5px; flex-wrap:wrap; } | |
| .seg button { border:0; background:transparent; color:var(--muted); | |
| cursor:pointer; font:600 10.5px/1 ui-monospace, Menlo, monospace; | |
| letter-spacing:.08em; padding:6px 12px; border-radius:3px; } | |
| .seg button.on { background:var(--panel); color:var(--ink); | |
| box-shadow:0 1px 2px rgba(0,0,0,.14); } | |
| textarea,input[type=text] { | |
| width:100%; background:var(--code-bg); color:var(--ink); | |
| border:1px solid var(--line); border-radius:5px; padding:10px 12px; | |
| font:12.5px/1.5 ui-monospace, Menlo, monospace; resize:vertical; | |
| } | |
| textarea:focus,input:focus,select:focus { outline:2px solid var(--accent); | |
| border-color:transparent; } | |
| select,input[type=number] { background:var(--code-bg); color:var(--ink); | |
| border:1px solid var(--line); border-radius:4px; padding:6px 8px; | |
| font:11.5px ui-monospace, Menlo, monospace; } | |
| .hint { color:var(--faint); font-size:10.5px; margin:7px 2px 0; | |
| line-height:1.6; } | |
| .ctrl { display:flex; flex-wrap:wrap; gap:14px; margin-top:12px; | |
| align-items:center; } | |
| .ctrl label { display:flex; align-items:center; gap:6px; font-size:10.5px; | |
| color:var(--muted); font-weight:600; letter-spacing:.04em; | |
| text-transform:uppercase; } | |
| .ctrl input[type=number] { width:70px; } | |
| .runrow { display:flex; gap:12px; align-items:center; margin-top:14px; } | |
| button.run { background:var(--accent); color:#fff; border:0; | |
| border-radius:5px; font:700 11px/1 ui-monospace, Menlo, monospace; | |
| letter-spacing:.14em; text-transform:uppercase; padding:10px 22px; | |
| cursor:pointer; } | |
| button.run:hover { filter:brightness(1.07); } | |
| .btn { background:var(--panel-2); border:1px solid var(--line); | |
| color:var(--ink); border-radius:4px; padding:6px 13px; cursor:pointer; | |
| font:600 11px ui-monospace, Menlo, monospace; } | |
| .btn:hover { border-color:var(--accent); } | |
| .btn.danger { color:var(--signal); } | |
| .btn.danger:hover { border-color:var(--signal); } | |
| .btn:disabled { opacity:.5; cursor:default; } | |
| .qstat { color:var(--faint); font-size:11px; } | |
| .err { color:var(--signal); font-size:11.5px; margin-top:10px; | |
| white-space:pre-wrap; } | |
| .empty { color:var(--faint); padding:36px; text-align:center; | |
| font-size:11.5px; } | |
| /* result grid */ | |
| .hitgrid { display:grid; grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); | |
| gap:12px; margin-top:14px; } | |
| .hit { background:var(--panel-2); border-radius:5px; overflow:hidden; | |
| border:1px solid var(--line); cursor:pointer; position:relative; } | |
| .hit img { width:100%; aspect-ratio:4/3; object-fit:cover; display:block; | |
| background:var(--grid); } | |
| .hit .m { padding:7px 10px; font-size:10.5px; color:var(--muted); } | |
| .hit .m b { color:var(--ink); } | |
| .hit .play { position:absolute; inset:0; display:flex; align-items:center; | |
| justify-content:center; aspect-ratio:4/3; opacity:0; transition:opacity .12s; | |
| pointer-events:none; } | |
| .hit:hover .play { opacity:1; } | |
| .hit .play span { width:42px; height:42px; border-radius:50%; | |
| background:rgba(12,16,20,.68); display:flex; align-items:center; | |
| justify-content:center; } | |
| .hit .play svg { width:15px; height:15px; margin-left:2px; fill:#fff; } | |
| /* index and maintenance rows */ | |
| .ixrow { display:flex; align-items:center; gap:12px; padding:8px 0; | |
| border-bottom:1px solid var(--grid); font-size:12px; } | |
| .ixrow:last-child { border-bottom:0; } | |
| .ixrow .grow { flex:1; } | |
| /* map */ | |
| .mapgrid { display:grid; grid-template-columns:1fr 230px; gap:16px; } | |
| .canvasbox { position:relative; } | |
| canvas.viz { width:100%; display:block; border-radius:5px; | |
| background:var(--panel-2); cursor:crosshair; } | |
| .legend { font-size:11.5px; } | |
| .legend h3 { font-size:9.5px; letter-spacing:.16em; text-transform:uppercase; | |
| color:var(--faint); margin:0 0 6px; font-weight:600; } | |
| .legend .li { display:flex; align-items:center; gap:7px; padding:2px 0; | |
| color:var(--muted); } | |
| .legend .li i { width:9px; height:9px; border-radius:50%; flex:none; } | |
| .legend .li.noise i { background:transparent; | |
| border:1.5px solid var(--faint); } | |
| .legend .cnt { margin-left:auto; color:var(--faint); } | |
| .hovercard { position:absolute; z-index:20; pointer-events:none; width:190px; | |
| background:var(--panel); border:1px solid var(--line); border-radius:6px; | |
| box-shadow:var(--shadow); overflow:hidden; display:none; } | |
| .hovercard .ph { width:190px; height:120px; background:var(--panel-2); | |
| display:flex; align-items:center; justify-content:center; | |
| color:var(--faint); font-size:10.5px; } | |
| .hovercard img { width:100%; display:block; } | |
| .hovercard .m { padding:7px 10px; font-size:10.5px; color:var(--muted); } | |
| .hovercard .m b { color:var(--ink); display:block; font-size:11px; } | |
| /* player */ | |
| .player-back { position:fixed; inset:0; background:rgba(8,10,13,.72); | |
| z-index:100; display:none; align-items:center; justify-content:center; | |
| backdrop-filter:blur(3px); } | |
| .player-back.on { display:flex; } | |
| .player { width:min(760px, 92vw); background:var(--panel); | |
| border:1px solid var(--line); border-radius:8px; box-shadow:var(--shadow); | |
| overflow:hidden; } | |
| .player video { width:100%; display:block; background:#000; | |
| min-height:280px; } | |
| .player .loading { height:340px; display:flex; flex-direction:column; | |
| align-items:center; justify-content:center; color:var(--faint); | |
| font-size:12px; gap:10px; } | |
| .spin { width:16px; height:16px; border:2px solid var(--line); | |
| border-top-color:var(--accent); border-radius:50%; | |
| animation:spin .8s linear infinite; } | |
| @keyframes spin { to { transform:rotate(360deg); } } | |
| @media (prefers-reduced-motion: reduce) { .spin { animation:none; } } | |
| .player .bar { display:flex; align-items:center; gap:12px; | |
| padding:10px 14px; } | |
| .player .bar b { font-size:12px; } | |
| .player .bar .t { color:var(--faint); font-size:11px; } | |
| .player .bar button { margin-left:auto; background:var(--panel-2); | |
| border:1px solid var(--line); color:var(--muted); border-radius:4px; | |
| padding:6px 12px; cursor:pointer; font-size:11px; } | |
| .player .bar button + button { margin-left:0; } | |
| .player .bar button:hover { color:var(--ink); } | |
| </style> | |
| </head> | |
| <body> | |
| <aside> | |
| <div class="brand"><b>ELIDE<i>DB</i></b> | |
| <div class="sub">timestamp-native multimodal store</div></div> | |
| <div class="rail-h lbl">Stores</div> | |
| <div class="stores" id="stores"></div> | |
| <div class="railfoot">Parquet tables under a transaction log.<br> | |
| Reads are byte-range and counted.</div> | |
| </aside> | |
| <div class="main"> | |
| <div class="topbar"> | |
| <h1 id="store-title"></h1> | |
| <nav id="views" role="tablist"> | |
| <button data-v="overview" class="on">Overview</button> | |
| <button data-v="analytics">Analytics</button> | |
| <button data-v="search">Search</button> | |
| <button data-v="data">Data & Storage</button> | |
| <button data-v="arch">Architecture</button> | |
| <button data-v="map">Map</button> | |
| <button data-v="geo" id="geo-btn" style="display:none">Geo</button> | |
| <button data-v="ops">Ops</button> | |
| </nav> | |
| <span class="topstat tnum" id="store-sub"></span> | |
| </div> | |
| <div class="content"> | |
| <div class="view on" id="v-overview"></div> | |
| <div class="view" id="v-analytics"></div> | |
| <div class="view" id="v-search"> | |
| <div class="panel"> | |
| <div class="hd"><h2>Search</h2> | |
| <span class="note">every result is a playable clip</span></div> | |
| <div class="bd"> | |
| <div class="seg" id="qmodes"> | |
| <button data-q="set" class="on">Event</button> | |
| <button data-q="predicate">Predicate</button> | |
| <button data-q="sql">SQL</button> | |
| <button data-q="window">Window</button> | |
| </div> | |
| <div id="q-set" style="margin-top:12px"> | |
| <input type="text" id="set-input" placeholder="Describe an event"> | |
| <div class="hint"> | |
| Returns up to K clips and only clips the index is confident in; | |
| a query the corpus cannot answer returns nothing. Scores fuse | |
| every channel this store carries. | |
| </div> | |
| <div class="ctrl"> | |
| <label>Max results | |
| <input type="number" id="set-k" value="10" min="1" max="24"></label> | |
| <label title="Verify each candidate with the geometry tracker: the named objects must be present, move, and satisfy the stated relation. Slower."> | |
| <input type="checkbox" id="set-audit"> Geometry audit</label> | |
| <label>Stream <select id="set-stream"><option value="">any</option></select></label> | |
| <label>From s <input type="number" id="set-t0"></label> | |
| <label>To s <input type="number" id="set-t1"></label> | |
| </div> | |
| </div> | |
| <div id="q-predicate" style="display:none; margin-top:12px"> | |
| <div class="ctrl" style="margin-top:0"> | |
| <label>Table <select id="pred-table"></select></label> | |
| <label>Column <select id="pred-col"></select></label> | |
| <label>Op <select id="pred-op"> | |
| <option value=">=">≥</option><option value="<=">≤</option> | |
| <option value="==">=</option><option value="between">between</option> | |
| </select></label> | |
| <label>Value <input type="number" step="any" id="pred-val" style="width:110px"></label> | |
| <label id="pred-val2-wrap" style="display:none">and | |
| <input type="number" step="any" id="pred-val2" style="width:110px"></label> | |
| </div> | |
| <div class="hint">Filter rows by a numeric column. The result | |
| reports how many files and bytes the scan touched versus | |
| skipped.</div> | |
| </div> | |
| <div id="q-sql" style="display:none; margin-top:12px"> | |
| <textarea id="sql-input" rows="4"></textarea> | |
| <div class="hint">Read-only SQL over every table in this store.</div> | |
| </div> | |
| <div id="q-window" style="display:none; margin-top:12px"> | |
| <input type="text" id="win-input" | |
| placeholder="start,end in seconds from capture start"> | |
| <div class="hint">Retrieve one aligned time window across every | |
| table: frames as playable streams, sensors as rows.</div> | |
| </div> | |
| <div class="runrow"> | |
| <button class="run" id="run">Run</button> | |
| <span class="qstat" id="q-stat"></span> | |
| </div> | |
| <div id="q-out"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="view" id="v-data"></div> | |
| <div class="view" id="v-storage"> | |
| <div class="panel"> | |
| <div class="hd"><h2>Storage</h2> | |
| <span class="note">transaction log and physical file layout, | |
| read from the Parquet footers</span></div> | |
| <div class="bd"> | |
| <div class="seg" id="storage-tables"></div> | |
| <div id="storage-out"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="view" id="v-arch"> | |
| <div class="seg" id="arch-seg"> | |
| <button data-p="db" class="on">Database engine</button> | |
| <button data-p="model">Retrieval model</button> | |
| </div> | |
| <div id="arch-pane-db"></div> | |
| <div id="arch-pane-model" style="display:none"> | |
| <div id="arch-wrap"> | |
| <svg id="arch-svg"><g id="arch-cam"></g></svg> | |
| <div id="arch-title"></div> | |
| <div id="arch-detail"></div> | |
| <div id="arch-hint">drag to pan · wheel to zoom · click a block | |
| for its schema · double click resets</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="view" id="v-map"> | |
| <div class="panel"> | |
| <div class="hd"><h2>Embedding map</h2> | |
| <span class="note">2D layout for browsing only; retrieval ranks | |
| in the full vector space<span id="map-src"></span></span></div> | |
| <div class="bd"> | |
| <div class="mapgrid"> | |
| <div class="canvasbox"> | |
| <canvas id="scatter" class="viz" height="470"></canvas> | |
| <div class="hovercard" id="card"> | |
| <div class="ph">decoding</div><div class="m"></div></div> | |
| </div> | |
| <div class="legend" id="legend"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="view" id="v-geo"> | |
| <div class="panel"> | |
| <div class="hd"><h2>Geo trace</h2> | |
| <span class="note" id="geo-note"></span></div> | |
| <div class="bd"> | |
| <canvas id="route" class="viz" height="420" style="cursor:default"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="view" id="v-ops"></div> | |
| </div> | |
| </div> | |
| <div class="chart-tip" id="chart-tip"></div> | |
| <div class="player-back" id="player-back"> | |
| <div class="player"> | |
| <div id="player-media"><div class="loading"><div class="spin"></div> | |
| decoding and muxing the clip</div></div> | |
| <div class="bar"><b id="player-title"></b> | |
| <span class="t" id="player-time"></span> | |
| <button id="player-pin">open as window</button> | |
| <button id="player-close">close</button></div> | |
| </div> | |
| </div> | |
| <script> | |
| ; | |
| /* validated categorical palette (fixed order, never cycled) */ | |
| const CAT = { | |
| light:["#2a78d6","#eb6834","#1baf7a","#eda100","#e87ba4","#008300","#4a3aa7","#e34948"], | |
| dark: ["#3987e5","#d95926","#199e70","#c98500","#d55181","#008300","#9085e9","#e66767"], | |
| otherLight:"#9aa5b0", otherDark:"#5c6873", | |
| }; | |
| const $ = id => document.getElementById(id); | |
| const cssVar = n => getComputedStyle(document.documentElement).getPropertyValue(n).trim(); | |
| const mode = () => matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; | |
| const fmtB = b => b >= 1e9 ? (b/1e9).toFixed(2)+" GB" : b >= 1e6 ? (b/1e6).toFixed(1)+" MB" : (b/1e3).toFixed(0)+" KB"; | |
| const fmtN = n => n >= 1e6 ? (n/1e6).toFixed(2)+" M" : n >= 1e3 ? (n/1e3).toFixed(1)+" k" : ""+n; | |
| const esc = s => String(s).replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c])); | |
| const PLAY = `<span><svg viewBox="0 0 14 14"><path d="M2 1l11 6-11 6z"/></svg></span>`; | |
| let STORES = [], CUR = null, MAP = null, GEO = null, PIN = null; | |
| const rel = ns => ((ns - CUR.min_ts) / 1e9); | |
| async function j(url, opts) { | |
| const r = await fetch(url, opts); | |
| const d = await r.json().catch(() => ({error: r.statusText})); | |
| if (!r.ok) throw new Error(d.error || r.statusText); | |
| return d; | |
| } | |
| /* shared chart tooltip */ | |
| const tip = $("chart-tip"); | |
| function tipShow(e, html) { | |
| tip.innerHTML = html; tip.style.display = "block"; | |
| const x = Math.min(e.clientX + 14, innerWidth - 300); | |
| tip.style.left = x + "px"; | |
| tip.style.top = Math.min(e.clientY + 14, innerHeight - 90) + "px"; | |
| } | |
| function tipHide() { tip.style.display = "none"; } | |
| function wireTips(root) { | |
| root.querySelectorAll("[data-tip]").forEach(el => { | |
| el.addEventListener("pointermove", e => tipShow(e, el.dataset.tip)); | |
| el.addEventListener("pointerleave", tipHide); | |
| }); | |
| } | |
| /* stores rail */ | |
| async function loadStores() { | |
| STORES = await j("/api/stores"); | |
| $("stores").innerHTML = STORES.map(s => ` | |
| <div class="store-card" data-k="${s.key}"> | |
| <div class="n">${esc(s.name)}</div> | |
| <div class="m tnum">${s.tables.length} tables · ${fmtN(s.rows)} rows · ${fmtB(s.db_bytes)}</div> | |
| </div>`).join("") || `<div class="empty">no stores found</div>`; | |
| document.querySelectorAll(".store-card").forEach(el => | |
| el.addEventListener("click", () => selectStore(el.dataset.k))); | |
| if (STORES.length) selectStore(STORES[0].key); | |
| } | |
| async function selectStore(key, refresh) { | |
| if (refresh) STORES = await j("/api/stores"); | |
| CUR = STORES.find(s => s.key === key); | |
| MAP = GEO = PIN = IXDATA = ANA = null; ARCH = null; | |
| document.querySelectorAll(".store-card").forEach(el => | |
| el.classList.toggle("on", el.dataset.k === key)); | |
| $("store-title").textContent = CUR.name; | |
| $("store-sub").textContent = | |
| `${fmtB(CUR.db_bytes)} · ${Math.round((CUR.max_ts - CUR.min_ts)/1e9)} s span`; | |
| $("set-stream").innerHTML = `<option value="">any</option>`; | |
| renderOverview(); renderStorageTabs(); | |
| $("geo-btn").style.display = "none"; | |
| j(`/api/geo?store=${key}`).then(g => { | |
| GEO = g; | |
| if (g.points.length) $("geo-btn").style.display = ""; | |
| }).catch(() => {}); | |
| j(`/api/map?store=${key}`).then(mp => { | |
| const names = [...new Set((mp.points || []).map(p => p.s))].sort(); | |
| $("set-stream").innerHTML = `<option value="">any</option>` + | |
| names.map(n => `<option>${esc(n)}</option>`).join(""); | |
| }).catch(() => {}); | |
| const cur = document.querySelector("#views button.on").dataset.v; | |
| routeView(cur); | |
| } | |
| /* overview: identity, coverage timeline, tables */ | |
| function fmtDur(sec) { | |
| if (sec == null) return ""; | |
| if (sec >= 5400) return (sec / 3600).toFixed(1) + " h"; | |
| if (sec >= 90) return Math.round(sec / 60) + " min"; | |
| return Math.round(sec) + " s"; | |
| } | |
| function renderOverview() { | |
| const s = CUR; | |
| const extentS = Math.round((s.max_ts - s.min_ts) / 1e9); | |
| const covS = s.covered_s != null ? Math.round(s.covered_s) : null; | |
| const ratio = s.raw_bytes > 0 && s.db_bytes > 0 | |
| ? s.raw_bytes / s.db_bytes : null; | |
| const tiles = []; | |
| if (s.raw_bytes > 0) | |
| tiles.push({k:"source data", v: fmtB(s.raw_bytes), | |
| d:`${s.raw_files} original file${s.raw_files === 1 ? "" : "s"} as ingested`}); | |
| tiles.push({k:"database", v: fmtB(s.db_bytes), hero:true, | |
| d: s.standalone ? "serves without the source" | |
| : "depends on linked media"}); | |
| if (ratio != null && ratio > 1.05) | |
| tiles.push({k:"compression", v: ratio.toFixed(1) + "x", | |
| d:"source bytes per database byte", hero:true}); | |
| tiles.push({k:"media", v: fmtB(s.media_bytes + s.linked_bytes), | |
| d: s.linked_bytes > 0 ? "linked from another store" | |
| : "managed, byte-range decoded"}); | |
| tiles.push({k:"tables", v: fmtB(s.bytes), | |
| d:`${s.tables.length} Parquet tables · ${fmtN(s.rows)} rows`}); | |
| tiles.push({k:"coverage", | |
| v: covS != null ? fmtDur(covS) : fmtDur(extentS), | |
| d: covS != null && extentS > covS * 1.2 | |
| ? `recorded content over a ${fmtDur(extentS)} extent` | |
| : "recorded content"}); | |
| tiles.push({k:"vectors", v: fmtN(s.vec_rows), | |
| d:`${s.vec_tables} vector table${s.vec_tables === 1 ? "" : "s"}`}); | |
| let h = `<div class="tiles">` + tiles.map(t => | |
| `<div class="tile${t.hero?" hero":""}"><div class="k">${t.k}</div> | |
| <div class="v tnum">${t.v}</div>${t.d?`<div class="d">${t.d}</div>`:""}</div>`).join("") + `</div>`; | |
| if (s.raw_missing > 0) | |
| h += `<div class="panel"><div class="bd"> | |
| <span class="pill">source removed</span> | |
| <span class="qstat">${s.raw_missing} original source | |
| file${s.raw_missing === 1 ? "" : "s"} no longer exist on disk. | |
| ${s.standalone ? "The store serves anyway; that is the point." : | |
| "This store still depends on linked media."}</span></div></div>`; | |
| if (s.linked_bytes > 0) | |
| h += `<div class="panel" style="border-color:var(--signal)"><div class="bd"> | |
| <span class="pill warn">linked media</span> | |
| <span class="qstat">${fmtB(s.linked_bytes)} of media is symlinked | |
| from another store. Remove the target and this store stops | |
| serving frames. Copy the media in to make it standalone.</span></div></div>`; | |
| if (s.external_bytes > 0) | |
| h += `<div class="panel" style="border-color:var(--signal)"><div class="bd"> | |
| <span class="pill warn">external media</span> | |
| <span class="qstat">${fmtB(s.external_bytes)} of media is referenced | |
| outside the store. Run <b>elidedb adopt ${esc(s.key)}</b> to make the | |
| database standalone.</span></div></div>`; | |
| const lo = s.min_ts, hi = s.max_ts || lo + 1; | |
| h += `<div class="panel"><div class="hd"><h2>Coverage</h2> | |
| <span class="note">each table on the store timeline; a query window | |
| outside a bar never opens that table's files</span></div><div class="bd">`; | |
| const kindColor = {frame_index:"var(--accent)", timeseries:"#7d8fa3", | |
| embeddings:"#8a7fe0", centroids:"#8a7fe0"}; | |
| for (const t of s.tables) { | |
| if (!t.rows || t.table === "centroids") continue; | |
| const l = ((t.min_ts - lo) / (hi - lo)) * 100; | |
| const w = Math.max(((t.max_ts - t.min_ts) / (hi - lo)) * 100, 0.35); | |
| h += `<div class="bar-row"><div class="id">${esc(t.table)}</div> | |
| <div class="bar-track"><div class="tl-bar" | |
| style="left:${l.toFixed(2)}%;width:${w.toFixed(2)}%;background:${kindColor[t.kind]||"var(--accent)"}" | |
| data-tip="<b>${esc(t.table)}</b>${fmtN(t.rows)} rows · ${fmtB(t.bytes)}"></div></div> | |
| <div class="bar-val tnum">${fmtN(t.rows)} · ${fmtB(t.bytes)}</div></div>`; | |
| } | |
| h += `<div class="axis indent tnum"><span>0 s</span> | |
| <span>${Math.round((hi-lo)/2e9)} s</span> | |
| <span>${Math.round((hi-lo)/1e9)} s</span></div></div></div>`; | |
| h += `<div class="panel"><div class="hd"><h2>Tables</h2> | |
| <span class="note">version counts commits; older versions stay readable</span></div> | |
| <div class="bd"><div class="tablewrap"><table><thead><tr> | |
| <th>table</th><th>kind</th><th class="r">rows</th><th class="r">size</th> | |
| <th class="r">files</th><th class="r">version</th><th>window</th></tr></thead><tbody>`; | |
| for (const t of s.tables) { | |
| h += `<tr><td>${esc(t.table)}</td> | |
| <td><span class="badge">${t.kind}</span></td> | |
| <td class="r tnum">${t.rows.toLocaleString()}</td> | |
| <td class="r tnum">${fmtB(t.bytes)}</td> | |
| <td class="r tnum">${t.files}</td><td class="r tnum">v${t.version}</td> | |
| <td class="tnum">${t.table === "centroids" ? "" : | |
| `+${rel(t.min_ts).toFixed(1)} to +${rel(t.max_ts).toFixed(1)} s`}</td></tr>`; | |
| } | |
| h += `</tbody></table></div></div></div>`; | |
| $("v-overview").innerHTML = h; | |
| wireTips($("v-overview")); | |
| } | |
| /* analytics: general to any datastore */ | |
| let ANA = null; | |
| async function loadAnalytics() { | |
| const el = $("v-analytics"); | |
| if (!ANA) { | |
| el.innerHTML = `<div class="empty">reading logs and footers</div>`; | |
| try { ANA = await j(`/api/analytics?store=${CUR.key}`); } | |
| catch (e) { el.innerHTML = `<div class="err">${esc(e.message)}</div>`; return; } | |
| } | |
| const a = ANA, spanS = (a.span.hi - a.span.lo) / 1e9; | |
| const totB = a.tables.reduce((x, t) => x + t.bytes, 0); | |
| const totR = a.tables.reduce((x, t) => x + t.rows, 0); | |
| let h = `<div class="tiles"> | |
| <div class="tile hero"><div class="k">bytes on disk</div> | |
| <div class="v tnum">${fmtB(totB)}</div> | |
| <div class="d">${a.tables.length} tables</div></div> | |
| <div class="tile"><div class="k">rows</div> | |
| <div class="v tnum">${fmtN(totR)}</div></div> | |
| <div class="tile"><div class="k">commits</div> | |
| <div class="v tnum">${a.commit_total}</div> | |
| <div class="d">across all logs</div></div> | |
| <div class="tile"><div class="k">vector tables</div> | |
| <div class="v tnum">${a.vectors.length}</div> | |
| <div class="d">${a.base_rows ? fmtN(a.base_rows) + " base units" : ""}</div></div> | |
| <div class="tile"><div class="k">indexes</div> | |
| <div class="v tnum">${a.indexes.bptree.length + a.indexes.ann.length}</div> | |
| <div class="d">${a.indexes.bptree.length} btree · ${a.indexes.ann.length} ann</div></div> | |
| <div class="tile"><div class="k">span</div> | |
| <div class="v tnum">${Math.round(spanS)}<small> s</small></div></div> | |
| </div>`; | |
| /* storage by table: one measure, one hue */ | |
| const byBytes = [...a.tables].filter(t => t.rows).sort((x, y) => y.bytes - x.bytes); | |
| const maxB = Math.max(...byBytes.map(t => t.bytes), 1); | |
| h += `<div class="panel"><div class="hd"><h2>Storage by table</h2> | |
| <span class="note">compressed bytes on disk</span></div><div class="bd">`; | |
| for (const t of byBytes) { | |
| h += `<div class="bar-row"><div class="id">${esc(t.table)}</div> | |
| <div class="bar-track"><div class="bar-fill" | |
| style="width:${(100 * t.bytes / maxB).toFixed(2)}%" | |
| data-tip="<b>${esc(t.table)}</b>${fmtB(t.bytes)} · ${t.rows.toLocaleString()} rows · ${t.files} files · ${t.bpr ?? "?"} bytes per row"></div></div> | |
| <div class="bar-val tnum">${fmtB(t.bytes)}</div></div>`; | |
| } | |
| h += `</div></div>`; | |
| /* temporal density: rows per bucket per table, footer stats only */ | |
| const dTables = Object.keys(a.density); | |
| if (dTables.length) { | |
| h += `<div class="panel"><div class="hd"><h2>Row density over time</h2> | |
| <span class="note">rows per time bucket from row group statistics; | |
| this is the elision map a range query prunes against</span></div><div class="bd">`; | |
| for (const name of dTables) { | |
| const cells = a.density[name], mx = Math.max(...cells, 1); | |
| const strip = cells.map((v, i) => { | |
| const t0 = (spanS * i / a.buckets).toFixed(0); | |
| const t1 = (spanS * (i + 1) / a.buckets).toFixed(0); | |
| return `<div class="heat-cell" | |
| style="background:${v ? `color-mix(in srgb, var(--accent) ${Math.round(12 + 88 * v / mx)}%, var(--panel-2))` : "transparent"}" | |
| data-tip="<b>${esc(name)}</b>~${v.toLocaleString()} rows · +${t0} to +${t1} s"></div>`; | |
| }).join(""); | |
| h += `<div class="heat-row"><div class="id">${esc(name)}</div> | |
| <div class="heat-strip">${strip}</div> | |
| <div class="bar-val tnum">${fmtN(cells.reduce((x, y) => x + y, 0))}</div></div>`; | |
| } | |
| h += `<div class="axis indent tnum"><span>0 s</span> | |
| <span>${Math.round(spanS / 2)} s</span><span>${Math.round(spanS)} s</span></div> | |
| </div></div>`; | |
| } | |
| /* vector inventory */ | |
| if (a.vectors.length) { | |
| h += `<div class="panel"><div class="hd"><h2>Vector inventory</h2> | |
| <span class="note">every embedding table, with coverage against the | |
| store's base units</span></div><div class="bd"> | |
| <div class="tablewrap"><table><thead><tr> | |
| <th>table</th><th>encoder</th><th class="r">dim</th> | |
| <th class="r">rows</th><th>coverage</th><th class="r">size</th> | |
| </tr></thead><tbody>`; | |
| for (const v of a.vectors) { | |
| const cov = v.per_base == null ? "" : | |
| `<div class="bar-track" style="width:110px;display:inline-block;vertical-align:middle"> | |
| <div class="bar-fill${v.per_base < 1 ? " dim" : ""}" | |
| style="width:${Math.min(100, 100 * Math.min(v.per_base, 1)).toFixed(0)}%" | |
| data-tip="<b>${esc(v.table)}</b>${v.per_base} vectors per base unit"></div> | |
| </div> <span class="tnum" style="font-size:10.5px">${v.per_base}x</span>`; | |
| h += `<tr><td>${esc(v.table)}</td> | |
| <td style="font-size:10.5px">${esc(v.model)}</td> | |
| <td class="r tnum">${v.dim ?? ""}</td> | |
| <td class="r tnum">${v.rows.toLocaleString()}</td> | |
| <td>${cov}</td> | |
| <td class="r tnum">${fmtB(v.bytes)}</td></tr>`; | |
| } | |
| h += `</tbody></table></div></div></div>`; | |
| } | |
| /* fitted retrieval profile */ | |
| if (a.fitted && a.fitted.set_weights) { | |
| const f = a.fitted; | |
| const rowOf = (w, title, alpha, q, r) => { | |
| const entries = Object.entries(w).sort((x, y) => y[1] - x[1]); | |
| const mx = Math.max(...entries.map(e => e[1]), 1); | |
| let g = `<div style="flex:1;min-width:280px"> | |
| <div class="lbl" style="margin-bottom:8px">${title}</div>`; | |
| for (const [c, v] of entries) { | |
| g += `<div class="bar-row" style="grid-template-columns:56px 1fr 40px"> | |
| <div class="id">${esc(c)}</div> | |
| <div class="bar-track"><div class="bar-fill${v ? "" : " dim"}" | |
| style="width:${(100 * v / mx).toFixed(1)}%" | |
| data-tip="<b>${esc(c)}</b>ordering weight ${v}"></div></div> | |
| <div class="bar-val tnum">${v}</div></div>`; | |
| } | |
| g += `<div style="margin-top:8px;display:flex;gap:6px;flex-wrap:wrap"> | |
| <span class="pill">cut ${alpha}</span> | |
| <span class="pill">filter q ${Number(q).toFixed(2)}</span> | |
| <span class="pill">dedup r ${r}</span></div></div>`; | |
| return g; | |
| }; | |
| h += `<div class="panel"><div class="hd"><h2>Retrieval profile</h2> | |
| <span class="note">per-store fitted roles: which channels order, | |
| filter, and where the returned set ends · holdout objective | |
| ${f.loqo_mean ?? ""}</span></div> | |
| <div class="bd"><div style="display:flex;gap:32px;flex-wrap:wrap">` + | |
| rowOf(f.set_weights_dir || {}, "directional queries", | |
| f.cut_alpha_dir ?? 0, f.filter_quantile_dir ?? 0, f.nms_r_dir ?? 0) + | |
| rowOf(f.set_weights || {}, "descriptive queries", | |
| f.cut_alpha ?? 0, f.filter_quantile ?? 0, f.nms_r ?? 0) + | |
| `</div></div></div>`; | |
| } | |
| /* write history */ | |
| if (a.commits.length) { | |
| h += `<div class="panel"><div class="hd"><h2>Write history</h2> | |
| <span class="note">latest ${a.commits.length} of ${a.commit_total} | |
| commits, straight from the transaction logs</span></div><div class="bd"> | |
| <div class="tablewrap"><table><thead><tr> | |
| <th>table</th><th>op</th><th class="r">version</th> | |
| <th class="r">rows added</th><th>utc</th></tr></thead><tbody>` + | |
| [...a.commits].reverse().map(c => `<tr> | |
| <td>${esc(c.table)}</td> | |
| <td><span class="badge">${esc(c.op)}</span></td> | |
| <td class="r tnum">v${c.version}</td> | |
| <td class="r tnum">${(c.rows || 0).toLocaleString()}</td> | |
| <td class="tnum" style="font-size:10.5px">${esc(c.ts_utc)}</td></tr>`).join("") + | |
| `</tbody></table></div></div></div>`; | |
| } | |
| el.innerHTML = h; | |
| wireTips(el); | |
| } | |
| /* storage */ | |
| function renderStorageTabs() { | |
| const tabs = CUR.tables.filter(t => t.rows); | |
| $("storage-tables").innerHTML = tabs.map((t, i) => | |
| `<button data-t="${esc(t.table)}"${i === 0 ? ' class="on"' : ''}>${esc(t.table)}</button>`).join(""); | |
| $("storage-out").innerHTML = ""; | |
| document.querySelectorAll("#storage-tables button").forEach(b => | |
| b.addEventListener("click", () => { | |
| document.querySelectorAll("#storage-tables button").forEach(x => | |
| x.classList.toggle("on", x === b)); | |
| loadStorage(b.dataset.t); | |
| })); | |
| } | |
| async function drawBytes() { | |
| const host = $("bytes-panel"); if (!host) return; | |
| if (!BYTES) { | |
| host.innerHTML = `<div class="panel"><div class="bd"> | |
| <div class="empty">measuring bytes</div></div></div>`; | |
| try { BYTES = await j(`/api/bytes?store=${CUR.key}`); } | |
| catch (e) { host.innerHTML = ""; return; } | |
| } | |
| const B = BYTES, tot = B.total || 1; | |
| const seg = (label, v, cls, note) => v <= 0 ? "" : ` | |
| <div class="byrow"><div class="bylab ${cls}">${label}</div> | |
| <div class="bybar"><i class="${cls}" style="width:${ | |
| Math.max(0.4, 100 * v / tot)}%"></i></div> | |
| <div class="byval">${fmtB(v)} <span>${(100 * v / tot).toFixed(1)}%</span></div> | |
| <div class="bynote">${note}</div></div>`; | |
| host.innerHTML = `<div class="panel"><div class="hd"> | |
| <h2>Bytes on disk</h2><span class="note">a directory size adds | |
| together four different things; only the first is the database</span> | |
| </div><div class="bd"> | |
| ${seg("live", B.live, "b-live", | |
| "parquet the LOG points at — this IS the database")} | |
| ${seg("orphaned", B.orphaned, "b-orph", | |
| "superseded by a replace/compact commit; still on disk until vacuum")} | |
| ${seg("media", B.media, "b-media", | |
| "managed video renditions, byte-range decoded, never copied raw")} | |
| ${seg("cache", B.cache, "b-cache", | |
| "_cache/ — ITM cross-encoder vision tokens. DISPOSABLE: deleting it " | |
| + "costs recompute time, never correctness")} | |
| <div class="bynote" style="margin-top:9px"> | |
| models are <b>not</b> in the store — they live in models/ at the repo | |
| root (iv2_stage2_1b alone is 2.8 GB), which is why “store size” and | |
| “what this system needs on disk” are different numbers. | |
| </div> | |
| <div class="dbsec" style="margin-top:14px">COMPRESSION</div> | |
| <div class="bynote">${fmtB(B.compressed)} stored for | |
| ${fmtB(B.uncompressed)} of column data — <b>${B.ratio}×</b> overall. | |
| The ratio is low because <b>vectors dominate and vectors do not | |
| compress</b>: they are already fp16-packed high-entropy floats, and | |
| that halving happened at write time so it never shows here. | |
| Structural tables do compress — see the ratio column.</div> | |
| <table class="dbt"><tr><th>table</th><th class="n">rows</th> | |
| <th class="n">stored</th><th class="n">raw cols</th> | |
| <th class="n">ratio</th><th>codec · encodings</th></tr>` + | |
| B.tables.map(r => `<tr><td>${esc(r.table)}</td> | |
| <td class="n">${fmtN(r.rows)}</td> | |
| <td class="n">${fmtB(r.compressed)}</td> | |
| <td class="n">${fmtB(r.uncompressed)}</td> | |
| <td class="n"${r.ratio >= 2 ? ' style="color:#5accb9"' : ''}>${r.ratio}×</td> | |
| <td style="color:var(--dim)">${esc((r.codec[0] || "") + " · " + | |
| r.encodings.join(" ").replace(/RLE_DICTIONARY/g, "RLE_DICT"))}</td> | |
| </tr>`).join("") + `</table></div></div>`; | |
| } | |
| async function loadStorage(table) { | |
| table = table || document.querySelector("#storage-tables button.on")?.dataset.t; | |
| if (!table) return; | |
| $("storage-out").innerHTML = `<div class="empty">reading Parquet footers</div>`; | |
| try { | |
| const d = await j(`/api/storage?store=${CUR.key}&table=${encodeURIComponent(table)}`); | |
| const H3 = t => `<div class="lbl" style="margin:18px 0 8px">${t}</div>`; | |
| let h = H3(`transaction log · ${d.history.length} commit${d.history.length > 1 ? "s" : ""} · v${d.version}`); | |
| h += `<div class="tablewrap"><table><thead><tr><th>v</th><th>op</th> | |
| <th class="r">rows added</th><th>utc</th><th>meta</th></tr></thead><tbody>` + | |
| d.history.map(c => `<tr><td class="tnum">v${c.version}</td> | |
| <td><span class="badge">${esc(c.op)}</span></td> | |
| <td class="r tnum">${c.added_rows.toLocaleString()}</td> | |
| <td class="tnum" style="font-size:10.5px">${esc(c.ts_utc)}</td> | |
| <td style="font-size:10.5px;max-width:340px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(JSON.stringify(c.meta))}</td></tr>`).join("") + | |
| `</tbody></table></div>`; | |
| h += H3(`active files · ${d.files.length} · row groups are the pruning unit`); | |
| const maxB = Math.max(...d.files.map(f => f.bytes), 1); | |
| for (const f of d.files) { | |
| const segs = f.row_groups.map(g => { | |
| const t0 = g.min_ts != null ? `+${rel(g.min_ts).toFixed(1)}s` : "?"; | |
| const t1 = g.max_ts != null ? `+${rel(g.max_ts).toFixed(1)}s` : "?"; | |
| return `<div data-tip="<b>row group</b>${g.rows.toLocaleString()} rows · ${fmtB(g.bytes)} · ${t0} to ${t1}" | |
| style="flex:${Math.max(g.bytes, 1)};background:var(--accent);opacity:.85;border-right:2px solid var(--panel);min-width:3px"></div>`; | |
| }).join(""); | |
| h += `<div style="margin:12px 0"> | |
| <div style="display:flex;justify-content:space-between;gap:12px;font-size:11px"> | |
| <span style="overflow:hidden;text-overflow:ellipsis">${esc(f.name)}</span> | |
| <span class="tnum" style="color:var(--faint);flex:none">${f.rows.toLocaleString()} rows · ${f.row_groups.length} groups · ${fmtB(f.bytes)} · footer ${fmtB(f.footer_bytes)}</span></div> | |
| <div style="display:flex;height:20px;border-radius:3px;overflow:hidden;background:var(--panel-2);width:${Math.max(100 * f.bytes / maxB, 10)}%;margin-top:4px">${segs}</div></div>`; | |
| } | |
| if (d.files[0]) | |
| h += `<div class="hint" style="margin-top:14px">columns: ${esc(d.files[0].columns.join(" · "))}</div>`; | |
| $("storage-out").innerHTML = h; | |
| wireTips($("storage-out")); | |
| } catch (e) { | |
| $("storage-out").innerHTML = `<div class="err">${esc(e.message)}</div>`; | |
| } | |
| } | |
| /* embedding map */ | |
| async function loadMap() { | |
| if (!MAP) { | |
| $("legend").innerHTML = `<h3>clusters</h3><div class="li">computing layout</div>`; | |
| try { MAP = await j(`/api/map?store=${CUR.key}`); } | |
| catch (e) { $("legend").innerHTML = `<div class="err">${esc(e.message)}</div>`; return; } | |
| } | |
| // say WHERE the colours come from: a store with no cluster column | |
| // gets k-means over the 2D layout, and that must never be mistaken | |
| // for retrieval structure | |
| const ms = $("map-src"); | |
| if (ms && MAP && MAP.cluster_source) | |
| ms.innerHTML = ` · colours: ${esc(MAP.cluster_source)}`; | |
| drawScatter(); | |
| } | |
| function drawScatter() { | |
| const canvas = $("scatter"), pts = MAP ? MAP.points : []; | |
| const pal = CAT[mode()], other = mode() === "dark" ? CAT.otherDark : CAT.otherLight; | |
| const dpr = devicePixelRatio || 1, W = canvas.clientWidth, H = 470; | |
| canvas.width = W * dpr; canvas.height = H * dpr; | |
| const ctx = canvas.getContext("2d"); ctx.scale(dpr, dpr); | |
| ctx.clearRect(0, 0, W, H); | |
| if (!pts.length) { | |
| ctx.fillStyle = cssVar("--faint"); ctx.font = "12px ui-monospace, Menlo"; | |
| ctx.fillText("no embeddings table in this store", 24, 40); | |
| $("legend").innerHTML = ""; return; | |
| } | |
| const counts = {}; | |
| for (const p of pts) if (p.c >= 0) counts[p.c] = (counts[p.c] || 0) + 1; | |
| const ranked = Object.entries(counts).sort((a, b) => b[1] - a[1]); | |
| const slot = new Map(ranked.map(([c], i) => [+c, i < 8 ? i : -2])); | |
| const xs = pts.map(p => p.x), ys = pts.map(p => p.y); | |
| const x0 = Math.min(...xs), x1 = Math.max(...xs), y0 = Math.min(...ys), y1 = Math.max(...ys); | |
| const pad = 24; | |
| for (const p of pts) { | |
| p.X = pad + ((p.x - x0) / (x1 - x0 || 1)) * (W - 2 * pad); | |
| p.Y = H - pad - ((p.y - y0) / (y1 - y0 || 1)) * (H - 2 * pad); | |
| } | |
| ctx.strokeStyle = cssVar("--grid"); | |
| for (let g = 1; g < 4; g++) { | |
| ctx.beginPath(); ctx.moveTo(W*g/4, 8); ctx.lineTo(W*g/4, H-8); ctx.stroke(); | |
| ctx.beginPath(); ctx.moveTo(8, H*g/4); ctx.lineTo(W-8, H*g/4); ctx.stroke(); | |
| } | |
| const r = pts.length > 400 ? 3.4 : 5; | |
| for (const p of pts) { | |
| ctx.beginPath(); ctx.arc(p.X, p.Y, r, 0, 7); | |
| if (p.c < 0) { ctx.strokeStyle = cssVar("--faint"); ctx.lineWidth = 1.4; ctx.stroke(); } | |
| else { | |
| const s = slot.get(p.c); | |
| ctx.fillStyle = s === -2 ? other : pal[s]; ctx.fill(); | |
| ctx.strokeStyle = cssVar("--panel-2"); ctx.lineWidth = 1; ctx.stroke(); | |
| } | |
| } | |
| let lh = `<h3>clusters</h3>`; | |
| ranked.slice(0, 8).forEach(([c, n], i) => | |
| lh += `<div class="li"><i style="background:${pal[i]}"></i>cluster ${c}<span class="cnt tnum">${n}</span></div>`); | |
| const otherN = ranked.slice(8).reduce((a, [, n]) => a + n, 0); | |
| if (otherN) lh += `<div class="li"><i style="background:${other}"></i>${ranked.length - 8} more<span class="cnt tnum">${otherN}</span></div>`; | |
| const noise = pts.filter(p => p.c < 0).length; | |
| if (noise) lh += `<div class="li noise"><i></i>noise<span class="cnt tnum">${noise}</span></div>`; | |
| lh += `<h3 style="margin-top:14px">windows</h3> | |
| <div class="li">plotted<span class="cnt tnum">${pts.length}</span></div>` + | |
| (MAP.sampled_of > pts.length ? | |
| `<div class="li">of<span class="cnt tnum">${MAP.sampled_of.toLocaleString()}</span></div>` : "") + | |
| `<div class="li">previews<span class="cnt">decoded live</span></div> | |
| <div class="li">click<span class="cnt">plays the clip</span></div>`; | |
| $("legend").innerHTML = lh; | |
| } | |
| let hoverP = null, thumbTimer = null; | |
| $("scatter").addEventListener("pointermove", e => { | |
| if (!MAP) return; | |
| const rect = $("scatter").getBoundingClientRect(); | |
| const mx = e.clientX - rect.left, my = e.clientY - rect.top; | |
| let best = null, bd = 400; | |
| for (const p of MAP.points) { | |
| const d = (p.X - mx) ** 2 + (p.Y - my) ** 2; | |
| if (d < bd) { bd = d; best = p; } | |
| } | |
| const card = $("card"); | |
| if (!best) { card.style.display = "none"; hoverP = null; return; } | |
| if (best !== hoverP) { | |
| hoverP = best; | |
| card.querySelector(".m").innerHTML = | |
| `<b>${esc(best.s)}</b>+${rel(best.t0).toFixed(1)} to +${rel(best.t1).toFixed(1)} s · cluster ${best.c}`; | |
| let ph = card.querySelector(".ph"), img = card.querySelector("img"); | |
| if (img) img.remove(); | |
| if (!ph) { ph = document.createElement("div"); ph.className = "ph"; | |
| ph.textContent = "decoding"; card.prepend(ph); } | |
| clearTimeout(thumbTimer); | |
| thumbTimer = setTimeout(() => { | |
| const im = new Image(); | |
| im.onload = () => { if (hoverP === best) { | |
| card.querySelector(".ph")?.remove(); card.prepend(im); } }; | |
| im.src = `/api/thumb?store=${CUR.key}&stream=${encodeURIComponent(best.s)}&t=${best.t0}&w=380`; | |
| }, 120); | |
| } | |
| card.style.left = Math.min(Math.max(best.X + 14, 0), $("scatter").clientWidth - 200) + "px"; | |
| card.style.top = Math.max(best.Y - 175, 4) + "px"; | |
| card.style.display = "block"; | |
| }); | |
| $("scatter").addEventListener("pointerleave", () => { | |
| $("card").style.display = "none"; hoverP = null; }); | |
| $("scatter").addEventListener("click", () => { | |
| if (hoverP) openPlayer(hoverP.s, hoverP.t0, hoverP.t1); | |
| }); | |
| /* geo */ | |
| function drawGeo() { | |
| if (!GEO || !GEO.points.length) return; | |
| const canvas = $("route"), R = GEO.points; | |
| $("geo-note").textContent = | |
| `table ${GEO.table} · ${R.length} fixes, downsampled · the accent span is covered by frame-indexed video`; | |
| const dpr = devicePixelRatio || 1, W = canvas.clientWidth, H = 420; | |
| canvas.width = W * dpr; canvas.height = H * dpr; | |
| const ctx = canvas.getContext("2d"); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H); | |
| const las = R.map(p => p.la), los = R.map(p => p.lo); | |
| const la0 = Math.min(...las), la1 = Math.max(...las), lo0 = Math.min(...los), lo1 = Math.max(...los); | |
| const sc = Math.cos(((la0 + la1) / 2) * Math.PI / 180); | |
| const k = Math.min((W - 48) / ((lo1 - lo0) * sc || 1), (H - 48) / ((la1 - la0) || 1)); | |
| const px = p => (W - (lo1 - lo0) * sc * k) / 2 + (p.lo - lo0) * sc * k; | |
| const py = p => H - (H - (la1 - la0) * k) / 2 - (p.la - la0) * k; | |
| const vids = CUR.tables.filter(t => t.kind === "frame_index"); | |
| const vlo = Math.min(...vids.map(t => t.min_ts), Infinity); | |
| const vhi = Math.max(...vids.map(t => t.max_ts), -Infinity); | |
| ctx.strokeStyle = "#7d8fa3"; ctx.lineWidth = 2; ctx.lineJoin = "round"; | |
| ctx.beginPath(); R.forEach((p, i) => i ? ctx.lineTo(px(p), py(p)) : ctx.moveTo(px(p), py(p))); ctx.stroke(); | |
| ctx.strokeStyle = cssVar("--accent"); ctx.lineWidth = 6; ctx.lineCap = "round"; | |
| ctx.beginPath(); let st = false; | |
| for (const p of R) if (p.t >= vlo && p.t <= vhi) { | |
| st ? ctx.lineTo(px(p), py(p)) : ctx.moveTo(px(p), py(p)); st = true; } | |
| ctx.stroke(); | |
| ctx.fillStyle = cssVar("--muted"); ctx.font = "11px ui-monospace, Menlo"; | |
| const s0 = R[0], s1 = R[R.length - 1]; | |
| ctx.beginPath(); ctx.arc(px(s0), py(s0), 4, 0, 7); ctx.fill(); | |
| ctx.fillText("start", px(s0) + 8, py(s0) + 4); | |
| ctx.beginPath(); ctx.arc(px(s1), py(s1), 4, 0, 7); ctx.fill(); | |
| ctx.fillText("end", px(s1) + 8, py(s1) + 4); | |
| } | |
| /* clip player */ | |
| function openPlayer(stream, t0, t1) { | |
| PIN = {s: stream, t0, t1}; | |
| $("player-back").classList.add("on"); | |
| $("player-title").textContent = stream; | |
| const durS = (t1 - t0) / 1e9; | |
| $("player-time").textContent = | |
| `+${rel(t0).toFixed(2)} to +${rel(t1).toFixed(2)} s · ${durS.toFixed(1)} s` + | |
| (durS > 30 ? " · first 30 s previewed" : ""); | |
| $("player-media").innerHTML = | |
| `<div class="loading"><div class="spin"></div>decoding and muxing the clip</div>`; | |
| const url = `/api/clip?store=${CUR.key}&stream=${encodeURIComponent(stream)}&t0=${t0}&t1=${t1}&w=720`; | |
| fetch(url).then(async r => { | |
| if (!r.ok) { | |
| let d = {}; | |
| try { d = await r.json(); } catch (_) {} | |
| const why = d.error || `server returned ${r.status}`; | |
| $("player-media").innerHTML = | |
| `<div class="loading"><b>${esc(why)}</b>` + | |
| (d.detail ? `<div style="margin-top:6px">${esc(d.detail)}</div>` : "") + | |
| `</div>`; | |
| return; | |
| } | |
| const v = document.createElement("video"); | |
| v.controls = true; v.autoplay = true; v.loop = true; v.playsInline = true; | |
| v.src = URL.createObjectURL(await r.blob()); | |
| v.addEventListener("loadeddata", () => { | |
| $("player-media").innerHTML = ""; $("player-media").appendChild(v); | |
| }); | |
| v.addEventListener("error", () => { | |
| $("player-media").innerHTML = | |
| `<div class="loading">the clip built but this browser refused to play it</div>`; | |
| }); | |
| }).catch(e => { | |
| $("player-media").innerHTML = | |
| `<div class="loading">could not reach the server: ${esc(String(e))}</div>`; | |
| }); | |
| } | |
| function closePlayer() { | |
| $("player-back").classList.remove("on"); | |
| $("player-media").querySelector("video")?.pause(); | |
| $("player-media").innerHTML = ""; | |
| } | |
| $("player-close").addEventListener("click", closePlayer); | |
| $("player-back").addEventListener("click", e => { | |
| if (e.target === $("player-back")) closePlayer(); }); | |
| addEventListener("keydown", e => { if (e.key === "Escape") closePlayer(); }); | |
| $("player-pin").addEventListener("click", () => { | |
| if (!PIN) return; | |
| closePlayer(); | |
| setQMode("window"); | |
| $("win-input").value = `${rel(PIN.t0).toFixed(2)},${rel(PIN.t1).toFixed(2)}`; | |
| document.querySelector('#views button[data-v="search"]').click(); | |
| }); | |
| /* search console */ | |
| function setQMode(m) { | |
| document.querySelectorAll("#qmodes button").forEach(b => | |
| b.classList.toggle("on", b.dataset.q === m)); | |
| $("q-set").style.display = m === "set" ? "" : "none"; | |
| $("q-predicate").style.display = m === "predicate" ? "" : "none"; | |
| $("q-sql").style.display = m === "sql" ? "" : "none"; | |
| $("q-window").style.display = m === "window" ? "" : "none"; | |
| if (m === "predicate") fillPredicate(); | |
| if (m === "sql" && !$("sql-input").value) | |
| $("sql-input").value = "SELECT count(*) AS rows, min(ts), max(ts) FROM " + | |
| (CUR.tables.find(t => t.kind === "timeseries")?.table || CUR.tables[0].table); | |
| } | |
| document.querySelectorAll("#qmodes button").forEach(b => | |
| b.addEventListener("click", () => setQMode(b.dataset.q))); | |
| let IXDATA = null; | |
| async function fillPredicate() { | |
| IXDATA = IXDATA || await j(`/api/indexes?store=${CUR.key}`).catch(() => ({indexable:{}})); | |
| const tsel = $("pred-table"); | |
| tsel.innerHTML = Object.keys(IXDATA.indexable).map(t => `<option>${esc(t)}</option>`).join(""); | |
| tsel.onchange = () => { | |
| $("pred-col").innerHTML = (IXDATA.indexable[tsel.value] || []) | |
| .map(c => `<option>${esc(c)}</option>`).join(""); | |
| }; | |
| tsel.onchange(); | |
| } | |
| $("pred-op").addEventListener("change", () => | |
| $("pred-val2-wrap").style.display = $("pred-op").value === "between" ? "" : "none"); | |
| $("run").addEventListener("click", runQuery); | |
| $("set-input").addEventListener("keydown", e => { if (e.key === "Enter") runQuery(); }); | |
| $("win-input").addEventListener("keydown", e => { if (e.key === "Enter") runQuery(); }); | |
| async function runQuery() { | |
| const m = document.querySelector("#qmodes button.on").dataset.q; | |
| const out = $("q-out"), stat = $("q-stat"); | |
| out.innerHTML = ""; stat.textContent = "running"; | |
| try { | |
| let body = {store: CUR.key}; | |
| if (m === "set") { | |
| body.type = "context"; body.text = $("set-input").value; | |
| body.k = +$("set-k").value || 10; | |
| const cs = $("set-stream").value; if (cs) body.streams = [cs]; | |
| if ($("set-audit").checked) body.rerank = true; | |
| const ca = $("set-t0").value, cb = $("set-t1").value; | |
| if (ca !== "") body.t0 = Math.round(CUR.min_ts + +ca * 1e9); | |
| if (cb !== "") body.t1 = Math.round(CUR.min_ts + +cb * 1e9); | |
| } | |
| if (m === "predicate") { | |
| body.type = "predicate"; body.table = $("pred-table").value; | |
| body.column = $("pred-col").value; body.op = $("pred-op").value; | |
| body.value = $("pred-val").value; | |
| if (body.op === "between") body.value2 = $("pred-val2").value; | |
| } | |
| if (m === "sql") Object.assign(body, {type: "sql", sql: $("sql-input").value}); | |
| if (m === "window") { | |
| const [a, b] = $("win-input").value.split(",").map(Number); | |
| Object.assign(body, {type: "window", | |
| t0: Math.round(CUR.min_ts + a * 1e9), t1: Math.round(CUR.min_ts + b * 1e9)}); | |
| } | |
| const r = await j("/api/query", {method: "POST", | |
| headers: {"Content-Type": "application/json"}, body: JSON.stringify(body)}); | |
| if (m === "sql" || m === "predicate") { | |
| const st = r.stats; | |
| stat.textContent = (m === "predicate" | |
| ? `${r.count.toLocaleString()} rows · ${st.files} files · ${st.elided_pct}% elided · ` | |
| : `${r.rows.length} rows · `) + `${r.ms} ms`; | |
| out.innerHTML = `<div class="tablewrap" style="margin-top:14px"><table><thead><tr>` + | |
| r.columns.map(c => `<th>${esc(c)}</th>`).join("") + `</tr></thead><tbody>` + | |
| r.rows.map(row => `<tr>` + row.map(v => | |
| `<td class="tnum">${esc(typeof v === "number" && !Number.isInteger(v) ? v.toFixed(4) : (v ?? ""))}</td>`).join("") + `</tr>`).join("") + | |
| `</tbody></table></div>`; | |
| } else if (m === "set") { | |
| const S = r.stats; | |
| if (S.no_match && !r.hits.length) { | |
| stat.textContent = `${r.ms} ms`; | |
| out.innerHTML = `<div class="empty">This corpus does not contain | |
| that action. The index abstained rather than returning | |
| look-alikes.</div>`; | |
| return; | |
| } | |
| const chans = S.channels && S.channels.length ? | |
| `channels ${S.channels.join(" · ")} · ` : ""; | |
| stat.textContent = `${r.hits.length} clips · ${chans}${r.ms} ms`; | |
| out.innerHTML = `<div class="hitgrid">` + r.hits.map(h => { | |
| const dur = (h.t1 - h.t0) / 1e9; | |
| const mid = Math.round((+h.t0 + +h.t1) / 2); | |
| return ` | |
| <div class="hit" data-s="${esc(h.stream)}" data-t0="${h.t0}" data-t1="${h.t1}"> | |
| <img src="/api/thumb?store=${CUR.key}&stream=${encodeURIComponent(h.stream)}&t=${mid}&w=420" alt=""> | |
| <div class="play">${PLAY}</div> | |
| <div class="m"><b class="tnum">${h.score.toFixed(3)}</b> · ${esc(h.stream)}<br> | |
| <span class="tnum">+${rel(h.t0).toFixed(1)} to +${rel(h.t1).toFixed(1)} s · ${dur.toFixed(1)} s</span></div> | |
| </div>`; | |
| }).join("") + `</div>`; | |
| out.querySelectorAll(".hit").forEach(el => el.addEventListener("click", () => | |
| openPlayer(el.dataset.s, +el.dataset.t0, +el.dataset.t1))); | |
| } else if (m === "window") { | |
| const s = r.stats; | |
| stat.textContent = `${s.files} files · ${fmtB(s.bytes_touched)} of ${fmtB(s.corpus_bytes)} read · ${s.elided_pct}% elided · ${r.ms} ms`; | |
| let h = ""; | |
| for (const [name, t] of Object.entries(r.tables)) { | |
| if (t.kind === "frames") { | |
| const [wa, wb] = $("win-input").value.split(",").map(Number); | |
| const wt0 = Math.round(CUR.min_ts + wa * 1e9), wt1 = Math.round(CUR.min_ts + wb * 1e9); | |
| h += `<div style="margin-top:14px"><b>${esc(name)}</b> | |
| <span class="qstat"> ${t.count} frames · ${t.streams.length} streams</span> | |
| <div class="hitgrid" style="margin-top:8px">` + t.streams.map(sname => ` | |
| <div class="hit" data-s="${esc(sname)}" data-t0="${wt0}" data-t1="${wt1}"> | |
| <img src="/api/thumb?store=${CUR.key}&stream=${encodeURIComponent(sname)}&t=${wt0}&w=420"> | |
| <div class="play">${PLAY}</div> | |
| <div class="m"><b>${esc(sname)}</b></div></div>`).join("") + `</div></div>`; | |
| } else if (t.count) { | |
| h += `<div style="margin-top:14px"><b>${esc(name)}</b> | |
| <span class="qstat"> ${t.count.toLocaleString()} rows</span> | |
| <div class="tablewrap"><table><thead><tr>` + | |
| t.columns.slice(0, 8).map(c => `<th>${esc(c)}</th>`).join("") + | |
| `</tr></thead><tbody>` + t.head.map(row => | |
| `<tr>` + row.slice(0, 8).map(v => `<td class="tnum">${esc(typeof v === "number" && !Number.isInteger(v) ? v.toFixed(3) : v)}</td>`).join("") + `</tr>`).join("") + | |
| `</tbody></table></div></div>`; | |
| } | |
| } | |
| out.innerHTML = h || `<div class="empty">the window is empty</div>`; | |
| out.querySelectorAll(".hit").forEach(el => el.addEventListener("click", () => | |
| openPlayer(el.dataset.s, +el.dataset.t0, +el.dataset.t1))); | |
| } | |
| } catch (e) { | |
| stat.textContent = ""; | |
| out.innerHTML = `<div class="err">${esc(e.message)}</div>`; | |
| } | |
| } | |
| /* data: schema and sample rows */ | |
| let BYTES = null; | |
| async function loadData(table) { | |
| const el = $("v-data"); | |
| if (!table) { | |
| el.innerHTML = `<div id="bytes-panel"></div> | |
| <div class="panel"><div class="hd"><h2>Tables</h2> | |
| <span class="note">columns, types, real rows — and what each one | |
| costs on disk</span></div> | |
| <div class="bd"><div class="empty">reading schema</div></div></div>`; | |
| drawBytes(); | |
| const all = await j(`/api/schema?store=${CUR.key}`).catch(e => ({error: e.message})); | |
| if (all.error) { | |
| el.querySelector(".bd").innerHTML = `<div class="err">${esc(all.error)}</div>`; | |
| return; | |
| } | |
| el.querySelector(".bd").innerHTML = | |
| `<div class="seg" id="data-tabs"></div><div id="data-detail"></div>`; | |
| $("data-tabs").innerHTML = all.map((t, i) => | |
| `<button data-t="${esc(t.table)}"${i === 0 ? ' class="on"' : ''}>${esc(t.table)}</button>`).join(""); | |
| document.querySelectorAll("#data-tabs button").forEach(b => b.onclick = () => { | |
| document.querySelectorAll("#data-tabs button").forEach(x => | |
| x.classList.toggle("on", x === b)); | |
| loadData(b.dataset.t); | |
| }); | |
| if (all.length) loadData(all[0].table); | |
| return; | |
| } | |
| const d = (await j(`/api/schema?store=${CUR.key}&table=${encodeURIComponent(table)}`))[0]; | |
| if (!d) return; | |
| const span = d.rows ? `+${rel(d.min_ts).toFixed(1)} to +${rel(d.max_ts).toFixed(1)} s` : ""; | |
| let h = `<div class="ctrl" style="margin:14px 0 10px"> | |
| <span class="badge">${esc(d.kind)}</span> | |
| <span class="qstat tnum">${d.rows.toLocaleString()} rows · ${fmtB(d.bytes)} · v${d.version} · ${span}</span></div>`; | |
| h += `<div class="tablewrap"><table><thead><tr><th>column</th><th>type</th></tr></thead><tbody>` + | |
| d.columns.map(c => `<tr><td>${esc(c.name)}</td> | |
| <td style="color:var(--faint)">${esc(c.type)}</td></tr>`).join("") + | |
| `</tbody></table></div>`; | |
| if (d.sample && d.sample.length) { | |
| const cols = Object.keys(d.sample[0]).slice(0, 9); | |
| h += `<div class="lbl" style="margin:20px 0 8px">first rows</div> | |
| <div class="tablewrap"><table><thead><tr>` + | |
| cols.map(c => `<th>${esc(c)}</th>`).join("") + `</tr></thead><tbody>` + | |
| d.sample.map(r => `<tr>` + cols.map(c => { | |
| let v = r[c]; | |
| if (c === "ts" && typeof v === "number") | |
| v = v === 0 ? "" : `+${rel(v).toFixed(3)} s`; | |
| return `<td class="tnum">${esc(v ?? "")}</td>`; | |
| }).join("") + `</tr>`).join("") + `</tbody></table></div>`; | |
| } | |
| $("data-detail").innerHTML = h; | |
| } | |
| /* ops: indexes + maintenance */ | |
| async function loadOps() { | |
| const el = $("v-ops"); | |
| el.innerHTML = `<div class="empty">reading index state</div>`; | |
| const d = await j(`/api/indexes?store=${CUR.key}`).catch(e => ({error: e.message})); | |
| IXDATA = d; | |
| if (d.error) { el.innerHTML = `<div class="err">${esc(d.error)}</div>`; return; } | |
| let h = `<div class="panel"><div class="hd"><h2>Secondary indexes</h2> | |
| <span class="note">zone maps prune the time axis for free; a btree on | |
| any other numeric column prunes its predicates too</span></div><div class="bd">`; | |
| for (const [tbl, cols] of Object.entries(d.indexable)) { | |
| for (const c of cols) { | |
| const ex = d.bptree.find(x => x.table === tbl && x.column === c); | |
| h += `<div class="ixrow"><span>${esc(tbl)}.${esc(c)}</span> | |
| <span class="grow"></span> | |
| ${ex ? `<span class="pill ok">v${ex.version} · ${fmtB(ex.bytes)}</span>` : ""} | |
| <button class="btn" data-ix-t="${esc(tbl)}" data-ix-c="${esc(c)}">${ex ? "rebuild" : "build"}</button></div>`; | |
| } | |
| } | |
| if (!Object.keys(d.indexable).length) | |
| h += `<div class="empty">no numeric columns to index</div>`; | |
| h += `</div></div>`; | |
| if (d.has_embeddings) { | |
| h += `<div class="panel"><div class="hd"><h2>Vector indexes</h2> | |
| <span class="note">approximate tiers over the embeddings table; | |
| exact scan needs no build and stays available</span></div><div class="bd">`; | |
| for (const kind of ["hnsw", "ivfpq"]) { | |
| const ex = d.ann.find(x => x.kind === kind); | |
| h += `<div class="ixrow"><span>${kind.toUpperCase()}</span><span class="grow"></span> | |
| ${ex ? `<span class="pill ${ex.stale ? "warn" : "ok"}">v${ex.version}${ex.stale ? " stale" : ""} · ${fmtB(ex.bytes)}</span>` : ""} | |
| <button class="btn" data-ann="${kind}">${ex ? "rebuild" : "build"}</button></div>`; | |
| } | |
| h += `</div></div>`; | |
| } | |
| h += `<div class="panel"><div class="hd"><h2>Maintenance</h2> | |
| <span class="note">each operation is one atomic commit; earlier | |
| versions stay readable until vacuumed</span></div><div class="bd"> | |
| <div class="ixrow"><span>Compact</span> | |
| <span class="qstat">rewrite a table into few large sorted files</span> | |
| <span class="grow"></span> | |
| <select id="mt-compact-tbl"></select> | |
| <button class="btn" id="mt-compact">Compact</button> | |
| <span class="qstat" id="mt-compact-out"></span></div> | |
| <div class="ixrow"><span>Vacuum</span> | |
| <span class="qstat">drop files unreachable from the last</span> | |
| <input type="number" id="mt-retain" value="3" min="1" style="width:52px"> | |
| <span class="qstat">versions</span> | |
| <span class="grow"></span> | |
| <button class="btn" id="mt-vacuum-dry">Dry run</button> | |
| <button class="btn danger" id="mt-vacuum">Vacuum</button> | |
| <span class="qstat" id="mt-vacuum-out"></span></div> | |
| <div class="ixrow"><span>Delete range</span> | |
| <select id="mt-del-tbl"></select> | |
| <span class="qstat">from s</span> | |
| <input type="number" id="mt-del-t0" style="width:80px"> | |
| <span class="qstat">to s</span> | |
| <input type="number" id="mt-del-t1" style="width:80px"> | |
| <span class="grow"></span> | |
| <button class="btn danger" id="mt-del">Delete</button> | |
| <span class="qstat" id="mt-del-out"></span></div> | |
| </div></div>`; | |
| el.innerHTML = h; | |
| el.querySelectorAll("[data-ix-t]").forEach(btn => btn.onclick = () => | |
| buildIx(btn, {table: btn.dataset.ixT, column: btn.dataset.ixC})); | |
| el.querySelectorAll("[data-ann]").forEach(btn => btn.onclick = () => | |
| buildIx(btn, {ann: btn.dataset.ann})); | |
| const tbls = CUR.tables.filter(t => t.rows && t.table !== "centroids") | |
| .map(t => `<option>${esc(t.table)}</option>`).join(""); | |
| $("mt-compact-tbl").innerHTML = tbls; $("mt-del-tbl").innerHTML = tbls; | |
| const post = (o) => j("/api/maintenance", {method: "POST", | |
| headers: {"Content-Type": "application/json"}, | |
| body: JSON.stringify({store: CUR.key, ...o})}); | |
| $("mt-compact").onclick = async () => { | |
| $("mt-compact-out").textContent = "working"; | |
| try { const r = await post({op: "compact", table: $("mt-compact-tbl").value}); | |
| $("mt-compact-out").textContent = | |
| `${r.files_before} to ${r.files_after} files · ${r.ratio}x · ${r.ms} ms`; | |
| selectStore(CUR.key, true); | |
| } catch (e) { $("mt-compact-out").innerHTML = `<span class="err">${esc(e.message)}</span>`; } | |
| }; | |
| const vac = (dry) => async () => { | |
| const out2 = $("mt-vacuum-out"); out2.textContent = "working"; | |
| try { const r = await post({op: "vacuum", retain: +$("mt-retain").value, dry_run: dry}); | |
| out2.textContent = `${dry ? "would free" : "freed"} ${fmtB(r.bytes_freed)} · ${r.files_removed} files · ${r.ms} ms`; | |
| if (!dry) selectStore(CUR.key, true); | |
| } catch (e) { out2.innerHTML = `<span class="err">${esc(e.message)}</span>`; } | |
| }; | |
| $("mt-vacuum-dry").onclick = vac(true); $("mt-vacuum").onclick = vac(false); | |
| $("mt-del").onclick = async () => { | |
| if (!confirm("Delete rows in this time range? Earlier versions keep the data until vacuum.")) return; | |
| const out2 = $("mt-del-out"); out2.textContent = "working"; | |
| try { const r = await post({op: "delete_range", table: $("mt-del-tbl").value, | |
| t0: Math.round(CUR.min_ts + +$("mt-del-t0").value * 1e9), | |
| t1: Math.round(CUR.min_ts + +$("mt-del-t1").value * 1e9)}); | |
| out2.textContent = `deleted ${r.rows_deleted.toLocaleString()} rows · ${r.files_rewritten} rewritten · ${r.ms} ms`; | |
| selectStore(CUR.key, true); | |
| } catch (e) { out2.innerHTML = `<span class="err">${esc(e.message)}</span>`; } | |
| }; | |
| } | |
| async function buildIx(btn, spec) { | |
| btn.disabled = true; const old = btn.textContent; btn.textContent = "building"; | |
| try { | |
| const r = await j("/api/build_index", {method: "POST", | |
| headers: {"Content-Type": "application/json"}, | |
| body: JSON.stringify({store: CUR.key, ...spec})}); | |
| btn.textContent = `done · ${r.ms} ms`; | |
| setTimeout(loadOps, 700); | |
| } catch (e) { btn.textContent = "failed"; btn.title = e.message; | |
| btn.disabled = false; setTimeout(() => btn.textContent = old, 1500); } | |
| } | |
| /* view routing */ | |
| function routeView(v) { | |
| if (v === "map") loadMap(); | |
| if (v === "arch") loadArch(); | |
| if (v === "geo") drawGeo(); | |
| if (v === "data") loadData(); | |
| if (v === "ops") loadOps(); | |
| if (v === "analytics") loadAnalytics(); | |
| if (v === "overview") renderOverview(); | |
| } | |
| document.querySelectorAll("#views button").forEach(b => | |
| b.addEventListener("click", () => { | |
| document.querySelectorAll("#views button").forEach(x => | |
| x.classList.toggle("on", x === b)); | |
| document.querySelectorAll(".view").forEach(v => v.classList.remove("on")); | |
| $("v-" + b.dataset.v).classList.add("on"); | |
| routeView(b.dataset.v); | |
| })); | |
| matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { | |
| drawScatter(); drawGeo(); }); | |
| addEventListener("resize", () => { clearTimeout(window._rz); | |
| window._rz = setTimeout(() => { if (MAP) drawScatter(); drawGeo(); }, 150); }); | |
| /* read-only deployments have no operations surface at all */ | |
| j("/api/version").then(v => { | |
| if (v.readonly) | |
| document.querySelector('#views button[data-v="ops"]').style.display = "none"; | |
| }).catch(() => {}); | |
| loadStores(); | |
| </script> | |
| <style> | |
| /* architecture schematic */ | |
| #v-arch { position:relative; padding:0; | |
| height:calc(100vh - 118px); min-height:480px; } | |
| #arch-seg { position:absolute; top:8px; left:12px; z-index:6; } | |
| #arch-pane-db { position:absolute; inset:44px 0 0 0; overflow:auto; | |
| padding:0 14px 24px; background:#11151c; } | |
| /* never shrink below legibility: scroll the pane instead of scaling the | |
| diagram down until the labels are unreadable */ | |
| #db-svg { display:block; width:1400px; min-width:1400px; } | |
| #db-info { position:sticky; bottom:0; margin-top:10px; | |
| background:rgba(13,17,23,.96); border:1px solid var(--line); | |
| border-radius:4px; padding:9px 12px; font:12px/1.65 var(--mono); | |
| color:#dce8f2; max-width:110ch; } | |
| #db-info b { color:var(--accent); letter-spacing:.06em; } | |
| text.db-lane { fill:#9fb4c8; font:600 10px var(--mono); | |
| letter-spacing:.11em; } | |
| text.db-t { fill:#ffffff; font:600 12px var(--mono); } | |
| text.db-s { fill:#cfe0ee; font:11px var(--mono); } | |
| .db-node rect { transition:filter .12s; } | |
| .db-node:hover rect { filter:brightness(1.55); } | |
| rect.db-b { stroke-width:1; } | |
| rect.db-l0 { fill:#10161f; stroke:#3a4756; } /* lake */ | |
| rect.db-l1 { fill:#131b26; stroke:#4a6076; } /* store */ | |
| rect.db-l2 { fill:#16212c; stroke:#7fb0ff; } /* table */ | |
| rect.db-l3 { fill:#152430; stroke:#5accb9; } /* file */ | |
| rect.db-l4 { fill:#13282a; stroke:#5accb9; } /* rowgrp*/ | |
| rect.db-l5 { fill:#1b2320; stroke:#7f9a6b; } /* chunk */ | |
| rect.db-l6 { fill:#222417; stroke:#b0a85f; } /* page */ | |
| rect.db-l7 { fill:#2a2418; stroke:#c99a5b; } /* value */ | |
| rect.db-commitband { fill:#241f2e; stroke:#a07fd4; } | |
| rect.db-footerband { fill:#1a2733; stroke:#7fb0ff; stroke-dasharray:3 2; } | |
| rect.db-prune { fill:#2a2418; stroke:#c99a5b; } | |
| .an-train { fill:#1d2a24; stroke:#5accb9; stroke-width:1; } | |
| #db-head { padding:4px 0 8px; } | |
| #db-info .mk { color:#a07fd4; } | |
| path.db-link { fill:none; stroke:#4a6076; stroke-width:1; opacity:.5; } | |
| #arch-pane-model { position:absolute; inset:44px 0 0 0; } | |
| /* database pane: dense, monospaced, built to be read out loud */ | |
| .dbsec { margin:16px 0 6px; font:600 11px/1.5 var(--mono); | |
| letter-spacing:.09em; text-transform:uppercase; color:var(--accent); } | |
| .dbnote { font:11px/1.6 var(--mono); color:var(--dim); max-width:96ch; | |
| margin:0 0 8px; } | |
| table.dbt { border-collapse:collapse; font:11px/1.5 var(--mono); | |
| width:100%; margin:4px 0 10px; } | |
| table.dbt th { text-align:left; color:var(--dim); font-weight:500; | |
| border-bottom:1px solid var(--line); padding:3px 8px 3px 0; | |
| white-space:nowrap; } | |
| table.dbt td { padding:2px 8px 2px 0; border-bottom:1px solid | |
| rgba(120,150,170,.07); white-space:nowrap; } | |
| table.dbt td.n, table.dbt th.n { text-align:right; | |
| font-variant-numeric:tabular-nums; } | |
| .dbbar { display:inline-block; height:8px; background:var(--accent); | |
| vertical-align:middle; border-radius:1px; } | |
| .dbbar.cold { background:#3a4756; } | |
| .dbkpi { display:flex; gap:26px; flex-wrap:wrap; margin:6px 0 12px; } | |
| .dbkpi div { font:11px/1.4 var(--mono); color:var(--dim); } | |
| .dbkpi b { display:block; font:600 19px/1.3 var(--mono); color:var(--ink); | |
| font-variant-numeric:tabular-nums; } | |
| .byrow { display:grid; grid-template-columns:78px 1fr 132px 2fr; | |
| gap:10px; align-items:center; margin:5px 0; | |
| font:11px/1.5 var(--mono); } | |
| .bylab { font-weight:600; text-transform:uppercase; letter-spacing:.07em; } | |
| .bybar { height:12px; background:rgba(120,150,170,.10); border-radius:2px; | |
| overflow:hidden; } | |
| .bybar i { display:block; height:100%; } | |
| .byval { text-align:right; color:var(--ink); | |
| font-variant-numeric:tabular-nums; } | |
| .byval span { color:var(--dim); } | |
| .bynote { color:var(--dim); font:10.5px/1.55 var(--mono); } | |
| .b-live { color:#5accb9; background:#5accb9; } | |
| .b-orph { color:#c99a5b; background:#c99a5b; } | |
| .b-media { color:#7fb0ff; background:#7fb0ff; } | |
| .b-cache { color:#a07fd4; background:#a07fd4; } | |
| .bylab.b-live, .bylab.b-orph, .bylab.b-media, .bylab.b-cache | |
| { background:transparent; } | |
| .dbpath { color:var(--ink); background:rgba(120,150,170,.10); | |
| padding:1px 5px; border-radius:2px; } | |
| button.mini { border:1px solid var(--line); background:transparent; | |
| color:var(--muted); font:11px/1.4 var(--mono); padding:2px 7px; | |
| border-radius:3px; cursor:pointer; margin:0 2px 3px 0; } | |
| button.mini:hover { color:var(--ink); } | |
| button.mini.on { background:var(--accent); color:#0d1117; | |
| border-color:var(--accent); } | |
| #arch-wrap { position:absolute; inset:0; overflow:hidden; background:#11151c; } | |
| #arch-wrap::before { content:""; position:absolute; inset:0; | |
| background-image: | |
| linear-gradient(rgba(120,150,170,.06) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(120,150,170,.06) 1px, transparent 1px), | |
| linear-gradient(rgba(120,150,170,.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(120,150,170,.03) 1px, transparent 1px); | |
| background-size:120px 120px, 120px 120px, 24px 24px, 24px 24px; | |
| pointer-events:none; } | |
| #arch-svg { width:100%; height:100%; cursor:grab; display:block; } | |
| #arch-svg:active { cursor:grabbing; } | |
| #arch-svg text { font-family:ui-monospace, "SF Mono", Menlo, monospace; } | |
| .an-card { fill:#171c25; stroke:#39424f; stroke-width:1.2; rx:3; } | |
| .an-card.sel { stroke:#5accb9; stroke-width:2; } | |
| .an-head { fill:#1e242f; } | |
| .an-title { fill:#e3e8ee; font-size:12.5px; font-weight:700; | |
| letter-spacing:.04em; } | |
| .an-kind { font-size:8.5px; letter-spacing:.14em; } | |
| .an-stat { fill:#8b96a4; font-size:9.5px; } | |
| .an-field-n { fill:#b4bec9; font-size:9.5px; } | |
| .an-field-t { fill:#68727e; font-size:9.5px; } | |
| .an-badge { fill:#12261f; stroke:#265c4c; } | |
| .an-badge-t { fill:#5accb9; font-size:8px; letter-spacing:.08em; } | |
| .an-lane { fill:#535e6b; font-size:10px; letter-spacing:.35em; opacity:.85; } | |
| .an-lane-rule { stroke:#2a323d; stroke-dasharray:2 6; } | |
| .an-trace { stroke:#46525f; stroke-width:1.3; fill:none; } | |
| .an-flow { stroke:#3fb9a5; stroke-width:1.3; fill:none; | |
| stroke-dasharray:3 9; animation:an-dash 1.6s linear infinite; opacity:.7; } | |
| @keyframes an-dash { to { stroke-dashoffset:-12; } } | |
| @media (prefers-reduced-motion: reduce) { .an-flow { animation:none; } } | |
| .an-elabel { fill:#68727e; font-size:8.5px; } | |
| .an-chan { fill:#171c25; stroke:#39424f; } | |
| .an-chan-t { fill:#9fded1; font-size:10px; font-weight:600; } | |
| .an-chan-n { fill:#68727e; font-size:8.5px; } | |
| .an-stage { fill:#171c25; stroke:#3fb9a5; } | |
| /* the panel now carries the elements, the models and the training | |
| loop, so it needs to scroll and to keep clear of the pan hint */ | |
| #arch-title { position:absolute; right:18px; bottom:36px; top:14px; | |
| width:430px; overflow:auto; pointer-events:auto; | |
| border:1px solid #39424f; background:rgba(17,21,28,.96); | |
| font-family:ui-monospace, Menlo, monospace; color:#b8c4d2; | |
| font-size:11px; line-height:1.6; padding:9px 13px; } | |
| #arch-title b { color:#e3e8ee; font-size:11.5px; letter-spacing:.12em; } | |
| #arch-title .rule { border-top:1px solid #2a323d; margin:5px -12px; } | |
| #arch-hint { position:absolute; left:18px; bottom:16px; color:#535e6b; | |
| font:10px ui-monospace, Menlo, monospace; pointer-events:none; } | |
| #arch-detail { position:absolute; top:14px; right:14px; width:340px; | |
| max-height:calc(100% - 90px); overflow:auto; display:none; | |
| border:1px solid #39424f; background:rgba(17,21,28,.97); | |
| font-family:ui-monospace, Menlo, monospace; font-size:11px; | |
| color:#b4bec9; padding:12px 14px; } | |
| #arch-detail.on { display:block; } | |
| #arch-detail h4 { margin:0 0 4px; color:#5accb9; font-size:13px; } | |
| #arch-detail table { width:100%; border-collapse:collapse; margin:6px 0; } | |
| #arch-detail td { padding:1.5px 4px 1.5px 0; | |
| border-bottom:1px solid #232a34; vertical-align:top; } | |
| #arch-detail td:last-child { color:#68727e; text-align:right; } | |
| #arch-detail pre { background:#10141a; border:1px solid #232a34; | |
| padding:6px; overflow:auto; font-size:10px; color:#8b96a4; } | |
| #arch-detail .x { position:sticky; float:right; top:0; cursor:pointer; | |
| color:#68727e; } | |
| </style> | |
| <script> | |
| let ARCH = null, DBI = null, DBI_TABLE = null; | |
| /* one explanation surface for the model pane, mirroring db-info: | |
| click a channel or a stage and read what it is and why it is there */ | |
| function archNote(title, id, note) { | |
| const d = $("arch-detail"); | |
| d.className = "on"; | |
| d.innerHTML = `<h4>${esc(title)}</h4>` + | |
| `<div style="color:#8fa3b8;font:10px var(--mono);margin-bottom:6px">` + | |
| `id ${esc(id || "")}</div>` + | |
| `<div style="line-height:1.65">${esc(note || "")}</div>`; | |
| } | |
| async function loadArch() { | |
| if (!ARCH) ARCH = await j(`/api/architecture?store=${CUR.key}`); | |
| drawArch(); | |
| await loadDb(); | |
| document.querySelectorAll("#arch-seg button").forEach(b => b.onclick = () => { | |
| document.querySelectorAll("#arch-seg button").forEach(x => | |
| x.classList.toggle("on", x === b)); | |
| const db = b.dataset.p === "db"; | |
| $("arch-pane-db").style.display = db ? "" : "none"; | |
| $("arch-pane-model").style.display = db ? "none" : ""; | |
| if (!db) { | |
| // the pane was display:none on first draw, so getBBox() returned | |
| // zeros and the fit was skipped - fit once, now that it is visible | |
| const sv = $("arch-svg"); | |
| if (!sv._fitted) { sv._view = null; sv._fitted = true; } | |
| drawArch(); | |
| } | |
| }); | |
| } | |
| async function loadDb(table) { | |
| const q = table ? `&table=${encodeURIComponent(table)}` : ""; | |
| DBI = await j(`/api/dbinternals?store=${CUR.key}${q}`); | |
| DBI_TABLE = DBI.table; | |
| drawDb(); | |
| } | |
| /* DATABASE ENGINE, DRAWN. Five lanes left to right, every box sized and | |
| populated from THIS store right now: store -> meta files -> tables -> | |
| (log commits + parquet files) -> pages -> column chunks. Hover any box | |
| for what it is and why it exists. */ | |
| const DB_DOC = { | |
| store: ["STORE", "A directory. No server, no daemon, no custom byte " | |
| + "format outside the media itself. Everything is Parquet under a " | |
| + "transaction log, so another engine can read it directly."], | |
| meta: ["META FILE", "Sidecar state next to the tables. Authoritative " | |
| + "(_store.json), fitted artifact (_set_weights.json), cache " | |
| + "(_vocab.json, grown lazily per new word) or disposable (_cache/, " | |
| + "deleting it costs recompute time, never correctness)."], | |
| tables: ["TABLES", "One directory per table, each with its OWN commit " | |
| + "log. Every table must carry ts (int64 ns) sorted within a file - " | |
| + "the single schema law, and what makes time the primary axis."], | |
| table: ["TABLE", "State is the fold of its log, not the files in the " | |
| + "directory. Height here is proportional to bytes on disk."], | |
| log: ["COMMIT LOG", "Append-only JSON commits. Version N is immutable " | |
| + "forever (snapshot isolation + time travel), a multi-file load " | |
| + "lands as ONE entry (atomicity), and the lock is O_EXCL on the " | |
| + "next filename - single writer, many readers, no coordinator."], | |
| commit: ["COMMIT", "One manifest entry: op, schema, and the file list " | |
| + "with zone maps. op=replace removes the old file set in the same " | |
| + "commit that adds the new one - it exists because appending a " | |
| + "rebuild once doubled the events table to 16,526 rows silently."], | |
| file: ["PARQUET FILE", "Immutable, never edited in place. Its " | |
| + "min_ts/max_ts live in the COMMIT ENTRY, so a time-window query " | |
| + "drops the whole file before opening a single footer. That is " | |
| + "prune layer 1, and it costs zero I/O against the data."], | |
| page: ["ROW GROUP (page)", "A page carries its own ts statistics, so " | |
| + "groups outside the window are never decompressed - prune layer " | |
| + "2. Sized by BYTES (8 MB target) not by row count, or a 1152-d " | |
| + "vector table emits groups nobody can skip inside. This is the " | |
| + "random-access vs compression tension the Lance paper is about."], | |
| chunk: ["COLUMN CHUNK", "Each column is stored separately inside the " | |
| + "page, so a projection touches only the columns it asked for. " | |
| + "Width here is the compressed size."], | |
| prune: ["PUSHDOWN, EXECUTED", "A real scan run just now. bytes_touched " | |
| + "is charged as footer + overlapping row groups + projected column " | |
| + "chunks - the same arithmetic the reader performs, not an " | |
| + "estimate. Every read goes through the counter, which is what " | |
| + "makes the elision number honest."], | |
| media: ["MEDIA", "Store-managed renditions. Raw sources are NEVER " | |
| + "copied in: a frames row whose source starts with @ is managed, " | |
| + "anything else is an external reference-in-place. The store is an " | |
| + "index over your video, not a second copy of it."], | |
| frames: ["FRAME INDEX", "Not frames - (ts, byte_offset, packet_size, " | |
| + "keyframe, codec, source, stream), one row per frame pointing INTO " | |
| + "a media file. Pixels are produced last, from byte ranges, only " | |
| + "for rows that survived every prune. Late materialization."], | |
| }; | |
| function dbInfo(kind, extra) { | |
| const d = DB_DOC[kind]; if (!d) return; | |
| $("db-info").innerHTML = `<b>${d[0]}</b>${extra ? " · " + extra : ""} | |
| <div class="rule"></div>${d[1]}`; | |
| } | |
| function drawDb() { | |
| const d = DBI, P = d.prune || {}, L = d.ladder || []; | |
| const by = {}; L.forEach(x => by[x.id] = x); | |
| const NS = "http://www.w3.org/2000/svg"; | |
| $("arch-pane-db").innerHTML = | |
| `<div id="db-head"></div><svg id="db-svg"><g id="db-cam"></g></svg> | |
| <div id="db-info"></div>`; | |
| const cam = $("db-cam"); | |
| const el = (t, a, parent) => { const e = document.createElementNS(NS, t); | |
| for (const k in a) e.setAttribute(k, a[k]); | |
| (parent || cam).appendChild(e); return e; }; | |
| const txt = (x, y, s, cls, parent, anchor) => { | |
| const e = el("text", { x, y, class: cls }, parent); | |
| if (anchor) e.setAttribute("text-anchor", anchor); | |
| e.textContent = s; return e; }; | |
| /* table picker */ | |
| $("db-head").innerHTML = d.tables.map(t => | |
| `<button class="mini${t === d.table ? " on" : ""}" | |
| onclick="loadDb('${t}')">${esc(t)}</button>`).join(" "); | |
| /* CONTAINMENT. Each rung physically encloses the next, because that | |
| is the actual relationship: a value lives in a page, which lives in | |
| a column chunk, which lives in a row group, which lives in a file, | |
| which a commit lists, which a table folds, which a store holds. */ | |
| const W = 1400, PAD = 21, HEAD = 30; | |
| const levels = ["lake", "store", "table", "file", "rowgroup", | |
| "chunk", "page", "value"]; | |
| const hover = (r, id) => { | |
| r.addEventListener("mouseenter", () => { | |
| const x = by[id]; if (!x) return; | |
| $("db-info").innerHTML = | |
| `<b>${esc(x.label)}</b> <span style="color:var(--ink)"> | |
| ${esc(x.n)}</span> <span>${esc(x.sub)}</span> | |
| <div class="rule"></div> | |
| <div><span class="mk">metadata:</span> ${esc(x.meta)}</div> | |
| <div style="margin-top:5px">${esc(x.why)}</div>`; | |
| }); | |
| }; | |
| const N = levels.length; | |
| // bottoms stagger by BSTEP so each nested box visibly ends inside its | |
| // parent - without it the TABLE and the FILE ended within 6px of each | |
| // other and their two bands (commit, footer) drew on top of one another | |
| const BSTEP = 26, BASE = 14 + N * (HEAD + 6) + 150; | |
| const H = BASE + 14; | |
| let bandY = []; | |
| levels.forEach((id, i) => { | |
| const x = PAD + i * PAD, w = W - 2 * (PAD + i * PAD); | |
| const y = 14 + i * (HEAD + 6); | |
| const h = (BASE - i * BSTEP) - y; | |
| const g = el("g", { class: "db-node" }); | |
| const r = el("rect", { x, y, width: w, height: h, rx: 4, | |
| class: "db-b db-l" + i }, g); | |
| hover(r, id); | |
| const it = by[id] || {}; | |
| txt(x + 11, y + 19, it.label || id.toUpperCase(), "db-t", g); | |
| // right-hand stats are anchored to the right edge, so an over-long | |
| // string grows leftward and collides with the label - cap it | |
| const stat = `${it.n || ""} ${it.sub || ""}`; | |
| txt(x + w - 11, y + 19, stat.length > 62 ? stat.slice(0, 61) + "…" : stat, | |
| "db-s", g, "end"); | |
| bandY.push({ x, y, w, h }); | |
| }); | |
| /* the two things that hang OFF the ladder rather than inside it: | |
| the commit log (sibling of the files, inside the table) and the | |
| footer (inside the file, after the row groups) */ | |
| const tb = bandY[2], fb = bandY[3]; | |
| const cg = el("g", { class: "db-node" }); | |
| const cr = el("rect", { x: tb.x + 12, y: tb.y + tb.h - 34, | |
| width: tb.w - 24, height: 26, rx: 3, class: "db-b db-commitband" }, cg); | |
| hover(cr, "commit"); | |
| const lc = by["commit"] || {}, ll = by["log"] || {}; | |
| txt(tb.x + 22, tb.y + tb.h - 16, | |
| `${lc.label || "COMMIT"} — ${lc.n || ""} · ${ll.sub || ""}`, | |
| "db-s", cg); | |
| txt(tb.x + tb.w - 22, tb.y + tb.h - 16, | |
| `${lc.sub || ""}`, "db-s", cg, "end"); | |
| const fg = el("g", { class: "db-node" }); | |
| const fr = el("rect", { x: fb.x + 12, y: fb.y + fb.h - 34, | |
| width: fb.w - 24, height: 26, rx: 3, class: "db-b db-footerband" }, fg); | |
| hover(fr, "footer"); | |
| const ft = by["footer"] || {}; | |
| txt(fb.x + 22, fb.y + fb.h - 16, | |
| `${ft.label || "FOOTER"} — ${ft.n || ""} · ${ft.meta || ""}`, | |
| "db-s", fg); | |
| /* pushdown badge, measured, sitting under the ladder */ | |
| if (P && !P.error) { | |
| const y = H + 6; | |
| const g = el("g", { class: "db-node" }); | |
| const r = el("rect", { x: PAD, y, width: W - 2 * PAD, height: 42, | |
| rx: 4, class: "db-b db-prune" }, g); | |
| hover(r, "prune"); | |
| txt(PAD + 12, y + 18, "PREDICATE PUSHDOWN · executed now", "db-t", g); | |
| txt(PAD + 12, y + 33, | |
| `layer 1 (commit zone maps): ${P.files_touched}/${P.files_total} ` + | |
| `files kept · layer 2 (row-group stats + projection): ` + | |
| `${fmtB(P.bytes_touched)} read of ${fmtB(P.corpus_bytes)} · ` + | |
| `${fmtN(P.rows_returned)} rows · ${P.elided_pct}% elided` + | |
| (d.n_row_groups <= 1 ? " (one row group — nothing to prune)" : ""), | |
| "db-s", g); | |
| } | |
| const TOT = H + 60; | |
| const svg = $("db-svg"); | |
| svg.setAttribute("viewBox", `0 0 ${W} ${TOT}`); | |
| svg.style.height = TOT + "px"; | |
| $("db-info").innerHTML = | |
| `<b>Hover any layer.</b> The ladder reads bottom-up: a VALUE lives | |
| in a PAGE, pages make a COLUMN CHUNK, chunks make a ROW GROUP, | |
| groups make a FILE, a COMMIT lists files, the TABLE is the fold of | |
| its commits, the STORE holds tables, the LAKE holds stores.`; | |
| } | |
| function archLane(n) { | |
| if (n.kind === "media") return 0; | |
| if (n.kind === "model") return 3; | |
| if (["centroids"].includes(n.id)) return 3; | |
| if (n.kind === "embeddings") return 2; | |
| return 1; | |
| } | |
| const AN_LANES = ["MEDIA", "RAW TABLES", "VECTOR TABLES", "ARTIFACTS"]; | |
| const AN_KINDC = { media:"#c99a5b", timeseries:"#7fb0ff", | |
| embeddings:"#5accb9", model:"#d98fd4", | |
| frame_index:"#7fb0ff" }; | |
| function fmtTs(ns) { return ns == null ? "" : | |
| new Date(ns / 1e6).toISOString().slice(0, 19).replace("T", " "); } | |
| function drawArch() { | |
| const svg = $("arch-svg"), cam = $("arch-cam"); | |
| cam.innerHTML = ""; | |
| const NS = "http://www.w3.org/2000/svg"; | |
| const el = (t, a, parent) => { const e = document.createElementNS(NS, t); | |
| for (const k in a) e.setAttribute(k, a[k]); | |
| (parent || cam).appendChild(e); return e; }; | |
| const txt = (x, y, s, cls, parent, anchor) => { | |
| const e = el("text", { x, y, class: cls }, parent); | |
| if (anchor) e.setAttribute("text-anchor", anchor); | |
| e.textContent = s; return e; }; | |
| const LANE_X = [40, 330, 660, 990], LANE_W = 250, PIPE_X = 1330; | |
| const laneY = [70, 70, 70, 70]; | |
| const pos = {}; | |
| const byLane = [[], [], [], []]; | |
| ARCH.nodes.forEach(n => byLane[archLane(n)].push(n)); | |
| byLane.forEach(l => l.sort((a, b) => (b.bytes || 0) - (a.bytes || 0))); | |
| const g_edges = el("g", {}), g_nodes = el("g", {}); | |
| byLane.forEach((lane, li) => { | |
| txt(LANE_X[li], 40, AN_LANES[li], "an-lane"); | |
| el("line", { x1: LANE_X[li] - 20, y1: 50, x2: LANE_X[li] - 20, | |
| y2: 1400, class: "an-lane-rule" }); | |
| lane.forEach(n => { | |
| const fields = (n.fields || []).slice(0, 7); | |
| const h = 46 + fields.length * 13 + | |
| ((n.sidecars || []).length || n.meta_json ? 18 : 6); | |
| const g = el("g", { class: "an-node", "data-id": n.id, | |
| transform: `translate(${LANE_X[li]},${laneY[li]})` }, | |
| g_nodes); | |
| pos[n.id] = { x: LANE_X[li], y: laneY[li], w: LANE_W, h }; | |
| laneY[li] += h + 26; | |
| el("rect", { width: LANE_W, height: h, class: "an-card" }, g); | |
| el("rect", { width: LANE_W, height: 22, class: "an-head" }, g); | |
| el("rect", { width: 3, height: h, | |
| fill: AN_KINDC[n.kind] || "#7fb0ff" }, g); | |
| txt(10, 15, n.id.replace("model:", ""), "an-title", g); | |
| const kt = txt(LANE_W - 8, 14, n.kind.toUpperCase(), "an-kind", g, | |
| "end"); | |
| kt.setAttribute("fill", AN_KINDC[n.kind] || "#7fb0ff"); | |
| txt(10, 36, (n.rows != null ? fmtN(n.rows) + " rows · " : "") + | |
| fmtB(n.bytes || 0) + | |
| (n.files != null ? ` · ${n.files}f` : "") + | |
| (n.version ? ` · v${n.version}` : ""), "an-stat", g); | |
| fields.forEach((f, i) => { | |
| txt(12, 52 + i * 13, f.name, "an-field-n", g); | |
| txt(LANE_W - 10, 52 + i * 13, f.type.length > 18 ? | |
| f.type.slice(0, 17) + ".." : f.type, "an-field-t", g, "end"); | |
| }); | |
| let bx = 10; | |
| const by = 46 + fields.length * 13 + 2; | |
| const badge = (s) => { | |
| const w = s.length * 5.4 + 10; | |
| el("rect", { x: bx, y: by - 9, width: w, height: 12, rx: 6, | |
| class: "an-badge" }, g); | |
| txt(bx + w / 2, by, s, "an-badge-t", g, "middle"); | |
| bx += w + 6; }; | |
| if ((n.sidecars || []).length) badge("MMAP x" + n.sidecars.length); | |
| if (n.meta_json) badge("META"); | |
| g.style.cursor = "pointer"; | |
| }); | |
| }); | |
| ARCH.edges.forEach(e => { | |
| const a = pos[e.from], b = pos[e.to]; | |
| if (!a || !b) return; | |
| const x1 = a.x + a.w, y1 = a.y + 22, x2 = b.x, y2 = b.y + 22; | |
| const mx = (x1 + x2) / 2; | |
| const d = `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`; | |
| el("path", { d, class: "an-trace" }, g_edges); | |
| el("path", { d, class: "an-flow" }, g_edges); | |
| txt(mx, (y1 + y2) / 2 - 5, e.label || "", "an-elabel", g_edges, | |
| "middle"); | |
| }); | |
| /* query pipeline: channels feed the fitted selection chain */ | |
| txt(PIPE_X, 40, "QUERY PIPELINE", "an-lane"); | |
| el("line", { x1: PIPE_X - 20, y1: 50, x2: PIPE_X - 20, y2: 1400, | |
| class: "an-lane-rule" }); | |
| let cy = 70; | |
| const CH_W = 190, ST_X = PIPE_X + CH_W + 90; | |
| ARCH.channels.forEach(c => { | |
| const g = el("g", { transform: `translate(${PIPE_X},${cy})` }); | |
| const cr = el("rect", { width: CH_W, height: 34, rx: 3, | |
| class: "an-chan" }, g); | |
| txt(10, 14, c.label, "an-chan-t", g); | |
| txt(10, 26, (c.note || "").slice(0, 46), "an-chan-n", g); | |
| txt(CH_W - 8, 14, c.id, "an-kind", g, "end"); | |
| // click any block for the full explanation, same as the DB pane | |
| cr.style.cursor = "pointer"; | |
| cr.onclick = () => archNote("CHANNEL · " + c.label, c.id, c.note); | |
| const y = cy + 17; | |
| const d = `M${PIPE_X + CH_W},${y} C${PIPE_X + CH_W + 45},${y} ` + | |
| `${PIPE_X + CH_W + 45},${92} ${ST_X},${92}`; | |
| el("path", { d, class: "an-trace" }, g_edges); | |
| el("path", { d, class: "an-flow" }, g_edges); | |
| cy += 46; | |
| }); | |
| let sy = 70; | |
| const F = ARCH.fitted || {}; | |
| const stageNote = { | |
| fuse: F.set_weights ? "per-store weights, holdout " + (F.loqo_mean ?? "") : null, | |
| cut: F.cut_alpha != null ? `alpha ${F.cut_alpha_dir ?? 0} dir · ${F.cut_alpha} other` : null, | |
| nms: F.nms_r_dir != null ? `radius ${F.nms_r_dir} dir · ${F.nms_r ?? 0} other` : null, | |
| }; | |
| let prev = null; | |
| ARCH.pipeline.forEach(p => { | |
| const g = el("g", { transform: `translate(${ST_X},${sy})` }); | |
| const sr = el("rect", { width: 150, height: 44, rx: 3, | |
| class: "an-stage" }, g); | |
| txt(10, 18, p.label, "an-chan-t", g); | |
| txt(10, 32, (stageNote[p.id] || p.note || "").slice(0, 26), | |
| "an-chan-n", g); | |
| sr.style.cursor = "pointer"; | |
| sr.onclick = () => archNote("STAGE · " + p.label, p.id, | |
| stageNote[p.id] || p.note); | |
| if (prev != null) { | |
| const x = ST_X + 75; | |
| const d = `M${x},${prev + 44} L${x},${sy}`; | |
| el("path", { d, class: "an-trace" }, g_edges); | |
| el("path", { d, class: "an-flow" }, g_edges); | |
| } | |
| prev = sy; | |
| sy += 66; | |
| }); | |
| /* the five elements: what a clip is decomposed into. Rendered only | |
| when this store actually carries them, so an un-elemented store | |
| shows nothing rather than a promise. */ | |
| const els = ARCH.elements || []; | |
| const elemHtml = els.length ? ( | |
| `<div class="rule"></div><b>FIVE ELEMENTS</b><br>` + | |
| els.map(e => `${esc(e.label)} <span style="opacity:.6">` + | |
| `${fmtN(e.n)}</span><br><span style="opacity:.55;font-size:10px">` + | |
| `${esc(e.note)}</span>`).join("<br>")) : ""; | |
| const mods = ARCH.models || []; | |
| const modHtml = mods.length ? ( | |
| `<div class="rule"></div><b>MODELS</b><br>` + | |
| mods.map(m => { | |
| const head = m.kind === "teacher" | |
| ? `${esc(m.id)} · yield ${m.yield} / prec ${m.prec}` | |
| : `${esc(m.id)} · ${m.params_M}M · ${m.read_ms}ms read · ` + | |
| `${m.write_s_per_demo}s/demo write`; | |
| const env = m.env && Object.keys(m.env).length | |
| ? `<br><span style="opacity:.55;font-size:10px">env ` + | |
| Object.entries(m.env).map(([k, v]) => `${esc(k)}=${esc(String(v))}`) | |
| .join(" ") + `</span>` : ""; | |
| return `${head}${env}<br><span style="opacity:.55;font-size:10px">` + | |
| `${esc(m.note)}</span>`; | |
| }).join("<br>")) : ""; | |
| /* TRAINING / RETRAINING LOOP — drawn as a cycle, because it is one: | |
| serving produces no labels, but more DATA widens the corpus | |
| statistics the whole system is built on, and re-running teacher → | |
| distill is the retrain. Click a step for what it does. */ | |
| const TR = ARCH.training || []; | |
| if (TR.length) { | |
| // BELOW WHATEVER THE LANES ACTUALLY REACH. A fixed y collided with | |
| // the RAW TABLES lane, whose height depends on how many tables the | |
| // store has - the one thing that varies per store. | |
| const TY = Math.max(...laneY, cy, sy) + 78, TW = 230, TG = 26; | |
| txt(LANE_X[0], TY - 22, "TRAINING / RETRAINING LOOP", "an-lane"); | |
| TR.forEach((t, i) => { | |
| const x = LANE_X[0] + i * (TW + TG); | |
| const g = el("g", { transform: `translate(${x},${TY})` }); | |
| const r = el("rect", { width: TW, height: 46, rx: 3, | |
| class: "an-train" }, g); | |
| r.style.cursor = "pointer"; | |
| r.onclick = () => archNote(t.label, t.id, t.note); | |
| txt(10, 18, t.label, "an-chan-t", g); | |
| txt(10, 33, (t.note || "").slice(0, 34) + "…", "an-chan-n", g); | |
| if (i) { | |
| const d0 = `M${x - TG},${TY + 23} L${x},${TY + 23}`; | |
| el("path", { d: d0, class: "an-trace" }, g_edges); | |
| el("path", { d: d0, class: "an-flow" }, g_edges); | |
| } | |
| }); | |
| // the loop closes: step 6 feeds step 3 again | |
| const x0 = LANE_X[0] + (TR.length - 1) * (TW + TG) + TW / 2; | |
| const x1 = LANE_X[0] + 2 * (TW + TG) + TW / 2; | |
| el("path", { class: "an-trace", | |
| d: `M${x0},${TY + 46} C${x0},${TY + 96} ${x1},${TY + 96} ${x1},${TY + 46}` }, | |
| g_edges); | |
| txt((x0 + x1) / 2, TY + 92, "more data → relabel → retrain", | |
| "an-chan-n", g_edges, "middle"); | |
| } | |
| const TRH = (ARCH.training || []).length ? ( | |
| `<div class="rule"></div><b>TRAINING / RETRAINING LOOP</b><br>` + | |
| ARCH.training.map(t => | |
| `<span style="color:#5accb9">${esc(t.label)}</span><br>` + | |
| `<span style="opacity:.62;font-size:10px">${esc(t.note)}</span>`) | |
| .join("<br>") + | |
| `<br><span style="color:#5accb9">↻ step 6 feeds step 3</span>` + | |
| `<span style="opacity:.62;font-size:10px"> — nothing here needs ` + | |
| `human annotation</span>`) : ""; | |
| $("arch-title").innerHTML = | |
| `<b>ELIDEDB · STORE SCHEMATIC</b><div class="rule"></div>` + | |
| `store ${esc(ARCH.store)}<br>` + | |
| `tables ${ARCH.totals.tables} · ` + | |
| `${fmtN(ARCH.totals.rows)} rows · ${fmtB(ARCH.totals.bytes)}<br>` + | |
| `channels ${ARCH.channels.map(c => c.id).join(" · ")}` + | |
| elemHtml + modHtml + TRH + | |
| `<div class="rule"></div>drawn from live state · ` + | |
| new Date().toISOString().slice(0, 16).replace("T", " "); | |
| // FIT THE WHOLE SCHEMATIC ON FIRST DRAW. The fixed 0.82 zoom left the | |
| // training loop (drawn below the lanes) off screen entirely, so the | |
| // one part that explains how the models are PRODUCED was invisible | |
| // unless you knew to pan for it. | |
| let view = svg._view; | |
| if (!view) { | |
| view = { x: 0, y: 0, k: 0.82 }; | |
| try { | |
| const bb = cam.getBBox(), r = svg.getBoundingClientRect(); | |
| if (bb.width && bb.height && r.width && r.height) { | |
| view.k = Math.max(0.45, Math.min(r.width / (bb.width + 60), | |
| r.height / (bb.height + 60), 1)); | |
| view.x = 24 - bb.x * view.k; | |
| view.y = 16 - bb.y * view.k; | |
| } | |
| } catch (e) { /* keep the default */ } | |
| } | |
| svg._view = view; | |
| const apply = () => cam.setAttribute("transform", | |
| `translate(${view.x},${view.y}) scale(${view.k})`); | |
| apply(); | |
| svg.onwheel = (ev) => { ev.preventDefault(); | |
| const r = svg.getBoundingClientRect(); | |
| const px = ev.clientX - r.left, py = ev.clientY - r.top; | |
| const k2 = Math.min(3, Math.max(.15, | |
| view.k * (ev.deltaY < 0 ? 1.12 : 0.89))); | |
| view.x = px - (px - view.x) * k2 / view.k; | |
| view.y = py - (py - view.y) * k2 / view.k; | |
| view.k = k2; apply(); }; | |
| let drag = null; | |
| svg.onpointerdown = (ev) => { | |
| const nodeG = ev.target.closest(".an-node"); | |
| drag = { px: ev.clientX, py: ev.clientY, node: nodeG, | |
| vx: view.x, vy: view.y }; | |
| if (nodeG) { const p = pos[nodeG.dataset.id]; | |
| drag.nx = p.x; drag.ny = p.y; } | |
| svg.setPointerCapture(ev.pointerId); }; | |
| svg.onpointermove = (ev) => { if (!drag) return; | |
| const dx = ev.clientX - drag.px, dy = ev.clientY - drag.py; | |
| if (drag.node) { | |
| const p = pos[drag.node.dataset.id]; | |
| p.x = drag.nx + dx / view.k; p.y = drag.ny + dy / view.k; | |
| drag.node.setAttribute("transform", `translate(${p.x},${p.y})`); | |
| drag.moved = (Math.abs(dx) + Math.abs(dy)) > 3; | |
| if (drag.moved) redrawEdges(); | |
| } else { view.x = drag.vx + dx; view.y = drag.vy + dy; apply(); } }; | |
| function redrawEdges() { | |
| g_edges.querySelectorAll(".an-trace, .an-flow, .an-elabel") | |
| .forEach(e => e.remove()); | |
| ARCH.edges.forEach(e => { | |
| const a = pos[e.from], b = pos[e.to]; | |
| if (!a || !b) return; | |
| const x1 = a.x + a.w, y1 = a.y + 22, x2 = b.x, y2 = b.y + 22; | |
| const mx = (x1 + x2) / 2; | |
| const d = `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`; | |
| el("path", { d, class: "an-trace" }, g_edges); | |
| el("path", { d, class: "an-flow" }, g_edges); | |
| txt(mx, (y1 + y2) / 2 - 5, e.label || "", "an-elabel", g_edges, | |
| "middle"); | |
| }); } | |
| svg.onpointerup = (ev) => { | |
| if (drag && drag.node && !drag.moved) archSelect(drag.node.dataset.id); | |
| drag = null; }; | |
| svg.ondblclick = () => { view.x = 0; view.y = 0; view.k = 0.82; apply(); }; | |
| } | |
| function archSelect(id) { | |
| const n = ARCH.nodes.find(x => x.id === id); | |
| if (!n) return; | |
| document.querySelectorAll(".an-card").forEach(c => | |
| c.classList.remove("sel")); | |
| const g = document.querySelector(`.an-node[data-id="${CSS.escape(id)}"]`); | |
| if (g) g.querySelector(".an-card").classList.add("sel"); | |
| let h = `<span class="x" onclick="$('arch-detail').classList.remove('on')">x</span>` + | |
| `<h4>${esc(n.id)}</h4>` + | |
| `<div>${n.kind} · ${n.rows != null ? fmtN(n.rows) + " rows · " : ""}` + | |
| `${fmtB(n.bytes || 0)}${n.files != null ? " · " + n.files + " files" : ""}` + | |
| `${n.version ? " · " + n.version + " commits" : ""}</div>` + | |
| `<div style="color:#68727e">${fmtTs(n.min_ts)} to ${fmtTs(n.max_ts)}</div>`; | |
| if ((n.fields || []).length) { | |
| h += `<table>` + n.fields.map(f => | |
| `<tr><td>${esc(f.name)}</td><td>${esc(f.type)}</td></tr>`).join("") + | |
| `</table>`; | |
| } | |
| if ((n.sidecars || []).length) | |
| h += `<div>mmap sidecars:</div><pre>${esc(n.sidecars.join("\n"))}</pre>`; | |
| if (n.meta && Object.keys(n.meta).length) | |
| h += `<div>table meta:</div><pre>${esc(JSON.stringify(n.meta, null, 1))}</pre>`; | |
| $("arch-detail").innerHTML = h; | |
| $("arch-detail").classList.add("on"); | |
| } | |
| </script> | |
| </body> | |
| </html> | |