squeezr-ai 1.21.6 → 1.21.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,4 +3,4 @@
3
3
  * Dark GitHub-style theme, sidebar navigation, 4 pages.
4
4
  * All data via SSE (/squeezr/events) + REST (/squeezr/history, /squeezr/projects).
5
5
  */
6
- export declare const DASHBOARD_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>Squeezr Dashboard</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#09090b;--bg2:#111113;--bg3:#1a1a1e;--bg4:#252529;\n --border:#2a2a2e;--text:#e4e4e7;--muted:#71717a;\n --green:#22c55e;--yellow:#eab308;--red:#ef4444;\n --blue:#22c55e;--purple:#a78bfa;--orange:#f59e0b;--accent:#16a34a\n}\nhtml,body{height:100%;background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,-apple-system,sans-serif;font-size:13px;line-height:1.5}\na{color:var(--blue);text-decoration:none}\ncode{font-family:'Cascadia Code','Fira Mono','Consolas',monospace}\n\n/* \u2500\u2500 App shell \u2500\u2500 */\n#app{display:flex;height:100vh;overflow:hidden}\n\n/* \u2500\u2500 Sidebar \u2500\u2500 */\n#sidebar{\n width:200px;flex-shrink:0;background:var(--bg2);\n border-right:1px solid var(--border);\n display:flex;flex-direction:column;overflow:hidden\n}\n#sidebar-brand{padding:16px 16px 12px;border-bottom:1px solid var(--border)}\n#sidebar-brand .logo{font-size:18px;font-weight:700;letter-spacing:.3px;line-height:1}\n#sidebar-brand .logo span{color:var(--blue)}\n#sidebar-brand .ver{font-size:11px;color:var(--muted);margin-top:3px}\n\nnav{flex:1;padding:8px 0;overflow-y:auto}\n.nav-item{\n display:flex;align-items:center;gap:9px;padding:8px 16px;\n color:var(--muted);cursor:pointer;border-radius:0;\n transition:background .1s,color .1s;user-select:none\n}\n.nav-item:hover{background:var(--bg3);color:var(--text)}\n.nav-item.active{background:var(--bg3);color:var(--blue)}\n.nav-item svg{flex-shrink:0;opacity:.8}\n.nav-item.active svg{opacity:1}\n.nav-label{font-size:13px}\n\n#sidebar-footer{padding:12px 16px;border-top:1px solid var(--border)}\n.status-row{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--muted)}\n.dot{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 5px var(--green);flex-shrink:0}\n.dot.off{background:var(--red);box-shadow:0 0 5px var(--red)}\n\n/* \u2500\u2500 Main content \u2500\u2500 */\n#content{flex:1;display:flex;flex-direction:column;overflow:hidden}\n#page-header{\n display:flex;align-items:center;gap:10px;padding:12px 20px;\n background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0\n}\n#page-title{font-size:15px;font-weight:600}\n#project-badge{\n font-size:11px;background:var(--bg3);border:1px solid var(--border);\n border-radius:12px;padding:2px 10px;color:var(--blue);font-weight:500\n}\n#conn-pill{\n font-size:11px;padding:2px 8px;border-radius:10px;\n background:rgba(63,185,80,.15);color:var(--green);border:1px solid rgba(63,185,80,.3)\n}\n#conn-pill.err{background:rgba(248,81,73,.15);color:var(--red);border-color:rgba(248,81,73,.3)}\n\n#pages{flex:1;overflow-y:auto;padding:16px 20px}\n.page{display:none}\n.page.active{display:block}\n\n/* \u2500\u2500 Cards \u2500\u2500 */\n.cards-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(175px,1fr));gap:10px;margin-bottom:14px}\n.card{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:14px 16px}\n.card-label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}\n.card-value{font-size:26px;font-weight:700;line-height:1.1}\n.card-sub{font-size:11px;color:var(--muted);margin-top:3px}\n.c-green .card-value{color:var(--green)}\n.c-blue .card-value{color:var(--blue)}\n.c-yellow .card-value{color:var(--yellow)}\n.c-orange .card-value{color:var(--orange)}\n\n/* \u2500\u2500 Sections \u2500\u2500 */\n.section{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin-bottom:14px}\n.section-title{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;font-weight:600}\n\n/* \u2500\u2500 Bars \u2500\u2500 */\n.bar-row{display:flex;align-items:center;gap:8px;margin-bottom:7px}\n.bar-label{font-size:12px;color:var(--muted);width:130px;flex-shrink:0}\n.bar-track{flex:1;height:7px;background:var(--bg3);border-radius:4px;overflow:hidden}\n.bar-fill{height:100%;border-radius:4px;transition:width .4s,background .4s}\n.bar-val{font-size:11px;width:36px;text-align:right;flex-shrink:0;color:var(--muted)}\n\n/* \u2500\u2500 Sparkline \u2500\u2500 */\ncanvas#sparkline{width:100%;height:72px;display:block}\n\n/* \u2500\u2500 Tables \u2500\u2500 */\ntable{width:100%;border-collapse:collapse}\nth{font-size:11px;color:var(--muted);text-align:left;padding:4px 8px;border-bottom:1px solid var(--border);font-weight:500;letter-spacing:.3px;text-transform:uppercase}\ntd{padding:6px 8px;font-size:12px;border-bottom:1px solid var(--border)}\ntr:last-child td{border-bottom:none}\n.td-right{text-align:right;font-variant-numeric:tabular-nums}\n.mini-bar{display:inline-block;height:5px;border-radius:2px;vertical-align:middle;margin-right:5px;opacity:.75}\n.tag{display:inline-block;background:var(--bg3);border:1px solid var(--border);border-radius:3px;padding:1px 6px;font-size:11px;font-family:monospace}\n\n/* \u2500\u2500 Cache row \u2500\u2500 */\n.cache-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px}\n.cache-card{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:10px 14px}\n.cache-card .cache-label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:3px}\n.cache-card .cache-val{font-size:18px;font-weight:600;color:var(--purple)}\n\n/* \u2500\u2500 Mode buttons \u2500\u2500 */\n.mode-btns{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px}\n.mode-btn{\n display:flex;align-items:center;gap:6px;padding:6px 14px;\n border-radius:6px;border:1px solid var(--border);background:var(--bg3);\n color:var(--muted);cursor:pointer;font-size:12px;transition:all .15s\n}\n.mode-btn:hover{border-color:var(--blue);color:var(--text)}\n.mode-btn.active{border-color:var(--accent);background:var(--accent);color:#fff}\n.mode-btn.active svg{stroke:white}\n#mode-desc{font-size:12px;color:var(--muted);min-height:16px}\n\n/* \u2500\u2500 Projects page \u2500\u2500 */\n.project-table td:first-child code{font-size:12px}\n.project-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:6px}\n\n/* \u2500\u2500 History page \u2500\u2500 */\n#hist-layout{display:grid;grid-template-columns:220px 1fr;gap:12px;min-height:400px}\n#hist-projects{background:var(--bg2);border:1px solid var(--border);border-radius:8px;overflow:hidden}\n#hist-sessions{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:0}\n.hist-proj-item{\n padding:9px 14px;cursor:pointer;border-bottom:1px solid var(--border);\n display:flex;justify-content:space-between;align-items:center;\n font-size:12px;color:var(--muted);transition:background .1s\n}\n.hist-proj-item:last-child{border-bottom:none}\n.hist-proj-item:hover{background:var(--bg3)}\n.hist-proj-item.active{background:var(--bg3);color:var(--blue)}\n.hist-proj-count{font-size:11px;background:var(--bg4);border-radius:10px;padding:1px 7px}\n.hist-sessions-header{padding:12px 16px;border-bottom:1px solid var(--border);font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;font-weight:600}\n.session-card{padding:12px 16px;border-bottom:1px solid var(--border)}\n.session-card:last-child{border-bottom:none}\n.session-date{font-size:12px;font-weight:600;color:var(--text);margin-bottom:4px}\n.session-time{font-size:11px;color:var(--muted);margin-bottom:6px}\n.session-stats{display:flex;gap:14px;flex-wrap:wrap}\n.session-stat{font-size:11px;color:var(--muted)}\n.session-stat span{color:var(--text);font-weight:500}\n.session-project-badge{font-size:10px;background:var(--bg4);border:1px solid var(--border);border-radius:10px;padding:1px 8px;color:var(--blue);margin-left:6px}\n.empty-msg{padding:32px 16px;text-align:center;color:var(--muted);font-size:12px}\n\n/* \u2500\u2500 Settings \u2500\u2500 */\n.config-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--border);font-size:12px}\n.config-row:last-child{border-bottom:none}\n.config-key{color:var(--muted)}\n.config-val{font-family:monospace;color:var(--text)}\n\n/* \u2500\u2500 Limits page \u2500\u2500 */\n.limits-cli-section{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin-bottom:14px}\n.limits-cli-header{display:flex;align-items:center;gap:8px;margin-bottom:12px}\n.limits-cli-name{font-size:13px;font-weight:600;color:var(--text)}\n.limits-cli-badge{font-size:10px;padding:1px 7px;border-radius:10px;border:1px solid;margin-left:2px}\n.limits-cli-badge.live{border-color:rgba(63,185,80,.4);color:var(--green);background:rgba(63,185,80,.1)}\n.limits-cli-badge.error{border-color:rgba(248,81,73,.4);color:var(--red);background:rgba(248,81,73,.1)}\n.limits-cli-badge.warn{border-color:rgba(210,153,34,.4);color:var(--yellow);background:rgba(210,153,34,.1)}\n.limits-cli-badge.none{border-color:var(--border);color:var(--muted);background:transparent}\n.limits-gauge-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin-bottom:10px}\n.limits-gauge{background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:10px 12px}\n.limits-gauge-label{font-size:11px;color:var(--muted);margin-bottom:6px;display:flex;justify-content:space-between}\n.limits-gauge-bar{height:6px;background:var(--bg4);border-radius:3px;overflow:hidden;margin-bottom:5px}\n.limits-gauge-fill{height:100%;border-radius:3px;transition:width .5s,background .5s}\n.limits-gauge-bottom{display:flex;justify-content:space-between;font-size:11px}\n.limits-gauge-remaining{color:var(--text);font-weight:500}\n.limits-gauge-reset{color:var(--muted)}\n.limits-usage-row{display:flex;gap:16px;flex-wrap:wrap;padding-top:8px;border-top:1px solid var(--border);margin-top:4px}\n.limits-usage-item{font-size:12px;color:var(--muted)}\n.limits-usage-item span{color:var(--text);font-weight:500}\n.limits-no-data{padding:16px;text-align:center;color:var(--muted);font-size:12px}\n.limits-billing-row{display:flex;gap:10px;flex-wrap:wrap;padding:8px 0 2px}\n.limits-credit-card{flex:1;min-width:120px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:10px 12px}\n.limits-credit-label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}\n.limits-credit-val{font-size:20px;font-weight:600;color:var(--green)}\n.limits-budget-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:6px}\n.limits-budget-input{background:var(--bg3);border:1px solid var(--border);border-radius:5px;padding:5px 10px;color:var(--text);font-size:12px;width:140px;outline:none}\n.limits-budget-input:focus{border-color:var(--blue)}\n.limits-budget-label{font-size:12px;color:var(--muted)}\n\n/* \u2500\u2500 Footer bar \u2500\u2500 */\n#footer{padding:7px 20px;border-top:1px solid var(--border);background:var(--bg2);font-size:11px;color:var(--muted);display:flex;gap:16px;flex-shrink:0}\n#footer a{color:var(--muted)}#footer a:hover{color:var(--blue)}\n</style>\n</head>\n<body>\n<div id=\"app\">\n\n<!-- \u2500\u2500 Sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<div id=\"sidebar\">\n <div id=\"sidebar-brand\">\n <div class=\"logo\" style=\"display:flex;align-items:center;gap:8px\">\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 80 80\" fill=\"none\"><rect width=\"80\" height=\"80\" rx=\"16\" fill=\"#16a34a\"/><rect x=\"8\" y=\"14\" width=\"64\" height=\"10\" rx=\"5\" fill=\"white\" opacity=\"0.35\"/><rect x=\"16\" y=\"35\" width=\"48\" height=\"10\" rx=\"5\" fill=\"white\" opacity=\"0.65\"/><rect x=\"24\" y=\"56\" width=\"32\" height=\"10\" rx=\"5\" fill=\"white\" opacity=\"1\"/></svg>\n Squee<span>zr</span>\n </div>\n <div class=\"ver\" id=\"sb-ver\">v\u2014</div>\n </div>\n\n <nav>\n <div class=\"nav-item active\" data-page=\"overview\">\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M1 2.5A1.5 1.5 0 012.5 1h3A1.5 1.5 0 017 2.5v3A1.5 1.5 0 015.5 7h-3A1.5 1.5 0 011 5.5v-3zm8 0A1.5 1.5 0 0110.5 1h3A1.5 1.5 0 0115 2.5v3A1.5 1.5 0 0113.5 7h-3A1.5 1.5 0 019 5.5v-3zm-8 8A1.5 1.5 0 012.5 9h3A1.5 1.5 0 017 10.5v3A1.5 1.5 0 015.5 15h-3A1.5 1.5 0 011 13.5v-3zm8 0A1.5 1.5 0 0110.5 9h3a1.5 1.5 0 011.5 1.5v3A1.5 1.5 0 0113.5 15h-3A1.5 1.5 0 019 13.5v-3z\"/>\n </svg>\n <span class=\"nav-label\">Overview</span>\n </div>\n <div class=\"nav-item\" data-page=\"projects\">\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M9.828 3h3.982a2 2 0 011.992 2.181l-.637 7A2 2 0 0113.174 14H2.826a2 2 0 01-1.991-1.819l-.637-7a1.99 1.99 0 01.342-1.31L.5 3a2 2 0 012-2h3.672a2 2 0 011.414.586l.828.828A2 2 0 009.828 3zm-8.322.12C1.72 3.042 1.95 3 2.19 3h5.396l-.707-.707A1 1 0 006.172 2H2.5a1 1 0 00-1 .981l.006.139z\"/>\n </svg>\n <span class=\"nav-label\">Projects</span>\n </div>\n <div class=\"nav-item\" data-page=\"history\">\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 3.5a.5.5 0 00-1 0V9a.5.5 0 00.252.434l3.5 2a.5.5 0 00.496-.868L8 8.71V3.5z\"/>\n <path d=\"M8 16A8 8 0 108 0a8 8 0 000 16zm7-8A7 7 0 111 8a7 7 0 0114 0z\"/>\n </svg>\n <span class=\"nav-label\">History</span>\n </div>\n <div class=\"nav-item\" data-page=\"limits\">\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"20\" x2=\"18\" y2=\"10\"/>\n <line x1=\"12\" y1=\"20\" x2=\"12\" y2=\"4\"/>\n <line x1=\"6\" y1=\"20\" x2=\"6\" y2=\"14\"/>\n <line x1=\"2\" y1=\"20\" x2=\"22\" y2=\"20\"/>\n </svg>\n <span class=\"nav-label\">Limits</span>\n </div>\n <div class=\"nav-item\" data-page=\"settings\">\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 4.754a3.246 3.246 0 100 6.492 3.246 3.246 0 000-6.492zM5.754 8a2.246 2.246 0 114.492 0 2.246 2.246 0 01-4.492 0z\"/>\n <path d=\"M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 01-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 01-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 01.52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 011.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 011.255-.52l.292.16c1.64.892 3.433-.902 2.54-2.541l-.159-.292a.873.873 0 01.52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 01-.52-1.255l.16-.292c.892-1.64-.901-3.433-2.541-2.54l-.292.159a.873.873 0 01-1.255-.52l-.094-.319z\"/>\n </svg>\n <span class=\"nav-label\">Settings</span>\n </div>\n </nav>\n\n <div id=\"sidebar-footer\">\n <div class=\"status-row\">\n <div class=\"dot\" id=\"status-dot\"></div>\n <span id=\"status-text\">Connecting\u2026</span>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500 Main content \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<div id=\"content\">\n <div id=\"page-header\">\n <span id=\"page-title\">Overview</span>\n <span id=\"project-badge\" style=\"display:none\"></span>\n <span id=\"conn-pill\">\u25CF live</span>\n </div>\n\n <div id=\"pages\">\n\n <!-- \u2500\u2500\u2500 Overview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"page active\" id=\"page-overview\">\n <div class=\"cards-grid\">\n <div class=\"card c-green\">\n <div class=\"card-label\">Tokens Saved</div>\n <div class=\"card-value\" id=\"c-tokens\">\u2014</div>\n <div class=\"card-sub\" id=\"c-chars\">\u2014 chars</div>\n </div>\n <div class=\"card c-blue\">\n <div class=\"card-label\">Compression</div>\n <div class=\"card-value\" id=\"c-pct\">\u2014</div>\n <div class=\"card-sub\">of tool results</div>\n </div>\n <div class=\"card c-yellow\">\n <div class=\"card-label\">Requests</div>\n <div class=\"card-value\" id=\"c-req\">\u2014</div>\n <div class=\"card-sub\" id=\"c-compressions\">\u2014 compressions</div>\n </div>\n <div class=\"card c-orange\">\n <div class=\"card-label\">Est. Cost Saved</div>\n <div class=\"card-value\" id=\"c-cost\">\u2014</div>\n <div class=\"card-sub\">@ $3 / MTok</div>\n </div>\n </div>\n\n <div class=\"section\">\n <div class=\"section-title\">Context pressure \u2014 last request</div>\n <div class=\"bar-row\">\n <span class=\"bar-label\">Before compression</span>\n <div class=\"bar-track\"><div class=\"bar-fill\" id=\"bar-msg\" style=\"width:0%\"></div></div>\n <span class=\"bar-val\" id=\"pct-msg\">0%</span>\n </div>\n <div class=\"bar-row\">\n <span class=\"bar-label\">After compression</span>\n <div class=\"bar-track\"><div class=\"bar-fill\" id=\"bar-out\" style=\"width:0%\"></div></div>\n <span class=\"bar-val\" id=\"pct-out\">0%</span>\n </div>\n <div class=\"bar-row\" style=\"margin-bottom:0\">\n <span class=\"bar-label\">Session cache hits</span>\n <div class=\"bar-track\"><div class=\"bar-fill\" id=\"bar-cache\" style=\"width:0%;background:var(--purple)\"></div></div>\n <span class=\"bar-val\" id=\"pct-cache\">0</span>\n </div>\n </div>\n\n <div class=\"section\">\n <div class=\"section-title\">Activity \u2014 tokens saved per request <span style=\"font-weight:400;text-transform:none;letter-spacing:0\">(last 60)</span></div>\n <canvas id=\"sparkline\"></canvas>\n </div>\n\n <div class=\"section\">\n <div class=\"section-title\">By tool</div>\n <table>\n <thead>\n <tr>\n <th>Tool</th>\n <th class=\"td-right\">Calls</th>\n <th class=\"td-right\">Tokens saved</th>\n <th>Savings</th>\n </tr>\n </thead>\n <tbody id=\"tools-body\">\n <tr><td colspan=\"4\" style=\"color:var(--muted);padding:14px 8px;text-align:center\">No data yet\u2026</td></tr>\n </tbody>\n </table>\n </div>\n\n <div class=\"cache-row\">\n <div class=\"cache-card\">\n <div class=\"cache-label\">Session cache</div>\n <div class=\"cache-val\" id=\"c-scache\">\u2014</div>\n </div>\n <div class=\"cache-card\">\n <div class=\"cache-label\">Expand store</div>\n <div class=\"cache-val\" id=\"c-expand\">\u2014</div>\n </div>\n <div class=\"cache-card\">\n <div class=\"cache-label\">LRU cache</div>\n <div class=\"cache-val\" id=\"c-lru\">\u2014</div>\n </div>\n <div class=\"cache-card\">\n <div class=\"cache-label\">Pattern hits</div>\n <div class=\"cache-val\" id=\"c-patterns\">\u2014</div>\n </div>\n </div>\n\n <!-- Savings breakdown -->\n <div class=\"section\">\n <div class=\"section-title\">Savings Breakdown</div>\n <div class=\"cache-grid\" style=\"grid-template-columns:1fr 1fr 1fr\">\n <div class=\"cache-item\">\n <div class=\"cache-label\">Deterministic</div>\n <div class=\"cache-val\" id=\"bd-det\" style=\"color:var(--green)\">\u2014</div>\n </div>\n <div class=\"cache-item\">\n <div class=\"cache-label\">AI compression</div>\n <div class=\"cache-val\" id=\"bd-ai\" style=\"color:var(--blue)\">\u2014</div>\n </div>\n <div class=\"cache-item\">\n <div class=\"cache-label\">Read dedup</div>\n <div class=\"cache-val\" id=\"bd-dedup\" style=\"color:var(--purple)\">\u2014</div>\n </div>\n <div class=\"cache-item\">\n <div class=\"cache-label\">System prompt</div>\n <div class=\"cache-val\" id=\"bd-sysprompt\">\u2014</div>\n </div>\n <div class=\"cache-item\">\n <div class=\"cache-label\">Tag overhead</div>\n <div class=\"cache-val\" id=\"bd-overhead\" style=\"color:var(--muted)\">\u2014</div>\n </div>\n <div class=\"cache-item\">\n <div class=\"cache-label\">AI calls</div>\n <div class=\"cache-val\" id=\"bd-aicalls\" style=\"color:var(--muted)\">\u2014</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- \u2500\u2500\u2500 Projects \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"page\" id=\"page-projects\">\n <div class=\"section\" style=\"margin-bottom:0\">\n <div class=\"section-title\" id=\"projects-section-title\">All projects \u2014 this session + history</div>\n <table class=\"project-table\">\n <thead>\n <tr>\n <th>Project</th>\n <th class=\"td-right\">Sessions</th>\n <th class=\"td-right\">Requests</th>\n <th class=\"td-right\">Tokens saved</th>\n <th class=\"td-right\">Last seen</th>\n </tr>\n </thead>\n <tbody id=\"projects-body\">\n <tr><td colspan=\"5\" style=\"color:var(--muted);padding:20px 8px;text-align:center\">Loading\u2026</td></tr>\n </tbody>\n </table>\n </div>\n </div>\n\n <!-- \u2500\u2500\u2500 History \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"page\" id=\"page-history\">\n <div id=\"hist-layout\">\n <div id=\"hist-projects\">\n <div style=\"padding:10px 14px;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid var(--border)\">Projects</div>\n <div id=\"hist-proj-list\"></div>\n </div>\n <div id=\"hist-sessions\">\n <div class=\"hist-sessions-header\" id=\"hist-sessions-header\">Select a project</div>\n <div id=\"hist-sessions-list\"><div class=\"empty-msg\">Select a project on the left to view sessions.</div></div>\n </div>\n </div>\n </div>\n\n <!-- \u2500\u2500\u2500 Limits \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"page\" id=\"page-limits\">\n\n <!-- Anthropic -->\n <div class=\"limits-cli-section\" id=\"lim-anthropic\">\n <div class=\"limits-cli-header\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"currentColor\" style=\"color:var(--orange)\">\n <path d=\"M13.83 2.34a2.09 2.09 0 0 0-3.66 0L1.13 18.9A2.09 2.09 0 0 0 2.96 22h18.08a2.09 2.09 0 0 0 1.83-3.1L13.83 2.34ZM12 8a1 1 0 0 1 1 1v5a1 1 0 0 1-2 0V9a1 1 0 0 1 1-1Zm0 10a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"/>\n </svg>\n <svg width=\"16\" height=\"16\" fill=\"var(--text)\" viewBox=\"0 0 16 16\" style=\"vertical-align:middle;margin-right:6px\"><path d=\"m3.127 10.604 3.135-1.76.053-.153-.053-.085H6.11l-.525-.032-1.791-.048-1.554-.065-1.505-.08-.38-.081L0 7.832l.036-.234.32-.214.455.04 1.009.069 1.513.105 1.097.064 1.626.17h.259l.036-.105-.089-.065-.068-.064-1.566-1.062-1.695-1.121-.887-.646-.48-.327-.243-.306-.104-.67.435-.48.585.04.15.04.593.456 1.267.981 1.654 1.218.242.202.097-.068.012-.049-.109-.181-.9-1.626-.96-1.655-.428-.686-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089.279.242.411.94.666 1.48 1.033 2.014.302.597.162.553.06.17h.105v-.097l.085-1.134.157-1.392.154-1.792.052-.504.25-.605.497-.327.387.186.319.456-.045.294-.19 1.23-.37 1.93-.243 1.29h.142l.161-.16.654-.868 1.097-1.372.484-.545.565-.601.363-.287h.686l.505.751-.226.775-.707.895-.585.759-.839 1.13-.524.904.048.072.125-.012 1.897-.403 1.024-.186 1.223-.21.553.258.06.263-.218.536-1.307.323-1.533.307-2.284.54-.028.02.032.04 1.029.098.44.024h1.077l2.005.15.525.346.315.424-.053.323-.807.411-3.631-.863-.872-.218h-.12v.073l.726.71 1.331 1.202 1.667 1.55.084.383-.214.302-.226-.032-1.464-1.101-.565-.497-1.28-1.077h-.084v.113l.295.432 1.557 2.34.08.718-.112.234-.404.141-.444-.08-.911-1.28-.94-1.44-.759-1.291-.093.053-.448 4.821-.21.246-.484.186-.403-.307-.214-.496.214-.98.258-1.28.21-1.016.19-1.263.112-.42-.008-.028-.092.012-.953 1.307-1.448 1.957-1.146 1.227-.274.109-.477-.247.045-.44.266-.39 1.586-2.018.956-1.25.617-.723-.004-.105h-.036l-4.212 2.736-.75.096-.324-.302.04-.496.154-.162 1.267-.871z\"/></svg><span class=\"limits-cli-name\">Anthropic \u00B7 Claude Code</span>\n <span class=\"limits-cli-badge none\" id=\"ant-badge\">no data yet</span>\n </div>\n <div class=\"limits-gauge-grid\">\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\">\n <span id=\"ant-tok-label\">Tokens / minute</span>\n <span id=\"ant-tok-pct\" style=\"color:var(--muted)\">\u2014</span>\n </div>\n <div class=\"limits-gauge-bar\"><div class=\"limits-gauge-fill\" id=\"ant-tok-fill\" style=\"width:0%\"></div></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"ant-tok-rem\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"ant-tok-reset\"></span>\n </div>\n </div>\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\">\n <span id=\"ant-req-label\">Requests / minute</span>\n <span id=\"ant-req-pct\" style=\"color:var(--muted)\">\u2014</span>\n </div>\n <div class=\"limits-gauge-bar\"><div class=\"limits-gauge-fill\" id=\"ant-req-fill\" style=\"width:0%\"></div></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"ant-req-rem\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"ant-req-reset\"></span>\n </div>\n </div>\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\">\n <span id=\"ant-inp-label\">Input tokens / minute</span>\n <span id=\"ant-inp-pct\" style=\"color:var(--muted)\">\u2014</span>\n </div>\n <div class=\"limits-gauge-bar\"><div class=\"limits-gauge-fill\" id=\"ant-inp-fill\" style=\"width:0%\"></div></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"ant-inp-rem\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"ant-inp-reset\"></span>\n </div>\n </div>\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\">\n <span id=\"ant-out-label\">Output tokens / minute</span>\n <span id=\"ant-out-pct\" style=\"color:var(--muted)\">\u2014</span>\n </div>\n <div class=\"limits-gauge-bar\"><div class=\"limits-gauge-fill\" id=\"ant-out-fill\" style=\"width:0%\"></div></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"ant-out-rem\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"ant-out-reset\"></span>\n </div>\n </div>\n </div>\n <div class=\"limits-usage-row\">\n <div class=\"limits-usage-item\">Session input: <span id=\"ant-u-inp-s\">\u2014</span></div>\n <div class=\"limits-usage-item\">Session output: <span id=\"ant-u-out-s\">\u2014</span></div>\n <div class=\"limits-usage-item\">Today input: <span id=\"ant-u-inp-d\">\u2014</span></div>\n <div class=\"limits-usage-item\">Today output: <span id=\"ant-u-out-d\">\u2014</span></div>\n <div class=\"limits-usage-item\" style=\"margin-left:auto\">\n <a href=\"https://console.anthropic.com/settings/usage\" target=\"_blank\" style=\"color:var(--muted);font-size:11px\">View billing \u2197</a>\n </div>\n </div>\n </div>\n\n <!-- OpenAI -->\n <div class=\"limits-cli-section\" id=\"lim-openai\">\n <div class=\"limits-cli-header\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"currentColor\" style=\"color:var(--text)\">\n <path d=\"M22.28 9.27a6.17 6.17 0 0 0-.53-5.06 6.24 6.24 0 0 0-6.7-2.99A6.23 6.23 0 0 0 10.36 0a6.24 6.24 0 0 0-5.95 4.32 6.23 6.23 0 0 0-4.16 3.02 6.24 6.24 0 0 0 .77 7.32 6.17 6.17 0 0 0 .53 5.06 6.24 6.24 0 0 0 6.7 2.99A6.23 6.23 0 0 0 13.64 24a6.25 6.25 0 0 0 5.96-4.33 6.23 6.23 0 0 0 4.15-3.02 6.24 6.24 0 0 0-.77-7.31l.3-.07ZM13.64 22.5a4.63 4.63 0 0 1-2.97-1.08l.15-.08 4.93-2.85a.82.82 0 0 0 .41-.71v-6.96l2.08 1.2a.08.08 0 0 1 .04.06v5.76a4.65 4.65 0 0 1-4.64 4.66Zm-9.95-4.27a4.63 4.63 0 0 1-.55-3.12l.14.09 4.93 2.85a.82.82 0 0 0 .82 0l6.02-3.47v2.4a.08.08 0 0 1-.03.06L10.06 20a4.65 4.65 0 0 1-6.37-1.77Zm-1.28-10.8a4.63 4.63 0 0 1 2.42-2.04v5.88a.82.82 0 0 0 .41.71l6.01 3.47-2.08 1.2a.08.08 0 0 1-.08 0L4.22 13.7a4.65 4.65 0 0 1-.81-6.27Zm17.09 3.99-6.02-3.48L15.56 7a.08.08 0 0 1 .08 0l4.87 2.81a4.64 4.64 0 0 1-.72 8.38v-5.88a.82.82 0 0 0-.39-.69Zm2.07-3.14-.14-.09-4.92-2.87a.82.82 0 0 0-.83 0L9.67 9.79V7.4a.08.08 0 0 1 .03-.06L14.6 4.5a4.64 4.64 0 0 1 6.9 4.81l.07-.03Zm-13.03 4.28-2.08-1.2a.08.08 0 0 1-.04-.06V5.5a4.64 4.64 0 0 1 7.62-3.56l-.15.08L7.9 4.87a.82.82 0 0 0-.41.71l-.01 6.98Zm1.13-2.43 2.68-1.55 2.68 1.55v3.1l-2.68 1.54-2.68-1.54v-3.1Z\"/>\n </svg>\n <svg width=\"16\" height=\"16\" fill=\"var(--text)\" viewBox=\"0 0 16 16\" style=\"vertical-align:middle;margin-right:6px\"><path d=\"M14.949 6.547a3.94 3.94 0 0 0-.348-3.273 4.11 4.11 0 0 0-4.4-1.934A4.1 4.1 0 0 0 8.423.2 4.15 4.15 0 0 0 6.305.086a4.1 4.1 0 0 0-1.891.948 4.04 4.04 0 0 0-1.158 1.753 4.1 4.1 0 0 0-1.563.679A4 4 0 0 0 .554 4.72a3.99 3.99 0 0 0 .502 4.731 3.94 3.94 0 0 0 .346 3.274 4.11 4.11 0 0 0 4.402 1.933c.382.425.852.764 1.377.995.526.231 1.095.35 1.67.346 1.78.002 3.358-1.132 3.901-2.804a4.1 4.1 0 0 0 1.563-.68 4 4 0 0 0 1.14-1.253 3.99 3.99 0 0 0-.506-4.716m-6.097 8.406a3.05 3.05 0 0 1-1.945-.694l.096-.054 3.23-1.838a.53.53 0 0 0 .265-.455v-4.49l1.366.778q.02.011.025.035v3.722c-.003 1.653-1.361 2.992-3.037 2.996m-6.53-2.75a2.95 2.95 0 0 1-.36-2.01l.095.057 3.233 1.84a.53.53 0 0 0 .527 0l3.949-2.246v1.555a.05.05 0 0 1-.022.041L6.473 13.3c-1.454.826-3.311.335-4.15-1.098m-.85-6.94A3.02 3.02 0 0 1 3.07 3.949v3.785a.51.51 0 0 0 .262.451l3.93 2.237-1.366.779a.05.05 0 0 1-.048 0L2.585 9.342a2.98 2.98 0 0 1-1.113-4.094zm11.216 2.571L8.747 5.576l1.362-.776a.05.05 0 0 1 .048 0l3.265 1.86a3 3 0 0 1 1.173 1.207 2.96 2.96 0 0 1-.27 3.2 3.05 3.05 0 0 1-1.36.997V8.279a.52.52 0 0 0-.276-.445m1.36-2.015-.097-.057-3.226-1.855a.53.53 0 0 0-.53 0L6.249 6.153V4.598a.04.04 0 0 1 .019-.04L9.533 2.7a3.07 3.07 0 0 1 3.257.139c.474.325.843.778 1.066 1.303.223.526.289 1.103.191 1.664zM5.503 8.575 4.139 7.8a.05.05 0 0 1-.026-.037V4.049c0-.57.166-1.127.476-1.607s.752-.864 1.275-1.105a3.08 3.08 0 0 1 3.234.41l-.096.054-3.23 1.838a.53.53 0 0 0-.265.455zm.742-1.577 1.758-1 1.762 1v2l-1.755 1-1.762-1z\"/></svg><span class=\"limits-cli-name\">OpenAI \u00B7 Codex</span>\n <span class=\"limits-cli-badge none\" id=\"oai-badge\">no data yet</span>\n </div>\n <div class=\"limits-gauge-grid\">\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\">\n <span id=\"oai-tok-label\">Session window</span>\n <span id=\"oai-tok-pct\" style=\"color:var(--muted)\">\u2014</span>\n </div>\n <div class=\"limits-gauge-bar\"><div class=\"limits-gauge-fill\" id=\"oai-tok-fill\" style=\"width:0%\"></div></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"oai-tok-rem\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"oai-tok-reset\"></span>\n </div>\n </div>\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\">\n <span id=\"oai-req-label\">Weekly window</span>\n <span id=\"oai-req-pct\" style=\"color:var(--muted)\">\u2014</span>\n </div>\n <div class=\"limits-gauge-bar\"><div class=\"limits-gauge-fill\" id=\"oai-req-fill\" style=\"width:0%\"></div></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"oai-req-rem\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"oai-req-reset\"></span>\n </div>\n </div>\n </div>\n <div class=\"limits-billing-row\" id=\"oai-billing-row\" style=\"display:none\">\n <div class=\"limits-credit-card\">\n <div class=\"limits-credit-label\">Credits remaining</div>\n <div class=\"limits-credit-val\" id=\"oai-credits\">\u2014</div>\n </div>\n <div class=\"limits-credit-card\">\n <div class=\"limits-credit-label\">Hard limit</div>\n <div class=\"limits-credit-val\" style=\"color:var(--yellow)\" id=\"oai-hard-lim\">\u2014</div>\n </div>\n </div>\n <div class=\"limits-usage-row\">\n <div class=\"limits-usage-item\">Session input: <span id=\"oai-u-inp-s\">\u2014</span></div>\n <div class=\"limits-usage-item\">Session output: <span id=\"oai-u-out-s\">\u2014</span></div>\n <div class=\"limits-usage-item\">Today input: <span id=\"oai-u-inp-d\">\u2014</span></div>\n <div class=\"limits-usage-item\">Today output: <span id=\"oai-u-out-d\">\u2014</span></div>\n <div class=\"limits-usage-item\" style=\"margin-left:auto\">\n <a href=\"https://platform.openai.com/usage\" target=\"_blank\" style=\"color:var(--muted);font-size:11px\">View billing \u2197</a>\n </div>\n </div>\n </div>\n\n <!-- Gemini -->\n <div class=\"limits-cli-section\" id=\"lim-gemini\">\n <div class=\"limits-cli-header\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"currentColor\" style=\"color:var(--blue)\">\n <path d=\"M12 0C5.37 0 0 5.37 0 12s5.37 12 12 12 12-5.37 12-12S18.63 0 12 0zm0 22C6.49 22 2 17.51 2 12S6.49 2 12 2s10 4.49 10 10-4.49 10-10 10zm-1-14h2v7h-2zm0 9h2v2h-2z\"/>\n </svg>\n <svg width=\"16\" height=\"16\" fill=\"var(--text)\" viewBox=\"0 0 16 16\" style=\"vertical-align:middle;margin-right:6px\"><path d=\"M15.545 6.558a9.4 9.4 0 0 1 .139 1.626c0 2.434-.87 4.492-2.384 5.885h.002C11.978 15.292 10.158 16 8 16A8 8 0 1 1 8 0a7.7 7.7 0 0 1 5.352 2.082l-2.284 2.284A4.35 4.35 0 0 0 8 3.166c-2.087 0-3.86 1.408-4.492 3.304a4.8 4.8 0 0 0 0 3.063h.003c.635 1.893 2.405 3.301 4.492 3.301 1.078 0 2.004-.276 2.722-.764h-.003a3.7 3.7 0 0 0 1.599-2.431H8v-3.08z\"/></svg><span class=\"limits-cli-name\">Google \u00B7 Gemini CLI</span>\n <span class=\"limits-cli-badge warn\" id=\"gem-badge\">only on 429 errors</span>\n </div>\n <div id=\"gem-nodata\" class=\"limits-no-data\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" style=\"margin-bottom:6px;display:block;margin-inline:auto;opacity:.4\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"/><line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"/>\n </svg>\n Google does not expose quota headers on successful responses.<br>\n Data appears here only after a 429 rate-limit error.<br>\n <a href=\"https://aistudio.google.com/app/usage\" target=\"_blank\" style=\"margin-top:8px;display:inline-block\">View quotas in AI Studio \u2197</a>\n </div>\n <div id=\"gem-data\" style=\"display:none\">\n <div class=\"limits-gauge-grid\">\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\"><span>Last known token limit</span></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"gem-tok-lim\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"gem-errors\">0 errors</span>\n </div>\n </div>\n </div>\n </div>\n <div class=\"limits-usage-row\">\n <div class=\"limits-usage-item\">Session input: <span id=\"gem-u-inp-s\">\u2014</span></div>\n <div class=\"limits-usage-item\">Session output: <span id=\"gem-u-out-s\">\u2014</span></div>\n <div class=\"limits-usage-item\">Today input: <span id=\"gem-u-inp-d\">\u2014</span></div>\n <div class=\"limits-usage-item\">Today output: <span id=\"gem-u-out-d\">\u2014</span></div>\n <div class=\"limits-usage-item\" style=\"margin-left:auto\">\n <a href=\"https://aistudio.google.com/app/usage\" target=\"_blank\" style=\"color:var(--muted);font-size:11px\">View quotas \u2197</a>\n </div>\n </div>\n </div>\n\n <!-- Personal budget -->\n <div class=\"limits-cli-section\" style=\"margin-bottom:0\">\n <div class=\"limits-cli-header\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/>\n <path d=\"M12 8v4l3 3\"/>\n </svg>\n <span class=\"limits-cli-name\">Personal daily budget</span>\n <span class=\"limits-cli-badge none\">optional</span>\n </div>\n <div class=\"limits-budget-row\">\n <input class=\"limits-budget-input\" id=\"budget-input\" type=\"number\" placeholder=\"e.g. 5000000\" min=\"0\">\n <span class=\"limits-budget-label\">tokens / day</span>\n <button class=\"btn-save\" id=\"budget-save\" style=\"padding:4px 12px;font-size:11px;border-radius:6px;border:1px solid var(--border);background:var(--bg3);color:var(--muted);cursor:pointer;transition:all .15s\" onmouseover=\"this.style.borderColor='var(--blue)';this.style.color='var(--text)'\" onmouseout=\"this.style.borderColor='var(--border)';this.style.color='var(--muted)'\">Save</button>\n </div>\n <div id=\"budget-bar-wrap\" style=\"margin-top:10px;display:none\">\n <div style=\"display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-bottom:5px\">\n <span>Tokens used today through Squeezr</span>\n <span id=\"budget-pct-label\">0%</span>\n </div>\n <div class=\"limits-gauge-bar\" style=\"height:10px\">\n <div class=\"limits-gauge-fill\" id=\"budget-bar\" style=\"width:0%\"></div>\n </div>\n <div style=\"display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-top:4px\">\n <span id=\"budget-used-label\">0 used</span>\n <span id=\"budget-limit-label\">of \u2014</span>\n </div>\n </div>\n </div>\n\n </div>\n\n <!-- \u2500\u2500\u2500 Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"page\" id=\"page-settings\">\n <div class=\"section\" style=\"margin-bottom:14px\">\n <div class=\"section-title\">Compression mode</div>\n <div class=\"mode-btns\">\n <button class=\"mode-btn\" data-mode=\"soft\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <path d=\"M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2.5 2.5 0 1 1 19.5 12H2\"/>\n </svg>\n Soft\n </button>\n <button class=\"mode-btn active\" data-mode=\"normal\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"4\" y1=\"21\" x2=\"4\" y2=\"14\"/><line x1=\"4\" y1=\"10\" x2=\"4\" y2=\"3\"/>\n <line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"12\"/><line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"3\"/>\n <line x1=\"20\" y1=\"21\" x2=\"20\" y2=\"16\"/><line x1=\"20\" y1=\"12\" x2=\"20\" y2=\"3\"/>\n <line x1=\"1\" y1=\"14\" x2=\"7\" y2=\"14\"/><line x1=\"9\" y1=\"8\" x2=\"15\" y2=\"8\"/>\n <line x1=\"17\" y1=\"16\" x2=\"23\" y2=\"16\"/>\n </svg>\n Normal\n </button>\n <button class=\"mode-btn\" data-mode=\"aggressive\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/>\n </svg>\n Aggressive\n </button>\n <button class=\"mode-btn\" data-mode=\"critical\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/>\n <line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/>\n <line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n Critical\n </button>\n </div>\n <div id=\"mode-desc\">Normal \u2014 threshold 800 chars, last 3 results uncompressed</div>\n </div>\n\n <div class=\"section\">\n <div class=\"section-title\">Configuration</div>\n <div id=\"config-rows\">\n <div class=\"config-row\"><span class=\"config-key\">Mode</span><span class=\"config-val\" id=\"cfg-mode\">\u2014</span></div>\n <div class=\"config-row\"><span class=\"config-key\">Port</span><span class=\"config-val\" id=\"cfg-port\">\u2014</span></div>\n <div class=\"config-row\"><span class=\"config-key\">Dry-run</span><span class=\"config-val\" id=\"cfg-dryrun\">\u2014</span></div>\n <div class=\"config-row\"><span class=\"config-key\">LRU cache entries</span><span class=\"config-val\" id=\"cfg-lru\">\u2014</span></div>\n <div class=\"config-row\"><span class=\"config-key\">Session cache entries</span><span class=\"config-val\" id=\"cfg-scache\">\u2014</span></div>\n <div class=\"config-row\"><span class=\"config-key\">Version</span><span class=\"config-val\" id=\"cfg-version\">\u2014</span></div>\n </div>\n </div>\n\n <div class=\"section\" style=\"margin-bottom:0\">\n <div class=\"section-title\">Links</div>\n <div style=\"display:flex;gap:16px;flex-wrap:wrap;font-size:12px\">\n <a href=\"/squeezr/stats\" target=\"_blank\">/squeezr/stats JSON</a>\n <a href=\"/squeezr/history\" target=\"_blank\">/squeezr/history JSON</a>\n <a href=\"/squeezr/projects\" target=\"_blank\">/squeezr/projects JSON</a>\n <a href=\"https://github.com/sergioramosv/Squeezr\" target=\"_blank\">GitHub</a>\n </div>\n </div>\n </div>\n\n </div><!-- /pages -->\n\n <div id=\"footer\">\n <span>Squeezr v<span id=\"f-version\">\u2014</span></span>\n <span id=\"f-mode\">mode: active</span>\n <span id=\"f-port\"></span>\n <span id=\"conn-status\" style=\"margin-left:auto;color:var(--green)\">\u25CF connected</span>\n </div>\n</div><!-- /content -->\n\n</div><!-- /app -->\n\n<script>\n// \u2500\u2500 Sparkline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst MAX_PTS = 60\nconst sparkData = []\nlet lastTokens = 0\nfunction pushSpark(t) {\n sparkData.push(Math.max(0, t - lastTokens))\n lastTokens = t\n if (sparkData.length > MAX_PTS) sparkData.shift()\n}\nfunction drawSpark() {\n const cv = document.getElementById('sparkline')\n if (!cv) return\n const dpr = window.devicePixelRatio || 1\n const r = cv.getBoundingClientRect()\n cv.width = r.width * dpr; cv.height = r.height * dpr\n const ctx = cv.getContext('2d')\n ctx.scale(dpr, dpr)\n const w = r.width, h = r.height\n const mx = Math.max(...sparkData, 1)\n ctx.clearRect(0, 0, w, h)\n if (sparkData.length < 2) return\n const step = w / (MAX_PTS - 1)\n ctx.beginPath(); ctx.moveTo(0, h)\n sparkData.forEach((v, i) => ctx.lineTo(i * step, h - (v / mx) * (h - 4)))\n ctx.lineTo((sparkData.length - 1) * step, h)\n ctx.closePath()\n const g = ctx.createLinearGradient(0, 0, 0, h)\n g.addColorStop(0, 'rgba(63,185,80,.3)'); g.addColorStop(1, 'rgba(63,185,80,0)')\n ctx.fillStyle = g; ctx.fill()\n ctx.beginPath()\n sparkData.forEach((v, i) => {\n const x = i * step, y = h - (v / mx) * (h - 4)\n i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)\n })\n ctx.strokeStyle = '#3fb950'; ctx.lineWidth = 1.5; ctx.stroke()\n}\nwindow.addEventListener('resize', drawSpark)\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction fmtN(n) {\n if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'\n if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'\n return String(n)\n}\nfunction fmtCost(tok) {\n const u = (tok / 1e6) * 3\n return u < 0.01 ? '<$0.01' : u < 1 ? '$' + u.toFixed(3) : '$' + u.toFixed(2)\n}\nfunction fmtUptime(s) {\n if (s < 60) return s + 's'\n if (s < 3600) return Math.floor(s / 60) + 'm ' + (s % 60) + 's'\n return Math.floor(s / 3600) + 'h ' + Math.floor((s % 3600) / 60) + 'm'\n}\nfunction fmtTs(ms) {\n if (!ms) return '\u2014'\n const d = new Date(ms)\n return d.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})\n}\nfunction fmtTime(ms) {\n if (!ms) return '\u2014'\n const d = new Date(ms)\n return d.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false})\n}\nfunction fmtDur(startMs, endMs) {\n const s = Math.round((endMs - startMs) / 1000)\n if (s < 60) return s + 's'\n if (s < 3600) return Math.floor(s / 60) + 'm ' + (s % 60) + 's'\n return Math.floor(s / 3600) + 'h ' + Math.floor((s % 3600) / 60) + 'm'\n}\nfunction timeAgo(ms) {\n if (!ms) return ''\n const diff = Math.round((Date.now() - ms) / 1000)\n if (diff < 60) return 'just now'\n if (diff < 3600) return Math.floor(diff / 60) + 'm ago'\n if (diff < 86400) return Math.floor(diff / 3600) + 'h ago'\n if (diff < 172800) return 'yesterday'\n return Math.floor(diff / 86400) + 'd ago'\n}\nfunction barColor(p) {\n if (p >= 90) return 'var(--red)'\n if (p >= 75) return 'var(--yellow)'\n if (p >= 50) return 'var(--orange)'\n return 'var(--blue)'\n}\nfunction setBar(bid, vid, pct, label, noColor) {\n const b = document.getElementById(bid), v = document.getElementById(vid)\n b.style.width = Math.min(pct, 100) + '%'\n if (!noColor) b.style.background = barColor(pct)\n v.textContent = label\n}\nconst PROJECT_COLORS = ['#58a6ff','#3fb950','#ffa657','#bc8cff','#d29922','#f85149','#79c0ff','#56d364']\nfunction projectColor(name) {\n let h = 0; for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) & 0xffff\n return PROJECT_COLORS[h % PROJECT_COLORS.length]\n}\n\n// \u2500\u2500 Overview render \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction renderOverview(d) {\n document.getElementById('c-tokens').textContent = fmtN(d.total_saved_tokens)\n document.getElementById('c-chars').textContent = (d.total_saved_chars || 0).toLocaleString() + ' chars'\n document.getElementById('c-pct').textContent = (d.savings_pct || 0) + '%'\n document.getElementById('c-req').textContent = fmtN(d.requests || 0)\n document.getElementById('c-compressions').textContent = (d.compressions || 0) + ' compressions'\n document.getElementById('c-cost').textContent = fmtCost(d.total_saved_tokens || 0)\n document.getElementById('f-version').textContent = d.version || '\u2014'\n document.getElementById('sb-ver').textContent = 'v' + (d.version || '\u2014')\n document.getElementById('f-mode').textContent = 'mode: ' + (d.dry_run ? 'dry-run' : 'active')\n document.getElementById('f-port').textContent = 'port: ' + (d.port || '\u2014')\n // uptime removed from UI\n\n // Project badge\n const proj = d.current_project\n const badge = document.getElementById('project-badge')\n if (proj && proj !== 'unknown') {\n badge.textContent = proj\n badge.style.display = ''\n badge.style.borderColor = projectColor(proj)\n badge.style.color = projectColor(proj)\n } else {\n badge.style.display = 'none'\n }\n\n // Pressure bars\n const msgPct = Math.min(Math.round((d.last_original_chars || 0) / 80), 100)\n const outPct = Math.min(Math.round((d.last_compressed_chars || 0) / 80), 100)\n const ch = d.session_cache_hits || 0\n const cachePct = Math.round((ch / Math.max(ch + (d.compressions || 1), 1)) * 100)\n setBar('bar-msg', 'pct-msg', msgPct, msgPct + '%')\n setBar('bar-out', 'pct-out', outPct, outPct + '%')\n setBar('bar-cache', 'pct-cache', cachePct, ch, true)\n\n // Sparkline\n pushSpark(d.total_saved_tokens || 0)\n drawSpark()\n\n // Tool table\n const bt = d.by_tool || {}\n const rows = Object.entries(bt).sort((a, b) => b[1].saved_tokens - a[1].saved_tokens)\n const maxSaved = rows[0]?.[1]?.saved_tokens || 1\n const tbody = document.getElementById('tools-body')\n if (rows.length === 0) {\n tbody.innerHTML = '<tr><td colspan=\"4\" style=\"color:var(--muted);padding:14px 8px;text-align:center\">No tool results compressed yet\u2026</td></tr>'\n } else {\n tbody.innerHTML = rows.map(([tool, t]) => {\n const bw = Math.round((t.saved_tokens / maxSaved) * 72)\n return `<tr>\n <td><code class=\"tag\">${tool}</code></td>\n <td class=\"td-right\" style=\"color:var(--muted)\">${t.count}</td>\n <td class=\"td-right\">${fmtN(t.saved_tokens)}</td>\n <td><span class=\"mini-bar\" style=\"width:${bw}px;background:var(--green)\"></span>${t.avg_pct}%</td>\n </tr>`\n }).join('')\n }\n\n // Cache stats\n document.getElementById('c-scache').textContent = d.session_cache_size ?? '\u2014'\n document.getElementById('c-expand').textContent = d.expand_store_size ?? '\u2014'\n document.getElementById('c-lru').textContent = d.cache?.size ?? '\u2014'\n document.getElementById('c-patterns').textContent = d.pattern_hits\n ? Object.values(d.pattern_hits).reduce((s, v) => s + v, 0).toLocaleString()\n : '\u2014'\n\n // Settings config panel\n document.getElementById('cfg-mode').textContent = d.mode || '\u2014'\n document.getElementById('cfg-port').textContent = d.port || '\u2014'\n document.getElementById('cfg-dryrun').textContent = d.dry_run ? 'yes' : 'no'\n document.getElementById('cfg-lru').textContent = d.cache?.size ?? '\u2014'\n document.getElementById('cfg-scache').textContent = d.session_cache_size ?? '\u2014'\n document.getElementById('cfg-version').textContent = d.version || '\u2014'\n\n // Sync active mode button\n document.querySelectorAll('.mode-btn').forEach(b => {\n b.classList.toggle('active', b.dataset.mode === d.mode)\n })\n const modeMap = {\n soft: 'Soft \u2014 threshold 3000 chars, last 10 results uncompressed, no AI',\n normal: 'Normal \u2014 threshold 800 chars, last 3 results uncompressed',\n aggressive: 'Aggressive \u2014 threshold 200 chars, last 1 result uncompressed',\n critical: 'Critical \u2014 threshold 50 chars, everything compressed'\n }\n document.getElementById('mode-desc').textContent = modeMap[d.mode] || ''\n\n // Savings breakdown\n const bd = d.breakdown\n if (bd) {\n const fmtC = (n) => n > 0 ? '-' + fmtN(n) : '0'\n document.getElementById('bd-det').textContent = fmtC(bd.deterministic)\n document.getElementById('bd-ai').textContent = fmtC(bd.ai_compression)\n document.getElementById('bd-dedup').textContent = fmtC(bd.read_dedup)\n document.getElementById('bd-sysprompt').textContent = fmtC(bd.system_prompt)\n document.getElementById('bd-overhead').textContent = bd.overhead > 0 ? '+' + fmtN(bd.overhead) : '0'\n document.getElementById('bd-aicalls').textContent = bd.ai_calls > 0 ? bd.ai_calls + ' calls' : '0'\n }\n}\n\n// \u2500\u2500 Projects page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nasync function loadProjects() {\n try {\n const r = await fetch('/squeezr/projects')\n const { projects } = await r.json()\n const tbody = document.getElementById('projects-body')\n const entries = Object.entries(projects).sort((a, b) => b[1].savedTokens - a[1].savedTokens)\n if (entries.length === 0) {\n tbody.innerHTML = '<tr><td colspan=\"5\" style=\"color:var(--muted);padding:20px 8px;text-align:center\">No project data yet \u2014 start making requests.</td></tr>'\n return\n }\n tbody.innerHTML = entries.map(([name, p]) => `<tr>\n <td><span class=\"project-dot\" style=\"background:${projectColor(name)}\"></span><code>${name}</code></td>\n <td class=\"td-right\" style=\"color:var(--muted)\">${p.sessions}</td>\n <td class=\"td-right\">${p.requests}</td>\n <td class=\"td-right\" style=\"color:var(--green)\">${fmtN(p.savedTokens)}</td>\n <td class=\"td-right\" style=\"color:var(--muted);font-size:11px\">${p.lastSeen ? fmtTs(p.lastSeen) : '\u2014'}</td>\n </tr>`).join('')\n } catch {\n document.getElementById('projects-body').innerHTML = '<tr><td colspan=\"5\" style=\"color:var(--muted);padding:20px 8px;text-align:center\">Failed to load projects.</td></tr>'\n }\n}\n\n// \u2500\u2500 History page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet histData = null\nlet selectedHistProj = '__all__'\n\nasync function loadHistory() {\n try {\n const r = await fetch('/squeezr/history')\n histData = await r.json()\n renderHistProjects()\n renderHistSessions()\n } catch {\n document.getElementById('hist-proj-list').innerHTML = '<div class=\"empty-msg\">Failed to load history.</div>'\n }\n}\n\nfunction renderHistProjects() {\n if (!histData) return\n const all = [...histData.sessions]\n if (histData.current && histData.current.requests > 0) {\n const idx = all.findIndex(s => s.id === histData.current.id)\n if (idx >= 0) all[idx] = histData.current; else all.push(histData.current)\n }\n\n // Group by project\n const byProj = {}\n for (const s of all) {\n if (!byProj[s.project]) byProj[s.project] = 0\n byProj[s.project]++\n }\n\n const list = document.getElementById('hist-proj-list')\n let html = `<div class=\"hist-proj-item${selectedHistProj === '__all__' ? ' active' : ''}\" data-proj=\"__all__\">\n <span>All projects</span>\n <span class=\"hist-proj-count\">${all.length}</span>\n </div>`\n for (const [name, cnt] of Object.entries(byProj).sort((a, b) => b[1] - a[1])) {\n const active = selectedHistProj === name ? ' active' : ''\n html += `<div class=\"hist-proj-item${active}\" data-proj=\"${name}\">\n <span><span class=\"project-dot\" style=\"background:${projectColor(name)}\"></span>${name}</span>\n <span class=\"hist-proj-count\">${cnt}</span>\n </div>`\n }\n list.innerHTML = html\n\n list.querySelectorAll('.hist-proj-item').forEach(el => {\n el.addEventListener('click', () => {\n selectedHistProj = el.dataset.proj\n list.querySelectorAll('.hist-proj-item').forEach(x => x.classList.remove('active'))\n el.classList.add('active')\n renderHistSessions()\n })\n })\n}\n\nfunction renderHistSessions() {\n if (!histData) return\n let sessions = [...histData.sessions]\n if (histData.current && histData.current.requests > 0) {\n const idx = sessions.findIndex(s => s.id === histData.current.id)\n if (idx >= 0) sessions[idx] = histData.current; else sessions.push(histData.current)\n }\n // Filter empty sessions and sort newest first\n sessions = sessions.filter(s => s.requests > 0)\n sessions.sort((a, b) => b.startTime - a.startTime)\n\n if (selectedHistProj !== '__all__') {\n sessions = sessions.filter(s => s.project === selectedHistProj)\n }\n\n const header = document.getElementById('hist-sessions-header')\n header.textContent = selectedHistProj === '__all__'\n ? `All sessions (${sessions.length})`\n : `${selectedHistProj} \u2014 ${sessions.length} session${sessions.length !== 1 ? 's' : ''}`\n\n const list = document.getElementById('hist-sessions-list')\n if (sessions.length === 0) {\n list.innerHTML = '<div class=\"empty-msg\">No sessions found.</div>'\n return\n }\n\n // Group by day\n const byDay = {}\n for (const s of sessions) {\n const day = new Date(s.startTime).toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric',year:'numeric'})\n if (!byDay[day]) byDay[day] = []\n byDay[day].push(s)\n }\n\n let html = ''\n for (const [day, daySessions] of Object.entries(byDay)) {\n html += `<div style=\"padding:8px 16px;font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;background:var(--bg3);border-bottom:1px solid var(--border)\">${day}</div>`\n for (const s of daySessions) {\n const isCurrent = s.id === histData.current?.id\n const projBadge = selectedHistProj === '__all__' ? `<span class=\"session-project-badge\">${s.project}</span>` : ''\n html += `<div class=\"session-card\">\n <div class=\"session-date\">\n ${fmtTime(s.startTime)} \u2192 ${fmtTime(s.endTime)}\n <span style=\"color:var(--muted);font-weight:400\"> (${fmtDur(s.startTime, s.endTime)})</span>\n <span style=\"color:var(--muted);font-weight:400;margin-left:6px\">${timeAgo(s.endTime)}</span>\n ${isCurrent ? '<span style=\"font-size:10px;color:var(--green);margin-left:8px\">\u25CF active</span>' : ''}\n ${projBadge}\n </div>\n <div class=\"session-stats\">\n <div class=\"session-stat\">Requests: <span>${s.requests}</span></div>\n <div class=\"session-stat\">Tokens saved: <span style=\"color:var(--green)\">${fmtN(s.savedTokens)}</span></div>\n <div class=\"session-stat\">Compressions: <span>${s.compressions}</span></div>\n </div>\n </div>`\n }\n }\n list.innerHTML = html\n}\n\n// \u2500\u2500 Limits page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet limitsCountdownTimer = null\n\nfunction fmtTokens(n) {\n if (!n && n !== 0) return '\u2014'\n if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M'\n if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'\n return String(n)\n}\n\nfunction gaugeColor(pct) {\n if (pct >= 90) return 'var(--red)'\n if (pct >= 70) return 'var(--yellow)'\n if (pct >= 40) return 'var(--orange)'\n return 'var(--green)'\n}\n\nfunction fillGauge(fillId, pctId, remId, resetId, remaining, limit, resetEpoch) {\n if (!limit) {\n document.getElementById(fillId).style.width = '0%'\n document.getElementById(pctId).textContent = '\u2014'\n document.getElementById(remId).textContent = '\u2014'\n if (resetId) document.getElementById(resetId).textContent = ''\n return\n }\n const used = limit - remaining\n const pct = Math.max(0, Math.min(100, Math.round((used / limit) * 100)))\n const fill = document.getElementById(fillId)\n fill.style.width = pct + '%'\n fill.style.background = gaugeColor(pct)\n document.getElementById(pctId).textContent = pct + '% used'\n document.getElementById(pctId).style.color = gaugeColor(pct)\n document.getElementById(remId).textContent = fmtTokens(remaining) + ' remaining'\n if (resetId && resetEpoch) {\n const secs = Math.max(0, Math.round((resetEpoch - Date.now()) / 1000))\n document.getElementById(resetId).textContent = secs > 0 ? 'resets in ' + secs + 's' : 'resetting\u2026'\n }\n}\n\nfunction formatResetCountdown(resetEpoch) {\n const secs = resetEpoch > 0 ? Math.max(0, Math.round((resetEpoch - Date.now()) / 1000)) : 0\n if (secs <= 0) return ''\n if (secs >= 86400) return Math.floor(secs / 86400) + 'd ' + Math.floor((secs % 86400) / 3600) + 'h'\n if (secs >= 3600) return Math.floor(secs / 3600) + 'h ' + Math.floor((secs % 3600) / 60) + 'm'\n if (secs >= 60) return Math.floor(secs / 60) + 'm'\n return secs + 's'\n}\n\nfunction fillPercentGauge(fillId, pctId, remId, resetId, usedPercent, resetEpoch) {\n if (usedPercent == null) {\n document.getElementById(fillId).style.width = '0%'\n document.getElementById(pctId).textContent = '\u00E2\u20AC\u201D'\n document.getElementById(remId).textContent = '\u00E2\u20AC\u201D'\n if (resetId) document.getElementById(resetId).textContent = ''\n return\n }\n const pct = Math.max(0, Math.min(100, Math.round(usedPercent || 0)))\n const fill = document.getElementById(fillId)\n fill.style.width = pct + '%'\n fill.style.background = gaugeColor(pct)\n document.getElementById(pctId).textContent = pct + '%'\n document.getElementById(pctId).style.color = gaugeColor(pct)\n document.getElementById(remId).textContent = pct >= 80 ? pct + '% used' : (100 - pct) + '% free'\n const resetStr = formatResetCountdown(resetEpoch)\n if (resetId) document.getElementById(resetId).textContent = resetStr ? 'resets in ' + resetStr : ''\n}\n\nfunction renderLimits(d) {\n if (!d) return\n const { anthropic, openai, gemini } = d\n\n // \u2500\u2500 Anthropic \u2500\u2500\n const arl = anthropic?.rl\n const au = anthropic?.usage\n const antHasUsage = au && (au.inputSession > 0 || au.outputSession > 0)\n if (arl?.hasData) {\n document.getElementById('ant-badge').className = 'limits-cli-badge live'\n document.getElementById('ant-badge').textContent = 'live'\n fillGauge('ant-tok-fill','ant-tok-pct','ant-tok-rem','ant-tok-reset', arl.tokensRemaining, arl.tokensLimit, arl.tokensResetEpoch)\n fillGauge('ant-req-fill','ant-req-pct','ant-req-rem','ant-req-reset', arl.requestsRemaining, arl.requestsLimit, arl.requestsResetEpoch)\n fillGauge('ant-inp-fill','ant-inp-pct','ant-inp-rem','ant-inp-reset', arl.inputTokensRemaining, arl.inputTokensLimit, arl.tokensResetEpoch)\n fillGauge('ant-out-fill','ant-out-pct','ant-out-rem','ant-out-reset', arl.outputTokensRemaining, arl.outputTokensLimit, arl.tokensResetEpoch)\n } else if (anthropic?.unified?.hasData) {\n // Subscription (OAuth): unified rate limits with 5h/7d windows\n const u = anthropic.unified\n document.getElementById('ant-badge').className = 'limits-cli-badge live'\n document.getElementById('ant-badge').textContent = 'subscription'\n // Relabel gauges for subscription windows\n document.getElementById('ant-tok-label').textContent = '5-hour window'\n document.getElementById('ant-req-label').textContent = '7-day window'\n document.getElementById('ant-inp-label').textContent = 'Session input'\n document.getElementById('ant-out-label').textContent = 'Session output'\n // 5-hour window\n const pct5h = Math.round(u.fiveHourUtilization * 100)\n document.getElementById('ant-tok-fill').style.width = pct5h + '%'\n document.getElementById('ant-tok-fill').style.background = gaugeColor(pct5h)\n document.getElementById('ant-tok-pct').textContent = pct5h + '%'\n document.getElementById('ant-tok-pct').style.color = gaugeColor(pct5h)\n const secs5h = u.fiveHourResetEpoch > 0 ? Math.max(0, Math.round((u.fiveHourResetEpoch - Date.now()) / 1000)) : 0\n const resetStr5h = secs5h > 3600 ? Math.floor(secs5h/3600) + 'h ' + Math.floor((secs5h%3600)/60) + 'm'\n : secs5h > 60 ? Math.floor(secs5h/60) + 'm'\n : secs5h > 0 ? secs5h + 's' : ''\n document.getElementById('ant-tok-rem').textContent = u.fiveHourStatus === 'allowed'\n ? (pct5h >= 80 ? pct5h + '% used' : (100-pct5h) + '% free')\n : 'throttled \u2014 resets in ' + resetStr5h\n document.getElementById('ant-tok-reset').textContent = resetStr5h ? 'resets in ' + resetStr5h : ''\n // 7-day window\n const pct7d = Math.round(u.sevenDayUtilization * 100)\n document.getElementById('ant-req-fill').style.width = pct7d + '%'\n document.getElementById('ant-req-fill').style.background = gaugeColor(pct7d)\n document.getElementById('ant-req-pct').textContent = pct7d + '%'\n document.getElementById('ant-req-pct').style.color = gaugeColor(pct7d)\n const secs7d = u.sevenDayResetEpoch > 0 ? Math.max(0, Math.round((u.sevenDayResetEpoch - Date.now()) / 1000)) : 0\n const resetStr7d = secs7d > 86400 ? Math.floor(secs7d/86400) + 'd ' + Math.floor((secs7d%86400)/3600) + 'h'\n : secs7d > 3600 ? Math.floor(secs7d/3600) + 'h ' + Math.floor((secs7d%3600)/60) + 'm'\n : secs7d > 0 ? Math.floor(secs7d/60) + 'm' : ''\n document.getElementById('ant-req-rem').textContent = u.sevenDayStatus === 'allowed'\n ? (pct7d >= 80 ? pct7d + '% used' : (100-pct7d) + '% free')\n : 'throttled \u2014 resets in ' + resetStr7d\n document.getElementById('ant-req-reset').textContent = resetStr7d ? 'resets in ' + resetStr7d : ''\n // Input/output: show session totals\n document.getElementById('ant-inp-pct').textContent = fmtTokens(au?.inputSession || 0)\n document.getElementById('ant-inp-rem').textContent = 'session input'\n document.getElementById('ant-out-pct').textContent = fmtTokens(au?.outputSession || 0)\n document.getElementById('ant-out-rem').textContent = 'session output'\n } else if (antHasUsage) {\n // Fallback: no rate limit headers at all, but usage is tracked\n document.getElementById('ant-badge').className = 'limits-cli-badge live'\n document.getElementById('ant-badge').textContent = 'tracking'\n document.getElementById('ant-tok-pct').textContent = fmtTokens((au?.inputSession || 0) + (au?.outputSession || 0))\n document.getElementById('ant-tok-rem').textContent = 'session total'\n document.getElementById('ant-req-pct').textContent = (au?.requestsSession || 0) + ' reqs'\n document.getElementById('ant-req-rem').textContent = 'session total'\n document.getElementById('ant-inp-pct').textContent = fmtTokens(au?.inputSession || 0)\n document.getElementById('ant-inp-rem').textContent = 'session input'\n document.getElementById('ant-out-pct').textContent = fmtTokens(au?.outputSession || 0)\n document.getElementById('ant-out-rem').textContent = 'session output'\n }\n if (au) {\n document.getElementById('ant-u-inp-s').textContent = fmtTokens(au.inputSession)\n document.getElementById('ant-u-out-s').textContent = fmtTokens(au.outputSession)\n document.getElementById('ant-u-inp-d').textContent = fmtTokens(au.inputToday)\n document.getElementById('ant-u-out-d').textContent = fmtTokens(au.outputToday)\n }\n\n // \u2500\u2500 OpenAI \u2500\u2500\n const os = openai?.session\n const orl = openai?.rl\n const ou = openai?.usage\n const oaiHasUsage = ou && (ou.inputSession > 0 || ou.outputSession > 0)\n if (os?.hasData) {\n document.getElementById('oai-badge').className = 'limits-cli-badge live'\n document.getElementById('oai-badge').textContent = os.planType || 'session'\n document.getElementById('oai-tok-label').textContent = (os.primary?.windowDurationMins || 300) >= 300 ? '5-hour window' : 'session window'\n document.getElementById('oai-req-label').textContent = (os.secondary?.windowDurationMins || 0) >= 10080 ? '7-day window' : 'weekly window'\n fillPercentGauge('oai-tok-fill','oai-tok-pct','oai-tok-rem','oai-tok-reset', os.primary?.usedPercent, os.primary?.resetsAt || 0)\n fillPercentGauge('oai-req-fill','oai-req-pct','oai-req-rem','oai-req-reset', os.secondary?.usedPercent, os.secondary?.resetsAt || 0)\n } else if (orl?.hasData) {\n document.getElementById('oai-badge').className = 'limits-cli-badge live'\n document.getElementById('oai-badge').textContent = 'live'\n document.getElementById('oai-tok-label').textContent = 'tokens / minute'\n document.getElementById('oai-req-label').textContent = 'requests / minute'\n fillGauge('oai-tok-fill','oai-tok-pct','oai-tok-rem','oai-tok-reset', orl.tokensRemaining, orl.tokensLimit, orl.tokensResetEpoch)\n fillGauge('oai-req-fill','oai-req-pct','oai-req-rem','oai-req-reset', orl.requestsRemaining, orl.requestsLimit, orl.requestsResetEpoch)\n } else if (oaiHasUsage) {\n document.getElementById('oai-badge').className = 'limits-cli-badge live'\n document.getElementById('oai-badge').textContent = 'tracking'\n document.getElementById('oai-tok-label').textContent = 'session total'\n document.getElementById('oai-req-label').textContent = 'session requests'\n document.getElementById('oai-tok-pct').textContent = fmtTokens(ou.inputSession + ou.outputSession)\n document.getElementById('oai-tok-rem').textContent = 'session total'\n document.getElementById('oai-req-pct').textContent = ou.requestsSession + ' reqs'\n document.getElementById('oai-req-rem').textContent = 'session total'\n }\n const ob = openai?.billing\n if (ob?.hardLimitUsd > 0) {\n document.getElementById('oai-billing-row').style.display = 'flex'\n document.getElementById('oai-credits').textContent = '$' + (ob.creditBalanceUsd || 0).toFixed(2)\n document.getElementById('oai-hard-lim').textContent = '$' + ob.hardLimitUsd.toFixed(2)\n }\n if (ou) {\n document.getElementById('oai-u-inp-s').textContent = fmtTokens(ou.inputSession)\n document.getElementById('oai-u-out-s').textContent = fmtTokens(ou.outputSession)\n document.getElementById('oai-u-inp-d').textContent = fmtTokens(ou.inputToday)\n document.getElementById('oai-u-out-d').textContent = fmtTokens(ou.outputToday)\n }\n\n // \u2500\u2500 Gemini \u2500\u2500\n const ge = gemini?.errors\n const gu = gemini?.usage\n const gemHasUsage = gu && (gu.inputSession > 0 || gu.outputSession > 0)\n if (ge?.hasData) {\n document.getElementById('gem-nodata').style.display = 'none'\n document.getElementById('gem-data').style.display = 'block'\n document.getElementById('gem-tok-lim').textContent = fmtTokens(gemini.rl?.tokensLimit)\n document.getElementById('gem-errors').textContent = ge.errorCount429 + ' rate-limit errors'\n document.getElementById('gem-badge').className = 'limits-cli-badge error'\n document.getElementById('gem-badge').textContent = ge.errorCount429 + ' 429 errors'\n } else if (gemHasUsage) {\n document.getElementById('gem-nodata').style.display = 'none'\n document.getElementById('gem-data').style.display = 'block'\n document.getElementById('gem-badge').className = 'limits-cli-badge live'\n document.getElementById('gem-badge').textContent = 'tracking'\n }\n if (gu) {\n document.getElementById('gem-u-inp-s').textContent = fmtTokens(gu.inputSession)\n document.getElementById('gem-u-out-s').textContent = fmtTokens(gu.outputSession)\n document.getElementById('gem-u-inp-d').textContent = fmtTokens(gu.inputToday)\n document.getElementById('gem-u-out-d').textContent = fmtTokens(gu.outputToday)\n }\n\n // \u2500\u2500 Budget \u2500\u2500\n updateBudgetBar(au, ou, gu)\n}\n\n// Countdown ticker \u2014 updates reset countdowns every second without SSE\nfunction startLimitsCountdown(limitsData) {\n if (limitsCountdownTimer) clearInterval(limitsCountdownTimer)\n limitsCountdownTimer = setInterval(() => {\n const updateReset = (id, resetEpoch) => {\n if (!resetEpoch) return\n const el = document.getElementById(id)\n if (!el) return\n const secs = Math.max(0, Math.round((resetEpoch - Date.now()) / 1000))\n el.textContent = secs > 0 ? 'resets in ' + secs + 's' : 'resetting\u2026'\n }\n const d = limitsData\n if (d?.anthropic?.rl?.hasData) {\n updateReset('ant-tok-reset', d.anthropic.rl.tokensResetEpoch)\n updateReset('ant-req-reset', d.anthropic.rl.requestsResetEpoch)\n updateReset('ant-inp-reset', d.anthropic.rl.tokensResetEpoch)\n updateReset('ant-out-reset', d.anthropic.rl.tokensResetEpoch)\n }\n if (d?.openai?.rl?.hasData) {\n updateReset('oai-tok-reset', d.openai.rl.tokensResetEpoch)\n updateReset('oai-req-reset', d.openai.rl.requestsResetEpoch)\n }\n if (d?.openai?.session?.hasData) {\n const tok = d.openai.session.primary?.resetsAt || 0\n const req = d.openai.session.secondary?.resetsAt || 0\n updateReset('oai-tok-reset', tok)\n updateReset('oai-req-reset', req)\n }\n }, 1000)\n}\n\n// \u2500\u2500 Budget logic \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet dailyBudget = parseInt(localStorage.getItem('squeezr_budget') || '0')\n\nfunction updateBudgetBar(au, ou, gu) {\n const budget = dailyBudget\n const budgetInput = document.getElementById('budget-input')\n if (budgetInput && !budgetInput.value) budgetInput.value = budget || ''\n\n const wrap = document.getElementById('budget-bar-wrap')\n if (!budget) { wrap.style.display = 'none'; return }\n wrap.style.display = 'block'\n\n const totalToday = ((au?.inputToday || 0) + (au?.outputToday || 0) +\n (ou?.inputToday || 0) + (ou?.outputToday || 0) +\n (gu?.inputToday || 0) + (gu?.outputToday || 0))\n const pct = Math.min(100, Math.round((totalToday / budget) * 100))\n const fill = document.getElementById('budget-bar')\n fill.style.width = pct + '%'\n fill.style.background = gaugeColor(pct)\n document.getElementById('budget-pct-label').textContent = pct + '%'\n document.getElementById('budget-pct-label').style.color = gaugeColor(pct)\n document.getElementById('budget-used-label').textContent = fmtTokens(totalToday) + ' used today'\n document.getElementById('budget-limit-label').textContent = 'of ' + fmtTokens(budget) + ' / day'\n}\n\ndocument.getElementById('budget-save').addEventListener('click', () => {\n const val = parseInt(document.getElementById('budget-input').value || '0')\n dailyBudget = val\n localStorage.setItem('squeezr_budget', String(val))\n document.getElementById('budget-save').textContent = '\u2713 Saved'\n setTimeout(() => document.getElementById('budget-save').textContent = 'Save', 2000)\n // Re-render budget bar with latest limits data\n if (lastLimitsData) {\n const u = lastLimitsData.usage\n updateBudgetBar(u?.anthropic, u?.openai, u?.gemini)\n }\n})\n\n// Restore budget from localStorage on load\nconst savedBudget = localStorage.getItem('squeezr_budget')\nif (savedBudget) document.getElementById('budget-input').value = savedBudget\n\n// \u2500\u2500 Navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst pageTitles = { overview: 'Overview', projects: 'Projects', history: 'History', limits: 'Limits', settings: 'Settings' }\n\ndocument.querySelectorAll('.nav-item').forEach(item => {\n item.addEventListener('click', () => {\n const page = item.dataset.page\n document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'))\n document.querySelectorAll('.page').forEach(p => p.classList.remove('active'))\n item.classList.add('active')\n document.getElementById('page-' + page).classList.add('active')\n document.getElementById('page-title').textContent = pageTitles[page] || page\n if (page === 'projects') loadProjects()\n if (page === 'history') loadHistory()\n if (page === 'limits') {\n if (lastLimitsData) {\n renderLimits(lastLimitsData)\n startLimitsCountdown(lastLimitsData)\n }\n }\n if (page !== 'limits' && limitsCountdownTimer) {\n clearInterval(limitsCountdownTimer)\n limitsCountdownTimer = null\n }\n })\n})\n\n// \u2500\u2500 Mode selector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndocument.querySelectorAll('.mode-btn[data-mode]').forEach(btn => {\n btn.addEventListener('click', async () => {\n const mode = btn.dataset.mode\n if (!mode) return\n const prevActive = document.querySelector('.mode-btn.active')\n document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'))\n btn.classList.add('active')\n try {\n const res = await fetch('/squeezr/config', {\n method: 'POST',\n headers: {'content-type':'application/json'},\n body: JSON.stringify({ mode })\n })\n if (!res.ok) throw new Error('HTTP ' + res.status)\n } catch(e) {\n // Revert to previous mode on failure\n btn.classList.remove('active')\n if (prevActive) prevActive.classList.add('active')\n console.error('mode update failed', e)\n }\n })\n})\n\n// \u2500\u2500 SSE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst dot = document.getElementById('status-dot')\nconst statusText = document.getElementById('status-text')\nconst connPill = document.getElementById('conn-pill')\nconst connStatus = document.getElementById('conn-status')\nlet lastLimitsData = null\n\nfunction connect() {\n const es = new EventSource('/squeezr/events')\n es.onmessage = e => {\n try {\n const d = JSON.parse(e.data)\n renderOverview(d)\n if (d.limits) {\n lastLimitsData = d.limits\n // Only render limits page if it's currently visible\n const limPage = document.getElementById('page-limits')\n if (limPage && limPage.classList.contains('active')) {\n renderLimits(d.limits)\n if (!limitsCountdownTimer) startLimitsCountdown(d.limits)\n else { /* update the data reference for the countdown */ lastLimitsData = d.limits }\n }\n }\n } catch(err) { console.error(err) }\n }\n es.onopen = () => {\n dot.classList.remove('off')\n statusText.textContent = 'Connected'\n connPill.className = ''\n connPill.textContent = '\u25CF live'\n connStatus.style.color = 'var(--green)'\n connStatus.textContent = '\u25CF connected'\n }\n es.onerror = () => {\n dot.classList.add('off')\n statusText.textContent = 'Reconnecting\u2026'\n connPill.className = 'err'\n connPill.textContent = '\u25CF offline'\n connStatus.style.color = 'var(--red)'\n connStatus.textContent = '\u25CF reconnecting\u2026'\n es.close()\n setTimeout(connect, 3000)\n }\n}\nconnect()\n</script>\n</body>\n</html>";
6
+ export declare const DASHBOARD_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>Squeezr Dashboard</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#09090b;--bg2:#111113;--bg3:#1a1a1e;--bg4:#252529;\n --border:#2a2a2e;--text:#e4e4e7;--muted:#71717a;\n --green:#22c55e;--yellow:#eab308;--red:#ef4444;\n --blue:#22c55e;--purple:#a78bfa;--orange:#f59e0b;--accent:#16a34a\n}\nhtml,body{height:100%;background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,-apple-system,sans-serif;font-size:13px;line-height:1.5}\na{color:var(--blue);text-decoration:none}\ncode{font-family:'Cascadia Code','Fira Mono','Consolas',monospace}\n\n/* \u2500\u2500 App shell \u2500\u2500 */\n#app{display:flex;height:100vh;overflow:hidden}\n\n/* \u2500\u2500 Sidebar \u2500\u2500 */\n#sidebar{\n width:200px;flex-shrink:0;background:var(--bg2);\n border-right:1px solid var(--border);\n display:flex;flex-direction:column;overflow:hidden\n}\n#sidebar-brand{padding:16px 16px 12px;border-bottom:1px solid var(--border)}\n#sidebar-brand .logo{font-size:18px;font-weight:700;letter-spacing:.3px;line-height:1}\n#sidebar-brand .logo span{color:var(--blue)}\n#sidebar-brand .ver{font-size:11px;color:var(--muted);margin-top:3px}\n\nnav{flex:1;padding:8px 0;overflow-y:auto}\n.nav-item{\n display:flex;align-items:center;gap:9px;padding:8px 16px;\n color:var(--muted);cursor:pointer;border-radius:0;\n transition:background .1s,color .1s;user-select:none\n}\n.nav-item:hover{background:var(--bg3);color:var(--text)}\n.nav-item.active{background:var(--bg3);color:var(--blue)}\n.nav-item svg{flex-shrink:0;opacity:.8}\n.nav-item.active svg{opacity:1}\n.nav-label{font-size:13px}\n\n#sidebar-footer{padding:12px 16px;border-top:1px solid var(--border)}\n.status-row{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--muted)}\n.dot{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 5px var(--green);flex-shrink:0}\n.dot.off{background:var(--red);box-shadow:0 0 5px var(--red)}\n\n/* \u2500\u2500 Main content \u2500\u2500 */\n#content{flex:1;display:flex;flex-direction:column;overflow:hidden}\n#page-header{\n display:flex;align-items:center;gap:10px;padding:12px 20px;\n background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0\n}\n#page-title{font-size:15px;font-weight:600}\n#project-badge{\n font-size:11px;background:var(--bg3);border:1px solid var(--border);\n border-radius:12px;padding:2px 10px;color:var(--blue);font-weight:500\n}\n#conn-pill{\n font-size:11px;padding:2px 8px;border-radius:10px;\n background:rgba(63,185,80,.15);color:var(--green);border:1px solid rgba(63,185,80,.3)\n}\n#conn-pill.err{background:rgba(248,81,73,.15);color:var(--red);border-color:rgba(248,81,73,.3)}\n\n#pages{flex:1;overflow-y:auto;padding:16px 20px}\n.page{display:none}\n.page.active{display:block}\n\n/* \u2500\u2500 Cards \u2500\u2500 */\n.cards-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(175px,1fr));gap:10px;margin-bottom:14px}\n.card{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:14px 16px}\n.card-label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}\n.card-value{font-size:26px;font-weight:700;line-height:1.1}\n.card-sub{font-size:11px;color:var(--muted);margin-top:3px}\n.c-green .card-value{color:var(--green)}\n.c-blue .card-value{color:var(--blue)}\n.c-yellow .card-value{color:var(--yellow)}\n.c-orange .card-value{color:var(--orange)}\n\n/* \u2500\u2500 Sections \u2500\u2500 */\n.section{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin-bottom:14px}\n.section-title{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;font-weight:600}\n\n/* \u2500\u2500 Bars \u2500\u2500 */\n.bar-row{display:flex;align-items:center;gap:8px;margin-bottom:7px}\n.bar-label{font-size:12px;color:var(--muted);width:130px;flex-shrink:0}\n.bar-track{flex:1;height:7px;background:var(--bg3);border-radius:4px;overflow:hidden}\n.bar-fill{height:100%;border-radius:4px;transition:width .4s,background .4s}\n.bar-val{font-size:11px;width:36px;text-align:right;flex-shrink:0;color:var(--muted)}\n\n/* \u2500\u2500 Sparkline \u2500\u2500 */\ncanvas#sparkline{width:100%;height:72px;display:block}\n\n/* \u2500\u2500 Tables \u2500\u2500 */\ntable{width:100%;border-collapse:collapse}\nth{font-size:11px;color:var(--muted);text-align:left;padding:4px 8px;border-bottom:1px solid var(--border);font-weight:500;letter-spacing:.3px;text-transform:uppercase}\ntd{padding:6px 8px;font-size:12px;border-bottom:1px solid var(--border)}\ntr:last-child td{border-bottom:none}\n.td-right{text-align:right;font-variant-numeric:tabular-nums}\n.mini-bar{display:inline-block;height:5px;border-radius:2px;vertical-align:middle;margin-right:5px;opacity:.75}\n.tag{display:inline-block;background:var(--bg3);border:1px solid var(--border);border-radius:3px;padding:1px 6px;font-size:11px;font-family:monospace}\n\n/* \u2500\u2500 Cache row \u2500\u2500 */\n.cache-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px}\n.cache-card{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:10px 14px}\n.cache-card .cache-label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:3px}\n.cache-card .cache-val{font-size:18px;font-weight:600;color:var(--purple)}\n\n/* \u2500\u2500 Mode buttons \u2500\u2500 */\n.mode-btns{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px}\n.mode-btn{\n display:flex;align-items:center;gap:6px;padding:6px 14px;\n border-radius:6px;border:1px solid var(--border);background:var(--bg3);\n color:var(--muted);cursor:pointer;font-size:12px;transition:all .15s\n}\n.mode-btn:hover{border-color:var(--blue);color:var(--text)}\n.mode-btn.active{border-color:var(--accent);background:var(--accent);color:#fff}\n.mode-btn.active svg{stroke:white}\n#mode-desc{font-size:12px;color:var(--muted);min-height:16px}\n\n/* \u2500\u2500 Projects page \u2500\u2500 */\n.project-table td:first-child code{font-size:12px}\n.project-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:6px}\n\n/* \u2500\u2500 History page \u2500\u2500 */\n#hist-layout{display:grid;grid-template-columns:220px 1fr;gap:12px;min-height:400px}\n#hist-projects{background:var(--bg2);border:1px solid var(--border);border-radius:8px;overflow:hidden}\n#hist-sessions{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:0}\n.hist-proj-item{\n padding:9px 14px;cursor:pointer;border-bottom:1px solid var(--border);\n display:flex;justify-content:space-between;align-items:center;\n font-size:12px;color:var(--muted);transition:background .1s\n}\n.hist-proj-item:last-child{border-bottom:none}\n.hist-proj-item:hover{background:var(--bg3)}\n.hist-proj-item.active{background:var(--bg3);color:var(--blue)}\n.hist-proj-count{font-size:11px;background:var(--bg4);border-radius:10px;padding:1px 7px}\n.hist-sessions-header{padding:12px 16px;border-bottom:1px solid var(--border);font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;font-weight:600}\n.session-card{padding:12px 16px;border-bottom:1px solid var(--border)}\n.session-card:last-child{border-bottom:none}\n.session-date{font-size:12px;font-weight:600;color:var(--text);margin-bottom:4px}\n.session-time{font-size:11px;color:var(--muted);margin-bottom:6px}\n.session-stats{display:flex;gap:14px;flex-wrap:wrap}\n.session-stat{font-size:11px;color:var(--muted)}\n.session-stat span{color:var(--text);font-weight:500}\n.session-project-badge{font-size:10px;background:var(--bg4);border:1px solid var(--border);border-radius:10px;padding:1px 8px;color:var(--blue);margin-left:6px}\n.empty-msg{padding:32px 16px;text-align:center;color:var(--muted);font-size:12px}\n\n/* \u2500\u2500 Settings \u2500\u2500 */\n.config-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--border);font-size:12px}\n.config-row:last-child{border-bottom:none}\n.config-key{color:var(--muted)}\n.config-val{font-family:monospace;color:var(--text)}\n\n/* \u2500\u2500 Limits page \u2500\u2500 */\n.limits-cli-section{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin-bottom:14px}\n.limits-cli-header{display:flex;align-items:center;gap:8px;margin-bottom:12px}\n.limits-cli-name{font-size:13px;font-weight:600;color:var(--text)}\n.limits-cli-badge{font-size:10px;padding:1px 7px;border-radius:10px;border:1px solid;margin-left:2px}\n.limits-cli-badge.live{border-color:rgba(63,185,80,.4);color:var(--green);background:rgba(63,185,80,.1)}\n.limits-cli-badge.error{border-color:rgba(248,81,73,.4);color:var(--red);background:rgba(248,81,73,.1)}\n.limits-cli-badge.warn{border-color:rgba(210,153,34,.4);color:var(--yellow);background:rgba(210,153,34,.1)}\n.limits-cli-badge.none{border-color:var(--border);color:var(--muted);background:transparent}\n.limits-gauge-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin-bottom:10px}\n.limits-gauge{background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:10px 12px}\n.limits-gauge-label{font-size:11px;color:var(--muted);margin-bottom:6px;display:flex;justify-content:space-between}\n.limits-gauge-bar{height:6px;background:var(--bg4);border-radius:3px;overflow:hidden;margin-bottom:5px}\n.limits-gauge-fill{height:100%;border-radius:3px;transition:width .5s,background .5s}\n.limits-gauge-bottom{display:flex;justify-content:space-between;font-size:11px}\n.limits-gauge-remaining{color:var(--text);font-weight:500}\n.limits-gauge-reset{color:var(--muted)}\n.limits-usage-row{display:flex;gap:16px;flex-wrap:wrap;padding-top:8px;border-top:1px solid var(--border);margin-top:4px}\n.limits-usage-item{font-size:12px;color:var(--muted)}\n.limits-usage-item span{color:var(--text);font-weight:500}\n.limits-no-data{padding:16px;text-align:center;color:var(--muted);font-size:12px}\n.limits-billing-row{display:flex;gap:10px;flex-wrap:wrap;padding:8px 0 2px}\n.limits-credit-card{flex:1;min-width:120px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:10px 12px}\n.limits-credit-label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}\n.limits-credit-val{font-size:20px;font-weight:600;color:var(--green)}\n.limits-budget-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:6px}\n.limits-budget-input{background:var(--bg3);border:1px solid var(--border);border-radius:5px;padding:5px 10px;color:var(--text);font-size:12px;width:140px;outline:none}\n.limits-budget-input:focus{border-color:var(--blue)}\n.limits-budget-label{font-size:12px;color:var(--muted)}\n\n/* \u2500\u2500 Footer bar \u2500\u2500 */\n#footer{padding:7px 20px;border-top:1px solid var(--border);background:var(--bg2);font-size:11px;color:var(--muted);display:flex;gap:16px;flex-shrink:0}\n#footer a{color:var(--muted)}#footer a:hover{color:var(--blue)}\n</style>\n</head>\n<body>\n<div id=\"app\">\n\n<!-- \u2500\u2500 Sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<div id=\"sidebar\">\n <div id=\"sidebar-brand\">\n <div class=\"logo\" style=\"display:flex;align-items:center;gap:8px\">\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 80 80\" fill=\"none\"><rect width=\"80\" height=\"80\" rx=\"16\" fill=\"#16a34a\"/><rect x=\"8\" y=\"14\" width=\"64\" height=\"10\" rx=\"5\" fill=\"white\" opacity=\"0.35\"/><rect x=\"16\" y=\"35\" width=\"48\" height=\"10\" rx=\"5\" fill=\"white\" opacity=\"0.65\"/><rect x=\"24\" y=\"56\" width=\"32\" height=\"10\" rx=\"5\" fill=\"white\" opacity=\"1\"/></svg>\n Squee<span>zr</span>\n </div>\n <div class=\"ver\" id=\"sb-ver\">v\u2014</div>\n </div>\n\n <nav>\n <div class=\"nav-item active\" data-page=\"overview\">\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M1 2.5A1.5 1.5 0 012.5 1h3A1.5 1.5 0 017 2.5v3A1.5 1.5 0 015.5 7h-3A1.5 1.5 0 011 5.5v-3zm8 0A1.5 1.5 0 0110.5 1h3A1.5 1.5 0 0115 2.5v3A1.5 1.5 0 0113.5 7h-3A1.5 1.5 0 019 5.5v-3zm-8 8A1.5 1.5 0 012.5 9h3A1.5 1.5 0 017 10.5v3A1.5 1.5 0 015.5 15h-3A1.5 1.5 0 011 13.5v-3zm8 0A1.5 1.5 0 0110.5 9h3a1.5 1.5 0 011.5 1.5v3A1.5 1.5 0 0113.5 15h-3A1.5 1.5 0 019 13.5v-3z\"/>\n </svg>\n <span class=\"nav-label\">Overview</span>\n </div>\n <div class=\"nav-item\" data-page=\"projects\">\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M9.828 3h3.982a2 2 0 011.992 2.181l-.637 7A2 2 0 0113.174 14H2.826a2 2 0 01-1.991-1.819l-.637-7a1.99 1.99 0 01.342-1.31L.5 3a2 2 0 012-2h3.672a2 2 0 011.414.586l.828.828A2 2 0 009.828 3zm-8.322.12C1.72 3.042 1.95 3 2.19 3h5.396l-.707-.707A1 1 0 006.172 2H2.5a1 1 0 00-1 .981l.006.139z\"/>\n </svg>\n <span class=\"nav-label\">Projects</span>\n </div>\n <div class=\"nav-item\" data-page=\"history\">\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 3.5a.5.5 0 00-1 0V9a.5.5 0 00.252.434l3.5 2a.5.5 0 00.496-.868L8 8.71V3.5z\"/>\n <path d=\"M8 16A8 8 0 108 0a8 8 0 000 16zm7-8A7 7 0 111 8a7 7 0 0114 0z\"/>\n </svg>\n <span class=\"nav-label\">History</span>\n </div>\n <div class=\"nav-item\" data-page=\"limits\">\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"20\" x2=\"18\" y2=\"10\"/>\n <line x1=\"12\" y1=\"20\" x2=\"12\" y2=\"4\"/>\n <line x1=\"6\" y1=\"20\" x2=\"6\" y2=\"14\"/>\n <line x1=\"2\" y1=\"20\" x2=\"22\" y2=\"20\"/>\n </svg>\n <span class=\"nav-label\">Limits</span>\n </div>\n <div class=\"nav-item\" data-page=\"settings\">\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 4.754a3.246 3.246 0 100 6.492 3.246 3.246 0 000-6.492zM5.754 8a2.246 2.246 0 114.492 0 2.246 2.246 0 01-4.492 0z\"/>\n <path d=\"M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 01-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 01-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 01.52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 011.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 011.255-.52l.292.16c1.64.892 3.433-.902 2.54-2.541l-.159-.292a.873.873 0 01.52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 01-.52-1.255l.16-.292c.892-1.64-.901-3.433-2.541-2.54l-.292.159a.873.873 0 01-1.255-.52l-.094-.319z\"/>\n </svg>\n <span class=\"nav-label\">Settings</span>\n </div>\n </nav>\n\n <div id=\"sidebar-footer\">\n <div class=\"status-row\">\n <div class=\"dot\" id=\"status-dot\"></div>\n <span id=\"status-text\">Connecting\u2026</span>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500 Main content \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<div id=\"content\">\n <div id=\"page-header\">\n <span id=\"page-title\">Overview</span>\n <span id=\"project-badge\" style=\"display:none\"></span>\n <span id=\"conn-pill\">\u25CF live</span>\n </div>\n\n <div id=\"pages\">\n\n <!-- \u2500\u2500\u2500 Overview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"page active\" id=\"page-overview\">\n <div class=\"cards-grid\">\n <div class=\"card c-green\">\n <div class=\"card-label\">Tokens Saved</div>\n <div class=\"card-value\" id=\"c-tokens\">\u2014</div>\n <div class=\"card-sub\" id=\"c-chars\">\u2014 chars</div>\n </div>\n <div class=\"card c-blue\">\n <div class=\"card-label\">Compression</div>\n <div class=\"card-value\" id=\"c-pct\">\u2014</div>\n <div class=\"card-sub\">of tool results</div>\n </div>\n <div class=\"card c-yellow\">\n <div class=\"card-label\">Requests</div>\n <div class=\"card-value\" id=\"c-req\">\u2014</div>\n <div class=\"card-sub\" id=\"c-compressions\">\u2014 compressions</div>\n </div>\n <div class=\"card c-orange\">\n <div class=\"card-label\">Est. Cost Saved</div>\n <div class=\"card-value\" id=\"c-cost\">\u2014</div>\n <div class=\"card-sub\">@ $3 / MTok</div>\n </div>\n </div>\n\n <div class=\"section\">\n <div class=\"section-title\">Context pressure \u2014 last request</div>\n <div class=\"bar-row\">\n <span class=\"bar-label\">Before compression</span>\n <div class=\"bar-track\"><div class=\"bar-fill\" id=\"bar-msg\" style=\"width:0%\"></div></div>\n <span class=\"bar-val\" id=\"pct-msg\">0%</span>\n </div>\n <div class=\"bar-row\">\n <span class=\"bar-label\">After compression</span>\n <div class=\"bar-track\"><div class=\"bar-fill\" id=\"bar-out\" style=\"width:0%\"></div></div>\n <span class=\"bar-val\" id=\"pct-out\">0%</span>\n </div>\n <div class=\"bar-row\" style=\"margin-bottom:0\">\n <span class=\"bar-label\">Session cache hits</span>\n <div class=\"bar-track\"><div class=\"bar-fill\" id=\"bar-cache\" style=\"width:0%;background:var(--purple)\"></div></div>\n <span class=\"bar-val\" id=\"pct-cache\">0</span>\n </div>\n </div>\n\n <div class=\"section\">\n <div class=\"section-title\">Activity \u2014 tokens saved per request <span style=\"font-weight:400;text-transform:none;letter-spacing:0\">(last 60)</span></div>\n <canvas id=\"sparkline\"></canvas>\n </div>\n\n <div class=\"section\">\n <div class=\"section-title\">By tool</div>\n <table>\n <thead>\n <tr>\n <th>Tool</th>\n <th class=\"td-right\">Calls</th>\n <th class=\"td-right\">Tokens saved</th>\n <th>Savings</th>\n </tr>\n </thead>\n <tbody id=\"tools-body\">\n <tr><td colspan=\"4\" style=\"color:var(--muted);padding:14px 8px;text-align:center\">No data yet\u2026</td></tr>\n </tbody>\n </table>\n </div>\n\n <div class=\"cache-row\">\n <div class=\"cache-card\">\n <div class=\"cache-label\">Session cache</div>\n <div class=\"cache-val\" id=\"c-scache\">\u2014</div>\n </div>\n <div class=\"cache-card\">\n <div class=\"cache-label\">Expand store</div>\n <div class=\"cache-val\" id=\"c-expand\">\u2014</div>\n </div>\n <div class=\"cache-card\">\n <div class=\"cache-label\">LRU cache</div>\n <div class=\"cache-val\" id=\"c-lru\">\u2014</div>\n </div>\n <div class=\"cache-card\">\n <div class=\"cache-label\">Pattern hits</div>\n <div class=\"cache-val\" id=\"c-patterns\">\u2014</div>\n </div>\n </div>\n\n <!-- Savings breakdown -->\n <div class=\"section\">\n <div class=\"section-title\">Savings Breakdown</div>\n <div class=\"cache-grid\" style=\"grid-template-columns:1fr 1fr 1fr\">\n <div class=\"cache-item\">\n <div class=\"cache-label\">Deterministic</div>\n <div class=\"cache-val\" id=\"bd-det\" style=\"color:var(--green)\">\u2014</div>\n </div>\n <div class=\"cache-item\">\n <div class=\"cache-label\">AI compression</div>\n <div class=\"cache-val\" id=\"bd-ai\" style=\"color:var(--blue)\">\u2014</div>\n </div>\n <div class=\"cache-item\">\n <div class=\"cache-label\">Read dedup</div>\n <div class=\"cache-val\" id=\"bd-dedup\" style=\"color:var(--purple)\">\u2014</div>\n </div>\n <div class=\"cache-item\">\n <div class=\"cache-label\">System prompt</div>\n <div class=\"cache-val\" id=\"bd-sysprompt\">\u2014</div>\n </div>\n <div class=\"cache-item\">\n <div class=\"cache-label\">Tag overhead</div>\n <div class=\"cache-val\" id=\"bd-overhead\" style=\"color:var(--muted)\">\u2014</div>\n </div>\n <div class=\"cache-item\">\n <div class=\"cache-label\">AI calls</div>\n <div class=\"cache-val\" id=\"bd-aicalls\" style=\"color:var(--muted)\">\u2014</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- \u2500\u2500\u2500 Projects \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"page\" id=\"page-projects\">\n <div class=\"section\" style=\"margin-bottom:0\">\n <div class=\"section-title\" id=\"projects-section-title\">All projects \u2014 this session + history</div>\n <table class=\"project-table\">\n <thead>\n <tr>\n <th>Project</th>\n <th class=\"td-right\">Sessions</th>\n <th class=\"td-right\">Requests</th>\n <th class=\"td-right\">Tokens saved</th>\n <th class=\"td-right\">Last seen</th>\n </tr>\n </thead>\n <tbody id=\"projects-body\">\n <tr><td colspan=\"5\" style=\"color:var(--muted);padding:20px 8px;text-align:center\">Loading\u2026</td></tr>\n </tbody>\n </table>\n </div>\n </div>\n\n <!-- \u2500\u2500\u2500 History \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"page\" id=\"page-history\">\n <div id=\"hist-layout\">\n <div id=\"hist-projects\">\n <div style=\"padding:10px 14px;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid var(--border)\">Projects</div>\n <div id=\"hist-proj-list\"></div>\n </div>\n <div id=\"hist-sessions\">\n <div class=\"hist-sessions-header\" id=\"hist-sessions-header\">Select a project</div>\n <div id=\"hist-sessions-list\"><div class=\"empty-msg\">Select a project on the left to view sessions.</div></div>\n </div>\n </div>\n </div>\n\n <!-- \u2500\u2500\u2500 Limits \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"page\" id=\"page-limits\">\n\n <!-- Anthropic -->\n <div class=\"limits-cli-section\" id=\"lim-anthropic\">\n <div class=\"limits-cli-header\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"currentColor\" style=\"color:var(--orange)\">\n <path d=\"M13.83 2.34a2.09 2.09 0 0 0-3.66 0L1.13 18.9A2.09 2.09 0 0 0 2.96 22h18.08a2.09 2.09 0 0 0 1.83-3.1L13.83 2.34ZM12 8a1 1 0 0 1 1 1v5a1 1 0 0 1-2 0V9a1 1 0 0 1 1-1Zm0 10a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"/>\n </svg>\n <svg width=\"16\" height=\"16\" fill=\"var(--text)\" viewBox=\"0 0 16 16\" style=\"vertical-align:middle;margin-right:6px\"><path d=\"m3.127 10.604 3.135-1.76.053-.153-.053-.085H6.11l-.525-.032-1.791-.048-1.554-.065-1.505-.08-.38-.081L0 7.832l.036-.234.32-.214.455.04 1.009.069 1.513.105 1.097.064 1.626.17h.259l.036-.105-.089-.065-.068-.064-1.566-1.062-1.695-1.121-.887-.646-.48-.327-.243-.306-.104-.67.435-.48.585.04.15.04.593.456 1.267.981 1.654 1.218.242.202.097-.068.012-.049-.109-.181-.9-1.626-.96-1.655-.428-.686-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089.279.242.411.94.666 1.48 1.033 2.014.302.597.162.553.06.17h.105v-.097l.085-1.134.157-1.392.154-1.792.052-.504.25-.605.497-.327.387.186.319.456-.045.294-.19 1.23-.37 1.93-.243 1.29h.142l.161-.16.654-.868 1.097-1.372.484-.545.565-.601.363-.287h.686l.505.751-.226.775-.707.895-.585.759-.839 1.13-.524.904.048.072.125-.012 1.897-.403 1.024-.186 1.223-.21.553.258.06.263-.218.536-1.307.323-1.533.307-2.284.54-.028.02.032.04 1.029.098.44.024h1.077l2.005.15.525.346.315.424-.053.323-.807.411-3.631-.863-.872-.218h-.12v.073l.726.71 1.331 1.202 1.667 1.55.084.383-.214.302-.226-.032-1.464-1.101-.565-.497-1.28-1.077h-.084v.113l.295.432 1.557 2.34.08.718-.112.234-.404.141-.444-.08-.911-1.28-.94-1.44-.759-1.291-.093.053-.448 4.821-.21.246-.484.186-.403-.307-.214-.496.214-.98.258-1.28.21-1.016.19-1.263.112-.42-.008-.028-.092.012-.953 1.307-1.448 1.957-1.146 1.227-.274.109-.477-.247.045-.44.266-.39 1.586-2.018.956-1.25.617-.723-.004-.105h-.036l-4.212 2.736-.75.096-.324-.302.04-.496.154-.162 1.267-.871z\"/></svg><span class=\"limits-cli-name\">Anthropic \u00B7 Claude Code</span>\n <span class=\"limits-cli-badge none\" id=\"ant-badge\">no data yet</span>\n </div>\n <div class=\"limits-gauge-grid\">\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\">\n <span id=\"ant-tok-label\">Tokens / minute</span>\n <span id=\"ant-tok-pct\" style=\"color:var(--muted)\">\u2014</span>\n </div>\n <div class=\"limits-gauge-bar\"><div class=\"limits-gauge-fill\" id=\"ant-tok-fill\" style=\"width:0%\"></div></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"ant-tok-rem\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"ant-tok-reset\"></span>\n </div>\n </div>\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\">\n <span id=\"ant-req-label\">Requests / minute</span>\n <span id=\"ant-req-pct\" style=\"color:var(--muted)\">\u2014</span>\n </div>\n <div class=\"limits-gauge-bar\"><div class=\"limits-gauge-fill\" id=\"ant-req-fill\" style=\"width:0%\"></div></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"ant-req-rem\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"ant-req-reset\"></span>\n </div>\n </div>\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\">\n <span id=\"ant-inp-label\">Input tokens / minute</span>\n <span id=\"ant-inp-pct\" style=\"color:var(--muted)\">\u2014</span>\n </div>\n <div class=\"limits-gauge-bar\"><div class=\"limits-gauge-fill\" id=\"ant-inp-fill\" style=\"width:0%\"></div></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"ant-inp-rem\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"ant-inp-reset\"></span>\n </div>\n </div>\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\">\n <span id=\"ant-out-label\">Output tokens / minute</span>\n <span id=\"ant-out-pct\" style=\"color:var(--muted)\">\u2014</span>\n </div>\n <div class=\"limits-gauge-bar\"><div class=\"limits-gauge-fill\" id=\"ant-out-fill\" style=\"width:0%\"></div></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"ant-out-rem\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"ant-out-reset\"></span>\n </div>\n </div>\n </div>\n <div class=\"limits-usage-row\">\n <div class=\"limits-usage-item\">Session input: <span id=\"ant-u-inp-s\">\u2014</span></div>\n <div class=\"limits-usage-item\">Session output: <span id=\"ant-u-out-s\">\u2014</span></div>\n <div class=\"limits-usage-item\">Today input: <span id=\"ant-u-inp-d\">\u2014</span></div>\n <div class=\"limits-usage-item\">Today output: <span id=\"ant-u-out-d\">\u2014</span></div>\n <div class=\"limits-usage-item\" style=\"margin-left:auto\">\n <a href=\"https://console.anthropic.com/settings/usage\" target=\"_blank\" style=\"color:var(--muted);font-size:11px\">View billing \u2197</a>\n </div>\n </div>\n </div>\n\n <!-- OpenAI -->\n <div class=\"limits-cli-section\" id=\"lim-openai\">\n <div class=\"limits-cli-header\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"currentColor\" style=\"color:var(--text)\">\n <path d=\"M22.28 9.27a6.17 6.17 0 0 0-.53-5.06 6.24 6.24 0 0 0-6.7-2.99A6.23 6.23 0 0 0 10.36 0a6.24 6.24 0 0 0-5.95 4.32 6.23 6.23 0 0 0-4.16 3.02 6.24 6.24 0 0 0 .77 7.32 6.17 6.17 0 0 0 .53 5.06 6.24 6.24 0 0 0 6.7 2.99A6.23 6.23 0 0 0 13.64 24a6.25 6.25 0 0 0 5.96-4.33 6.23 6.23 0 0 0 4.15-3.02 6.24 6.24 0 0 0-.77-7.31l.3-.07ZM13.64 22.5a4.63 4.63 0 0 1-2.97-1.08l.15-.08 4.93-2.85a.82.82 0 0 0 .41-.71v-6.96l2.08 1.2a.08.08 0 0 1 .04.06v5.76a4.65 4.65 0 0 1-4.64 4.66Zm-9.95-4.27a4.63 4.63 0 0 1-.55-3.12l.14.09 4.93 2.85a.82.82 0 0 0 .82 0l6.02-3.47v2.4a.08.08 0 0 1-.03.06L10.06 20a4.65 4.65 0 0 1-6.37-1.77Zm-1.28-10.8a4.63 4.63 0 0 1 2.42-2.04v5.88a.82.82 0 0 0 .41.71l6.01 3.47-2.08 1.2a.08.08 0 0 1-.08 0L4.22 13.7a4.65 4.65 0 0 1-.81-6.27Zm17.09 3.99-6.02-3.48L15.56 7a.08.08 0 0 1 .08 0l4.87 2.81a4.64 4.64 0 0 1-.72 8.38v-5.88a.82.82 0 0 0-.39-.69Zm2.07-3.14-.14-.09-4.92-2.87a.82.82 0 0 0-.83 0L9.67 9.79V7.4a.08.08 0 0 1 .03-.06L14.6 4.5a4.64 4.64 0 0 1 6.9 4.81l.07-.03Zm-13.03 4.28-2.08-1.2a.08.08 0 0 1-.04-.06V5.5a4.64 4.64 0 0 1 7.62-3.56l-.15.08L7.9 4.87a.82.82 0 0 0-.41.71l-.01 6.98Zm1.13-2.43 2.68-1.55 2.68 1.55v3.1l-2.68 1.54-2.68-1.54v-3.1Z\"/>\n </svg>\n <svg width=\"16\" height=\"16\" fill=\"var(--text)\" viewBox=\"0 0 16 16\" style=\"vertical-align:middle;margin-right:6px\"><path d=\"M14.949 6.547a3.94 3.94 0 0 0-.348-3.273 4.11 4.11 0 0 0-4.4-1.934A4.1 4.1 0 0 0 8.423.2 4.15 4.15 0 0 0 6.305.086a4.1 4.1 0 0 0-1.891.948 4.04 4.04 0 0 0-1.158 1.753 4.1 4.1 0 0 0-1.563.679A4 4 0 0 0 .554 4.72a3.99 3.99 0 0 0 .502 4.731 3.94 3.94 0 0 0 .346 3.274 4.11 4.11 0 0 0 4.402 1.933c.382.425.852.764 1.377.995.526.231 1.095.35 1.67.346 1.78.002 3.358-1.132 3.901-2.804a4.1 4.1 0 0 0 1.563-.68 4 4 0 0 0 1.14-1.253 3.99 3.99 0 0 0-.506-4.716m-6.097 8.406a3.05 3.05 0 0 1-1.945-.694l.096-.054 3.23-1.838a.53.53 0 0 0 .265-.455v-4.49l1.366.778q.02.011.025.035v3.722c-.003 1.653-1.361 2.992-3.037 2.996m-6.53-2.75a2.95 2.95 0 0 1-.36-2.01l.095.057 3.233 1.84a.53.53 0 0 0 .527 0l3.949-2.246v1.555a.05.05 0 0 1-.022.041L6.473 13.3c-1.454.826-3.311.335-4.15-1.098m-.85-6.94A3.02 3.02 0 0 1 3.07 3.949v3.785a.51.51 0 0 0 .262.451l3.93 2.237-1.366.779a.05.05 0 0 1-.048 0L2.585 9.342a2.98 2.98 0 0 1-1.113-4.094zm11.216 2.571L8.747 5.576l1.362-.776a.05.05 0 0 1 .048 0l3.265 1.86a3 3 0 0 1 1.173 1.207 2.96 2.96 0 0 1-.27 3.2 3.05 3.05 0 0 1-1.36.997V8.279a.52.52 0 0 0-.276-.445m1.36-2.015-.097-.057-3.226-1.855a.53.53 0 0 0-.53 0L6.249 6.153V4.598a.04.04 0 0 1 .019-.04L9.533 2.7a3.07 3.07 0 0 1 3.257.139c.474.325.843.778 1.066 1.303.223.526.289 1.103.191 1.664zM5.503 8.575 4.139 7.8a.05.05 0 0 1-.026-.037V4.049c0-.57.166-1.127.476-1.607s.752-.864 1.275-1.105a3.08 3.08 0 0 1 3.234.41l-.096.054-3.23 1.838a.53.53 0 0 0-.265.455zm.742-1.577 1.758-1 1.762 1v2l-1.755 1-1.762-1z\"/></svg><span class=\"limits-cli-name\">OpenAI \u00B7 Codex</span>\n <span class=\"limits-cli-badge none\" id=\"oai-badge\">no data yet</span>\n </div>\n <div class=\"limits-gauge-grid\">\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\">\n <span id=\"oai-tok-label\">Session window</span>\n <span id=\"oai-tok-pct\" style=\"color:var(--muted)\">\u2014</span>\n </div>\n <div class=\"limits-gauge-bar\"><div class=\"limits-gauge-fill\" id=\"oai-tok-fill\" style=\"width:0%\"></div></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"oai-tok-rem\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"oai-tok-reset\"></span>\n </div>\n </div>\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\">\n <span id=\"oai-req-label\">Weekly window</span>\n <span id=\"oai-req-pct\" style=\"color:var(--muted)\">\u2014</span>\n </div>\n <div class=\"limits-gauge-bar\"><div class=\"limits-gauge-fill\" id=\"oai-req-fill\" style=\"width:0%\"></div></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"oai-req-rem\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"oai-req-reset\"></span>\n </div>\n </div>\n </div>\n <div class=\"limits-billing-row\" id=\"oai-billing-row\" style=\"display:none\">\n <div class=\"limits-credit-card\">\n <div class=\"limits-credit-label\">Credits remaining</div>\n <div class=\"limits-credit-val\" id=\"oai-credits\">\u2014</div>\n </div>\n <div class=\"limits-credit-card\">\n <div class=\"limits-credit-label\">Hard limit</div>\n <div class=\"limits-credit-val\" style=\"color:var(--yellow)\" id=\"oai-hard-lim\">\u2014</div>\n </div>\n </div>\n <div class=\"limits-usage-row\">\n <div class=\"limits-usage-item\">Session input: <span id=\"oai-u-inp-s\">\u2014</span></div>\n <div class=\"limits-usage-item\">Session output: <span id=\"oai-u-out-s\">\u2014</span></div>\n <div class=\"limits-usage-item\">Today input: <span id=\"oai-u-inp-d\">\u2014</span></div>\n <div class=\"limits-usage-item\">Today output: <span id=\"oai-u-out-d\">\u2014</span></div>\n <div class=\"limits-usage-item\" style=\"margin-left:auto\">\n <a href=\"https://platform.openai.com/usage\" target=\"_blank\" style=\"color:var(--muted);font-size:11px\">View billing \u2197</a>\n </div>\n </div>\n </div>\n\n <!-- Gemini -->\n <div class=\"limits-cli-section\" id=\"lim-gemini\">\n <div class=\"limits-cli-header\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"currentColor\" style=\"color:var(--blue)\">\n <path d=\"M12 0C5.37 0 0 5.37 0 12s5.37 12 12 12 12-5.37 12-12S18.63 0 12 0zm0 22C6.49 22 2 17.51 2 12S6.49 2 12 2s10 4.49 10 10-4.49 10-10 10zm-1-14h2v7h-2zm0 9h2v2h-2z\"/>\n </svg>\n <svg width=\"16\" height=\"16\" fill=\"var(--text)\" viewBox=\"0 0 16 16\" style=\"vertical-align:middle;margin-right:6px\"><path d=\"M15.545 6.558a9.4 9.4 0 0 1 .139 1.626c0 2.434-.87 4.492-2.384 5.885h.002C11.978 15.292 10.158 16 8 16A8 8 0 1 1 8 0a7.7 7.7 0 0 1 5.352 2.082l-2.284 2.284A4.35 4.35 0 0 0 8 3.166c-2.087 0-3.86 1.408-4.492 3.304a4.8 4.8 0 0 0 0 3.063h.003c.635 1.893 2.405 3.301 4.492 3.301 1.078 0 2.004-.276 2.722-.764h-.003a3.7 3.7 0 0 0 1.599-2.431H8v-3.08z\"/></svg><span class=\"limits-cli-name\">Google \u00B7 Gemini CLI</span>\n <span class=\"limits-cli-badge warn\" id=\"gem-badge\">only on 429 errors</span>\n </div>\n <div id=\"gem-nodata\" class=\"limits-no-data\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" style=\"margin-bottom:6px;display:block;margin-inline:auto;opacity:.4\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"/><line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"/>\n </svg>\n Google does not expose quota headers on successful responses.<br>\n Data appears here only after a 429 rate-limit error.<br>\n <a href=\"https://aistudio.google.com/app/usage\" target=\"_blank\" style=\"margin-top:8px;display:inline-block\">View quotas in AI Studio \u2197</a>\n </div>\n <div id=\"gem-data\" style=\"display:none\">\n <div class=\"limits-gauge-grid\">\n <div class=\"limits-gauge\">\n <div class=\"limits-gauge-label\"><span>Last known token limit</span></div>\n <div class=\"limits-gauge-bottom\">\n <span class=\"limits-gauge-remaining\" id=\"gem-tok-lim\">\u2014</span>\n <span class=\"limits-gauge-reset\" id=\"gem-errors\">0 errors</span>\n </div>\n </div>\n </div>\n </div>\n <div class=\"limits-usage-row\">\n <div class=\"limits-usage-item\">Session input: <span id=\"gem-u-inp-s\">\u2014</span></div>\n <div class=\"limits-usage-item\">Session output: <span id=\"gem-u-out-s\">\u2014</span></div>\n <div class=\"limits-usage-item\">Today input: <span id=\"gem-u-inp-d\">\u2014</span></div>\n <div class=\"limits-usage-item\">Today output: <span id=\"gem-u-out-d\">\u2014</span></div>\n <div class=\"limits-usage-item\" style=\"margin-left:auto\">\n <a href=\"https://aistudio.google.com/app/usage\" target=\"_blank\" style=\"color:var(--muted);font-size:11px\">View quotas \u2197</a>\n </div>\n </div>\n </div>\n\n <!-- Personal budget -->\n <div class=\"limits-cli-section\" style=\"margin-bottom:0\">\n <div class=\"limits-cli-header\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/>\n <path d=\"M12 8v4l3 3\"/>\n </svg>\n <span class=\"limits-cli-name\">Personal daily budget</span>\n <span class=\"limits-cli-badge none\">optional</span>\n </div>\n <div class=\"limits-budget-row\">\n <input class=\"limits-budget-input\" id=\"budget-input\" type=\"number\" placeholder=\"e.g. 5000000\" min=\"0\">\n <span class=\"limits-budget-label\">tokens / day</span>\n <button class=\"btn-save\" id=\"budget-save\" style=\"padding:4px 12px;font-size:11px;border-radius:6px;border:1px solid var(--border);background:var(--bg3);color:var(--muted);cursor:pointer;transition:all .15s\" onmouseover=\"this.style.borderColor='var(--blue)';this.style.color='var(--text)'\" onmouseout=\"this.style.borderColor='var(--border)';this.style.color='var(--muted)'\">Save</button>\n </div>\n <div id=\"budget-bar-wrap\" style=\"margin-top:10px;display:none\">\n <div style=\"display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-bottom:5px\">\n <span>Tokens used today through Squeezr</span>\n <span id=\"budget-pct-label\">0%</span>\n </div>\n <div class=\"limits-gauge-bar\" style=\"height:10px\">\n <div class=\"limits-gauge-fill\" id=\"budget-bar\" style=\"width:0%\"></div>\n </div>\n <div style=\"display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-top:4px\">\n <span id=\"budget-used-label\">0 used</span>\n <span id=\"budget-limit-label\">of \u2014</span>\n </div>\n </div>\n </div>\n\n </div>\n\n <!-- \u2500\u2500\u2500 Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"page\" id=\"page-settings\">\n <div class=\"section\" style=\"margin-bottom:14px\">\n <div class=\"section-title\">Compression mode</div>\n <div class=\"mode-btns\">\n <button class=\"mode-btn\" data-mode=\"soft\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <path d=\"M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2.5 2.5 0 1 1 19.5 12H2\"/>\n </svg>\n Soft\n </button>\n <button class=\"mode-btn active\" data-mode=\"normal\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"4\" y1=\"21\" x2=\"4\" y2=\"14\"/><line x1=\"4\" y1=\"10\" x2=\"4\" y2=\"3\"/>\n <line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"12\"/><line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"3\"/>\n <line x1=\"20\" y1=\"21\" x2=\"20\" y2=\"16\"/><line x1=\"20\" y1=\"12\" x2=\"20\" y2=\"3\"/>\n <line x1=\"1\" y1=\"14\" x2=\"7\" y2=\"14\"/><line x1=\"9\" y1=\"8\" x2=\"15\" y2=\"8\"/>\n <line x1=\"17\" y1=\"16\" x2=\"23\" y2=\"16\"/>\n </svg>\n Normal\n </button>\n <button class=\"mode-btn\" data-mode=\"aggressive\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/>\n </svg>\n Aggressive\n </button>\n <button class=\"mode-btn\" data-mode=\"critical\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/>\n <line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/>\n <line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n Critical\n </button>\n </div>\n <div id=\"mode-desc\">Normal \u2014 threshold 800 chars, last 3 results uncompressed</div>\n </div>\n\n <div class=\"section\">\n <div class=\"section-title\">Configuration</div>\n <div id=\"config-rows\">\n <div class=\"config-row\"><span class=\"config-key\">Mode</span><span class=\"config-val\" id=\"cfg-mode\">\u2014</span></div>\n <div class=\"config-row\"><span class=\"config-key\">Port</span><span class=\"config-val\" id=\"cfg-port\">\u2014</span></div>\n <div class=\"config-row\"><span class=\"config-key\">Dry-run</span><span class=\"config-val\" id=\"cfg-dryrun\">\u2014</span></div>\n <div class=\"config-row\"><span class=\"config-key\">LRU cache entries</span><span class=\"config-val\" id=\"cfg-lru\">\u2014</span></div>\n <div class=\"config-row\"><span class=\"config-key\">Session cache entries</span><span class=\"config-val\" id=\"cfg-scache\">\u2014</span></div>\n <div class=\"config-row\"><span class=\"config-key\">Version</span><span class=\"config-val\" id=\"cfg-version\">\u2014</span></div>\n </div>\n </div>\n\n <div class=\"section\" style=\"margin-bottom:0\">\n <div class=\"section-title\">Links</div>\n <div style=\"display:flex;gap:16px;flex-wrap:wrap;font-size:12px\">\n <a href=\"/squeezr/stats\" target=\"_blank\">/squeezr/stats JSON</a>\n <a href=\"/squeezr/history\" target=\"_blank\">/squeezr/history JSON</a>\n <a href=\"/squeezr/projects\" target=\"_blank\">/squeezr/projects JSON</a>\n <a href=\"https://github.com/sergioramosv/Squeezr\" target=\"_blank\">GitHub</a>\n </div>\n </div>\n </div>\n\n </div><!-- /pages -->\n\n <div id=\"footer\">\n <span>Squeezr v<span id=\"f-version\">\u2014</span></span>\n <span id=\"f-mode\">mode: active</span>\n <span id=\"f-port\"></span>\n <span id=\"conn-status\" style=\"margin-left:auto;color:var(--green)\">\u25CF connected</span>\n </div>\n</div><!-- /content -->\n\n</div><!-- /app -->\n\n<script>\n// \u2500\u2500 Sparkline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst MAX_PTS = 60\nconst sparkData = []\nlet lastTokens = 0\nfunction pushSpark(t) {\n sparkData.push(Math.max(0, t - lastTokens))\n lastTokens = t\n if (sparkData.length > MAX_PTS) sparkData.shift()\n}\nfunction drawSpark() {\n const cv = document.getElementById('sparkline')\n if (!cv) return\n const dpr = window.devicePixelRatio || 1\n const r = cv.getBoundingClientRect()\n cv.width = r.width * dpr; cv.height = r.height * dpr\n const ctx = cv.getContext('2d')\n ctx.scale(dpr, dpr)\n const w = r.width, h = r.height\n const mx = Math.max(...sparkData, 1)\n ctx.clearRect(0, 0, w, h)\n if (sparkData.length < 2) return\n const step = w / (MAX_PTS - 1)\n ctx.beginPath(); ctx.moveTo(0, h)\n sparkData.forEach((v, i) => ctx.lineTo(i * step, h - (v / mx) * (h - 4)))\n ctx.lineTo((sparkData.length - 1) * step, h)\n ctx.closePath()\n const g = ctx.createLinearGradient(0, 0, 0, h)\n g.addColorStop(0, 'rgba(63,185,80,.3)'); g.addColorStop(1, 'rgba(63,185,80,0)')\n ctx.fillStyle = g; ctx.fill()\n ctx.beginPath()\n sparkData.forEach((v, i) => {\n const x = i * step, y = h - (v / mx) * (h - 4)\n i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)\n })\n ctx.strokeStyle = '#3fb950'; ctx.lineWidth = 1.5; ctx.stroke()\n}\nwindow.addEventListener('resize', drawSpark)\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction fmtN(n) {\n if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'\n if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'\n return String(n)\n}\nfunction fmtCost(tok) {\n const u = (tok / 1e6) * 3\n return u < 0.01 ? '<$0.01' : u < 1 ? '$' + u.toFixed(3) : '$' + u.toFixed(2)\n}\nfunction fmtUptime(s) {\n if (s < 60) return s + 's'\n if (s < 3600) return Math.floor(s / 60) + 'm ' + (s % 60) + 's'\n return Math.floor(s / 3600) + 'h ' + Math.floor((s % 3600) / 60) + 'm'\n}\nfunction fmtTs(ms) {\n if (!ms) return '\u2014'\n const d = new Date(ms)\n return d.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})\n}\nfunction fmtTime(ms) {\n if (!ms) return '\u2014'\n const d = new Date(ms)\n return d.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false})\n}\nfunction fmtDur(startMs, endMs) {\n const s = Math.round((endMs - startMs) / 1000)\n if (s < 60) return s + 's'\n if (s < 3600) return Math.floor(s / 60) + 'm ' + (s % 60) + 's'\n return Math.floor(s / 3600) + 'h ' + Math.floor((s % 3600) / 60) + 'm'\n}\nfunction timeAgo(ms) {\n if (!ms) return ''\n const diff = Math.round((Date.now() - ms) / 1000)\n if (diff < 60) return 'just now'\n if (diff < 3600) return Math.floor(diff / 60) + 'm ago'\n if (diff < 86400) return Math.floor(diff / 3600) + 'h ago'\n if (diff < 172800) return 'yesterday'\n return Math.floor(diff / 86400) + 'd ago'\n}\nfunction barColor(p) {\n if (p >= 90) return 'var(--red)'\n if (p >= 75) return 'var(--yellow)'\n if (p >= 50) return 'var(--orange)'\n return 'var(--blue)'\n}\nfunction setBar(bid, vid, pct, label, noColor) {\n const b = document.getElementById(bid), v = document.getElementById(vid)\n b.style.width = Math.min(pct, 100) + '%'\n if (!noColor) b.style.background = barColor(pct)\n v.textContent = label\n}\nconst PROJECT_COLORS = ['#58a6ff','#3fb950','#ffa657','#bc8cff','#d29922','#f85149','#79c0ff','#56d364']\nfunction projectColor(name) {\n let h = 0; for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) & 0xffff\n return PROJECT_COLORS[h % PROJECT_COLORS.length]\n}\n\n// \u2500\u2500 Overview render \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction renderOverview(d) {\n document.getElementById('c-tokens').textContent = fmtN(d.total_saved_tokens)\n document.getElementById('c-chars').textContent = (d.total_saved_chars || 0).toLocaleString() + ' chars'\n document.getElementById('c-pct').textContent = (d.savings_pct || 0) + '%'\n document.getElementById('c-req').textContent = fmtN(d.requests || 0)\n document.getElementById('c-compressions').textContent = (d.compressions || 0) + ' compressions'\n document.getElementById('c-cost').textContent = fmtCost(d.total_saved_tokens || 0)\n document.getElementById('f-version').textContent = d.version || '\u2014'\n document.getElementById('sb-ver').textContent = 'v' + (d.version || '\u2014')\n document.getElementById('f-mode').textContent = 'mode: ' + (d.dry_run ? 'dry-run' : 'active')\n document.getElementById('f-port').textContent = 'port: ' + (d.port || '\u2014')\n // uptime removed from UI\n\n // Project badge\n const proj = d.current_project\n const badge = document.getElementById('project-badge')\n if (proj && proj !== 'unknown') {\n badge.textContent = proj\n badge.style.display = ''\n badge.style.borderColor = projectColor(proj)\n badge.style.color = projectColor(proj)\n } else {\n badge.style.display = 'none'\n }\n\n // Pressure bars\n const msgPct = Math.min(Math.round((d.last_original_chars || 0) / 80), 100)\n const outPct = Math.min(Math.round((d.last_compressed_chars || 0) / 80), 100)\n const ch = d.session_cache_hits || 0\n const cachePct = Math.round((ch / Math.max(ch + (d.compressions || 1), 1)) * 100)\n setBar('bar-msg', 'pct-msg', msgPct, msgPct + '%')\n setBar('bar-out', 'pct-out', outPct, outPct + '%')\n setBar('bar-cache', 'pct-cache', cachePct, ch, true)\n\n // Sparkline\n pushSpark(d.total_saved_tokens || 0)\n drawSpark()\n\n // Tool table\n const bt = d.by_tool || {}\n const rows = Object.entries(bt).sort((a, b) => b[1].saved_tokens - a[1].saved_tokens)\n const maxSaved = rows[0]?.[1]?.saved_tokens || 1\n const tbody = document.getElementById('tools-body')\n if (rows.length === 0) {\n tbody.innerHTML = '<tr><td colspan=\"4\" style=\"color:var(--muted);padding:14px 8px;text-align:center\">No tool results compressed yet\u2026</td></tr>'\n } else {\n tbody.innerHTML = rows.map(([tool, t]) => {\n const bw = Math.round((t.saved_tokens / maxSaved) * 72)\n return `<tr>\n <td><code class=\"tag\">${tool}</code></td>\n <td class=\"td-right\" style=\"color:var(--muted)\">${t.count}</td>\n <td class=\"td-right\">${fmtN(t.saved_tokens)}</td>\n <td><span class=\"mini-bar\" style=\"width:${bw}px;background:var(--green)\"></span>${t.avg_pct}%</td>\n </tr>`\n }).join('')\n }\n\n // Cache stats\n document.getElementById('c-scache').textContent = d.session_cache_size ?? '\u2014'\n document.getElementById('c-expand').textContent = d.expand_store_size ?? '\u2014'\n document.getElementById('c-lru').textContent = d.cache?.size ?? '\u2014'\n document.getElementById('c-patterns').textContent = d.pattern_hits\n ? Object.values(d.pattern_hits).reduce((s, v) => s + v, 0).toLocaleString()\n : '\u2014'\n\n // Settings config panel\n document.getElementById('cfg-mode').textContent = d.mode || '\u2014'\n document.getElementById('cfg-port').textContent = d.port || '\u2014'\n document.getElementById('cfg-dryrun').textContent = d.dry_run ? 'yes' : 'no'\n document.getElementById('cfg-lru').textContent = d.cache?.size ?? '\u2014'\n document.getElementById('cfg-scache').textContent = d.session_cache_size ?? '\u2014'\n document.getElementById('cfg-version').textContent = d.version || '\u2014'\n\n // Sync active mode button\n document.querySelectorAll('.mode-btn').forEach(b => {\n b.classList.toggle('active', b.dataset.mode === d.mode)\n })\n const modeMap = {\n soft: 'Soft \u2014 threshold 3000 chars, last 10 results uncompressed, no AI',\n normal: 'Normal \u2014 threshold 800 chars, last 3 results uncompressed',\n aggressive: 'Aggressive \u2014 threshold 200 chars, last 1 result uncompressed',\n critical: 'Critical \u2014 threshold 50 chars, everything compressed'\n }\n document.getElementById('mode-desc').textContent = modeMap[d.mode] || ''\n\n // Savings breakdown\n const bd = d.breakdown\n if (bd) {\n const fmtC = (n) => n > 0 ? '-' + fmtN(n) : '0'\n document.getElementById('bd-det').textContent = fmtC(bd.deterministic)\n document.getElementById('bd-ai').textContent = fmtC(bd.ai_compression)\n document.getElementById('bd-dedup').textContent = fmtC(bd.read_dedup)\n document.getElementById('bd-sysprompt').textContent = fmtC(bd.system_prompt)\n document.getElementById('bd-overhead').textContent = bd.overhead > 0 ? '+' + fmtN(bd.overhead) : '0'\n document.getElementById('bd-aicalls').textContent = bd.ai_calls > 0 ? bd.ai_calls + ' calls' : '0'\n }\n}\n\n// \u2500\u2500 Projects page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nasync function loadProjects() {\n try {\n const r = await fetch('/squeezr/projects')\n const { projects } = await r.json()\n const tbody = document.getElementById('projects-body')\n const entries = Object.entries(projects).sort((a, b) => b[1].savedTokens - a[1].savedTokens)\n if (entries.length === 0) {\n tbody.innerHTML = '<tr><td colspan=\"5\" style=\"color:var(--muted);padding:20px 8px;text-align:center\">No project data yet \u2014 start making requests.</td></tr>'\n return\n }\n tbody.innerHTML = entries.map(([name, p]) => `<tr>\n <td><span class=\"project-dot\" style=\"background:${projectColor(name)}\"></span><code>${name}</code></td>\n <td class=\"td-right\" style=\"color:var(--muted)\">${p.sessions}</td>\n <td class=\"td-right\">${p.requests}</td>\n <td class=\"td-right\" style=\"color:var(--green)\">${fmtN(p.savedTokens)}</td>\n <td class=\"td-right\" style=\"color:var(--muted);font-size:11px\">${p.lastSeen ? fmtTs(p.lastSeen) : '\u2014'}</td>\n </tr>`).join('')\n } catch {\n document.getElementById('projects-body').innerHTML = '<tr><td colspan=\"5\" style=\"color:var(--muted);padding:20px 8px;text-align:center\">Failed to load projects.</td></tr>'\n }\n}\n\n// \u2500\u2500 History page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet histData = null\nlet selectedHistProj = '__all__'\n\nasync function loadHistory() {\n try {\n const r = await fetch('/squeezr/history')\n histData = await r.json()\n renderHistProjects()\n renderHistSessions()\n } catch {\n document.getElementById('hist-proj-list').innerHTML = '<div class=\"empty-msg\">Failed to load history.</div>'\n }\n}\n\nfunction renderHistProjects() {\n if (!histData) return\n const all = [...histData.sessions]\n if (histData.current && histData.current.requests > 0) {\n const idx = all.findIndex(s => s.id === histData.current.id)\n if (idx >= 0) all[idx] = histData.current; else all.push(histData.current)\n }\n\n // Group by project\n const byProj = {}\n for (const s of all) {\n if (!byProj[s.project]) byProj[s.project] = 0\n byProj[s.project]++\n }\n\n const list = document.getElementById('hist-proj-list')\n let html = `<div class=\"hist-proj-item${selectedHistProj === '__all__' ? ' active' : ''}\" data-proj=\"__all__\">\n <span>All projects</span>\n <span class=\"hist-proj-count\">${all.length}</span>\n </div>`\n for (const [name, cnt] of Object.entries(byProj).sort((a, b) => b[1] - a[1])) {\n const active = selectedHistProj === name ? ' active' : ''\n html += `<div class=\"hist-proj-item${active}\" data-proj=\"${name}\">\n <span><span class=\"project-dot\" style=\"background:${projectColor(name)}\"></span>${name}</span>\n <span class=\"hist-proj-count\">${cnt}</span>\n </div>`\n }\n list.innerHTML = html\n\n list.querySelectorAll('.hist-proj-item').forEach(el => {\n el.addEventListener('click', () => {\n selectedHistProj = el.dataset.proj\n list.querySelectorAll('.hist-proj-item').forEach(x => x.classList.remove('active'))\n el.classList.add('active')\n renderHistSessions()\n })\n })\n}\n\nfunction renderHistSessions() {\n if (!histData) return\n let sessions = [...histData.sessions]\n if (histData.current && histData.current.requests > 0) {\n const idx = sessions.findIndex(s => s.id === histData.current.id)\n if (idx >= 0) sessions[idx] = histData.current; else sessions.push(histData.current)\n }\n // Filter empty sessions and sort newest first\n sessions = sessions.filter(s => s.requests > 0)\n sessions.sort((a, b) => b.startTime - a.startTime)\n\n if (selectedHistProj !== '__all__') {\n sessions = sessions.filter(s => s.project === selectedHistProj)\n }\n\n const header = document.getElementById('hist-sessions-header')\n header.textContent = selectedHistProj === '__all__'\n ? `All sessions (${sessions.length})`\n : `${selectedHistProj} \u2014 ${sessions.length} session${sessions.length !== 1 ? 's' : ''}`\n\n const list = document.getElementById('hist-sessions-list')\n if (sessions.length === 0) {\n list.innerHTML = '<div class=\"empty-msg\">No sessions found.</div>'\n return\n }\n\n // Group by day\n const byDay = {}\n for (const s of sessions) {\n const day = new Date(s.startTime).toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric',year:'numeric'})\n if (!byDay[day]) byDay[day] = []\n byDay[day].push(s)\n }\n\n let html = ''\n for (const [day, daySessions] of Object.entries(byDay)) {\n html += `<div style=\"padding:8px 16px;font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;background:var(--bg3);border-bottom:1px solid var(--border)\">${day}</div>`\n for (const s of daySessions) {\n const isCurrent = s.id === histData.current?.id\n const projBadge = selectedHistProj === '__all__' ? `<span class=\"session-project-badge\">${s.project}</span>` : ''\n html += `<div class=\"session-card\">\n <div class=\"session-date\">\n ${fmtTime(s.startTime)} \u2192 ${fmtTime(s.endTime)}\n <span style=\"color:var(--muted);font-weight:400\"> (${fmtDur(s.startTime, s.endTime)})</span>\n <span style=\"color:var(--muted);font-weight:400;margin-left:6px\">${timeAgo(s.endTime)}</span>\n ${isCurrent ? '<span style=\"font-size:10px;color:var(--green);margin-left:8px\">\u25CF active</span>' : ''}\n ${projBadge}\n </div>\n <div class=\"session-stats\">\n <div class=\"session-stat\">Requests: <span>${s.requests}</span></div>\n <div class=\"session-stat\">Tokens saved: <span style=\"color:var(--green)\">${fmtN(s.savedTokens)}</span></div>\n <div class=\"session-stat\">Compressions: <span>${s.compressions}</span></div>\n </div>\n </div>`\n }\n }\n list.innerHTML = html\n}\n\n// \u2500\u2500 Limits page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet limitsCountdownTimer = null\n\nfunction fmtTokens(n) {\n if (!n && n !== 0) return '\u2014'\n if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M'\n if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'\n return String(n)\n}\n\nfunction gaugeColor(pct) {\n if (pct >= 90) return 'var(--red)'\n if (pct >= 70) return 'var(--yellow)'\n if (pct >= 40) return 'var(--orange)'\n return 'var(--green)'\n}\n\nfunction fillGauge(fillId, pctId, remId, resetId, remaining, limit, resetEpoch) {\n if (!limit) {\n document.getElementById(fillId).style.width = '0%'\n document.getElementById(pctId).textContent = '\u2014'\n document.getElementById(remId).textContent = '\u2014'\n if (resetId) document.getElementById(resetId).textContent = ''\n return\n }\n const used = limit - remaining\n const pct = Math.max(0, Math.min(100, Math.round((used / limit) * 100)))\n const fill = document.getElementById(fillId)\n fill.style.width = pct + '%'\n fill.style.background = gaugeColor(pct)\n document.getElementById(pctId).textContent = pct + '% used'\n document.getElementById(pctId).style.color = gaugeColor(pct)\n document.getElementById(remId).textContent = fmtTokens(remaining) + ' remaining'\n if (resetId && resetEpoch) {\n const secs = Math.max(0, Math.round((resetEpoch - Date.now()) / 1000))\n document.getElementById(resetId).textContent = secs > 0 ? 'resets in ' + secs + 's' : ''\n }\n}\n\nfunction formatResetCountdown(resetEpoch) {\n const secs = resetEpoch > 0 ? Math.max(0, Math.round((resetEpoch - Date.now()) / 1000)) : 0\n if (secs <= 0) return ''\n if (secs >= 86400) return Math.floor(secs / 86400) + 'd ' + Math.floor((secs % 86400) / 3600) + 'h'\n if (secs >= 3600) return Math.floor(secs / 3600) + 'h ' + Math.floor((secs % 3600) / 60) + 'm'\n if (secs >= 60) return Math.floor(secs / 60) + 'm'\n return secs + 's'\n}\n\nfunction fillPercentGauge(fillId, pctId, remId, resetId, usedPercent, resetEpoch) {\n if (usedPercent == null) {\n document.getElementById(fillId).style.width = '0%'\n document.getElementById(pctId).textContent = '\u00E2\u20AC\u201D'\n document.getElementById(remId).textContent = '\u00E2\u20AC\u201D'\n if (resetId) document.getElementById(resetId).textContent = ''\n return\n }\n const pct = Math.max(0, Math.min(100, Math.round(usedPercent || 0)))\n const fill = document.getElementById(fillId)\n fill.style.width = pct + '%'\n fill.style.background = gaugeColor(pct)\n document.getElementById(pctId).textContent = pct + '%'\n document.getElementById(pctId).style.color = gaugeColor(pct)\n document.getElementById(remId).textContent = pct >= 80 ? pct + '% used' : (100 - pct) + '% free'\n const resetStr = formatResetCountdown(resetEpoch)\n if (resetId) document.getElementById(resetId).textContent = resetStr ? 'resets in ' + resetStr : ''\n}\n\nfunction renderLimits(d) {\n if (!d) return\n const { anthropic, openai, gemini } = d\n\n // \u2500\u2500 Anthropic \u2500\u2500\n const arl = anthropic?.rl\n const au = anthropic?.usage\n const antHasUsage = au && (au.inputSession > 0 || au.outputSession > 0)\n if (anthropic?.unified?.hasData) {\n // Subscription (OAuth): unified rate limits with 5h/7d windows\n const u = anthropic.unified\n document.getElementById('ant-badge').className = 'limits-cli-badge live'\n document.getElementById('ant-badge').textContent = 'subscription'\n // Relabel gauges for subscription windows\n document.getElementById('ant-tok-label').textContent = '5-hour window'\n document.getElementById('ant-req-label').textContent = '7-day window'\n document.getElementById('ant-inp-label').textContent = 'Session input'\n document.getElementById('ant-out-label').textContent = 'Session output'\n // 5-hour window\n const pct5h = Math.round(u.fiveHourUtilization * 100)\n document.getElementById('ant-tok-fill').style.width = pct5h + '%'\n document.getElementById('ant-tok-fill').style.background = gaugeColor(pct5h)\n document.getElementById('ant-tok-pct').textContent = pct5h + '%'\n document.getElementById('ant-tok-pct').style.color = gaugeColor(pct5h)\n const secs5h = u.fiveHourResetEpoch > 0 ? Math.max(0, Math.round((u.fiveHourResetEpoch - Date.now()) / 1000)) : 0\n const resetStr5h = secs5h > 3600 ? Math.floor(secs5h/3600) + 'h ' + Math.floor((secs5h%3600)/60) + 'm'\n : secs5h > 60 ? Math.floor(secs5h/60) + 'm'\n : secs5h > 0 ? secs5h + 's' : ''\n document.getElementById('ant-tok-rem').textContent = u.fiveHourStatus === 'allowed'\n ? (pct5h >= 80 ? pct5h + '% used' : (100-pct5h) + '% free')\n : 'throttled \u2014 resets in ' + resetStr5h\n document.getElementById('ant-tok-reset').textContent = resetStr5h ? 'resets in ' + resetStr5h : ''\n // 7-day window\n const pct7d = Math.round(u.sevenDayUtilization * 100)\n document.getElementById('ant-req-fill').style.width = pct7d + '%'\n document.getElementById('ant-req-fill').style.background = gaugeColor(pct7d)\n document.getElementById('ant-req-pct').textContent = pct7d + '%'\n document.getElementById('ant-req-pct').style.color = gaugeColor(pct7d)\n const secs7d = u.sevenDayResetEpoch > 0 ? Math.max(0, Math.round((u.sevenDayResetEpoch - Date.now()) / 1000)) : 0\n const resetStr7d = secs7d > 86400 ? Math.floor(secs7d/86400) + 'd ' + Math.floor((secs7d%86400)/3600) + 'h'\n : secs7d > 3600 ? Math.floor(secs7d/3600) + 'h ' + Math.floor((secs7d%3600)/60) + 'm'\n : secs7d > 0 ? Math.floor(secs7d/60) + 'm' : ''\n document.getElementById('ant-req-rem').textContent = u.sevenDayStatus === 'allowed'\n ? (pct7d >= 80 ? pct7d + '% used' : (100-pct7d) + '% free')\n : 'throttled \u2014 resets in ' + resetStr7d\n document.getElementById('ant-req-reset').textContent = resetStr7d ? 'resets in ' + resetStr7d : ''\n // Input/output: show session totals\n document.getElementById('ant-inp-pct').textContent = fmtTokens(au?.inputSession || 0)\n document.getElementById('ant-inp-rem').textContent = 'session input'\n document.getElementById('ant-out-pct').textContent = fmtTokens(au?.outputSession || 0)\n document.getElementById('ant-out-rem').textContent = 'session output'\n } else if (arl?.hasData) {\n document.getElementById('ant-badge').className = 'limits-cli-badge live'\n document.getElementById('ant-badge').textContent = 'live'\n fillGauge('ant-tok-fill','ant-tok-pct','ant-tok-rem','ant-tok-reset', arl.tokensRemaining, arl.tokensLimit, arl.tokensResetEpoch)\n fillGauge('ant-req-fill','ant-req-pct','ant-req-rem','ant-req-reset', arl.requestsRemaining, arl.requestsLimit, arl.requestsResetEpoch)\n fillGauge('ant-inp-fill','ant-inp-pct','ant-inp-rem','ant-inp-reset', arl.inputTokensRemaining, arl.inputTokensLimit, arl.tokensResetEpoch)\n fillGauge('ant-out-fill','ant-out-pct','ant-out-rem','ant-out-reset', arl.outputTokensRemaining, arl.outputTokensLimit, arl.tokensResetEpoch)\n } else if (antHasUsage) {\n // Fallback: no rate limit headers at all, but usage is tracked\n document.getElementById('ant-badge').className = 'limits-cli-badge live'\n document.getElementById('ant-badge').textContent = 'tracking'\n document.getElementById('ant-tok-pct').textContent = fmtTokens((au?.inputSession || 0) + (au?.outputSession || 0))\n document.getElementById('ant-tok-rem').textContent = 'session total'\n document.getElementById('ant-req-pct').textContent = (au?.requestsSession || 0) + ' reqs'\n document.getElementById('ant-req-rem').textContent = 'session total'\n document.getElementById('ant-inp-pct').textContent = fmtTokens(au?.inputSession || 0)\n document.getElementById('ant-inp-rem').textContent = 'session input'\n document.getElementById('ant-out-pct').textContent = fmtTokens(au?.outputSession || 0)\n document.getElementById('ant-out-rem').textContent = 'session output'\n }\n if (au) {\n document.getElementById('ant-u-inp-s').textContent = fmtTokens(au.inputSession)\n document.getElementById('ant-u-out-s').textContent = fmtTokens(au.outputSession)\n document.getElementById('ant-u-inp-d').textContent = fmtTokens(au.inputToday)\n document.getElementById('ant-u-out-d').textContent = fmtTokens(au.outputToday)\n }\n\n // \u2500\u2500 OpenAI \u2500\u2500\n const os = openai?.session\n const orl = openai?.rl\n const ou = openai?.usage\n const oaiHasUsage = ou && (ou.inputSession > 0 || ou.outputSession > 0)\n if (os?.hasData) {\n document.getElementById('oai-badge').className = 'limits-cli-badge live'\n document.getElementById('oai-badge').textContent = os.planType || 'session'\n document.getElementById('oai-tok-label').textContent = (os.primary?.windowDurationMins || 300) >= 300 ? '5-hour window' : 'session window'\n document.getElementById('oai-req-label').textContent = (os.secondary?.windowDurationMins || 0) >= 10080 ? '7-day window' : 'weekly window'\n fillPercentGauge('oai-tok-fill','oai-tok-pct','oai-tok-rem','oai-tok-reset', os.primary?.usedPercent, os.primary?.resetsAt || 0)\n fillPercentGauge('oai-req-fill','oai-req-pct','oai-req-rem','oai-req-reset', os.secondary?.usedPercent, os.secondary?.resetsAt || 0)\n } else if (orl?.hasData) {\n document.getElementById('oai-badge').className = 'limits-cli-badge live'\n document.getElementById('oai-badge').textContent = 'live'\n document.getElementById('oai-tok-label').textContent = 'tokens / minute'\n document.getElementById('oai-req-label').textContent = 'requests / minute'\n fillGauge('oai-tok-fill','oai-tok-pct','oai-tok-rem','oai-tok-reset', orl.tokensRemaining, orl.tokensLimit, orl.tokensResetEpoch)\n fillGauge('oai-req-fill','oai-req-pct','oai-req-rem','oai-req-reset', orl.requestsRemaining, orl.requestsLimit, orl.requestsResetEpoch)\n } else if (oaiHasUsage) {\n document.getElementById('oai-badge').className = 'limits-cli-badge live'\n document.getElementById('oai-badge').textContent = 'tracking'\n document.getElementById('oai-tok-label').textContent = 'session total'\n document.getElementById('oai-req-label').textContent = 'session requests'\n document.getElementById('oai-tok-pct').textContent = fmtTokens(ou.inputSession + ou.outputSession)\n document.getElementById('oai-tok-rem').textContent = 'session total'\n document.getElementById('oai-req-pct').textContent = ou.requestsSession + ' reqs'\n document.getElementById('oai-req-rem').textContent = 'session total'\n }\n const ob = openai?.billing\n if (ob?.hardLimitUsd > 0) {\n document.getElementById('oai-billing-row').style.display = 'flex'\n document.getElementById('oai-credits').textContent = '$' + (ob.creditBalanceUsd || 0).toFixed(2)\n document.getElementById('oai-hard-lim').textContent = '$' + ob.hardLimitUsd.toFixed(2)\n }\n if (ou) {\n document.getElementById('oai-u-inp-s').textContent = fmtTokens(ou.inputSession)\n document.getElementById('oai-u-out-s').textContent = fmtTokens(ou.outputSession)\n document.getElementById('oai-u-inp-d').textContent = fmtTokens(ou.inputToday)\n document.getElementById('oai-u-out-d').textContent = fmtTokens(ou.outputToday)\n }\n\n // \u2500\u2500 Gemini \u2500\u2500\n const ge = gemini?.errors\n const gu = gemini?.usage\n const gemHasUsage = gu && (gu.inputSession > 0 || gu.outputSession > 0)\n if (ge?.hasData) {\n document.getElementById('gem-nodata').style.display = 'none'\n document.getElementById('gem-data').style.display = 'block'\n document.getElementById('gem-tok-lim').textContent = fmtTokens(gemini.rl?.tokensLimit)\n document.getElementById('gem-errors').textContent = ge.errorCount429 + ' rate-limit errors'\n document.getElementById('gem-badge').className = 'limits-cli-badge error'\n document.getElementById('gem-badge').textContent = ge.errorCount429 + ' 429 errors'\n } else if (gemHasUsage) {\n document.getElementById('gem-nodata').style.display = 'none'\n document.getElementById('gem-data').style.display = 'block'\n document.getElementById('gem-badge').className = 'limits-cli-badge live'\n document.getElementById('gem-badge').textContent = 'tracking'\n }\n if (gu) {\n document.getElementById('gem-u-inp-s').textContent = fmtTokens(gu.inputSession)\n document.getElementById('gem-u-out-s').textContent = fmtTokens(gu.outputSession)\n document.getElementById('gem-u-inp-d').textContent = fmtTokens(gu.inputToday)\n document.getElementById('gem-u-out-d').textContent = fmtTokens(gu.outputToday)\n }\n\n // \u2500\u2500 Budget \u2500\u2500\n updateBudgetBar(au, ou, gu)\n}\n\n// Countdown ticker \u2014 updates reset countdowns every second without SSE\nfunction startLimitsCountdown(limitsData) {\n if (limitsCountdownTimer) clearInterval(limitsCountdownTimer)\n limitsCountdownTimer = setInterval(() => {\n const updateReset = (id, resetEpoch) => {\n if (!resetEpoch) return\n const el = document.getElementById(id)\n if (!el) return\n const secs = Math.max(0, Math.round((resetEpoch - Date.now()) / 1000))\n el.textContent = secs > 0 ? 'resets in ' + secs + 's' : ''\n }\n const d = limitsData\n if (d?.anthropic?.rl?.hasData) {\n updateReset('ant-tok-reset', d.anthropic.rl.tokensResetEpoch)\n updateReset('ant-req-reset', d.anthropic.rl.requestsResetEpoch)\n updateReset('ant-inp-reset', d.anthropic.rl.tokensResetEpoch)\n updateReset('ant-out-reset', d.anthropic.rl.tokensResetEpoch)\n }\n if (d?.openai?.rl?.hasData) {\n updateReset('oai-tok-reset', d.openai.rl.tokensResetEpoch)\n updateReset('oai-req-reset', d.openai.rl.requestsResetEpoch)\n }\n if (d?.openai?.session?.hasData) {\n const tok = d.openai.session.primary?.resetsAt || 0\n const req = d.openai.session.secondary?.resetsAt || 0\n updateReset('oai-tok-reset', tok)\n updateReset('oai-req-reset', req)\n }\n }, 1000)\n}\n\n// \u2500\u2500 Budget logic \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet dailyBudget = parseInt(localStorage.getItem('squeezr_budget') || '0')\n\nfunction updateBudgetBar(au, ou, gu) {\n const budget = dailyBudget\n const budgetInput = document.getElementById('budget-input')\n if (budgetInput && !budgetInput.value) budgetInput.value = budget || ''\n\n const wrap = document.getElementById('budget-bar-wrap')\n if (!budget) { wrap.style.display = 'none'; return }\n wrap.style.display = 'block'\n\n const totalToday = ((au?.inputToday || 0) + (au?.outputToday || 0) +\n (ou?.inputToday || 0) + (ou?.outputToday || 0) +\n (gu?.inputToday || 0) + (gu?.outputToday || 0))\n const pct = Math.min(100, Math.round((totalToday / budget) * 100))\n const fill = document.getElementById('budget-bar')\n fill.style.width = pct + '%'\n fill.style.background = gaugeColor(pct)\n document.getElementById('budget-pct-label').textContent = pct + '%'\n document.getElementById('budget-pct-label').style.color = gaugeColor(pct)\n document.getElementById('budget-used-label').textContent = fmtTokens(totalToday) + ' used today'\n document.getElementById('budget-limit-label').textContent = 'of ' + fmtTokens(budget) + ' / day'\n}\n\ndocument.getElementById('budget-save').addEventListener('click', () => {\n const val = parseInt(document.getElementById('budget-input').value || '0')\n dailyBudget = val\n localStorage.setItem('squeezr_budget', String(val))\n document.getElementById('budget-save').textContent = '\u2713 Saved'\n setTimeout(() => document.getElementById('budget-save').textContent = 'Save', 2000)\n // Re-render budget bar with latest limits data\n if (lastLimitsData) {\n const u = lastLimitsData.usage\n updateBudgetBar(u?.anthropic, u?.openai, u?.gemini)\n }\n})\n\n// Restore budget from localStorage on load\nconst savedBudget = localStorage.getItem('squeezr_budget')\nif (savedBudget) document.getElementById('budget-input').value = savedBudget\n\n// \u2500\u2500 Navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst pageTitles = { overview: 'Overview', projects: 'Projects', history: 'History', limits: 'Limits', settings: 'Settings' }\n\ndocument.querySelectorAll('.nav-item').forEach(item => {\n item.addEventListener('click', () => {\n const page = item.dataset.page\n document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'))\n document.querySelectorAll('.page').forEach(p => p.classList.remove('active'))\n item.classList.add('active')\n document.getElementById('page-' + page).classList.add('active')\n document.getElementById('page-title').textContent = pageTitles[page] || page\n if (page === 'projects') loadProjects()\n if (page === 'history') loadHistory()\n if (page === 'limits') {\n if (lastLimitsData) {\n renderLimits(lastLimitsData)\n startLimitsCountdown(lastLimitsData)\n }\n }\n if (page !== 'limits' && limitsCountdownTimer) {\n clearInterval(limitsCountdownTimer)\n limitsCountdownTimer = null\n }\n })\n})\n\n// \u2500\u2500 Mode selector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndocument.querySelectorAll('.mode-btn[data-mode]').forEach(btn => {\n btn.addEventListener('click', async () => {\n const mode = btn.dataset.mode\n if (!mode) return\n const prevActive = document.querySelector('.mode-btn.active')\n document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'))\n btn.classList.add('active')\n try {\n const res = await fetch('/squeezr/config', {\n method: 'POST',\n headers: {'content-type':'application/json'},\n body: JSON.stringify({ mode })\n })\n if (!res.ok) throw new Error('HTTP ' + res.status)\n } catch(e) {\n // Revert to previous mode on failure\n btn.classList.remove('active')\n if (prevActive) prevActive.classList.add('active')\n console.error('mode update failed', e)\n }\n })\n})\n\n// \u2500\u2500 SSE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst dot = document.getElementById('status-dot')\nconst statusText = document.getElementById('status-text')\nconst connPill = document.getElementById('conn-pill')\nconst connStatus = document.getElementById('conn-status')\nlet lastLimitsData = null\n\nfunction connect() {\n const es = new EventSource('/squeezr/events')\n es.onmessage = e => {\n try {\n const d = JSON.parse(e.data)\n renderOverview(d)\n if (d.limits) {\n lastLimitsData = d.limits\n // Only render limits page if it's currently visible\n const limPage = document.getElementById('page-limits')\n if (limPage && limPage.classList.contains('active')) {\n renderLimits(d.limits)\n if (!limitsCountdownTimer) startLimitsCountdown(d.limits)\n else { /* update the data reference for the countdown */ lastLimitsData = d.limits }\n }\n }\n } catch(err) { console.error(err) }\n }\n es.onopen = () => {\n dot.classList.remove('off')\n statusText.textContent = 'Connected'\n connPill.className = ''\n connPill.textContent = '\u25CF live'\n connStatus.style.color = 'var(--green)'\n connStatus.textContent = '\u25CF connected'\n }\n es.onerror = () => {\n dot.classList.add('off')\n statusText.textContent = 'Reconnecting\u2026'\n connPill.className = 'err'\n connPill.textContent = '\u25CF offline'\n connStatus.style.color = 'var(--red)'\n connStatus.textContent = '\u25CF reconnecting\u2026'\n es.close()\n setTimeout(connect, 3000)\n }\n}\nconnect()\n</script>\n</body>\n</html>";
package/dist/dashboard.js CHANGED
@@ -1050,7 +1050,7 @@ function fillGauge(fillId, pctId, remId, resetId, remaining, limit, resetEpoch)
1050
1050
  document.getElementById(remId).textContent = fmtTokens(remaining) + ' remaining'
1051
1051
  if (resetId && resetEpoch) {
1052
1052
  const secs = Math.max(0, Math.round((resetEpoch - Date.now()) / 1000))
1053
- document.getElementById(resetId).textContent = secs > 0 ? 'resets in ' + secs + 's' : 'resetting…'
1053
+ document.getElementById(resetId).textContent = secs > 0 ? 'resets in ' + secs + 's' : ''
1054
1054
  }
1055
1055
  }
1056
1056
 
@@ -1090,17 +1090,10 @@ function renderLimits(d) {
1090
1090
  const arl = anthropic?.rl
1091
1091
  const au = anthropic?.usage
1092
1092
  const antHasUsage = au && (au.inputSession > 0 || au.outputSession > 0)
1093
- if (arl?.hasData) {
1094
- document.getElementById('ant-badge').className = 'limits-cli-badge live'
1095
- document.getElementById('ant-badge').textContent = 'live'
1096
- fillGauge('ant-tok-fill','ant-tok-pct','ant-tok-rem','ant-tok-reset', arl.tokensRemaining, arl.tokensLimit, arl.tokensResetEpoch)
1097
- fillGauge('ant-req-fill','ant-req-pct','ant-req-rem','ant-req-reset', arl.requestsRemaining, arl.requestsLimit, arl.requestsResetEpoch)
1098
- fillGauge('ant-inp-fill','ant-inp-pct','ant-inp-rem','ant-inp-reset', arl.inputTokensRemaining, arl.inputTokensLimit, arl.tokensResetEpoch)
1099
- fillGauge('ant-out-fill','ant-out-pct','ant-out-rem','ant-out-reset', arl.outputTokensRemaining, arl.outputTokensLimit, arl.tokensResetEpoch)
1100
- } else if (anthropic?.unified?.hasData) {
1101
- // Subscription (OAuth): unified rate limits with 5h/7d windows
1102
- const u = anthropic.unified
1103
- document.getElementById('ant-badge').className = 'limits-cli-badge live'
1093
+ if (anthropic?.unified?.hasData) {
1094
+ // Subscription (OAuth): unified rate limits with 5h/7d windows
1095
+ const u = anthropic.unified
1096
+ document.getElementById('ant-badge').className = 'limits-cli-badge live'
1104
1097
  document.getElementById('ant-badge').textContent = 'subscription'
1105
1098
  // Relabel gauges for subscription windows
1106
1099
  document.getElementById('ant-tok-label').textContent = '5-hour window'
@@ -1137,10 +1130,17 @@ function renderLimits(d) {
1137
1130
  document.getElementById('ant-req-reset').textContent = resetStr7d ? 'resets in ' + resetStr7d : ''
1138
1131
  // Input/output: show session totals
1139
1132
  document.getElementById('ant-inp-pct').textContent = fmtTokens(au?.inputSession || 0)
1140
- document.getElementById('ant-inp-rem').textContent = 'session input'
1141
- document.getElementById('ant-out-pct').textContent = fmtTokens(au?.outputSession || 0)
1142
- document.getElementById('ant-out-rem').textContent = 'session output'
1143
- } else if (antHasUsage) {
1133
+ document.getElementById('ant-inp-rem').textContent = 'session input'
1134
+ document.getElementById('ant-out-pct').textContent = fmtTokens(au?.outputSession || 0)
1135
+ document.getElementById('ant-out-rem').textContent = 'session output'
1136
+ } else if (arl?.hasData) {
1137
+ document.getElementById('ant-badge').className = 'limits-cli-badge live'
1138
+ document.getElementById('ant-badge').textContent = 'live'
1139
+ fillGauge('ant-tok-fill','ant-tok-pct','ant-tok-rem','ant-tok-reset', arl.tokensRemaining, arl.tokensLimit, arl.tokensResetEpoch)
1140
+ fillGauge('ant-req-fill','ant-req-pct','ant-req-rem','ant-req-reset', arl.requestsRemaining, arl.requestsLimit, arl.requestsResetEpoch)
1141
+ fillGauge('ant-inp-fill','ant-inp-pct','ant-inp-rem','ant-inp-reset', arl.inputTokensRemaining, arl.inputTokensLimit, arl.tokensResetEpoch)
1142
+ fillGauge('ant-out-fill','ant-out-pct','ant-out-rem','ant-out-reset', arl.outputTokensRemaining, arl.outputTokensLimit, arl.tokensResetEpoch)
1143
+ } else if (antHasUsage) {
1144
1144
  // Fallback: no rate limit headers at all, but usage is tracked
1145
1145
  document.getElementById('ant-badge').className = 'limits-cli-badge live'
1146
1146
  document.getElementById('ant-badge').textContent = 'tracking'
@@ -1239,7 +1239,7 @@ function startLimitsCountdown(limitsData) {
1239
1239
  const el = document.getElementById(id)
1240
1240
  if (!el) return
1241
1241
  const secs = Math.max(0, Math.round((resetEpoch - Date.now()) / 1000))
1242
- el.textContent = secs > 0 ? 'resets in ' + secs + 's' : 'resetting…'
1242
+ el.textContent = secs > 0 ? 'resets in ' + secs + 's' : ''
1243
1243
  }
1244
1244
  const d = limitsData
1245
1245
  if (d?.anthropic?.rl?.hasData) {
package/dist/limits.js CHANGED
@@ -296,6 +296,24 @@ export async function maybeRefreshOpenAIBilling(apiKey) {
296
296
  }
297
297
  // ── Snapshot for API / SSE ────────────────────────────────────────────────────
298
298
  let openAISessionRefreshInFlight = null;
299
+ function nextOpenAISessionResetEpoch() {
300
+ const candidates = [
301
+ openAISessionLimits.primary?.resetsAt ?? 0,
302
+ openAISessionLimits.secondary?.resetsAt ?? 0,
303
+ ].filter((v) => Number.isFinite(v) && v > Date.now());
304
+ return candidates.length ? Math.min(...candidates) : 0;
305
+ }
306
+ function openAISessionRefreshIntervalMs() {
307
+ if (!openAISessionLimits.hasData)
308
+ return 60_000;
309
+ const nextReset = nextOpenAISessionResetEpoch();
310
+ if (!nextReset)
311
+ return 60_000;
312
+ const remainingMs = nextReset - Date.now();
313
+ if (remainingMs <= 2 * 60_000)
314
+ return 15_000;
315
+ return 60_000;
316
+ }
299
317
  function codexAppServerCommand() {
300
318
  if (process.platform === 'win32') {
301
319
  const cmdShim = process.env.APPDATA ? join(process.env.APPDATA, 'npm', 'codex.cmd') : 'codex.cmd';
@@ -308,7 +326,7 @@ function codexAppServerCommand() {
308
326
  return { cmd: 'codex', args: ['app-server', '--listen', 'stdio://'] };
309
327
  }
310
328
  export async function maybeRefreshOpenAISessionLimits(force = false) {
311
- if (!force && openAISessionLimits.lastFetched > 0 && Date.now() - openAISessionLimits.lastFetched < 60_000)
329
+ if (!force && openAISessionLimits.lastFetched > 0 && Date.now() - openAISessionLimits.lastFetched < openAISessionRefreshIntervalMs())
312
330
  return;
313
331
  if (openAISessionRefreshInFlight)
314
332
  return openAISessionRefreshInFlight;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squeezr-ai",
3
- "version": "1.21.6",
3
+ "version": "1.21.8",
4
4
  "description": "AI proxy that compresses Claude Code, Codex, Aider, Gemini CLI and Ollama context windows to save thousands of tokens per session",
5
5
  "keywords": [
6
6
  "claude",