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

This commit is contained in:
2026-09-04 14:55:59 +10:00
commit 3278890491
41 changed files with 8920 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
<!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="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/css/admin.css">
</head>
<body>
<!-- ------------------------------------------------------------ login -->
<!-- ---------------------------------------------------------- console -->
<div id="console">
<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, company, 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>