sunnyware / static /tools.html
shamiur's picture
Part 252: Fix tools page 2FA header injection
3a38da5
Raw History Blame Contribute Delete
17.7 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Sunnyware - Tools</title>
<style>
:root{
--bg:#0b0d12; --card:#151922; --card2:#1b202b;
--text:#e7ecf3; --dim:#8892a6;
--accent:#5b8def; --ok:#2ecc71; --warn:#f5a623; --err:#e74c3c;
--border:#232a38;
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;background:var(--bg);color:var(--text);
font:14px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
header{display:flex;align-items:center;gap:16px;padding:14px 24px;
border-bottom:1px solid var(--border);position:sticky;top:0;
background:rgba(11,13,18,.94);backdrop-filter:blur(8px);z-index:20;
flex-wrap:wrap}
header h1{font-size:18px;margin:0;letter-spacing:.3px}
header .spacer{flex:1}
header a{color:var(--accent);text-decoration:none;font-size:13px}
header a:hover{text-decoration:underline}
.badge{padding:3px 9px;border-radius:14px;font-size:11px;
font-weight:600;letter-spacing:.4px;text-transform:uppercase;
display:inline-block}
.badge.ok{background:rgba(46,204,113,.15);color:var(--ok);
border:1px solid rgba(46,204,113,.4)}
.badge.warn{background:rgba(245,166,35,.15);color:var(--warn);
border:1px solid rgba(245,166,35,.4)}
.badge.err{background:rgba(231,76,60,.15);color:var(--err);
border:1px solid rgba(231,76,60,.4)}
.badge.dim{background:rgba(136,146,166,.15);color:var(--dim);
border:1px solid rgba(136,146,166,.3)}
.muted{color:var(--dim);font-size:12px}
main{max-width:1400px;margin:0 auto;padding:20px 24px 40px}
.topbar{background:var(--card);border:1px solid var(--border);
border-radius:10px;padding:16px 20px;margin-bottom:16px;
position:sticky;top:66px;z-index:10;backdrop-filter:blur(8px)}
.topbar .headline{display:flex;align-items:baseline;gap:12px;
flex-wrap:wrap}
.topbar .count{font-size:28px;font-weight:700;color:var(--accent)}
.topbar .sub{color:var(--dim);font-size:13px}
.topbar .bar{width:100%;height:8px;background:var(--card2);
border-radius:4px;overflow:hidden;margin-top:12px}
.topbar .bar>i{display:block;height:100%;
background:linear-gradient(90deg,var(--accent),#7fb0ff);
transition:width .3s ease}
.topbar .bar.warn>i{background:linear-gradient(90deg,#f5a623,#ffce6b)}
.topbar .bar.err>i{background:linear-gradient(90deg,#e74c3c,#ff8c80)}
.filters{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px;
align-items:center}
.filters button{background:var(--card2);border:1px solid var(--border);
color:var(--text);padding:6px 14px;border-radius:16px;font-size:12px;
cursor:pointer;font:inherit;transition:all .12s}
.filters button:hover{border-color:var(--accent)}
.filters button.active{background:var(--accent);color:#fff;
border-color:var(--accent)}
.searchbox{flex:1;min-width:180px}
.searchbox input{width:100%;background:var(--card2);
border:1px solid var(--border);color:var(--text);
padding:6px 12px;border-radius:16px;font:inherit;font-size:12px}
.searchbox input:focus{outline:none;border-color:var(--accent)}
.group{margin-top:16px;background:var(--card);
border:1px solid var(--border);border-radius:10px;overflow:hidden}
.group h2{margin:0;padding:12px 18px;font-size:12px;
text-transform:uppercase;letter-spacing:.8px;color:var(--dim);
background:var(--card2);cursor:pointer;user-select:none;
display:flex;align-items:center;gap:8px}
.group h2::before{content:"\25BC";font-size:9px;
transition:transform .15s;display:inline-block}
.group.collapsed h2::before{transform:rotate(-90deg)}
.group.collapsed .rows{display:none}
.group h2 .count{margin-left:auto;color:var(--text);font-weight:600;
font-size:11px}
.rows{display:flex;flex-direction:column}
.row{border-top:1px solid var(--border);padding:10px 18px;
display:flex;align-items:center;gap:12px;cursor:pointer;
transition:background .1s}
.row:hover{background:var(--card2)}
.row .dot{width:9px;height:9px;border-radius:50%;
background:var(--dim);flex:0 0 auto}
.row .dot.ok{background:var(--ok)}
.row .dot.warn{background:var(--warn)}
.row .dot.err{background:var(--err)}
.row .dot.hollow{background:transparent;
border:1px solid var(--dim);width:7px;height:7px}
.row .name{font-family:var(--mono);font-size:12.5px;flex:1;
min-width:0;overflow:hidden;text-overflow:ellipsis;
white-space:nowrap}
.row .meta{font-size:11px;color:var(--dim);flex:0 0 auto;
text-align:right;min-width:80px}
.row .arrow{color:var(--dim);font-size:10px;flex:0 0 auto;
transition:transform .15s}
.row.open{background:var(--card2)}
.row.open .arrow{transform:rotate(90deg)}
.detail{background:var(--card2);border-top:1px solid var(--border);
padding:12px 18px 14px 42px;font-size:12px;display:none}
.detail.open{display:block}
.detail dl{margin:0;display:grid;grid-template-columns:auto 1fr;
gap:4px 16px;font-size:11.5px}
.detail dt{color:var(--dim)}
.detail dd{margin:0;font-family:var(--mono);word-break:break-all}
.detail .fix{margin-top:10px;padding:8px 12px;border-radius:6px;
background:rgba(91,141,239,.1);border:1px solid rgba(91,141,239,.3);
font-size:11.5px}
.detail .fix strong{color:var(--accent)}
.detail .actions{margin-top:8px;display:flex;gap:8px;flex-wrap:wrap}
.detail .actions a{color:var(--accent);font-size:11.5px;
text-decoration:none;padding:4px 10px;border:1px solid var(--border);
border-radius:6px}
.detail .actions a:hover{border-color:var(--accent)}
.empty{padding:40px;text-align:center;color:var(--dim);
font-style:italic}
.err-msg{color:var(--err);font-size:12px;font-family:var(--mono);
margin-top:6px}
@media (max-width:640px){
.row .meta{display:none}
.detail dl{grid-template-columns:1fr}
}
</style>
</head>
<body>
<header>
<h1>Sunnyware - Tools</h1>
<span id="badge-health" class="badge dim">...</span>
<span class="spacer"></span>
<span class="muted" id="refresh-info">-</span>
<a href="/status">&larr; Back to status</a>
</header>
<main>
<div class="topbar" id="topbar">
<div class="headline">
<span class="count" id="t-count">...</span>
<span class="sub" id="t-sub">loading</span>
</div>
<div class="bar" id="t-bar"><i style="width:0%"></i></div>
<div class="filters" id="filters">
<button data-filter="all" class="active">All</button>
<button data-filter="broken">Broken</button>
<button data-filter="degraded">Degraded</button>
<button data-filter="working">Working</button>
<button data-filter="never_used">Never used</button>
<button data-filter="rate_limited">Rate Limited</button>
<div class="searchbox">
<input id="search" placeholder="Search tools..." />
</div>
</div>
</div>
<div id="groups"></div>
<div class="err-msg" id="err-box" style="display:none"></div>
</main>
<script>
// Part 252: Inject API key and 2FA session header into all fetch calls
(function() {
const _origFetch = window.fetch;
window.fetch = function(url, options) {
if (!options) options = {};
if (!options.headers) options.headers = {};
const isApiCall = (typeof url === 'string' && (url.startsWith('/') || url.startsWith(location.origin)));
if (isApiCall) {
const apiKey = localStorage.getItem('sunnyware_admin_key') || '';
const sess = localStorage.getItem('sunnyware_2fa_session') || '';
if (options.headers instanceof Headers) {
if (apiKey) options.headers.set('X-API-Key', apiKey);
if (sess) options.headers.set('X-2FA-Session', sess);
} else {
if (apiKey) options.headers['X-API-Key'] = apiKey;
if (sess) options.headers['X-2FA-Session'] = sess;
}
}
return _origFetch(url, options);
};
})();
// Original tools page script
// Part 192b: Global API Key Interceptor for Tools Dashboard
let _apiKey = localStorage.getItem('sunnyware_admin_key') || '';
if (!_apiKey) {
_apiKey = prompt('Enter API Key to view Tools Dashboard:') || '';
if (_apiKey) localStorage.setItem('sunnyware_admin_key', _apiKey);
}
const _originalFetch = window.fetch;
window.fetch = function(url, options) {
if (!options) options = {};
if (!options.headers) options.headers = {};
if (typeof url === 'string' && url.startsWith('/api')) {
if (options.headers instanceof Headers) {
options.headers.set('X-API-Key', _apiKey);
} else {
options.headers['X-API-Key'] = _apiKey;
}
}
return _originalFetch(url, options);
};
"use strict";
var POLL_MS = 10000;
var currentFilter = "all";
var currentSearch = "";
var expanded = {};
function el(tag, cls, txt){
var e = document.createElement(tag);
if (cls) e.className = cls;
if (txt != null) e.textContent = txt;
return e;
}
function badge(cls, txt){
var b = el("span", "badge " + cls);
b.textContent = txt;
return b;
}
function dotCls(status){
if (status === "broken") return "err";
if (status === "degraded") return "warn";
if (status === "working") return "ok";
return "hollow";
}
function badgeCls(status){
if (status === "broken") return "err";
if (status === "degraded") return "warn";
if (status === "working") return "ok";
return "dim";
}
function humanStatus(status){
if (status === "broken") return "BROKEN";
if (status === "degraded") return "DEGRADED";
if (status === "working") return "WORKING";
return "NEVER USED";
}
var GROUP_LABELS = {
core: "Core", memory: "Memory", files: "Files", web: "Web",
data: "Data", sql: "SQL", workflow: "Workflow", roles: "Roles",
other: "Other"
};
function renderGroup(cat, tools){
if (!tools.length) return null;
var g = el("div", "group");
g.dataset.cat = cat;
var h = el("h2");
h.appendChild(el("span", null, GROUP_LABELS[cat] || cat));
h.appendChild(el("span", "count", String(tools.length)));
h.addEventListener("click", function(){
g.classList.toggle("collapsed");
try {
var st = JSON.parse(localStorage.getItem("tools_collapse") || "{}");
st[cat] = g.classList.contains("collapsed");
localStorage.setItem("tools_collapse", JSON.stringify(st));
} catch(e){}
});
g.appendChild(h);
var rows = el("div", "rows");
tools.forEach(function(t){
var rowId = t.name;
var row = el("div", "row");
row.dataset.id = rowId;
if (expanded[rowId]) row.classList.add("open");
row.appendChild(el("span", "dot " + dotCls(t.status)));
row.appendChild(el("span", "name", t.name));
row.appendChild(badge(badgeCls(t.status), humanStatus(t.status)));
var meta = el("div", "meta");
if (t.status === "never_used"){
meta.textContent = "no calls";
} else if (t.is_open){
meta.textContent = "open " + t.remaining_open_s + "s";
} else if (t.rate_cooldown_s > 0){
meta.textContent = "cool " + t.rate_cooldown_s + "s";
} else if (t.rpm_limit > 0){
meta.textContent = t.rpm_current + "/" + t.rpm_limit + " rpm";
} else if (t.total_success + t.total_fails > 0){
var tot = t.total_success + t.total_fails;
meta.textContent = Math.round((t.total_success / tot) * 100) + "% ok";
}
row.appendChild(meta);
row.appendChild(el("span", "arrow", "\u25B6"));
var detail = el("div", "detail");
if (expanded[rowId]) detail.classList.add("open");
row.addEventListener("click", function(){
expanded[rowId] = !expanded[rowId];
row.classList.toggle("open");
detail.classList.toggle("open");
});
rows.appendChild(row);
var dl = el("dl");
dl.appendChild(el("dt", null, "category"));
dl.appendChild(el("dd", null, t.category || "other"));
dl.appendChild(el("dt", null, "status"));
dl.appendChild(el("dd", null, t.status));
if (t.total_success > 0 || t.total_fails > 0){
dl.appendChild(el("dt", null, "calls"));
dl.appendChild(el("dd", null,
t.total_success + " ok / " + t.total_fails + " failed"));
}
if (t.is_open){
dl.appendChild(el("dt", null, "breaker"));
dl.appendChild(el("dd", null,
"OPEN for " + t.remaining_open_s + "s"));
}
if (t.rpm_limit > 0 || t.rate_cooldown_s > 0){
dl.appendChild(el("dt", null, "rate limit"));
dl.appendChild(el("dd", null, t.rpm_current + " / " + (t.rpm_limit || "?") + " rpm"));
if (t.rate_cooldown_s > 0){
dl.appendChild(el("dt", null, "rate cooldown"));
dl.appendChild(el("dd", null, "cooling for " + t.rate_cooldown_s + "s"));
}
}
if (t.last_error){
dl.appendChild(el("dt", null, "last error"));
dl.appendChild(el("dd", null, t.last_error));
}
detail.appendChild(dl);
if (t.reason && t.reason !== t.last_error){
var fix = el("div", "fix");
fix.appendChild(el("strong", null, "reason: "));
fix.appendChild(document.createTextNode(String(t.reason).slice(0, 300)));
detail.appendChild(fix);
}
var actions = el("div", "actions");
var link = el("a");
link.href = "#";
link.textContent = "View raw JSON";
link.onclick = function(e){
e.preventDefault();
window.open("/api/tools/health", "_blank");
};
actions.appendChild(link);
detail.appendChild(actions);
rows.appendChild(detail);
});
g.appendChild(rows);
return g;
}
function applyFilters(tools){
return tools.filter(function(t){
if (currentFilter === "rate_limited" && !(t.rate_cooldown_s > 0)) return false;
if (currentFilter !== "all" && currentFilter !== "rate_limited" && t.status !== currentFilter) return false;
if (currentSearch && t.name.toLowerCase().indexOf(currentSearch) === -1)
return false;
return true;
});
}
function render(snap){
var s = snap.summary || {};
var total = s.total || 0;
var working = s.working || 0;
var degraded = s.degraded || 0;
var broken = s.broken || 0;
var neverUsed = s.never_used || 0;
var called = Math.max(0, total - neverUsed);
var healthy = working + neverUsed;
document.getElementById("t-count").textContent = total;
var sub = document.getElementById("t-sub");
if (called === 0){
sub.textContent = "0 calls this session \u00b7 all ready";
} else if (broken === 0 && degraded === 0){
sub.textContent = called + " called \u00b7 0 degraded \u00b7 0 broken";
} else {
sub.textContent = called + " called \u00b7 " + degraded +
" degraded \u00b7 " + broken + " broken";
}
var bar = document.getElementById("t-bar");
var pct = total > 0 ? Math.round((healthy / total) * 100) : 100;
bar.className = "bar" + (broken > 0 ? " err" : (degraded > 0 ? " warn" : ""));
bar.firstElementChild.style.width = pct + "%";
var counts = {all: total, broken: broken, degraded: degraded,
working: working, never_used: neverUsed};
var btns = document.querySelectorAll("#filters button");
for (var i = 0; i < btns.length; i++){
var f = btns[i].dataset.filter;
var base = btns[i].textContent.replace(/\s*\(\d+\)$/,"");
btns[i].textContent = base + " (" + (counts[f] || 0) + ")";
}
var groupsBox = document.getElementById("groups");
groupsBox.innerHTML = "";
var allTools = snap.tools || [];
var visible = applyFilters(allTools);
if (!visible.length){
groupsBox.appendChild(el("div", "empty", "No tools match this filter"));
return;
}
var byCat = {};
visible.forEach(function(t){
var cat = t.category || "other";
if (!byCat[cat]) byCat[cat] = [];
byCat[cat].push(t);
});
var order = ["core","memory","files","web","data","sql","workflow",
"roles","other"];
var seen = {};
order.forEach(function(cat){
if (byCat[cat]){
var g = renderGroup(cat, byCat[cat]);
if (g){
try {
var st = JSON.parse(localStorage.getItem("tools_collapse") || "{}");
if (st[cat]) g.classList.add("collapsed");
} catch(e){}
groupsBox.appendChild(g);
seen[cat] = true;
}
}
});
Object.keys(byCat).forEach(function(cat){
if (seen[cat]) return;
var g = renderGroup(cat, byCat[cat]);
if (g) groupsBox.appendChild(g);
});
}
async function poll(){
var healthBadge = document.getElementById("badge-health");
var errBox = document.getElementById("err-box");
try {
var r = await fetch("/api/tools/health", {cache:"no-store"});
if (!r.ok){
healthBadge.className = "badge err";
healthBadge.textContent = "HTTP " + r.status;
errBox.style.display = "block";
errBox.textContent = "Failed to fetch tools health: HTTP " + r.status;
return;
}
var snap = await r.json();
healthBadge.className = "badge ok";
healthBadge.textContent = "READY";
errBox.style.display = "none";
document.getElementById("refresh-info").textContent =
"updated " + new Date().toLocaleTimeString();
render(snap);
} catch(e){
healthBadge.className = "badge err";
healthBadge.textContent = "UNREACHABLE";
errBox.style.display = "block";
errBox.textContent = (e && e.message) || "fetch failed";
}
}
document.querySelectorAll("#filters button").forEach(function(b){
b.addEventListener("click", function(){
document.querySelectorAll("#filters button").forEach(function(x){
x.classList.remove("active");
});
b.classList.add("active");
currentFilter = b.dataset.filter;
poll();
});
});
document.getElementById("search").addEventListener("input", function(e){
currentSearch = (e.target.value || "").toLowerCase().trim();
poll();
});
poll();
setInterval(poll, POLL_MS);
</script>
</body>
</html>