Add live auto-refresh, search/sort, health, reporting totals, and badge-times modal
Build and Push Docker Image / build (push) Successful in 6s
Build and Push Docker Image / build (push) Successful in 6s
Dashboard usability pass: - Auto-refresh: poll today every 45s with a live "updated Xs ago" indicator and toggle; auto-pauses on past dates. - Table search, click-to-sort column headers, and clickable summary pills that filter by on-time / late. - View state (date, cutoff, controller, filters) persists to localStorage and the URL for reloads, bookmarking, and sharing; adds a Today button. - First-run onboarding CTA when no controllers exist, plus a header health pill and per-controller last-event time driven by a new last_event_at column (stamped on webhook ingest; auto-migrated on boot). - Report pivot gains per-person on-time/late/absent totals and an All row; CSV export now downloads via fetch/blob so failures surface as errors. - New badge-in times modal (GET /api/badge-events) listing every badge-in for a person on a day — across all identities for merged people. Docs updated to cover all of the above. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -18,9 +18,18 @@ first-badge time per human, not per badge UUID — no more false LATE warnings
|
||||
for staff who badge into multiple buildings before the cutoff. (05/28/26)
|
||||
|
||||
**Reporting:** export first badge-in times over a date range for one person or a
|
||||
multi-selected group. View an on-screen pivot (users × dates) or download a
|
||||
long-format CSV. Days with no badge-in are reported as **absent**, so it doubles
|
||||
as an attendance report. (06/19/26)
|
||||
multi-selected group. View an on-screen pivot (users × dates) with per-person
|
||||
on-time / late / absent totals, or download a long-format CSV. Days with no
|
||||
badge-in are reported as **absent**, so it doubles as an attendance report. (06/19/26)
|
||||
|
||||
**Live & searchable:** the table auto-refreshes on a 45-second interval with a
|
||||
live "updated Xs ago" indicator, plus a header **health pill** that shows when the
|
||||
last badge event actually arrived — so a dead webhook is obvious at a glance.
|
||||
Filter rows by name, click any column header to sort, and click the summary pills
|
||||
to show only on-time or late people. View settings (date, cutoff, controller,
|
||||
filters) persist across reloads and are encoded in the URL for bookmarking and
|
||||
sharing. Click any person's badge time — or their **Times** button — to see
|
||||
**every** badge-in for that day, not just first and latest. (07/01/26)
|
||||
|
||||
---
|
||||
|
||||
@@ -183,8 +192,11 @@ Per-controller actions in the modal:
|
||||
| **Date picker** | Choose which day to view (defaults to the browser's local date) |
|
||||
| **Badged in by** | Set your on-time cutoff (HH:MM) |
|
||||
| **Controller** | Filter the table to one controller, or show All |
|
||||
| **Search** | Filter the visible rows by name (client-side, instant) |
|
||||
| **Show filtered** | Include filtered tenants in the table (dimmed and tagged) |
|
||||
| **Refresh** | Reload the table |
|
||||
| **Auto-refresh** | Toggle live 45-second polling of the current day (auto-pauses when viewing a past date) |
|
||||
| **Today** | Jump the date picker back to today |
|
||||
| **Refresh** | Reload the table now |
|
||||
| **Sync Users** | Pull latest users from every enabled controller |
|
||||
| **🚫 Filtered** | Open the filtered-tenants modal to review and unhide |
|
||||
| **📊 Report** | Open the date-range report builder (first badge-in per day, per user, CSV export) |
|
||||
@@ -196,14 +208,18 @@ Per-controller actions in the modal:
|
||||
|
||||
| Column | Description |
|
||||
|---|---|
|
||||
| **#** | Row number |
|
||||
| **#** | Row number (within the current sort/filter) |
|
||||
| **Name** | Resolved display name from UniFi Access |
|
||||
| **Source** | Controller this badge event came from |
|
||||
| **First Badge In** | Earliest door entry for the day — never changes once set |
|
||||
| **First Badge In** | Earliest door entry for the day — never changes once set. **Click it** (or the Times button) to see the full list of the day's badge-ins |
|
||||
| **Latest Badge In** | Most recent entry — shows *"— same"* if only one badge event |
|
||||
| **Actor ID** | First 8 characters of the UniFi user UUID |
|
||||
| **Status** | ON TIME (green) or LATE (red) based on first badge vs cutoff |
|
||||
| **Actions** | **Hide** filters this person out of future views; **Merge** joins two badge identities so they count as one human |
|
||||
| **Actions** | **Times** opens every badge-in for that person on the selected day; **Hide** filters this person out of future views; **Merge** joins two badge identities so they count as one human |
|
||||
|
||||
> Click a sortable header (Name, First/Latest Badge In, Status) to sort by that
|
||||
> column; click again to reverse. Click the **on time** / **late** / **total**
|
||||
> summary pills to show only that group.
|
||||
|
||||
> Once two identities are merged, the Source column shows a chip for every
|
||||
> controller the person badged into that day, plus a "MERGED" pill so it's
|
||||
@@ -211,6 +227,23 @@ Per-controller actions in the modal:
|
||||
|
||||
### Interface notes
|
||||
|
||||
- **Auto-refresh & live status.** When viewing today with Auto-refresh on, the
|
||||
table reloads every 45 seconds. The pill in the summary row shows *"updated Xs
|
||||
ago"* (or *"paused"* / *"viewing past day"* when polling is off).
|
||||
- **Ingestion health.** The pill in the header shows the freshest badge event
|
||||
across all enabled controllers — *"last badge 3m ago"* (green), *"no events…"*
|
||||
(amber, stale), or *"webhook not registered"* — so a broken webhook is caught
|
||||
without digging into the Controllers modal (which also shows per-controller
|
||||
last-event time).
|
||||
- **Badge-in times.** Clicking a person's First-badge time or their **Times**
|
||||
button lists every badge-in for that day, oldest first, tagged *first* and
|
||||
*latest* and labelled with the source controller. For merged people the list
|
||||
spans all their identities.
|
||||
- **View persistence & sharing.** Date, cutoff, controller, and the filter
|
||||
toggles are saved to the browser and mirrored into the URL, so reloading keeps
|
||||
your view and you can bookmark or share a link to a specific day/controller.
|
||||
- **First-run onboarding.** With no controllers configured, the table shows an
|
||||
"Add a Controller" call-to-action instead of an empty grid.
|
||||
- On narrow screens, the attendance table automatically changes into stacked
|
||||
row cards with labels for each field, so all columns remain visible without
|
||||
sideways scrolling.
|
||||
@@ -289,7 +322,8 @@ In the report modal:
|
||||
- **Run Report** renders an on-screen **pivot**: one row per selected user, one
|
||||
column per date, each cell showing that day's first badge-in time
|
||||
(green = on time, red = late). Weekends are shaded and days with no badge-in
|
||||
show as **—**.
|
||||
show as **—**. Trailing **On time / Late / Absent** columns total each person
|
||||
over the range, and a final **All** row totals every column.
|
||||
- **Export CSV** downloads the same data in **long format** — one row per
|
||||
user-per-day (`Name, Date, Weekday, First In, Status, Sources`) — ideal for
|
||||
Excel or Sheets.
|
||||
@@ -313,10 +347,11 @@ git pull
|
||||
/usr/bin/docker compose up -d --build
|
||||
```
|
||||
|
||||
The SQLite database in `./data/` persists across rebuilds. On first start after
|
||||
upgrading from a single-controller install, existing badge events are
|
||||
automatically attached to the seeded "Default" controller — nothing to migrate
|
||||
by hand.
|
||||
The SQLite database in `./data/` persists across rebuilds. Schema changes (such
|
||||
as the `last_event_at` column added for ingestion health) are applied
|
||||
automatically on boot. On first start after upgrading from a single-controller
|
||||
install, existing badge events are automatically attached to the seeded
|
||||
"Default" controller — nothing to migrate by hand.
|
||||
|
||||
---
|
||||
|
||||
@@ -331,6 +366,7 @@ reverse proxy with auth in front of it.
|
||||
| `POST` | `/api/unifi-access/<controller_id>` | webhook body | Receives UniFi Access webhook for that controller |
|
||||
| `POST` | `/api/unifi-access` | webhook body | Legacy alias — routes to the oldest controller |
|
||||
| `GET` | `/api/first-badge-status` | `date`, `cutoff`, `controller_id?`, `include_filtered?` | Returns first + latest badge per user (filtered tenants hidden unless `include_filtered=1`) |
|
||||
| `GET` | `/api/badge-events` | `date`, and either `person_id` **or** `controller_id`+`actor_id` | Every badge-in for one subject on one day, oldest first (for a merged person, across all their identities) |
|
||||
| `GET` | `/api/report/subjects` | `controller_id?`, `include_filtered?` | List selectable report subjects (merged persons + unmerged actors), each with a stable `key` |
|
||||
| `GET` | `/api/report` | `start`, `end`, `cutoff?`, `subjects?`, `controller_id?`, `include_filtered?`, `format?` | First badge-in per subject per day over the range. JSON (pivot-shaped) by default; `format=csv` returns a long-format download. Missing days reported as `ABSENT`. `subjects` is a comma-separated list of subject `key`s (empty = all) |
|
||||
| `GET` | `/api/users` | `controller_id?`, `filtered?` | List cached actors with their filtered flag |
|
||||
@@ -342,7 +378,7 @@ reverse proxy with auth in front of it.
|
||||
| `POST` | `/api/persons/<id>/members` | `controller_id`, `actor_id` | Add another identity to an existing person |
|
||||
| `DELETE` | `/api/persons/<id>/members/<cid>/<aid>` | — | Split one identity off; dissolves the person if it was the last member |
|
||||
| `GET` | `/api/persons/suggestions` | — | Exact full-name matches across controllers, excluding already-merged actors |
|
||||
| `GET` | `/api/controllers` | — | List configured controllers |
|
||||
| `GET` | `/api/controllers` | — | List configured controllers (includes `last_sync_at` and `last_event_at` for health) |
|
||||
| `POST` | `/api/controllers` | `name`, `host`, `port`, `api_token` | Add a controller (also registers webhook) |
|
||||
| `PATCH` | `/api/controllers/<id>` | `name?`, `enabled?` | Rename or enable/disable a controller |
|
||||
| `DELETE` | `/api/controllers/<id>` | — | Remove a controller (deletes webhook + its events) |
|
||||
|
||||
Reference in New Issue
Block a user