<!doctype html><htmllang="en"><head><metacharset="utf-8" /><metaname="viewport"content="width=device-width, initial-scale=1" /><title>lytics โ cyberstates</title><linkrel="preconnect"href="https://fonts.googleapis.com" /><linkrel="preconnect"href="https://fonts.gstatic.com"crossorigin /><linkhref="https://fonts.googleapis.com/css2?family=Play:wght@400;700&display=swap"rel="stylesheet" /><style>:root{--cyber-black:#000000;--cyber-dark:#0a0a0a;--cyber-green:#00ff41;--cyber-cyan:#00e5ff;--cyber-magenta:#ff00ff;--cyber-yellow:#ffd700;--cyber-orange:#ff6600;--cyber-red:#ff0040;--cyber-dim:#1a1a1a;--cyber-muted:#333333;--cyber-text:#e0e0e0;}*{margin:0;padding:0;box-sizing:border-box;}html{overflow-y:scroll;scrollbar-gutter:stable;}body{font-family:"Play",sans-serif;background:var(--cyber-black);color:var(--cyber-text);min-height:100vh;-webkit-font-smoothing:antialiased;}::-webkit-scrollbar{width:6px;}::-webkit-scrollbar-track{background:var(--cyber-black);}::-webkit-scrollbar-thumb{background:var(--cyber-muted);border-radius:3px;}::-webkit-scrollbar-thumb:hover{background:var(--cyber-green);}.page-frame{max-width:1100px;margin:0auto;padding:20px24px80px;}@media(max-width:640px){.page-frame{padding:12px10px72px;}}/* header */.header-row1{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:8px;flex-wrap:wrap;}.logo{font-size:clamp(22px, 4vw, 36px);
font-weight:700;line-height:1.1;color:#fff;text-decoration:none;}.logospan.mark{color:var(--cyber-green);}.logo-suffix{min-height:14px;line-height:14px;font-size:11px;letter-spacing:3px;text-transform:uppercase;color:rgba(0,255,65,0.55);margin-top:4px;}.nav-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center;}.nav-btn{font-family:"Play",sans-serif;font-size:12px;font-weight:700;letter-spacing:2px;color:#000;text-decoration:none;background:var(--btn-c);border:1pxsolidvar(--btn-c);padding:8px14px;border-radius:2px;box-shadow:0014pxcolor-mix(in srgb, var(--btn-c)35%, transparent);
transition:all0.15s;white-space:nowrap;}.nav-btn:hover{filter:brightness(1.1);}.nav-states{--btn-c:var(--cyber-green);}.nav-tokens{--btn-c:var(--cyber-yellow);}.nav-lytics{--btn-c:var(--cyber-cyan);}.nav-btn.nav-here{background:transparent;color:var(--btn-c);box-shadow:none;}.meta-line{font-size:12px;letter-spacing:1px;color:#555;margin:8px020px;line-height:1.6;}.meta-linetime{color:#777;}/* KPI cards โ match .stat-grid / .stat-card */.section-label{font-size:11px;letter-spacing:3px;text-transform:uppercase;color:#555;margin:28px012px;}.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;}.stat-card{background:#050505;border:1pxsolid#1a1a1a;border-radius:2px;padding:14px16px;min-width:0;}.stat-label{font-size:10px;letter-spacing:2px;text-transform:uppercase;color:#555;margin-bottom:8px;}.stat-value{font-size:clamp(18px, 3vw, 26px);
font-weight:700;color:var(--cyber-green);font-variant-numeric:tabular-nums;line-height:1.15;}.stat-value.cyan{color:var(--cyber-cyan);}.stat-value.yellow{color:var(--cyber-yellow);}.stat-value.orange{color:var(--cyber-orange);}.stat-value.magenta{color:var(--cyber-magenta);}.stat-hint{font-size:11px;color:#444;margin-top:6px;letter-spacing:0.5px;}.hint{font-size:12px;color:#3a3a3a;margin:14px00;letter-spacing:0.5px;line-height:1.5;}/* window chips */.window-chips{display:flex;gap:6px;flex-wrap:wrap;margin:008px;}.chip{font-family:"Play",sans-serif;font-size:11px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;padding:6px12px;border:1pxsolid#222;border-radius:2px;background:transparent;color:#666;cursor:pointer;}.chip:hover{border-color:var(--cyber-green);color:var(--cyber-green);}.chip.on{background:var(--cyber-green);border-color:var(--cyber-green);color:#000;}/* pulse โ dual metric compare */.pulse-wrap{border:1pxsolid#111;border-radius:2px;background:#050505;padding:12px12px8px;position:relative;}.pulse-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px;}.pulse-metric-btn{font-family:"Play",sans-serif;font-size:11px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;padding:6px12px;border-radius:2px;border:1pxsolid;background:transparent;cursor:pointer;display:inline-flex;align-items:center;gap:6px;}.pulse-metric-btn.swatch{width:8px;height:8px;border-radius:1px;flex:00auto;}.pulse-metric-btn.a{color:var(--cyber-green);border-color:rgba(0,255,65,0.45);}.pulse-metric-btn.a.swatch{background:var(--cyber-green);}.pulse-metric-btn.a:hover,
.pulse-metric-btn.a.open{background:rgba(0,255,65,0.1);border-color:var(--cyber-green);}.pulse-metric-btn.b{color:var(--cyber-magenta);border-color:rgba(255,0,255,0.45);}.pulse-metric-btn.b.swatch{background:var(--cyber-magenta);}.pulse-metric-btn.b:hover,
.pulse-metric-btn.b.open{background:rgba(255,0,255,0.1);border-color:var(--cyber-magenta);}.pulse-vs{font-size:10px;letter-spacing:2px;color:#444;}.pulse-menu{position:absolute;z-index:20;top:48px;min-width:180px;background:#0a0a0a;border:1pxsolid#222;border-radius:2px;box-shadow:08px28pxrgba(0,0,0,0.7);padding:4px;display:none;}.pulse-menu.on{display:block;}.pulse-menu.slot-a{left:12px;}.pulse-menu.slot-b{left:160px;}.pulse-menubutton{display:block;width:100%;text-align:left;font-family:"Play",sans-serif;font-size:11px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;padding:8px10px;border:none;border-radius:2px;background:transparent;color:#888;cursor:pointer;}.pulse-menubutton:hover{background:#111;color:#eee;}.pulse-menubutton.on{color:var(--cyber-green);background:rgba(0,255,65,0.08);}.pulse-menu.slot-bbutton.on{color:var(--cyber-magenta);background:rgba(255,0,255,0.08);}.pulse-menubutton:disabled{color:#333;cursor:default;}.pulse-chart{width:100%;height:160px;display:block;}.pulse-chart.grid{stroke:#151515;stroke-width:1;}.pulse-chart.axis-lab{fill:#444;font-size:9px;font-family:"Play",sans-serif;letter-spacing:0.5px;}.pulse-chart.x-lab{fill:#555;font-size:9px;font-family:"Play",sans-serif;}.pulse-chart.line-a{fill:none;stroke:var(--cyber-green);stroke-width:2;stroke-linejoin:round;stroke-linecap:round;}.pulse-chart.line-b{fill:none;stroke:var(--cyber-magenta);stroke-width:2;stroke-linejoin:round;stroke-linecap:round;}.pulse-chart.area-a{fill:rgba(0,255,65,0.08);stroke:none;}.pulse-chart.area-b{fill:rgba(255,0,255,0.07);stroke:none;}.pulse-chart.dot-a{fill:var(--cyber-green);}.pulse-chart.dot-b{fill:var(--cyber-magenta);}.pulse-chart.hover-line{stroke:#333;stroke-width:1;stroke-dasharray:23;}.pulse-tip{position:absolute;pointer-events:none;background:#0a0a0a;border:1pxsolid#222;border-radius:2px;padding:8px10px;font-size:11px;letter-spacing:0.5px;color:#ccc;display:none;z-index:15;min-width:120px;box-shadow:06px20pxrgba(0,0,0,0.6);}.pulse-tip.on{display:block;}.pulse-tip.t{color:#666;margin-bottom:4px;font-size:10px;}.pulse-tip.row-a{color:var(--cyber-green);}.pulse-tip.row-b{color:var(--cyber-magenta);}.pulse-empty{color:#444;font-size:13px;padding:40px24px;letter-spacing:1px;text-align:center;}/* live strip */.live-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:10px;}.live-pulse-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--cyber-red);box-shadow:0010pxvar(--cyber-red);margin-right:8px;animation:live-blink1.4sease-in-outinfinite;}@keyframeslive-blink{0%,
100%{opacity:1;}50%{opacity:0.35;}}.live-stats{font-size:12px;letter-spacing:1px;color:#666;}.live-statsb{color:var(--cyber-green);font-weight:700;}.live-strip{display:flex;gap:8px;overflow-x:auto;padding-bottom:4px;min-height:72px;}.live-card{flex:00auto;min-width:160px;max-width:220px;background:#050505;border:1pxsolid#1a1a1a;border-radius:2px;padding:10px12px;}.live-card.ago{font-size:10px;letter-spacing:1px;color:var(--cyber-red);margin-bottom:4px;}.live-card.path{font-size:13px;color:#eee;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:4px;}.live-card.meta{font-size:11px;color:#555;letter-spacing:0.5px;}.live-empty{color:#444;font-size:13px;padding:20px8px;letter-spacing:1px;}/* frequency histogram */.freq-grid{display:grid;grid-template-columns:1fr1fr;gap:16px;}@media(max-width:800px){.freq-grid{grid-template-columns:1fr;}}.freq-panel{border:1pxsolid#111;border-radius:2px;background:#050505;padding:12px14px;}.freq-panel.stat-label{margin-bottom:10px;}.freq-kpis{display:flex;gap:14px;flex-wrap:wrap;margin-bottom:12px;font-size:11px;letter-spacing:1px;color:#555;}.freq-kpisb{color:var(--cyber-cyan);font-weight:700;}.freq-row{display:grid;grid-template-columns:48px1fr36px;gap:8px;align-items:center;margin-bottom:6px;}.freq-row.lab{font-size:11px;color:#888;font-variant-numeric:tabular-nums;}.freq-row.bar-track{height:12px;background:#0a0a0a;border:1pxsolid#151515;border-radius:1px;overflow:hidden;}.freq-row.bar{height:100%;background:linear-gradient(90deg,rgba(0,255,65,0.35),
var(--cyber-green));
min-width:0;}.freq-row.n{font-size:11px;color:#aaa;text-align:right;font-variant-numeric:tabular-nums;}/* path flow */.flow-list{border:1pxsolid#111;border-radius:2px;background:#050505;overflow:hidden;}.flow-row{display:grid;grid-template-columns:1frauto1frauto;gap:10px;align-items:center;padding:10px14px;border-bottom:1pxsolid#111;}.flow-row:last-child{border-bottom:none;}.flow-row:hover{background:#080808;}.flow-from,
.flow-to{font-size:13px;color:#ccc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.flow-from{text-align:right;color:var(--cyber-cyan);}.flow-to{color:var(--cyber-green);}.flow-arrow{color:#444;font-size:12px;letter-spacing:1px;}.flow-meta{font-size:11px;color:#666;font-variant-numeric:tabular-nums;white-space:nowrap;}.flow-bar{grid-column:1 / -1;height:3px;background:#111;border-radius:1px;overflow:hidden;margin-top:-4px;}.flow-bar>i{display:block;height:100%;background:var(--cyber-magenta);opacity:0.7;}.flow-empty{color:#444;font-size:13px;padding:24px;letter-spacing:1px;}/* funnel builder */.funnel-builder{border:1pxsolid#111;border-radius:2px;background:#050505;padding:14px16px;}.funnel-steps{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;min-height:36px;}.funnel-step-chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;letter-spacing:0.5px;padding:6px8px6px12px;border:1pxsolidrgba(0,255,65,0.35);border-radius:2px;background:rgba(0,255,65,0.06);color:var(--cyber-green);max-width:220px;}.funnel-step-chip.ord{color:#555;font-size:10px;min-width:1.2em;}.funnel-step-chip.lab{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.funnel-step-chipbutton{border:none;background:transparent;color:#666;cursor:pointer;font-size:14px;line-height:1;padding:02px;font-family:"Play",sans-serif;}.funnel-step-chipbutton:hover{color:var(--cyber-red);}.funnel-arrow{color:#333;font-size:12px;}.funnel-add-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;}.funnel-add-rowinput{flex:11160px;min-width:120px;font-family:"Play",sans-serif;font-size:13px;padding:8px12px;border:1pxsolid#222;border-radius:2px;background:#0a0a0a;color:#eee;}.funnel-add-rowinput:focus{outline:none;border-color:var(--cyber-green);}.funnel-add-rowbutton,
.funnel-presetsbutton{font-family:"Play",sans-serif;font-size:11px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;padding:8px12px;border:1pxsolid#222;border-radius:2px;background:transparent;color:#888;cursor:pointer;}.funnel-add-rowbutton:hover,
.funnel-presetsbutton:hover{border-color:var(--cyber-green);color:var(--cyber-green);}.funnel-add-rowbutton.primary{background:var(--cyber-green);border-color:var(--cyber-green);color:#000;}.funnel-presets{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;}.funnel-presets.tag{font-size:11px;letter-spacing:0.5px;padding:4px10px;border:1pxsolid#1a1a1a;border-radius:2px;background:#0a0a0a;color:#777;cursor:pointer;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.funnel-presets.tag:hover{border-color:var(--cyber-cyan);color:var(--cyber-cyan);}.funnel-presets.tag.in{border-color:rgba(0,255,65,0.4);color:#555;cursor:default;}.funnel-chart{display:flex;flex-direction:column;gap:8px;}.funnel-row{display:grid;grid-template-columns:minmax(80px,140px)1frauto;gap:10px;align-items:center;}.funnel-row.step-lab{font-size:12px;color:#aaa;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.funnel-row.bar-wrap{height:28px;background:#0a0a0a;border:1pxsolid#151515;border-radius:2px;overflow:hidden;position:relative;}.funnel-row.bar{height:100%;background:linear-gradient(90deg,rgba(0,229,255,0.25),
var(--cyber-cyan));
min-width:0;transition:width0.25sease;}.funnel-row.nums{font-size:12px;font-variant-numeric:tabular-nums;color:#ccc;white-space:nowrap;min-width:110px;text-align:right;}.funnel-row.nums.drop{color:#555;margin-left:6px;}.funnel-row.nums.drop.bad{color:var(--cyber-orange);}.funnel-empty{color:#444;font-size:13px;padding:20px8px;letter-spacing:1px;}.funnel-summary{margin-top:12px;font-size:12px;letter-spacing:1px;color:#666;}.funnel-summaryb{color:var(--cyber-magenta);font-weight:700;}@media(max-width:640px){.funnel-row{grid-template-columns:1fr;gap:4px;}.funnel-row.step-lab{text-align:left;}.funnel-row.nums{text-align:left;min-width:0;}}.split-2{display:grid;grid-template-columns:1fr1fr;gap:16px;}@media(max-width:800px){.split-2{grid-template-columns:1fr;}}/* retention heat cells */.ret-cell{text-align:right;font-variant-numeric:tabular-nums;font-weight:700;font-size:12px;letter-spacing:0.5px;border-radius:2px;padding:6px8px!important;min-width:52px;}.ret-cell.ret-n{display:block;font-size:10px;font-weight:400;color:#555;margin-top:2px;}.ret-cell.zero{color:#333;font-weight:400;}.ret-cell.w0{color:var(--cyber-green);}/* tables โ match .cyber-table */.table-wrap{overflow-x:auto;margin-top:4px;border:1pxsolid#111;border-radius:2px;}.cyber-table{width:100%;border-collapse:separate;border-spacing:0;font-size:13px;}.cyber-tableth{background:#050505;border-bottom:1pxsolidvar(--cyber-muted);padding:12px16px;text-align:left;font-size:11px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:var(--cyber-green);white-space:nowrap;user-select:none;}.cyber-tableth.num,
.cyber-tabletd.num{text-align:right;font-variant-numeric:tabular-nums;}.cyber-tabletd{border-bottom:1pxsolid#111;padding:10px16px;color:#ccc;}.cyber-tabletbodytr:hovertd{background:#080808;color:#eee;}.cyber-tabletbodytr:last-childtd{border-bottom:none;}.cyber-tablea{color:var(--cyber-cyan);text-decoration:none;}.cyber-tablea:hover{color:var(--cyber-green);}/* path segment tree */.path-cell{display:flex;align-items:center;gap:8px;min-width:0;}.path-tog{flex:00auto;width:18px;height:18px;padding:0;border:1pxsolid#333;border-radius:2px;background:#0a0a0a;color:var(--cyber-green);font-size:11px;font-family:"Play",sans-serif;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;}.path-tog:hover{border-color:var(--cyber-green);background:#111;}.path-tog.leaf{visibility:hidden;pointer-events:none;}.path-lab{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.path-lab.group{color:#fff;font-weight:700;}.path-lab.child{color:#aaa;}.path-count{flex:00auto;font-size:10px;color:#555;letter-spacing:0.5px;}tr.path-childtd{background:#050505;}tr.path-childtd:first-child{padding-left:28px;}tr.path-group{cursor:pointer;}tr.path-group:hovertd{background:#0c0c0c;}.empty{color:#444;font-size:13px;padding:16px;letter-spacing:1px;}.dock-credit{margin-top:48px;font-size:12px;color:#333;letter-spacing:1px;}.dock-credita{color:var(--cyber-green);text-decoration:none;opacity:0.75;}.dock-credita:hover{opacity:1;}@media(max-width:640px){.stat-grid{grid-template-columns:repeat(2,minmax(0,1fr));}.stat-value{font-size:18px;}.cyber-tableth,
.cyber-tabletd{padding:8px10px;font-size:12px;}.nav-btn{padding:6px10px;font-size:11px;letter-spacing:1px;}}</style></head><body><divclass="page-frame"><divclass="header-row1"><div><aclass="logo"href="/lytics/"><spanclass="mark">๐</span> LYTICS</a><divclass="logo-suffix"id="logo-suffix">
cyberstates ยท last 7 days
</div></div><divclass="nav-row"><aclass="nav-btn nav-states"href="/">STATES</a><aclass="nav-btn nav-tokens"href="/tokens">TOKENS</a><aclass="nav-btn nav-lytics nav-here"href="/lytics/">LYTICS</a></div></div><divclass="window-chips"id="chips"></div><divclass="window-chips"id="aud-chips"></div><divclass="meta-line"id="meta">loading windowโฆ</div><divclass="section-label"><spanclass="live-pulse-dot"aria-hidden="true"></span>live
</div><divclass="live-head"><divclass="live-stats"id="live-stats">loadingโฆ</div></div><divclass="live-strip"id="live"></div><pclass="hint">
last 15 minutes ยท wall-clock presence ยท refreshes with the dash
</p><divclass="section-label">overview</div><divclass="stat-grid"id="ov"></div><divclass="stat-grid"id="ov-aud"style="margin-top:12px"></div><divclass="freq-panel"style="margin-top:12px"><divclass="stat-label">scroll reach</div><divclass="freq-kpis"id="scroll-kpis"></div><divid="scroll-hist"></div></div><pclass="hint">
core: neurons โ visits โ views ยท derivatives: depth =
views/visit ยท dwell = attention/visit ยท att/neuron = total
looking per unique key ยท scroll = max % reached per neuronรpage
(from attention sensor) ยท NEW = first-seen in window ยท RETURNING
= seen before window
</p><divclass="section-label">frequency</div><divclass="freq-grid"><divclass="freq-panel"><divclass="stat-label">visits per neuron</div><divclass="freq-kpis"id="freq-visits-kpis"></div><divid="freq-visits"></div></div><divclass="freq-panel"><divclass="stat-label">days active per neuron</div><divclass="freq-kpis"id="freq-days-kpis"></div><divid="freq-days"></div></div></div><divclass="table-wrap"style="margin-top:12px"><tableclass="cyber-table"id="freq-top"></table></div><pclass="hint">
shape of engagement ยท flat left = healthy long tail ยท spike on
32+ = whales / bots ยท top list is heaviest keys (truncated)
</p><divclass="section-label">retention</div><divclass="stat-grid"id="ret-kpis"></div><divclass="table-wrap"style="margin-top:12px"><tableclass="cyber-table"id="retention"></table></div><pclass="hint">
cohort = week of first-seen ยท W0 = that week ยท Wn = still active
n weeks later ยท % of cohort size ยท returns = first seen in
window, came back within 7d
</p><divclass="section-label">pulse</div><divclass="pulse-wrap"id="pulse-wrap"><divclass="pulse-head"><buttontype="button"class="pulse-metric-btn a"id="pulse-ma"aria-haspopup="listbox"><spanclass="swatch"></span><spanclass="lab">NEURONS</span></button><spanclass="pulse-vs">vs</span><buttontype="button"class="pulse-metric-btn b"id="pulse-mb"aria-haspopup="listbox"><spanclass="swatch"></span><spanclass="lab">ATTENTION</span></button></div><divclass="pulse-menu"id="pulse-menu"role="listbox"></div><divid="pulse"></div><divclass="pulse-tip"id="pulse-tip"></div></div><pclass="hint">
pick any two metrics ยท left axis green ยท right axis magenta ยท
each scaled independently ยท day buckets (hour in 24h)
</p><divclass="section-label">landings ยท exits</div><divclass="split-2"><div><divclass="stat-label"style="margin-bottom:8px">
entry pages
</div><divclass="table-wrap"><tableclass="cyber-table"id="entries"></table></div></div><div><divclass="stat-label"style="margin-bottom:8px">
exit pages
</div><divclass="table-wrap"><tableclass="cyber-table"id="exits"></table></div></div></div><pclass="hint">
neurons = unique visitors who entered / left here ยท visits =
passage count ยท grouped by first path segment (click to expand)
</p><divclass="section-label">path flow</div><divclass="flow-list"id="pathflow"></div><pclass="hint">
consecutive pageviews inside a visit ยท from โ to ยท thickness =
neurons who took the edge ยท answers "after X, where?"
</p><divclass="section-label">funnel</div><divclass="funnel-builder"><divclass="funnel-steps"id="funnel-steps"></div><divclass="funnel-add-row"><inputid="funnel-input"type="text"placeholder="/path or paste pathname"autocomplete="off"spellcheck="false" /><buttontype="button"class="primary"id="funnel-add">
add step
</button><buttontype="button"id="funnel-clear">clear</button></div><divclass="funnel-presets"id="funnel-presets"></div><divclass="funnel-chart"id="funnel-chart"></div><divclass="funnel-summary"id="funnel-summary"></div></div><pclass="hint">
ordered path steps ยท neuron must hit each step later than the
previous (any visit, full window) ยท v1 = page paths only ยท
convert/tx goals come next
</p><divclass="section-label">top pages</div><divclass="table-wrap"><tableclass="cyber-table"id="particles"></table></div><pclass="hint">
grouped by first path segment ยท click a folder to expand
children ยท segment neurons/visits are sums of child paths (may
overlap)
</p><divclass="section-label">geo</div><divclass="table-wrap"><tableclass="cyber-table"id="countries"></table></div><divclass="section-label">devices</div><divclass="table-wrap"><tableclass="cyber-table"id="devices"></table></div><divclass="section-label">visits by source</div><divclass="table-wrap"><tableclass="cyber-table"id="sources"></table></div><divclass="section-label">visits by channel</div><divclass="table-wrap"><tableclass="cyber-table"id="channels"></table></div><divclass="dock-credit">
๐ด a <ahref="https://cyberia.to">cyberia</a> project ยท
<ahref="https://cyberstates.net">cyberstates.net</a></div></div><script>constroot=location.pathname.replace(/\/?$/,"/");constWINDOWS=[{id:"24h",label:"24h",ms:24*3600*1000,bucket:"hour",},{id:"7d",label:"7d",ms:7*24*3600*1000,bucket:"day",},{id:"30d",label:"30d",ms:30*24*3600*1000,bucket:"day",},];constAUDIENCES=[{id:"all",label:"ALL"},{id:"new",label:"NEW"},{id:"returning",label:"RETURNING"},];letwindowId=newURLSearchParams(location.search).get("w")||"7d";if(!WINDOWS.some((w)=>w.id===windowId))windowId="7d";letaudienceId=newURLSearchParams(location.search).get("a")||"all";if(!AUDIENCES.some((a)=>a.id===audienceId))audienceId="all";functionwindowSpec(){returnWINDOWS.find((w)=>w.id===windowId)||WINDOWS[1];}functionaudienceSpec(){return(AUDIENCES.find((a)=>a.id===audienceId)||AUDIENCES[0]);}functionqs(){constw=windowSpec();constto=Date.now()+1;constfrom=Date.now()-w.ms;return`?from=${from}&to=${to}&limit=50&audience=${audienceId}`;}constget=(name,extra="")=>{constbase=qs();constjoin=extra.startsWith("?")?"&"+extra.slice(1):extra;returnfetch(`${root}api/report/${name}${base}${join}`).then((r)=>r.json(),);};functionsetQueryState(){constu=newURL(location.href);u.searchParams.set("w",windowId);u.searchParams.set("a",audienceId);history.replaceState(null,"",u);}constfmt=(ms)=>{if(ms==null||ms===0)return"0s";if(ms>=60000)return`${(ms/60000).toFixed(1)}m`;if(ms>=1000)return`${(ms/1000).toFixed(1)}s`;return`${ms}ms`;};constfmtDepth=(milli)=>milli==null?"โ":(milli/1000).toFixed(2);constfmtDay=(ms)=>{constd=newDate(ms);return`${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2,"0")}`;};constfmtHour=(ms)=>{constd=newDate(ms);return`${String(d.getUTCHours()).padStart(2,"0")}h`;};constfmtStamp=(ms)=>newDate(ms).toISOString().replace("T","").replace(/\.\d{3}Z$/," UTC");constshortPath=(p)=>{if(!p)return"โ";if(p.length<=42)returnp;returnp.slice(0,20)+"โฆ"+p.slice(-18);};functiontable(el,headers,data,cols){if(!data||!data.length){el.innerHTML=`<tbody><tr><td class="empty" colspan="${headers.length}">โ no data in window</td></tr></tbody>`;return;}el.innerHTML=`<thead><tr>${headers.map((h,i)=>`<th class="${cols[i][1] || ""}">${h}</th>`,).join("")}</tr></thead><tbody>`+data.map((r)=>`<tr>${cols.map(([c,cls])=>`<td class="${cls || ""}">${r[c] ?? ""}</td>`,).join("")}</tr>`,).join("")+`</tbody>`;}// path segment rollup: first segment = folder, rest = children.
// expanded state survives window chip changes within the session.
constpathExpanded={entries:newSet(),exits:newSet(),particles:newSet(),};functionpathParts(pathname){if(!pathname||pathname==="/")return{group:"/",rest:null};constsegs=pathname.replace(/^\//,"").split("/").filter(Boolean);if(segs.length===0)return{group:"/",rest:null};if(segs.length===1)return{group:"/"+segs[0],rest:null};return{group:"/"+segs[0],rest:segs.slice(1).join("/"),};}/** @param rows raw API rows with .pathname
* @param sumKeys numeric keys to sum onto groups
*/functiongroupByPathSegment(rows,sumKeys){constmap=newMap();for(constrowofrows||[]){constpath=row.pathname||"/";const{group,rest}=pathParts(path);letg=map.get(group);if(!g){g={group,metrics:Object.fromEntries(sumKeys.map((k)=>[k,0]),),children:[],leaf:null,// single-segment path is the group itself
};map.set(group,g);}for(constkofsumKeys){g.metrics[k]=(g.metrics[k]||0)+(Number(row[k])||0);}if(rest==null){// exact group path (e.g. /tokens or /)
g.leaf=row;}else{g.children.push({label:rest,pathname:path,row,});}}// sort groups by first sum key desc (neurons preferred)
constprimary=sumKeys[0];constgroups=[...map.values()].sort((a,b)=>(b.metrics[primary]||0)-(a.metrics[primary]||0),);for(constgofgroups){g.children.sort((a,b)=>(Number(b.row[primary])||0)-(Number(a.row[primary])||0),);}returngroups;}/**
* render expandable path-segment table.
* formatCells(row|metrics, isGroup) โ array of cell HTML for metric cols
* pathCol is always first.
*/functionrenderPathTable(el,expandKey,headers,groups,formatCells,){if(!groups||!groups.length){el.innerHTML=`<tbody><tr><td class="empty" colspan="${headers.length}">โ no data in window</td></tr></tbody>`;return;}constexpanded=pathExpanded[expandKey];consthead=`<thead><tr>${headers.map((h,i)=>`<th class="${i === 0 ? "" : "num"}">${h}</th>`,).join("")}</tr></thead>`;constpathCell=(label,{isGroup,open,nChildren,title},)=>{
const tog = isGroup
? `<button type="button"class="path-tog"aria-expanded="${open}" data-group="${label}"title="${open ? "collapse" : "expand"}">${open ? "โ" : "+"}</button>`:`<span class="path-tog leaf" aria-hidden="true"></span>`;
const count =
isGroup && nChildren
? `<span class="path-count">${nChildren}</span>`
: "";
return `<div class="path-cell">${tog}? "group" : "child"}" title="${title || label}">${label}</span>${count}</div>`;
};
let body = "";
for (const g of groups) {
const hasKids = g.children.length > 0;
const open = hasKids && expanded.has(g.group);
//groupwithonlyaleafandnokids โ plainrowconstisFolder=hasKids;
constcells=formatCells(g.metrics,true,g);
body+=`<tr class="${isFolder ? "path-group" : ""}" ${isFolder ? `data-group="${g.group}"` : ""}>
<td>${pathCell(g.group,{isGroup:isFolder,open,nChildren:g.children.length,title:g.group,})}</td>
${cells.map((c) => `
${c}</td>`).join("")}</tr>`;
if (open) {
for (const ch of g.children) {
const chCells = formatCells(ch.row, false, g);
body += `<tr class="path-child">
<td>${pathCell(ch.label, {
isGroup: false,
open: false,
nChildren: 0,
title: ch.pathname,
})}</td>${chCells.map((c) => `
${c}</td>`).join("")}
</tr>`;
}
}
}
el.innerHTML = head + `<tbody>${body}</tbody>`;
//togglehandlersel.querySelectorAll("tr.path-group").forEach((tr)=>{consttoggle=()=>{constkey=tr.dataset.group;if(expanded.has(key))expanded.delete(key);elseexpanded.add(key);renderPathTable(el,expandKey,headers,groups,formatCells,);};tr.addEventListener("click",(e)=>{if(e.target.closest("a"))return;toggle();});});
}
functioncards(items){returnitems.map(([label,value,hint,klass])=>`<div class="stat-card">
<div class="stat-label">${label}</div>
<div class="stat-value ${klass || ""}">${value ?? "โ"}</div>
<div class="stat-hint">${hint}</div>
</div>`,).join("");}functionrenderChips(){document.getElementById("chips").innerHTML=WINDOWS.map((w)=>`<button type="button" class="chip ${w.id === windowId ? "on" : ""}" data-w="${w.id}">${w.label}</button>`,).join("");document.querySelectorAll("#chips .chip").forEach((btn)=>{btn.onclick=()=>{windowId=btn.dataset.w;setQueryState();refresh();};});document.getElementById("aud-chips").innerHTML=AUDIENCES.map((a)=>`<button type="button" class="chip ${a.id === audienceId ? "on" : ""}" data-a="${a.id}">${a.label}</button>`,).join("");document.querySelectorAll("#aud-chips .chip").forEach((btn)=>{btn.onclick=()=>{audienceId=btn.dataset.a;setQueryState();refresh();};});}functionrenderAudienceSplit(o){constel=document.getElementById("ov-aud");// only when looking at ALL โ filtered views already are the slice
if(audienceId!=="all"||!o.new||!o.returning||typeofo.new!=="object"){el.innerHTML="";return;}constn=o.new;constr=o.returning;consttotalN=o.neurons||0||1;constpct=(x)=>`${Math.round(((Number(x)||0)/totalN)*100)}%`;el.innerHTML=cards([["NEW NEURONS",n.neurons??0,`${pct(n.neurons)} of window ยท first-seen here`,"yellow",],["NEW VISITS",n.visits??0,`depth ${fmtDepth(n.views_per_visit_milli)} ยท dwell ${fmt(n.attention_ms_per_visit)}`,"yellow",],["NEW ATT",fmt(n.attention_ms),`att/neuron ${fmt(n.attention_ms_per_neuron)}`,"yellow",],["RET NEURONS",r.neurons??0,`${pct(r.neurons)} of window ยท seen before`,"cyan",],["RET VISITS",r.visits??0,`depth ${fmtDepth(r.views_per_visit_milli)} ยท dwell ${fmt(r.attention_ms_per_visit)}`,"cyan",],["RET ATT",fmt(r.attention_ms),`att/neuron ${fmt(r.attention_ms_per_neuron)}`,"cyan",],]);}constWEEK_MS=7*24*3600*1000;functionheatStyle(pct){// 0..100 โ green fill intensity on black
if(pct==null||pct<=0)return"";consta=Math.min(0.85,0.08+(pct/100)*0.75);return`background: rgba(0, 255, 65, ${a.toFixed(3)}); color: ${pct >= 40 ? "#000" : "var(--cyber-green)"}`;}functionrenderRetentionMatrix(rows){constel=document.getElementById("retention");if(!rows||!rows.length){el.innerHTML=`<tbody><tr><td class="empty">โ not enough history for cohorts yet</td></tr></tbody>`;return;}constmaxW=Math.max(...rows.map((r)=>(r.weeks||[]).length),1,);// oldest cohort on top, newest at bottom (classic)
constsorted=[...rows].sort((a,b)=>(a.cohort_start_ms||0)-(b.cohort_start_ms||0),);constnow=Date.now();consthead=`<thead><tr><th>COHORT</th><th class="num">SIZE</th>`+Array.from({length:maxW},(_,i)=>`<th class="num">W${i}</th>`,).join("")+`</tr></thead>`;constbody=sorted.map((r)=>{constsize=r.size||0;constweeks=r.weeks||[];conststart=r.cohort_start_ms||0;constcells=Array.from({length:maxW},(_,i)=>{// week offset not yet elapsed for this cohort
if(start+i*WEEK_MS>now){return`<td class="ret-cell zero num">โ</td>`;}if(i===0){return`<td class="ret-cell w0 num" title="cohort week ยท size">${size}<span class="ret-n">100%</span></td>`;}if(!size){return`<td class="ret-cell zero num">โ</td>`;}constn=weeks[i]||0;constpct=Math.round((n/size)*100);if(n===0){return`<td class="ret-cell zero num">0%<span class="ret-n">0</span></td>`;}return`<td class="ret-cell num" style="${heatStyle(pct)}" title="${n} of ${size} neurons still active">${pct}%<span class="ret-n">${n}</span></td>`;}).join("");return`<tr>
<td>${fmtDay(start)}</td>
<td class="num">${size}</td>
${cells}
</tr>`;}).join("");el.innerHTML=head+`<tbody>${body}</tbody>`;}asyncfunctionrenderRetention(w){// matrix: full event history (server ignores window)
constmatrix=awaitget("retention","?weeks=8");renderRetentionMatrix(Array.isArray(matrix)?matrix:[]);// returns: first-seen in current window, came back within 7d
constret=awaitget("returns",`?horizon_ms=${WEEK_MS}`);constcohort=ret.cohort??0;constreturned=ret.returned??0;constrate=cohort>0?`${Math.round((returned/cohort)*100)}%`:"โ";document.getElementById("ret-kpis").innerHTML=cards([["NEW (WINDOW)",cohort,`first-seen in ${w.label}`,"yellow",],["RETURNED 7D",returned,"came back within 7 days","cyan",],["RETURN RATE",rate,"returned / new","magenta"],]);}// dual-metric pulse: any pair from the overview vocabulary
constPULSE_METRICS=[{id:"neurons",label:"NEURONS",hint:"unique keys",value:(r)=>Number(r.neurons)||0,fmt:(v)=>String(Math.round(v)),},{id:"visits",label:"VISITS",hint:"arrivals in bucket",value:(r)=>Number(r.visits)||0,fmt:(v)=>String(Math.round(v)),},{id:"views",label:"VIEWS",hint:"pageviews",value:(r)=>Number(r.views)||0,fmt:(v)=>String(Math.round(v)),},{id:"attention",label:"ATTENTION",hint:"minutes looking",value:(r)=>(Number(r.attention_ms)||0)/60000,fmt:(v)=>v>=60?`${(v/60).toFixed(1)}h`:v>=1?`${v.toFixed(1)}m`:`${Math.round(v*60)}s`,},{id:"depth",label:"DEPTH",hint:"views / visit",value:(r)=>(Number(r.views_per_visit_milli)||0)/1000,fmt:(v)=>v.toFixed(2),},{id:"dwell",label:"DWELL",hint:"attention / visit",value:(r)=>Number(r.attention_ms_per_visit)||0,fmt:(v)=>fmt(v),},{id:"att_neuron",label:"ATT/NEURON",hint:"looking per key",value:(r)=>Number(r.attention_ms_per_neuron)||0,fmt:(v)=>fmt(v),},];letpulseMetricA=localStorage.getItem("pulse_ma")||"neurons";letpulseMetricB=localStorage.getItem("pulse_mb")||"attention";letpulseMenuSlot=null;// 'a' | 'b' | null
letpulseRows=[];letpulseBucket="day";functionmetricById(id){return(PULSE_METRICS.find((m)=>m.id===id)||PULSE_METRICS[0]);}functionsyncPulseButtons(){consta=metricById(pulseMetricA);constb=metricById(pulseMetricB);document.querySelector("#pulse-ma .lab").textContent=a.label;document.querySelector("#pulse-mb .lab").textContent=b.label;document.getElementById("pulse-ma").classList.toggle("open",pulseMenuSlot==="a");document.getElementById("pulse-mb").classList.toggle("open",pulseMenuSlot==="b");}functionclosePulseMenu(){pulseMenuSlot=null;constmenu=document.getElementById("pulse-menu");menu.classList.remove("on","slot-a","slot-b");syncPulseButtons();}functionopenPulseMenu(slot){pulseMenuSlot=slot;constmenu=document.getElementById("pulse-menu");constcurrent=slot==="a"?pulseMetricA:pulseMetricB;constother=slot==="a"?pulseMetricB:pulseMetricA;menu.innerHTML=PULSE_METRICS.map((m)=>`<button type="button" role="option" data-id="${m.id}" class="${m.id === current ? "on" : ""}" ${m.id === other ? "disabled" : ""} title="${m.hint}">${m.label}${m.hint}</span></button>`,).join("");menu.classList.add("on",slot==="a"?"slot-a":"slot-b");menu.classList.remove(slot==="a"?"slot-b":"slot-a");menu.querySelectorAll("button[data-id]").forEach((btn)=>{if(btn.disabled)return;btn.onclick=(e)=>{e.stopPropagation();constid=btn.dataset.id;if(slot==="a"){pulseMetricA=id;localStorage.setItem("pulse_ma",id);}else{pulseMetricB=id;localStorage.setItem("pulse_mb",id);}closePulseMenu();renderPulse(pulseRows,pulseBucket);};});syncPulseButtons();}functioninitPulsePickers(){constma=document.getElementById("pulse-ma");constmb=document.getElementById("pulse-mb");if(ma.dataset.bound)return;ma.dataset.bound="1";ma.onclick=(e)=>{e.stopPropagation();if(pulseMenuSlot==="a")closePulseMenu();elseopenPulseMenu("a");};mb.onclick=(e)=>{e.stopPropagation();if(pulseMenuSlot==="b")closePulseMenu();elseopenPulseMenu("b");};document.addEventListener("click",(e)=>{if(!e.target.closest("#pulse-wrap"))closePulseMenu();});}functionrenderPulse(rows,bucket){pulseRows=Array.isArray(rows)?rows:[];pulseBucket=bucket;initPulsePickers();syncPulseButtons();closePulseMenu();constel=document.getElementById("pulse");consttip=document.getElementById("pulse-tip");tip.classList.remove("on");if(!pulseRows.length){el.innerHTML=`<div class="pulse-empty">โ no pulse in window</div>`;return;}constma=metricById(pulseMetricA);constmb=metricById(pulseMetricB);constvalsA=pulseRows.map((r)=>ma.value(r));constvalsB=pulseRows.map((r)=>mb.value(r));constmaxA=Math.max(1e-9,...valsA);constmaxB=Math.max(1e-9,...valsB);constwrapEl=document.getElementById("pulse-wrap");constW=Math.max(320,(wrapEl.clientWidth||640)-24);constH=160;constpad={t:12,r:48,b:28,l:48};constiw=W-pad.l-pad.r;constih=H-pad.t-pad.b;constn=pulseRows.length;constxAt=(i)=>pad.l+(n===1?iw/2:(i/(n-1))*iw);constyA=(v)=>pad.t+ih-(v/maxA)*ih;constyB=(v)=>pad.t+ih-(v/maxB)*ih;constptsA=valsA.map((v,i)=>[xAt(i),yA(v)]);constptsB=valsB.map((v,i)=>[xAt(i),yB(v)]);constline=(pts)=>pts.map(([x,y],i)=>`${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`,).join("");constarea=(pts)=>{if(!pts.length)return"";constbase=pad.t+ih;return(line(pts)+` L${pts[pts.length - 1][0].toFixed(1)},${base} L${pts[0][0].toFixed(1)},${base} Z`);};// y-axis ticks (3)
constticks=[0,0.5,1];constgrid=ticks.map((t)=>{consty=pad.t+ih*(1-t);return`<line class="grid" x1="${pad.l}" x2="${W - pad.r}" y1="${y}" y2="${y}"/>
<text class="axis-lab" x="${pad.l - 6}" y="${y + 3}" text-anchor="end">${ma.fmt(maxA*t)}</text>
<text class="axis-lab" x="${W - pad.r + 6}" y="${y + 3}" text-anchor="start">${mb.fmt(maxB*t)}</text>`;}).join("");// x labels โ show up to ~8
conststep=Math.max(1,Math.ceil(n/8));constxLabs=pulseRows.map((r,i)=>{if(i%step!==0&&i!==n-1)return"";constlab=bucket==="hour"?fmtHour(r.t):fmtDay(r.t).slice(5);return`<text class="x-lab" x="${xAt(i)}" y="${H - 8}" text-anchor="middle">${lab}</text>`;}).join("");constdotsA=ptsA.map(([x,y],i)=>`<circle class="dot-a" data-i="${i}" cx="${x}" cy="${y}" r="3"/>`,).join("");constdotsB=ptsB.map(([x,y],i)=>`<circle class="dot-b" data-i="${i}" cx="${x}" cy="${y}" r="3"/>`,).join("");// invisible hit targets for hover
consthits=pulseRows.map((_,i)=>{constx=xAt(i);consthalf=n===1?iw/2:iw/(n-1)/2;return`<rect data-i="${i}" x="${x - half}" y="${pad.t}" width="${Math.max(half * 2, 8)}" height="${ih}" fill="transparent"/>`;}).join("");el.innerHTML=`<svg class="pulse-chart" viewBox="0 0 ${W} ${H}" width="100%" height="${H}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="pulse ${ma.label} vs ${mb.label}">
${grid}
<path class="area-a" d="${area(ptsA)}"/>
<path class="area-b" d="${area(ptsB)}"/>
<path class="line-a" d="${line(ptsA)}"/>
<path class="line-b" d="${line(ptsB)}"/>
${dotsA}${dotsB}
${xLabs}
<g class="hits">${hits}</g>
<line class="hover-line" id="pulse-hover" x1="0" x2="0" y1="${pad.t}" y2="${pad.t + ih}" style="display:none"/>
</svg>`;constsvg=el.querySelector("svg");consthover=el.querySelector("#pulse-hover");constwrap=document.getElementById("pulse-wrap");constshowTip=(i,clientX)=>{constr=pulseRows[i];constva=valsA[i];constvb=valsB[i];tip.innerHTML=`<div class="t">${fmtStamp(r.t)}</div>
<div class="row-a">${ma.label} ยท ${ma.fmt(va)}</div>
<div class="row-b">${mb.label} ยท ${mb.fmt(vb)}</div>`;tip.classList.add("on");constwrapRect=wrap.getBoundingClientRect();constleft=Math.min(Math.max(8,clientX-wrapRect.left+12),wrapRect.width-140,);tip.style.left=`${left}px`;tip.style.top=`52px`;constx=xAt(i);hover.setAttribute("x1",x);hover.setAttribute("x2",x);hover.style.display="";};svg.querySelectorAll(".hits rect").forEach((rect)=>{rect.addEventListener("mousemove",(e)=>{showTip(Number(rect.dataset.i),e.clientX);});rect.addEventListener("mouseleave",()=>{tip.classList.remove("on");hover.style.display="none";});});}functionfmtAgo(ms){if(ms==null)return"โ";if(ms<1000)return"now";if(ms<60000)return`${Math.round(ms/1000)}s ago`;if(ms<3600000)return`${Math.round(ms/60000)}m ago`;return`${(ms/3600000).toFixed(1)}h ago`;}functionrenderLive(data){conststats=document.getElementById("live-stats");constel=document.getElementById("live");constactive=data.active??0;constviews=data.views??0;constatt=data.attention_ms??0;stats.innerHTML=`<b>${active} active ยท ${views}</b> views ยท <b>${fmt(att)}</b> attention ยท last 15m`;constrows=data.neurons||[];if(!rows.length){el.innerHTML=`<div class="live-empty">โ nobody in the last 15 minutes</div>`;return;}el.innerHTML=rows.slice(0,24).map((r)=>{constpath=shortPath(r.path||"โ");return`<div class="live-card" title="${r.neuron||""}">
<div class="ago">${fmtAgo(r.ago_ms)}</div>
<div class="path">${path}</div>
<div class="meta">${r.country || "โ"} ยท ${r.views || 0}v ยท ${fmt(r.attention_ms)}</div>
</div>`;}).join("");}functionrenderFreqHist(el,kpisEl,block){if(!block||!block.buckets){el.innerHTML=`<div class="live-empty">โ</div>`;kpisEl.innerHTML="";return;}kpisEl.innerHTML=`median <b>${block.median ?? 0} ยท p90 ${block.p90 ?? 0}</b> ยท max <b>${block.max??0}</b>`;constbuckets=block.buckets||[];constmaxN=Math.max(1,...buckets.map((b)=>Number(b.neurons)||0),);el.innerHTML=buckets.map((b)=>{constn=Number(b.neurons)||0;constpct=Math.round((n/maxN)*100);return`<div class="freq-row">
<div class="lab">${b.label}</div>
<div class="bar-track"><div class="bar" style="width:${pct}%"></div></div>
<div class="n">${n}</div>
</div>`;}).join("");}functionrenderScrollHist(scroll){constkpis=document.getElementById("scroll-kpis");constel=document.getElementById("scroll-hist");if(!scroll||!scroll.buckets){kpis.innerHTML="";el.innerHTML=`<div class="live-empty">โ no scroll samples yet</div>`;return;}kpis.innerHTML=`p50 <b>${scroll.p50 ?? 0}% ยท p90 ${scroll.p90 ?? 0}%</b> ยท max <b>${scroll.max ?? 0}% ยท ${scroll.samples ?? 0}</b> reaches`;constbuckets=scroll.buckets||[];constmaxN=Math.max(1,...buckets.map((b)=>Number(b.neurons)||0),);el.innerHTML=buckets.map((b)=>{constn=Number(b.neurons)||0;constpct=Math.round((n/maxN)*100);return`<div class="freq-row">
<div class="lab">${b.label}</div>
<div class="bar-track"><div class="bar" style="width:${pct}%; background: linear-gradient(90deg, rgba(255,102,0,0.35), var(--cyber-orange))"></div></div>
<div class="n">${n}</div>
</div>`;}).join("");}functionrenderFrequency(data){renderFreqHist(document.getElementById("freq-visits"),document.getElementById("freq-visits-kpis"),data.visits,);renderFreqHist(document.getElementById("freq-days"),document.getElementById("freq-days-kpis"),data.days,);table(document.getElementById("freq-top"),["NEURON","VISITS","DAYS","VIEWS","ATTENTION"],(data.top||[]).map((r)=>({neuron:r.neuron,visits:r.visits,days:r.days,views:r.views,att:fmt(r.attention_ms),})),[["neuron",""],["visits","num"],["days","num"],["views","num"],["att","num"],],);}functionrenderPathflow(data){constel=document.getElementById("pathflow");constedges=data.edges||[];if(!edges.length){el.innerHTML=`<div class="flow-empty">โ no multi-page visits in window</div>`;return;}constmaxN=Math.max(1,...edges.map((e)=>Number(e.neurons)||0),);el.innerHTML=edges.slice(0,25).map((e)=>{constn=Number(e.neurons)||0;constt=Number(e.transitions)||0;constpct=Math.max(4,Math.round((n/maxN)*100));return`<div class="flow-row">
<div class="flow-from" title="${e.from || ""}">${shortPath(e.from)}</div>
<div class="flow-arrow">โ</div>
<div class="flow-to" title="${e.to || ""}">${shortPath(e.to)}</div>
<div class="flow-meta">${n}n ยท ${t}ร</div>
<div class="flow-bar"><i style="width:${pct}%"></i></div>
</div>`;}).join("");}// โโ funnel builder (v1: ordered page paths) โโโโโโโโโโโโโโโโโโ
constFUNNEL_KEY="lytics_funnel_steps";constFUNNEL_DEFAULT=["/","/tokens","/token/usd"];letfunnelSteps=loadFunnelSteps();letfunnelPathHints=[];functionloadFunnelSteps(){try{constraw=localStorage.getItem(FUNNEL_KEY);if(!raw)return[...FUNNEL_DEFAULT];constarr=JSON.parse(raw);if(!Array.isArray(arr)||!arr.length)return[...FUNNEL_DEFAULT];returnarr.map(normalizePath).filter(Boolean).slice(0,12);}catch(_){return[...FUNNEL_DEFAULT];}}functionsaveFunnelSteps(){localStorage.setItem(FUNNEL_KEY,JSON.stringify(funnelSteps));}functionnormalizePath(p){if(p==null)returnnull;lets=String(p).trim();if(!s)returnnull;// allow pasting full URL
try{if(s.startsWith("http://")||s.startsWith("https://")){s=newURL(s).pathname;}}catch(_){}if(!s.startsWith("/"))s="/"+s;// strip query/hash
s=s.split("?")[0].split("#")[0];// collapse trailing slash except root
if(s.length>1&&s.endsWith("/"))s=s.slice(0,-1);returns;}functionrenderFunnelStepsUI(){constel=document.getElementById("funnel-steps");if(!funnelSteps.length){el.innerHTML=`<span class="funnel-empty">add path steps โ</span>`;return;}el.innerHTML=funnelSteps.map((s,i)=>{constarrow=i<funnelSteps.length-1?`<span class="funnel-arrow">โ</span>`:"";return`<span class="funnel-step-chip" title="${s}">${i + 1}</span><span class="lab">${s}arrow}`;}).join("");el.querySelectorAll("button[data-i]").forEach((btn)=>{btn.onclick=(e)=>{e.stopPropagation();consti=Number(btn.dataset.i);funnelSteps.splice(i,1);saveFunnelSteps();renderFunnelStepsUI();renderFunnelPresets();runFunnel();};});}functionrenderFunnelPresets(){constel=document.getElementById("funnel-presets");constinSet=newSet(funnelSteps);consthints=funnelPathHints.length?funnelPathHints:["/","/tokens","/doctrine","/token/usd"];el.innerHTML=`<span style="font-size:10px;letter-spacing:1px;color:#444;margin-right:4px">QUICK</span>`+hints.slice(0,12).map((p)=>{conston=inSet.has(p);return`<button type="button" class="tag ${on ? "in" : ""}" data-p="${p}" ${on ? "disabled" : ""}>${p}</button>`;}).join("");el.querySelectorAll("button.tag:not(.in)").forEach((btn)=>{btn.onclick=()=>addFunnelStep(btn.dataset.p);});}functionaddFunnelStep(raw){constp=normalizePath(raw);if(!p)return;if(funnelSteps.includes(p))return;if(funnelSteps.length>=12)return;funnelSteps.push(p);saveFunnelSteps();renderFunnelStepsUI();renderFunnelPresets();runFunnel();}functioninitFunnelBuilder(){constinput=document.getElementById("funnel-input");constadd=document.getElementById("funnel-add");constclear=document.getElementById("funnel-clear");if(add.dataset.bound)return;add.dataset.bound="1";add.onclick=()=>{addFunnelStep(input.value);input.value="";input.focus();};input.addEventListener("keydown",(e)=>{if(e.key==="Enter"){e.preventDefault();add.onclick();}});clear.onclick=()=>{funnelSteps=[];saveFunnelSteps();renderFunnelStepsUI();renderFunnelPresets();document.getElementById("funnel-chart").innerHTML=`<div class="funnel-empty">โ add steps to build a funnel</div>`;document.getElementById("funnel-summary").textContent="";};renderFunnelStepsUI();renderFunnelPresets();}functionrenderFunnelChart(rows){constel=document.getElementById("funnel-chart");constsum=document.getElementById("funnel-summary");if(!rows||!rows.length){el.innerHTML=`<div class="funnel-empty">โ no data for these steps</div>`;sum.textContent="";return;}constmaxN=Math.max(1,...rows.map((r)=>Number(r.neurons)||0),);constbase=Number(rows[0].neurons)||0;el.innerHTML=rows.map((r,i)=>{constn=Number(r.neurons)||0;constprev=i===0?n:Number(rows[i-1].neurons)||0;constpctW=Math.round((n/maxN)*100);letdrop="";if(i>0){constd=prev>0?Math.round((1-n/prev)*100):0;drop=`<span class="drop ${d >= 50 ? "bad" : ""}">โ${d}%</span>`;}constofBase=base>0&&i>0?` ยท ${Math.round((n/base)*100)}% of start`:"";return`<div class="funnel-row">
<div class="step-lab" title="${r.step || ""}">${i + 1}. ${shortPath(r.step)}</div>
<div class="bar-wrap"><div class="bar" style="width:${pctW}%"></div></div>
<div class="nums"><b>${n} n${drop}${ofBase ? `${ofBase}</span>` : ""}</div>
</div>`;
})
.join("");
const last = Number(rows[rows.length - 1].neurons) || 0;
const conv = base > 0 ? Math.round((last / base) * 100) : 0;
sum.innerHTML = `conversion<b>${conv}% ยท ${base} โ ${last} neurons ยท ${rows.length}steps`;
}
async function runFunnel() {
initFunnelBuilder();
if (!funnelSteps.length) {
document.getElementById("funnel-chart").innerHTML =
`<divclass="funnel-empty">โ addstepstobuildafunnel</div>`;
document.getElementById("funnel-summary").textContent = "";
return;
}
conststepsParam = funnelSteps
.map(encodeURIComponent)
.join(",");
try{
const rows = await get("funnel", `?steps=${stepsParam}`);
renderFunnelChart(Array.isArray(rows) ? rows : []);
} catch (_) {
renderFunnelChart([]);
}
}
async function refresh() {
renderChips();
const w = windowSpec();
const aud = audienceSpec();
// live first โ wall-clock, cheap, feels alive
try {
const live = await get(
"live",
`?horizon_ms=${15*60*1000}`,
);
renderLive(live);
} catch (_) {
renderLive({ active: 0, neurons: [] });
}
const o = await get("overview");
const from = o.from ?? Date.now() - w.ms;
const to = o.to ?? Date.now();
const asOf = o.as_of ?? Date.now();
document.getElementById("meta").innerHTML =
`WINDOW<timedatetime="${new Date(from).toISOString()}">${fmtDay(from)}</time>` +` โ <time datetime="${new Date(to).toISOString()}">${fmtDay(to)}</time>`+` ยท ${aud.label}`+` ยท UTC ยท AS OF <time datetime="${new Date(asOf).toISOString()}">${fmtStamp(asOf)}</time>`;
document.getElementById("logo-suffix").textContent=`cyberstates ยท ${w.label} ยท ${aud.label} ยท ${fmtDay(from)} โ ${fmtDay(to)}`;
// core funnel: neurons โ visits โ views
// derivatives: depth, dwell, attention, attention/neuron
// when audience chip is NEW/RETURNING this is already the slice
constdepth=fmtDepth(o.views_per_visit_milli);
constdwell=fmt(o.attention_ms_per_visit);
constattN=fmt(o.attention_ms_per_neuron);
constwho=audienceId==="all"?"unique keys":audienceId==="new"?"first-seen in window":"seen before window";
constscr=o.scroll||{};
constscrP50=scr.p50!=null?`${scr.p50}%`:"โ";
constscrP90=scr.p90!=null?`${scr.p90}%`:"โ";
document.getElementById("ov").innerHTML=cards([["NEURONS",o.neurons,who,""],["VISITS",o.visits,"entries ยท no timeout","yellow"],["VIEWS",o.views,"pages seen","cyan"],["DEPTH",depth,"views / visit","orange"],["DWELL",dwell,"attention / visit","magenta"],["ATTENTION",fmt(o.attention_ms),"total looking","cyan"],["ATT / NEURON",attN,"looking per unique key","magenta"],["SCROLL p50",scrP50,`${scr.samples??0} page reaches`,"orange",],["SCROLL p90",scrP90,`max ${scr.max!=null?scr.max+"%":"โ"}`,"magenta",],]);
renderAudienceSplit(o);
renderScrollHist(scr);
// frequency โ whale vs long tail
try{renderFrequency(awaitget("frequency"));
}catch(_){renderFrequency({});
}// retention โ full history matrix + returns in current window
awaitrenderRetention(w);
// pulse
constseries=awaitget("timeseries",`?bucket=${w.bucket}`);
renderPulse(Array.isArray(series)?series:[],w.bucket);
// landings / exits โ grouped by first path segment
constpass=awaitget("passages","?limit=100");
// path flow โ consecutive edges inside visits
try{renderPathflow(awaitget("pathflow", "?limit=30")); } catch(_){renderPathflow({edges: [] });
}constpassFmt=(m)=>[m.neurons??"โ",m.passages??m.visits??"โ",];
renderPathTable(document.getElementById("entries"),"entries",["ENTRY","NEURONS","VISITS"],groupByPathSegment(pass.entries||[],["neurons","passages",]),(m)=>passFmt(m),);
renderPathTable(document.getElementById("exits"),"exits",["EXIT","NEURONS","VISITS"],groupByPathSegment(pass.exits||[],["neurons","passages",]),(m)=>passFmt(m),);
// top pages โ funnel + attention + scroll, first-segment rollup
constparticles=awaitget("particles","?limit=100");
// path hints for funnel quick-add (leaf paths only)
funnelPathHints=(Array.isArray(particles)?particles:[]).map((p)=>p.pathname).filter(Boolean).slice(0,16);
renderFunnelPresets();
awaitrunFunnel();
constparticleFmt=(m)=>{constneurons=Number(m.neurons)||0;constvisits=Number(m.visits)||0;constviews=Number(m.views)||0;constatt=Number(m.attention_ms)||0;// re-derive from summed totals on groups; leaves use same path
constdepthMilli=visits>0?Math.round((views/visits)*1000):0;constdwell=visits>0?att/visits:0;constattN=neurons>0?att/neurons:0;constsp50=m.scroll_p50!=null&&m.scroll_samples?`${m.scroll_p50}%`:"โ";return[neurons||"โ",visits||"โ",views||"โ",fmtDepth(depthMilli),fmt(dwell),fmt(attN),sp50,fmt(att),];};
// for segment groups: recompute scroll p50 from children is hard
// without raw samples โ show "โ" on folder rows (no scroll_p50)
renderPathTable(document.getElementById("particles"),"particles",["PATH","NEURONS","VISITS","VIEWS","DEPTH","DWELL","ATT/N","SCROLL","ATTENTION",],groupByPathSegment(particles,["neurons","visits","views","attention_ms",]),(m,_isGroup,g)=>{// leaf path group: metrics summed from one row โ pull
// scroll off the original leaf (not additive).
if(g&&g.leaf){returnparticleFmt({
...m,scroll_p50:g.leaf.scroll_p50,scroll_samples:g.leaf.scroll_samples,});}// folder with children: scroll not rollable here
if(g&&g.children&&g.children.length){returnparticleFmt({
...m,scroll_p50:null,scroll_samples:0,});}returnparticleFmt(m);},);
// geo + devices โ funnel: neurons / visits / views
constfunnelCols=[["name",""],["neurons","num"],["visits","num"],["views","num"],["depth","num"],["dwell","num"],["att_n","num"],];
table(document.getElementById("countries"),["COUNTRY","NEURONS","VISITS","VIEWS","DEPTH","DWELL","ATT/N",],(awaitget("countries","?limit=20")).map((r)=>({name:r.country??"โ",neurons:r.neurons??"",visits:r.visits??"",views:r.views??"",depth:fmtDepth(r.views_per_visit_milli),dwell:fmt(r.attention_ms_per_visit),att_n:fmt(r.attention_ms_per_neuron),})),funnelCols,);
constdev=awaitget("devices","?limit=20");
constmapFunnel=(name,r)=>({name,neurons:r.neurons??"",visits:r.visits??"",views:r.views??"",depth:fmtDepth(r.views_per_visit_milli),dwell:fmt(r.attention_ms_per_visit),att_n:fmt(r.attention_ms_per_neuron),});
constclassRows=(dev.classes||[]).map((r)=>mapFunnel(r.class??"โ",r),);
constbrowserRows=(dev.browsers||[]).slice(0,8).map((r)=>mapFunnel(r.browser??"โ",r));
table(document.getElementById("devices"),["CLASS / BROWSER","NEURONS","VISITS","VIEWS","DEPTH","DWELL","ATT/N",],[...classRows,...browserRows],funnelCols,);
constmapVisitRow=(r)=>({name:r.source??r.channel??r.key,neurons:r.neurons??"โ",visits:r.visits,views:r.views,depth:fmtDepth(r.views_per_visit_milli),dwell:fmt(r.attention_ms_per_visit),att_n:fmt(r.attention_ms_per_neuron),attention:fmt(r.attention_ms),});
constvisitCols=[["name",""],["neurons","num"],["visits","num"],["views","num"],["depth","num"],["dwell","num"],["att_n","num"],["attention","num"],];
table(document.getElementById("sources"),["SOURCE","NEURONS","VISITS","VIEWS","DEPTH","DWELL","ATT/N","ATTENTION",],(awaitget("sources","?limit=40")).map(mapVisitRow),visitCols,);
table(document.getElementById("channels"),["CHANNEL","NEURONS","VISITS","VIEWS","DEPTH","DWELL","ATT/N","ATTENTION",],(awaitget("channels")).map(mapVisitRow),visitCols,);
}
refresh();
setInterval(refresh,8000);
</script></body></html>