Files
lagomareGates/src/static/admin.html
2026-05-10 11:58:56 +02:00

540 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0f0f1a" />
<title>Lagomare Gates - Admin</title>
<link rel="icon" type="image/svg+xml" href="/static/logo.svg" />
<link rel="stylesheet" href="/static/style.css" />
<style>
/* ── Admin login ────────────────────────────────────────────────────── */
#login-view {
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
#login-view h1 { font-size: 1.5rem; font-weight: 800; margin-bottom: .25rem; }
#login-view .sub { color: var(--text-muted); font-size: .9rem; margin-bottom: 2rem; }
#login-view .card { width: 100%; max-width: 360px; }
/* ── Admin shell ────────────────────────────────────────────────────── */
#admin-view {
display: flex;
flex-direction: column;
min-height: 100dvh;
}
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
background: var(--surface);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 10;
}
.app-header h2 { font-size: 1.05rem; font-weight: 800; }
/* ── Tabs ───────────────────────────────────────────────────────────── */
.tabs {
display: flex;
gap: 0;
background: var(--surface);
border-bottom: 1px solid var(--border);
overflow-x: auto;
scrollbar-width: none;
}
.tab-btn {
background: none;
border: none;
border-bottom: 3px solid transparent;
color: var(--text-muted);
cursor: pointer;
font-size: .9rem;
font-weight: 600;
padding: .85rem 1.5rem;
white-space: nowrap;
transition: color .15s, border-color .15s;
}
.tab-btn:hover { color: var(--text); }
.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
/* ── Tab panes ──────────────────────────────────────────────────────── */
.tab-content { flex: 1; padding: 1.5rem; max-width: 1200px; margin: 0 auto; width: 100%; }
.tab-pane { display: none; }
.tab-pane.active { display: block; }
/* ── Section header ─────────────────────────────────────────────────── */
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.section-header h3 { font-size: 1rem; font-weight: 700; }
</style>
</head>
<body>
<!-- ── Admin login ─────────────────────────────────────────────────────── -->
<div id="login-view">
<div style="font-size:2rem;margin-bottom:.5rem">🔐</div>
<h1>Admin Panel</h1>
<p class="sub">Lagomare Gates</p>
<div class="card">
<form id="login-form">
<div class="field">
<label for="admin-username">Username</label>
<input id="admin-username" type="text" autocomplete="username" />
</div>
<div class="field">
<label for="admin-password">Password</label>
<input id="admin-password" type="password" autocomplete="current-password" />
</div>
<p id="login-error" class="error-msg hidden"></p>
<button type="submit" class="btn btn-primary btn-full" style="margin-top:.25rem">Sign in</button>
</form>
</div>
</div>
<!-- ── Admin shell ─────────────────────────────────────────────────────── -->
<div id="admin-view" class="hidden">
<header class="app-header">
<h2>⚙️ Admin - Lagomare Gates</h2>
<div style="display:flex;align-items:center;gap:.75rem">
<span id="header-username" style="font-size:.85rem;color:var(--text-muted)"></span>
<button id="logout-btn" class="btn btn-ghost" style="font-size:.85rem;padding:.5rem 1rem">Logout</button>
</div>
</header>
<nav class="tabs">
<button class="tab-btn active" data-tab="keypasses">Keypasses</button>
<button class="tab-btn" data-tab="gates">Gates</button>
<button class="tab-btn admin-only" data-tab="credentials">AVConnect Credentials</button>
<button class="tab-btn" data-tab="stats">Statistics</button>
<button class="tab-btn admin-only" data-tab="admins">Admins</button>
<button class="tab-btn admin-only" data-tab="telegram">Notifications</button>
</nav>
<div class="tab-content">
<!-- ── Keypasses pane ─────────────────────────────────────────────── -->
<div id="tab-keypasses" class="tab-pane active">
<div class="section-header">
<h3>Keypasses</h3>
<button id="btn-new-keypass" class="btn btn-primary">+ New Keypass</button>
</div>
<div class="table-wrap card" style="padding:0">
<table id="keypasses-table">
<thead>
<tr>
<th>Code</th>
<th>Description</th>
<th>Gates</th>
<th>Expires</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody id="keypasses-body"></tbody>
</table>
</div>
</div>
<!-- ── Gates pane ─────────────────────────────────────────────────── -->
<div id="tab-gates" class="tab-pane">
<div class="section-header">
<h3>Gates</h3>
<button id="btn-new-gate" class="btn btn-primary admin-only">+ Add Gate</button>
</div>
<div class="table-wrap card" style="padding:0">
<table id="gates-table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Group</th>
<th>Type</th>
<th>AVConnect Macro ID</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody id="gates-body"></tbody>
</table>
</div>
</div>
<!-- ── Credentials pane ───────────────────────────────────────────── -->
<div id="tab-credentials" class="tab-pane">
<h3 style="margin-bottom:1rem">AVConnect Credentials</h3>
<div class="card" style="max-width:440px">
<form id="credentials-form">
<div class="field">
<label for="cred-username">Username</label>
<input id="cred-username" type="text" autocomplete="off" />
</div>
<div class="field">
<label for="cred-password">Password</label>
<input id="cred-password" type="password" autocomplete="new-password"
placeholder="Leave empty to keep current" />
</div>
<p id="cred-error" class="error-msg hidden"></p>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
<h3 style="margin:1.5rem 0 1rem">Mock Mode</h3>
<div class="card" style="max-width:440px">
<p style="color:var(--text-muted);font-size:.9rem;margin-bottom:1rem">
When enabled, gate open requests always succeed without contacting AVConnect.
</p>
<label style="display:flex;align-items:center;gap:.75rem;cursor:pointer;margin:0">
<input type="checkbox" id="mock-toggle" style="width:1.1rem;height:1.1rem;flex-shrink:0;cursor:pointer" />
<span style="font-weight:600">Enable mock AVConnect</span>
</label>
</div>
</div>
<!-- ── Statistics pane ───────────────────────────────────────────── -->
<div id="tab-stats" class="tab-pane">
<div class="section-header">
<h3>Gate Access Log</h3>
<button id="btn-refresh-stats" class="btn btn-ghost" style="font-size:.85rem;padding:.5rem 1rem">↻ Refresh</button>
</div>
<!-- Filter bar -->
<div style="display:flex;flex-wrap:wrap;gap:.75rem;margin-bottom:1rem;align-items:flex-end">
<div style="display:flex;flex-direction:column;gap:.3rem">
<label style="font-size:.8rem;font-weight:600;color:var(--text-muted)">Keypass code</label>
<input id="filter-keypass" type="text" placeholder="Any"
style="width:140px;font-family:monospace;text-transform:uppercase" autocomplete="off" />
</div>
<div style="display:flex;flex-direction:column;gap:.3rem">
<label style="font-size:.8rem;font-weight:600;color:var(--text-muted)">Gate</label>
<select id="filter-gate" style="width:160px">
<option value="">Any</option>
</select>
</div>
<div style="display:flex;flex-direction:column;gap:.3rem">
<label style="font-size:.8rem;font-weight:600;color:var(--text-muted)">Result</label>
<select id="filter-success" style="width:110px">
<option value="">Any</option>
<option value="true">Success</option>
<option value="false">Failed</option>
</select>
</div>
<div style="display:flex;flex-direction:column;gap:.3rem">
<label style="font-size:.8rem;font-weight:600;color:var(--text-muted)">From</label>
<input id="filter-from" type="datetime-local" style="width:180px" />
</div>
<div style="display:flex;flex-direction:column;gap:.3rem">
<label style="font-size:.8rem;font-weight:600;color:var(--text-muted)">To</label>
<input id="filter-to" type="datetime-local" style="width:180px" />
</div>
<button id="btn-stats-filter" class="btn btn-primary" style="font-size:.85rem;padding:.5rem 1rem">Filter</button>
<button id="btn-stats-reset" class="btn btn-ghost" style="font-size:.85rem;padding:.5rem 1rem">Reset</button>
</div>
<div class="table-wrap card" style="padding:0">
<table id="stats-table">
<thead>
<tr>
<th>Time</th>
<th>Keypass</th>
<th>Gate</th>
<th>IP</th>
<th>User Agent</th>
<th>Result</th>
</tr>
</thead>
<tbody id="stats-body"></tbody>
</table>
</div>
<!-- Pagination -->
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:1rem;font-size:.9rem;flex-wrap:wrap;gap:.5rem">
<span id="stats-total-label" style="color:var(--text-muted)"></span>
<div style="display:flex;gap:.5rem;align-items:center">
<button id="btn-stats-prev" class="btn btn-ghost" style="font-size:.85rem;padding:.4rem .9rem">← Prev</button>
<span id="stats-page-label" style="color:var(--text-muted);min-width:90px;text-align:center"></span>
<button id="btn-stats-next" class="btn btn-ghost" style="font-size:.85rem;padding:.4rem .9rem">Next →</button>
</div>
</div>
</div>
<!-- ── Admins pane ──────────────────────────────────────────────────── -->
<div id="tab-admins" class="tab-pane">
<div class="section-header">
<h3>Admin Users</h3>
<button id="btn-new-admin" class="btn btn-primary">+ Add Admin</button>
</div>
<div class="table-wrap card" style="padding:0">
<table id="admins-table">
<thead>
<tr>
<th>Username</th>
<th></th>
</tr>
</thead>
<tbody id="admins-body"></tbody>
</table>
</div>
</div>
<!-- ── Telegram / Notifications pane ────────────────────────────────── -->
<div id="tab-telegram" class="tab-pane">
<h3 style="margin-bottom:1rem">Telegram Notifications</h3>
<div class="card" style="max-width:480px">
<p style="color:var(--text-muted);font-size:.9rem;margin-bottom:1.25rem">
Send a message to a Telegram group or chat every time a gate is opened.
Create a bot via <a href="https://t.me/BotFather" target="_blank" rel="noopener" style="color:var(--primary)">@BotFather</a>,
add it to your group, and paste its token and the chat ID below.
</p>
<form id="telegram-form">
<div class="field">
<label for="tg-token">Bot Token</label>
<input id="tg-token" type="password" autocomplete="off"
placeholder="Leave empty to keep current" />
</div>
<div class="field">
<label for="tg-chat-id">Chat / Group ID</label>
<input id="tg-chat-id" type="text" autocomplete="off"
placeholder="e.g. -1001234567890" required />
</div>
<div class="field">
<label style="display:flex;align-items:center;gap:.75rem;cursor:pointer;margin:0">
<input type="checkbox" id="tg-enabled" checked style="width:1.1rem;height:1.1rem;flex-shrink:0;cursor:pointer" />
<span style="font-weight:600">Enable notifications</span>
</label>
</div>
<p id="tg-status" style="font-size:.85rem;color:var(--text-muted);margin-bottom:.75rem"></p>
<p id="tg-error" class="error-msg hidden"></p>
<div style="display:flex;gap:.75rem;flex-wrap:wrap">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" id="btn-tg-test" class="btn btn-ghost">Send test message</button>
</div>
</form>
</div>
</div>
</div><!-- /.tab-content -->
</div><!-- /#admin-view -->
<!-- ── Keypass modal ───────────────────────────────────────────────────── -->
<div id="keypass-modal" class="modal-backdrop hidden">
<div class="modal">
<h3>New Keypass</h3>
<form id="keypass-form">
<div class="field">
<label for="kp-desc">Description</label>
<input id="kp-desc" type="text" placeholder="e.g. Guests - June 2026" required />
</div>
<div class="field">
<label for="kp-code">Code <span style="color:var(--text-muted);font-weight:400">(leave empty to auto-generate)</span></label>
<input id="kp-code" type="text" autocomplete="off" autocorrect="off" autocapitalize="characters"
spellcheck="false" placeholder="Auto-generated"
style="font-family:monospace;letter-spacing:.1em;text-transform:uppercase" maxlength="40" />
</div>
<!-- Auto-generation options — hidden when a manual code is typed -->
<div id="kp-autogen-options" class="field" style="background:var(--surface2);border-radius:8px;padding:.75rem;border:1px solid var(--border);display:flex;flex-wrap:wrap;gap:.75rem;align-items:flex-end">
<div style="display:flex;flex-direction:column;gap:.3rem">
<label for="kp-charset" style="font-size:.8rem;font-weight:600;color:var(--text-muted)">Character set</label>
<select id="kp-charset" style="width:auto;font-size:.9rem">
<option value="alphanumeric">AZ + 09</option>
<option value="alpha">AZ only</option>
<option value="numeric">09 only</option>
<option value="passphrase" selected>Passphrase (4 words)</option>
</select>
</div>
<div id="kp-length-wrap" style="display:flex;flex-direction:column;gap:.3rem">
<label for="kp-length" style="font-size:.8rem;font-weight:600;color:var(--text-muted)">Length <span id="kp-length-val" style="font-weight:700;color:var(--text)">12</span></label>
<input id="kp-length" type="range" min="6" max="32" value="12" style="width:100%;cursor:pointer" />
</div>
</div>
<div class="field" id="kp-expires-field">
<label for="kp-expires">Expiry date &amp; time</label>
<input id="kp-expires" type="datetime-local" />
<label style="display:flex;align-items:center;gap:.75rem;cursor:pointer;color:var(--text);margin-top:.6rem">
<input type="checkbox" id="kp-never-expires" style="width:1rem;height:1rem;flex-shrink:0" />
<span style="font-size:.9rem">Never expires</span>
</label>
</div>
<div class="field">
<label style="margin-bottom:.5rem">Allowed gates</label>
<div id="kp-gates-container" style="display:flex;flex-direction:column;gap:.3rem;max-height:180px;overflow-y:auto">
<label id="kp-all-gates-row" style="display:flex;align-items:center;gap:.75rem;cursor:pointer;padding:.5rem .75rem;background:var(--surface2);border-radius:6px;border:1px solid var(--border);margin:0">
<input type="checkbox" id="kp-all-gates" checked style="width:1rem;height:1rem;flex-shrink:0" />
<span>All gates</span>
</label>
<div id="kp-gate-checks" style="display:flex;flex-direction:column;gap:.3rem"></div>
</div>
</div>
<p id="kp-error" class="error-msg hidden"></p>
<div class="modal-actions">
<button type="button" id="kp-cancel" class="btn btn-ghost">Cancel</button>
<button type="submit" class="btn btn-primary">Create</button>
</div>
</form>
</div>
</div>
<!-- ── Keypass edit modal ────────────────────────────────────────────────── -->
<div id="kp-edit-modal" class="modal-backdrop hidden">
<div class="modal">
<h3>Edit Keypass</h3>
<form id="kp-edit-form">
<input type="hidden" id="kp-edit-id" />
<div class="field">
<label for="kp-edit-desc">Description</label>
<input id="kp-edit-desc" type="text" required />
</div>
<div class="field">
<label for="kp-edit-expires">Expiry date &amp; time</label>
<input id="kp-edit-expires" type="datetime-local" />
<label style="display:flex;align-items:center;gap:.75rem;cursor:pointer;color:var(--text);margin-top:.6rem">
<input type="checkbox" id="kp-edit-never" style="width:1rem;height:1rem;flex-shrink:0" />
<span style="font-size:.9rem">Never expires</span>
</label>
</div>
<div class="field">
<label style="margin-bottom:.5rem">Allowed gates</label>
<div id="kp-edit-gates-container" style="display:flex;flex-direction:column;gap:.3rem;max-height:180px;overflow-y:auto">
<label id="kp-edit-all-gates-row" style="display:flex;align-items:center;gap:.75rem;cursor:pointer;padding:.5rem .75rem;background:var(--surface2);border-radius:6px;border:1px solid var(--border);margin:0">
<input type="checkbox" id="kp-edit-all-gates" style="width:1rem;height:1rem;flex-shrink:0" />
<span>All gates</span>
</label>
<div id="kp-edit-gate-checks" style="display:flex;flex-direction:column;gap:.3rem"></div>
</div>
</div>
<p id="kp-edit-error" class="error-msg hidden"></p>
<div class="modal-actions">
<button type="button" id="kp-edit-cancel" class="btn btn-ghost">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
<!-- ── Gate modal ──────────────────────────────────────────────────────── -->
<div id="gate-modal" class="modal-backdrop hidden">
<div class="modal">
<h3 id="gate-modal-title">Add Gate</h3>
<form id="gate-form">
<input type="hidden" id="gate-edit-id" />
<div class="field">
<label for="gate-name">Name</label>
<input id="gate-name" type="text" placeholder="e.g. Main entrance - Car" required />
</div>
<div class="field">
<label for="gate-group-name">Group <span style="color:var(--text-muted);font-weight:400">(optional)</span></label>
<input id="gate-group-name" type="text" placeholder="e.g. Main entrance" list="gate-group-list" autocomplete="off" />
<datalist id="gate-group-list"></datalist>
</div>
<div class="field">
<label for="gate-type">Type</label>
<select id="gate-type">
<option value="car">Car</option>
<option value="pedestrian">Pedestrian</option>
</select>
</div>
<div class="field">
<label for="gate-avconnect-macro-id">AVConnect Macro ID</label>
<input id="gate-avconnect-macro-id" type="text" placeholder="e.g. 42" required />
</div>
<div class="field">
<label for="gate-status">Status</label>
<select id="gate-status">
<option value="enabled">Enabled</option>
<option value="disabled">Disabled</option>
</select>
</div>
<p id="gate-error" class="error-msg hidden"></p>
<div class="modal-actions">
<button type="button" id="gate-cancel" class="btn btn-ghost">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
<!-- ── Change password modal ─────────────────────────────────────────────── -->
<div id="chpw-modal" class="modal-backdrop hidden">
<div class="modal">
<h3>Change Password</h3>
<form id="chpw-form">
<input type="hidden" id="chpw-username" />
<div class="field">
<label for="chpw-new">New password</label>
<input id="chpw-new" type="password" autocomplete="new-password" required />
</div>
<div class="field">
<label for="chpw-confirm">Confirm password</label>
<input id="chpw-confirm" type="password" autocomplete="new-password" required />
</div>
<p id="chpw-error" class="error-msg hidden"></p>
<div class="modal-actions">
<button type="button" id="chpw-cancel" class="btn btn-ghost">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
<!-- ── Admin user modal ──────────────────────────────────────────────────── -->
<div id="admin-modal" class="modal-backdrop hidden">
<div class="modal">
<h3>Add Admin</h3>
<form id="admin-form">
<div class="field">
<label for="admin-new-username">Username</label>
<input id="admin-new-username" type="text" autocomplete="off" required />
</div>
<div class="field">
<label for="admin-new-password">Password</label>
<input id="admin-new-password" type="password" autocomplete="new-password" required />
</div>
<div class="field">
<label for="admin-new-role">Role</label>
<select id="admin-new-role">
<option value="admin">Admin (full access)</option>
<option value="manager">Manager (keypasses only)</option>
</select>
</div>
<p id="admin-modal-error" class="error-msg hidden"></p>
<div class="modal-actions">
<button type="button" id="admin-cancel" class="btn btn-ghost">Cancel</button>
<button type="submit" class="btn btn-primary">Create</button>
</div>
</form>
</div>
</div>
<!-- ── QR Code modal ─────────────────────────────────────────────────────── -->
<div id="qr-modal" class="modal-backdrop hidden">
<div class="modal" style="text-align:center;max-width:340px">
<h3 style="margin-bottom:.2rem">Keypass QR Code</h3>
<p id="qr-modal-desc" style="color:var(--text-muted);font-size:.85rem;margin-bottom:1.25rem"></p>
<div style="display:flex;justify-content:center;align-items:center;min-height:220px;background:var(--surface2);border-radius:8px;padding:1rem">
<img id="qr-img" src="" alt="QR Code" style="max-width:100%;border-radius:4px;display:block" />
</div>
<p style="color:var(--text-muted);font-size:.78rem;margin-top:.75rem">Scan to open the app and login automatically.</p>
<div class="modal-actions" style="justify-content:center;margin-top:1rem">
<button type="button" id="qr-close" class="btn btn-ghost">Close</button>
<a id="qr-download" download="keypass-qr.png" class="btn btn-primary" style="text-decoration:none">Download</a>
</div>
</div>
</div>
<!-- ── Toast ───────────────────────────────────────────────────────────── -->
<div id="toast" class="toast hidden" aria-live="assertive"></div>
<script src="/static/admin.js"></script>
</body>
</html>