From 28af669993a9d4bf7f8e421e6a314f3253c24b1f Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Tue, 21 Oct 2025 20:23:58 +0000 Subject: [PATCH] Creating admin interface --- .copilot-instructions.md | 57 ++ backend/app/api/endpoints/pprs.py | 48 +- backend/app/core/utils.py | 14 + backend/app/crud/crud_journal.py | 35 + backend/app/crud/crud_ppr.py | 63 +- web/admin.html | 1078 +++++++++++++++++++++++++++++ 6 files changed, 1276 insertions(+), 19 deletions(-) create mode 100644 backend/app/core/utils.py create mode 100644 backend/app/crud/crud_journal.py create mode 100644 web/admin.html diff --git a/.copilot-instructions.md b/.copilot-instructions.md index 5791bea..d2334af 100644 --- a/.copilot-instructions.md +++ b/.copilot-instructions.md @@ -216,10 +216,67 @@ The API sends real-time updates via WebSocket for: - **Username:** `admin` - **Password:** `admin123` +## Journal System + +All PPR changes are automatically logged in a journal system for audit trail purposes. + +### Get PPR Journal Entries + +**Endpoint:** `GET /api/v1/pprs/{ppr_id}/journal` + +**Response:** +```json +[ + { + "id": 1, + "ppr_id": 3, + "entry": "PPR created for G-ADMIN", + "user": "admin", + "ip": "172.23.0.1", + "entry_dt": "2025-10-21T20:01:01" + }, + { + "id": 2, + "ppr_id": 3, + "entry": "captain changed from 'Test Admin' to 'Updated Admin User'", + "user": "admin", + "ip": "172.23.0.1", + "entry_dt": "2025-10-21T20:01:17" + } +] +``` + +### Automatic Journal Logging + +The system automatically logs: +- PPR creation +- All field changes with old and new values +- Status changes +- User and IP address for each change + +## Web Interfaces + +### Public Arrivals/Departures Board +- **URL:** http://localhost:8082 +- **Features:** Real-time arrivals and departures display +- **Authentication:** None required + +### Admin Interface +- **URL:** http://localhost:8082/admin.html +- **Features:** + - Complete PPR management (CRUD operations) + - Advanced filtering by status, date range + - Inline editing with modal interface + - Journal/audit trail viewing + - Quick status updates (Confirm, Land, Depart, Cancel) + - New PPR entry creation +- **Authentication:** Username/password prompt (uses API token) + ## Development URLs - API Base: http://localhost:8001/api/v1 - Public Web Interface: http://localhost:8082 +- Admin Interface: http://localhost:8082/admin.html - API Documentation: http://localhost:8001/docs - Database: localhost:3307 (MySQL) diff --git a/backend/app/api/endpoints/pprs.py b/backend/app/api/endpoints/pprs.py index 8255d13..06b18c4 100644 --- a/backend/app/api/endpoints/pprs.py +++ b/backend/app/api/endpoints/pprs.py @@ -4,8 +4,10 @@ from sqlalchemy.orm import Session from datetime import date from app.api.deps import get_db, get_current_active_user from app.crud.crud_ppr import ppr as crud_ppr -from app.schemas.ppr import PPR, PPRCreate, PPRUpdate, PPRStatus, PPRStatusUpdate +from app.crud.crud_journal import journal as crud_journal +from app.schemas.ppr import PPR, PPRCreate, PPRUpdate, PPRStatus, PPRStatusUpdate, Journal from app.models.ppr import User +from app.core.utils import get_client_ip router = APIRouter() @@ -37,7 +39,8 @@ async def create_ppr( current_user: User = Depends(get_current_active_user) ): """Create a new PPR record""" - ppr = crud_ppr.create(db, obj_in=ppr_in, created_by=current_user.username) + client_ip = get_client_ip(request) + ppr = crud_ppr.create(db, obj_in=ppr_in, created_by=current_user.username, user_ip=client_ip) # Send real-time update via WebSocket if hasattr(request.app.state, 'connection_manager'): @@ -85,7 +88,8 @@ async def update_ppr( detail="PPR record not found" ) - ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in) + client_ip = get_client_ip(request) + ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in, user=current_user.username, user_ip=client_ip) # Send real-time update if hasattr(request.app.state, 'connection_manager'): @@ -117,8 +121,8 @@ async def patch_ppr( detail="PPR record not found" ) - # For PATCH, we only update fields that are explicitly provided (not None) - ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in) + client_ip = get_client_ip(request) + ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in, user=current_user.username, user_ip=client_ip) # Send real-time update if hasattr(request.app.state, 'connection_manager'): @@ -143,15 +147,20 @@ async def update_ppr_status( current_user: User = Depends(get_current_active_user) ): """Update PPR status (LANDED, DEPARTED, etc.)""" - ppr = crud_ppr.update_status(db, ppr_id=ppr_id, status=status_update.status) + client_ip = get_client_ip(request) + ppr = crud_ppr.update_status( + db, + ppr_id=ppr_id, + status=status_update.status, + user=current_user.username, + user_ip=client_ip + ) if not ppr: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="PPR record not found" ) - # Log the status change (you might want to create a journal entry here) - # Send real-time update if hasattr(request.app.state, 'connection_manager'): await request.app.state.connection_manager.broadcast({ @@ -175,7 +184,8 @@ async def delete_ppr( current_user: User = Depends(get_current_active_user) ): """Delete (soft delete) a PPR record""" - ppr = crud_ppr.delete(db, ppr_id=ppr_id) + client_ip = get_client_ip(request) + ppr = crud_ppr.delete(db, ppr_id=ppr_id, user=current_user.username, user_ip=client_ip) if not ppr: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -192,4 +202,22 @@ async def delete_ppr( } }) - return ppr \ No newline at end of file + return ppr + + +@router.get("/{ppr_id}/journal", response_model=List[Journal]) +async def get_ppr_journal( + ppr_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_active_user) +): + """Get journal entries for a specific PPR""" + # Verify PPR exists + ppr = crud_ppr.get(db, ppr_id=ppr_id) + if not ppr: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="PPR record not found" + ) + + return crud_journal.get_by_ppr_id(db, ppr_id=ppr_id) \ No newline at end of file diff --git a/backend/app/core/utils.py b/backend/app/core/utils.py new file mode 100644 index 0000000..a0d2339 --- /dev/null +++ b/backend/app/core/utils.py @@ -0,0 +1,14 @@ +from fastapi import Request + + +def get_client_ip(request: Request) -> str: + """Extract client IP address from request""" + forwarded = request.headers.get("X-Forwarded-For") + if forwarded: + return forwarded.split(",")[0].strip() + + real_ip = request.headers.get("X-Real-IP") + if real_ip: + return real_ip + + return request.client.host if request.client else "127.0.0.1" \ No newline at end of file diff --git a/backend/app/crud/crud_journal.py b/backend/app/crud/crud_journal.py new file mode 100644 index 0000000..669527f --- /dev/null +++ b/backend/app/crud/crud_journal.py @@ -0,0 +1,35 @@ +from typing import List +from sqlalchemy.orm import Session +from app.models.ppr import Journal +from app.schemas.ppr import JournalCreate + + +class CRUDJournal: + def create(self, db: Session, obj_in: JournalCreate) -> Journal: + db_obj = Journal(**obj_in.dict()) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def get_by_ppr_id(self, db: Session, ppr_id: int) -> List[Journal]: + return db.query(Journal).filter(Journal.ppr_id == ppr_id).order_by(Journal.entry_dt.desc()).all() + + def log_change( + self, + db: Session, + ppr_id: int, + entry: str, + user: str, + ip: str + ) -> Journal: + journal_in = JournalCreate( + ppr_id=ppr_id, + entry=entry, + user=user, + ip=ip + ) + return self.create(db, journal_in) + + +journal = CRUDJournal() \ No newline at end of file diff --git a/backend/app/crud/crud_ppr.py b/backend/app/crud/crud_ppr.py index 76225cf..dd10be5 100644 --- a/backend/app/crud/crud_ppr.py +++ b/backend/app/crud/crud_ppr.py @@ -4,6 +4,7 @@ from sqlalchemy import and_, or_, func, desc from datetime import date, datetime from app.models.ppr import PPRRecord, PPRStatus from app.schemas.ppr import PPRCreate, PPRUpdate +from app.crud.crud_journal import journal as crud_journal class CRUDPPR: @@ -62,7 +63,7 @@ class CRUDPPR: ) ).order_by(PPRRecord.etd).all() - def create(self, db: Session, obj_in: PPRCreate, created_by: str) -> PPRRecord: + def create(self, db: Session, obj_in: PPRCreate, created_by: str, user_ip: str = "127.0.0.1") -> PPRRecord: db_obj = PPRRecord( **obj_in.dict(), created_by=created_by, @@ -71,28 +72,52 @@ class CRUDPPR: db.add(db_obj) db.commit() db.refresh(db_obj) + + # Log creation in journal + crud_journal.log_change( + db, + db_obj.id, + f"PPR created for {db_obj.ac_reg}", + created_by, + user_ip + ) + return db_obj - def update(self, db: Session, db_obj: PPRRecord, obj_in: PPRUpdate) -> PPRRecord: + def update(self, db: Session, db_obj: PPRRecord, obj_in: PPRUpdate, user: str = "system", user_ip: str = "127.0.0.1") -> PPRRecord: update_data = obj_in.dict(exclude_unset=True) - for field, value in update_data.items(): - setattr(db_obj, field, value) + changes = [] + + for field, value in update_data.items(): + old_value = getattr(db_obj, field) + if old_value != value: + changes.append(f"{field} changed from '{old_value}' to '{value}'") + setattr(db_obj, field, value) + + if changes: + db.add(db_obj) + db.commit() + db.refresh(db_obj) + + # Log changes in journal + for change in changes: + crud_journal.log_change(db, db_obj.id, change, user, user_ip) - db.add(db_obj) - db.commit() - db.refresh(db_obj) return db_obj def update_status( self, db: Session, ppr_id: int, - status: PPRStatus + status: PPRStatus, + user: str = "system", + user_ip: str = "127.0.0.1" ) -> Optional[PPRRecord]: db_obj = self.get(db, ppr_id) if not db_obj: return None + old_status = db_obj.status db_obj.status = status # Set timestamps based on status @@ -104,16 +129,36 @@ class CRUDPPR: db.add(db_obj) db.commit() db.refresh(db_obj) + + # Log status change in journal + crud_journal.log_change( + db, + db_obj.id, + f"Status changed from {old_status.value} to {status.value}", + user, + user_ip + ) + return db_obj - def delete(self, db: Session, ppr_id: int) -> Optional[PPRRecord]: + def delete(self, db: Session, ppr_id: int, user: str = "system", user_ip: str = "127.0.0.1") -> Optional[PPRRecord]: db_obj = self.get(db, ppr_id) if db_obj: + old_status = db_obj.status # Soft delete by setting status db_obj.status = PPRStatus.DELETED db.add(db_obj) db.commit() db.refresh(db_obj) + + # Log the deletion in journal + crud_journal.log_change( + db, + db_obj.id, + f"PPR marked as DELETED (was {old_status.value})", + user, + user_ip + ) return db_obj diff --git a/web/admin.html b/web/admin.html new file mode 100644 index 0000000..e4c1188 --- /dev/null +++ b/web/admin.html @@ -0,0 +1,1078 @@ + + + + + + PPR Admin Interface + + + +
+

✈️ PPR Administration

+ +
+
+ +
+
+ + +
+ + +
+ + + +
+ +
+
+ + +
+
+ 🛬 Today's Arrivals - 0 entries (NEW & CONFIRMED) +
+ +
+
+ Loading arrivals... +
+ + + + +
+ + +
+
+ 🛫 Today's Departures - 0 entries (LANDED) +
+ +
+
+ Loading departures... +
+ + + + +
+
+ + + + + + + \ No newline at end of file