CAR-KEY-KILLER: Complete jamming system with enhanced error handling and dark hacker documentation
- Validated all jamming functions line-by-line - Added individual radio status tracking with error reporting - Enhanced web interface with live status updates - Rewrote README with dark/hacker aesthetic - Added detailed effectiveness explanation (30-100m range) - Fixed state management and error handling - Tested compilation with PlatformIO - Added comprehensive legal warnings - Added .gitignore for build artifacts
This commit is contained in:
497
safe-ui-polish/index.html
Normal file
497
safe-ui-polish/index.html
Normal file
@@ -0,0 +1,497 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Embedded Device Dashboard</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b1020;
|
||||
--panel: #121a2f;
|
||||
--panel-2: #0f1629;
|
||||
--text: #e8edf7;
|
||||
--muted: #a5b4cf;
|
||||
--border: #283555;
|
||||
--accent: #4f8cff;
|
||||
--accent-2: #7aa6ff;
|
||||
--good: #1f9d55;
|
||||
--warn: #c78b19;
|
||||
--bad: #d14343;
|
||||
--shadow: 0 12px 28px rgba(0, 0, 0, 0.28);
|
||||
--radius: 12px;
|
||||
--radius-sm: 8px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: radial-gradient(1200px 700px at 20% -10%, #1a2550 0%, var(--bg) 50%);
|
||||
color: var(--text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.35rem;
|
||||
margin: 0;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--muted);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--warn);
|
||||
box-shadow: 0 0 0 5px rgba(199, 139, 25, 0.18);
|
||||
transition: all 180ms ease;
|
||||
}
|
||||
|
||||
.dot.ok {
|
||||
background: var(--good);
|
||||
box-shadow: 0 0 0 5px rgba(31, 157, 85, 0.2);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background: linear-gradient(180deg, var(--panel), var(--panel-2));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.metrics {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.metric {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.metric .label {
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.metric .value {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
width: 100%;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
button {
|
||||
appearance: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
background: var(--accent);
|
||||
transition: transform 100ms ease, filter 150ms ease, border-color 150ms ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
filter: brightness(1.06);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
button.critical {
|
||||
background: var(--bad);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
input:focus-visible {
|
||||
outline: 2px solid var(--accent-2);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 999px;
|
||||
padding: 7px 11px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
min-height: 280px;
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: #0c1222;
|
||||
padding: 12px;
|
||||
line-height: 1.4;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 0.8rem;
|
||||
color: #d9e5ff;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 14px;
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.toasts {
|
||||
position: fixed;
|
||||
right: 14px;
|
||||
bottom: 14px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
z-index: 50;
|
||||
width: min(360px, calc(100vw - 28px));
|
||||
}
|
||||
|
||||
.toast {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: #111b33;
|
||||
color: var(--text);
|
||||
padding: 10px 12px;
|
||||
box-shadow: var(--shadow);
|
||||
font-size: 0.88rem;
|
||||
animation: slideIn 140ms ease;
|
||||
}
|
||||
|
||||
.toast.ok { border-left: 4px solid var(--good); }
|
||||
.toast.warn { border-left: 4px solid var(--warn); }
|
||||
.toast.err { border-left: 4px solid var(--bad); }
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateY(8px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<div class="topbar">
|
||||
<div>
|
||||
<h1>Embedded Device Dashboard</h1>
|
||||
<div class="sub">Polished, responsive, and API-ready web interface</div>
|
||||
</div>
|
||||
<div class="chip">
|
||||
<span id="onlineDot" class="dot"></span>
|
||||
<span id="onlineText">Connecting...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Live Status</h2>
|
||||
<div id="statePill" class="status-pill">
|
||||
<span class="dot" id="stateDot"></span>
|
||||
<span id="stateText">Unknown</span>
|
||||
</div>
|
||||
<div class="metrics">
|
||||
<div class="metric">
|
||||
<div class="label">Uptime</div>
|
||||
<div class="value" id="uptime">--</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">Free Heap</div>
|
||||
<div class="value" id="heap">--</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">Power</div>
|
||||
<div class="value" id="powerMetric">--</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Controls</h2>
|
||||
<div class="controls">
|
||||
<div class="field">
|
||||
<label for="powerSlider">Power Level: <span id="powerLabel">10</span></label>
|
||||
<input id="powerSlider" type="range" min="0" max="10" step="1" value="10">
|
||||
</div>
|
||||
<div class="row">
|
||||
<button id="toggleBtn">Toggle</button>
|
||||
<button id="applyBtn" class="secondary">Apply Settings</button>
|
||||
<button id="refreshBtn" class="secondary">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="card" style="margin-top: 16px;">
|
||||
<h2>System Log</h2>
|
||||
<pre id="logView">Loading logs...</pre>
|
||||
<div class="footer">Auto-refresh every 1 second</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="toasts" class="toasts" aria-live="polite" aria-atomic="true"></div>
|
||||
|
||||
<script>
|
||||
const ui = {
|
||||
onlineDot: document.getElementById("onlineDot"),
|
||||
onlineText: document.getElementById("onlineText"),
|
||||
stateDot: document.getElementById("stateDot"),
|
||||
stateText: document.getElementById("stateText"),
|
||||
uptime: document.getElementById("uptime"),
|
||||
heap: document.getElementById("heap"),
|
||||
powerMetric: document.getElementById("powerMetric"),
|
||||
powerSlider: document.getElementById("powerSlider"),
|
||||
powerLabel: document.getElementById("powerLabel"),
|
||||
toggleBtn: document.getElementById("toggleBtn"),
|
||||
applyBtn: document.getElementById("applyBtn"),
|
||||
refreshBtn: document.getElementById("refreshBtn"),
|
||||
logView: document.getElementById("logView"),
|
||||
toasts: document.getElementById("toasts")
|
||||
};
|
||||
|
||||
const state = {
|
||||
enabled: false,
|
||||
pollHandle: null,
|
||||
inFlight: false
|
||||
};
|
||||
|
||||
function showToast(message, kind = "ok", timeout = 2600) {
|
||||
const el = document.createElement("div");
|
||||
el.className = "toast " + kind;
|
||||
el.textContent = message;
|
||||
ui.toasts.appendChild(el);
|
||||
window.setTimeout(() => {
|
||||
el.remove();
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
function setOnline(online) {
|
||||
ui.onlineDot.classList.toggle("ok", online);
|
||||
ui.onlineText.textContent = online ? "Device online" : "Device offline";
|
||||
}
|
||||
|
||||
function fmtUptime(ms) {
|
||||
const s = Math.max(0, Math.floor(ms / 1000));
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = s % 60;
|
||||
if (h > 0) return h + "h " + m + "m " + sec + "s";
|
||||
if (m > 0) return m + "m " + sec + "s";
|
||||
return sec + "s";
|
||||
}
|
||||
|
||||
function renderStatus(telemetry) {
|
||||
state.enabled = Boolean(telemetry.jamming_enabled);
|
||||
const active = state.enabled;
|
||||
ui.stateDot.classList.toggle("ok", active);
|
||||
ui.stateText.textContent = active ? "ACTIVE" : "INACTIVE";
|
||||
ui.toggleBtn.textContent = active ? "Stop" : "Start";
|
||||
ui.uptime.textContent = fmtUptime(Number(telemetry.uptime_ms || 0));
|
||||
ui.heap.textContent = String(telemetry.free_heap ?? "--");
|
||||
ui.powerMetric.textContent = String(telemetry.jam_power ?? "--") + " dBm";
|
||||
ui.powerSlider.value = String(telemetry.jam_power ?? 0);
|
||||
ui.powerLabel.textContent = ui.powerSlider.value;
|
||||
}
|
||||
|
||||
async function fetchJson(path, options) {
|
||||
const res = await fetch(path, options);
|
||||
if (!res.ok) throw new Error(path + " failed with " + res.status);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function fetchText(path, options) {
|
||||
const res = await fetch(path, options);
|
||||
if (!res.ok) throw new Error(path + " failed with " + res.status);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
async function refreshAll(silent = false) {
|
||||
if (state.inFlight) return;
|
||||
state.inFlight = true;
|
||||
try {
|
||||
const [telemetry, logs] = await Promise.all([
|
||||
fetchJson("/api/telemetry"),
|
||||
fetchText("/api/log")
|
||||
]);
|
||||
renderStatus(telemetry);
|
||||
ui.logView.textContent = logs || "(No log entries)";
|
||||
ui.logView.scrollTop = ui.logView.scrollHeight;
|
||||
setOnline(true);
|
||||
} catch (err) {
|
||||
setOnline(false);
|
||||
if (!silent) showToast("Refresh failed: " + err.message, "err", 3200);
|
||||
} finally {
|
||||
state.inFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggle() {
|
||||
ui.toggleBtn.disabled = true;
|
||||
try {
|
||||
const out = await fetchJson("/api/toggle", { method: "POST" });
|
||||
state.enabled = Boolean(out.enabled);
|
||||
showToast(state.enabled ? "Started successfully" : "Stopped successfully");
|
||||
await refreshAll(true);
|
||||
} catch (err) {
|
||||
showToast("Toggle failed: " + err.message, "err", 3200);
|
||||
} finally {
|
||||
ui.toggleBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onApply() {
|
||||
ui.applyBtn.disabled = true;
|
||||
try {
|
||||
const payload = { power: Number(ui.powerSlider.value) };
|
||||
const out = await fetchJson("/api/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (out && out.success) {
|
||||
showToast("Settings applied");
|
||||
await refreshAll(true);
|
||||
} else {
|
||||
throw new Error("settings response invalid");
|
||||
}
|
||||
} catch (err) {
|
||||
showToast("Apply failed: " + err.message, "err", 3200);
|
||||
} finally {
|
||||
ui.applyBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
ui.powerSlider.addEventListener("input", () => {
|
||||
ui.powerLabel.textContent = ui.powerSlider.value;
|
||||
});
|
||||
ui.toggleBtn.addEventListener("click", onToggle);
|
||||
ui.applyBtn.addEventListener("click", onApply);
|
||||
ui.refreshBtn.addEventListener("click", () => refreshAll(false));
|
||||
window.addEventListener("visibilitychange", () => {
|
||||
if (document.visibilityState === "visible") refreshAll(true);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
bindEvents();
|
||||
await refreshAll(true);
|
||||
state.pollHandle = window.setInterval(() => refreshAll(true), 1000);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
showToast("Startup failed: " + err.message, "err", 4000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user