bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
222 lines
8.3 KiB
Python
222 lines
8.3 KiB
Python
"""
|
|
Admin Dashboard
|
|
Web-based admin interface for IOM platform
|
|
"""
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import HTMLResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.templating import Jinja2Templates
|
|
import json
|
|
|
|
app = FastAPI(title="IOM Admin Dashboard")
|
|
|
|
# Templates
|
|
templates = Jinja2Templates(directory="admin/templates")
|
|
|
|
# Mock data for dashboard
|
|
DASHBOARD_DATA = {
|
|
"stats": {
|
|
"total_observations": 15420,
|
|
"total_objects": 8934,
|
|
"active_missions": 234,
|
|
"pending_reviews": 89,
|
|
"avg_rgi": 34.5,
|
|
"trend": "improving"
|
|
},
|
|
"recent_observations": [
|
|
{"id": "OBS-001", "location": "Stockholm City", "condition": 3, "timestamp": "2026-06-26T10:00:00Z"},
|
|
{"id": "OBS-002", "location": "Gothenburg Port", "condition": 4, "timestamp": "2026-06-26T09:30:00Z"},
|
|
{"id": "OBS-003", "location": "Malmö Bridge", "condition": 2, "timestamp": "2026-06-26T09:00:00Z"}
|
|
],
|
|
"alerts": [
|
|
{"type": "critical", "message": "Bridge condition degraded to 4", "location": "Malmö Bridge"},
|
|
{"type": "warning", "message": "High RGI in district 3", "location": "Stockholm"},
|
|
{"type": "info", "message": "New zoomer registered", "location": "Gothenburg"}
|
|
],
|
|
"top_issues": [
|
|
{"issue": "Damaged sidewalks", "count": 234, "trend": "up"},
|
|
{"issue": "Broken lighting", "count": 189, "trend": "down"},
|
|
{"issue": "Graffiti", "count": 156, "trend": "stable"}
|
|
]
|
|
}
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def dashboard(request: Request):
|
|
"""Main dashboard"""
|
|
return templates.TemplateResponse("dashboard.html", {
|
|
"request": request,
|
|
"data": DASHBOARD_DATA
|
|
})
|
|
|
|
|
|
@app.get("/api/stats")
|
|
async def get_stats():
|
|
"""Get dashboard stats"""
|
|
return DASHBOARD_DATA["stats"]
|
|
|
|
|
|
@app.get("/api/observations")
|
|
async def get_observations():
|
|
"""Get recent observations"""
|
|
return DASHBOARD_DATA["recent_observations"]
|
|
|
|
|
|
@app.get("/api/alerts")
|
|
async def get_alerts():
|
|
"""Get active alerts"""
|
|
return DASHBOARD_DATA["alerts"]
|
|
|
|
|
|
# HTML Template
|
|
def create_dashboard_template():
|
|
"""Create dashboard HTML template"""
|
|
html = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>IOM Admin Dashboard</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
|
.header { background: #1a1a2e; color: white; padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; }
|
|
.header h1 { font-size: 1.5rem; }
|
|
.nav { display: flex; gap: 2rem; }
|
|
.nav a { color: white; text-decoration: none; opacity: 0.8; }
|
|
.nav a:hover { opacity: 1; }
|
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
|
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
|
|
.stat-card { background: white; padding: 1.5rem; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
.stat-card h3 { font-size: 0.875rem; color: #666; margin-bottom: 0.5rem; }
|
|
.stat-card .value { font-size: 2rem; font-weight: bold; color: #1a1a2e; }
|
|
.stat-card .trend { font-size: 0.875rem; margin-top: 0.5rem; }
|
|
.trend.up { color: #e74c3c; }
|
|
.trend.down { color: #2ecc71; }
|
|
.trend.stable { color: #f39c12; }
|
|
.section { background: white; padding: 1.5rem; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 1.5rem; }
|
|
.section h2 { font-size: 1.25rem; margin-bottom: 1rem; color: #1a1a2e; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th, td { text-align: left; padding: 0.75rem; border-bottom: 1px solid #eee; }
|
|
th { font-weight: 600; color: #666; font-size: 0.875rem; }
|
|
.badge { padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
|
|
.badge-critical { background: #fee; color: #c33; }
|
|
.badge-warning { background: #ffeaa7; color: #b7791f; }
|
|
.badge-info { background: #e3f2fd; color: #1976d2; }
|
|
.badge-condition-1 { background: #e8f5e9; color: #2e7d32; }
|
|
.badge-condition-2 { background: #fff3e0; color: #ef6c00; }
|
|
.badge-condition-3 { background: #fce4ec; color: #c2185b; }
|
|
.badge-condition-4 { background: #f3e5f5; color: #7b1fa2; }
|
|
.badge-condition-5 { background: #ffebee; color: #c62828; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>🏗️ IOM Admin Dashboard</h1>
|
|
<nav class="nav">
|
|
<a href="/">Dashboard</a>
|
|
<a href="/observations">Observations</a>
|
|
<a href="/objects">Objects</a>
|
|
<a href="/analytics">Analytics</a>
|
|
<a href="/settings">Settings</a>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<h3>Total Observations</h3>
|
|
<div class="value">{{ data.stats.total_observations }}</div>
|
|
<div class="trend {{ data.stats.trend }}">↗ {{ data.stats.trend }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Total Objects</h3>
|
|
<div class="value">{{ data.stats.total_objects }}</div>
|
|
<div class="trend stable">→ stable</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Active Missions</h3>
|
|
<div class="value">{{ data.stats.active_missions }}</div>
|
|
<div class="trend up">↗ increasing</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Pending Reviews</h3>
|
|
<div class="value">{{ data.stats.pending_reviews }}</div>
|
|
<div class="trend down">↘ decreasing</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Average RGI</h3>
|
|
<div class="value">{{ data.stats.avg_rgi }}</div>
|
|
<div class="trend down">↘ improving</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>🚨 Active Alerts</h2>
|
|
<table>
|
|
<thead>
|
|
<tr><th>Type</th><th>Message</th><th>Location</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for alert in data.alerts %}
|
|
<tr>
|
|
<td><span class="badge badge-{{ alert.type }}">{{ alert.type }}</span></td>
|
|
<td>{{ alert.message }}</td>
|
|
<td>{{ alert.location }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>📊 Recent Observations</h2>
|
|
<table>
|
|
<thead>
|
|
<tr><th>ID</th><th>Location</th><th>Condition</th><th>Time</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for obs in data.recent_observations %}
|
|
<tr>
|
|
<td>{{ obs.id }}</td>
|
|
<td>{{ obs.location }}</td>
|
|
<td><span class="badge badge-condition-{{ obs.condition }}">Condition {{ obs.condition }}</span></td>
|
|
<td>{{ obs.timestamp }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>⚠️ Top Issues</h2>
|
|
<table>
|
|
<thead>
|
|
<tr><th>Issue</th><th>Count</th><th>Trend</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for issue in data.top_issues %}
|
|
<tr>
|
|
<td>{{ issue.issue }}</td>
|
|
<td>{{ issue.count }}</td>
|
|
<td><span class="trend {{ issue.trend }}">{{ issue.trend }}</span></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
import os
|
|
os.makedirs("admin/templates", exist_ok=True)
|
|
with open("admin/templates/dashboard.html", "w") as f:
|
|
f.write(html)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
create_dashboard_template()
|
|
print("Dashboard template created")
|