aee0f09db8
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
499 lines
22 KiB
Python
499 lines
22 KiB
Python
"""
|
|
System Administration Router för LandveX Admin Backend.
|
|
Hanterar användare/roller, tenant-hantering, audit log, inställningar,
|
|
CloudFront/S3, backup och återställning.
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from typing import List, Optional, Dict, Any
|
|
from datetime import datetime, date
|
|
|
|
from app.database import get_db
|
|
from app.dependencies import get_current_user, require_admin, require_manager
|
|
from app.models import User, UserRole
|
|
|
|
router = APIRouter(prefix="/system", tags=["system"])
|
|
|
|
|
|
# ─── Users & Roles ────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/users")
|
|
async def list_system_users(
|
|
role: Optional[str] = Query(None),
|
|
status: Optional[str] = Query(None),
|
|
tenant_id: Optional[str] = Query(None),
|
|
search: Optional[str] = Query(None),
|
|
limit: int = Query(50, ge=1, le=500),
|
|
offset: int = Query(0, ge=0),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Lista alla systemanvändare med filtrering."""
|
|
users = [
|
|
{"id": "usr-001", "email": "admin@landvex.com", "first_name": "Admin", "last_name": "User", "role": "admin", "status": "active", "tenant_id": None, "is_superuser": True, "last_login": "2026-07-04T06:00:00Z", "created_at": "2024-01-01T00:00:00Z"},
|
|
{"id": "usr-002", "email": "erik@landvex.com", "first_name": "Erik", "last_name": "Svensson", "role": "admin", "status": "active", "tenant_id": "tenant-001", "is_superuser": False, "last_login": "2026-07-03T18:00:00Z", "created_at": "2024-01-01T00:00:00Z"},
|
|
{"id": "usr-003", "email": "johan@landvex.com", "first_name": "Johan", "last_name": "Berglund", "role": "manager", "status": "active", "tenant_id": "tenant-001", "is_superuser": False, "last_login": "2026-07-04T07:00:00Z", "created_at": "2024-06-01T00:00:00Z"},
|
|
{"id": "usr-004", "email": "analyst@landvex.com", "first_name": "Analyst", "last_name": "One", "role": "analyst", "status": "active", "tenant_id": "tenant-001", "is_superuser": False, "last_login": "2026-07-01T09:00:00Z", "created_at": "2024-03-01T00:00:00Z"},
|
|
{"id": "usr-005", "email": "viewer@landvex.com", "first_name": "Viewer", "last_name": "One", "role": "viewer", "status": "inactive", "tenant_id": "tenant-001", "is_superuser": False, "last_login": None, "created_at": "2024-03-01T00:00:00Z"},
|
|
]
|
|
if role:
|
|
users = [u for u in users if u["role"] == role]
|
|
if status:
|
|
users = [u for u in users if u["status"] == status]
|
|
if tenant_id:
|
|
users = [u for u in users if u["tenant_id"] == tenant_id]
|
|
if search:
|
|
users = [u for u in users if search.lower() in u["email"].lower() or search.lower() in u["first_name"].lower() or search.lower() in u["last_name"].lower()]
|
|
return {"users": users[offset:offset+limit], "total": len(users), "limit": limit, "offset": offset}
|
|
|
|
|
|
@router.get("/users/{user_id}")
|
|
async def get_system_user(
|
|
user_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Hämta en specifik användare."""
|
|
return {
|
|
"id": user_id,
|
|
"email": "admin@landvex.com",
|
|
"first_name": "Admin",
|
|
"last_name": "User",
|
|
"role": "admin",
|
|
"status": "active",
|
|
"tenant_id": None,
|
|
"is_superuser": True,
|
|
"permissions": ["*"],
|
|
"last_login": "2026-07-04T06:00:00Z",
|
|
"login_history": [
|
|
{"timestamp": "2026-07-04T06:00:00Z", "ip": "192.168.1.1", "user_agent": "Mozilla/5.0"},
|
|
{"timestamp": "2026-07-03T18:00:00Z", "ip": "192.168.1.1", "user_agent": "Mozilla/5.0"},
|
|
],
|
|
"created_at": "2024-01-01T00:00:00Z",
|
|
"updated_at": "2026-07-04T06:00:00Z",
|
|
}
|
|
|
|
|
|
@router.patch("/users/{user_id}/role")
|
|
async def update_user_role(
|
|
user_id: str,
|
|
role: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Uppdatera en användares roll."""
|
|
return {
|
|
"status": "updated",
|
|
"user_id": user_id,
|
|
"new_role": role,
|
|
"updated_by": str(current_user.id),
|
|
"updated_at": datetime.now().isoformat(),
|
|
}
|
|
|
|
|
|
@router.patch("/users/{user_id}/status")
|
|
async def update_user_status(
|
|
user_id: str,
|
|
status: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Uppdatera en användares status (active/inactive/pending)."""
|
|
return {
|
|
"status": "updated",
|
|
"user_id": user_id,
|
|
"new_status": status,
|
|
"updated_by": str(current_user.id),
|
|
"updated_at": datetime.now().isoformat(),
|
|
}
|
|
|
|
|
|
# ─── Roles & Permissions ──────────────────────────────────────────────────────
|
|
|
|
@router.get("/roles")
|
|
async def list_roles(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Lista alla roller och deras behörigheter."""
|
|
return {
|
|
"roles": [
|
|
{
|
|
"name": "admin",
|
|
"description": "Fullständig åtkomst till alla moduler och inställningar",
|
|
"permissions": [
|
|
"users:*", "tenants:*", "content:*", "quixzoom:*",
|
|
"economy:*", "hr:*", "system:*", "audit:*", "settings:*"
|
|
],
|
|
"user_count": 2,
|
|
},
|
|
{
|
|
"name": "manager",
|
|
"description": "Hantera innehåll, missions och team-medlemmar inom sin tenant",
|
|
"permissions": [
|
|
"content:read", "content:write", "quixzoom:*",
|
|
"economy:read", "hr:read", "hr:write", "audit:read"
|
|
],
|
|
"user_count": 1,
|
|
},
|
|
{
|
|
"name": "analyst",
|
|
"description": "Läsa och analysera data, skapa rapporter",
|
|
"permissions": [
|
|
"content:read", "quixzoom:read", "economy:read",
|
|
"hr:read", "audit:read"
|
|
],
|
|
"user_count": 1,
|
|
},
|
|
{
|
|
"name": "viewer",
|
|
"description": "Endast läsåtkomst till dashboards och rapporter",
|
|
"permissions": [
|
|
"content:read", "quixzoom:read", "economy:read", "hr:read"
|
|
],
|
|
"user_count": 1,
|
|
},
|
|
]
|
|
}
|
|
|
|
|
|
# ─── Audit Log ────────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/audit-log")
|
|
async def get_system_audit_log(
|
|
action: Optional[str] = Query(None),
|
|
entity_type: Optional[str] = Query(None),
|
|
user_id: Optional[str] = Query(None),
|
|
from_date: Optional[date] = Query(None),
|
|
to_date: Optional[date] = Query(None),
|
|
limit: int = Query(50, ge=1, le=500),
|
|
offset: int = Query(0, ge=0),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_manager),
|
|
):
|
|
"""Hämta system audit log med filtrering."""
|
|
logs = [
|
|
{"id": "log-001", "timestamp": "2026-07-04T06:30:00Z", "action": "UPDATE", "entity_type": "article", "entity_id": "decision-first-intelligence", "actor_id": "usr-002", "actor_email": "erik@landvex.com", "actor_ip": "192.168.1.1", "details": "Updated article content", "success": True},
|
|
{"id": "log-002", "timestamp": "2026-07-04T06:15:00Z", "action": "CREATE", "entity_type": "mission", "entity_id": "mission-003", "actor_id": "usr-003", "actor_email": "johan@landvex.com", "actor_ip": "192.168.1.2", "details": "Created mission: Oslo Road Condition", "success": True},
|
|
{"id": "log-003", "timestamp": "2026-07-04T06:00:00Z", "action": "LOGIN", "entity_type": "user", "entity_id": "usr-002", "actor_id": "usr-002", "actor_email": "erik@landvex.com", "actor_ip": "192.168.1.1", "details": "User login", "success": True},
|
|
{"id": "log-004", "timestamp": "2026-07-03T18:00:00Z", "action": "DELETE", "entity_type": "article", "entity_id": "old-draft-001", "actor_id": "usr-002", "actor_email": "erik@landvex.com", "actor_ip": "192.168.1.1", "details": "Deleted draft article", "success": True},
|
|
{"id": "log-005", "timestamp": "2026-07-03T15:30:00Z", "action": "UPDATE", "entity_type": "user", "entity_id": "usr-005", "actor_id": "usr-002", "actor_email": "erik@landvex.com", "actor_ip": "192.168.1.1", "details": "Changed user status to inactive", "success": True},
|
|
{"id": "log-006", "timestamp": "2026-07-03T12:00:00Z", "action": "EXPORT", "entity_type": "report", "entity_id": "payroll-2026-06", "actor_id": "usr-003", "actor_email": "johan@landvex.com", "actor_ip": "192.168.1.2", "details": "Exported payroll report", "success": True},
|
|
{"id": "log-007", "timestamp": "2026-07-03T09:00:00Z", "action": "CREATE", "entity_type": "transaction", "entity_id": "tx-001", "actor_id": "usr-002", "actor_email": "erik@landvex.com", "actor_ip": "192.168.1.1", "details": "Created journal entry", "success": True},
|
|
{"id": "log-008", "timestamp": "2026-07-02T16:00:00Z", "action": "UPDATE", "entity_type": "tenant", "entity_id": "tenant-001", "actor_id": "usr-001", "actor_email": "admin@landvex.com", "actor_ip": "10.0.0.1", "details": "Updated tenant settings", "success": True},
|
|
{"id": "log-009", "timestamp": "2026-07-02T10:00:00Z", "action": "INVALIDATE", "entity_type": "cloudfront", "entity_id": "E2M3J95HLUR89H", "actor_id": "system", "actor_email": "system", "actor_ip": None, "details": "Auto-invalidated cache", "success": True},
|
|
{"id": "log-010", "timestamp": "2026-07-01T08:00:00Z", "action": "BACKUP", "entity_type": "database", "entity_id": "landvex-admin", "actor_id": "system", "actor_email": "system", "actor_ip": None, "details": "Daily backup completed", "success": True},
|
|
]
|
|
if action:
|
|
logs = [l for l in logs if l["action"] == action]
|
|
if entity_type:
|
|
logs = [l for l in logs if l["entity_type"] == entity_type]
|
|
if user_id:
|
|
logs = [l for l in logs if l["actor_id"] == user_id]
|
|
return {"logs": logs[offset:offset+limit], "total": len(logs), "limit": limit, "offset": offset}
|
|
|
|
|
|
# ─── Settings ─────────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/settings")
|
|
async def get_all_settings(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Hämta alla systeminställningar."""
|
|
return {
|
|
"general": {
|
|
"company_name": "Landvex AB",
|
|
"org_number": "559123-4567",
|
|
"address": "Storgatan 1, 111 22 Stockholm",
|
|
"phone": "+46 8 123 45 67",
|
|
"email": "info@landvex.com",
|
|
"website": "https://landvex.com",
|
|
"currency": "SEK",
|
|
"language": "sv",
|
|
"timezone": "Europe/Stockholm",
|
|
},
|
|
"content": {
|
|
"auto_publish": False,
|
|
"publish_schedule": "manual",
|
|
"seo_auto_optimize": True,
|
|
"default_author": "Landvex Team",
|
|
"featured_image_required": False,
|
|
},
|
|
"quixzoom": {
|
|
"mission_auto_approve": False,
|
|
"min_quality_score": 85,
|
|
"payment_threshold": 100,
|
|
"max_mission_duration_days": 14,
|
|
"contributor_onboarding_required": True,
|
|
},
|
|
"economy": {
|
|
"ledger_sync_interval": "hourly",
|
|
"default_vat_rate": 25,
|
|
"fiscal_year_start": "01-01",
|
|
"invoice_prefix": "INV",
|
|
"payment_terms_days": 30,
|
|
},
|
|
"hr": {
|
|
"vacation_days_per_year": 25,
|
|
"sick_leave_notification_days": 1,
|
|
"work_hours_per_day": 8,
|
|
"overtime_threshold": 40,
|
|
},
|
|
"notifications": {
|
|
"email_enabled": True,
|
|
"slack_enabled": False,
|
|
"slack_webhook_url": None,
|
|
"alert_on_system_error": True,
|
|
"alert_on_low_balance": True,
|
|
"low_balance_threshold": 50000,
|
|
},
|
|
"security": {
|
|
"mfa_required": False,
|
|
"password_min_length": 8,
|
|
"password_expiry_days": 90,
|
|
"session_timeout_minutes": 60,
|
|
"max_login_attempts": 5,
|
|
"lockout_duration_minutes": 30,
|
|
},
|
|
}
|
|
|
|
|
|
@router.put("/settings/{module}")
|
|
async def update_module_settings(
|
|
module: str,
|
|
settings: Dict[str, Any],
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Uppdatera inställningar för en specifik modul."""
|
|
valid_modules = ["general", "content", "quixzoom", "economy", "hr", "notifications", "security"]
|
|
if module not in valid_modules:
|
|
raise HTTPException(status_code=400, detail=f"Invalid module. Must be one of: {', '.join(valid_modules)}")
|
|
return {
|
|
"status": "updated",
|
|
"module": module,
|
|
"settings": settings,
|
|
"updated_by": str(current_user.id),
|
|
"updated_at": datetime.now().isoformat(),
|
|
}
|
|
|
|
|
|
# ─── CloudFront / S3 ──────────────────────────────────────────────────────────
|
|
|
|
@router.get("/cloudfront")
|
|
async def get_cloudfront_distributions(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Hämta CloudFront distributions."""
|
|
return {
|
|
"distributions": [
|
|
{
|
|
"id": "E2M3J95HLUR89H",
|
|
"domain": "d1234.cloudfront.net",
|
|
"status": "Deployed",
|
|
"enabled": True,
|
|
"origin": "landvex-prod.s3.eu-north-1.amazonaws.com",
|
|
"ssl_certificate": "landvex.com",
|
|
"price_class": "PriceClass_100",
|
|
"last_modified": "2026-07-01T00:00:00Z",
|
|
"invalidations": {"pending": 0, "completed": 45},
|
|
},
|
|
{
|
|
"id": "EE30B9WM5ZYM7",
|
|
"domain": "d5678.cloudfront.net",
|
|
"status": "Deployed",
|
|
"enabled": True,
|
|
"origin": "quixzoom-landing-prod.s3.eu-north-1.amazonaws.com",
|
|
"ssl_certificate": "quixzoom.com",
|
|
"price_class": "PriceClass_100",
|
|
"last_modified": "2026-06-15T00:00:00Z",
|
|
"invalidations": {"pending": 0, "completed": 12},
|
|
},
|
|
]
|
|
}
|
|
|
|
|
|
@router.get("/cloudfront/{distribution_id}")
|
|
async def get_cloudfront_distribution(
|
|
distribution_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Hämta detaljer för en CloudFront distribution."""
|
|
return {
|
|
"id": distribution_id,
|
|
"domain": "d1234.cloudfront.net",
|
|
"status": "Deployed",
|
|
"enabled": True,
|
|
"origin": "landvex-prod.s3.eu-north-1.amazonaws.com",
|
|
"ssl_certificate": "landvex.com",
|
|
"price_class": "PriceClass_100",
|
|
"cache_behaviors": [
|
|
{"path_pattern": "*.html", "ttl": 86400, "compress": True},
|
|
{"path_pattern": "*.css", "ttl": 604800, "compress": True},
|
|
{"path_pattern": "*.js", "ttl": 604800, "compress": True},
|
|
{"path_pattern": "*.jpg", "ttl": 2592000, "compress": False},
|
|
],
|
|
"functions": [
|
|
{"name": "landvex-handler", "status": "active", "purpose": "www-redirect + directory index"},
|
|
],
|
|
"invalidations": [
|
|
{"id": "I123456", "status": "Completed", "paths": ["/insights/*"], "created_at": "2026-07-04T06:15:00Z"},
|
|
{"id": "I123455", "status": "Completed", "paths": ["/"], "created_at": "2026-07-03T18:00:00Z"},
|
|
],
|
|
}
|
|
|
|
|
|
@router.post("/cloudfront/{distribution_id}/invalidate")
|
|
async def invalidate_cloudfront(
|
|
distribution_id: str,
|
|
paths: List[str],
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Invalidera CloudFront cache."""
|
|
return {
|
|
"status": "invalidated",
|
|
"distribution_id": distribution_id,
|
|
"paths": paths,
|
|
"invalidation_id": f"I{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
|
"created_at": datetime.now().isoformat(),
|
|
"created_by": str(current_user.id),
|
|
}
|
|
|
|
|
|
@router.get("/s3/buckets")
|
|
async def get_s3_buckets(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Hämta S3 buckets."""
|
|
return {
|
|
"buckets": [
|
|
{"name": "landvex-prod", "region": "eu-north-1", "objects": 1250, "size_gb": 2.3, "versioning": "Enabled", "encryption": "AES256"},
|
|
{"name": "quixzoom-landing-prod", "region": "eu-north-1", "objects": 340, "size_gb": 0.8, "versioning": "Enabled", "encryption": "AES256"},
|
|
{"name": "landvex-backups", "region": "eu-north-1", "objects": 156, "size_gb": 4.5, "versioning": "Suspended", "encryption": "AES256"},
|
|
]
|
|
}
|
|
|
|
|
|
@router.get("/s3/buckets/{bucket_name}/objects")
|
|
async def get_s3_objects(
|
|
bucket_name: str,
|
|
prefix: Optional[str] = Query(None),
|
|
limit: int = Query(50, ge=1, le=1000),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Hämta objekt i en S3 bucket."""
|
|
objects = [
|
|
{"key": "index.html", "size": 12450, "last_modified": "2026-07-04T06:00:00Z", "etag": '"abc123"', "storage_class": "STANDARD"},
|
|
{"key": "insights/cost-of-outdated-data/index.html", "size": 8934, "last_modified": "2026-07-04T05:00:00Z", "etag": '"def456"', "storage_class": "STANDARD"},
|
|
{"key": "css/main.css", "size": 4521, "last_modified": "2026-07-01T00:00:00Z", "etag": '"ghi789"', "storage_class": "STANDARD"},
|
|
]
|
|
if prefix:
|
|
objects = [o for o in objects if o["key"].startswith(prefix)]
|
|
return {"bucket": bucket_name, "objects": objects[:limit], "total": len(objects)}
|
|
|
|
|
|
# ─── Backup & Restore ─────────────────────────────────────────────────────────
|
|
|
|
@router.get("/backups")
|
|
async def list_backups(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Lista alla backups."""
|
|
return {
|
|
"backups": [
|
|
{"id": "bk-20260704", "type": "automated", "status": "completed", "size_mb": 45.2, "created_at": "2026-07-04T03:00:00Z", "expires_at": "2026-08-04T03:00:00Z", "location": "s3://landvex-backups/daily/2026-07-04.sql.gz"},
|
|
{"id": "bk-20260703", "type": "automated", "status": "completed", "size_mb": 44.8, "created_at": "2026-07-03T03:00:00Z", "expires_at": "2026-08-03T03:00:00Z", "location": "s3://landvex-backups/daily/2026-07-03.sql.gz"},
|
|
{"id": "bk-20260702", "type": "automated", "status": "completed", "size_mb": 44.5, "created_at": "2026-07-02T03:00:00Z", "expires_at": "2026-08-02T03:00:00Z", "location": "s3://landvex-backups/daily/2026-07-02.sql.gz"},
|
|
{"id": "bk-20260701", "type": "manual", "status": "completed", "size_mb": 44.1, "created_at": "2026-07-01T12:00:00Z", "expires_at": None, "location": "s3://landvex-backups/manual/2026-07-01.sql.gz"},
|
|
],
|
|
"schedule": {
|
|
"daily": "03:00 UTC",
|
|
"weekly": "Sunday 03:00 UTC",
|
|
"retention_days": 30,
|
|
},
|
|
}
|
|
|
|
|
|
@router.post("/backups")
|
|
async def create_backup(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Skapa en manuell backup."""
|
|
return {
|
|
"status": "started",
|
|
"backup_id": f"bk-{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
|
"type": "manual",
|
|
"started_at": datetime.now().isoformat(),
|
|
"created_by": str(current_user.id),
|
|
}
|
|
|
|
|
|
@router.post("/backups/{backup_id}/restore")
|
|
async def restore_backup(
|
|
backup_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Återställ från backup."""
|
|
return {
|
|
"status": "started",
|
|
"backup_id": backup_id,
|
|
"started_at": datetime.now().isoformat(),
|
|
"warning": "This will overwrite current data. Ensure you have a fresh backup first.",
|
|
"created_by": str(current_user.id),
|
|
}
|
|
|
|
|
|
@router.delete("/backups/{backup_id}")
|
|
async def delete_backup(
|
|
backup_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin),
|
|
):
|
|
"""Ta bort en backup."""
|
|
return {
|
|
"status": "deleted",
|
|
"backup_id": backup_id,
|
|
"deleted_by": str(current_user.id),
|
|
"deleted_at": datetime.now().isoformat(),
|
|
}
|
|
|
|
|
|
# ─── System Health ────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/health")
|
|
async def get_detailed_health(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_manager),
|
|
):
|
|
"""Detaljerad system-hälsa."""
|
|
return {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"overall": "healthy",
|
|
"services": {
|
|
"api": {"status": "healthy", "response_time_ms": 45, "last_check": datetime.now().isoformat()},
|
|
"database": {"status": "healthy", "response_time_ms": 12, "last_check": datetime.now().isoformat(), "connections": 8, "max_connections": 100},
|
|
"redis": {"status": "healthy", "response_time_ms": 3, "last_check": datetime.now().isoformat()},
|
|
"s3": {"status": "healthy", "response_time_ms": 89, "last_check": datetime.now().isoformat()},
|
|
"cloudfront": {"status": "healthy", "response_time_ms": 120, "last_check": datetime.now().isoformat()},
|
|
},
|
|
"resources": {
|
|
"cpu_percent": 23.5,
|
|
"memory_percent": 45.2,
|
|
"disk_percent": 62.1,
|
|
"disk_free_gb": 45.3,
|
|
},
|
|
"uptime": {
|
|
"api_seconds": 86400,
|
|
"database_seconds": 604800,
|
|
},
|
|
}
|