ux: activity logging for all CRUD ops, batch status management, dashboard now shows recent activity
This commit is contained in:
+104
-9
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user