ux: activity logging for all CRUD ops, batch status management, dashboard now shows recent activity

This commit is contained in:
Leo
2026-05-30 21:20:49 -04:00
parent 2cafe6879a
commit a3328c765d
2 changed files with 160 additions and 12 deletions
+104 -9
View File
@@ -756,6 +756,9 @@ def update_user(user_id: int, body: UserUpdate):
if body.role is not None:
role = validate_roles(body.role)
conn.execute("UPDATE users SET role = ? WHERE id = ?", (role, user_id))
_log_activity(conn, "updated", "user", user_id,
f"User '{existing['username']}' role changed to '{role}'")
conn.commit()
if body.password is not None:
pw = body.password.strip()
@@ -764,6 +767,9 @@ def update_user(user_id: int, body: UserUpdate):
raise HTTPException(status_code=422, detail="Password must not be empty")
password_hash = _hash_password(pw)
conn.execute("UPDATE users SET password_hash = ? WHERE id = ?", (password_hash, user_id))
_log_activity(conn, "updated", "user", user_id,
f"User '{existing['username']}' password changed")
conn.commit()
conn.commit()
row = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
@@ -774,12 +780,15 @@ def update_user(user_id: int, body: UserUpdate):
@app.delete("/api/users/{user_id}", status_code=204)
def delete_user(user_id: int):
conn = get_db()
existing = conn.execute("SELECT id FROM users WHERE id = ?", (user_id,)).fetchone()
existing = conn.execute("SELECT id, username FROM users WHERE id = ?", (user_id,)).fetchone()
if existing is None:
conn.close()
raise HTTPException(status_code=404, detail="User not found")
conn.execute("DELETE FROM users WHERE id = ?", (user_id,))
conn.commit()
_log_activity(conn, "deleted", "user", user_id,
f"User '{existing['username']}' deleted")
conn.commit()
conn.close()
@@ -829,6 +838,9 @@ def login(body: dict):
(row["id"], token),
)
conn.commit()
_log_activity(conn, "login", "user", row["id"],
f"User '{username}' logged in")
conn.commit()
conn.close()
result = _user_to_dict(row)
@@ -992,6 +1004,9 @@ def create_location(body: LocationCreate):
)
conn.commit()
loc_id = cursor.lastrowid
_log_activity(conn, "created", "location", loc_id,
f"Location '{name}' created")
conn.commit()
row = conn.execute("SELECT * FROM locations WHERE id = ?", (loc_id,)).fetchone()
result = row_to_dict(row)
result["rooms"] = []
@@ -1041,7 +1056,7 @@ _LOCATION_FIELDS = [
@app.put("/api/locations/{loc_id}")
def update_location(loc_id: int, body: LocationUpdate):
conn = get_db()
existing = conn.execute("SELECT id FROM locations WHERE id = ?", (loc_id,)).fetchone()
existing = conn.execute("SELECT id, name FROM locations WHERE id = ?", (loc_id,)).fetchone()
if existing is None:
conn.close()
raise HTTPException(status_code=404, detail="Location not found")
@@ -1077,6 +1092,9 @@ def update_location(loc_id: int, body: LocationUpdate):
values + [loc_id],
)
conn.commit()
_log_activity(conn, "updated", "location", loc_id,
f"Location '{existing['name']}' updated")
conn.commit()
row = conn.execute("SELECT * FROM locations WHERE id = ?", (loc_id,)).fetchone()
result = row_to_dict(row)
@@ -1088,12 +1106,15 @@ def update_location(loc_id: int, body: LocationUpdate):
@app.delete("/api/locations/{loc_id}", status_code=204)
def delete_location(loc_id: int):
conn = get_db()
existing = conn.execute("SELECT id FROM locations WHERE id = ?", (loc_id,)).fetchone()
existing = conn.execute("SELECT id, name FROM locations WHERE id = ?", (loc_id,)).fetchone()
if existing is None:
conn.close()
raise HTTPException(status_code=404, detail="Location not found")
conn.execute("DELETE FROM locations WHERE id = ?", (loc_id,))
conn.commit()
_log_activity(conn, "deleted", "location", loc_id,
f"Location '{existing['name']}' deleted")
conn.commit()
conn.close()
@@ -1135,6 +1156,9 @@ def create_room(body: RoomCreate):
)
conn.commit()
room_id = cursor.lastrowid
_log_activity(conn, "created", "room", room_id,
f"Room '{body.name.strip()}' created in location {body.location_id}")
conn.commit()
row = conn.execute("SELECT * FROM rooms WHERE id = ?", (room_id,)).fetchone()
conn.close()
return row_to_dict(row)
@@ -1143,7 +1167,7 @@ def create_room(body: RoomCreate):
@app.put("/api/rooms/{room_id}")
def update_room(room_id: int, body: RoomUpdate):
conn = get_db()
existing = conn.execute("SELECT id FROM rooms WHERE id = ?", (room_id,)).fetchone()
existing = conn.execute("SELECT id, name FROM rooms WHERE id = ?", (room_id,)).fetchone()
if existing is None:
conn.close()
raise HTTPException(status_code=404, detail="Room not found")
@@ -1169,6 +1193,9 @@ def update_room(room_id: int, body: RoomUpdate):
values + [room_id],
)
conn.commit()
_log_activity(conn, "updated", "room", room_id,
f"Room '{existing['name']}' updated")
conn.commit()
row = conn.execute("SELECT * FROM rooms WHERE id = ?", (room_id,)).fetchone()
conn.close()
@@ -1178,12 +1205,15 @@ def update_room(room_id: int, body: RoomUpdate):
@app.delete("/api/rooms/{room_id}", status_code=204)
def delete_room(room_id: int):
conn = get_db()
existing = conn.execute("SELECT id FROM rooms WHERE id = ?", (room_id,)).fetchone()
existing = conn.execute("SELECT id, name FROM rooms WHERE id = ?", (room_id,)).fetchone()
if existing is None:
conn.close()
raise HTTPException(status_code=404, detail="Room not found")
conn.execute("DELETE FROM rooms WHERE id = ?", (room_id,))
conn.commit()
_log_activity(conn, "deleted", "room", room_id,
f"Room '{existing['name']}' deleted")
conn.commit()
conn.close()
@@ -1226,6 +1256,9 @@ def _settings_create(conn: sqlite3.Connection, entity: str, body: dict):
)
conn.commit()
eid = cursor.lastrowid
_log_activity(conn, "created", entity, eid,
f"{entity[:-1].title()} '{body.get('name', '')}' created")
conn.commit()
row = conn.execute(f"SELECT * FROM {table} WHERE id = ?", (eid,)).fetchone()
return row_to_dict(row)
except sqlite3.IntegrityError:
@@ -1237,7 +1270,7 @@ def _settings_update(conn: sqlite3.Connection, entity: str, eid: int, body: dict
table = schema["table"]
columns = schema["columns"]
existing = conn.execute(f"SELECT id FROM {table} WHERE id = ?", (eid,)).fetchone()
existing = conn.execute(f"SELECT id, name FROM {table} WHERE id = ?", (eid,)).fetchone()
if existing is None:
raise HTTPException(status_code=404, detail=f"{entity[:-1]} not found")
@@ -1253,6 +1286,9 @@ def _settings_update(conn: sqlite3.Connection, entity: str, eid: int, body: dict
f"UPDATE {table} SET {set_clause} WHERE id = ?", values + [eid]
)
conn.commit()
_log_activity(conn, "updated", entity, eid,
f"{entity[:-1].title()} '{existing['name']}' updated")
conn.commit()
row = conn.execute(f"SELECT * FROM {table} WHERE id = ?", (eid,)).fetchone()
return row_to_dict(row)
@@ -1270,11 +1306,14 @@ def _settings_get(conn: sqlite3.Connection, entity: str, eid: int):
def _settings_delete(conn: sqlite3.Connection, entity: str, eid: int):
schema = _SETTINGS_SCHEMAS[entity]
table = schema["table"]
existing = conn.execute(f"SELECT id FROM {table} WHERE id = ?", (eid,)).fetchone()
existing = conn.execute(f"SELECT id, name FROM {table} WHERE id = ?", (eid,)).fetchone()
if existing is None:
raise HTTPException(status_code=404, detail=f"{entity[:-1]} not found")
conn.execute(f"DELETE FROM {table} WHERE id = ?", (eid,))
conn.commit()
_log_activity(conn, "deleted", entity, eid,
f"{entity[:-1].title()} '{existing['name']}' deleted")
conn.commit()
_SETTINGS_ENTITIES = ["categories", "makes", "models", "key_names", "key_types", "badge_types"]
@@ -1376,7 +1415,7 @@ def list_geofences():
def update_geofence(geofence_id: int, body: GeofenceUpdate):
conn = get_db()
existing = conn.execute(
"SELECT id FROM geofences WHERE id = ?", (geofence_id,)
"SELECT id, name FROM geofences WHERE id = ?", (geofence_id,)
).fetchone()
if existing is None:
conn.close()
@@ -1407,6 +1446,10 @@ def update_geofence(geofence_id: int, body: GeofenceUpdate):
_sync_geofence_users(conn, geofence_id, body.user_ids)
conn.commit()
_log_activity(conn, "updated", "geofence", geofence_id,
f"Geofence '{existing['name']}' updated")
conn.commit()
row = conn.execute(
"SELECT * FROM geofences WHERE id = ?", (geofence_id,)
).fetchone()
@@ -1419,13 +1462,16 @@ def update_geofence(geofence_id: int, body: GeofenceUpdate):
def delete_geofence(geofence_id: int):
conn = get_db()
existing = conn.execute(
"SELECT id FROM geofences WHERE id = ?", (geofence_id,)
"SELECT id, name FROM geofences WHERE id = ?", (geofence_id,)
).fetchone()
if existing is None:
conn.close()
raise HTTPException(status_code=404, detail="Geofence not found")
conn.execute("DELETE FROM geofences WHERE id = ?", (geofence_id,))
conn.commit()
_log_activity(conn, "deleted", "geofence", geofence_id,
f"Geofence '{existing['name']}' deleted")
conn.commit()
conn.close()
@@ -1761,6 +1807,51 @@ def clear_asset_gps(asset_id: int):
conn.close()
# ─── Batch Status Update API ─────────────────────────────────────────────────
class BatchStatusUpdate(BaseModel):
ids: list[int]
status: str
@app.post("/api/assets/batch-status")
def batch_update_status(body: BatchStatusUpdate):
"""Update the status of multiple assets at once."""
if not body.ids:
raise HTTPException(status_code=422, detail="No asset IDs provided")
if body.status not in VALID_STATUSES:
raise HTTPException(
status_code=422,
detail=f"Invalid status. Must be one of: {', '.join(sorted(VALID_STATUSES))}",
)
conn = get_db()
placeholders = ",".join(["?"] * len(body.ids))
# Verify all assets exist
existing = conn.execute(
f"SELECT id, name FROM assets WHERE id IN ({placeholders})", body.ids
).fetchall()
if len(existing) != len(body.ids):
conn.close()
raise HTTPException(status_code=404, detail="One or more asset IDs not found")
# Update all assets
conn.execute(
f"UPDATE assets SET status = ?, updated_at = datetime('now') WHERE id IN ({placeholders})",
[body.status] + body.ids,
)
_log_activity(conn, "updated", "asset", 0,
f"Batch status change: {len(body.ids)} assets set to '{body.status}'")
conn.commit()
conn.close()
return {"updated": len(body.ids), "status": body.status}
# ─── Icon Upload API ────────────────────────────────────────────────────────
@@ -1795,6 +1886,10 @@ def reset_database(request: Request):
try:
# Disable FK checks for clean drop
conn.execute("PRAGMA foreign_keys=OFF")
username = user.get("username", "Unknown") if user else "Unknown"
_log_activity(conn, "reset", "database", 0,
f"Database reset by '{username}'")
conn.commit()
tables = [
"visits", "activity_log", "geofence_users", "geofences",
"asset_badges", "asset_keys", "checkins", "sessions",