Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
HTML
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>silo · Aesthetic Computer</title><link rel="icon" href="https://aesthetic.computer/icon/128x128/prompt.png" type="image/png" /><link rel="stylesheet" href="https://aesthetic.computer/type/webfonts/berkeley-mono-variable.css" /><style>:root { --bg: #111; --bg2: #1a1a1a; --fg: #ccc; --fg2: #888; --accent: #6c6; --border: #333; --hover: #222; --ok: #6c6; --err: #c66; --warn: #cc6;}[data-theme="light"] { --bg: #f4f4f4; --bg2: #fff; --fg: #222; --fg2: #777; --accent: #396; --border: #ccc; --hover: #eee; --ok: #396; --err: #c44; --warn: #a80;}* { margin: 0; padding: 0; box-sizing: border-box; }html, body { height: 100%; overflow: hidden; }body { font-family: 'Berkeley Mono Variable', monospace; font-size: 13px; line-height: 1.4; background: var(--bg); color: var(--fg); cursor: url('https://aesthetic.computer/aesthetic.computer/cursors/precise.svg') 12 12, auto;}a { color: var(--accent); text-decoration: none; }a:hover { text-decoration: underline; }
/* login */#login { display: flex; position: fixed; inset: 0; z-index: 100; background: var(--bg); align-items: center; justify-content: center; flex-direction: column; gap: 8px;}#login h1 { font-size: 18px; font-weight: normal; color: var(--fg); letter-spacing: 3px; }#login p { font-size: 11px; color: var(--fg2); }#loginBtn { font-family: inherit; font-size: 12px; padding: 6px 16px; margin-top: 8px; background: var(--bg2); color: var(--fg); border: 1px solid var(--border); cursor: pointer;}#loginBtn:hover { border-color: var(--accent); }#authStatus { font-size: 11px; color: var(--fg2); }
/* layout */#dashboard { display: none; flex-direction: column; height: 100vh; overflow: hidden;}#dashboard.visible { display: flex; }
/* header */.bar { display: flex; align-items: center; gap: 6px; padding: 6px 8px; border-bottom: 1px solid var(--border); background: var(--bg); z-index: 10; flex-shrink: 0; flex-wrap: wrap;}.bar-title { font-size: 14px; color: var(--fg); cursor: pointer; letter-spacing: 2px; }.bar-title:hover { color: var(--accent); }.bar-sep { color: var(--border); }.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--fg2); display: inline-block; flex-shrink: 0; }.dot.ok { background: var(--ok); }.dot.err { background: var(--err); }.bar-dim { font-size: 11px; color: var(--fg2); }.bar-right { margin-left: auto; display: flex; align-items: center; gap: 6px; }.btn { font-family: inherit; font-size: 11px; padding: 2px 8px; background: var(--bg2); color: var(--fg2); border: 1px solid var(--border); cursor: pointer;}.btn:hover { color: var(--fg); border-color: var(--fg2); }
/* tabs */.tab-bar { display: flex; overflow-x: auto; white-space: nowrap; gap: 0; border-bottom: 1px solid var(--border); padding: 0 6px; background: var(--bg); flex-shrink: 0;}.tab-btn { font-family: inherit; font-size: 11px; padding: 5px 10px; background: none; color: var(--fg2); border: none; border-bottom: 2px solid transparent; cursor: pointer; white-space: nowrap;}.tab-btn:hover { color: var(--fg); }.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
/* panels */.panels { flex: 1; overflow: hidden; position: relative; }.panel { display: none; position: absolute; inset: 0; overflow-y: auto; padding: 8px;}.panel.active { display: block; }
/* overview two-col on wide screens */.overview-grid { display: grid; grid-template-columns: 1fr; gap: 6px; }@media (min-width: 700px) { .overview-grid { grid-template-columns: 1fr 1fr; }}
.card { border: 1px solid var(--border); background: var(--bg2); padding: 8px;}.card-hd { font-size: 11px; color: var(--fg2); text-transform: uppercase; letter-spacing: 1px; padding-bottom: 4px; margin-bottom: 6px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center;}.card-hd b { color: var(--fg); font-weight: normal; }
/* stats */.kv { display: flex; justify-content: space-between; padding: 2px 0; font-size: 12px; }.kv .k { color: var(--fg2); }.kv .v { color: var(--fg); font-variant-numeric: tabular-nums; }.kv .v.ok { color: var(--ok); }.kv .v.err { color: var(--err); }.kv .v.warn { color: var(--warn); }
/* table */.tbl { width: 100%; border-collapse: collapse; font-size: 12px; }.tbl th { text-align: left; font-weight: normal; color: var(--fg2); font-size: 10px; text-transform: uppercase; letter-spacing: 1px; padding: 2px 4px; border-bottom: 1px solid var(--border); }.tbl td { padding: 2px 4px; border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent); }.tbl td.r { text-align: right; font-variant-numeric: tabular-nums; }.tbl tr:hover td { background: var(--hover); }
/* storage bars */.sbar { display: flex; align-items: center; gap: 6px; padding: 3px 0; font-size: 12px; }.sbar-name { min-width: 100px; color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.sbar-track { flex: 1; height: 6px; background: var(--border); overflow: hidden; }.sbar-fill { height: 100%; background: var(--accent); }.sbar-val { min-width: 80px; text-align: right; color: var(--fg2); font-size: 11px; font-variant-numeric: tabular-nums; }
/* log */.log-panel { display: none; position: absolute; inset: 0; flex-direction: column; }.log-panel.active { display: flex; }.log-hd { font-size: 11px; color: var(--fg2); text-transform: uppercase; letter-spacing: 1px; padding: 8px 8px 4px; flex-shrink: 0;}.log-hd b { color: var(--fg); font-weight: normal; }.log-scroll { flex: 1; overflow-y: auto; padding: 0 8px 8px; }.log-row { display: flex; gap: 8px; padding: 1px 0; font-size: 11px; }.log-t { color: var(--fg2); min-width: 60px; font-variant-numeric: tabular-nums; }.log-m { color: var(--fg); word-break: break-word; }.log-m.error { color: var(--err); }.log-m.warn { color: var(--warn); }
/* compare */.sync-badge { font-size: 10px; padding: 1px 4px; border: 1px solid; display: inline-block; }.sync-badge.synced { color: var(--ok); border-color: var(--ok); }.sync-badge.unsynced { color: var(--err); border-color: var(--err); }.active-badge { font-size: 10px; padding: 1px 4px; background: var(--accent); color: var(--bg); }
.loading { color: var(--fg2); }
/* sync button */.sync-btn { font-family: inherit; font-size: 10px; padding: 1px 6px; background: var(--bg); color: var(--fg2); border: 1px solid var(--border); cursor: pointer; margin-left: 4px;}.sync-btn:hover { color: var(--fg); border-color: var(--fg2); }.sync-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* firehose */.firehose-panel { padding: 0 !important; overflow: hidden !important; }.firehose-panel canvas { display: block; width: 100%; height: 100%; }.firehose-hud { position: absolute; top: 8px; right: 12px; z-index: 2; display: flex; gap: 12px; font-size: 11px; color: var(--fg2); pointer-events: none;}.firehose-counter { color: var(--fg); }.firehose-rate { color: var(--accent); }.firehose-sidebar { position: absolute; bottom: 8px; left: 8px; z-index: 2; font-size: 10px; pointer-events: none; max-height: calc(100% - 40px); overflow: hidden;}.fh-counter-row { display: flex; align-items: center; gap: 4px; padding: 1px 0; opacity: 0.7;}.fh-counter-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }.fh-counter-ns { color: var(--fg); min-width: 80px; }.fh-counter-val { color: var(--fg); font-variant-numeric: tabular-nums; min-width: 30px; text-align: right; }.fh-counter-ops { color: var(--fg2); margin-left: 2px; }
/* sub-tabs */.sub-tab-bar { display: flex; gap: 0; margin-bottom: 8px; border-bottom: 1px solid var(--border); padding: 0 2px;}.sub-tab-btn { font-family: inherit; font-size: 11px; padding: 4px 10px; background: none; color: var(--fg2); border: none; border-bottom: 2px solid transparent; cursor: pointer; white-space: nowrap;}.sub-tab-btn:hover { color: var(--fg); }.sub-tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }.sub-tab-btn.deprecated { opacity: 0.6; }.dep-badge { font-size: 9px; padding: 1px 3px; background: var(--warn); color: var(--bg); vertical-align: middle; margin-left: 2px;}.sub-panel { display: none; }.sub-panel.active { display: block; }
/* data grid */.data-grid { display: grid; grid-template-columns: 1fr; gap: 8px; }@media (min-width: 700px) { .data-grid { grid-template-columns: 220px 1fr; }}
/* pie chart */#pieChart { display: block; margin: 4px auto; }.pie-legend-row { display: flex; align-items: center; gap: 4px; padding: 2px 0; font-size: 11px; }.pie-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }.pie-label { color: var(--fg); flex: 1; }.pie-val { color: var(--fg2); font-variant-numeric: tabular-nums; }
/* category groups */.cat-group { margin-bottom: 8px; }.cat-hd { font-size: 11px; color: var(--fg); padding: 3px 6px; display: flex; justify-content: space-between; align-items: center; background: color-mix(in srgb, var(--bg2) 80%, var(--border));}.cat-meta { color: var(--fg2); font-size: 10px; }
/* deprecated card */.card.deprecated { border-color: var(--warn); opacity: 0.7; }.card.deprecated:hover { opacity: 0.9; }
/* backup empty */.backup-empty { color: var(--fg2); font-size: 12px; padding: 8px 0; }.backup-detail { font-size: 11px; color: var(--fg2); margin-top: 4px; }
/* browse */.browse-layout { display: grid; grid-template-columns: 1fr; gap: 6px; }@media (min-width: 700px) { .browse-layout { grid-template-columns: 180px 1fr; }}.browse-sidebar { max-height: 60vh; overflow-y: auto; }.browse-coll { padding: 3px 6px; font-size: 12px; cursor: pointer; color: var(--fg2); border-left: 2px solid transparent;}.browse-coll:hover { color: var(--fg); background: var(--hover); }.browse-coll.active { color: var(--accent); border-left-color: var(--accent); }.browse-coll-count { float: right; font-size: 10px; color: var(--fg2); }.browse-input { font-family: inherit; font-size: 11px; padding: 2px 6px; background: var(--bg); color: var(--fg); border: 1px solid var(--border);}.browse-input:focus { border-color: var(--accent); outline: none; }.browse-json { font-family: 'Berkeley Mono Variable', monospace; font-size: 11px; background: var(--bg); color: var(--fg); padding: 8px; border: 1px solid var(--border); overflow: auto; max-height: 50vh; white-space: pre-wrap; word-break: break-word; line-height: 1.4; margin-top: 4px;}.browse-json.editing { border-color: var(--accent); outline: none;}.browse-row { cursor: pointer; }.browse-row:hover td { background: var(--hover); }
@media (max-width: 640px) { .bar { gap: 4px; padding: 4px 6px; } body { font-size: 12px; } .card { padding: 6px; }}</style></head><body><div id="login"> <h1>silo</h1> <p>aesthetic.computer data dashboard</p> <button id="loginBtn">sign in</button> <button id="loginResetBtn" class="btn" style="display:none;margin-top:6px">clear session & retry</button> <p id="authStatus">loading...</p></div>
<div id="dashboard"> <div class="bar"> <span class="bar-title" id="logo">silo</span> <span class="bar-sep">/</span> <span class="dot" id="wsStatus"></span> <span class="bar-dim" id="wsStatusText">ws</span> <span class="bar-sep">/</span> <span class="dot" id="mongoStatus"></span> <span class="bar-dim" id="mongoStatusText">db</span> <span class="bar-sep">/</span> <span class="dot" id="redisStatus"></span> <span class="bar-dim" id="redisStatusText">redis</span> <span class="bar-dim" id="uptimeText"></span> <div class="bar-right"> <button class="btn" id="themeBtn">light</button> <button class="btn" id="logoutBtn" style="display:none">logout</button> </div> </div>
<div class="tab-bar" id="tabBar"> <button class="tab-btn active" data-tab="0">overview</button> <button class="tab-btn" data-tab="1">data</button> <button class="tab-btn" data-tab="2">services</button> <button class="tab-btn" data-tab="3">storage</button> <button class="tab-btn" data-tab="4">log</button> <button class="tab-btn" data-tab="5">firehose</button> <button class="tab-btn" data-tab="6">feed</button> <button class="tab-btn" data-tab="7">telemetry</button> <button class="tab-btn" data-tab="8">lith</button> <button class="tab-btn" data-tab="9">datomic</button> <button class="tab-btn" data-tab="10">kidlisp</button> </div>
<div class="panels"> <!-- overview --> <div class="panel active" data-panel="0"> <div class="overview-grid"> <div class="card"> <div class="card-hd">stats</div> <div class="kv"><span class="k">users</span><span class="v" id="s-users">-</span></div> <div class="kv"><span class="k">paintings</span><span class="v" id="s-paintings">-</span></div> <div class="kv"><span class="k">kidlisp</span><span class="v" id="s-kidlisp">-</span></div> <div class="kv"><span class="k">moods</span><span class="v" id="s-moods">-</span></div> <div class="kv"><span class="k">chat</span><span class="v" id="s-chat">-</span></div> <div class="kv"><span class="k">collections</span><span class="v" id="s-cols">-</span></div> <div class="kv"><span class="k">total docs</span><span class="v" id="s-docs">-</span></div> <div class="kv"><span class="k">db size</span><span class="v" id="s-dbsize">-</span></div> <div class="kv"><span class="k">storage</span><span class="v" id="s-storage">-</span></div> <div class="kv"><span class="k">billing est</span><span class="v" id="s-billing">-</span></div> </div> <div class="card"> <div class="card-hd">services</div> <div class="kv"><span class="k"><span class="dot" id="svc-oven-dot"></span> oven</span><span class="v" id="svc-oven-meta">...</span></div> <div class="kv"><span class="k"><span class="dot" id="svc-session-dot"></span> session</span><span class="v" id="svc-session-meta">...</span></div> <div class="kv"><span class="k"><span class="dot" id="svc-feed-dot"></span> feed</span><span class="v" id="svc-feed-meta">...</span></div> <div class="kv"><span class="k"><span class="dot" id="svc-insta-dot"></span> instagram</span><span class="v" id="svc-insta-meta">...</span></div> <div class="kv"><span class="k"><span class="dot" id="svc-tiktok-dot"></span> tiktok</span><span class="v" id="svc-tiktok-meta">...</span></div> <div style="margin-top:8px"> <div class="card-hd">redis</div> <div id="redisTile" class="loading">loading...</div> </div> <div style="margin-top:8px"> <div class="card-hd">billing</div> <div id="billingTile" class="loading">loading...</div> </div> </div> </div> </div>
<!-- data (collections, backups, atlas) --> <div class="panel" data-panel="1"> <div class="sub-tab-bar"> <button class="sub-tab-btn active" data-subtab="collections">collections</button> <button class="sub-tab-btn" data-subtab="browse">browse</button> <button class="sub-tab-btn" data-subtab="backups">backups</button> <button class="sub-tab-btn deprecated" data-subtab="atlas">atlas <span class="dep-badge">deprecated</span></button> </div>
<div class="sub-panel active" data-subpanel="collections"> <div class="data-grid"> <div class="card"> <div class="card-hd">size by category</div> <canvas id="pieChart"></canvas> <div id="pieLegend"></div> </div> <div class="card"> <div class="card-hd">all collections <b id="colCount"></b></div> <div id="categorizedCollections" class="loading">loading...</div> </div> </div> </div>
<div class="sub-panel" data-subpanel="browse"> <div class="browse-layout"> <div class="browse-sidebar card"> <div class="card-hd">collections</div> <div id="browseCollList" class="loading">loading...</div> </div> <div class="browse-main card"> <div class="card-hd"> <span id="browseCollName">select a collection</span> <span style="display:flex;gap:4px;align-items:center"> <input id="browseSearch" placeholder="search..." class="browse-input" style="width:140px"> <button class="btn" id="browseSearchBtn">go</button> </span> </div> <div id="browseDocs" style="font-size:12px;color:var(--fg2)">click a collection to browse</div> <div id="browsePager" style="margin-top:6px;font-size:11px;display:flex;gap:6px;align-items:center"> <button class="btn" id="browsePrev" disabled>prev</button> <span id="browseRange" style="color:var(--fg2)"></span> <button class="btn" id="browseNext" disabled>next</button> </div> </div> </div> <div id="browseDocDetail" style="display:none"> <div class="card" style="margin-top:6px"> <div class="card-hd"> <span>document <b id="browseDocId"></b></span> <span style="display:flex;gap:4px"> <button class="btn" id="browseDocEdit">edit</button> <button class="btn" id="browseDocSave" style="display:none">save</button> <button class="btn" id="browseDocCancel" style="display:none">cancel</button> <button class="btn" id="browseDocDelete">delete</button> <button class="btn" id="browseDocClose">close</button> </span> </div> <pre id="browseDocJson" class="browse-json"></pre> </div> </div> </div>
<div class="sub-panel" data-subpanel="backups"> <div class="card"> <div class="card-hd">database backups</div> <div id="backupsList" class="loading">loading...</div> </div> </div>
<div class="sub-panel" data-subpanel="atlas"> <div class="card deprecated" style="margin-bottom:6px"> <div class="card-hd"><span style="color:var(--warn)">atlas</span> <b id="dbSyncStatus"></b> <button class="sync-btn" id="syncBtn" title="Sync Atlas to Primary">sync from atlas</button></div> <div style="font-size:11px; color:var(--fg2); margin-bottom:6px;">Atlas is the legacy database. Silo is now the source of truth. This view is kept for reference and emergency sync only.</div> <div id="dbCompare" class="loading">loading...</div> </div> </div> </div>
<!-- services detail --> <div class="panel" data-panel="2"> <div class="card"> <div class="card-hd">services</div> <div class="kv"><span class="k"><span class="dot" id="svc-oven-dot2"></span> oven</span><span class="v" id="svc-oven-meta2">...</span></div> <div class="kv"><span class="k"><span class="dot" id="svc-session-dot2"></span> session</span><span class="v" id="svc-session-meta2">...</span></div> <div class="kv"><span class="k"><span class="dot" id="svc-feed-dot2"></span> feed</span><span class="v" id="svc-feed-meta2">...</span></div> </div> <div class="card" style="margin-top:6px"> <div class="card-hd">redis</div> <div id="redisTile2" class="loading">loading...</div> </div> <div class="card" style="margin-top:6px"> <div class="card-hd">billing</div> <div id="billingTile2" class="loading">loading...</div> </div> <div class="card" style="margin-top:6px"> <div class="card-hd"><span class="dot" id="insta-dot"></span> instagram</div> <div class="kv"><span class="k">status</span><span class="v" id="insta-status">...</span></div> <div class="kv"><span class="k">account</span><span class="v" id="insta-account">-</span></div> <div class="kv"><span class="k">session age</span><span class="v" id="insta-age">-</span></div> <div id="insta-actions" style="margin-top:6px"> <button class="btn" id="instaLoginBtn">login</button> <button class="btn" id="instaLogoutBtn" style="display:none">logout</button> <span id="insta-challenge" style="display:none"> <input id="instaCode" placeholder="verification code" style="font-family:inherit;font-size:11px;padding:2px 6px;background:var(--bg);color:var(--fg);border:1px solid var(--border);width:120px;"> <button class="btn" id="instaVerifyBtn">verify</button> </span> <span id="insta-msg" style="font-size:11px;color:var(--fg2);margin-left:6px"></span> </div> </div> <div class="card" style="margin-top:6px"> <div class="card-hd"><span class="dot" id="tiktok-dot"></span> tiktok</div> <div class="kv"><span class="k">status</span><span class="v" id="tiktok-status">...</span></div> <div class="kv"><span class="k">account</span><span class="v" id="tiktok-account">-</span></div> <div class="kv"><span class="k">token age</span><span class="v" id="tiktok-age">-</span></div> <div class="kv"><span class="k">expires</span><span class="v" id="tiktok-expires">-</span></div> <div id="tiktok-stats" style="display:none;margin-top:4px"> <div class="kv"><span class="k">followers</span><span class="v" id="tiktok-followers">-</span></div> <div class="kv"><span class="k">likes</span><span class="v" id="tiktok-likes">-</span></div> <div class="kv"><span class="k">videos</span><span class="v" id="tiktok-videos">-</span></div> </div> <div id="tiktok-actions" style="margin-top:6px"> <button class="btn" id="tiktokConnectBtn">connect</button> <button class="btn" id="tiktokDisconnectBtn" style="display:none">disconnect</button> <button class="btn" id="tiktokRefreshBtn" style="display:none">refresh token</button> <span id="tiktok-msg" style="font-size:11px;color:var(--fg2);margin-left:6px"></span> </div> </div> </div>
<!-- storage --> <div class="panel" data-panel="3"> <div class="card"> <div class="card-hd">storage <b id="stoTotal"></b></div> <div id="storageBuckets" class="loading">loading...</div> </div> </div>
<!-- log --> <div class="log-panel" data-panel="4"> <div class="log-hd">log <b id="logCount">0</b></div> <div class="log-scroll" id="logEntries"></div> </div>
<!-- firehose --> <div class="panel firehose-panel" data-panel="5"> <div class="firehose-hud"> <span class="firehose-counter"><span id="firehoseCount">0</span> events</span> <span class="firehose-rate" id="firehoseRate">0/s</span> </div> <div class="firehose-sidebar" id="firehoseCounters"></div> <canvas id="firehoseCanvas"></canvas> </div>
<!-- feed (dp1-feed) --> <div class="panel" data-panel="6"> <div class="overview-grid"> <div class="card"> <div class="card-hd">dp1-feed <b id="feed-runtime"></b></div> <div class="kv"><span class="k">status</span><span class="v" id="feed-status">...</span></div> <div class="kv"><span class="k">version</span><span class="v" id="feed-version">-</span></div> <div class="kv"><span class="k">deployment</span><span class="v" id="feed-deployment">-</span></div> <div class="kv"><span class="k">runtime</span><span class="v" id="feed-runtime-val">-</span></div> <div class="kv"><span class="k">response</span><span class="v" id="feed-response">-</span></div> </div> <div class="card"> <div class="card-hd">endpoints</div> <div id="feed-endpoints" style="font-size:11px;color:var(--fg2)">loading...</div> </div> </div> <div class="card" style="margin-top:6px"> <div class="card-hd">playlists <b id="feed-playlist-count"></b></div> <div id="feed-playlists" class="loading">loading...</div> </div> <div class="card" style="margin-top:6px"> <div class="card-hd">channels <b id="feed-channel-count"></b></div> <div id="feed-channels" class="loading">loading...</div> </div> </div>
<!-- telemetry --> <div class="panel" data-panel="7"> <div class="overview-grid"> <div class="card"> <div class="card-hd">GPU / KidLisp Logs</div> <div class="kv"><span class="k">total events</span><span class="v" id="tl-kl-total">-</span></div> <div class="kv"><span class="k">est. size</span><span class="v" id="tl-kl-size">-</span></div> <div class="kv"><span class="k">by type</span><span class="v" id="tl-kl-types" style="white-space:pre-wrap">-</span></div> <div class="kv"><span class="k">by effect</span><span class="v" id="tl-kl-effects" style="white-space:pre-wrap">-</span></div> <div class="kv"><span class="k">by GPU</span><span class="v" id="tl-kl-gpus" style="white-space:pre-wrap">-</span></div> <div style="margin-top:8px"> <button onclick="purgeKidlispLogs()" style="background:#c33;color:#fff;border:none;padding:4px 12px;border-radius:3px;cursor:pointer;font-size:11px">purge all</button> </div> </div> <div class="card"> <div class="card-hd">Boot Logs</div> <div class="kv"><span class="k">total boots</span><span class="v" id="tl-bt-total">-</span></div> <div class="kv"><span class="k">by status</span><span class="v" id="tl-bt-status" style="white-space:pre-wrap">-</span></div> </div> </div> <div class="card" style="margin-top:6px"> <div class="card-hd">recent GPU events</div> <div id="tl-recent" style="font-size:11px;color:var(--fg2);max-height:400px;overflow-y:auto">loading...</div> </div> </div>
<!-- lith panel --> <div class="panel" data-panel="8"> <div class="overview-grid" id="lith-overview"> <div class="card"> <div class="card-hd">lith status</div> <div class="kv"><span class="k">uptime</span><span class="v" id="lith-uptime">-</span></div> <div class="kv"><span class="k">boot</span><span class="v" id="lith-boot">-</span></div> <div class="kv"><span class="k">functions loaded</span><span class="v" id="lith-fn-count">-</span></div> <div class="kv"><span class="k">memory (RSS / heap)</span><span class="v" id="lith-mem">-</span></div> <div class="kv"><span class="k">total calls</span><span class="v" id="lith-total-calls">-</span></div> <div class="kv"><span class="k">total errors</span><span class="v" id="lith-total-errors">-</span></div> </div> <div class="card"> <div class="card-hd">top functions (by calls)</div> <div id="lith-top-fns" style="font-size:11px;max-height:300px;overflow-y:auto">loading...</div> </div> </div>
<div style="display:flex;gap:4px;padding:4px 6px;border-bottom:1px solid var(--border)"> <button class="sub-tab-btn active" onclick="lithSubTab(this,'lith-errors-panel')">errors</button> <button class="sub-tab-btn" onclick="lithSubTab(this,'lith-requests-panel')">recent requests</button> <button class="sub-tab-btn" onclick="lithSubTab(this,'lith-traffic-panel')">traffic</button> </div>
<div id="lith-errors-panel" class="sub-panel active" style="display:block;padding:6px;overflow-y:auto;max-height:calc(100vh - 360px)"> <table style="width:100%;font-size:11px;border-collapse:collapse" id="lith-errors-table"> <thead><tr style="color:var(--fg2);text-align:left"> <th style="padding:2px 6px">time</th> <th style="padding:2px 6px">function</th> <th style="padding:2px 6px">status</th> <th style="padding:2px 6px">path</th> <th style="padding:2px 6px">error</th> </tr></thead> <tbody id="lith-errors-body"></tbody> </table> </div>
<div id="lith-requests-panel" class="sub-panel" style="display:none;padding:6px;overflow-y:auto;max-height:calc(100vh - 360px)"> <table style="width:100%;font-size:11px;border-collapse:collapse" id="lith-requests-table"> <thead><tr style="color:var(--fg2);text-align:left"> <th style="padding:2px 6px">time</th> <th style="padding:2px 6px">function</th> <th style="padding:2px 6px">ms</th> <th style="padding:2px 6px">status</th> <th style="padding:2px 6px">path</th> </tr></thead> <tbody id="lith-requests-body"></tbody> </table> </div>
<div id="lith-traffic-panel" class="sub-panel" style="display:none;padding:6px;overflow-y:auto;max-height:calc(100vh - 360px)"> <div style="display:flex;gap:12px;flex-wrap:wrap"> <div style="flex:1;min-width:250px"> <div style="font-size:12px;color:var(--fg2);margin-bottom:4px">by path (last 500 requests)</div> <div id="lith-traffic-paths" style="font-size:11px">loading...</div> </div> <div style="flex:1;min-width:200px"> <div style="font-size:12px;color:var(--fg2);margin-bottom:4px">by host</div> <div id="lith-traffic-hosts" style="font-size:11px">loading...</div> </div> <div style="min-width:120px"> <div style="font-size:12px;color:var(--fg2);margin-bottom:4px">by status</div> <div id="lith-traffic-status" style="font-size:11px">loading...</div> </div> </div> </div>
</div>
<!-- datomic panel (read-only admin) --> <div class="panel" data-panel="9"> <div class="overview-grid"> <div class="card"> <div class="card-hd">status <b id="dt-basisT"></b></div> <div class="kv"><span class="k"><span class="dot" id="dt-tx-dot"></span> transactor</span><span class="v" id="dt-tx">-</span></div> <div class="kv"><span class="k">schema</span><span class="v" id="dt-schema-ok">-</span></div> <div class="kv"><span class="k">kidlisp entities</span><span class="v" id="dt-c-kidlisp">-</span></div> <div class="kv"><span class="k">users</span><span class="v" id="dt-c-users">-</span></div> <div class="kv"><span class="k">keeps</span><span class="v" id="dt-c-keeps">-</span></div> <div class="kv"><span class="k">tx (last hour)</span><span class="v" id="dt-tx-hour">-</span></div> </div> <div class="card"> <div class="card-hd">backups <span class="bar-dim" style="font-size:10px">pg_dump · 14d retention</span></div> <div id="dt-backups" style="font-size:11px;max-height:200px;overflow-y:auto">loading...</div> </div> </div>
<div style="display:flex;gap:4px;padding:4px 6px;border-bottom:1px solid var(--border);margin-top:6px"> <button class="sub-tab-btn active" onclick="dtSubTab(this,'dt-schema-panel')">schema</button> <button class="sub-tab-btn" onclick="dtSubTab(this,'dt-entities-panel')">entities</button> <button class="sub-tab-btn" onclick="dtSubTab(this,'dt-txlog-panel')">tx log</button> <button class="sub-tab-btn" onclick="dtSubTab(this,'dt-query-panel')">query</button> </div>
<div id="dt-schema-panel" class="sub-panel active" style="display:block;padding:6px;overflow-y:auto;max-height:calc(100vh - 380px)"> <table style="width:100%;font-size:11px;border-collapse:collapse"> <thead><tr style="color:var(--fg2);text-align:left"> <th style="padding:2px 6px">ident</th> <th style="padding:2px 6px">type</th> <th style="padding:2px 6px">card</th> <th style="padding:2px 6px">flags</th> <th style="padding:2px 6px">doc</th> </tr></thead> <tbody id="dt-schema-body"></tbody> </table> </div>
<div id="dt-entities-panel" class="sub-panel" style="display:none;padding:6px;max-height:calc(100vh - 380px);overflow-y:auto"> <div style="display:flex;gap:6px;align-items:center;margin-bottom:6px"> <span class="bar-dim">type:</span> <button class="btn" onclick="dtLoadEntities('kidlisp')">kidlisp</button> <button class="btn" onclick="dtLoadEntities('user')">user</button> <button class="btn" onclick="dtLoadEntities('keep')">keep</button> <span class="bar-dim" id="dt-ent-status" style="margin-left:8px"></span> </div> <div id="dt-entities-list" style="font-size:11px">pick a type</div> <div id="dt-entity-detail" style="margin-top:8px;padding:6px;background:var(--bg);border:1px solid var(--border);display:none"> <div class="card-hd">entity <b id="dt-entity-eid"></b> <span class="bar-right"> <button class="btn" onclick="dtLoadEntityHistory()">history</button> <button class="btn" onclick="document.getElementById('dt-entity-detail').style.display='none'">close</button> </span> </div> <pre id="dt-entity-body" style="white-space:pre-wrap;font-size:11px;margin:0"></pre> </div> </div>
<div id="dt-txlog-panel" class="sub-panel" style="display:none;padding:6px;max-height:calc(100vh - 380px);overflow-y:auto"> <table style="width:100%;font-size:11px;border-collapse:collapse"> <thead><tr style="color:var(--fg2);text-align:left"> <th style="padding:2px 6px">tx</th> <th style="padding:2px 6px">at</th> </tr></thead> <tbody id="dt-txlog-body"></tbody> </table> </div>
<div id="dt-query-panel" class="sub-panel" style="display:none;padding:6px;max-height:calc(100vh - 380px);overflow-y:auto"> <div style="font-size:11px;color:var(--fg2);margin-bottom:4px"> read-only datalog · timeout 5s · capped 10k rows </div> <textarea id="dt-query-text" rows="6" style="width:100%;font-family:inherit;font-size:11px;background:var(--bg);color:var(--fg);border:1px solid var(--border);padding:4px" placeholder="[:find ?code ?hits :where [?e :kidlisp/code ?code] [?e :kidlisp/hits ?hits]]"></textarea> <div style="margin:4px 0;display:flex;gap:4px;align-items:center"> <button class="btn" onclick="dtRunQuery()">run</button> <span class="bar-dim" id="dt-query-status"></span> </div> <pre id="dt-query-result" style="font-size:11px;margin:0;white-space:pre-wrap"></pre> </div> </div>
<!-- kidlisp panel (v2: structural browser via Datomic AST) --> <div class="panel" data-panel="10"> <div style="display:flex;gap:6px;align-items:center;padding:4px 6px;border-bottom:1px solid var(--border)"> <input id="kl-search" placeholder="code or fragment..." class="browse-input" style="font-family:inherit;font-size:11px;padding:3px 6px;background:var(--bg);color:var(--fg);border:1px solid var(--border);width:180px"> <button class="btn" onclick="klLoad('hits')">by hits</button> <button class="btn" onclick="klLoad('recent')">recent</button> <span class="bar-dim" id="kl-status"></span> </div> <div style="display:grid;grid-template-columns:minmax(200px,1fr) 1.2fr 1fr;gap:6px;padding:6px;height:calc(100vh - 150px);overflow:hidden"> <!-- piece list --> <div class="card" style="overflow-y:auto;padding:4px"> <div class="card-hd">pieces <b id="kl-list-count"></b></div> <div id="kl-list" style="font-size:11px">pick a sort to load</div> </div> <!-- source + metadata --> <div class="card" style="overflow-y:auto;padding:4px"> <div class="card-hd"> <span id="kl-detail-code">—</span> <span class="bar-dim" id="kl-detail-meta"></span> </div> <pre id="kl-source" style="font-size:11px;margin:0;white-space:pre-wrap;color:var(--fg)">select a piece</pre> <div style="margin-top:6px;padding-top:4px;border-top:1px solid var(--border);font-size:10px;color:var(--fg2)" id="kl-meta"></div> </div> <!-- AST tree + structural search --> <div class="card" style="overflow-y:auto;padding:4px"> <div class="card-hd">structure <span class="bar-dim" style="font-size:10px">click op → corpus search</span></div> <div id="kl-tree" style="font-size:11px;font-family:inherit">—</div> <div style="margin-top:6px;padding-top:4px;border-top:1px solid var(--border)"> <div class="card-hd"> <span id="kl-struct-label">pieces using X</span> <span class="bar-dim" id="kl-struct-count"></span> </div> <div id="kl-struct-result" style="font-size:11px;max-height:240px;overflow-y:auto">click an op in the tree above</div> </div> </div> </div> </div>
</div></div>
<script>let auth0Client = null, accessToken = null, logCount = 0;// Theme: respect system preference when user hasn't picked one explicitly.const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');const savedTheme = localStorage.getItem('silo-theme');let darkMode = savedTheme ? savedTheme === 'dark' : prefersDark.matches;applyTheme();
// Track system changes only while the user has no explicit preference saved.prefersDark.addEventListener('change', (e) => { if (!localStorage.getItem('silo-theme')) { darkMode = e.matches; applyTheme(); }});
function applyTheme() { document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : 'light'); const btn = document.getElementById('themeBtn'); if (btn) btn.textContent = darkMode ? 'light' : 'dark';}
document.getElementById('themeBtn').onclick = () => { darkMode = !darkMode; localStorage.setItem('silo-theme', darkMode ? 'dark' : 'light'); applyTheme();};
async function authFetch(url) { const resp = await fetch(url, { headers: accessToken ? { Authorization: 'Bearer ' + accessToken } : {}, }); if (resp.status === 401 && auth0Client) { try { accessToken = await auth0Client.getTokenSilently({ cacheMode: 'off' }); return fetch(url, { headers: { Authorization: 'Bearer ' + accessToken } }); } catch { auth0Client.loginWithRedirect(); } } return resp;}
async function initAuth() { document.getElementById('authStatus').textContent = 'loading...'; try { await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = 'https://cdn.auth0.com/js/auth0-spa-js/2.1/auth0-spa-js.production.js'; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); const config = await fetch('/auth/config').then(r => r.json()); auth0Client = await window.auth0.createAuth0Client({ domain: config.domain, clientId: config.clientId, cacheLocation: 'localstorage', useRefreshTokens: true, authorizationParams: { redirect_uri: location.origin + location.pathname }, }); if (location.search.includes('state=') && location.search.includes('code=')) { await auth0Client.handleRedirectCallback(); history.replaceState({}, '', location.pathname); } if (await auth0Client.isAuthenticated()) { accessToken = await auth0Client.getTokenSilently(); const me = await authFetch('/auth/me').then(r => r.json()); if (!me.isAdmin) { document.getElementById('authStatus').textContent = 'access denied [@' + (me.handle || '?') + ']'; document.getElementById('loginBtn').textContent = 'sign out'; document.getElementById('loginBtn').onclick = () => auth0Client.logout({ logoutParams: { returnTo: location.origin + location.pathname } }); return; } document.getElementById('login').style.display = 'none'; document.getElementById('dashboard').classList.add('visible'); document.getElementById('logoutBtn').style.display = 'inline'; connectWS(); loadAll(); } else { document.getElementById('authStatus').textContent = ''; document.getElementById('loginBtn').onclick = () => auth0Client.loginWithRedirect(); } } catch (err) { console.error('Auth init failed:', err); const msg = err?.message || String(err); const isTokenErr = /refresh token|invalid_grant|login_required|unknown.*token|expired/i.test(msg); document.getElementById('authStatus').textContent = isTokenErr ? 'session expired — clear session & retry' : 'error: ' + msg; // Surface a recovery button the user can click. const resetBtn = document.getElementById('loginResetBtn'); resetBtn.style.display = 'inline-block'; resetBtn.onclick = async () => { // Nuke Auth0's local cache, then let them sign in fresh. Works even // when auth0Client itself failed to initialize. try { await auth0Client?.logout({ openUrl: false }); } catch {} Object.keys(localStorage) .filter((k) => k.startsWith('@@auth0spajs@@') || k.includes('auth0')) .forEach((k) => localStorage.removeItem(k)); location.reload(); }; // Sign-in button becomes a full redirect (bypasses any cached state). document.getElementById('loginBtn').onclick = () => { if (auth0Client) auth0Client.loginWithRedirect(); else location.reload(); }; } document.getElementById('logoutBtn').onclick = () => { auth0Client?.logout({ logoutParams: { returnTo: location.origin + location.pathname } }); };}
// wsconst protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';const wsUrl = protocol + '//' + location.host + '/ws';let ws = null;function connectWS() { ws = new WebSocket(wsUrl); ws.onopen = () => dot('wsStatus', true); ws.onclose = () => { dot('wsStatus', false); setTimeout(connectWS, 2000); }; ws.onmessage = (e) => { const data = JSON.parse(e.data); if (data.logEntry) addLog(data.logEntry); if (data.firehose) fhIngest(data.firehose); if (data.fairyPoint) fhFirefly(data.fairyPoint); };}function dot(id, ok) { const el = document.getElementById(id); if (el) el.className = 'dot ' + (ok ? 'ok' : 'err');}
// dataasync function loadOverview() { try { const d = await authFetch('/api/overview').then(r => r.json()); const db = d.db || {}; document.getElementById('s-users').textContent = fmt(db.users); document.getElementById('s-paintings').textContent = fmt(db.paintings); document.getElementById('s-kidlisp').textContent = fmt(db.kidlisp); document.getElementById('s-moods').textContent = fmt(db.moods); document.getElementById('s-chat').textContent = fmt((db.chatSystem||0) + (db.chatClock||0)); document.getElementById('s-cols').textContent = fmt(db.totalCollections); document.getElementById('s-docs').textContent = fmt(db.totalDocuments); if (db.size) { document.getElementById('s-dbsize').textContent = fmtBytes(db.size.dataSize); } document.getElementById('s-storage').textContent = (d.storage?.totalGB || '0') + ' GB'; dot('mongoStatus', true);
// redis (update both overview and detail tabs) if (d.redis?.connected) { dot('redisStatus', true); const redisHtml = kv('memory', d.redis.usedMemory || '-') + kv('peak', d.redis.peakMemory || '-') + kv('keys', fmt(d.redis.totalKeys)) + kv('clients', d.redis.connectedClients) + kv('hit rate', d.redis.hitRate + '%') + kv('uptime', fmtDuration(d.redis.uptimeSeconds)) + kv('version', d.redis.version || '-'); document.getElementById('redisTile').innerHTML = redisHtml; const r2 = document.getElementById('redisTile2'); if (r2) r2.innerHTML = redisHtml; } else { dot('redisStatus', false); const msg = '<span class="loading">not connected</span>'; document.getElementById('redisTile').innerHTML = msg; const r2 = document.getElementById('redisTile2'); if (r2) r2.innerHTML = msg; }
// uptime const up = Math.floor((d.uptime || 0) / 1000); document.getElementById('uptimeText').textContent = Math.floor(up/3600) + 'h' + Math.floor((up%3600)/60) + 'm'; } catch (e) { console.error(e); }}
async function loadCompare() { try { const d = await authFetch('/api/db/compare').then(r => r.json()); const el = document.getElementById('dbCompare'); const badge = document.getElementById('dbSyncStatus');
if (d.sameConnection) { badge.innerHTML = '<span class="sync-badge synced">same db</span>'; el.innerHTML = kv('primary', d.primaryLabel) + kv('active', '<span class="active-badge">' + d.activeDb + '</span>') + kv('status', 'single connection') + (d.primarySize ? kv('data', fmtBytes(d.primarySize.dataSize)) + kv('indexes', fmtBytes(d.primarySize.indexSize)) : ''); } else { badge.innerHTML = d.allSynced ? '<span class="sync-badge synced">synced</span>' : '<span class="sync-badge unsynced">out of sync</span>';
let html = kv('primary', d.primaryLabel + (d.primaryConnected ? '' : ' (down)')) + kv('atlas', d.atlasLabel + (d.atlasConnected ? '' : ' (down)')) + kv('active', '<span class="active-badge">' + d.activeDb + '</span>');
if (d.primarySize) html += kv('primary size', fmtBytes(d.primarySize.dataSize)); if (d.atlasSize) html += kv('atlas size', fmtBytes(d.atlasSize.dataSize));
// show mismatched collections const mismatched = (d.collections || []).filter(c => !c.synced); if (mismatched.length > 0) { html += '<div style="margin-top:6px; font-size:11px; color:var(--fg2)">mismatched:</div>'; html += '<table class="tbl" style="margin-top:2px"><thead><tr><th>collection</th><th class="r">primary</th><th class="r">atlas</th></tr></thead><tbody>'; for (const c of mismatched) { html += '<tr><td>' + esc(c.name) + '</td><td class="r">' + (c.primary ?? '-') + '</td><td class="r">' + (c.atlas ?? '-') + '</td></tr>'; } html += '</tbody></table>'; } el.innerHTML = html; } } catch (e) { document.getElementById('dbCompare').innerHTML = '<span class="loading">error loading</span>'; }}
let cachedCollections = null, cachedCatMeta = {};async function loadCollections() { try { const resp = await authFetch('/api/db/collections').then(r => r.json()); const cols = resp.collections || resp; const cats = resp.categories || {}; cachedCollections = cols; cachedCatMeta = cats; document.getElementById('colCount').textContent = cols.length; renderCategorizedCollections(cols, cats); drawPieChart(cols, cats); } catch (e) { const el = document.getElementById('categorizedCollections'); if (el) el.innerHTML = '<span class="loading">error loading</span>'; }}
function renderCategorizedCollections(collections, catMeta) { const el = document.getElementById('categorizedCollections'); if (!el) return; const catOrder = ['identity', 'content', 'communication', 'system', 'other']; const groups = {}; for (const c of collections) { const cat = c.category || 'other'; if (!groups[cat]) groups[cat] = []; groups[cat].push(c); } let html = ''; for (const cat of catOrder) { const items = groups[cat]; if (!items || items.length === 0) continue; const meta = catMeta[cat] || { label: cat, color: '#888' }; const catTotal = items.reduce((s, c) => s + c.count, 0); const catSize = items.reduce((s, c) => s + (c.size || 0), 0); html += '<div class="cat-group">'; html += '<div class="cat-hd" style="border-left:3px solid ' + meta.color + '">'; html += '<span>' + esc(meta.label) + '</span>'; html += '<span class="cat-meta">' + fmt(catTotal) + ' docs / ' + fmtBytes(catSize) + '</span>'; html += '</div>'; html += '<table class="tbl"><thead><tr><th>name</th><th style="text-align:right">docs</th><th style="text-align:right">size</th></tr></thead><tbody>'; for (const c of items.sort((a, b) => b.count - a.count)) { html += '<tr><td>' + esc(c.name) + '</td><td class="r">' + fmt(c.count) + '</td><td class="r">' + fmtBytes(c.size) + '</td></tr>'; } html += '</tbody></table></div>'; } el.innerHTML = html; el.classList.remove('loading');}
function drawPieChart(collections, catMeta) { const canvas = document.getElementById('pieChart'); if (!canvas) return; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const container = canvas.parentElement; const size = Math.min(container.clientWidth - 16, 200); canvas.width = size * dpr; canvas.height = size * dpr; canvas.style.width = size + 'px'; canvas.style.height = size + 'px';
// Aggregate by category const catTotals = {}; for (const c of collections) { const cat = c.category || 'other'; catTotals[cat] = (catTotals[cat] || 0) + (c.size || c.count || 0); } const total = Object.values(catTotals).reduce((s, v) => s + v, 0); if (total === 0) { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--fg2').trim(); ctx.font = (11 * dpr) + 'px "Berkeley Mono Variable", monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('no data', canvas.width / 2, canvas.height / 2); return; }
const cx = canvas.width / 2, cy = canvas.height / 2; const radius = Math.min(cx, cy) * 0.9; let startAngle = -Math.PI / 2; const slices = Object.entries(catTotals).sort((a, b) => b[1] - a[1]);
ctx.clearRect(0, 0, canvas.width, canvas.height); for (const [cat, val] of slices) { const sliceAngle = (val / total) * Math.PI * 2; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, radius, startAngle, startAngle + sliceAngle); ctx.closePath(); ctx.fillStyle = (catMeta[cat] || { color: '#888' }).color; ctx.fill(); startAngle += sliceAngle; } // Donut hole const bg2 = getComputedStyle(document.documentElement).getPropertyValue('--bg2').trim() || '#1a1a1a'; ctx.beginPath(); ctx.arc(cx, cy, radius * 0.55, 0, Math.PI * 2); ctx.fillStyle = bg2; ctx.fill(); // Center text ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--fg').trim() || '#ccc'; ctx.font = (12 * dpr) + 'px "Berkeley Mono Variable", monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(fmtBytes(total), cx, cy);
// Legend const legend = document.getElementById('pieLegend'); if (legend) { legend.innerHTML = slices.map(([cat, val]) => { const pct = ((val / total) * 100).toFixed(0); const meta = catMeta[cat] || { label: cat, color: '#888' }; return '<div class="pie-legend-row">' + '<span class="pie-dot" style="background:' + meta.color + '"></span>' + '<span class="pie-label">' + esc(meta.label) + '</span>' + '<span class="pie-val">' + pct + '%</span></div>'; }).join(''); }}
async function loadBackups() { const el = document.getElementById('backupsList'); if (!el) return; el.innerHTML = '<span class="loading">loading...</span>'; try { const data = await authFetch('/api/db/backups').then(r => r.json()); if (!data.backups || data.backups.length === 0) { let html = '<div class="backup-empty">No backups found.'; html += '<div class="backup-detail">Local path: ' + esc(data.backupPath || '/var/backups/mongodb') + '</div>'; if (data.backupBucket) { html += '<div class="backup-detail">S3 bucket: ' + esc(data.backupBucket) + '</div>'; } else { html += '<div class="backup-detail">S3 bucket: not configured (set BACKUP_BUCKET)</div>'; } html += '</div>'; el.innerHTML = html; el.classList.remove('loading'); return; } let html = '<table class="tbl"><thead><tr><th>name</th><th>source</th><th style="text-align:right">size</th><th style="text-align:right">date</th></tr></thead><tbody>'; for (const b of data.backups) { const date = b.date ? new Date(b.date).toLocaleString() : '-'; const srcBadge = b.source === 's3' ? '<span style="color:var(--accent)">s3</span>' : '<span style="color:var(--fg2)">local</span>'; html += '<tr><td>' + esc(b.name) + '</td><td>' + srcBadge + '</td><td class="r">' + fmtBytes(b.size) + '</td><td class="r">' + date + '</td></tr>'; } html += '</tbody></table>'; el.innerHTML = html; el.classList.remove('loading'); } catch (e) { el.innerHTML = '<span class="loading">error loading backups</span>'; }}
async function loadStorage() { try { const data = await authFetch('/api/storage/buckets').then(r => r.json()); const el = document.getElementById('storageBuckets'); if (!data.length) { el.textContent = 'none'; return; } const maxGB = Math.max(...data.map(b => parseFloat(b.gb)), 0.01); const totalGB = data.reduce((s, b) => s + parseFloat(b.gb), 0).toFixed(2); document.getElementById('stoTotal').textContent = totalGB + ' GB'; el.innerHTML = data.map(b => { const pct = (parseFloat(b.gb) / maxGB * 100).toFixed(0); return '<div class="sbar"><span class="sbar-name">' + esc(b.bucket) + '</span>' + '<div class="sbar-track"><div class="sbar-fill" style="width:' + pct + '%"></div></div>' + '<span class="sbar-val">' + b.gb + ' GB / ' + fmt(b.objects) + '</span></div>'; }).join(''); el.classList.remove('loading'); } catch (e) { document.getElementById('storageBuckets').textContent = 'error'; }}
async function loadServices() { checkSvc('oven'); checkSvc('session'); checkSvc('feed'); }async function checkSvc(name) { try { const d = await authFetch('/api/services/' + name).then(r => r.json()); const ok = d.status === 'ok'; dot('svc-' + name + '-dot', ok); dot('svc-' + name + '-dot2', ok); const txt = ok ? d.responseMs + 'ms' : 'down'; const cls = 'v ' + (ok ? 'ok' : 'err'); for (const suffix of ['', '2']) { const el = document.getElementById('svc-' + name + '-meta' + suffix); if (el) { el.textContent = txt; el.className = cls; } } } catch (e) { dot('svc-' + name + '-dot', false); dot('svc-' + name + '-dot2', false); for (const suffix of ['', '2']) { const m = document.getElementById('svc-' + name + '-meta' + suffix); if (m) { m.textContent = 'error'; m.className = 'v err'; } } }}
async function loadBilling() { const setTiles = (html) => { const b1 = document.getElementById('billingTile'); const b2 = document.getElementById('billingTile2'); if (b1) b1.innerHTML = html; if (b2) b2.innerHTML = html; }; try { const d = await authFetch('/api/services/billing').then(r => r.json()); if (d.status !== 'ok') { setTiles('<span class="loading">unavailable</span>'); document.getElementById('s-billing').textContent = '-'; return; }
const providers = Array.isArray(d.providers) ? d.providers : []; const top = providers .filter(p => p && !p.skipped) .slice(0, 4) .map(p => p.name || p.provider) .join(', ') || 'none';
setTiles( kv('estimate', d.summary?.monthlyEstimate || '-') + kv('providers', fmt(providers.length)) + kv('top', esc(top)) + kv('updated', d.generated ? new Date(d.generated).toLocaleTimeString() : '-') );
document.getElementById('s-billing').textContent = d.summary?.monthlyEstimate || '-'; } catch (e) { setTiles('<span class="loading">error</span>'); document.getElementById('s-billing').textContent = '-'; }}
// --- Instagram ---let instaTwoFactorId = null, instaTwoFactorMethod = null;
async function loadInstaStatus() { try { const d = await authFetch('/api/insta/status').then(r => r.json()); const ok = d.loggedIn; dot('insta-dot', ok); dot('svc-insta-dot', ok); const el = document.getElementById('insta-status'); const metaEl = document.getElementById('svc-insta-meta'); if (ok) { el.textContent = 'active'; el.className = 'v ok'; if (metaEl) { metaEl.textContent = '@' + (d.username || '?'); metaEl.className = 'v ok'; } document.getElementById('instaLoginBtn').style.display = 'none'; document.getElementById('instaLogoutBtn').style.display = ''; } else if (d.challengeInProgress) { el.textContent = 'challenge'; el.className = 'v warn'; if (metaEl) { metaEl.textContent = 'challenge'; metaEl.className = 'v warn'; } document.getElementById('insta-challenge').style.display = ''; } else { el.textContent = 'not logged in'; el.className = 'v err'; if (metaEl) { metaEl.textContent = 'offline'; metaEl.className = 'v err'; } document.getElementById('instaLoginBtn').style.display = ''; document.getElementById('instaLogoutBtn').style.display = 'none'; } document.getElementById('insta-account').textContent = d.username ? '@' + d.username : '-'; document.getElementById('insta-age').textContent = d.sessionAge != null ? fmtDuration(d.sessionAge) : '-'; } catch (e) { dot('insta-dot', false); dot('svc-insta-dot', false); const metaEl = document.getElementById('svc-insta-meta'); if (metaEl) { metaEl.textContent = 'error'; metaEl.className = 'v err'; } }}
document.getElementById('instaLoginBtn').onclick = async () => { const msg = document.getElementById('insta-msg'); msg.textContent = 'logging in...'; try { const resp = await fetch('/api/insta/login', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(accessToken ? { Authorization: 'Bearer ' + accessToken } : {}), }, }); const d = await resp.json(); if (d.ok) { msg.textContent = 'logged in!'; loadInstaStatus(); } else if (d.challenge) { msg.textContent = d.message || 'check email/SMS'; document.getElementById('insta-challenge').style.display = ''; instaTwoFactorId = null; } else if (d.twoFactor) { msg.textContent = d.message || 'enter 2FA code'; document.getElementById('insta-challenge').style.display = ''; instaTwoFactorId = d.twoFactorIdentifier; instaTwoFactorMethod = d.method; } else { msg.textContent = d.error || 'failed'; } } catch (e) { msg.textContent = 'error: ' + e.message; }};
document.getElementById('instaVerifyBtn').onclick = async () => { const code = document.getElementById('instaCode').value.trim(); if (!code) return; const msg = document.getElementById('insta-msg'); msg.textContent = 'verifying...'; try { const body = { code }; if (instaTwoFactorId) { body.twoFactorIdentifier = instaTwoFactorId; body.method = instaTwoFactorMethod; } const resp = await fetch('/api/insta/challenge', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(accessToken ? { Authorization: 'Bearer ' + accessToken } : {}), }, body: JSON.stringify(body), }); const d = await resp.json(); if (d.ok) { msg.textContent = 'verified!'; document.getElementById('insta-challenge').style.display = 'none'; instaTwoFactorId = null; loadInstaStatus(); } else { msg.textContent = d.error || 'verification failed'; } } catch (e) { msg.textContent = 'error: ' + e.message; }};
document.getElementById('instaLogoutBtn').onclick = async () => { const msg = document.getElementById('insta-msg'); msg.textContent = 'logging out...'; try { await fetch('/api/insta/logout', { method: 'POST', headers: accessToken ? { Authorization: 'Bearer ' + accessToken } : {}, }); msg.textContent = ''; loadInstaStatus(); } catch (e) { msg.textContent = 'error: ' + e.message; }};
// --- TikTok ---async function loadTiktokStatus() { try { const d = await authFetch('/api/tiktok/status').then(r => r.json()); const ok = d.connected; dot('tiktok-dot', ok); dot('svc-tiktok-dot', ok); const el = document.getElementById('tiktok-status'); const metaEl = document.getElementById('svc-tiktok-meta'); if (ok) { el.textContent = 'connected'; el.className = 'v ok'; if (metaEl) { metaEl.textContent = '@' + (d.username || '?'); metaEl.className = 'v ok'; } document.getElementById('tiktokConnectBtn').style.display = 'none'; document.getElementById('tiktokDisconnectBtn').style.display = ''; document.getElementById('tiktokRefreshBtn').style.display = ''; // Fetch live stats try { const profile = await fetch('/tiktok?action=profile').then(r => r.json()); if (profile.followerCount != null) { document.getElementById('tiktok-stats').style.display = ''; document.getElementById('tiktok-followers').textContent = profile.followerCountFormatted; document.getElementById('tiktok-likes').textContent = profile.likesCountFormatted; document.getElementById('tiktok-videos').textContent = profile.videoCountFormatted; } } catch {} } else { el.textContent = 'not connected'; el.className = 'v err'; if (metaEl) { metaEl.textContent = 'offline'; metaEl.className = 'v err'; } document.getElementById('tiktokConnectBtn').style.display = ''; document.getElementById('tiktokDisconnectBtn').style.display = 'none'; document.getElementById('tiktokRefreshBtn').style.display = 'none'; document.getElementById('tiktok-stats').style.display = 'none'; } document.getElementById('tiktok-account').textContent = d.username ? '@' + d.username : '-'; document.getElementById('tiktok-age').textContent = d.sessionAge != null ? fmtDuration(d.sessionAge) : '-'; document.getElementById('tiktok-expires').textContent = d.expiresAt ? new Date(d.expiresAt).toLocaleString() : '-'; } catch (e) { dot('tiktok-dot', false); dot('svc-tiktok-dot', false); const metaEl = document.getElementById('svc-tiktok-meta'); if (metaEl) { metaEl.textContent = 'error'; metaEl.className = 'v err'; } }}
document.getElementById('tiktokConnectBtn').onclick = async () => { const msg = document.getElementById('tiktok-msg'); msg.textContent = 'redirecting...'; try { const d = await authFetch('/api/tiktok/auth').then(r => r.json()); if (d.url) { const popup = window.open(d.url, 'tiktok-auth', 'width=600,height=700'); // Poll for popup close const check = setInterval(() => { if (!popup || popup.closed) { clearInterval(check); msg.textContent = ''; loadTiktokStatus(); } }, 1000); } else { msg.textContent = d.error || 'failed to get auth URL'; } } catch (e) { msg.textContent = 'error: ' + e.message; }};
document.getElementById('tiktokDisconnectBtn').onclick = async () => { const msg = document.getElementById('tiktok-msg'); msg.textContent = 'disconnecting...'; try { await fetch('/api/tiktok/disconnect', { method: 'POST', headers: accessToken ? { Authorization: 'Bearer ' + accessToken } : {}, }); msg.textContent = ''; loadTiktokStatus(); } catch (e) { msg.textContent = 'error: ' + e.message; }};
document.getElementById('tiktokRefreshBtn').onclick = async () => { const msg = document.getElementById('tiktok-msg'); msg.textContent = 'refreshing...'; try { const d = await authFetch('/api/tiktok/refresh', { method: 'POST' }).then(r => r.json()); msg.textContent = d.ok ? 'refreshed!' : 'refresh failed'; loadTiktokStatus(); } catch (e) { msg.textContent = 'error: ' + e.message; }};
// log (append at bottom, auto-scroll when near bottom)function addLog(entry) { logCount++; document.getElementById('logCount').textContent = logCount; const el = document.getElementById('logEntries'); const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40; const div = document.createElement('div'); div.className = 'log-row'; const t = new Date(entry.time); const ts = String(t.getHours()).padStart(2,'0') + ':' + String(t.getMinutes()).padStart(2,'0') + ':' + String(t.getSeconds()).padStart(2,'0'); div.innerHTML = '<span class="log-t">' + ts + '</span><span class="log-m ' + (entry.type || '') + '">' + esc(entry.msg) + '</span>'; el.appendChild(div); while (el.children.length > 200) el.removeChild(el.firstChild); if (atBottom) el.scrollTop = el.scrollHeight;}
// utilfunction fmt(n) { return n == null ? '-' : Number(n).toLocaleString(); }function esc(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; }function kv(k, v) { return '<div class="kv"><span class="k">' + k + '</span><span class="v">' + v + '</span></div>'; }function fmtBytes(b) { if (b == null) return '-'; if (b > 1e9) return (b/1e9).toFixed(2) + ' GB'; if (b > 1e6) return (b/1e6).toFixed(1) + ' MB'; if (b > 1e3) return (b/1e3).toFixed(0) + ' KB'; return b + ' B';}function fmtDuration(s) { if (!s) return '-'; const d = Math.floor(s/86400), h = Math.floor((s%86400)/3600), m = Math.floor((s%3600)/60); if (d > 0) return d + 'd ' + h + 'h'; if (h > 0) return h + 'h ' + m + 'm'; return m + 'm';}
document.getElementById('logo').onclick = () => { location.href = location.host === 'silo.aesthetic.computer' ? 'https://aesthetic.computer' : '/';};
// --- Firehose Visualization ---const FIREHOSE_COLORS = { 'chat-system': '#4a4', 'chat-clock': '#4c4', 'chat-sotce': '#6a4', 'users': '#48f', '@handles': '#68c', 'verifications': '#cc4', 'paintings': '#a6f', 'pieces': '#c6a', 'kidlisp': '#fa4', 'moods': '#f8a', 'tapes': '#f80',};const FH_DEFAULT_COLOR = '#888';const OP_SYM = { insert: '+', update: '~', replace: '=', delete: '-' };
const fh = { particles: [], ambient: [], fireflies: [], canvas: null, ctx: null, running: false, totalEvents: 0, eventsThisSec: 0, eps: 0, lastRateReset: Date.now(), MAX_P: 200, MAX_AMB: 30, MAX_FLY: 60, counters: {},};
const FH_URL_COLOR = '#0ee';const FH_TS_COLOR = '#777';const FH_URL_RE = /(https?:\/\/[^\s"'<>]+|[a-z0-9-]+\.[a-z]{2,}(?:\/[^\s"'<>]*))/gi;
function fhLabelSegments(label, baseColor) { const segs = []; let last = 0, m; FH_URL_RE.lastIndex = 0; while ((m = FH_URL_RE.exec(label)) !== null) { if (m.index > last) segs.push({ text: label.slice(last, m.index), color: baseColor }); segs.push({ text: m[0], color: FH_URL_COLOR }); last = FH_URL_RE.lastIndex; } if (last < label.length) segs.push({ text: label.slice(last), color: baseColor }); return segs;}
function fhTimestamp(time) { return new Date(time).toLocaleTimeString('en-US', { timeZone: 'America/Los_Angeles', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, });}
function fhParticle(ev) { const c = fh.canvas; if (!c) return null; const dpr = window.devicePixelRatio || 1; const w = c.width / dpr, h = c.height / dpr; const color = FIREHOSE_COLORS[ev.ns] || FH_DEFAULT_COLOR; const ts = fhTimestamp(ev.time); const body = (OP_SYM[ev.op] || '?') + ' ' + ev.ns + (ev.summary ? ' ' + ev.summary : ''); const bodySegs = fhLabelSegments(body, color); const segments = [{ text: ts + ' ', color: FH_TS_COLOR }, ...bodySegs]; return { x: 40 + Math.random() * 60, y: 40 + Math.random() * (h - 80), vx: 0.5 + Math.random() * 1.5, vy: (Math.random() - 0.5) * 0.3, color, segments, label: ts + ' ' + body, alpha: 1, size: 4 + Math.random() * 3, life: 5000 + Math.random() * 3000, born: Date.now(), };}
function fhAmbient() { const c = fh.canvas; if (!c) return null; const dpr = window.devicePixelRatio || 1; return { x: Math.random() * (c.width / dpr), y: Math.random() * (c.height / dpr), vx: (Math.random() - 0.5) * 0.2, vy: (Math.random() - 0.5) * 0.15, alpha: 0.08 + Math.random() * 0.12, size: 1 + Math.random() * 2, color: darkMode ? '#444' : '#ccc', life: 8000 + Math.random() * 6000, born: Date.now(), };}
const FIREFLY_COLORS = ['#fa4', '#fc6', '#f80', '#fe8', '#fd4', '#ec6'];
function fhFirefly(point) { if (!fh.canvas) return; const dpr = window.devicePixelRatio || 1; const w = fh.canvas.width / dpr, h = fh.canvas.height / dpr; const nx = Math.max(0, Math.min(1, point.x)); const ny = Math.max(0, Math.min(1, point.y)); if (fh.fireflies.length >= fh.MAX_FLY) fh.fireflies.shift(); fh.fireflies.push({ x: nx * w, y: ny * h, vx: (Math.random() - 0.5) * 0.3, vy: (Math.random() - 0.5) * 0.3, alpha: 0.9, size: 2 + Math.random() * 2, color: FIREFLY_COLORS[Math.floor(Math.random() * FIREFLY_COLORS.length)], life: 1500 + Math.random() * 1000, born: Date.now(), pulsePhase: Math.random() * Math.PI * 2, });}
function fhIngest(ev) { fh.totalEvents++; fh.eventsThisSec++; if (!fh.counters[ev.ns]) fh.counters[ev.ns] = {}; fh.counters[ev.ns][ev.op] = (fh.counters[ev.ns][ev.op] || 0) + 1; const el = document.getElementById('firehoseCount'); if (el) el.textContent = fh.totalEvents.toLocaleString(); fhUpdateCounters(); if (fh.particles.length >= fh.MAX_P) fh.particles.shift(); const p = fhParticle(ev); if (p) fh.particles.push(p);}
function fhUpdateCounters() { const el = document.getElementById('firehoseCounters'); if (!el) return; const entries = Object.entries(fh.counters) .map(([ns, ops]) => { const total = Object.values(ops).reduce((s, n) => s + n, 0); return { ns, total, ops }; }) .sort((a, b) => b.total - a.total); el.innerHTML = entries.map(e => { const color = FIREHOSE_COLORS[e.ns] || FH_DEFAULT_COLOR; const parts = Object.entries(e.ops).map(([op, n]) => (OP_SYM[op] || op) + n).join(' '); return '<div class="fh-counter-row">' + '<span class="fh-counter-dot" style="background:' + color + '"></span>' + '<span class="fh-counter-ns">' + e.ns + '</span>' + '<span class="fh-counter-val">' + e.total + '</span>' + '<span class="fh-counter-ops">' + parts + '</span></div>'; }).join('');}
function fhTick() { if (!fh.running) return; requestAnimationFrame(fhTick); const { canvas, ctx, particles, ambient } = fh; if (!canvas || !ctx) return; const now = Date.now(); const dpr = window.devicePixelRatio || 1;
if (now - fh.lastRateReset >= 1000) { fh.eps = fh.eventsThisSec; fh.eventsThisSec = 0; fh.lastRateReset = now; const rEl = document.getElementById('firehoseRate'); if (rEl) rEl.textContent = fh.eps + '/s'; }
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = darkMode ? '#111' : '#f4f4f4'; ctx.fillRect(0, 0, canvas.width, canvas.height);
while (ambient.length < fh.MAX_AMB) { const a = fhAmbient(); if (a) ambient.push(a); } for (let i = ambient.length - 1; i >= 0; i--) { const p = ambient[i]; const age = now - p.born; if (age > p.life) { ambient.splice(i, 1); continue; } p.x += p.vx; p.y += p.vy; const prog = age / p.life; const fade = prog < 0.1 ? prog / 0.1 : prog > 0.8 ? (1 - prog) / 0.2 : 1; ctx.globalAlpha = p.alpha * fade; ctx.beginPath(); ctx.arc(p.x * dpr, p.y * dpr, p.size * dpr, 0, Math.PI * 2); ctx.fillStyle = p.color; ctx.fill(); }
// Fireflies (fairy:point cursors from session server) for (let i = fh.fireflies.length - 1; i >= 0; i--) { const f = fh.fireflies[i]; const age = now - f.born; if (age > f.life) { fh.fireflies.splice(i, 1); continue; } f.x += f.vx; f.y += f.vy; const prog = age / f.life; const fade = prog < 0.05 ? prog / 0.05 : prog > 0.6 ? (1 - prog) / 0.4 : 1; const pulse = 0.7 + 0.3 * Math.sin(age * 0.008 + f.pulsePhase); const alpha = f.alpha * fade * pulse; // Outer glow ctx.globalAlpha = alpha * 0.3; ctx.beginPath(); ctx.arc(f.x * dpr, f.y * dpr, (f.size * 2.5) * dpr, 0, Math.PI * 2); ctx.fillStyle = f.color; ctx.fill(); // Inner core ctx.globalAlpha = alpha; ctx.beginPath(); ctx.arc(f.x * dpr, f.y * dpr, f.size * dpr, 0, Math.PI * 2); ctx.fillStyle = f.color; ctx.fill(); }
ctx.textBaseline = 'middle'; ctx.font = (11 * dpr) + 'px "Berkeley Mono Variable", monospace'; for (let i = particles.length - 1; i >= 0; i--) { const p = particles[i]; const age = now - p.born; if (age > p.life) { particles.splice(i, 1); continue; } p.x += p.vx; p.y += p.vy; const prog = age / p.life; p.alpha = prog < 0.05 ? prog / 0.05 : 1 - prog * prog; if (p.alpha <= 0) { particles.splice(i, 1); continue; } ctx.globalAlpha = p.alpha; ctx.beginPath(); ctx.arc(p.x * dpr, p.y * dpr, p.size * dpr, 0, Math.PI * 2); ctx.fillStyle = p.color; ctx.fill(); let textX = (p.x + p.size + 6) * dpr; const textY = p.y * dpr; if (p.segments) { for (const seg of p.segments) { ctx.globalAlpha = p.alpha * 0.85; ctx.fillStyle = seg.color; ctx.fillText(seg.text, textX, textY); textX += ctx.measureText(seg.text).width; } } else { ctx.globalAlpha = p.alpha * 0.85; ctx.fillStyle = p.color; ctx.fillText(p.label, textX, textY); } } ctx.globalAlpha = 1;}
function fhResize() { const c = fh.canvas; if (!c) return; const panel = c.parentElement; if (!panel) return; const dpr = window.devicePixelRatio || 1; const rect = panel.getBoundingClientRect(); c.width = rect.width * dpr; c.height = rect.height * dpr; c.style.width = rect.width + 'px'; c.style.height = rect.height + 'px';}
function fhInit() { fh.canvas = document.getElementById('firehoseCanvas'); if (!fh.canvas) return; fh.ctx = fh.canvas.getContext('2d'); fhResize(); window.addEventListener('resize', fhResize);}function fhStart() { if (fh.running) return; fh.running = true; fhResize(); fhTick(); }function fhStop() { fh.running = false; }
// --- tabs (always visible, persisted) ---let activeTab = parseInt(localStorage.getItem('silo-tab') || '0');const tabBtns = document.querySelectorAll('.tab-btn');const panels = document.querySelectorAll('[data-panel]');
// Hoisted ahead of setTab() call below — these are set when their// respective tabs first load. Declared here (rather than further down)// to avoid let/const temporal-dead-zone errors when setTab(activeTab)// runs synchronously at end of tab setup and activeTab happens to// point at a lazy-loaded tab (feed=6, telemetry=7).let feedLoaded = false;let telemetryLoaded = false;
function setTab(idx) { activeTab = idx; localStorage.setItem('silo-tab', idx); tabBtns.forEach((b, i) => b.classList.toggle('active', i === idx)); panels.forEach(p => p.classList.toggle('active', parseInt(p.dataset.panel) === idx)); if (idx === 4) { const el = document.getElementById('logEntries'); requestAnimationFrame(() => el.scrollTop = el.scrollHeight); } if (idx === 5) { fhStart(); } else { fhStop(); } if (idx === 6 && !feedLoaded) { loadFeed(); } if (idx === 7 && !telemetryLoaded) { loadTelemetry(); }}tabBtns.forEach(b => b.addEventListener('click', () => setTab(parseInt(b.dataset.tab))));setTab(activeTab);
// --- data sub-tabs ---let backupsLoaded = false;const subTabBtns = document.querySelectorAll('.sub-tab-btn');const subPanels = document.querySelectorAll('.sub-panel');subTabBtns.forEach(btn => { btn.addEventListener('click', () => { const target = btn.dataset.subtab; subTabBtns.forEach(b => b.classList.toggle('active', b.dataset.subtab === target)); subPanels.forEach(p => p.classList.toggle('active', p.dataset.subpanel === target)); if (target === 'backups' && !backupsLoaded) { backupsLoaded = true; loadBackups(); } if (target === 'browse' && !browseLoaded) { browseLoaded = true; loadBrowseCollections(); } });});
// --- sync button ---document.getElementById('syncBtn').onclick = async () => { if (!confirm('Sync all collections from Atlas to primary?\nThis will overwrite primary data.')) return; const btn = document.getElementById('syncBtn'); btn.disabled = true; btn.textContent = 'syncing...'; try { const resp = await fetch('/api/db/sync', { method: 'POST', headers: accessToken ? { Authorization: 'Bearer ' + accessToken, 'Content-Type': 'application/json' } : {}, }); const data = await resp.json(); if (data.ok) { btn.textContent = 'done (' + data.collections.length + ' collections)'; loadCompare(); loadOverview(); loadCollections(); setTimeout(() => { btn.textContent = 'sync from atlas'; btn.disabled = false; }, 4000); } else { alert('Sync failed: ' + (data.error || 'unknown')); btn.textContent = 'sync from atlas'; btn.disabled = false; } } catch (e) { alert('Sync failed: ' + e.message); btn.textContent = 'sync from atlas'; btn.disabled = false; }};
// --- feed tab ---// --- telemetry tab ---// telemetryLoaded hoisted near top of script (before setTab call).async function loadTelemetry() { telemetryLoaded = true; try { const data = await authFetch('/api/telemetry').then(r => r.json()); const kl = data.kidlispLogs || {}; const bt = data.boots || {};
document.getElementById('tl-kl-total').textContent = (kl.total || 0).toLocaleString(); document.getElementById('tl-kl-size').textContent = kl.estimatedSizeKB ? kl.estimatedSizeKB.toLocaleString() + ' KB' : '0 KB'; document.getElementById('tl-kl-types').textContent = (kl.byType || []).map(t => `${t._id || 'unknown'}: ${t.count}`).join('\n') || 'none'; document.getElementById('tl-kl-effects').textContent = (kl.byEffect || []).filter(e => e._id).map(e => `${e._id}: ${e.count}`).join('\n') || 'none'; document.getElementById('tl-kl-gpus').textContent = (kl.byGpu || []).filter(g => g._id).map(g => `${g._id}: ${g.count}`).join('\n') || 'none';
document.getElementById('tl-bt-total').textContent = (bt.total || 0).toLocaleString(); document.getElementById('tl-bt-status').textContent = (bt.byStatus || []).map(s => `${s._id || 'unknown'}: ${s.count}`).join('\n') || 'none';
// Recent events const recent = kl.recent || []; if (recent.length === 0) { document.getElementById('tl-recent').textContent = 'no events yet'; } else { const rows = recent.map(e => { const when = e.createdAt ? new Date(e.createdAt).toLocaleString() : '?'; const gpu = e.device?.gpu?.renderer || 'unknown GPU'; const ua = e.device?.mobile ? 'mobile' : 'desktop'; const country = e.server?.country || ''; return `<div style="padding:2px 0;border-bottom:1px solid var(--border)">` + `<b>${e.type || '?'}</b> ${e.effect || ''} — ${gpu} (${ua}) ${country} <span style="color:var(--fg3)">${when}</span>` + (e.gpuStatus?.disabled ? `<br><span style="color:#f80">disabled: ${Object.keys(e.gpuStatus.disabled).filter(k => e.gpuStatus.disabled[k]).join(', ')}</span>` : '') + `</div>`; }).join(''); document.getElementById('tl-recent').innerHTML = rows; } } catch (err) { document.getElementById('tl-recent').textContent = 'error: ' + err.message; }}
async function purgeKidlispLogs() { if (!confirm('Delete all kidlisp-logs? This cannot be undone.')) return; try { const res = await authFetch('/api/telemetry/kidlisp-logs', { method: 'DELETE' }).then(r => r.json()); alert(`Purged ${res.deleted || 0} logs`); telemetryLoaded = false; loadTelemetry(); } catch (err) { alert('Purge failed: ' + err.message); }}
// feedLoaded hoisted near top of script (before setTab call).async function loadFeed() { feedLoaded = true; try { const info = await authFetch('/api/services/feed/info').then(r => r.json()); if (info.status === 'ok') { document.getElementById('feed-status').textContent = 'online'; document.getElementById('feed-status').className = 'v ok'; document.getElementById('feed-version').textContent = info.version || '-'; document.getElementById('feed-deployment').textContent = info.deployment || '-'; document.getElementById('feed-runtime-val').textContent = info.runtime || '-'; document.getElementById('feed-response').textContent = info.responseMs + 'ms'; document.getElementById('feed-response').className = 'v ok'; const eps = info.endpoints; if (eps) { document.getElementById('feed-endpoints').innerHTML = Object.entries(eps) .map(([k, v]) => '<div class="kv"><span class="k">' + esc(k) + '</span><span class="v">' + esc(v) + '</span></div>') .join(''); } } else { document.getElementById('feed-status').textContent = 'unreachable'; document.getElementById('feed-status').className = 'v err'; } } catch (e) { document.getElementById('feed-status').textContent = 'error'; document.getElementById('feed-status').className = 'v err'; }
// Load playlists try { const data = await authFetch('/api/services/feed/playlists').then(r => r.json()); const el = document.getElementById('feed-playlists'); if (data.items && data.items.length > 0) { document.getElementById('feed-playlist-count').textContent = data.items.length; el.innerHTML = '<table class="tbl"><thead><tr><th>title</th><th>slug</th><th>items</th><th>created</th></tr></thead><tbody>' + data.items.map(p => '<tr><td>' + esc(p.title || '-') + '</td><td>' + esc(p.slug || '-') + '</td><td class="r">' + (p.items?.length || 0) + '</td><td>' + (p.created ? new Date(p.created).toLocaleDateString() : '-') + '</td></tr>').join('') + '</tbody></table>'; } else { document.getElementById('feed-playlist-count').textContent = '0'; el.innerHTML = '<span style="color:var(--fg2)">no playlists</span>'; } el.classList.remove('loading'); } catch (e) { document.getElementById('feed-playlists').innerHTML = '<span class="loading">error</span>'; }
// Load channels try { const data = await authFetch('/api/services/feed/channels').then(r => r.json()); const el = document.getElementById('feed-channels'); if (data.items && data.items.length > 0) { document.getElementById('feed-channel-count').textContent = data.items.length; el.innerHTML = '<table class="tbl"><thead><tr><th>title</th><th>slug</th><th>playlists</th><th>created</th></tr></thead><tbody>' + data.items.map(c => '<tr><td>' + esc(c.title || '-') + '</td><td>' + esc(c.slug || '-') + '</td><td class="r">' + (c.playlists?.length || 0) + '</td><td>' + (c.created ? new Date(c.created).toLocaleDateString() : '-') + '</td></tr>').join('') + '</tbody></table>'; } else { document.getElementById('feed-channel-count').textContent = '0'; el.innerHTML = '<span style="color:var(--fg2)">no channels</span>'; } el.classList.remove('loading'); } catch (e) { document.getElementById('feed-channels').innerHTML = '<span class="loading">error</span>'; }}
// --- Database Browser ---let browseState = { collection: null, skip: 0, limit: 25, total: 0, q: '', docs: [], rawDoc: null };
async function loadBrowseCollections() { const el = document.getElementById('browseCollList'); if (!el) return; try { const resp = await authFetch('/api/db/collections').then(r => r.json()); const cols = resp.collections || resp; el.innerHTML = cols.map(c => '<div class="browse-coll" data-coll="' + esc(c.name) + '">' + esc(c.name) + '<span class="browse-coll-count">' + fmt(c.count) + '</span></div>' ).join(''); el.classList.remove('loading'); el.querySelectorAll('.browse-coll').forEach(item => { item.onclick = () => { browseState.collection = item.dataset.coll; browseState.skip = 0; browseState.q = ''; document.getElementById('browseSearch').value = ''; el.querySelectorAll('.browse-coll').forEach(x => x.classList.remove('active')); item.classList.add('active'); loadBrowseDocs(); }; }); } catch (e) { el.innerHTML = '<span class="loading">error</span>'; }}
async function loadBrowseDocs() { const { collection, skip, limit, q } = browseState; if (!collection) return; document.getElementById('browseCollName').textContent = collection; const el = document.getElementById('browseDocs'); el.innerHTML = '<span class="loading">loading...</span>'; try { const params = new URLSearchParams({ skip, limit, sort: '_id', dir: 'desc' }); if (q) params.set('q', q); const data = await authFetch('/api/db/browse/' + encodeURIComponent(collection) + '?' + params).then(r => r.json()); browseState.total = data.total; browseState.docs = data.docs;
if (data.docs.length === 0) { el.innerHTML = '<span style="color:var(--fg2)">no documents' + (q ? ' matching "' + esc(q) + '"' : '') + '</span>'; } else { // Discover columns from first few docs const keys = new Set(); data.docs.slice(0, 10).forEach(d => Object.keys(d).forEach(k => keys.add(k))); const cols = ['_id', ...([...keys].filter(k => k !== '_id').slice(0, 5))];
let html = '<table class="tbl"><thead><tr>'; for (const c of cols) html += '<th>' + esc(c) + '</th>'; html += '</tr></thead><tbody>'; for (const doc of data.docs) { html += '<tr class="browse-row" data-id="' + esc(String(doc._id)) + '">'; for (const c of cols) { let val = doc[c]; if (val === undefined || val === null) val = ''; else if (typeof val === 'object') val = JSON.stringify(val).slice(0, 60); else val = String(val).slice(0, 60); html += '<td>' + esc(val) + '</td>'; } html += '</tr>'; } html += '</tbody></table>'; el.innerHTML = html;
el.querySelectorAll('.browse-row').forEach(row => { row.onclick = () => openBrowseDoc(row.dataset.id); }); }
// Pager const rangeEl = document.getElementById('browseRange'); rangeEl.textContent = (skip + 1) + '-' + Math.min(skip + limit, data.total) + ' of ' + fmt(data.total); document.getElementById('browsePrev').disabled = skip === 0; document.getElementById('browseNext').disabled = skip + limit >= data.total; } catch (e) { el.innerHTML = '<span class="loading">error: ' + esc(e.message) + '</span>'; }}
async function openBrowseDoc(id) { const detail = document.getElementById('browseDocDetail'); const jsonEl = document.getElementById('browseDocJson'); const idEl = document.getElementById('browseDocId'); detail.style.display = 'block'; idEl.textContent = id; jsonEl.textContent = 'loading...'; jsonEl.contentEditable = 'false'; jsonEl.classList.remove('editing'); document.getElementById('browseDocEdit').style.display = ''; document.getElementById('browseDocSave').style.display = 'none'; document.getElementById('browseDocCancel').style.display = 'none';
try { const doc = await authFetch('/api/db/browse/' + encodeURIComponent(browseState.collection) + '/' + encodeURIComponent(id)).then(r => r.json()); browseState.rawDoc = doc; jsonEl.textContent = JSON.stringify(doc, null, 2); } catch (e) { jsonEl.textContent = 'error: ' + e.message; }}
document.getElementById('browseDocEdit').onclick = () => { const jsonEl = document.getElementById('browseDocJson'); jsonEl.contentEditable = 'true'; jsonEl.classList.add('editing'); jsonEl.focus(); document.getElementById('browseDocEdit').style.display = 'none'; document.getElementById('browseDocSave').style.display = ''; document.getElementById('browseDocCancel').style.display = '';};
document.getElementById('browseDocCancel').onclick = () => { const jsonEl = document.getElementById('browseDocJson'); jsonEl.contentEditable = 'false'; jsonEl.classList.remove('editing'); if (browseState.rawDoc) jsonEl.textContent = JSON.stringify(browseState.rawDoc, null, 2); document.getElementById('browseDocEdit').style.display = ''; document.getElementById('browseDocSave').style.display = 'none'; document.getElementById('browseDocCancel').style.display = 'none';};
document.getElementById('browseDocSave').onclick = async () => { const jsonEl = document.getElementById('browseDocJson'); const id = document.getElementById('browseDocId').textContent; let parsed; try { parsed = JSON.parse(jsonEl.textContent); } catch (e) { alert('Invalid JSON: ' + e.message); return; } if (!confirm('Save changes to ' + browseState.collection + '/' + id + '?')) return; try { const resp = await fetch('/api/db/browse/' + encodeURIComponent(browseState.collection) + '/' + encodeURIComponent(id), { method: 'PUT', headers: { 'Content-Type': 'application/json', ...(accessToken ? { Authorization: 'Bearer ' + accessToken } : {}) }, body: JSON.stringify(parsed), }); const data = await resp.json(); if (data.ok) { jsonEl.contentEditable = 'false'; jsonEl.classList.remove('editing'); document.getElementById('browseDocEdit').style.display = ''; document.getElementById('browseDocSave').style.display = 'none'; document.getElementById('browseDocCancel').style.display = 'none'; loadBrowseDocs(); } else { alert('Save failed: ' + (data.error || 'unknown')); } } catch (e) { alert('Save error: ' + e.message); }};
document.getElementById('browseDocDelete').onclick = async () => { const id = document.getElementById('browseDocId').textContent; if (!confirm('Delete ' + browseState.collection + '/' + id + '? This cannot be undone.')) return; try { const resp = await fetch('/api/db/browse/' + encodeURIComponent(browseState.collection) + '/' + encodeURIComponent(id), { method: 'DELETE', headers: accessToken ? { Authorization: 'Bearer ' + accessToken } : {}, }); const data = await resp.json(); if (data.ok) { document.getElementById('browseDocDetail').style.display = 'none'; loadBrowseDocs(); } else { alert('Delete failed: ' + (data.error || 'unknown')); } } catch (e) { alert('Delete error: ' + e.message); }};
document.getElementById('browseDocClose').onclick = () => { document.getElementById('browseDocDetail').style.display = 'none';};
document.getElementById('browsePrev').onclick = () => { browseState.skip = Math.max(0, browseState.skip - browseState.limit); loadBrowseDocs();};
document.getElementById('browseNext').onclick = () => { browseState.skip += browseState.limit; loadBrowseDocs();};
document.getElementById('browseSearchBtn').onclick = () => { browseState.q = document.getElementById('browseSearch').value.trim(); browseState.skip = 0; loadBrowseDocs();};
document.getElementById('browseSearch').addEventListener('keydown', (e) => { if (e.key === 'Enter') { browseState.q = e.target.value.trim(); browseState.skip = 0; loadBrowseDocs(); }});
let browseLoaded = false;
function loadAll() { loadOverview(); loadCollections(); loadStorage(); loadServices(); loadBilling(); loadInstaStatus(); loadTiktokStatus(); // Atlas compare is deferred — only load when that sub-tab is first visited let atlasLoaded = false; const origClick = document.querySelector('.sub-tab-btn[data-subtab="atlas"]'); if (origClick) { const orig = origClick.onclick; origClick.addEventListener('click', () => { if (!atlasLoaded) { atlasLoaded = true; loadCompare(); } }); } fhInit(); // Redraw pie chart on resize window.addEventListener('resize', () => { if (cachedCollections) drawPieChart(cachedCollections, cachedCatMeta); }); setInterval(loadOverview, 30000); setInterval(loadServices, 60000); setInterval(loadBilling, 60000); setInterval(loadInstaStatus, 60000); setInterval(loadTiktokStatus, 60000);}
// --- Lith dashboard ---const LITH_URL = 'https://aesthetic.computer';let lithTimer = null;
function lithSubTab(btn, panelId) { btn.parentElement.querySelectorAll('.sub-tab-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); document.getElementById('lith-errors-panel').style.display = 'none'; document.getElementById('lith-requests-panel').style.display = 'none'; document.getElementById('lith-traffic-panel').style.display = 'none'; document.getElementById(panelId).style.display = 'block';}
function fmtUptime(s) { if (s < 60) return s + 's'; if (s < 3600) return Math.floor(s/60) + 'm ' + (s%60) + 's'; const h = Math.floor(s/3600); return h + 'h ' + Math.floor((s%3600)/60) + 'm';}
function fmtTime(iso) { if (!iso) return '-'; const d = new Date(iso); return d.toLocaleTimeString('en-US', { hour12: false }) + '.' + String(d.getMilliseconds()).padStart(3,'0');}
async function loadLith() { try { const [statsRes, errorsRes, reqsRes, trafficRes] = await Promise.all([ fetch(LITH_URL + '/lith/stats'), fetch(LITH_URL + '/lith/errors?limit=100'), fetch(LITH_URL + '/lith/requests?limit=100'), fetch(LITH_URL + '/lith/traffic').catch(() => ({ json: () => ({ total: 0 }) })), ]); const stats = await statsRes.json(); const errors = await errorsRes.json(); const reqs = await reqsRes.json(); const traffic = await trafficRes.json();
// Overview document.getElementById('lith-uptime').textContent = fmtUptime(stats.uptime); document.getElementById('lith-boot').textContent = fmtTime(stats.boot); document.getElementById('lith-fn-count').textContent = stats.functionsLoaded; document.getElementById('lith-mem').textContent = stats.memory.rss + ' MB / ' + stats.memory.heap + ' MB'; document.getElementById('lith-total-calls').textContent = fmt(stats.totals.calls); document.getElementById('lith-total-errors').textContent = stats.totals.errors; document.getElementById('lith-total-errors').style.color = stats.totals.errors > 0 ? 'var(--err)' : 'var(--ok)';
// Top functions const topEl = document.getElementById('lith-top-fns'); if (stats.functions.length === 0) { topEl.textContent = 'no calls yet'; } else { const maxCalls = stats.functions[0]?.calls || 1; topEl.innerHTML = stats.functions.slice(0, 30).map(f => { const pct = Math.max(2, Math.round(f.calls / maxCalls * 100)); const errStyle = f.errors > 0 ? 'color:var(--err)' : 'color:var(--fg2)'; return '<div style="display:flex;align-items:center;gap:6px;padding:1px 0">' + '<span style="width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(f.name) + '</span>' + '<div style="flex:1;height:8px;background:var(--bg);border-radius:2px;overflow:hidden">' + '<div style="width:' + pct + '%;height:100%;background:var(--accent);border-radius:2px"></div>' + '</div>' + '<span style="width:50px;text-align:right">' + fmt(f.calls) + '</span>' + '<span style="width:40px;text-align:right;' + errStyle + '">' + f.errors + 'e</span>' + '<span style="width:45px;text-align:right;color:var(--fg2)">' + f.avgMs + 'ms</span>' + '</div>'; }).join(''); }
// Errors table const errBody = document.getElementById('lith-errors-body'); if (errors.errors.length === 0) { errBody.innerHTML = '<tr><td colspan="5" style="padding:8px;color:var(--fg2)">no errors</td></tr>'; } else { errBody.innerHTML = errors.errors.map(e => { const errMsg = (e.error || '').substring(0, 120); return '<tr style="border-bottom:1px solid var(--border)">' + '<td style="padding:2px 6px;white-space:nowrap;color:var(--fg2)">' + fmtTime(e.time) + '</td>' + '<td style="padding:2px 6px;color:var(--accent)">' + esc(e.fn) + '</td>' + '<td style="padding:2px 6px;color:var(--err)">' + e.status + '</td>' + '<td style="padding:2px 6px;color:var(--fg2)">' + esc(e.path || '') + '</td>' + '<td style="padding:2px 6px;color:var(--fg2);max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(errMsg) + '</td>' + '</tr>'; }).join(''); }
// Requests table const reqBody = document.getElementById('lith-requests-body'); if (reqs.requests.length === 0) { reqBody.innerHTML = '<tr><td colspan="5" style="padding:8px;color:var(--fg2)">no requests yet</td></tr>'; } else { reqBody.innerHTML = reqs.requests.map(r => { const statusColor = r.status >= 500 ? 'var(--err)' : r.status >= 400 ? 'var(--warn)' : 'var(--fg)'; const msColor = r.ms > 1000 ? 'var(--warn)' : r.ms > 3000 ? 'var(--err)' : 'var(--fg2)'; return '<tr style="border-bottom:1px solid var(--border)">' + '<td style="padding:2px 6px;white-space:nowrap;color:var(--fg2)">' + fmtTime(r.time) + '</td>' + '<td style="padding:2px 6px;color:var(--accent)">' + esc(r.fn) + '</td>' + '<td style="padding:2px 6px;color:' + msColor + '">' + r.ms + '</td>' + '<td style="padding:2px 6px;color:' + statusColor + '">' + r.status + '</td>' + '<td style="padding:2px 6px;color:var(--fg2)">' + esc(r.path || '') + '</td>' + '</tr>'; }).join(''); } // Traffic panels function barRow(label, count, max) { var pct = Math.max(2, Math.round(count / max * 100)); return '<div style="display:flex;align-items:center;gap:4px;padding:1px 0">' + '<span style="width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(label) + '</span>' + '<div style="flex:1;height:6px;background:var(--bg);border-radius:2px;overflow:hidden">' + '<div style="width:' + pct + '%;height:100%;background:var(--accent);border-radius:2px"></div>' + '</div>' + '<span style="width:40px;text-align:right;color:var(--fg2)">' + count + '</span></div>'; } if (traffic.byPath && traffic.byPath.length) { var maxP = traffic.byPath[0][1]; document.getElementById('lith-traffic-paths').innerHTML = traffic.byPath.map(function(p) { return barRow(p[0], p[1], maxP); }).join(''); } else { document.getElementById('lith-traffic-paths').textContent = 'no data'; } if (traffic.byHost && traffic.byHost.length) { var maxH = traffic.byHost[0][1]; document.getElementById('lith-traffic-hosts').innerHTML = traffic.byHost.map(function(h) { return barRow(h[0], h[1], maxH); }).join(''); } else { document.getElementById('lith-traffic-hosts').textContent = 'no data'; } if (traffic.byStatus && traffic.byStatus.length) { document.getElementById('lith-traffic-status').innerHTML = traffic.byStatus.map(function(s) { var color = s[0].startsWith('5') ? 'var(--err)' : s[0].startsWith('4') ? 'var(--warn)' : 'var(--ok)'; return '<div style="padding:1px 0"><span style="color:' + color + ';font-weight:bold">' + s[0] + '</span> <span style="color:var(--fg2)">' + s[1] + '</span></div>'; }).join(''); }
} catch (err) { document.getElementById('lith-uptime').textContent = 'offline'; document.getElementById('lith-uptime').style.color = 'var(--err)'; console.error('lith fetch error:', err); }}
// Start polling lith when tab is selecteddocument.addEventListener('click', e => { if (e.target.matches('[data-tab="8"]')) { loadLith(); if (!lithTimer) lithTimer = setInterval(loadLith, 10000); }});
// ───────────────────────── datomic tab ─────────────────────────let datomicTimer = null;let datomicCurrentType = null;
function dtAuth() { return accessToken ? { 'Authorization': 'Bearer ' + accessToken } : {};}
async function dtFetch(path, opts = {}) { const r = await fetch(path, { ...opts, headers: { ...dtAuth(), ...(opts.headers || {}) } }); if (!r.ok) throw new Error(path + ' → ' + r.status); return r.json();}
function dtSubTab(btn, panelId) { const bar = btn.parentElement; bar.querySelectorAll('.sub-tab-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); const panel = btn.closest('.panel'); panel.querySelectorAll('.sub-panel').forEach(p => { p.style.display = 'none'; }); const target = document.getElementById(panelId); if (target) target.style.display = 'block'; if (panelId === 'dt-txlog-panel') dtLoadTxLog();}
async function loadDatomic() { try { const h = await dtFetch('/api/datomic/health'); document.getElementById('dt-tx').textContent = h.transactor ? 'ok' : 'down'; document.getElementById('dt-tx').className = 'v ' + (h.transactor ? 'ok' : 'err'); document.getElementById('dt-tx-dot').className = 'dot ' + (h.transactor ? 'ok' : 'err'); document.getElementById('dt-schema-ok').textContent = h.schema ? 'installed' : 'missing'; document.getElementById('dt-basisT').textContent = h.basisT != null ? ('t=' + h.basisT) : ''; } catch { document.getElementById('dt-tx').textContent = 'unreachable'; document.getElementById('dt-tx').className = 'v err'; document.getElementById('dt-tx-dot').className = 'dot err'; }
try { const s = await dtFetch('/api/datomic/stats'); document.getElementById('dt-c-kidlisp').textContent = s.entityCounts?.kidlisp ?? 0; document.getElementById('dt-c-users').textContent = s.entityCounts?.users ?? 0; document.getElementById('dt-c-keeps').textContent = s.entityCounts?.keeps ?? 0; document.getElementById('dt-tx-hour').textContent = s.txLastHour ?? 0; } catch {}
try { const { attributes } = await dtFetch('/api/datomic/schema'); document.getElementById('dt-schema-body').innerHTML = attributes.map(a => { const flags = [ a.unique && 'unique', a.indexed && 'indexed', a.isComponent && 'component', a.noHistory && 'noHistory', ].filter(Boolean).join(' '); return '<tr><td style="padding:2px 6px">' + (a.ident || '').toString() + '</td><td style="padding:2px 6px;color:var(--fg2)">' + (a.valueType || '').toString().replace(':db.type/', '') + '</td><td style="padding:2px 6px;color:var(--fg2)">' + (a.cardinality || '').toString().replace(':db.cardinality/', '') + '</td><td style="padding:2px 6px;color:var(--fg2)">' + flags + '</td><td style="padding:2px 6px;color:var(--fg2)">' + (a.doc || '').replace(/\n/g, ' ') + '</td></tr>'; }).join(''); } catch {}
try { const { backups } = await dtFetch('/api/datomic/backups'); const fmt = (n) => (n / 1024 / 1024).toFixed(1) + ' MB'; document.getElementById('dt-backups').innerHTML = backups.length ? backups.map(b => '<div style="padding:1px 0"><span>' + b.name + '</span> <span style="color:var(--fg2)">' + fmt(b.size) + ' · ' + new Date(b.at).toLocaleString() + '</span></div>').join('') : '<div style="color:var(--fg2)">no backups yet</div>'; } catch { document.getElementById('dt-backups').textContent = 'backups dir not present'; }}
async function dtLoadEntities(type) { datomicCurrentType = type; document.getElementById('dt-ent-status').textContent = 'loading...'; try { const data = await dtFetch('/api/datomic/entities/' + encodeURIComponent(type) + '?limit=100'); document.getElementById('dt-ent-status').textContent = data.total + ' total (showing ' + data.items.length + ')'; document.getElementById('dt-entities-list').innerHTML = data.items.map(it => { const label = it['kidlisp/code'] || it['user/sub'] || ('keep #' + it['keep/token-id']) || ('eid ' + it['db/id']); return '<div style="padding:2px 0;cursor:pointer;border-bottom:1px solid var(--border)" ' + 'onclick="dtShowEntity(' + it['db/id'] + ')">' + '<span style="color:var(--accent)">' + label + '</span> ' + '<span style="color:var(--fg2)">eid ' + it['db/id'] + '</span></div>'; }).join(''); } catch (err) { document.getElementById('dt-ent-status').textContent = err.message; }}
let dtCurrentEid = null;async function dtShowEntity(eid) { dtCurrentEid = eid; try { const entity = await dtFetch('/api/datomic/entity/' + eid); document.getElementById('dt-entity-eid').textContent = eid; document.getElementById('dt-entity-body').textContent = JSON.stringify(entity, null, 2); document.getElementById('dt-entity-detail').style.display = 'block'; } catch (err) { alert(err.message); }}
async function dtLoadEntityHistory() { if (dtCurrentEid == null) return; try { const data = await dtFetch('/api/datomic/entity/' + dtCurrentEid + '/history'); document.getElementById('dt-entity-body').textContent = JSON.stringify(data, null, 2); } catch (err) { alert(err.message); }}
async function dtLoadTxLog() { try { const { transactions } = await dtFetch('/api/datomic/tx-log?limit=100'); document.getElementById('dt-txlog-body').innerHTML = transactions.map(t => '<tr><td style="padding:2px 6px">' + t.tx + '</td>' + '<td style="padding:2px 6px;color:var(--fg2)">' + new Date(t.at).toLocaleString() + '</td></tr>' ).join(''); } catch { document.getElementById('dt-txlog-body').innerHTML = '<tr><td colspan="2" style="color:var(--err)">unavailable</td></tr>'; }}
async function dtRunQuery() { const q = document.getElementById('dt-query-text').value; document.getElementById('dt-query-status').textContent = 'running...'; document.getElementById('dt-query-result').textContent = ''; try { const r = await fetch('/api/datomic/query', { method: 'POST', headers: { 'content-type': 'application/json', ...dtAuth() }, body: JSON.stringify({ query: q, args: [] }), }); const data = await r.json(); if (!r.ok) { document.getElementById('dt-query-status').textContent = 'error ' + r.status; document.getElementById('dt-query-result').textContent = JSON.stringify(data, null, 2); return; } document.getElementById('dt-query-status').textContent = data.count + ' rows' + (data.truncated ? ' (truncated)' : ''); document.getElementById('dt-query-result').textContent = JSON.stringify(data.rows, null, 2); } catch (err) { document.getElementById('dt-query-status').textContent = err.message; }}
document.addEventListener('click', e => { if (e.target.matches('[data-tab="9"]')) { loadDatomic(); if (!datomicTimer) datomicTimer = setInterval(loadDatomic, 15000); }});
// ───────────────────────── kidlisp tab (v2: AST-aware) ─────────────────────────let klPieces = [];let klSelectedCode = null;
function klEsc(s) { return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', })[c]);}
async function klLoad(sort) { document.getElementById('kl-status').textContent = 'loading...'; try { const qs = new URLSearchParams({ sort: sort || 'hits', limit: '200' }); const data = await dtFetch('/api/datomic/kidlisp?' + qs.toString()); klPieces = data.recent || []; document.getElementById('kl-list-count').textContent = klPieces.length; document.getElementById('kl-status').textContent = ''; klRenderList(); } catch (err) { document.getElementById('kl-status').textContent = 'error: ' + err.message; }}
function klRenderList() { const filter = (document.getElementById('kl-search').value || '').toLowerCase(); const rows = klPieces.filter((p) => { if (!filter) return true; return ( (p.code || '').toLowerCase().includes(filter) || (p.source || '').toLowerCase().includes(filter) ); }); document.getElementById('kl-list').innerHTML = rows.map((p) => { const hits = p.hits ?? 0; const preview = (p.source || '').substring(0, 30).replace(/\n/g, ' '); const active = p.code === klSelectedCode ? 'background:var(--hover);' : ''; return ( '<div style="padding:2px 4px;cursor:pointer;border-bottom:1px solid var(--border);' + active + '" ' + 'onclick="klShow(\'' + klEsc(p.code) + '\')">' + '<span style="color:var(--accent)">$' + klEsc(p.code) + '</span> ' + '<span style="color:var(--fg2)">·</span> ' + '<span style="color:var(--fg2)">' + hits + '</span> ' + '<div style="color:var(--fg2);font-size:10px;margin-left:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + klEsc(preview) + '</div></div>' ); }).join('');}
document.addEventListener('input', (e) => { if (e.target.id === 'kl-search') klRenderList();});
async function klShow(code) { klSelectedCode = code; klRenderList(); document.getElementById('kl-detail-code').textContent = '$' + code; document.getElementById('kl-source').textContent = 'loading...'; document.getElementById('kl-tree').textContent = 'loading...'; document.getElementById('kl-struct-result').textContent = 'click an op in the tree above'; document.getElementById('kl-struct-count').textContent = ''; document.getElementById('kl-struct-label').textContent = 'pieces using X';
try { const piece = await dtFetch('/api/datomic/kidlisp/' + encodeURIComponent(code)); document.getElementById('kl-source').textContent = piece.source || '(empty)'; document.getElementById('kl-detail-meta').textContent = (piece.user ? piece.user.substring(0, 20) + ' · ' : '') + (piece.hits ?? 0) + ' hits'; const meta = []; if (piece.when) meta.push('created ' + new Date(piece.when).toLocaleDateString()); if (piece.atproto?.rkey) meta.push('atproto: ' + piece.atproto.rkey); if (piece.tezos?.minted) meta.push('tezos minted #' + piece.tezos.tokenId); if (piece.keeps?.length) meta.push(piece.keeps.length + ' keep(s)'); document.getElementById('kl-meta').textContent = meta.join(' · '); } catch (err) { document.getElementById('kl-source').textContent = 'error: ' + err.message; }
try { const ast = await dtFetch('/api/datomic/kidlisp/' + encodeURIComponent(code) + '/ast'); document.getElementById('kl-tree').innerHTML = klRenderTree(ast.nodes || []); } catch (err) { document.getElementById('kl-tree').textContent = 'no AST indexed yet — run backfill'; }}
function klRenderTree(nodes) { if (!nodes.length) return '<span style="color:var(--fg2)">(empty)</span>'; const byId = new Map(nodes.map((n) => [n.id, n])); const kids = new Map(); for (const n of nodes) { const p = n.parent ?? -1; if (!kids.has(p)) kids.set(p, []); kids.get(p).push(n); } for (const arr of kids.values()) arr.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
function walk(id, indent) { const n = byId.get(id); const pad = ' '.repeat(indent); let label; if (n.kind === ':call' || n.kind === 'call') { const op = n.op || '?'; label = pad + '<span class="kl-op" data-op="' + klEsc(op) + '" ' + 'onclick="klStructuralSearch(\'' + klEsc(op) + '\')" ' + 'style="color:var(--accent);cursor:pointer;text-decoration:underline dotted">(' + klEsc(op) + ')</span>'; } else { const kindLabel = String(n.kind || '').replace(/^:/, ''); label = pad + '<span style="color:var(--fg2)">' + kindLabel + ':</span><span>' + klEsc(n.literal ?? '') + '</span>'; } const out = [label]; for (const c of kids.get(id) ?? []) out.push(walk(c.id, indent + 1)); return out.join('<br>'); } return (kids.get(-1) ?? []).map((n) => walk(n.id, 0)).join('<br>');}
async function klStructuralSearch(op) { document.getElementById('kl-struct-label').textContent = "pieces using '" + op + "'"; document.getElementById('kl-struct-count').textContent = 'searching...'; document.getElementById('kl-struct-result').textContent = ''; try { const qs = new URLSearchParams({ op, limit: '100' }); const data = await dtFetch('/api/datomic/structural/pieces-using?' + qs.toString()); document.getElementById('kl-struct-count').textContent = data.count + ' pieces'; document.getElementById('kl-struct-result').innerHTML = (data.codes || []).map((c) => '<span style="display:inline-block;margin:1px 4px 1px 0;padding:1px 4px;background:var(--bg);border:1px solid var(--border);cursor:pointer;color:var(--accent)" ' + 'onclick="klShow(\'' + klEsc(c) + '\')">$' + klEsc(c) + '</span>' ).join('') || '(none)'; } catch (err) { document.getElementById('kl-struct-count').textContent = 'error'; document.getElementById('kl-struct-result').textContent = err.message; }}
document.addEventListener('click', e => { if (e.target.matches('[data-tab="10"]')) { if (!klPieces.length) klLoad('hits'); }});
initAuth();</script></body></html>