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:
@@ -59,7 +59,8 @@ def init_db():
|
||||
webhook_id TEXT NOT NULL DEFAULT '',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
last_sync_at TEXT
|
||||
last_sync_at TEXT,
|
||||
last_event_at TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
@@ -104,6 +105,10 @@ def init_db():
|
||||
"ALTER TABLE user_cache ADD COLUMN filtered INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
|
||||
# Migrate legacy controllers that pre-date the last_event_at column.
|
||||
if not _column_exists(db, "controllers", "last_event_at"):
|
||||
db.execute("ALTER TABLE controllers ADD COLUMN last_event_at TEXT")
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS persons (
|
||||
@@ -314,6 +319,7 @@ def controller_to_dict(row):
|
||||
"enabled": bool(row["enabled"]),
|
||||
"has_webhook": bool(row["webhook_id"]),
|
||||
"last_sync_at": row["last_sync_at"],
|
||||
"last_event_at": row["last_event_at"],
|
||||
}
|
||||
|
||||
|
||||
@@ -399,6 +405,56 @@ def first_badge_status():
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route("/api/badge-events")
|
||||
def badge_events_for_subject():
|
||||
"""All badge-in events for one subject on one day, oldest first. A subject is
|
||||
either a merged person (person_id) or a single actor (controller_id+actor_id)."""
|
||||
date = request.args.get("date", datetime.now(pytz.timezone(TZ)).strftime("%Y-%m-%d"))
|
||||
person_id = request.args.get("person_id", "").strip() or None
|
||||
ctrl_id = request.args.get("controller_id", "").strip() or None
|
||||
actor_id = request.args.get("actor_id", "").strip() or None
|
||||
|
||||
if not re.match(r"^\d{4}-\d{2}-\d{2}$", date):
|
||||
return jsonify({"error": "date must be YYYY-MM-DD"}), 400
|
||||
|
||||
with get_db() as db:
|
||||
if person_id:
|
||||
members = db.execute(
|
||||
"SELECT controller_id, actor_id FROM person_members WHERE person_id = ?",
|
||||
(person_id,),
|
||||
).fetchall()
|
||||
pairs = [(m["controller_id"], m["actor_id"]) for m in members]
|
||||
elif ctrl_id and actor_id:
|
||||
pairs = [(ctrl_id, actor_id)]
|
||||
else:
|
||||
return jsonify({"error": "person_id, or controller_id + actor_id, required"}), 400
|
||||
|
||||
if not pairs:
|
||||
return jsonify([])
|
||||
|
||||
placeholders = ",".join(["(?,?)"] * len(pairs))
|
||||
sql = f"""
|
||||
SELECT b.ts, b.controller_id, b.actor_id, c.name AS source
|
||||
FROM badge_events b
|
||||
LEFT JOIN controllers c ON c.id = b.controller_id
|
||||
WHERE b.date = ?
|
||||
AND (b.controller_id, b.actor_id) IN ({placeholders})
|
||||
ORDER BY b.ts ASC
|
||||
"""
|
||||
params = [date] + [v for pair in pairs for v in pair]
|
||||
rows = db.execute(sql, params).fetchall()
|
||||
|
||||
return jsonify([
|
||||
{
|
||||
"ts": r["ts"],
|
||||
"controller_id": r["controller_id"],
|
||||
"actor_id": r["actor_id"],
|
||||
"source": r["source"] or "—",
|
||||
}
|
||||
for r in rows
|
||||
])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reporting — first badge-in times over a date range
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -678,6 +734,10 @@ def _ingest_webhook(controller_id):
|
||||
"INSERT INTO badge_events (controller_id, actor_id, ts, date) VALUES (?, ?, ?, ?)",
|
||||
(controller_id, actor, ts_str, date),
|
||||
)
|
||||
db.execute(
|
||||
"UPDATE controllers SET last_event_at = ? WHERE id = ?",
|
||||
(datetime.now(timezone.utc).isoformat(), controller_id),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
log.info(
|
||||
|
||||
Reference in New Issue
Block a user