jason 0ad30aa6ce
Build and Push Docker Image / build (push) Successful in 8s
Update README.md
2026-05-28 07:42:59 -05:00
2026-05-28 00:39:46 -05:00
2026-05-28 00:39:46 -05:00
2026-03-04 20:07:02 -06:00
2026-05-28 00:54:54 -05:00
2026-03-04 21:43:49 -06:00
2026-03-04 19:42:38 -06:00
2026-05-28 07:42:59 -05:00
2026-05-28 00:39:46 -05:00

UniFi Access Badge-In Dashboard

A Dockerised Flask + SQLite attendance dashboard that receives real-time door unlock webhooks from one or more UniFi Access Developer API controllers, resolves badge holders to real names, and displays a unified live attendance table with first/latest badge times, source controller, and ON TIME / LATE status.

Multi-controller: add as many UniFi Access controllers as the host can reach. Webhooks are auto-registered when you add a controller from the UI. (05/28/26)


Requirements

  • Linux host with Docker + Docker Compose (Unraid works great)
  • One or more UniFi OS consoles running UniFi Access 1.9.1 or later
  • Network reachability from the dashboard host to each controller on port 12445
  • A Developer API token from each UniFi Access controller you plan to add

Step 1 — Open firewall port 12445 to each controller

The UniFi Access Open API runs exclusively on port 12445 (HTTPS, self-signed cert). The host running the dashboard must be able to reach each controller on that port.

In UniFi Network → Settings → Firewall & Security → Firewall Rules, add a LAN IN rule on each controller:

Field Value
Action Accept
Protocol TCP
Destination Port 12445
Source the subnet your dashboard host lives on

Verify from the dashboard host:

# Linux:
nc -zv 10.0.0.1 12445

# Windows PowerShell:
Test-NetConnection -ComputerName 10.0.0.1 -Port 12445

Step 2 — Generate a Developer API token on each controller

⚠️ This token is different from the UniFi OS / Network API token. Creating it in the wrong place will result in 401 Unauthorized errors.

  1. Open the UniFi OS console at https://<controller-ip> in a browser.
  2. Open the Access app (blue door icon).
  3. Go to Settings → General → Advanced → API Token.
  4. Click Create New, name it, enable all permission scopes, and pick a validity period.
  5. Click Create and immediately copy the token — it's only shown once.

Repeat for each controller you plan to add.


Step 3 — Clone the repo on the host

cd /mnt/user/appdata
git clone https://github.com/jasonMPM/unifi-access-dashboard.git unifi-access-dashboard
cd unifi-access-dashboard

Step 4 — Create your .env file

cp .env.example .env
nano .env

The UNIFI_* and WEBHOOK_SECRET values are optional. If set, they auto-create a "Default" controller on first boot — handy for single-controller installs. You can leave them blank and add every controller via the UI instead.

# Optional: seeds a "Default" controller on first boot
UNIFI_HOST=10.0.0.1
UNIFI_PORT=12445
UNIFI_API_TOKEN=YOUR_TOKEN_HERE
WEBHOOK_SECRET=

# Required
TZ=America/Chicago
DB_PATH=/data/dashboard.db

# Optional: override the URL the dashboard uses when registering webhooks
# DASHBOARD_BASE_URL=http://10.0.0.5:8000

Never commit .env to git. It is listed in .gitignore.


Step 5 — Build and start the container

cd /mnt/user/appdata/unifi-access-dashboard
/usr/bin/docker compose up -d --build

The container will:

  • Build the image from the local Dockerfile
  • Start Flask on port 8000
  • Create /data/dashboard.db inside the container (mapped to ./data/ on the host)
  • If env-var credentials are set, seed a "Default" controller and sync its users
  • Schedule a user-cache refresh every hour for every enabled controller

Verify it's running:

/usr/bin/docker ps
/usr/bin/docker logs -f unifi-access-dashboard

Step 6 — Add controllers from the UI

Navigate to:

http://<HOST-IP>:8000/

Click the ⚙ Controllers button in the header. For each UniFi Access instance you want to receive events from, fill in:

Field Value
Name Friendly label shown in the Source column (e.g. "Main Office", "Warehouse")
Host / IP Controller IP, e.g. 10.0.0.1
Port 12445 (don't change unless your controller is non-standard)
Developer API Token Token from Step 2

Click Add Controller. The dashboard will:

  1. Call the controller's POST /webhooks/endpoints with this dashboard's URL.
  2. Store the returned webhook secret so it can verify incoming events (HMAC-SHA256).
  3. Immediately sync the controller's user list to resolve names.

If the controller can't reach this dashboard at the URL shown in the form hint (it uses window.location.origin by default), set DASHBOARD_BASE_URL in .env and restart.

Per-controller actions in the modal:

Action Description
Test Hits the controller's /users endpoint to confirm the token works
Sync Pulls latest users from this controller right now
Enable / Disable Pause ingestion + sync without deleting the controller
Remove Deletes the webhook from the controller and wipes all its badge events from the dashboard

Dashboard controls

Control Description
Date picker Choose which day to view
Badged in by Set your on-time cutoff (HH:MM)
Controller Filter the table to one controller, or show All
Refresh Reload the table
Sync Users Pull latest users from every enabled controller
⚙ Controllers Add / manage controllers
Reset Day Delete all badge records for the selected date (respects the Controller filter — testing only)

Dashboard columns

Column Description
# Row number
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
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

The same physical person on two different controllers will appear as two rows (different controllers issue different user UUIDs). They're distinguishable by the Source column.


Updating from GitHub

cd /mnt/user/appdata/unifi-access-dashboard
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.


API reference

All endpoints are unauthenticated by design — this app assumes a LAN-only deployment. Do not expose port 8000 to the internet without putting a reverse proxy with auth in front of it.

Method Path Params / Body Description
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? Returns first + latest badge per user
GET /api/controllers List configured controllers
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)
POST /api/controllers/<id>/test Test controller reachability + token
POST /api/controllers/<id>/sync Sync one controller's user cache immediately
GET /api/sync-users Sync every enabled controller
DELETE /api/reset-day date, controller_id? Delete badge records for a date (optionally scoped to one controller)

Troubleshooting

Symptom Cause Fix
Add Controller fails with "webhook registration rejected" Token invalid or wrong scope Regenerate token in Access → Settings → General → API Token with all scopes enabled
Add Controller fails with a connection error Host unreachable on port 12445 Verify firewall rule from Step 1; nc -zv <ip> 12445 from the dashboard host
Events arrive but signature is rejected Webhook secret missing or stale Remove the controller and re-add it (the dashboard re-registers and gets a fresh secret)
Source column says "—" Pre-migration row with no controller_id Restart the container; the migration runs on every boot
Names show as Unknown (xxxxxxxx...) Users not synced yet for that controller Click Sync in the Controllers modal
Webhook URL stored in controller points to the wrong address Browser's origin isn't reachable from the controller Set DASHBOARD_BASE_URL in .env, remove + re-add the controller
Port 12445 connection refused Firewall blocking port Add LAN IN firewall rule in UniFi Network (Step 1)
Dashboard shows stale names after a user rename Cache not refreshed Click Sync Users or wait for the hourly auto-sync

Security notes

  • .env is excluded from git via .gitignore — never commit it.
  • API tokens are stored in plaintext in the SQLite DB; the dashboard assumes a LAN-only deployment. Filesystem permissions on ./data/dashboard.db are the only thing protecting them.
  • All admin endpoints (/api/controllers/*, /api/sync-users, /api/reset-day) are unauthenticated. Do not expose port 8000 publicly. If external access is required, place Nginx, Traefik, or Caddy with HTTPS + auth in front of port 8000.
  • Each controller's webhook_secret is enforced via HMAC-SHA256 on incoming events so spoofed webhook posts from outside the LAN are rejected.
S
Description
API Dashboard for Badge In events tied to Unifi Access
Readme 297 KiB
Languages
HTML 55.5%
Python 43.9%
Dockerfile 0.6%