Creating admin interface

This commit is contained in:
James Pattinson
2025-10-21 20:23:58 +00:00
parent f580d0fbf7
commit 28af669993
6 changed files with 1276 additions and 19 deletions

View File

@@ -216,10 +216,67 @@ The API sends real-time updates via WebSocket for:
- **Username:** `admin` - **Username:** `admin`
- **Password:** `admin123` - **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 ## Development URLs
- API Base: http://localhost:8001/api/v1 - API Base: http://localhost:8001/api/v1
- Public Web Interface: http://localhost:8082 - Public Web Interface: http://localhost:8082
- Admin Interface: http://localhost:8082/admin.html
- API Documentation: http://localhost:8001/docs - API Documentation: http://localhost:8001/docs
- Database: localhost:3307 (MySQL) - Database: localhost:3307 (MySQL)

View File

@@ -4,8 +4,10 @@ from sqlalchemy.orm import Session
from datetime import date from datetime import date
from app.api.deps import get_db, get_current_active_user from app.api.deps import get_db, get_current_active_user
from app.crud.crud_ppr import ppr as crud_ppr 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.models.ppr import User
from app.core.utils import get_client_ip
router = APIRouter() router = APIRouter()
@@ -37,7 +39,8 @@ async def create_ppr(
current_user: User = Depends(get_current_active_user) current_user: User = Depends(get_current_active_user)
): ):
"""Create a new PPR record""" """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 # Send real-time update via WebSocket
if hasattr(request.app.state, 'connection_manager'): if hasattr(request.app.state, 'connection_manager'):
@@ -85,7 +88,8 @@ async def update_ppr(
detail="PPR record not found" 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 # Send real-time update
if hasattr(request.app.state, 'connection_manager'): if hasattr(request.app.state, 'connection_manager'):
@@ -117,8 +121,8 @@ async def patch_ppr(
detail="PPR record not found" detail="PPR record not found"
) )
# For PATCH, we only update fields that are explicitly provided (not None) client_ip = get_client_ip(request)
ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in) 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 # Send real-time update
if hasattr(request.app.state, 'connection_manager'): if hasattr(request.app.state, 'connection_manager'):
@@ -143,15 +147,20 @@ async def update_ppr_status(
current_user: User = Depends(get_current_active_user) current_user: User = Depends(get_current_active_user)
): ):
"""Update PPR status (LANDED, DEPARTED, etc.)""" """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: if not ppr:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="PPR record not found" detail="PPR record not found"
) )
# Log the status change (you might want to create a journal entry here)
# Send real-time update # Send real-time update
if hasattr(request.app.state, 'connection_manager'): if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({ await request.app.state.connection_manager.broadcast({
@@ -175,7 +184,8 @@ async def delete_ppr(
current_user: User = Depends(get_current_active_user) current_user: User = Depends(get_current_active_user)
): ):
"""Delete (soft delete) a PPR record""" """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: if not ppr:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@@ -193,3 +203,21 @@ async def delete_ppr(
}) })
return ppr 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)

14
backend/app/core/utils.py Normal file
View File

@@ -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"

View File

@@ -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()

View File

@@ -4,6 +4,7 @@ from sqlalchemy import and_, or_, func, desc
from datetime import date, datetime from datetime import date, datetime
from app.models.ppr import PPRRecord, PPRStatus from app.models.ppr import PPRRecord, PPRStatus
from app.schemas.ppr import PPRCreate, PPRUpdate from app.schemas.ppr import PPRCreate, PPRUpdate
from app.crud.crud_journal import journal as crud_journal
class CRUDPPR: class CRUDPPR:
@@ -62,7 +63,7 @@ class CRUDPPR:
) )
).order_by(PPRRecord.etd).all() ).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( db_obj = PPRRecord(
**obj_in.dict(), **obj_in.dict(),
created_by=created_by, created_by=created_by,
@@ -71,28 +72,52 @@ class CRUDPPR:
db.add(db_obj) db.add(db_obj)
db.commit() db.commit()
db.refresh(db_obj) 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 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) update_data = obj_in.dict(exclude_unset=True)
for field, value in update_data.items(): changes = []
setattr(db_obj, field, value)
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 return db_obj
def update_status( def update_status(
self, self,
db: Session, db: Session,
ppr_id: int, ppr_id: int,
status: PPRStatus status: PPRStatus,
user: str = "system",
user_ip: str = "127.0.0.1"
) -> Optional[PPRRecord]: ) -> Optional[PPRRecord]:
db_obj = self.get(db, ppr_id) db_obj = self.get(db, ppr_id)
if not db_obj: if not db_obj:
return None return None
old_status = db_obj.status
db_obj.status = status db_obj.status = status
# Set timestamps based on status # Set timestamps based on status
@@ -104,16 +129,36 @@ class CRUDPPR:
db.add(db_obj) db.add(db_obj)
db.commit() db.commit()
db.refresh(db_obj) 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 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) db_obj = self.get(db, ppr_id)
if db_obj: if db_obj:
old_status = db_obj.status
# Soft delete by setting status # Soft delete by setting status
db_obj.status = PPRStatus.DELETED db_obj.status = PPRStatus.DELETED
db.add(db_obj) db.add(db_obj)
db.commit() db.commit()
db.refresh(db_obj) 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 return db_obj

1078
web/admin.html Normal file

File diff suppressed because it is too large Load Diff