diff --git a/backend/app/api/endpoints/auth.py b/backend/app/api/endpoints/auth.py
index f736778..12ab494 100644
--- a/backend/app/api/endpoints/auth.py
+++ b/backend/app/api/endpoints/auth.py
@@ -33,7 +33,11 @@ async def login_for_access_token(
subject=user.username, expires_delta=access_token_expires
)
- return {"access_token": access_token, "token_type": "bearer"}
+ return {
+ "access_token": access_token,
+ "token_type": "bearer",
+ "expires_in": settings.access_token_expire_minutes * 60 # seconds
+ }
@router.post("/test-token", response_model=User)
diff --git a/backend/app/schemas/ppr.py b/backend/app/schemas/ppr.py
index f04dbc6..b699778 100644
--- a/backend/app/schemas/ppr.py
+++ b/backend/app/schemas/ppr.py
@@ -154,6 +154,7 @@ class UserInDB(UserInDBBase):
class Token(BaseModel):
access_token: str
token_type: str
+ expires_in: int # Token expiry in seconds
class TokenData(BaseModel):
diff --git a/web/admin.html b/web/admin.html
index 4d68f2e..e4d5d13 100644
--- a/web/admin.html
+++ b/web/admin.html
@@ -653,6 +653,72 @@
No aircraft currently landed and ready to depart.
+
+
+
+
+
+
+
+
+
+ Loading departed aircraft...
+
+
+
+
+
+
+ | Registration |
+ Callsign |
+ Destination |
+ Departed |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading parked visitors...
+
+
+
+
+
+
+ | Registration |
+ Type |
+ From |
+ Arrived |
+ ETD |
+
+
+
+
+
+
+
+
+
+
@@ -1226,8 +1292,9 @@
const data = await response.json();
if (response.ok && data.access_token) {
- // Store token and user info with expiry (30 minutes from now)
- const expiryTime = new Date().getTime() + (30 * 60 * 1000); // 30 minutes
+ // Store token and user info with expiry from server response
+ const expiresInMs = (data.expires_in || 1800) * 1000; // Use server value or default to 30 min
+ const expiryTime = new Date().getTime() + expiresInMs;
localStorage.setItem('ppr_access_token', data.access_token);
localStorage.setItem('ppr_username', username);
@@ -1308,12 +1375,12 @@
return response;
}
- // Load PPR records - now loads both arrivals and departures
+ // Load PPR records - now loads all tables
async function loadPPRs() {
if (!accessToken) return;
- // Load both arrivals and departures simultaneously
- await Promise.all([loadArrivals(), loadDepartures()]);
+ // Load all tables simultaneously
+ await Promise.all([loadArrivals(), loadDepartures(), loadDeparted(), loadParked()]);
}
// Load arrivals (NEW and CONFIRMED status)
@@ -1394,6 +1461,171 @@
document.getElementById('departures-loading').style.display = 'none';
}
+ // Load departed aircraft (DEPARTED status with departed_dt today)
+ async function loadDeparted() {
+ document.getElementById('departed-loading').style.display = 'block';
+ document.getElementById('departed-table-content').style.display = 'none';
+ document.getElementById('departed-no-data').style.display = 'none';
+
+ try {
+ const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch departed aircraft');
+ }
+
+ const allPPRs = await response.json();
+ const today = new Date().toISOString().split('T')[0];
+
+ // Filter for aircraft departed today
+ const departed = allPPRs.filter(ppr => {
+ if (!ppr.departed_dt || ppr.status !== 'DEPARTED') {
+ return false;
+ }
+ const departedDate = ppr.departed_dt.split('T')[0];
+ return departedDate === today;
+ });
+
+ displayDeparted(departed);
+ } catch (error) {
+ console.error('Error loading departed aircraft:', error);
+ if (error.message !== 'Session expired. Please log in again.') {
+ showNotification('Error loading departed aircraft', true);
+ }
+ }
+
+ document.getElementById('departed-loading').style.display = 'none';
+ }
+
+ function displayDeparted(departed) {
+ const tbody = document.getElementById('departed-table-body');
+ document.getElementById('departed-count').textContent = departed.length;
+
+ if (departed.length === 0) {
+ document.getElementById('departed-no-data').style.display = 'block';
+ return;
+ }
+
+ // Sort by departed time
+ departed.sort((a, b) => new Date(a.departed_dt) - new Date(b.departed_dt));
+
+ tbody.innerHTML = '';
+ document.getElementById('departed-table-content').style.display = 'block';
+
+ for (const ppr of departed) {
+ const row = document.createElement('tr');
+ row.onclick = () => openPPRModal(ppr.id);
+ row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;';
+
+ row.innerHTML = `
+ ${ppr.ac_reg || '-'} |
+ ${ppr.ac_call || '-'} |
+ ${ppr.out_to || '-'} |
+ ${formatTimeOnly(ppr.departed_dt)} |
+ `;
+ tbody.appendChild(row);
+ }
+ }
+
+ // Load parked visitors (LANDED status with no ETD today or ETD not today)
+ async function loadParked() {
+ document.getElementById('parked-loading').style.display = 'block';
+ document.getElementById('parked-table-content').style.display = 'none';
+ document.getElementById('parked-no-data').style.display = 'none';
+
+ try {
+ const response = await authenticatedFetch('/api/v1/pprs/?limit=1000');
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch parked visitors');
+ }
+
+ const allPPRs = await response.json();
+ const today = new Date().toISOString().split('T')[0];
+
+ // Filter for parked visitors: LANDED status and (no ETD or ETD not today)
+ // Show all parked aircraft regardless of when they arrived
+ const parked = allPPRs.filter(ppr => {
+ if (ppr.status !== 'LANDED') {
+ return false;
+ }
+ // No ETD means parked
+ if (!ppr.etd) {
+ return true;
+ }
+ // ETD exists but is not today
+ const etdDate = ppr.etd.split('T')[0];
+ return etdDate !== today;
+ });
+
+ displayParked(parked);
+ } catch (error) {
+ console.error('Error loading parked visitors:', error);
+ if (error.message !== 'Session expired. Please log in again.') {
+ showNotification('Error loading parked visitors', true);
+ }
+ }
+
+ document.getElementById('parked-loading').style.display = 'none';
+ }
+
+ function displayParked(parked) {
+ const tbody = document.getElementById('parked-table-body');
+ document.getElementById('parked-count').textContent = parked.length;
+
+ if (parked.length === 0) {
+ document.getElementById('parked-no-data').style.display = 'block';
+ return;
+ }
+
+ // Sort by landed time
+ parked.sort((a, b) => {
+ if (!a.landed_dt) return 1;
+ if (!b.landed_dt) return -1;
+ return new Date(a.landed_dt) - new Date(b.landed_dt);
+ });
+
+ tbody.innerHTML = '';
+ document.getElementById('parked-table-content').style.display = 'block';
+
+ for (const ppr of parked) {
+ const row = document.createElement('tr');
+ row.onclick = () => openPPRModal(ppr.id);
+ row.style.cssText = 'font-size: 0.85rem !important; font-style: italic;';
+
+ // Format arrival: time if today, date if not
+ const today = new Date().toISOString().split('T')[0];
+ let arrivedDisplay = '-';
+ if (ppr.landed_dt) {
+ const landedDate = ppr.landed_dt.split('T')[0];
+ if (landedDate === today) {
+ // Today - show time only
+ arrivedDisplay = formatTimeOnly(ppr.landed_dt);
+ } else {
+ // Not today - show date (DD/MM)
+ const date = new Date(ppr.landed_dt);
+ arrivedDisplay = date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
+ }
+ }
+
+ // Format ETD as just the date (DD/MM)
+ let etdDisplay = '-';
+ if (ppr.etd) {
+ const etdDate = new Date(ppr.etd);
+ etdDisplay = etdDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit' });
+ }
+
+ row.innerHTML = `
+ ${ppr.ac_reg || '-'} |
+ ${ppr.ac_type || '-'} |
+ ${ppr.in_from || '-'} |
+ ${arrivedDisplay} |
+ ${etdDisplay} |
+ `;
+ tbody.appendChild(row);
+ }
+ }
+
// ICAO code to airport name cache
const airportNameCache = {};