Files
teslayoutube/static/pin.html
Erhan Keseli 74f49c7712 Initial commit: ASCILINE YouTube Streamer
ASCII-art YouTube streaming for the Tesla in-car browser.

- FastAPI server on a Mac mini, no Docker.
- yt-dlp resolver: ID/URL/search.
- ffmpeg with -re -fps_mode cfr for source-paced video; trivial drain
  consumer.  Separate ffmpeg for AAC/ADTS audio.
- Vendored ASCILINE renderer (MIT) for the binary wire protocol; pure
  fillText color path, on-demand selection flush.
- HMAC PIN-gated cookie; Secure flag scheme-aware so /audio works on
  plain http during local dev.
- LOW preset (120x50 24fps) verified clean on M4: FPS 24/24, JIT ~42ms.
2026-06-13 18:05:19 +02:00

286 lines
7.3 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ASCILINE — Enter PIN</title>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<style>
/* Inline minimal theme — matches style.css variables without importing it
so this page loads before /static/ is auth-gated. */
:root {
--bg: #000000;
--bg2: #0d0d0d;
--bg3: #1a1a1a;
--border: #2a2a2a;
--border-hi: #3d3d3d;
--accent: #F5A623;
--accent-dim:#b87a18;
--fg: #e8e8e8;
--fg-dim: #888;
--fg-faint: #444;
--red: #e84040;
--green: #3ecf6e;
--font-ui: 'Arial Narrow', Arial, sans-serif;
--font-mono: 'Courier New', monospace;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: var(--bg);
color: var(--fg);
font-family: var(--font-ui);
display: flex;
align-items: center;
justify-content: center;
-webkit-font-smoothing: antialiased;
}
#card {
width: 320px;
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
}
#brand {
font-size: 28px;
font-weight: 700;
letter-spacing: 6px;
color: var(--accent);
margin-bottom: 6px;
user-select: none;
}
#subtitle {
font-size: 12px;
letter-spacing: 3px;
color: var(--fg-faint);
text-transform: uppercase;
margin-bottom: 32px;
user-select: none;
}
/* PIN dots */
#pin-display {
display: flex;
gap: 14px;
margin-bottom: 24px;
min-height: 20px;
align-items: center;
justify-content: center;
}
.pin-dot {
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid var(--border-hi);
background: transparent;
transition: background 120ms, border-color 120ms;
}
.pin-dot.filled {
background: var(--accent);
border-color: var(--accent);
}
/* Status message */
#msg {
height: 22px;
font-size: 13px;
font-family: var(--font-mono);
letter-spacing: 0.5px;
color: var(--fg-dim);
margin-bottom: 20px;
text-align: center;
}
#msg.error { color: var(--red); }
#msg.locked { color: var(--red); }
/* Keypad */
#keypad {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
width: 100%;
}
.key {
height: 72px;
background: var(--bg3);
border: 1.5px solid var(--border-hi);
border-radius: 8px;
color: var(--fg);
font-size: 26px;
font-weight: 600;
font-family: var(--font-ui);
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
transition: background 80ms, border-color 80ms, transform 60ms;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.key:active:not(:disabled) {
background: var(--accent-dim);
border-color: var(--accent);
transform: scale(0.94);
}
.key:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.key-clear {
font-size: 16px;
font-weight: 700;
letter-spacing: 1px;
color: var(--fg-dim);
}
.key-zero {
/* 0 sits in the middle column of the last row */
}
.key-submit {
background: var(--accent);
border-color: var(--accent);
color: #000;
font-size: 20px;
}
.key-submit:hover:not(:disabled) {
background: #ffb838;
border-color: #ffb838;
}
</style>
</head>
<body>
<div id="card">
<div id="brand">ASCILINE</div>
<div id="subtitle">Enter PIN to continue</div>
<div id="pin-display"></div>
<div id="msg"></div>
<div id="keypad">
<button class="key" data-digit="1">1</button>
<button class="key" data-digit="2">2</button>
<button class="key" data-digit="3">3</button>
<button class="key" data-digit="4">4</button>
<button class="key" data-digit="5">5</button>
<button class="key" data-digit="6">6</button>
<button class="key" data-digit="7">7</button>
<button class="key" data-digit="8">8</button>
<button class="key" data-digit="9">9</button>
<button class="key key-clear" data-action="clear">CLR</button>
<button class="key key-zero" data-digit="0">0</button>
<button class="key key-submit" data-action="submit">&#10003;</button>
</div>
</div>
<script>
(function () {
const MAX_LEN = 8;
let pin = "";
let locked = false;
const display = document.getElementById("pin-display");
const msg = document.getElementById("msg");
const keys = document.querySelectorAll(".key");
function renderDots(len) {
display.innerHTML = "";
for (let i = 0; i < Math.max(len, 4); i++) {
const d = document.createElement("div");
d.className = "pin-dot" + (i < len ? " filled" : "");
display.appendChild(d);
}
}
function setMsg(text, cls) {
msg.textContent = text;
msg.className = cls || "";
}
function setDisabled(state) {
keys.forEach(k => k.disabled = state);
}
renderDots(0);
document.getElementById("keypad").addEventListener("click", function (e) {
if (locked) return;
const btn = e.target.closest(".key");
if (!btn || btn.disabled) return;
const digit = btn.dataset.digit;
const action = btn.dataset.action;
if (digit !== undefined) {
if (pin.length < MAX_LEN) {
pin += digit;
renderDots(pin.length);
setMsg("");
}
} else if (action === "clear") {
pin = "";
renderDots(0);
setMsg("");
} else if (action === "submit") {
if (pin.length === 0) { setMsg("Enter your PIN", ""); return; }
submit();
}
});
// Also support physical keyboard for desktop testing.
document.addEventListener("keydown", function (e) {
if (locked) return;
if (e.key >= "0" && e.key <= "9") {
if (pin.length < MAX_LEN) {
pin += e.key;
renderDots(pin.length);
setMsg("");
}
} else if (e.key === "Backspace") {
pin = pin.slice(0, -1);
renderDots(pin.length);
setMsg("");
} else if (e.key === "Enter") {
submit();
}
});
function submit() {
setDisabled(true);
setMsg("Checking…", "");
fetch("/api/auth", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ pin: pin }),
})
.then(function (r) { return r.json().then(function (d) { return { status: r.status, data: d }; }); })
.then(function (res) {
if (res.status === 200) {
setMsg("OK", "");
// Small delay so the user sees "OK" before redirect.
setTimeout(function () { window.location.replace("/"); }, 300);
} else if (res.status === 429) {
locked = true;
setDisabled(true);
const secs = res.data.retry_after || 600;
setMsg("Too many attempts. Wait " + Math.ceil(secs / 60) + " min.", "locked");
} else {
pin = "";
renderDots(0);
setDisabled(false);
setMsg("Wrong PIN", "error");
}
})
.catch(function () {
setDisabled(false);
setMsg("Network error", "error");
});
}
})();
</script>
</body>
</html>