Visitor sign in kiosk: multi-site, badge printing, WWCC expiry warnings, admin accounts with 2FA

This commit is contained in:
2026-08-31 09:47:36 +10:00
commit ed77493817
32 changed files with 5799 additions and 0 deletions
+204
View File
@@ -0,0 +1,204 @@
<!doctype html>
<html lang="en-AU">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin — visitor sign in</title>
<link rel="stylesheet" href="/css/admin.css">
</head>
<body>
<!-- ------------------------------------------------------------ login -->
<div id="login" class="login" hidden>
<div class="login-card">
<h1 id="login-heading">Visitor admin</h1>
<form class="login-step" id="step-password">
<label>
<span>Email address</span>
<input type="email" id="login-email" autocomplete="username" autocapitalize="none">
</label>
<label>
<span>Password</span>
<input type="password" id="login-password" autocomplete="current-password">
</label>
<p class="hint" id="domain-rule" hidden></p>
<button type="submit">Continue</button>
</form>
<form class="login-step" id="step-2fa" hidden>
<p class="hint" id="twofa-intro"></p>
<div id="twofa-setup" hidden>
<img id="twofa-qr" alt="Two factor setup QR code" width="200" height="200">
<p class="hint">Can't scan it? Enter this key by hand:<br><code id="twofa-secret"></code></p>
</div>
<label>
<span id="twofa-label">6 digit code</span>
<input id="twofa-code" inputmode="numeric" autocomplete="one-time-code" maxlength="12">
</label>
<button type="submit">Verify</button>
<button type="button" class="link-quiet" id="twofa-cancel">Start again</button>
</form>
<div class="login-step" id="step-recovery" hidden>
<p class="hint">Save these recovery codes somewhere safe. Each one works once, if you ever
lose the phone with your authenticator app on it.</p>
<ul class="recovery" id="recovery-list"></ul>
<button type="button" id="recovery-done">I've saved them</button>
</div>
<form class="login-step" id="step-newpassword" hidden>
<p class="hint">Set a password only you know before you continue.</p>
<label>
<span>Current password</span>
<input type="password" id="pw-current" autocomplete="current-password">
</label>
<label>
<span>New password</span>
<input type="password" id="pw-new" autocomplete="new-password">
</label>
<label>
<span>New password again</span>
<input type="password" id="pw-again" autocomplete="new-password">
</label>
<button type="submit">Save and continue</button>
</form>
<div class="login-step" id="step-setup" hidden>
<p class="hint" id="setup-message"></p>
</div>
<p class="err" id="login-error" hidden></p>
</div>
</div>
<!-- ---------------------------------------------------------- console -->
<div id="console" hidden>
<header class="topbar">
<strong id="site-name">Visitor admin</strong>
<nav>
<button class="tab on" data-tab="onsite">On site</button>
<button class="tab" data-tab="log">Visit log</button>
<button class="tab" data-tab="recurring">Recurring visitors <span class="badge-count" id="alert-count" hidden></span></button>
<button class="tab" data-tab="hosts">People to visit</button>
<button class="tab" data-tab="sites">Sites</button>
<button class="tab owner-only" data-tab="admins" hidden>Admins</button>
<button class="tab" data-tab="system">System</button>
</nav>
<label class="site-switch" id="site-switch" hidden>
<span>Site</span>
<select id="site-filter"></select>
</label>
<button id="logout" class="link">Sign out</button>
</header>
<p class="banner" id="expiry-banner" hidden></p>
<main>
<!-- ------------------------------------------------------ on site -->
<section class="panel on" id="panel-onsite">
<div class="panel-head">
<h2>Currently on site</h2>
<button class="ghost" id="refresh-onsite">Refresh</button>
</div>
<p class="stat" id="onsite-count"></p>
<div id="onsite-table"></div>
</section>
<!-- -------------------------------------------------------- log -->
<section class="panel" id="panel-log">
<div class="panel-head">
<h2>Visit log</h2>
<a class="ghost" id="csv-link" href="/admin/api/visits.csv">Download CSV</a>
</div>
<div class="filters">
<label><span>From</span><input type="date" id="log-from"></label>
<label><span>To</span><input type="date" id="log-to"></label>
<label class="grow"><span>Search name, host or contact</span><input id="log-q"></label>
<button class="ghost" id="log-search">Apply</button>
</div>
<div id="log-table"></div>
</section>
<!-- ------------------------------------------------- recurring -->
<section class="panel" id="panel-recurring">
<div class="panel-head">
<h2>Recurring visitors</h2>
<button class="primary" id="new-recurring">Add a recurring visitor</button>
</div>
<div id="expiry-summary"></div>
<div id="recurring-table"></div>
</section>
<!-- ----------------------------------------------------- hosts -->
<section class="panel" id="panel-hosts">
<div class="panel-head">
<h2>People a visitor can ask for</h2>
<button class="primary" id="new-host">Add a person</button>
</div>
<p class="hint" id="hosts-scope"></p>
<details class="import">
<summary>Import from CSV</summary>
<p class="hint">
Paste the file contents below, or choose a .csv file. Recognised column headings are
<code>name</code>, <code>email</code> and <code>area</code> (or department / team).
A single column of names works too. The import applies to the site selected above.
</p>
<input type="file" id="host-file" accept=".csv,text/csv">
<textarea id="host-csv" rows="6" placeholder="name,email,area&#10;Jess Rogerson,jess@example.com,Front office"></textarea>
<label class="inline"><input type="checkbox" id="host-replace"> Deactivate anyone at this site who is not in the file</label>
<button class="primary" id="do-host-import">Import</button>
</details>
<div id="hosts-table"></div>
</section>
<!-- ----------------------------------------------------- sites -->
<section class="panel" id="panel-sites">
<div class="panel-head">
<h2>Sites</h2>
<button class="primary owner-only" id="new-site" hidden>Add a site</button>
</div>
<p class="hint">Each site has its own name, its own list of people to visit, and its own
badge settings. Point a kiosk at one with
<code>/?site=<em>slug</em></code>, or let staff pick on first use.</p>
<div id="sites-list"></div>
</section>
<!-- ---------------------------------------------------- admins -->
<section class="panel" id="panel-admins">
<div class="panel-head">
<h2>Admin accounts</h2>
<button class="primary" id="new-admin">Invite an admin</button>
</div>
<p class="hint" id="admins-note"></p>
<div id="admins-table"></div>
</section>
<!-- ---------------------------------------------------- system -->
<section class="panel" id="panel-system">
<h2>System</h2>
<div id="system-body"></div>
<h2 class="section-gap">Your account</h2>
<div id="account-body"></div>
</section>
</main>
</div>
<dialog id="modal">
<form method="dialog" id="modal-form">
<h3 id="modal-title"></h3>
<div id="modal-body"></div>
<div class="modal-actions">
<button value="cancel" class="ghost" id="modal-cancel">Cancel</button>
<button value="save" class="primary" id="modal-save">Save</button>
</div>
</form>
</dialog>
<p class="toast" id="toast" role="status" hidden></p>
<footer class="foot">Created by: Jess Rogerson (yelling commands at Claude.AI)</footer>
<script src="/js/admin.js"></script>
</body>
</html>