diff --git a/admin_server.py b/admin_server.py index 36a39c4..ac610b4 100644 --- a/admin_server.py +++ b/admin_server.py @@ -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", diff --git a/static/index.html b/static/index.html index 958060d..1d946bc 100644 --- a/static/index.html +++ b/static/index.html @@ -1110,6 +1110,7 @@ +