540 lines
26 KiB
HTML
540 lines
26 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: 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">A–Z + 0–9</option>
|
||
<option value="alpha">A–Z only</option>
|
||
<option value="numeric">0–9 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 & 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>
|
||
|
||
<!-- ── 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>
|