287 lines
12 KiB
HTML
287 lines
12 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Household admin</title>
|
|
<link rel="stylesheet" href="style.css">
|
|
</head>
|
|
<body class="admin">
|
|
<!--
|
|
identity's admin panel — managing people, guests, rights and history, as opposed to
|
|
register.html (the door-panel kiosk's own self-service flow) and dashboard.html (the
|
|
ambient at-a-glance screen).
|
|
|
|
THIS IS THE ONE PAGE HERE NOT DESIGNED FOR A KIOSK. The other two are big-touch-
|
|
target, glanceable, and running unattended on a wall panel; this one is dense, has
|
|
destructive buttons on it, and is meant for a phone or laptop where you sat down
|
|
intending to administer something. That's why it opts into `body.admin`'s tighter
|
|
scale rather than inheriting the 64px-button kiosk defaults — a wall panel anyone
|
|
can walk up to should not have a "Prune 6 people" button on it.
|
|
|
|
Config comes from the URL exactly like the sibling pages: ?api=&token=.
|
|
-->
|
|
<div id="tabs">
|
|
<button type="button" class="tab active" data-panel="people">👥<span>People</span></button>
|
|
<button type="button" class="tab" data-panel="prune">🧹<span>Prune</span></button>
|
|
<button type="button" class="tab" data-panel="history">🕘<span>History</span></button>
|
|
<button type="button" class="tab" data-panel="floorplan">🗺️<span>Floorplan</span></button>
|
|
<button type="button" class="tab" data-panel="access">🔑<span>Access log</span></button>
|
|
</div>
|
|
|
|
<main>
|
|
<!-- ---------------------------------------------------------------- People -->
|
|
<section class="panel active" id="panel-people">
|
|
<section class="block">
|
|
<h2>People</h2>
|
|
<p class="hint">Tap a person to edit every field, their devices, door rights and chores.</p>
|
|
<div id="people-list" class="card-list"><p class="hint">Loading…</p></div>
|
|
</section>
|
|
</section>
|
|
|
|
<!-- ----------------------------------------------------------------- Prune -->
|
|
<section class="panel" id="panel-prune">
|
|
<section class="block">
|
|
<h2>Prune old records</h2>
|
|
<p class="hint">
|
|
Select everyone whose last visit was before a date — for clearing out
|
|
one-off guests. Nothing is deleted until you confirm the exact list below.
|
|
</p>
|
|
<div class="row">
|
|
<label class="field">
|
|
Last visited before
|
|
<input id="prune-date" type="date">
|
|
</label>
|
|
<button type="button" id="prune-select" class="btn">Select all</button>
|
|
</div>
|
|
<p id="prune-summary" class="hint"></p>
|
|
<div id="prune-list" class="card-list"></div>
|
|
<div class="row" style="margin-top:12px">
|
|
<button type="button" id="prune-delete" class="btn danger" disabled>Delete selected</button>
|
|
<span id="prune-status" class="hint"></span>
|
|
</div>
|
|
</section>
|
|
</section>
|
|
|
|
<!-- --------------------------------------------------------------- History -->
|
|
<section class="panel" id="panel-history">
|
|
<section class="block">
|
|
<h2>Who was home, when</h2>
|
|
<div class="row">
|
|
<label class="field">
|
|
Person
|
|
<select id="history-person"><option value="">Everyone</option></select>
|
|
</label>
|
|
<label class="field">
|
|
Since
|
|
<input id="history-since" type="date">
|
|
</label>
|
|
<button type="button" id="history-load" class="btn">Load</button>
|
|
</div>
|
|
<div id="visit-list" class="card-list"><p class="hint">Pick a range and load.</p></div>
|
|
</section>
|
|
|
|
<section class="block">
|
|
<h2>…and with whom</h2>
|
|
<p class="hint">Overlapping visits, worked out from the history above.</p>
|
|
<div id="copresence-list" class="card-list"></div>
|
|
</section>
|
|
</section>
|
|
|
|
<!-- ------------------------------------------------------------- Floorplan -->
|
|
<!--
|
|
The half that was missing from Phase 6 (project-plan open decision #22): /presence
|
|
has reported a best-effort `room` all along, but there was nothing to plot it on.
|
|
Rooms are DRAWN HERE BY HAND — nothing in this project knows the shape of this
|
|
flat, and the alternative to drawing it was inventing a coordinate format against
|
|
a guess, which is why this stayed deferred rather than half-built.
|
|
-->
|
|
<section class="panel" id="panel-floorplan">
|
|
<section class="block">
|
|
<div class="row">
|
|
<label class="field">
|
|
Level
|
|
<select id="fp-level"></select>
|
|
</label>
|
|
<button type="button" id="fp-add-level" class="btn">Add level</button>
|
|
<button type="button" id="fp-rename-level" class="btn">Rename</button>
|
|
<button type="button" id="fp-delete-level" class="btn danger">Delete level</button>
|
|
<label class="btn" style="cursor:pointer">
|
|
Background image
|
|
<input id="fp-image" type="file" accept="image/*" hidden>
|
|
</label>
|
|
<span class="grow"></span>
|
|
<label class="check">
|
|
<input id="fp-live" type="checkbox" checked>
|
|
Show who's home
|
|
</label>
|
|
</div>
|
|
<p class="hint" id="fp-hint">
|
|
Click on the plan to drop points, then <b>Finish</b> (or double-click) to close
|
|
the room. Drag a point to adjust it. Click a finished room to edit it.
|
|
</p>
|
|
</section>
|
|
|
|
<section class="block fp-layout">
|
|
<div class="fp-canvas-wrap">
|
|
<svg id="fp-canvas" viewBox="0 0 1000 700" preserveAspectRatio="xMidYMid meet">
|
|
<image id="fp-bg" x="0" y="0" width="1000" height="700" preserveAspectRatio="none"></image>
|
|
<g id="fp-rooms"></g>
|
|
<g id="fp-draft"></g>
|
|
<g id="fp-handles"></g>
|
|
</svg>
|
|
<div class="row" style="margin-top:8px">
|
|
<button type="button" id="fp-draw" class="btn primary">Draw a room</button>
|
|
<button type="button" id="fp-finish" class="btn" disabled>Finish</button>
|
|
<button type="button" id="fp-cancel" class="btn" disabled>Cancel</button>
|
|
<span id="fp-status" class="hint"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<aside class="fp-side">
|
|
<h3 id="fp-editor-title">Rooms</h3>
|
|
<div id="fp-room-list" class="card-list"></div>
|
|
|
|
<fieldset class="field-group" id="fp-room-editor" hidden>
|
|
<legend>Selected room</legend>
|
|
<label class="field">Name
|
|
<input id="fp-room-name" type="text" autocomplete="off">
|
|
</label>
|
|
<label class="field">
|
|
Home Assistant area
|
|
<input id="fp-room-area" type="text" list="fp-areas" placeholder="e.g. kitchen" autocomplete="off">
|
|
</label>
|
|
<datalist id="fp-areas"></datalist>
|
|
<p class="hint" id="fp-area-hint"></p>
|
|
<label class="field">Colour
|
|
<input id="fp-room-color" type="color" value="#6ea8fe">
|
|
</label>
|
|
<div class="row">
|
|
<button type="button" id="fp-room-save" class="btn primary">Save room</button>
|
|
<button type="button" id="fp-room-delete" class="btn danger">Delete</button>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<div id="fp-unplaced" class="hint" style="margin-top:14px"></div>
|
|
</aside>
|
|
</section>
|
|
</section>
|
|
|
|
<!-- ------------------------------------------------------------ Access log -->
|
|
<section class="panel" id="panel-access">
|
|
<section class="block">
|
|
<h2>Device access checks</h2>
|
|
<p class="hint">
|
|
Every time something asked whether a person may operate a device — allowed and
|
|
denied alike. identity only ever answers these; Home Assistant is what actually
|
|
moves a lock.
|
|
</p>
|
|
<div id="access-list" class="card-list"><p class="hint">Loading…</p></div>
|
|
</section>
|
|
</section>
|
|
</main>
|
|
|
|
<!-- The person editor. One dialog reused for whoever's selected, rather than an
|
|
always-rendered form per person — the list can get long and only one is ever
|
|
being edited. -->
|
|
<dialog id="editor">
|
|
<form method="dialog" id="editor-form">
|
|
<header class="editor-head">
|
|
<span class="avatar" id="edit-avatar">👤</span>
|
|
<h3 id="edit-title">Edit</h3>
|
|
<button type="button" class="btn ghost" id="edit-close">Close</button>
|
|
</header>
|
|
|
|
<div class="editor-body">
|
|
<label class="field">Name <span class="hint">— what the assistant says out loud</span>
|
|
<input id="edit-name" type="text" autocomplete="off">
|
|
</label>
|
|
<label class="field">Nickname <span class="hint">— what people call them; the assistant never uses it</span>
|
|
<input id="edit-nickname" type="text" autocomplete="off" placeholder="none">
|
|
</label>
|
|
<label class="field">Note
|
|
<input id="edit-note" type="text" autocomplete="off" placeholder="e.g. cousin, visits at Christmas">
|
|
</label>
|
|
|
|
<fieldset class="field-group">
|
|
<legend>Chores</legend>
|
|
<label class="check">
|
|
<input id="edit-chore-exempt" type="checkbox">
|
|
Exempt from chores
|
|
<span class="hint">— litter is always the exception; everyone cleans up what they left out</span>
|
|
</label>
|
|
<label class="field">Reminder style
|
|
<input id="edit-reminder-style" type="text" placeholder="e.g. be gentle, give me a few minutes">
|
|
</label>
|
|
<div class="field">
|
|
Assigned chores
|
|
<div id="edit-chore-types" class="chip-row"></div>
|
|
<p class="hint">
|
|
A strong preference, not a lock: if the assignee isn't home, whoever is
|
|
gets nudged instead. Litter ignores assignment entirely.
|
|
</p>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset class="field-group">
|
|
<legend>Arrival notifications</legend>
|
|
<label class="check">
|
|
<input id="edit-notify-on-arrival" type="checkbox">
|
|
Push me when someone gets home
|
|
</label>
|
|
<label class="check">
|
|
<input id="edit-announce-arrivals" type="checkbox">
|
|
Announce <em>my</em> arrivals to others
|
|
<span class="hint">— untick to keep your comings and goings private</span>
|
|
</label>
|
|
<div class="row">
|
|
<label class="field grow">
|
|
ntfy topic
|
|
<input id="edit-notify-topic" type="text" placeholder="(shared household topic)">
|
|
</label>
|
|
<button type="button" id="edit-test-notify" class="btn">Send test</button>
|
|
</div>
|
|
<p id="notify-hint" class="hint"></p>
|
|
</fieldset>
|
|
|
|
<fieldset class="field-group">
|
|
<legend>Devices used for presence</legend>
|
|
<div id="edit-identifiers" class="card-list"></div>
|
|
<div class="row">
|
|
<input id="edit-new-identifier" type="text" placeholder="device_tracker.pble_…" class="grow">
|
|
<button type="button" id="edit-add-identifier" class="btn">Add</button>
|
|
</div>
|
|
<p class="hint">
|
|
Must match TRUSTED_ENTITY_PREFIXES — only IRK-resolved or fixed-tag entities
|
|
can identify a person. For provisioning a tag that isn't in range yet.
|
|
</p>
|
|
</fieldset>
|
|
|
|
<fieldset class="field-group">
|
|
<legend>Device rights</legend>
|
|
<div id="edit-grants" class="card-list"></div>
|
|
<div class="row">
|
|
<input id="grant-entity" type="text" placeholder="lock.front_door" class="grow">
|
|
<input id="grant-permission" type="text" placeholder="operate" style="max-width:110px">
|
|
<input id="grant-expires" type="date" title="Optional expiry">
|
|
<button type="button" id="grant-add" class="btn">Grant</button>
|
|
</div>
|
|
<p class="hint">
|
|
Lets this person operate that entity — e.g. unlocking the front door for
|
|
themselves. An expiry date makes it a temporary key.
|
|
</p>
|
|
</fieldset>
|
|
|
|
<div class="row editor-footer">
|
|
<button type="button" id="edit-save" class="btn primary">Save changes</button>
|
|
<button type="button" id="edit-delete" class="btn danger">Delete person</button>
|
|
<span id="edit-status" class="hint"></span>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</dialog>
|
|
|
|
<script src="admin.js"></script>
|
|
</body>
|
|
</html>
|