2026-09-27 19:43:14 +02:00
<!doctype html>
< html lang = "en" >
< head >
< meta charset = "utf-8" >
< title > Shield< / title >
< style >
:root { color-scheme: light dark;
--bg:#0e131c; --panel:#141a24; --panel2:#191f2b; --line:rgba(255,255,255,.09);
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#0ac18e; --blue:#4c9eff; --warn:#f5a524; --danger:#ff5b5b; }
@media (prefers-color-scheme: light) {
:root { --bg:#f8faff; --panel:#ffffff; --panel2:#eff3fb; --line:rgba(0,0,0,.10);
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --blue:#2b6fd6; }
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body { background: var(--bg); color: var(--ink); font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; display: flex; flex-direction: column; }
header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--line); background: var(--panel); }
header h1 { margin: 0; font-size: 14px; font-weight: 600; }
header .pill { margin-left: auto; }
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: 999px;
font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
background: rgba(139,152,169,.14); color: var(--mut); }
.pill.on { color: #0b0e14; background: var(--acid); }
.pill.off { color: #fff; background: var(--danger); }
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.body { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; }
.card { border: 1px solid var(--line); background: var(--panel); border-radius: 14px; padding: 16px; }
.site { display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; }
.site .host { font: 600 13px/1.3 ui-monospace, "Cascadia Code", Consolas, monospace; color: var(--mut); overflow-wrap: anywhere; }
.site .num { font-size: 40px; font-weight: 700; line-height: 1.1; color: var(--blue); font-variant-numeric: tabular-nums; }
.site .num.allowed { color: var(--dim); }
.site .lbl { color: var(--mut); }
.site .sub { color: var(--dim); font-size: 11.5px; }
.btn { border: 1px solid var(--line); background: var(--panel2); color: var(--ink); padding: 9px 16px; border-radius: 10px;
font: inherit; font-weight: 600; cursor: pointer; }
.btn:hover { border-color: rgb(from var(--blue) r g b / .5); }
.btn.primary { background: var(--blue); color: #fff; border-color: transparent; }
.btn.quiet { background: transparent; }
.btn:disabled { opacity: .5; cursor: default; }
.btn.wide { margin-top: 10px; min-width: 200px; }
.row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.row + .row { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }
.row .k { font-weight: 600; }
.row .d { color: var(--dim); font-size: 11.5px; }
.row .v { color: var(--mut); font-variant-numeric: tabular-nums; text-align: right; }
.switch { position: relative; width: 40px; height: 22px; border-radius: 11px; background: var(--dim); border: none; cursor: pointer; flex: none; transition: background .15s; }
.switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: left .15s; }
.switch.on { background: var(--acid); } .switch.on::after { left: 20px; }
.note { color: var(--dim); font-size: 11.5px; line-height: 1.45; }
.note.warn { color: var(--warn); }
h2 { margin: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--dim); }
< / style >
< / head >
< body >
< header > < span > 🛡< / span > < h1 > Shield< / h1 > < span class = "pill" id = "pill" > < span class = "dot" > < / span > < span id = "pillText" > …< / span > < / span > < / header >
< div class = "body" >
< div class = "card site" id = "siteCard" >
< div class = "lbl" id = "siteLbl" > Open a site to see what Shield stops there.< / div >
< / div >
< div class = "card" >
< div class = "row" >
< div > < div class = "k" > Block trackers and ads< / div > < div class = "d" > On every site, using EasyList and EasyPrivacy.< / div > < / div >
< button class = "switch" id = "enabled" role = "switch" aria-checked = "true" title = "Turn Shield on or off" > < / button >
< / div >
< div class = "row" >
< div > < div class = "k" > Blocked since install< / div > < div class = "d" > Requests that never left the browser.< / div > < / div >
< div class = "v" id = "total" > 0< / div >
< / div >
< / div >
< div class = "card" >
< h2 > Rules< / h2 >
< div id = "lists" > < / div >
< div class = "row" style = "margin-top:12px; padding-top:12px; border-top:1px solid var(--line)" >
< div class = "note" id = "updated" > < / div >
< button class = "btn quiet" id = "refresh" > Update now< / button >
< / div >
< / div >
2026-09-27 20:37:30 +02:00
< div class = "note" > < a href = "#" id = "toSettings" style = "color:var(--blue)" > Manage in Settings< / a > · Shield stops requests to known tracking and advertising hosts before they leave Theseus. It does not hide leftover empty ad boxes yet. If a site misbehaves, allow it here and reload.< / div >
2026-09-27 19:43:14 +02:00
< / div >
< script >
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s ?? "").replace(/[& < >"']/g, (c) => ({ "&":"& ","< ":"< ",">":"> ",'"':"" ","'":"' " })[c]);
const ago = (t) => { if (!t) return "bundled with the add-on"; const m = Math.round((Date.now() - t) / 60000); if (m < 2 ) return " updated just now " ; if ( m < 90 ) return ` updated $ { m } min ago ` ; const h = Math.round(m / 60 ) ; if ( h < 36 ) return ` updated $ { h } h ago ` ; return ` updated $ { Math . round ( h / 24 ) } days ago ` ; } ;
let state = null, hintHost = null;
function render(s) {
state = s;
const on = s.enabled & & s.ready;
$("pill").className = "pill " + (s.enabled ? (s.ready ? "on" : "") : "off");
$("pillText").textContent = !s.enabled ? "Off" : !s.ready ? "Loading" : "On";
$("enabled").classList.toggle("on", s.enabled); $("enabled").setAttribute("aria-checked", String(s.enabled));
$("total").textContent = Number(s.blockedTotal || 0).toLocaleString();
const card = $("siteCard");
if (!s.site) {
card.innerHTML = `< div class = "lbl" > Open a site to see what Shield stops there.< / div > `;
} else if (s.site.allowed) {
card.innerHTML = `< div class = "host" > ${esc(s.site.host)}< / div > < div class = "num allowed" > —< / div > < div class = "lbl" > Allowed: nothing is blocked on this site.< / div >
< button class = "btn primary wide" id = "siteBtn" > Block on this site< / button > `;
} else {
card.innerHTML = `< div class = "host" > ${esc(s.site.host)}< / div > < div class = "num" > ${on ? Number(s.site.blocked).toLocaleString() : "—"}< / div >
< div class = "lbl" > ${on ? (s.site.blocked === 1 ? "request blocked on this page" : "requests blocked on this page") : "Shield is off"}< / div >
< div class = "sub" > ${esc(s.site.requests)} requests seen< / div >
< button class = "btn wide" id = "siteBtn" > Allow this site< / button > `;
}
const btn = $("siteBtn");
if (btn) btn.onclick = async () => {
const next = !s.site.allowed;
hintHost = s.site.host;
await window.silentmode.invoke("allow-site", { host: s.site.host, allowed: next });
};
if (s.site & & hintHost === s.site.host) card.insertAdjacentHTML("beforeend", `< div class = "sub" > Reload the page to apply.< / div > `);
else hintHost = null;
const L = s.lists || { versions: {}, titles: {} };
$("lists").innerHTML = Object.keys(L.titles || {}).map((id) => `< div class = "row" > < div class = "k" > ${esc(L.titles[id])}< / div > < div class = "v" > ${esc(L.versions[id] || "—")}< / div > < / div > `).join("");
$("updated").textContent = (L.source === "online" ? "Rules " + ago(L.updatedAt) : "Rules bundled with the add-on") + (s.refreshing ? " · refreshing…" : "");
$("refresh").disabled = !!s.refreshing;
}
$("enabled").onclick = () => window.silentmode.invoke("set-enabled", { enabled: !(state & & state.enabled) }).then(render);
2026-09-27 20:37:30 +02:00
$("toSettings").onclick = (e) => { e.preventDefault(); window.silentmode.invoke("open-settings"); };
2026-09-27 19:43:14 +02:00
$("refresh").onclick = () => { $("refresh").disabled = true; window.silentmode.invoke("refresh-lists").then((r) => { if (r & & !r.ok) $("updated").textContent = "Update failed: " + (r.error || "unknown"); }); };
window.silentmode.on("state", render);
window.silentmode.invoke("state").then(render);
window.silentmode.onVisibility((v) => { if (v) window.silentmode.invoke("state").then(render); });
< / script >
< / body >
< / html >