From f74b5fd7c5a5247d02ae95a9dfce278130085f4b Mon Sep 17 00:00:00 2001 From: jasonMPM Date: Wed, 4 Mar 2026 20:56:30 -0600 Subject: [PATCH] Add files via upload --- README.md | 218 +++++++++++++++++++++++++++++++++++++++++++----------- app.py | 10 ++- 2 files changed, 180 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index f4e5fc2..398fde0 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,15 @@ attendance dashboard with on-time / late status, first + latest badge times, and ## Features -- Receives webhooks from the UniFi Access developer API or legacy Alarm Manager format. -- Resolves UUIDs to real names via the UniFi Access REST API (cached in SQLite, refreshed every 6 hrs). -- **First badge in** column — never overwritten by subsequent badges. -- **Latest badge in** column — shows the most recent entry that day. -- **Sync Users** button — manually refreshes the user name cache. -- **Reset Day** button — confirmation modal deletes all records for the selected date (testing only). -- Green ON TIME / Red LATE status chips based on a configurable cutoff time. -- Fully Dockerised — single container, persistent SQLite volume. +- Receives webhooks from the UniFi Access developer API or legacy Alarm Manager format +- Resolves badge holder UUIDs to real names via the UniFi Access REST API +- User name cache stored in SQLite, auto-refreshed every 6 hours +- **First Badge In** column — locked to the earliest entry of the day, never overwritten +- **Latest Badge In** column — updates with each subsequent badge-in +- ON TIME / LATE status based on a configurable cutoff time (green / red chips) +- **Sync Users** button — manually pulls the latest user list from your controller +- **Reset Day** button — confirmation modal wipes all records for the selected date (testing only) +- Fully Dockerised — single container, SQLite persisted to a host volume --- @@ -23,33 +24,92 @@ attendance dashboard with on-time / late status, first + latest badge times, and ``` . -├── app.py -├── requirements.txt -├── Dockerfile -├── docker-compose.yml -├── .env.example +├── app.py # Flask application + API endpoints +├── requirements.txt # Python dependencies +├── Dockerfile # Container image definition +├── docker-compose.yml # Compose file for Unraid deployment +├── .env.example # Environment variable template (copy to .env) ├── .gitignore └── static/ - └── index.html + └── index.html # Single-page dashboard UI ``` --- -## Setup +## Step 1 — Generate a UniFi Access API token -### 1. Generate a UniFi Access API token +1. Open your **UniFi OS** web interface (e.g. `https://192.168.1.1`). +2. Navigate to **UniFi Access → Settings → Integrations** (or **Developer API**). +3. Click **Create API Key**, give it a name (e.g. `Dashboard`), and copy the token. -1. Open UniFi OS → UniFi Access → Settings → Integrations / Developer API. -2. Create a new API Key (Bearer Token) and copy it. +> Use a dedicated read-only admin account to generate the token where possible. -### 2. Create your .env file +--- + +## Step 2 — Clone the repo on your Unraid server + +SSH into Unraid and run: + +```bash +cd /mnt/user/appdata +git clone https://github.com//.git unifi-access-dashboard +cd unifi-access-dashboard +``` + +--- + +## Step 3 — Create your .env file ```bash cp .env.example .env -# edit .env and fill in UNIFI_HOST and UNIFI_API_TOKEN +nano .env ``` -### 3. Register the webhook with UniFi Access (run once) +Fill in your values: + +```dotenv +UNIFI_HOST=192.168.1.1 # IP address of your UniFi OS controller +UNIFI_API_TOKEN=YOUR_TOKEN_HERE # Bearer token from Step 1 +TZ=America/Chicago # Your local timezone +DB_PATH=/data/dashboard.db # Path inside the container — do not change +``` + +> **Never commit `.env` to git.** It is listed in `.gitignore`. + +--- + +## Step 4 — Build and start the container + +```bash +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) +- Immediately sync users from your UniFi Access controller +- Schedule a user cache refresh every 6 hours + +Verify it is running: + +```bash +docker ps +# Should show: unifi-access-dashboard Up X seconds +``` + +Check logs: + +```bash +docker logs -f unifi-access-dashboard +``` + +--- + +## Step 5 — Register the webhook with UniFi Access (run once) + +Run this from any machine on the same LAN as your controller. +Replace `192.168.1.1`, `YOUR_TOKEN_HERE`, and `YOUR_UNRAID_IP` with your real values: ```bash curl -k -X POST "https://192.168.1.1:45/api1/webhooks/endpoints" \ @@ -63,26 +123,85 @@ curl -k -X POST "https://192.168.1.1:45/api1/webhooks/endpoints" \ }' ``` -Verify registration: +Verify it was registered: ```bash curl -k -X GET "https://192.168.1.1:45/api1/webhooks/endpoints" \ -H "Authorization: Bearer YOUR_TOKEN_HERE" ``` -### 4. Deploy on Unraid +You should see your webhook listed with a unique `id` field. -```bash -cd /mnt/user/appdata -git clone https://github.com//.git unifi-access-dashboard -cd unifi-access-dashboard -cp .env.example .env && nano .env -docker compose up -d --build +> **Note:** The UniFi Access API runs on **port 45** over HTTPS with a self-signed certificate. +> Always use `-k` with curl, and `verify=False` is already set in the Python code. + +--- + +## Step 6 — Open the dashboard + +In a browser navigate to: + +``` +http://:8000/ ``` -Open: `http://:8000/` +### Using the dashboard -### 5. Updating from GitHub +| Control | Description | +|---|---| +| **Date picker** | Choose which day to view | +| **Badged in by** | Set your on-time cutoff (24-hr format, e.g. `09:00`) | +| **Refresh** | Reload the table for the selected date and cutoff | +| **Sync Users** | Immediately pull the latest user list from UniFi Access | +| **Reset Day** | Delete all badge-in records for the selected date (testing only — requires confirmation) | + +### Status chips + +| Chip | Meaning | +|---|---| +| 🟢 ON TIME | First badge-in was at or before the cutoff | +| 🔴 LATE | First badge-in was after the cutoff | + +### Columns + +| Column | Description | +|---|---| +| **#** | Row number | +| **Name** | Resolved display name from UniFi Access | +| **First Badge In** | Earliest door entry for the day — never changes | +| **Latest Badge In** | Most recent door entry — shows *"— same"* if only one badge event | +| **Actor ID** | First 8 characters of the UniFi user UUID | +| **Status** | ON TIME or LATE chip based on first badge vs cutoff | + +--- + +## Unraid GUI deployment (alternative to docker-compose) + +If you prefer the Unraid Docker tab UI after pushing an image to Docker Hub: + +```bash +# On your workstation: +docker build -t /unifi-access-dashboard:latest . +docker push /unifi-access-dashboard:latest +``` + +Then in Unraid → **Docker → Add Container**: + +| Field | Value | +|---|---| +| Name | `unifi-access-dashboard` | +| Repository | `/unifi-access-dashboard:latest` | +| Network Type | Bridge | +| Port mapping | Host `8000` → Container `8000` (TCP) | +| Path mapping | Host `/mnt/user/appdata/unifi-access-dashboard/data` → Container `/data` | +| Variable: `UNIFI_HOST` | `192.168.1.x` | +| Variable: `UNIFI_API_TOKEN` | your token | +| Variable: `TZ` | e.g. `America/Chicago` | +| Variable: `DB_PATH` | `/data/dashboard.db` | + +--- + +## Updating from GitHub ```bash cd /mnt/user/appdata/unifi-access-dashboard @@ -92,23 +211,34 @@ docker compose up -d --build --- -## API endpoints +## API endpoints reference -| Method | Path | Description | -|---|---|---| -| POST | `/api/unifi-access` | Receives webhook from UniFi Access | -| GET | `/api/first-badge-status` | Returns first + latest badge per user for a date | -| GET | `/api/sync-users` | Triggers immediate user cache sync | -| DELETE | `/api/reset-day?date=YYYY-MM-DD` | Deletes all records for the given date | +| Method | Path | Query params | Description | +|---|---|---|---| +| `POST` | `/api/unifi-access` | — | Receives UniFi Access webhook | +| `GET` | `/api/first-badge-status` | `date`, `cutoff` | Returns first + latest badge per user | +| `GET` | `/api/sync-users` | — | Triggers immediate user cache sync | +| `DELETE` | `/api/reset-day` | `date` | Deletes all records for the given date | --- ## Troubleshooting -| Symptom | Cause | Fix | +| Symptom | Likely cause | Fix | |---|---|---| -| Names show as `Unknown (UUID…)` | Users not cached yet | Click Sync Users | -| Webhook not arriving | Firewall / port | Ensure port 8000 reachable from controller | -| SSL error on curl | Self-signed cert | Use `-k` flag | -| 404 on `/api1/users` | Firmware path differs | Try `/api/v1/users` | -| Duplicate events | Both Alarm Manager and API webhooks active | Remove one or deduplicate by event ID | +| Names show as `Unknown (UUID…)` | Users not cached yet | Click **Sync Users** or wait for the 6-hr job | +| Webhook POST not arriving | Firewall or Docker network | Ensure port `8000` is reachable from the UniFi controller IP | +| `curl` returns SSL error | Self-signed cert on controller | Add `-k` to curl; already handled in Python | +| 404 on `/api1/users` | API path differs for your firmware | Try `/api/v1/users`; check your Access app version | +| Flask exits immediately | `.env` missing or malformed | Ensure `.env` exists alongside `docker-compose.yml` | +| Duplicate events | Both Alarm Manager and API webhooks active | Remove one; both are harmless but will store duplicate rows | +| Container rebuilds but old DB persists | Volume mount working correctly | This is expected — `./data/` survives rebuilds | + +--- + +## Security notes + +- `.env` is excluded from git via `.gitignore` — never commit it. +- The API token is never exposed to the browser or frontend. +- For access outside your LAN, place Nginx or Traefik with HTTPS in front of port `8000`. +- The `/api/reset-day` endpoint has no authentication — keep the container on your internal network only. diff --git a/app.py b/app.py index d51ad04..0ffa776 100644 --- a/app.py +++ b/app.py @@ -25,7 +25,6 @@ def get_db(): return conn -@app.before_first_request def init_db(): conn = get_db() conn.executescript(""" @@ -248,12 +247,15 @@ def send_static(path): # ── Startup ─────────────────────────────────────────────────────────────────── +# Initialise DB and kick off background scheduler at import time +# (works whether started via `flask run` or `python app.py`) +with app.app_context(): + init_db() + sync_unifi_users() + scheduler = BackgroundScheduler() scheduler.add_job(sync_unifi_users, "interval", hours=6) scheduler.start() if __name__ == "__main__": - with app.app_context(): - init_db() - sync_unifi_users() app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8000))) -- 2.49.1