417 lines
18 KiB
HTML
417 lines
18 KiB
HTML
<!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: 960px; 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>
|
|
</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>
|
|
</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>
|
|
<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>
|
|
</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>
|
|
|
|
</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="32" />
|
|
</div>
|
|
<div class="field" id="kp-expires-field">
|
|
<label for="kp-expires">Expiry date & 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 & 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>
|
|
|
|
<!-- ── Toast ───────────────────────────────────────────────────────────── -->
|
|
<div id="toast" class="toast hidden" aria-live="assertive"></div>
|
|
|
|
<script src="/static/admin.js"></script>
|
|
</body>
|
|
</html>
|