ui: proper login page + inline SVG favicon
Replaced window.prompt() with a real login screen: - Centered card, brand mark, hidden password-type input - Inline error state on invalid token - Overlay shown until token validated, then app rendered Favicon: inline data-URI SVG with concentric radio-wave rings, zero external requests, no /favicon.ico 404 in logs.
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<title>SNEK Dashboard</title>
|
<title>SNEK Dashboard</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='7' fill='%230f172a'/><circle cx='16' cy='16' r='3' fill='%2338bdf8'/><circle cx='16' cy='16' r='7' fill='none' stroke='%2338bdf8' stroke-width='1.5' opacity='0.6'/><circle cx='16' cy='16' r='11' fill='none' stroke='%2338bdf8' stroke-width='1' opacity='0.35'/></svg>">
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
@@ -18,7 +19,32 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen">
|
<body class="min-h-screen">
|
||||||
<div class="max-w-7xl mx-auto px-4 md:px-8 py-6 md:py-10">
|
|
||||||
|
<!-- Login overlay -->
|
||||||
|
<div id="login-overlay" class="hidden fixed inset-0 bg-slate-100 z-50 items-center justify-center px-4">
|
||||||
|
<div class="w-full max-w-sm bg-white border border-slate-200 rounded-xl p-8 shadow-sm">
|
||||||
|
<div class="flex items-center gap-3 mb-6">
|
||||||
|
<svg viewBox="0 0 32 32" class="w-8 h-8"><rect width="32" height="32" rx="7" fill="#0f172a"/><circle cx="16" cy="16" r="3" fill="#38bdf8"/><circle cx="16" cy="16" r="7" fill="none" stroke="#38bdf8" stroke-width="1.5" opacity="0.6"/><circle cx="16" cy="16" r="11" fill="none" stroke="#38bdf8" stroke-width="1" opacity="0.35"/></svg>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold text-slate-900">SNEK Dashboard</div>
|
||||||
|
<div class="text-xs text-slate-500">Sign in with your access token</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form id="login-form" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="login-token" class="block text-xs font-medium text-slate-700 mb-1.5">Access token</label>
|
||||||
|
<input id="login-token" type="password" autocomplete="off" required
|
||||||
|
class="mono w-full text-sm border border-slate-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-400" />
|
||||||
|
</div>
|
||||||
|
<div id="login-error" class="hidden text-xs text-rose-600 bg-rose-50 border border-rose-200 rounded-md px-3 py-2"></div>
|
||||||
|
<button type="submit" class="w-full bg-slate-900 text-white text-sm font-medium py-2 rounded-md hover:bg-slate-800 transition">
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="app" class="max-w-7xl mx-auto px-4 md:px-8 py-6 md:py-10">
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="mb-8">
|
<header class="mb-8">
|
||||||
@@ -78,20 +104,40 @@ const TOKEN_KEY = 'snek_dashboard_token';
|
|||||||
let token = localStorage.getItem(TOKEN_KEY) || '';
|
let token = localStorage.getItem(TOKEN_KEY) || '';
|
||||||
|
|
||||||
async function ensureToken() {
|
async function ensureToken() {
|
||||||
// Probe if the server requires a token
|
|
||||||
const probe = await fetch('/api/devices', { headers: authHeaders() });
|
const probe = await fetch('/api/devices', { headers: authHeaders() });
|
||||||
if (probe.ok) return;
|
if (probe.ok) return;
|
||||||
if (probe.status !== 401) return;
|
if (probe.status !== 401) return;
|
||||||
while (true) {
|
|
||||||
const t = window.prompt('Access token');
|
const overlay = document.getElementById('login-overlay');
|
||||||
if (t === null) return;
|
const form = document.getElementById('login-form');
|
||||||
|
const input = document.getElementById('login-token');
|
||||||
|
const error = document.getElementById('login-error');
|
||||||
|
const app = document.getElementById('app');
|
||||||
|
|
||||||
|
overlay.className = 'fixed inset-0 bg-slate-100 z-50 flex items-center justify-center px-4';
|
||||||
|
app.classList.add('hidden');
|
||||||
|
input.focus();
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const t = input.value.trim();
|
||||||
|
error.classList.add('hidden');
|
||||||
const r = await fetch('/api/devices', { headers: { Authorization: 'Bearer ' + t } });
|
const r = await fetch('/api/devices', { headers: { Authorization: 'Bearer ' + t } });
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
token = t.trim();
|
token = t;
|
||||||
localStorage.setItem(TOKEN_KEY, token);
|
localStorage.setItem(TOKEN_KEY, token);
|
||||||
return;
|
overlay.className = 'hidden';
|
||||||
}
|
app.classList.remove('hidden');
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
error.textContent = 'Invalid token. Please try again.';
|
||||||
|
error.classList.remove('hidden');
|
||||||
|
input.value = '';
|
||||||
|
input.focus();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
|
|||||||
Reference in New Issue
Block a user