VoxIP Kiosk OS 1.0.0: Raspberry Pi kiosk with web management
Chromium kiosk without a desktop for the Raspberry Pi 3, a Flask web interface on port 8080 (kiosk, system and network management), a restricted root helper, network changes with automatic rollback, a healthcheck timer, install/update/uninstall scripts and a pi-gen image build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
a33c4822bf
96 files changed
+7535
No files matched your search
@@ -0,0 +1,26 @@
|
||||
/* Local pages shown on the kiosk screen itself (file://). Large type, readable from a distance. */
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; overflow: hidden; cursor: none; user-select: none; }
|
||||
body {
|
||||
font-family: system-ui, "DejaVu Sans", "Liberation Sans", Arial, sans-serif;
|
||||
background: radial-gradient(ellipse at 30% 20%, #163a6b 0%, #0b2545 45%, #061529 100%);
|
||||
color: #eaf1fb;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.stage { text-align: center; padding: 4vh 6vw; max-width: 1400px; transition: transform 2s ease; }
|
||||
.logo { width: 9vmin; height: 9vmin; min-width: 64px; min-height: 64px; margin-bottom: 3vh; opacity: .95; }
|
||||
h1 { font-size: clamp(28px, 5vmin, 72px); margin: 0 0 2vh; font-weight: 700; letter-spacing: -.01em; }
|
||||
p { font-size: clamp(16px, 2.6vmin, 36px); margin: 0 0 1.5vh; color: #b9cbe4; }
|
||||
.device { font-size: clamp(14px, 2.2vmin, 28px); color: #7f9cc4; text-transform: uppercase; letter-spacing: .12em; margin-bottom: 2vh; }
|
||||
.clock { font-size: clamp(48px, 12vmin, 180px); font-weight: 300; font-variant-numeric: tabular-nums; margin: 3vh 0 1vh; color: #fff; }
|
||||
.date { font-size: clamp(18px, 3vmin, 40px); color: #b9cbe4; }
|
||||
.status { display: inline-flex; align-items: center; gap: 1.2vmin; margin-top: 4vh; padding: 1.2vmin 2.4vmin; border-radius: 999px; background: rgba(255, 255, 255, .08); font-size: clamp(14px, 2.2vmin, 28px); color: #d6e2f3; }
|
||||
.dot { width: 1.6vmin; height: 1.6vmin; min-width: 10px; min-height: 10px; border-radius: 50%; background: #f0a020; animation: pulse 1.6s infinite; }
|
||||
.spinner { width: 7vmin; height: 7vmin; margin: 4vh auto 0; border-radius: 50%; border: .8vmin solid rgba(255, 255, 255, .15); border-top-color: #4f9cf9; animation: spin 1s linear infinite; }
|
||||
.code { display: inline-block; font-family: "DejaVu Sans Mono", ui-monospace, monospace; font-size: clamp(40px, 10vmin, 150px); font-weight: 700; letter-spacing: .12em; padding: 2vh 4vw; margin: 2vh 0 3vh; border-radius: 2vmin; background: rgba(255, 255, 255, .1); border: 2px solid rgba(79, 156, 249, .6); color: #fff; }
|
||||
.urls { list-style: none; padding: 0; margin: 1vh 0 3vh; }
|
||||
.urls li { font-family: "DejaVu Sans Mono", ui-monospace, monospace; font-size: clamp(18px, 3.4vmin, 48px); color: #fff; margin: .8vh 0; }
|
||||
.hint { font-size: clamp(14px, 2vmin, 26px); color: #7f9cc4; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes pulse { 50% { opacity: .3; } }
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="VoxIP Kiosk">
|
||||
<rect x="2" y="4" width="28" height="19" rx="3" fill="#1d6fd8"/>
|
||||
<rect x="5" y="7" width="22" height="13" rx="1.5" fill="#0b2545"/>
|
||||
<path d="M10 10.5l3 6 3-6M18.5 10.5v6M21.5 16.5v-6h2a1.8 1.8 0 010 3.6h-2" fill="none" stroke="#fff" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="13" y="23" width="6" height="3" fill="#1d6fd8"/>
|
||||
<rect x="9" y="26" width="14" height="2.5" rx="1.25" fill="#1d6fd8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 543 B |
@@ -0,0 +1,56 @@
|
||||
<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Geen verbinding</title>
|
||||
<link rel="stylesheet" href="kiosk.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="stage" id="stage">
|
||||
<img class="logo" src="logo.svg" alt="">
|
||||
<div class="device" id="device"></div>
|
||||
<h1>De informatie is tijdelijk niet beschikbaar</h1>
|
||||
<p id="detail">Er is op dit moment geen verbinding met de server.</p>
|
||||
<div class="clock" id="clock">--:--</div>
|
||||
<div class="date" id="date"></div>
|
||||
<div class="status"><span class="dot" aria-hidden="true"></span><span id="status">Automatisch opnieuw verbinden…</span></div>
|
||||
</main>
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const fmtTime = new Intl.DateTimeFormat("nl-NL", { hour: "2-digit", minute: "2-digit" });
|
||||
const fmtDate = new Intl.DateTimeFormat("nl-NL", { weekday: "long", day: "numeric", month: "long", year: "numeric" });
|
||||
document.getElementById("device").textContent = params.get("name") || "";
|
||||
const host = params.get("host");
|
||||
const reason = params.get("reason");
|
||||
if (host) {
|
||||
document.getElementById("detail").textContent =
|
||||
"Geen verbinding met " + host + (reason ? " (" + reason + ")" : "") + ".";
|
||||
}
|
||||
const since = Number(params.get("since")) * 1000;
|
||||
|
||||
function tick() {
|
||||
const now = new Date();
|
||||
document.getElementById("clock").textContent = fmtTime.format(now);
|
||||
const date = fmtDate.format(now);
|
||||
document.getElementById("date").textContent = date.charAt(0).toUpperCase() + date.slice(1);
|
||||
if (since) {
|
||||
const minutes = Math.floor((now - since) / 60000);
|
||||
document.getElementById("status").textContent = minutes >= 1
|
||||
? "Offline sinds " + fmtTime.format(new Date(since)) + " — automatisch opnieuw verbinden…"
|
||||
: "Automatisch opnieuw verbinden…";
|
||||
}
|
||||
}
|
||||
tick();
|
||||
setInterval(tick, 1000);
|
||||
|
||||
// Shift the content slowly to avoid burn-in on screens that stay offline for long.
|
||||
let step = 0;
|
||||
setInterval(() => {
|
||||
step = (step + 1) % 8;
|
||||
const dx = [0, 12, 24, 12, 0, -12, -24, -12][step];
|
||||
const dy = [0, -8, 0, 8, 0, -8, 0, 8][step];
|
||||
document.getElementById("stage").style.transform = "translate(" + dx + "px," + dy + "px)";
|
||||
}, 60000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>VoxIP Kiosk — eerste configuratie</title>
|
||||
<link rel="stylesheet" href="kiosk.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="stage">
|
||||
<img class="logo" src="logo.svg" alt="">
|
||||
<h1>Welkom bij VoxIP Kiosk OS</h1>
|
||||
<p>Open de beheerinterface op een computer, tablet of telefoon in hetzelfde netwerk:</p>
|
||||
<ul class="urls" id="urls"></ul>
|
||||
<p>en voer deze setupcode in om een beheerderswachtwoord in te stellen:</p>
|
||||
<div class="code" id="code">—</div>
|
||||
<p class="hint">De code is eenmalig en verdwijnt zodra het wachtwoord is ingesteld.</p>
|
||||
</main>
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const scheme = params.get("scheme") === "https" ? "https" : "http";
|
||||
const port = params.get("port") || "8080";
|
||||
const suffix = (scheme === "http" && port === "80") || (scheme === "https" && port === "443") ? "" : ":" + port;
|
||||
const urls = [];
|
||||
if (params.get("host")) urls.push(scheme + "://" + params.get("host") + ".local" + suffix);
|
||||
(params.get("ips") || "").split(",").filter(Boolean).forEach((ip) => urls.push(scheme + "://" + ip + suffix));
|
||||
const list = document.getElementById("urls");
|
||||
if (!urls.length) urls.push("Nog geen netwerkverbinding — sluit een netwerkkabel aan");
|
||||
urls.forEach((u) => {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = u;
|
||||
list.appendChild(li);
|
||||
});
|
||||
document.getElementById("code").textContent = params.get("code") || "zie: sudo voxip-kiosk-ctl setup-code";
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>VoxIP Kiosk</title>
|
||||
<link rel="stylesheet" href="kiosk.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="stage">
|
||||
<img class="logo" src="logo.svg" alt="">
|
||||
<div class="device" id="device"></div>
|
||||
<h1>Bezig met laden…</h1>
|
||||
<p>De pagina wordt geopend zodra de verbinding gereed is.</p>
|
||||
<div class="spinner" aria-hidden="true"></div>
|
||||
</main>
|
||||
<script>
|
||||
// The kiosk agent normally navigates away from this page within seconds.
|
||||
// If the agent is not running, open the configured URL ourselves.
|
||||
const params = new URLSearchParams(location.search);
|
||||
document.getElementById("device").textContent = params.get("name") || "";
|
||||
const next = params.get("next") || "";
|
||||
if (next.startsWith("https://")) {
|
||||
setTimeout(() => location.replace(next), 45000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user