This commit is contained in:
@@ -93,11 +93,17 @@ def init_db():
|
||||
actor_id TEXT NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
filtered INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (controller_id, actor_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
if not _column_exists(db, "user_cache", "filtered"):
|
||||
db.execute(
|
||||
"ALTER TABLE user_cache ADD COLUMN filtered INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
|
||||
# Seed a Default controller from env vars when the table is empty.
|
||||
existing = db.execute("SELECT COUNT(*) AS n FROM controllers").fetchone()["n"]
|
||||
default_id = None
|
||||
@@ -299,6 +305,7 @@ def first_badge_status():
|
||||
date = request.args.get("date", datetime.now(pytz.timezone(TZ)).strftime("%Y-%m-%d"))
|
||||
cutoff = request.args.get("cutoff", "09:00")
|
||||
controller_filter = request.args.get("controller_id", "").strip() or None
|
||||
include_filtered = request.args.get("include_filtered") == "1"
|
||||
|
||||
if not re.match(r"^\d{2}:\d{2}$", cutoff):
|
||||
cutoff = "09:00"
|
||||
@@ -314,7 +321,8 @@ def first_badge_status():
|
||||
COALESCE(
|
||||
u.full_name,
|
||||
'Unknown (' || SUBSTR(b.actor_id,1,8) || '...)'
|
||||
) AS name
|
||||
) AS name,
|
||||
COALESCE(u.filtered, 0) AS filtered
|
||||
FROM badge_events b
|
||||
LEFT JOIN user_cache u
|
||||
ON u.actor_id = b.actor_id AND u.controller_id = b.controller_id
|
||||
@@ -325,6 +333,8 @@ def first_badge_status():
|
||||
if controller_filter:
|
||||
sql += " AND b.controller_id = ?"
|
||||
params.append(controller_filter)
|
||||
if not include_filtered:
|
||||
sql += " AND COALESCE(u.filtered, 0) = 0"
|
||||
sql += " GROUP BY b.actor_id, b.controller_id ORDER BY first_ts ASC"
|
||||
|
||||
with get_db() as db:
|
||||
@@ -335,12 +345,14 @@ def first_badge_status():
|
||||
first = r["first_ts"]
|
||||
latest = r["latest_ts"]
|
||||
result.append({
|
||||
"actor_id": r["actor_id"],
|
||||
"name": r["name"],
|
||||
"source": r["source"] or "—",
|
||||
"first_ts": first,
|
||||
"latest_ts": latest if latest != first else None,
|
||||
"status": "ON TIME" if first <= cutoff_end else "LATE",
|
||||
"actor_id": r["actor_id"],
|
||||
"controller_id": r["controller_id"],
|
||||
"name": r["name"],
|
||||
"source": r["source"] or "—",
|
||||
"first_ts": first,
|
||||
"latest_ts": latest if latest != first else None,
|
||||
"status": "ON TIME" if first <= cutoff_end else "LATE",
|
||||
"filtered": bool(r["filtered"]),
|
||||
})
|
||||
return jsonify(result)
|
||||
|
||||
@@ -575,6 +587,97 @@ def sync_one(controller_id):
|
||||
return jsonify({"status": "ok", "synced": n})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# User cache (tenant filtering)
|
||||
# ---------------------------------------------------------------------------
|
||||
@app.route("/api/users", methods=["GET"])
|
||||
def list_users():
|
||||
controller_id = request.args.get("controller_id", "").strip() or None
|
||||
filtered_arg = request.args.get("filtered")
|
||||
|
||||
sql = """
|
||||
SELECT
|
||||
u.controller_id,
|
||||
u.actor_id,
|
||||
u.full_name,
|
||||
u.updated_at,
|
||||
COALESCE(u.filtered, 0) AS filtered,
|
||||
c.name AS controller_name
|
||||
FROM user_cache u
|
||||
LEFT JOIN controllers c ON c.id = u.controller_id
|
||||
WHERE 1=1
|
||||
"""
|
||||
params = []
|
||||
if controller_id:
|
||||
sql += " AND u.controller_id = ?"
|
||||
params.append(controller_id)
|
||||
if filtered_arg in ("0", "1"):
|
||||
sql += " AND COALESCE(u.filtered, 0) = ?"
|
||||
params.append(int(filtered_arg))
|
||||
sql += " ORDER BY c.name, u.full_name"
|
||||
|
||||
with get_db() as db:
|
||||
rows = db.execute(sql, params).fetchall()
|
||||
return jsonify([
|
||||
{
|
||||
"controller_id": r["controller_id"],
|
||||
"controller_name": r["controller_name"] or "—",
|
||||
"actor_id": r["actor_id"],
|
||||
"full_name": r["full_name"],
|
||||
"updated_at": r["updated_at"],
|
||||
"filtered": bool(r["filtered"]),
|
||||
}
|
||||
for r in rows
|
||||
])
|
||||
|
||||
|
||||
@app.route("/api/users/<controller_id>/<actor_id>", methods=["PATCH"])
|
||||
def update_user(controller_id, actor_id):
|
||||
body = request.get_json(silent=True) or {}
|
||||
if "filtered" not in body:
|
||||
return jsonify({"error": "filtered field required"}), 400
|
||||
filtered = 1 if body["filtered"] else 0
|
||||
|
||||
with get_db() as db:
|
||||
ctrl = db.execute(
|
||||
"SELECT id FROM controllers WHERE id = ?", (controller_id,)
|
||||
).fetchone()
|
||||
if not ctrl:
|
||||
return jsonify({"error": "unknown controller"}), 404
|
||||
|
||||
now_iso = datetime.now(timezone.utc).isoformat()
|
||||
placeholder = f"User {actor_id[:8]}"
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO user_cache (controller_id, actor_id, full_name, updated_at, filtered)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(controller_id, actor_id) DO UPDATE SET
|
||||
filtered = excluded.filtered
|
||||
""",
|
||||
(controller_id, actor_id, placeholder, now_iso, filtered),
|
||||
)
|
||||
db.commit()
|
||||
row = db.execute(
|
||||
"""
|
||||
SELECT u.controller_id, u.actor_id, u.full_name, u.updated_at,
|
||||
COALESCE(u.filtered, 0) AS filtered, c.name AS controller_name
|
||||
FROM user_cache u
|
||||
LEFT JOIN controllers c ON c.id = u.controller_id
|
||||
WHERE u.controller_id = ? AND u.actor_id = ?
|
||||
""",
|
||||
(controller_id, actor_id),
|
||||
).fetchone()
|
||||
|
||||
return jsonify({
|
||||
"controller_id": row["controller_id"],
|
||||
"controller_name": row["controller_name"] or "—",
|
||||
"actor_id": row["actor_id"],
|
||||
"full_name": row["full_name"],
|
||||
"updated_at": row["updated_at"],
|
||||
"filtered": bool(row["filtered"]),
|
||||
})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Misc admin
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user