landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Routers package
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,539 @@
|
||||
"""
|
||||
Landvex Aid Intelligence (LAI) Router.
|
||||
Dedikerad modul för att mäta biståndseffekt med oberoende verifiering.
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from app.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/aid", tags=["aid_intelligence"])
|
||||
|
||||
|
||||
class ProjectType(str, Enum):
|
||||
SCHOOL = "school"
|
||||
HOSPITAL = "hospital"
|
||||
ROAD = "road"
|
||||
BRIDGE = "bridge"
|
||||
WATER = "water"
|
||||
POWER = "power"
|
||||
AGRICULTURE = "agriculture"
|
||||
RENEWABLE = "renewable"
|
||||
HOUSING = "housing"
|
||||
SANITATION = "sanitation"
|
||||
DIGITAL = "digital"
|
||||
ENVIRONMENTAL = "environmental"
|
||||
EMERGENCY = "emergency"
|
||||
FOOD = "food"
|
||||
EDUCATION = "education"
|
||||
HEALTHCARE = "healthcare"
|
||||
MICROFINANCE = "microfinance"
|
||||
WOMEN_EMPOWERMENT = "women_empowerment"
|
||||
YOUTH_EMPLOYMENT = "youth_employment"
|
||||
|
||||
|
||||
class ProjectStatus(str, Enum):
|
||||
PLANNED = "planned"
|
||||
ACTIVE = "active"
|
||||
COMPLETED = "completed"
|
||||
DELAYED = "delayed"
|
||||
ABANDONED = "abandoned"
|
||||
UNDER_MAINTENANCE = "under_maintenance"
|
||||
|
||||
|
||||
class ObservationType(str, Enum):
|
||||
INFRASTRUCTURE = "infrastructure"
|
||||
ACTIVITY = "activity"
|
||||
MAINTENANCE = "maintenance"
|
||||
USAGE = "usage"
|
||||
ENVIRONMENTAL = "environmental"
|
||||
SAFETY = "safety"
|
||||
ACCESSIBILITY = "accessibility"
|
||||
|
||||
|
||||
# ─── PUBLIC ENDPOINTS ─────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/status")
|
||||
async def get_aid_status():
|
||||
"""Publik status för LAI."""
|
||||
return {
|
||||
"service": "Landvex Aid Intelligence",
|
||||
"status": "operational",
|
||||
"version": "1.0.0",
|
||||
"regions": ["Sub-Saharan Africa", "East Africa", "West Africa"],
|
||||
"projects_monitored": 1250,
|
||||
"observations_collected": 45000,
|
||||
"contributors_active": 3200,
|
||||
"last_update": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/projects")
|
||||
async def list_public_projects(
|
||||
country: Optional[str] = None,
|
||||
project_type: Optional[ProjectType] = None,
|
||||
status: Optional[ProjectStatus] = None,
|
||||
):
|
||||
"""Lista projekt med filtrering (publik)."""
|
||||
projects = [
|
||||
{
|
||||
"id": "proj-001",
|
||||
"name": "Mogadishu Primary School Renovation",
|
||||
"country": "Somalia",
|
||||
"region": "East Africa",
|
||||
"type": "school",
|
||||
"status": "active",
|
||||
"funding_agency": "UNICEF",
|
||||
"budget_usd": 250000,
|
||||
"start_date": "2025-03-01",
|
||||
"expected_completion": "2026-06-30",
|
||||
"aii_score": 72,
|
||||
"confidence": 85,
|
||||
"observations_count": 156,
|
||||
"last_observation": "2026-07-03",
|
||||
"location": {"lat": 2.0469, "lng": 45.3182}
|
||||
},
|
||||
{
|
||||
"id": "proj-002",
|
||||
"name": "Kampala Water Supply Extension",
|
||||
"country": "Uganda",
|
||||
"region": "East Africa",
|
||||
"type": "water",
|
||||
"status": "active",
|
||||
"funding_agency": "World Bank",
|
||||
"budget_usd": 1200000,
|
||||
"start_date": "2024-09-01",
|
||||
"expected_completion": "2026-12-31",
|
||||
"aii_score": 68,
|
||||
"confidence": 78,
|
||||
"observations_count": 203,
|
||||
"last_observation": "2026-07-02",
|
||||
"location": {"lat": 0.3476, "lng": 32.5825}
|
||||
},
|
||||
{
|
||||
"id": "proj-003",
|
||||
"name": "Accra Solar Microgrid",
|
||||
"country": "Ghana",
|
||||
"region": "West Africa",
|
||||
"type": "renewable",
|
||||
"status": "completed",
|
||||
"funding_agency": "GIZ",
|
||||
"budget_usd": 450000,
|
||||
"start_date": "2024-01-15",
|
||||
"expected_completion": "2025-12-31",
|
||||
"aii_score": 89,
|
||||
"confidence": 92,
|
||||
"observations_count": 312,
|
||||
"last_observation": "2026-07-01",
|
||||
"location": {"lat": 5.6037, "lng": -0.1870}
|
||||
}
|
||||
]
|
||||
|
||||
if country:
|
||||
projects = [p for p in projects if p["country"].lower() == country.lower()]
|
||||
if project_type:
|
||||
projects = [p for p in projects if p["type"] == project_type.value]
|
||||
if status:
|
||||
projects = [p for p in projects if p["status"] == status.value]
|
||||
|
||||
return {
|
||||
"projects": projects,
|
||||
"total": len(projects),
|
||||
"filters": {
|
||||
"country": country,
|
||||
"type": project_type.value if project_type else None,
|
||||
"status": status.value if status else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/projects/{project_id}")
|
||||
async def get_public_project(project_id: str):
|
||||
"""Hämta detaljer för ett specifikt projekt (publik)."""
|
||||
return {
|
||||
"id": project_id,
|
||||
"name": "Mogadishu Primary School Renovation",
|
||||
"description": "Renovation and expansion of primary school facilities including classrooms, sanitation, and water access.",
|
||||
"country": "Somalia",
|
||||
"region": "East Africa",
|
||||
"district": "Banadir",
|
||||
"type": "school",
|
||||
"status": "active",
|
||||
"funding_agency": "UNICEF",
|
||||
"implementing_partner": "Somali Education Consortium",
|
||||
"budget_usd": 250000,
|
||||
"start_date": "2025-03-01",
|
||||
"expected_completion": "2026-06-30",
|
||||
"actual_completion": None,
|
||||
"location": {"lat": 2.0469, "lng": 45.3182, "accuracy": "10m"},
|
||||
"aii_score": 72,
|
||||
"confidence": 85,
|
||||
"dimensions": {
|
||||
"accessibility": {"score": 80, "confidence": 90},
|
||||
"operational_status": {"score": 75, "confidence": 85},
|
||||
"maintenance_quality": {"score": 65, "confidence": 80},
|
||||
"population_usage": {"score": 70, "confidence": 82},
|
||||
"economic_activity": {"score": 60, "confidence": 75},
|
||||
"safety": {"score": 78, "confidence": 88},
|
||||
"environmental_sustainability": {"score": 72, "confidence": 85},
|
||||
"community_engagement": {"score": 85, "confidence": 90}
|
||||
},
|
||||
"observations": {
|
||||
"total": 156,
|
||||
"last_30_days": 23,
|
||||
"contributors": 12,
|
||||
"ai_validated": 148,
|
||||
"human_verified": 45
|
||||
},
|
||||
"timeline": [
|
||||
{"date": "2025-03-01", "event": "Project start", "type": "milestone"},
|
||||
{"date": "2025-06-15", "event": "Foundation completed", "type": "milestone"},
|
||||
{"date": "2025-11-20", "event": "Structure completed", "type": "milestone"},
|
||||
{"date": "2026-02-10", "event": "Roof installed", "type": "milestone"},
|
||||
{"date": "2026-07-03", "event": "Interior work ongoing", "type": "observation"}
|
||||
],
|
||||
"risks": [
|
||||
{
|
||||
"type": "delay",
|
||||
"severity": "medium",
|
||||
"description": "Interior completion 2 weeks behind schedule",
|
||||
"detected": "2026-06-15",
|
||||
"confidence": 75
|
||||
}
|
||||
],
|
||||
"transparency": {
|
||||
"observations_url": f"/api/v1/aid/public/projects/{project_id}/observations",
|
||||
"methodology": "AII-v1.0",
|
||||
"data_sources": ["quiXzoom", "satellite", "open_data"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/projects/{project_id}/observations")
|
||||
async def get_project_observations(project_id: str):
|
||||
"""Hämta alla observationer för ett projekt (publik)."""
|
||||
return {
|
||||
"project_id": project_id,
|
||||
"observations": [
|
||||
{
|
||||
"id": "obs-001",
|
||||
"type": "infrastructure",
|
||||
"date": "2026-07-03",
|
||||
"contributor_id": "contr-123",
|
||||
"location": {"lat": 2.0469, "lng": 45.3182},
|
||||
"media": ["https://cdn.landvex.com/obs/001-a.jpg", "https://cdn.landvex.com/obs/001-b.jpg"],
|
||||
"ai_analysis": {
|
||||
"construction_progress": 85,
|
||||
"material_quality": "good",
|
||||
"safety_compliance": "acceptable"
|
||||
},
|
||||
"confidence": 88,
|
||||
"verified": True
|
||||
},
|
||||
{
|
||||
"id": "obs-002",
|
||||
"type": "activity",
|
||||
"date": "2026-07-02",
|
||||
"contributor_id": "contr-124",
|
||||
"location": {"lat": 2.0470, "lng": 45.3183},
|
||||
"media": ["https://cdn.landvex.com/obs/002-a.jpg"],
|
||||
"ai_analysis": {
|
||||
"worker_count": 12,
|
||||
"activity_level": "high",
|
||||
"equipment_present": True
|
||||
},
|
||||
"confidence": 82,
|
||||
"verified": True
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/aii")
|
||||
async def get_aid_impact_index(
|
||||
country: Optional[str] = None,
|
||||
region: Optional[str] = None,
|
||||
project_type: Optional[ProjectType] = None,
|
||||
):
|
||||
"""Hämta Aid Impact Index (AII) för regioner eller länder."""
|
||||
return {
|
||||
"index_name": "Aid Impact Index (AII)",
|
||||
"version": "1.0",
|
||||
"methodology_url": "https://landvex.com/aid/methodology",
|
||||
"filters": {
|
||||
"country": country,
|
||||
"region": region,
|
||||
"type": project_type.value if project_type else None
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"country": "Somalia",
|
||||
"region": "East Africa",
|
||||
"aii_score": 72,
|
||||
"confidence": 85,
|
||||
"projects_count": 45,
|
||||
"observations_count": 2340,
|
||||
"trend": "improving",
|
||||
"dimensions": {
|
||||
"accessibility": 75,
|
||||
"operational_status": 70,
|
||||
"maintenance_quality": 65,
|
||||
"population_usage": 72,
|
||||
"safety": 78
|
||||
}
|
||||
},
|
||||
{
|
||||
"country": "Uganda",
|
||||
"region": "East Africa",
|
||||
"aii_score": 68,
|
||||
"confidence": 78,
|
||||
"projects_count": 67,
|
||||
"observations_count": 3120,
|
||||
"trend": "stable",
|
||||
"dimensions": {
|
||||
"accessibility": 70,
|
||||
"operational_status": 72,
|
||||
"maintenance_quality": 60,
|
||||
"population_usage": 68,
|
||||
"safety": 70
|
||||
}
|
||||
},
|
||||
{
|
||||
"country": "Ghana",
|
||||
"region": "West Africa",
|
||||
"aii_score": 81,
|
||||
"confidence": 88,
|
||||
"projects_count": 89,
|
||||
"observations_count": 4560,
|
||||
"trend": "improving",
|
||||
"dimensions": {
|
||||
"accessibility": 85,
|
||||
"operational_status": 82,
|
||||
"maintenance_quality": 78,
|
||||
"population_usage": 80,
|
||||
"safety": 82
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/countries")
|
||||
async def list_countries():
|
||||
"""Lista alla länder med aktiv övervakning."""
|
||||
return {
|
||||
"countries": [
|
||||
{"code": "SO", "name": "Somalia", "region": "East Africa", "projects": 45, "aii": 72},
|
||||
{"code": "UG", "name": "Uganda", "region": "East Africa", "projects": 67, "aii": 68},
|
||||
{"code": "KE", "name": "Kenya", "region": "East Africa", "projects": 92, "aii": 74},
|
||||
{"code": "ET", "name": "Ethiopia", "region": "East Africa", "projects": 78, "aii": 65},
|
||||
{"code": "TZ", "name": "Tanzania", "region": "East Africa", "projects": 56, "aii": 70},
|
||||
{"code": "GH", "name": "Ghana", "region": "West Africa", "projects": 89, "aii": 81},
|
||||
{"code": "NG", "name": "Nigeria", "region": "West Africa", "projects": 134, "aii": 63},
|
||||
{"code": "SN", "name": "Senegal", "region": "West Africa", "projects": 34, "aii": 76},
|
||||
{"code": "ZA", "name": "South Africa", "region": "Southern Africa", "projects": 112, "aii": 82}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/risk-alerts")
|
||||
async def get_risk_alerts():
|
||||
"""Hämta aktuella riskvarningar."""
|
||||
return {
|
||||
"alerts": [
|
||||
{
|
||||
"id": "alert-001",
|
||||
"severity": "high",
|
||||
"type": "abandoned_project",
|
||||
"project_id": "proj-045",
|
||||
"project_name": "Lagos Road Rehabilitation",
|
||||
"country": "Nigeria",
|
||||
"description": "No observable activity for 90 days. Equipment removed from site.",
|
||||
"detected": "2026-06-15",
|
||||
"confidence": 92,
|
||||
"observations_supporting": 8
|
||||
},
|
||||
{
|
||||
"id": "alert-002",
|
||||
"severity": "medium",
|
||||
"type": "deterioration",
|
||||
"project_id": "proj-067",
|
||||
"project_name": "Nairobi Health Clinic",
|
||||
"country": "Kenya",
|
||||
"description": "Infrastructure showing signs of accelerated deterioration.",
|
||||
"detected": "2026-07-01",
|
||||
"confidence": 78,
|
||||
"observations_supporting": 5
|
||||
},
|
||||
{
|
||||
"id": "alert-003",
|
||||
"severity": "medium",
|
||||
"type": "delay",
|
||||
"project_id": "proj-001",
|
||||
"project_name": "Mogadishu Primary School Renovation",
|
||||
"country": "Somalia",
|
||||
"description": "Interior completion 2 weeks behind schedule.",
|
||||
"detected": "2026-06-20",
|
||||
"confidence": 75,
|
||||
"observations_supporting": 6
|
||||
}
|
||||
],
|
||||
"total": 3,
|
||||
"last_updated": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
# ─── AUTHENTICATED ENDPOINTS ──────────────────────────────────────────────────
|
||||
|
||||
@router.post("/projects")
|
||||
async def create_project(
|
||||
name: str,
|
||||
country: str,
|
||||
project_type: ProjectType,
|
||||
funding_agency: str,
|
||||
budget_usd: float,
|
||||
location: Dict[str, float],
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Skapa ett nytt projekt för övervakning."""
|
||||
return {
|
||||
"status": "created",
|
||||
"project_id": f"proj-{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
"name": name,
|
||||
"country": country,
|
||||
"type": project_type.value,
|
||||
"funding_agency": funding_agency,
|
||||
"budget_usd": budget_usd,
|
||||
"location": location,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"created_by": current_user.id if hasattr(current_user, 'id') else 'admin'
|
||||
}
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/detailed")
|
||||
async def get_project_detailed(
|
||||
project_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Hämta detaljerad projektinformation (autentiserad)."""
|
||||
return await get_public_project(project_id)
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/observations")
|
||||
async def add_observation(
|
||||
project_id: str,
|
||||
observation_type: ObservationType,
|
||||
location: Dict[str, float],
|
||||
media_urls: List[str],
|
||||
notes: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Lägg till en observation till ett projekt."""
|
||||
return {
|
||||
"status": "added",
|
||||
"observation_id": f"obs-{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
"project_id": project_id,
|
||||
"type": observation_type.value,
|
||||
"location": location,
|
||||
"media": media_urls,
|
||||
"notes": notes,
|
||||
"submitted_at": datetime.now().isoformat(),
|
||||
"submitted_by": current_user.id if hasattr(current_user, 'id') else 'admin'
|
||||
}
|
||||
|
||||
|
||||
@router.get("/dashboard")
|
||||
async def get_aid_dashboard(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Hämta LAI dashboard för admin."""
|
||||
return {
|
||||
"organization": "Landvex Aid Intelligence",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"summary": {
|
||||
"projects_monitored": 1250,
|
||||
"projects_active": 890,
|
||||
"projects_completed": 340,
|
||||
"projects_at_risk": 20,
|
||||
"observations_total": 45000,
|
||||
"observations_last_30_days": 3200,
|
||||
"contributors_active": 3200,
|
||||
"countries_covered": 9,
|
||||
"regions_covered": 3
|
||||
},
|
||||
"kpis": {
|
||||
"average_aii_score": 72,
|
||||
"average_confidence": 82,
|
||||
"verification_rate": 94,
|
||||
"ai_validation_rate": 91,
|
||||
"response_time_hours": 48
|
||||
},
|
||||
"funding_monitored": {
|
||||
"total_usd": 450000000,
|
||||
"by_region": {
|
||||
"East Africa": 180000000,
|
||||
"West Africa": 150000000,
|
||||
"Southern Africa": 120000000
|
||||
}
|
||||
},
|
||||
"recent_activity": [
|
||||
{
|
||||
"type": "observation",
|
||||
"project": "Mogadishu Primary School",
|
||||
"description": "New observation added",
|
||||
"time": "2026-07-03T14:30:00Z"
|
||||
},
|
||||
{
|
||||
"type": "alert",
|
||||
"project": "Lagos Road Rehabilitation",
|
||||
"description": "Risk alert: Abandoned project detected",
|
||||
"time": "2026-07-02T09:15:00Z"
|
||||
}
|
||||
],
|
||||
"alerts_summary": {
|
||||
"high": 1,
|
||||
"medium": 2,
|
||||
"low": 5
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/reports/executive")
|
||||
async def get_executive_report(
|
||||
period: str = "quarterly",
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Hämta executive report för ledningen."""
|
||||
return {
|
||||
"report_type": "Executive Summary",
|
||||
"period": period,
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"key_findings": [
|
||||
"72% average Aid Impact Index across all monitored projects",
|
||||
"94% of observations pass AI validation",
|
||||
"20 projects flagged for potential risk",
|
||||
"East Africa shows strongest improvement trend"
|
||||
],
|
||||
"recommendations": [
|
||||
"Increase observation frequency in high-risk projects",
|
||||
"Expand contributor network in West Africa",
|
||||
"Implement automated alert escalation",
|
||||
"Publish quarterly transparency report"
|
||||
],
|
||||
"financial_summary": {
|
||||
"total_monitored": "$450M",
|
||||
"high_performers": "$180M (40%)",
|
||||
"at_risk": "$12M (2.7%)",
|
||||
"verification_cost": "$450K (0.1%)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
"""
|
||||
Landvex Aid Intelligence v2 - Evidence-Based Impact Verification
|
||||
Independent platform for measuring, verifying and improving development aid outcomes.
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from app.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/aid-v2", tags=["aid_intelligence_v2"])
|
||||
|
||||
|
||||
class VerificationStatus(str, Enum):
|
||||
VERIFIED = "verified"
|
||||
PARTIALLY_VERIFIED = "partially_verified"
|
||||
INSUFFICIENT_EVIDENCE = "insufficient_evidence"
|
||||
CONTRADICTION_DETECTED = "contradiction_detected"
|
||||
NOT_VERIFIED = "not_verified"
|
||||
|
||||
|
||||
class EvidenceType(str, Enum):
|
||||
SATELLITE = "satellite"
|
||||
FIELD_OBSERVATION = "field_observation"
|
||||
AI_ANALYSIS = "ai_analysis"
|
||||
OPEN_DATA = "open_data"
|
||||
OFFICIAL_REPORT = "official_report"
|
||||
COMMUNITY_FEEDBACK = "community_feedback"
|
||||
|
||||
|
||||
# ─── PUBLIC ENDPOINTS ─────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/philosophy")
|
||||
async def get_philosophy():
|
||||
"""Core philosophy and brand position."""
|
||||
return {
|
||||
"mission": "Build Landvex Aid Intelligence as the world's leading independent platform for measuring, verifying and improving the real-world impact of development aid.",
|
||||
"principles": [
|
||||
"Evidence over speculation",
|
||||
"Transparency over opacity",
|
||||
"Continuous learning over blame",
|
||||
"Independent verification over assumptions",
|
||||
"Long-term societal outcomes over short-term activity metrics",
|
||||
],
|
||||
"position": "We believe development aid should create measurable, sustainable improvements. We measure observable progress rather than assuming success or failure.",
|
||||
"commitment": "Our purpose is not to advocate for or against any organization. Our purpose is to increase transparency, accountability and learning by making observable outcomes measurable.",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/projects")
|
||||
async def list_projects(
|
||||
country: Optional[str] = None,
|
||||
sector: Optional[str] = None,
|
||||
verification_status: Optional[VerificationStatus] = None,
|
||||
):
|
||||
"""List development projects with verification status."""
|
||||
projects = [
|
||||
{
|
||||
"id": "proj-001",
|
||||
"name": "Mogadishu Primary School Renovation",
|
||||
"country": "Somalia",
|
||||
"region": "East Africa",
|
||||
"sector": "education",
|
||||
"funding_agency": "UNICEF",
|
||||
"budget_usd": 250000,
|
||||
"verification_status": "partially_verified",
|
||||
"confidence_score": 72,
|
||||
"evidence_count": 156,
|
||||
"last_observation": "2026-07-03",
|
||||
"key_findings": [
|
||||
"Infrastructure 85% complete (observed)",
|
||||
"Interior work ongoing",
|
||||
"2 weeks behind reported schedule",
|
||||
],
|
||||
"discrepancies": [
|
||||
{
|
||||
"type": "schedule_variance",
|
||||
"description": "Reported completion 95% vs observed 85%",
|
||||
"confidence": 78,
|
||||
"severity": "medium",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "proj-002",
|
||||
"name": "Kampala Water Supply Extension",
|
||||
"country": "Uganda",
|
||||
"region": "East Africa",
|
||||
"sector": "water",
|
||||
"funding_agency": "World Bank",
|
||||
"budget_usd": 1200000,
|
||||
"verification_status": "verified",
|
||||
"confidence_score": 88,
|
||||
"evidence_count": 203,
|
||||
"last_observation": "2026-07-02",
|
||||
"key_findings": [
|
||||
"Infrastructure operational",
|
||||
"Community utilization high",
|
||||
"Maintenance schedule followed",
|
||||
],
|
||||
"discrepancies": [],
|
||||
},
|
||||
]
|
||||
|
||||
if country:
|
||||
projects = [p for p in projects if p["country"].lower() == country.lower()]
|
||||
if sector:
|
||||
projects = [p for p in projects if p["sector"] == sector]
|
||||
if verification_status:
|
||||
projects = [p for p in projects if p["verification_status"] == verification_status.value]
|
||||
|
||||
return {
|
||||
"projects": projects,
|
||||
"total": len(projects),
|
||||
"methodology": "All findings are evidence-based and include confidence scores. Discrepancies are signals for further review, not conclusions of wrongdoing.",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/projects/{project_id}/evidence")
|
||||
async def get_project_evidence(project_id: str):
|
||||
"""Get all evidence for a project with transparency layer."""
|
||||
return {
|
||||
"project_id": project_id,
|
||||
"evidence_chain": [
|
||||
{
|
||||
"id": "ev-001",
|
||||
"type": "field_observation",
|
||||
"date": "2026-07-03",
|
||||
"source": "QUIXZOOM contributor",
|
||||
"location": {"lat": 2.0469, "lng": 45.3182},
|
||||
"media": ["https://cdn.landvex.com/obs/001-a.jpg"],
|
||||
"ai_analysis": {
|
||||
"construction_progress": 85,
|
||||
"material_quality": "good",
|
||||
"activity_level": "high",
|
||||
},
|
||||
"confidence": 88,
|
||||
"verified": True,
|
||||
},
|
||||
{
|
||||
"id": "ev-002",
|
||||
"type": "satellite",
|
||||
"date": "2026-07-01",
|
||||
"source": "Sentinel-2",
|
||||
"analysis": {
|
||||
"change_detected": True,
|
||||
"construction_activity": "active",
|
||||
"completion_estimate": 82,
|
||||
},
|
||||
"confidence": 92,
|
||||
"verified": True,
|
||||
},
|
||||
{
|
||||
"id": "ev-003",
|
||||
"type": "official_report",
|
||||
"date": "2026-06-15",
|
||||
"source": "UNICEF quarterly report",
|
||||
"claimed_progress": 95,
|
||||
"confidence": 100, # Official report
|
||||
},
|
||||
],
|
||||
"transparency": {
|
||||
"methodology": "Evidence is weighted by source reliability and cross-validated when possible.",
|
||||
"known_uncertainties": [
|
||||
"Interior completion percentage is estimate based on visual observation",
|
||||
"Satellite data may not detect recent changes (< 5 days)",
|
||||
],
|
||||
"confidence_calculation": "Weighted average of evidence scores, adjusted for source reliability and recency.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/verification-framework")
|
||||
async def get_verification_framework():
|
||||
"""Impact Verification Framework - multidimensional evaluation."""
|
||||
return {
|
||||
"framework_name": "Landvex Impact Verification Framework v1.0",
|
||||
"dimensions": [
|
||||
{
|
||||
"name": "Infrastructure Completion",
|
||||
"description": "Observable physical completion of planned infrastructure",
|
||||
"indicators": ["Construction progress", "Material quality", "Specification compliance"],
|
||||
"measurement": "Visual observation, AI analysis, satellite comparison",
|
||||
},
|
||||
{
|
||||
"name": "Operational Continuity",
|
||||
"description": "Whether facilities remain operational over time",
|
||||
"indicators": ["Activity levels", "Staff presence", "Service availability"],
|
||||
"measurement": "Repeated field observations, community feedback",
|
||||
},
|
||||
{
|
||||
"name": "Maintenance Quality",
|
||||
"description": "Observable maintenance and upkeep",
|
||||
"indicators": ["Physical condition", "Repair frequency", "Cleanliness"],
|
||||
"measurement": "Visual inspection, deterioration tracking",
|
||||
},
|
||||
{
|
||||
"name": "Accessibility",
|
||||
"description": "Physical and economic access for target population",
|
||||
"indicators": ["Transport links", "Operating hours", "Cost barriers"],
|
||||
"measurement": "Field observation, community surveys",
|
||||
},
|
||||
{
|
||||
"name": "Community Utilization",
|
||||
"description": "Actual use by intended beneficiaries",
|
||||
"indicators": ["Visitor counts", "User satisfaction", "Usage patterns"],
|
||||
"measurement": "Observation, surveys, usage data",
|
||||
},
|
||||
{
|
||||
"name": "Environmental Sustainability",
|
||||
"description": "Environmental impact and sustainability",
|
||||
"indicators": ["Resource efficiency", "Pollution", "Ecosystem impact"],
|
||||
"measurement": "Satellite, environmental sensors, observation",
|
||||
},
|
||||
{
|
||||
"name": "Economic Enablement",
|
||||
"description": "Economic activity generated or enabled",
|
||||
"indicators": ["Local employment", "Market activity", "Income effects"],
|
||||
"measurement": "Economic observation, satellite (night lights), surveys",
|
||||
},
|
||||
],
|
||||
"scoring": {
|
||||
"scale": "0-100 per dimension",
|
||||
"confidence_interval": "Always included",
|
||||
"minimum_evidence": "At least 3 independent observations required",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/sustainable-impact-index")
|
||||
async def get_sustainable_impact_index():
|
||||
"""Sustainable Impact Index - long-term observable contribution."""
|
||||
return {
|
||||
"index_name": "Landvex Sustainable Impact Index (SII)",
|
||||
"version": "1.0",
|
||||
"description": "Estimates long-term observable contribution of development projects",
|
||||
"dimensions": [
|
||||
{"name": "Longevity", "weight": 0.15, "description": "Years of continuous operation"},
|
||||
{"name": "Community Benefit", "weight": 0.15, "description": "Observable community value"},
|
||||
{"name": "Infrastructure Durability", "weight": 0.15, "description": "Physical condition over time"},
|
||||
{"name": "Maintenance", "weight": 0.10, "description": "Observable upkeep quality"},
|
||||
{"name": "Operational Continuity", "weight": 0.10, "description": "Uninterrupted service"},
|
||||
{"name": "Population Reach", "weight": 0.10, "description": "Number of beneficiaries"},
|
||||
{"name": "Economic Enablement", "weight": 0.10, "description": "Economic activity generated"},
|
||||
{"name": "Education Impact", "weight": 0.05, "description": "Learning outcomes (where measurable)"},
|
||||
{"name": "Health Impact", "weight": 0.05, "description": "Health outcomes (where measurable)"},
|
||||
{"name": "Environmental Outcomes", "weight": 0.05, "description": "Environmental effects"},
|
||||
],
|
||||
"methodology": "Each dimension scored 0-100 based on measurable indicators. Weighted average produces final SII score. Confidence interval always included.",
|
||||
"example_scores": [
|
||||
{"project": "Accra Solar Microgrid", "sii_score": 89, "confidence": 92},
|
||||
{"project": "Mogadishu Primary School", "sii_score": 72, "confidence": 78},
|
||||
{"project": "Kampala Water Supply", "sii_score": 85, "confidence": 88},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/contradictions")
|
||||
async def list_contradictions():
|
||||
"""Identified discrepancies between reported and observed data."""
|
||||
return {
|
||||
"disclaimer": "These findings are signals for further review, not conclusions of wrongdoing. They indicate areas where additional verification may be valuable.",
|
||||
"contradictions": [
|
||||
{
|
||||
"id": "contr-001",
|
||||
"project": "Mogadishu Primary School Renovation",
|
||||
"type": "schedule_variance",
|
||||
"reported": "95% complete (June 2026)",
|
||||
"observed": "85% complete (July 2026)",
|
||||
"evidence_sources": ["Field observation", "Satellite", "AI analysis"],
|
||||
"confidence": 78,
|
||||
"severity": "medium",
|
||||
"recommended_action": "Follow-up observation in 30 days",
|
||||
},
|
||||
{
|
||||
"id": "contr-002",
|
||||
"project": "Lagos Road Rehabilitation",
|
||||
"type": "activity_gap",
|
||||
"reported": "Active construction",
|
||||
"observed": "No activity for 90 days, equipment removed",
|
||||
"evidence_sources": ["Field observation", "Satellite time-series"],
|
||||
"confidence": 92,
|
||||
"severity": "high",
|
||||
"recommended_action": "Contact implementing partner for status update",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/positive-outcomes")
|
||||
async def list_positive_outcomes():
|
||||
"""Projects demonstrating strong observable outcomes."""
|
||||
return {
|
||||
"description": "Projects that consistently demonstrate strong implementation and sustained operation. These enable benchmarking and dissemination of successful practices.",
|
||||
"projects": [
|
||||
{
|
||||
"id": "proj-003",
|
||||
"name": "Accra Solar Microgrid",
|
||||
"country": "Ghana",
|
||||
"sector": "energy",
|
||||
"strengths": [
|
||||
"Strong implementation (observed)",
|
||||
"Sustained operation (24+ months)",
|
||||
"High community utilization",
|
||||
"Long-term maintenance observed",
|
||||
"Positive environmental outcomes",
|
||||
"Transparent reporting",
|
||||
],
|
||||
"sii_score": 89,
|
||||
"confidence": 92,
|
||||
"benchmark_category": "Energy Infrastructure",
|
||||
},
|
||||
{
|
||||
"id": "proj-004",
|
||||
"name": "Nairobi Digital Health Records",
|
||||
"country": "Kenya",
|
||||
"sector": "health",
|
||||
"strengths": [
|
||||
"Operational continuity",
|
||||
"Staff training evident",
|
||||
"Patient utilization increasing",
|
||||
"Data transparency",
|
||||
],
|
||||
"sii_score": 84,
|
||||
"confidence": 86,
|
||||
"benchmark_category": "Digital Health",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/continuous-monitoring")
|
||||
async def get_monitoring_capabilities():
|
||||
"""Continuous monitoring capabilities and data sources."""
|
||||
return {
|
||||
"monitoring_types": [
|
||||
{
|
||||
"type": "Satellite Imagery",
|
||||
"frequency": "Every 5 days (Sentinel-2)",
|
||||
"capabilities": ["Change detection", "Construction progress", "Environmental monitoring"],
|
||||
"coverage": "Global",
|
||||
},
|
||||
{
|
||||
"type": "QUIXZOOM Field Observations",
|
||||
"frequency": "On-demand missions",
|
||||
"capabilities": ["Ground truth", "Activity verification", "Condition assessment"],
|
||||
"coverage": "20+ countries",
|
||||
},
|
||||
{
|
||||
"type": "AI Image Analysis",
|
||||
"frequency": "Real-time",
|
||||
"capabilities": ["Progress estimation", "Quality assessment", "Anomaly detection"],
|
||||
"coverage": "All observed projects",
|
||||
},
|
||||
{
|
||||
"type": "Open Data Integration",
|
||||
"frequency": "Continuous",
|
||||
"capabilities": ["Procurement tracking", "Financial transparency", "Report comparison"],
|
||||
"coverage": "Partner organizations",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/research")
|
||||
async def get_research_program():
|
||||
"""Development Effectiveness Science research track."""
|
||||
return {
|
||||
"program_name": "Landvex Development Effectiveness Science",
|
||||
"mission": "Develop scientifically grounded indices and models for measuring long-term societal impact",
|
||||
"research_areas": [
|
||||
{
|
||||
"name": "Impact Measurement Methodology",
|
||||
"description": "Rigorous methods for measuring development outcomes",
|
||||
"approach": "Combine established frameworks with novel AI-assisted techniques",
|
||||
},
|
||||
{
|
||||
"name": "Longitudinal Impact Analysis",
|
||||
"description": "Track project outcomes over 5-10 year periods",
|
||||
"approach": "Satellite time-series, repeated observations, outcome correlation",
|
||||
},
|
||||
{
|
||||
"name": "Bias Detection & Correction",
|
||||
"description": "Identify and correct for observational and selection biases",
|
||||
"approach": "Statistical methods, control groups, multi-source validation",
|
||||
},
|
||||
{
|
||||
"name": "Predictive Sustainability Modeling",
|
||||
"description": "Predict long-term sustainability based on early indicators",
|
||||
"approach": "Machine learning on historical project data",
|
||||
},
|
||||
],
|
||||
"publications": [
|
||||
{
|
||||
"title": "The Landvex Sustainable Impact Index: A Multidimensional Framework",
|
||||
"status": "In development",
|
||||
"expected_release": "2027 Q1",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ─── AUTHENTICATED ENDPOINTS ─────────────────────────────────────────────────
|
||||
|
||||
@router.get("/dashboard")
|
||||
async def get_dashboard(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Admin dashboard for Aid Intelligence."""
|
||||
return {
|
||||
"organization": "Landvex Aid Intelligence",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"summary": {
|
||||
"projects_monitored": 1250,
|
||||
"projects_verified": 890,
|
||||
"projects_with_discrepancies": 45,
|
||||
"projects_exemplary": 120,
|
||||
"observations_total": 45000,
|
||||
"evidence_sources": 5,
|
||||
"countries_covered": 9,
|
||||
},
|
||||
"verification_summary": {
|
||||
"verified": 890,
|
||||
"partially_verified": 280,
|
||||
"insufficient_evidence": 65,
|
||||
"contradiction_detected": 15,
|
||||
},
|
||||
"key_metrics": {
|
||||
"average_confidence": 82,
|
||||
"average_sii_score": 74,
|
||||
"verification_rate": 94,
|
||||
"evidence_growth_monthly": 3200,
|
||||
},
|
||||
"recent_activity": [
|
||||
{
|
||||
"type": "observation",
|
||||
"project": "Mogadishu Primary School",
|
||||
"description": "New field observation added",
|
||||
"time": "2026-07-03T14:30:00Z",
|
||||
},
|
||||
{
|
||||
"type": "discrepancy",
|
||||
"project": "Lagos Road Rehabilitation",
|
||||
"description": "Activity gap detected - no construction for 90 days",
|
||||
"time": "2026-07-02T09:15:00Z",
|
||||
},
|
||||
{
|
||||
"type": "positive_outcome",
|
||||
"project": "Accra Solar Microgrid",
|
||||
"description": "24 months continuous operation verified",
|
||||
"time": "2026-07-01T16:00:00Z",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/flag-for-review")
|
||||
async def flag_project_for_review(
|
||||
project_id: str,
|
||||
reason: str,
|
||||
evidence_ids: List[str],
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Flag a project for additional review (not accusation)."""
|
||||
return {
|
||||
"status": "flagged_for_review",
|
||||
"project_id": project_id,
|
||||
"reason": reason,
|
||||
"evidence_count": len(evidence_ids),
|
||||
"flagged_by": current_user.id if hasattr(current_user, 'id') else 'admin',
|
||||
"flagged_at": datetime.now().isoformat(),
|
||||
"next_steps": [
|
||||
"Contact implementing partner for clarification",
|
||||
"Schedule follow-up observation",
|
||||
"Review additional evidence sources",
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Audit Log API Routes för LandveX Admin Backend.
|
||||
Endpoints för att söka och granska audit-loggar.
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from app.dependencies import get_current_user, require_admin
|
||||
from app.database import get_db
|
||||
from app.schemas import AuditLogListOut
|
||||
from app.services.audit_service import AuditService
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/audit-logs", tags=["Audit"])
|
||||
|
||||
|
||||
@router.get("", response_model=AuditLogListOut)
|
||||
async def list_audit_logs(
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
entity_type: Optional[str] = Query(None),
|
||||
entity_id: Optional[str] = Query(None),
|
||||
actor_id: Optional[str] = Query(None),
|
||||
action: Optional[str] = Query(None),
|
||||
current_user: User = Depends(require_admin),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Lista audit-loggar med filtrering. Kräver admin."""
|
||||
audit_service = AuditService(db)
|
||||
logs = await audit_service.get_recent(limit=limit + offset)
|
||||
|
||||
# Filtrera i minnet (för små datamängder)
|
||||
# I produktion: lägg till databasfiltrering
|
||||
if entity_type:
|
||||
logs = [l for l in logs if l.entity_type == entity_type]
|
||||
if entity_id:
|
||||
logs = [l for l in logs if l.entity_id == entity_id]
|
||||
if actor_id:
|
||||
logs = [l for l in logs if l.actor_id == actor_id]
|
||||
if action:
|
||||
from app.models import AuditAction
|
||||
try:
|
||||
action_enum = AuditAction(action)
|
||||
logs = [l for l in logs if l.action == action_enum]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Apply offset after filtering
|
||||
total = len(logs)
|
||||
logs = logs[offset:offset + limit]
|
||||
|
||||
return AuditLogListOut(
|
||||
items=[log.to_dict() for log in logs],
|
||||
total=total, limit=limit, offset=offset
|
||||
)
|
||||
|
||||
|
||||
@router.get("/entity/{entity_type}/{entity_id}", response_model=AuditLogListOut)
|
||||
async def get_entity_audit_logs(
|
||||
entity_type: str,
|
||||
entity_id: str,
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
current_user: User = Depends(require_admin),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Hämta alla audit-loggar för en specifik entitet."""
|
||||
audit_service = AuditService(db)
|
||||
logs, total = await audit_service.get_by_entity(
|
||||
entity_type=entity_type, entity_id=entity_id, limit=limit, offset=offset
|
||||
)
|
||||
return AuditLogListOut(
|
||||
items=[log.to_dict() for log in logs],
|
||||
total=total, limit=limit, offset=offset
|
||||
)
|
||||
|
||||
|
||||
@router.get("/actor/{actor_id}", response_model=AuditLogListOut)
|
||||
async def get_actor_audit_logs(
|
||||
actor_id: str,
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
current_user: User = Depends(require_admin),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Hämta alla audit-loggar för en specifik användare (actor)."""
|
||||
audit_service = AuditService(db)
|
||||
logs, total = await audit_service.get_by_actor(
|
||||
actor_id=actor_id, limit=limit, offset=offset
|
||||
)
|
||||
return AuditLogListOut(
|
||||
items=[log.to_dict() for log in logs],
|
||||
total=total, limit=limit, offset=offset
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Auth-endpoints: login, refresh, change password."""
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_user
|
||||
from app.models import User
|
||||
from app.schemas import Token, LoginRequest, RefreshRequest, ChangePasswordRequest
|
||||
from app.security import create_access_token, create_refresh_token, decode_token
|
||||
from app.services.user_service import UserService
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["Authentication"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(data: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
service = UserService(db)
|
||||
user = await service.authenticate(data.email, data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
user.last_login_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
access_token = create_access_token(user.id)
|
||||
refresh_token = create_refresh_token(user.id)
|
||||
return Token(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
expires_in=30 * 60,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=Token)
|
||||
async def refresh(data: RefreshRequest, db: AsyncSession = Depends(get_db)):
|
||||
payload = decode_token(data.refresh_token)
|
||||
if payload is None or payload.get("type") != "refresh":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token",
|
||||
)
|
||||
|
||||
from uuid import UUID
|
||||
user_id = payload.get("sub")
|
||||
service = UserService(db)
|
||||
user = await service.get_by_id(UUID(user_id))
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
access_token = create_access_token(user.id)
|
||||
refresh_token = create_refresh_token(user.id)
|
||||
return Token(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
expires_in=30 * 60,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/change-password")
|
||||
async def change_password(
|
||||
data: ChangePasswordRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = UserService(db)
|
||||
await service.change_password(current_user, data.current_password, data.new_password)
|
||||
return {"message": "Password changed successfully"}
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def me(current_user: User = Depends(get_current_user)):
|
||||
from app.schemas import UserDetailOut
|
||||
return UserDetailOut.model_validate(current_user)
|
||||
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
Content Management Module
|
||||
Handles articles, SEO metadata, and content publishing
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import Article, SEOMetadata, ArticleStatus, User
|
||||
from app.core.security import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/content", tags=["content"])
|
||||
|
||||
# ─── ARTICLES ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/articles")
|
||||
async def list_articles(
|
||||
status: Optional[ArticleStatus] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List all articles with optional filtering."""
|
||||
query = db.query(Article)
|
||||
if status:
|
||||
query = query.filter(Article.status == status)
|
||||
articles = query.offset(skip).limit(limit).all()
|
||||
return {
|
||||
"articles": [
|
||||
{
|
||||
"id": a.id,
|
||||
"title": a.title,
|
||||
"slug": a.slug,
|
||||
"status": a.status.value,
|
||||
"author": a.author.full_name if a.author else None,
|
||||
"published_at": a.published_at.isoformat() if a.published_at else None,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
}
|
||||
for a in articles
|
||||
],
|
||||
"total": query.count(),
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
}
|
||||
|
||||
@router.post("/articles")
|
||||
async def create_article(
|
||||
title: str,
|
||||
slug: str,
|
||||
content: str,
|
||||
excerpt: Optional[str] = None,
|
||||
seo_title: Optional[str] = None,
|
||||
seo_description: Optional[str] = None,
|
||||
keywords: Optional[List[str]] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new article."""
|
||||
article = Article(
|
||||
title=title,
|
||||
slug=slug,
|
||||
content=content,
|
||||
excerpt=excerpt,
|
||||
author_id=current_user.id,
|
||||
seo_title=seo_title,
|
||||
seo_description=seo_description,
|
||||
keywords=keywords,
|
||||
)
|
||||
db.add(article)
|
||||
db.commit()
|
||||
db.refresh(article)
|
||||
return {"status": "created", "article_id": article.id, "slug": slug}
|
||||
|
||||
@router.get("/articles/{article_id}")
|
||||
async def get_article(
|
||||
article_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get a specific article."""
|
||||
article = db.query(Article).filter(Article.id == article_id).first()
|
||||
if not article:
|
||||
raise HTTPException(status_code=404, detail="Article not found")
|
||||
return {
|
||||
"id": article.id,
|
||||
"title": article.title,
|
||||
"slug": article.slug,
|
||||
"content": article.content,
|
||||
"excerpt": article.excerpt,
|
||||
"status": article.status.value,
|
||||
"author": article.author.full_name if article.author else None,
|
||||
"seo_title": article.seo_title,
|
||||
"seo_description": article.seo_description,
|
||||
"keywords": article.keywords,
|
||||
"published_at": article.published_at.isoformat() if article.published_at else None,
|
||||
"created_at": article.created_at.isoformat() if article.created_at else None,
|
||||
"updated_at": article.updated_at.isoformat() if article.updated_at else None,
|
||||
}
|
||||
|
||||
@router.put("/articles/{article_id}")
|
||||
async def update_article(
|
||||
article_id: int,
|
||||
title: Optional[str] = None,
|
||||
content: Optional[str] = None,
|
||||
status: Optional[ArticleStatus] = None,
|
||||
seo_title: Optional[str] = None,
|
||||
seo_description: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update an article."""
|
||||
article = db.query(Article).filter(Article.id == article_id).first()
|
||||
if not article:
|
||||
raise HTTPException(status_code=404, detail="Article not found")
|
||||
|
||||
if title:
|
||||
article.title = title
|
||||
if content:
|
||||
article.content = content
|
||||
if status:
|
||||
article.status = status
|
||||
if status == ArticleStatus.PUBLISHED and not article.published_at:
|
||||
article.published_at = datetime.utcnow()
|
||||
if seo_title:
|
||||
article.seo_title = seo_title
|
||||
if seo_description:
|
||||
article.seo_description = seo_description
|
||||
|
||||
db.commit()
|
||||
db.refresh(article)
|
||||
return {"status": "updated", "article_id": article.id}
|
||||
|
||||
@router.delete("/articles/{article_id}")
|
||||
async def delete_article(
|
||||
article_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Delete an article."""
|
||||
article = db.query(Article).filter(Article.id == article_id).first()
|
||||
if not article:
|
||||
raise HTTPException(status_code=404, detail="Article not found")
|
||||
db.delete(article)
|
||||
db.commit()
|
||||
return {"status": "deleted", "article_id": article_id}
|
||||
|
||||
# ─── SEO METADATA ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/seo")
|
||||
async def list_seo_metadata(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List all SEO metadata entries."""
|
||||
seo_entries = db.query(SEOMetadata).all()
|
||||
return {
|
||||
"entries": [
|
||||
{
|
||||
"id": s.id,
|
||||
"page_path": s.page_path,
|
||||
"title": s.title,
|
||||
"description": s.description,
|
||||
"audit_score": s.audit_score,
|
||||
"last_audit": s.last_audit.isoformat() if s.last_audit else None,
|
||||
}
|
||||
for s in seo_entries
|
||||
]
|
||||
}
|
||||
|
||||
@router.post("/seo")
|
||||
async def create_seo_metadata(
|
||||
page_path: str,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
canonical_url: Optional[str] = None,
|
||||
og_image: Optional[str] = None,
|
||||
schema_markup: Optional[dict] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create SEO metadata for a page."""
|
||||
seo = SEOMetadata(
|
||||
page_path=page_path,
|
||||
title=title,
|
||||
description=description,
|
||||
canonical_url=canonical_url,
|
||||
og_image=og_image,
|
||||
schema_markup=schema_markup,
|
||||
)
|
||||
db.add(seo)
|
||||
db.commit()
|
||||
db.refresh(seo)
|
||||
return {"status": "created", "seo_id": seo.id}
|
||||
|
||||
@router.put("/seo/{seo_id}")
|
||||
async def update_seo_metadata(
|
||||
seo_id: int,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
audit_score: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update SEO metadata."""
|
||||
seo = db.query(SEOMetadata).filter(SEOMetadata.id == seo_id).first()
|
||||
if not seo:
|
||||
raise HTTPException(status_code=404, detail="SEO metadata not found")
|
||||
|
||||
if title:
|
||||
seo.title = title
|
||||
if description:
|
||||
seo.description = description
|
||||
if audit_score is not None:
|
||||
seo.audit_score = audit_score
|
||||
seo.last_audit = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
db.refresh(seo)
|
||||
return {"status": "updated", "seo_id": seo.id}
|
||||
@@ -0,0 +1,283 @@
|
||||
"""
|
||||
Economy/Ledger Management Module
|
||||
Handles accounts, transactions, bank accounts, and financial reporting
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import Account, Transaction, BankAccount, AccountType, User
|
||||
from app.core.security import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/economy", tags=["economy"])
|
||||
|
||||
# ─── ACCOUNTS ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/accounts")
|
||||
async def list_accounts(
|
||||
account_type: Optional[AccountType] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List all accounts with optional filtering."""
|
||||
query = db.query(Account)
|
||||
if account_type:
|
||||
query = query.filter(Account.account_type == account_type)
|
||||
accounts = query.all()
|
||||
return {
|
||||
"accounts": [
|
||||
{
|
||||
"id": a.id,
|
||||
"account_number": a.account_number,
|
||||
"name": a.name,
|
||||
"type": a.account_type.value,
|
||||
"balance": float(a.balance) if a.balance else 0,
|
||||
"currency": a.currency,
|
||||
"is_active": a.is_active,
|
||||
}
|
||||
for a in accounts
|
||||
],
|
||||
"total": query.count(),
|
||||
}
|
||||
|
||||
@router.post("/accounts")
|
||||
async def create_account(
|
||||
account_number: str,
|
||||
name: str,
|
||||
account_type: AccountType,
|
||||
parent_id: Optional[int] = None,
|
||||
description: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new account."""
|
||||
account = Account(
|
||||
account_number=account_number,
|
||||
name=name,
|
||||
account_type=account_type,
|
||||
parent_id=parent_id,
|
||||
description=description,
|
||||
)
|
||||
db.add(account)
|
||||
db.commit()
|
||||
db.refresh(account)
|
||||
return {"status": "created", "account_id": account.id}
|
||||
|
||||
@router.get("/accounts/{account_id}")
|
||||
async def get_account(
|
||||
account_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get account details."""
|
||||
account = db.query(Account).filter(Account.id == account_id).first()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="Account not found")
|
||||
return {
|
||||
"id": account.id,
|
||||
"account_number": account.account_number,
|
||||
"name": account.name,
|
||||
"type": account.account_type.value,
|
||||
"balance": float(account.balance) if account.balance else 0,
|
||||
"currency": account.currency,
|
||||
"description": account.description,
|
||||
"is_active": account.is_active,
|
||||
}
|
||||
|
||||
# ─── TRANSACTIONS ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/transactions")
|
||||
async def list_transactions(
|
||||
account_id: Optional[int] = None,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List transactions with filtering."""
|
||||
query = db.query(Transaction)
|
||||
if account_id:
|
||||
query = query.filter(
|
||||
(Transaction.debit_account_id == account_id) |
|
||||
(Transaction.credit_account_id == account_id)
|
||||
)
|
||||
if start_date:
|
||||
query = query.filter(Transaction.date >= start_date)
|
||||
if end_date:
|
||||
query = query.filter(Transaction.date <= end_date)
|
||||
|
||||
transactions = query.order_by(Transaction.date.desc()).offset(skip).limit(limit).all()
|
||||
return {
|
||||
"transactions": [
|
||||
{
|
||||
"id": t.id,
|
||||
"transaction_id": t.transaction_id,
|
||||
"date": t.date.isoformat() if t.date else None,
|
||||
"description": t.description,
|
||||
"amount": float(t.amount) if t.amount else 0,
|
||||
"debit_account_id": t.debit_account_id,
|
||||
"credit_account_id": t.credit_account_id,
|
||||
"status": t.status,
|
||||
"reference": t.reference,
|
||||
}
|
||||
for t in transactions
|
||||
],
|
||||
"total": query.count(),
|
||||
}
|
||||
|
||||
@router.post("/transactions")
|
||||
async def create_transaction(
|
||||
transaction_id: str,
|
||||
date: datetime,
|
||||
description: str,
|
||||
amount: float,
|
||||
debit_account_id: int,
|
||||
credit_account_id: int,
|
||||
reference: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new transaction (double-entry bookkeeping)."""
|
||||
# Verify accounts exist
|
||||
debit_account = db.query(Account).filter(Account.id == debit_account_id).first()
|
||||
credit_account = db.query(Account).filter(Account.id == credit_account_id).first()
|
||||
|
||||
if not debit_account or not credit_account:
|
||||
raise HTTPException(status_code=404, detail="One or both accounts not found")
|
||||
|
||||
transaction = Transaction(
|
||||
transaction_id=transaction_id,
|
||||
date=date,
|
||||
description=description,
|
||||
amount=Decimal(str(amount)),
|
||||
debit_account_id=debit_account_id,
|
||||
credit_account_id=credit_account_id,
|
||||
reference=reference,
|
||||
created_by=current_user.id,
|
||||
)
|
||||
|
||||
# Update account balances
|
||||
debit_account.balance += Decimal(str(amount))
|
||||
credit_account.balance -= Decimal(str(amount))
|
||||
|
||||
db.add(transaction)
|
||||
db.commit()
|
||||
db.refresh(transaction)
|
||||
return {"status": "created", "transaction_id": transaction.id}
|
||||
|
||||
# ─── BANK ACCOUNTS ────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/bank-accounts")
|
||||
async def list_bank_accounts(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List all bank accounts."""
|
||||
accounts = db.query(BankAccount).all()
|
||||
return {
|
||||
"accounts": [
|
||||
{
|
||||
"id": a.id,
|
||||
"bank_name": a.bank_name,
|
||||
"account_name": a.account_name,
|
||||
"account_number": a.account_number,
|
||||
"iban": a.iban,
|
||||
"bic": a.bic,
|
||||
"currency": a.currency,
|
||||
"current_balance": float(a.current_balance) if a.current_balance else 0,
|
||||
"account_type": a.account_type,
|
||||
"is_active": a.is_active,
|
||||
}
|
||||
for a in accounts
|
||||
]
|
||||
}
|
||||
|
||||
@router.post("/bank-accounts")
|
||||
async def create_bank_account(
|
||||
bank_name: str,
|
||||
account_name: str,
|
||||
account_number: Optional[str] = None,
|
||||
iban: Optional[str] = None,
|
||||
bic: Optional[str] = None,
|
||||
currency: str = "SEK",
|
||||
account_type: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new bank account."""
|
||||
account = BankAccount(
|
||||
bank_name=bank_name,
|
||||
account_name=account_name,
|
||||
account_number=account_number,
|
||||
iban=iban,
|
||||
bic=bic,
|
||||
currency=currency,
|
||||
account_type=account_type,
|
||||
)
|
||||
db.add(account)
|
||||
db.commit()
|
||||
db.refresh(account)
|
||||
return {"status": "created", "bank_account_id": account.id}
|
||||
|
||||
# ─── REPORTS ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/reports/balance-sheet")
|
||||
async def get_balance_sheet(
|
||||
as_of: Optional[date] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Generate balance sheet report."""
|
||||
assets = db.query(Account).filter(Account.account_type == AccountType.ASSET).all()
|
||||
liabilities = db.query(Account).filter(Account.account_type == AccountType.LIABILITY).all()
|
||||
equity = db.query(Account).filter(Account.account_type == AccountType.EQUITY).all()
|
||||
|
||||
return {
|
||||
"report_type": "Balance Sheet",
|
||||
"as_of": as_of.isoformat() if as_of else datetime.utcnow().date().isoformat(),
|
||||
"assets": {
|
||||
"total": sum(float(a.balance) for a in assets),
|
||||
"accounts": [{"name": a.name, "balance": float(a.balance)} for a in assets]
|
||||
},
|
||||
"liabilities": {
|
||||
"total": sum(float(l.balance) for l in liabilities),
|
||||
"accounts": [{"name": l.name, "balance": float(l.balance)} for l in liabilities]
|
||||
},
|
||||
"equity": {
|
||||
"total": sum(float(e.balance) for e in equity),
|
||||
"accounts": [{"name": e.name, "balance": float(e.balance)} for e in equity]
|
||||
},
|
||||
}
|
||||
|
||||
@router.get("/reports/income-statement")
|
||||
async def get_income_statement(
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Generate income statement (resultaträkning)."""
|
||||
revenues = db.query(Account).filter(Account.account_type == AccountType.REVENUE).all()
|
||||
expenses = db.query(Account).filter(Account.account_type == AccountType.EXPENSE).all()
|
||||
|
||||
total_revenue = sum(float(r.balance) for r in revenues)
|
||||
total_expenses = sum(float(e.balance) for e in expenses)
|
||||
|
||||
return {
|
||||
"report_type": "Income Statement",
|
||||
"period": {"start": start_date.isoformat(), "end": end_date.isoformat()},
|
||||
"revenue": {
|
||||
"total": total_revenue,
|
||||
"accounts": [{"name": r.name, "balance": float(r.balance)} for r in revenues]
|
||||
},
|
||||
"expenses": {
|
||||
"total": total_expenses,
|
||||
"accounts": [{"name": e.name, "balance": float(e.balance)} for e in expenses]
|
||||
},
|
||||
"net_income": total_revenue - total_expenses,
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
"""
|
||||
HR Management Module
|
||||
Handles employees, contractors, payroll, and time tracking
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import Employee, Contractor, EmployeeStatus, User
|
||||
from app.core.security import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/hr", tags=["hr"])
|
||||
|
||||
# ─── EMPLOYEES ────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/employees")
|
||||
async def list_employees(
|
||||
status: Optional[EmployeeStatus] = None,
|
||||
department: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List all employees with optional filtering."""
|
||||
query = db.query(Employee)
|
||||
if status:
|
||||
query = query.filter(Employee.status == status)
|
||||
if department:
|
||||
query = query.filter(Employee.department.ilike(f"%{department}%"))
|
||||
employees = query.offset(skip).limit(limit).all()
|
||||
return {
|
||||
"employees": [
|
||||
{
|
||||
"id": e.id,
|
||||
"employee_id": e.employee_id,
|
||||
"full_name": f"{e.first_name} {e.last_name}",
|
||||
"email": e.email,
|
||||
"department": e.department,
|
||||
"position": e.position,
|
||||
"status": e.status.value,
|
||||
"start_date": e.start_date.isoformat() if e.start_date else None,
|
||||
"salary": float(e.salary) if e.salary else None,
|
||||
"salary_currency": e.salary_currency,
|
||||
}
|
||||
for e in employees
|
||||
],
|
||||
"total": query.count(),
|
||||
}
|
||||
|
||||
@router.post("/employees")
|
||||
async def create_employee(
|
||||
employee_id: str,
|
||||
first_name: str,
|
||||
last_name: str,
|
||||
email: str,
|
||||
department: str,
|
||||
position: str,
|
||||
salary: float,
|
||||
start_date: datetime,
|
||||
phone: Optional[str] = None,
|
||||
manager_id: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new employee record."""
|
||||
employee = Employee(
|
||||
employee_id=employee_id,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
email=email,
|
||||
phone=phone,
|
||||
department=department,
|
||||
position=position,
|
||||
salary=salary,
|
||||
start_date=start_date,
|
||||
manager_id=manager_id,
|
||||
)
|
||||
db.add(employee)
|
||||
db.commit()
|
||||
db.refresh(employee)
|
||||
return {"status": "created", "employee_id": employee.id}
|
||||
|
||||
@router.get("/employees/{employee_id}")
|
||||
async def get_employee(
|
||||
employee_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get employee details."""
|
||||
employee = db.query(Employee).filter(Employee.id == employee_id).first()
|
||||
if not employee:
|
||||
raise HTTPException(status_code=404, detail="Employee not found")
|
||||
return {
|
||||
"id": employee.id,
|
||||
"employee_id": employee.employee_id,
|
||||
"first_name": employee.first_name,
|
||||
"last_name": employee.last_name,
|
||||
"email": employee.email,
|
||||
"phone": employee.phone,
|
||||
"department": employee.department,
|
||||
"position": employee.position,
|
||||
"status": employee.status.value,
|
||||
"start_date": employee.start_date.isoformat() if employee.start_date else None,
|
||||
"end_date": employee.end_date.isoformat() if employee.end_date else None,
|
||||
"salary": float(employee.salary) if employee.salary else None,
|
||||
"salary_currency": employee.salary_currency,
|
||||
"manager_id": employee.manager_id,
|
||||
}
|
||||
|
||||
@router.put("/employees/{employee_id}")
|
||||
async def update_employee(
|
||||
employee_id: int,
|
||||
department: Optional[str] = None,
|
||||
position: Optional[str] = None,
|
||||
salary: Optional[float] = None,
|
||||
status: Optional[EmployeeStatus] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update employee information."""
|
||||
employee = db.query(Employee).filter(Employee.id == employee_id).first()
|
||||
if not employee:
|
||||
raise HTTPException(status_code=404, detail="Employee not found")
|
||||
|
||||
if department:
|
||||
employee.department = department
|
||||
if position:
|
||||
employee.position = position
|
||||
if salary:
|
||||
employee.salary = salary
|
||||
if status:
|
||||
employee.status = status
|
||||
if status == EmployeeStatus.TERMINATED:
|
||||
employee.end_date = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
db.refresh(employee)
|
||||
return {"status": "updated", "employee_id": employee.id}
|
||||
|
||||
# ─── CONTRACTORS ──────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/contractors")
|
||||
async def list_contractors(
|
||||
status: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List all contractors."""
|
||||
query = db.query(Contractor)
|
||||
if status:
|
||||
query = query.filter(Contractor.status == status)
|
||||
contractors = query.all()
|
||||
return {
|
||||
"contractors": [
|
||||
{
|
||||
"id": c.id,
|
||||
"contractor_id": c.contractor_id,
|
||||
"company_name": c.company_name,
|
||||
"contact_name": c.contact_name,
|
||||
"email": c.email,
|
||||
"services": c.services,
|
||||
"hourly_rate": float(c.hourly_rate) if c.hourly_rate else None,
|
||||
"currency": c.currency,
|
||||
"contract_start": c.contract_start.isoformat() if c.contract_start else None,
|
||||
"contract_end": c.contract_end.isoformat() if c.contract_end else None,
|
||||
"status": c.status,
|
||||
}
|
||||
for c in contractors
|
||||
],
|
||||
"total": query.count(),
|
||||
}
|
||||
|
||||
@router.post("/contractors")
|
||||
async def create_contractor(
|
||||
contractor_id: str,
|
||||
company_name: str,
|
||||
contact_name: str,
|
||||
email: str,
|
||||
services: List[str],
|
||||
hourly_rate: float,
|
||||
contract_start: datetime,
|
||||
contract_end: Optional[datetime] = None,
|
||||
phone: Optional[str] = None,
|
||||
currency: str = "SEK",
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new contractor record."""
|
||||
contractor = Contractor(
|
||||
contractor_id=contractor_id,
|
||||
company_name=company_name,
|
||||
contact_name=contact_name,
|
||||
email=email,
|
||||
phone=phone,
|
||||
services=services,
|
||||
hourly_rate=hourly_rate,
|
||||
currency=currency,
|
||||
contract_start=contract_start,
|
||||
contract_end=contract_end,
|
||||
)
|
||||
db.add(contractor)
|
||||
db.commit()
|
||||
db.refresh(contractor)
|
||||
return {"status": "created", "contractor_id": contractor.id}
|
||||
|
||||
@router.get("/contractors/{contractor_id}")
|
||||
async def get_contractor(
|
||||
contractor_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get contractor details."""
|
||||
contractor = db.query(Contractor).filter(Contractor.id == contractor_id).first()
|
||||
if not contractor:
|
||||
raise HTTPException(status_code=404, detail="Contractor not found")
|
||||
return {
|
||||
"id": contractor.id,
|
||||
"contractor_id": contractor.contractor_id,
|
||||
"company_name": contractor.company_name,
|
||||
"contact_name": contractor.contact_name,
|
||||
"email": contractor.email,
|
||||
"phone": contractor.phone,
|
||||
"services": contractor.services,
|
||||
"hourly_rate": float(contractor.hourly_rate) if contractor.hourly_rate else None,
|
||||
"currency": contractor.currency,
|
||||
"contract_start": contractor.contract_start.isoformat() if contractor.contract_start else None,
|
||||
"contract_end": contractor.contract_end.isoformat() if contractor.contract_end else None,
|
||||
"status": contractor.status,
|
||||
}
|
||||
|
||||
# ─── PAYROLL ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/payroll/summary")
|
||||
async def get_payroll_summary(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get payroll summary."""
|
||||
employees = db.query(Employee).filter(Employee.status == EmployeeStatus.ACTIVE).all()
|
||||
contractors = db.query(Contractor).filter(Contractor.status == "active").all()
|
||||
|
||||
total_salary = sum(float(e.salary) for e in employees if e.salary)
|
||||
total_contractor_cost = sum(float(c.hourly_rate) * 160 for c in contractors if c.hourly_rate) # Assume 160h/month
|
||||
|
||||
return {
|
||||
"period": datetime.utcnow().strftime("%Y-%m"),
|
||||
"employees": {
|
||||
"count": len(employees),
|
||||
"total_monthly_salary": total_salary,
|
||||
"currency": "SEK",
|
||||
},
|
||||
"contractors": {
|
||||
"count": len(contractors),
|
||||
"estimated_monthly_cost": total_contractor_cost,
|
||||
"currency": "SEK",
|
||||
},
|
||||
"total_monthly_cost": total_salary + total_contractor_cost,
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Invitation endpoints."""
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_user, require_admin, require_manager
|
||||
from app.models import User
|
||||
from app.schemas import InvitationCreate, InvitationOut, AcceptInvitationRequest, UserOut
|
||||
from app.services.invitation_service import InvitationService
|
||||
|
||||
router = APIRouter(prefix="/invitations", tags=["Invitations"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[InvitationOut])
|
||||
async def list_invitations(
|
||||
tenant_id: Optional[UUID] = Query(None),
|
||||
pending_only: bool = Query(True),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
current_user: User = Depends(require_manager),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = InvitationService(db)
|
||||
effective_tenant = tenant_id
|
||||
if not current_user.is_superuser and current_user.role.value == "manager":
|
||||
effective_tenant = current_user.tenant_id
|
||||
|
||||
items, total = await service.list_invitations(
|
||||
tenant_id=effective_tenant,
|
||||
pending_only=pending_only,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
@router.post("", response_model=InvitationOut, status_code=201)
|
||||
async def create_invitation(
|
||||
data: InvitationCreate,
|
||||
current_user: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = InvitationService(db)
|
||||
invitation = await service.create_invitation(data, invited_by=current_user)
|
||||
return InvitationOut.model_validate(invitation)
|
||||
|
||||
|
||||
@router.post("/accept", response_model=UserOut)
|
||||
async def accept_invitation(
|
||||
data: AcceptInvitationRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = InvitationService(db)
|
||||
user = await service.accept_invitation(data)
|
||||
return UserOut.model_validate(user)
|
||||
|
||||
|
||||
@router.get("/validate/{token}")
|
||||
async def validate_invitation(
|
||||
token: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = InvitationService(db)
|
||||
invitation = await service.get_by_token(token)
|
||||
if invitation is None:
|
||||
return {"valid": False, "reason": "not_found"}
|
||||
if invitation.accepted_at is not None:
|
||||
return {"valid": False, "reason": "already_accepted"}
|
||||
if invitation.expires_at < datetime.utcnow():
|
||||
return {"valid": False, "reason": "expired"}
|
||||
return {
|
||||
"valid": True,
|
||||
"email": invitation.email,
|
||||
"role": invitation.role.value,
|
||||
"expires_at": invitation.expires_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/{invitation_id}", status_code=204)
|
||||
async def revoke_invitation(
|
||||
invitation_id: UUID,
|
||||
current_user: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = InvitationService(db)
|
||||
await service.revoke_invitation(invitation_id, actor=current_user)
|
||||
return None
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
@@ -0,0 +1,631 @@
|
||||
"""
|
||||
Landvex Intelligence Fusion Engine (LIFE) - Core Architecture v3
|
||||
AI-drivet operativsystem för verklighetsintelligens
|
||||
|
||||
9 Core Blocks:
|
||||
1-6. Layer Architecture (Acquisition → Normalization → Evidence → Intelligence → Knowledge → Decision)
|
||||
7. Decision Engine (Vad bör användaren göra nu?)
|
||||
8. Learning Engine (Självutvärdering och förbättring)
|
||||
9. Integration Layer (API, Dashboards, Alerts, Missions)
|
||||
|
||||
Core Principle: Evidence Before Opinion
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
|
||||
from app.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/life-core", tags=["life_core"])
|
||||
|
||||
|
||||
# ─── LAYER 1: ACQUISITION ─────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/layer-1-acquisition")
|
||||
async def get_acquisition_layer():
|
||||
"""
|
||||
Layer 1 – Acquisition: Datainsamling från alla källor
|
||||
"""
|
||||
return {
|
||||
"layer": 1,
|
||||
"name": "Acquisition",
|
||||
"description": "Kontinuerlig datainsamling från alla tillgängliga källor",
|
||||
"sources": {
|
||||
"quixzoom": {
|
||||
"type": "Crowdsourced field observations",
|
||||
"frequency": "On-demand + continuous",
|
||||
"data_types": ["Photos", "Video", "Audio", "GPS", "Structured forms"],
|
||||
"active_contributors": 3200,
|
||||
"daily_observations": 450,
|
||||
},
|
||||
"satellites": {
|
||||
"type": "Geospatial intelligence",
|
||||
"frequency": "Every 5 days (Sentinel-2)",
|
||||
"data_types": ["Multispectral imagery", "SAR", "Night lights", "Thermal"],
|
||||
"coverage": "Global",
|
||||
"resolution": "10m - 30m",
|
||||
},
|
||||
"drones": {
|
||||
"type": "High-resolution aerial",
|
||||
"frequency": "On-demand",
|
||||
"data_types": ["RGB", "Multispectral", "LiDAR", "Thermal"],
|
||||
"resolution": "Sub-meter",
|
||||
},
|
||||
"sensors": {
|
||||
"type": "IoT and environmental sensors",
|
||||
"frequency": "Real-time",
|
||||
"data_types": ["Weather", "Air quality", "Water levels", "Seismic", "Noise"],
|
||||
},
|
||||
"news": {
|
||||
"type": "News intelligence",
|
||||
"frequency": "Real-time",
|
||||
"sources": 2500,
|
||||
"languages": ["en", "fr", "es", "ar", "sw", "pt"],
|
||||
},
|
||||
"rss": {
|
||||
"type": "RSS feeds and web monitoring",
|
||||
"frequency": "Hourly",
|
||||
"feeds_monitored": 1200,
|
||||
},
|
||||
"public_databases": {
|
||||
"type": "Open data portals",
|
||||
"frequency": "Daily",
|
||||
"databases": ["World Bank", "UN OCHA", "OpenStreetMap", "GADM", "GHSL"],
|
||||
},
|
||||
"documents": {
|
||||
"type": "Document ingestion",
|
||||
"frequency": "Continuous",
|
||||
"types": ["PDF reports", "Excel", "Word", "PowerPoint", "HTML"],
|
||||
},
|
||||
"apis": {
|
||||
"type": "Third-party APIs",
|
||||
"frequency": "Real-time to daily",
|
||||
"integrations": ["ReliefWeb", "GDACS", "INFORM", "ACLED"],
|
||||
},
|
||||
"gis_data": {
|
||||
"type": "Geographic information systems",
|
||||
"frequency": "Weekly",
|
||||
"layers": ["Administrative boundaries", "Roads", "Buildings", "Land use", "Hydrology"],
|
||||
},
|
||||
},
|
||||
"metrics": {
|
||||
"total_sources": 10,
|
||||
"active_streams": 45,
|
||||
"daily_data_points": 125000,
|
||||
"monthly_storage_gb": 450,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── LAYER 2: NORMALIZATION ───────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/layer-2-normalization")
|
||||
async def get_normalization_layer():
|
||||
"""
|
||||
Layer 2 – Normalization: Standardisering och kvalitetssäkring
|
||||
"""
|
||||
return {
|
||||
"layer": 2,
|
||||
"name": "Normalization",
|
||||
"description": "Standardisering, geokodning, ontologimappning och kvalitetssäkring",
|
||||
"processes": {
|
||||
"standardization": {
|
||||
"description": "Konvertera alla data till standardformat",
|
||||
"formats": ["GeoJSON", "WKT", "ISO 8601", "Schema.org"],
|
||||
"daily_records_processed": 125000,
|
||||
},
|
||||
"geocoding": {
|
||||
"description": "Placera allt på en karta",
|
||||
"methods": ["GPS coordinates", "Address geocoding", "Place name resolution", "Relative positioning"],
|
||||
"accuracy": "±3m (GPS) to ±100m (geocoded)",
|
||||
},
|
||||
"ontology_mapping": {
|
||||
"description": "Mappa till gemensam ontologi",
|
||||
"ontology": "Landvex Reality Ontology v1.0",
|
||||
"entities": ["Project", "Organization", "Location", "Event", "Observation", "Infrastructure"],
|
||||
"relationships": ["funds", "implements", "located_in", "observed_at", "affected_by"],
|
||||
},
|
||||
"entity_resolution": {
|
||||
"description": "Identifiera samma entitet från olika källor",
|
||||
"methods": ["Name matching", "Location matching", "Temporal alignment", "Contextual similarity"],
|
||||
"accuracy": 94,
|
||||
},
|
||||
"deduplication": {
|
||||
"description": "Ta bort dubbletter",
|
||||
"methods": ["Exact match", "Fuzzy match", "Temporal proximity", "Spatial proximity"],
|
||||
"daily_duplicates_removed": 12000,
|
||||
},
|
||||
"quality_assurance": {
|
||||
"description": "Kvalitetssäkring",
|
||||
"checks": ["Completeness", "Accuracy", "Consistency", "Timeliness", "Validity"],
|
||||
"rejection_rate": 3.2,
|
||||
},
|
||||
},
|
||||
"metrics": {
|
||||
"daily_records_normalized": 113000,
|
||||
"geocoding_success_rate": 97.5,
|
||||
"entity_resolution_accuracy": 94,
|
||||
"average_processing_time_ms": 45,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── LAYER 3: EVIDENCE ────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/layer-3-evidence")
|
||||
async def get_evidence_layer():
|
||||
"""
|
||||
Layer 3 – Evidence: Evidensgraf, lineage, minne och versionshantering
|
||||
"""
|
||||
return {
|
||||
"layer": 3,
|
||||
"name": "Evidence",
|
||||
"description": "Bygg en transparent, spårbar evidensbas",
|
||||
"components": {
|
||||
"evidence_graph": {
|
||||
"description": "Hierarkisk graf av alla observationer",
|
||||
"nodes": 125000,
|
||||
"edges": 450000,
|
||||
"levels": ["Observation", "Object", "Project", "Programme", "Organization", "Region", "Country"],
|
||||
},
|
||||
"evidence_lineage": {
|
||||
"description": "Komplett ursprung för varje datapunkt",
|
||||
"tracked_observations": 45000,
|
||||
"lineage_depth": "Source → Collected → Validated → Cross-validated → AI → Human → Revisions",
|
||||
},
|
||||
"reality_memory": {
|
||||
"description": "Permanent lagring av allt",
|
||||
"storage_tb": 2.4,
|
||||
"retention": "Permanent",
|
||||
"reproducibility": "100%",
|
||||
},
|
||||
"object_identity": {
|
||||
"description": "Unik identifiering av varje objekt",
|
||||
"objects_tracked": 15000,
|
||||
"identity_methods": ["UUID", "Geohash", "Temporal signature", "Feature fingerprint"],
|
||||
},
|
||||
"versioning": {
|
||||
"description": "Versionshantering av all data",
|
||||
"versions_stored": 234000,
|
||||
"versioning_strategy": "Immutable append-only",
|
||||
},
|
||||
},
|
||||
"metrics": {
|
||||
"total_evidence_items": 125000,
|
||||
"average_confidence": 82,
|
||||
"traceability": "100%",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── LAYER 4: INTELLIGENCE ────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/layer-4-intelligence")
|
||||
async def get_intelligence_layer():
|
||||
"""
|
||||
Layer 4 – Intelligence: Detektion, hypoteser, risk och prediktion
|
||||
"""
|
||||
return {
|
||||
"layer": 4,
|
||||
"name": "Intelligence",
|
||||
"description": "Omvandla evidens till intelligens genom AI och analys",
|
||||
"engines": {
|
||||
"change_detection": {
|
||||
"description": "Automatisk förändringsdetektion",
|
||||
"methods": ["Satellite differencing", "Temporal analysis", "Anomaly detection", "Trend analysis"],
|
||||
"detection_rate": 94,
|
||||
"false_positive_rate": 6,
|
||||
},
|
||||
"event_detection": {
|
||||
"description": "Identifiera meningsfulla händelser",
|
||||
"event_types": ["Natural disaster", "Conflict", "Infrastructure failure", "Construction delay", "Environmental incident"],
|
||||
"daily_events_detected": 12,
|
||||
"accuracy": 87,
|
||||
},
|
||||
"contradiction_detection": {
|
||||
"description": "Upptäck avvikelser mellan källor",
|
||||
"contradictions_found": 1247,
|
||||
"categories": ["Schedule", "Progress", "Operational status", "Budget", "Location"],
|
||||
"resolution_rate": 45,
|
||||
},
|
||||
"hypothesis_engine": {
|
||||
"description": "Generera förklaringshypoteser",
|
||||
"hypotheses_generated": 3400,
|
||||
"average_confidence": 68,
|
||||
"method": "Evidence-ranked, not conclusion-first",
|
||||
},
|
||||
"risk_engine": {
|
||||
"description": "Riskbedömning och varningar",
|
||||
"risk_models": ["Project delay", "Operational failure", "Environmental", "Security", "Financial"],
|
||||
"prediction_horizon": "30-90 days",
|
||||
"accuracy": 76,
|
||||
},
|
||||
"prediction_engine": {
|
||||
"description": "Prediktiv analys",
|
||||
"predictions": ["Completion date", "Operational sustainability", "Maintenance needs", "Budget variance"],
|
||||
"confidence_intervals": "Always included",
|
||||
},
|
||||
"confidence_engine": {
|
||||
"description": "Beräkna konfidens från multipla källor",
|
||||
"factors": ["Source reliability", "Cross-validation", "Temporal consistency", "Historical accuracy"],
|
||||
"average_confidence": 82,
|
||||
},
|
||||
},
|
||||
"metrics": {
|
||||
"daily_insights_generated": 450,
|
||||
"prediction_accuracy": 76,
|
||||
"false_positive_rate": 8,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── LAYER 5: KNOWLEDGE ───────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/layer-5-knowledge")
|
||||
async def get_knowledge_layer():
|
||||
"""
|
||||
Layer 5 – Knowledge: Kunskapsgrafer och DNA-profiler
|
||||
"""
|
||||
return {
|
||||
"layer": 5,
|
||||
"name": "Knowledge",
|
||||
"description": "Bygg strukturerad kunskap från råintelligens",
|
||||
"graphs": {
|
||||
"reality_dna": {
|
||||
"description": "Projektets fingeravtryck",
|
||||
"dimensions": 9,
|
||||
"profiles": 1250,
|
||||
"comparable": True,
|
||||
},
|
||||
"context_graph": {
|
||||
"description": "Kontextuell förståelse av omgivningen",
|
||||
"factors": ["Political", "Economic", "Climate", "Security", "Infrastructure", "Population"],
|
||||
"projects_with_context": 1250,
|
||||
},
|
||||
"urban_knowledge_graph": {
|
||||
"description": "Stadsutvecklingskunskap",
|
||||
"entities": ["Buildings", "Roads", "Utilities", "Services", "Zones"],
|
||||
"coverage": "45 cities",
|
||||
},
|
||||
"aid_knowledge_graph": {
|
||||
"description": "Biståndskunskap",
|
||||
"entities": ["Projects", "Donors", "Implementers", "Sectors", "Outcomes"],
|
||||
"coverage": "1250 projects",
|
||||
},
|
||||
"infrastructure_graph": {
|
||||
"description": "Infrastrukturkunskap",
|
||||
"entities": ["Roads", "Bridges", "Power", "Water", "Communications"],
|
||||
"coverage": "Global",
|
||||
},
|
||||
"environmental_graph": {
|
||||
"description": "Miljökunskap",
|
||||
"entities": ["Vegetation", "Water bodies", "Land use", "Climate", "Biodiversity"],
|
||||
"coverage": "Global",
|
||||
},
|
||||
},
|
||||
"metrics": {
|
||||
"total_entities": 450000,
|
||||
"total_relationships": 1200000,
|
||||
"knowledge_triples": 2100000,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── LAYER 6: DECISION ────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/layer-6-decision")
|
||||
async def get_decision_layer():
|
||||
"""
|
||||
Layer 6 – Decision: Presentation och beslutsstöd
|
||||
"""
|
||||
return {
|
||||
"layer": 6,
|
||||
"name": "Decision",
|
||||
"description": "Omvandla kunskap till handling",
|
||||
"interfaces": {
|
||||
"dashboards": {
|
||||
"description": "Interaktiva dashboards",
|
||||
"types": ["Executive", "Operational", "Analytical", "Public"],
|
||||
"widgets": 45,
|
||||
},
|
||||
"api": {
|
||||
"description": "Programmatisk åtkomst",
|
||||
"endpoints": 85,
|
||||
"formats": ["JSON", "GeoJSON", "CSV", "Parquet"],
|
||||
"rate_limit": "1000 req/min",
|
||||
},
|
||||
"alerts": {
|
||||
"description": "Realtidsvarningar",
|
||||
"channels": ["Email", "SMS", "Webhook", "Push", "Slack"],
|
||||
"alert_types": 25,
|
||||
},
|
||||
"missions": {
|
||||
"description": "QUIXZOOM uppdragsgenerering",
|
||||
"auto_generated": True,
|
||||
"priority_scoring": True,
|
||||
},
|
||||
"reports": {
|
||||
"description": "Automatisk rapportgenerering",
|
||||
"templates": ["Executive summary", "Technical", "Audit", "Impact assessment"],
|
||||
"formats": ["PDF", "HTML", "DOCX"],
|
||||
},
|
||||
"ai_agents": {
|
||||
"description": "AI-agenter för automatiserade uppgifter",
|
||||
"agents": ["Monitor", "Analyst", "Reporter", "Mission planner"],
|
||||
"autonomy_level": "Human-in-the-loop",
|
||||
},
|
||||
"automation": {
|
||||
"description": "Automatiserade arbetsflöden",
|
||||
"workflows": ["Event response", "Verification pipeline", "Alert escalation", "Report distribution"],
|
||||
},
|
||||
},
|
||||
"metrics": {
|
||||
"daily_api_calls": 45000,
|
||||
"active_dashboard_users": 320,
|
||||
"alerts_sent_daily": 1200,
|
||||
"reports_generated_monthly": 450,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── BLOCK 7: DECISION ENGINE ─────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/decision-engine")
|
||||
async def get_decision_engine():
|
||||
"""
|
||||
Decision Engine: Vad bör användaren göra nu?
|
||||
"""
|
||||
return {
|
||||
"block": 7,
|
||||
"name": "Decision Engine",
|
||||
"description": "Omvandla intelligens till konkreta rekommendationer",
|
||||
"principle": "LIFE vet vad som händer, varför det händer, och hur säker modellen är. Decision Engine svarar på: Vad bör användaren göra nu?",
|
||||
"recommendation_types": {
|
||||
"send_quixzoom_mission": {
|
||||
"description": "Skicka QUIXZOOM-verifieringsuppdrag",
|
||||
"trigger": "Otillräcklig field evidence eller avvikelse detekterad",
|
||||
"priority_calculation": "Risk × Uncertainty × Cost of verification",
|
||||
"example": "Projekt visar 85% completion men rapporterar 95%. Skicka mission för att fotografera interiör.",
|
||||
},
|
||||
"order_satellite_imagery": {
|
||||
"description": "Beställ ny satellitbild",
|
||||
"trigger": "Behov av uppdaterad geospatial data",
|
||||
"priority_calculation": "Data age × Change probability × Decision urgency",
|
||||
"example": "Senaste bilden är 10 dagar gammal och området har nyligen drabbats av översvämning.",
|
||||
},
|
||||
"flag_for_manual_review": {
|
||||
"description": "Markera för manuell granskning",
|
||||
"trigger": "Högkonfidens avvikelse eller komplex situation",
|
||||
"priority_calculation": "Confidence × Severity × Strategic importance",
|
||||
"example": "Stor avvikelse mellan rapporterad budget och observerad aktivitet. Kräver analytikergranskning.",
|
||||
},
|
||||
"notify_project_owner": {
|
||||
"description": "Informera projektägare om observerad avvikelse",
|
||||
"trigger": "Verifierad avvikelse som kan påverka projektet",
|
||||
"priority_calculation": "Severity × Verification confidence × Response time sensitivity",
|
||||
"example": "Observerad försening på 2 veckor. Informera implementerande partner.",
|
||||
},
|
||||
"wait": {
|
||||
"description": "Avvakta - evidensen är otillräcklig",
|
||||
"trigger": "För låg konfidens för att agera",
|
||||
"priority_calculation": "Monitoring continues, action deferred until confidence threshold reached",
|
||||
"example": "Endast en källa rapporterar avvikelse. Vänta på korsvalidering.",
|
||||
},
|
||||
"prioritize_followup": {
|
||||
"description": "Prioritera uppföljning baserat på risk och osäkerhet",
|
||||
"trigger": "Flera projekt kräver uppmärksamhet",
|
||||
"priority_calculation": "Risk score × Evidence gap × Strategic value × Resource availability",
|
||||
"example": "15 projekt flaggade. Prioritera de 5 med högst risk × osäkerhet.",
|
||||
},
|
||||
},
|
||||
"decision_matrix": {
|
||||
"high_confidence_high_risk": "Omedelbar åtgärd - hög konfidens och hög risk",
|
||||
"high_confidence_low_risk": "Dokumentera - låg prioritet men välunderbyggd",
|
||||
"low_confidence_high_risk": "Samla mer evidens - hög risk men osäker",
|
||||
"low_confidence_low_risk": "Övervaka - låg prioritet och osäker",
|
||||
},
|
||||
"example_decisions": [
|
||||
{
|
||||
"project": "Mogadishu Primary School",
|
||||
"situation": "85% observed vs 95% reported completion",
|
||||
"confidence": 78,
|
||||
"risk": "medium",
|
||||
"recommendation": "send_quixzoom_mission",
|
||||
"rationale": "Avvikelse verifierad av 3 oberoende källor. Interiör observation behövs för att klargöra.",
|
||||
"priority": 8.2,
|
||||
},
|
||||
{
|
||||
"project": "Lagos Road Rehabilitation",
|
||||
"situation": "No activity for 90 days, equipment removed",
|
||||
"confidence": 92,
|
||||
"risk": "high",
|
||||
"recommendation": "flag_for_manual_review",
|
||||
"rationale": "Högkonfidens indikation på projektavbrott. Kräver omedelbar analytikergranskning.",
|
||||
"priority": 9.5,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ─── BLOCK 8: LEARNING ENGINE ─────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/learning-engine")
|
||||
async def get_learning_engine():
|
||||
"""
|
||||
Learning Engine: Självutvärdering och kontinuerlig förbättring
|
||||
"""
|
||||
return {
|
||||
"block": 8,
|
||||
"name": "Learning Engine",
|
||||
"description": "LIFE utvärderar sina egna slutsatser och förbättrar sina modeller",
|
||||
"learning_loops": {
|
||||
"hypothesis_validation": {
|
||||
"description": "Vilka hypoteser visade sig stämma?",
|
||||
"method": "Jämför prediktion med faktisk utveckling",
|
||||
"metrics": {
|
||||
"total_hypotheses": 3400,
|
||||
"validated": 2150,
|
||||
"partially_validated": 890,
|
||||
"invalidated": 360,
|
||||
"accuracy": 73,
|
||||
},
|
||||
},
|
||||
"risk_indicator_performance": {
|
||||
"description": "Vilka riskindikatorer var mest prediktiva?",
|
||||
"method": "Korrelera tidiga varningssignaler med faktiska händelser",
|
||||
"top_indicators": [
|
||||
{"indicator": "Satellite activity decline", "predictive_power": 0.87},
|
||||
{"indicator": "Contributor report frequency drop", "predictive_power": 0.82},
|
||||
{"indicator": "News sentiment shift", "predictive_power": 0.78},
|
||||
{"indicator": "Budget execution lag", "predictive_power": 0.75},
|
||||
],
|
||||
},
|
||||
"source_accuracy": {
|
||||
"description": "Vilka datakällor gav bäst träffsäkerhet?",
|
||||
"method": "Utvärdera varje källas historiska noggrannhet",
|
||||
"rankings": [
|
||||
{"source": "Satellite imagery", "accuracy": 94, "reliability": "very_high"},
|
||||
{"source": "QUIXZOOM field obs", "accuracy": 88, "reliability": "high"},
|
||||
{"source": "Official reports", "accuracy": 82, "reliability": "high"},
|
||||
{"source": "News monitoring", "accuracy": 65, "reliability": "medium"},
|
||||
{"source": "Social media", "accuracy": 45, "reliability": "low"},
|
||||
],
|
||||
},
|
||||
"environmental_adaptation": {
|
||||
"description": "Hur presterar modeller i olika miljöer?",
|
||||
"method": "Jämför prediktionsnoggrannhet per region och sektor",
|
||||
"best_performing": [
|
||||
{"environment": "Urban infrastructure", "accuracy": 89},
|
||||
{"environment": "Rural agriculture", "accuracy": 76},
|
||||
{"environment": "Conflict zones", "accuracy": 62},
|
||||
],
|
||||
},
|
||||
},
|
||||
"model_improvement": {
|
||||
"description": "Kontinuerlig modellförbättring",
|
||||
"process": [
|
||||
"Samla utfall för alla prediktioner",
|
||||
"Identifiera systematiska fel",
|
||||
"Justera modellparametrar",
|
||||
"A/B-testa nya modeller",
|
||||
"Deploya förbättrade modeller",
|
||||
],
|
||||
"current_improvements": [
|
||||
{"model": "Construction progress estimator", "improvement": "+12% accuracy", "status": "deployed"},
|
||||
{"model": "Risk predictor", "improvement": "+8% precision", "status": "testing"},
|
||||
{"model": "Event detector", "improvement": "-15% false positives", "status": "training"},
|
||||
],
|
||||
},
|
||||
"feedback_integration": {
|
||||
"description": "Integrera mänsklig feedback",
|
||||
"sources": ["Analyst corrections", "Customer feedback", "Expert reviews", "Ground truth validation"],
|
||||
"feedback_items_processed": 4500,
|
||||
"incorporation_rate": 87,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── BLOCK 9: INTEGRATION LAYER ───────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/integration")
|
||||
async def get_integration_layer():
|
||||
"""
|
||||
Integration Layer: Anslutning till externa system och arbetsflöden
|
||||
"""
|
||||
return {
|
||||
"block": 9,
|
||||
"name": "Integration Layer",
|
||||
"description": "Anslut LIFE till omvärlden",
|
||||
"interfaces": {
|
||||
"rest_api": {
|
||||
"description": "REST API för alla LIFE-funktioner",
|
||||
"version": "v1",
|
||||
"endpoints": 120,
|
||||
"authentication": "OAuth 2.0 + API keys",
|
||||
"rate_limits": "1000 req/min standard, 10000 req/min enterprise",
|
||||
"formats": ["JSON", "GeoJSON", "CSV"],
|
||||
},
|
||||
"webhooks": {
|
||||
"description": "Realtidsnotifieringar",
|
||||
"events": ["new_observation", "change_detected", "risk_alert", "hypothesis_generated"],
|
||||
"delivery": "Guaranteed delivery with retry",
|
||||
},
|
||||
"streaming": {
|
||||
"description": "Realtidsdataströmmar",
|
||||
"protocols": ["WebSocket", "SSE", "MQTT"],
|
||||
"topics": ["observations", "events", "alerts", "insights"],
|
||||
},
|
||||
"sdk": {
|
||||
"description": "SDK för enkel integration",
|
||||
"languages": ["Python", "JavaScript", "Java", "Go"],
|
||||
"features": ["Data ingestion", "Query builder", "Visualization helpers"],
|
||||
},
|
||||
"plugins": {
|
||||
"description": "Plugin-arkitektur för anpassning",
|
||||
"types": ["Data source", "Analyzer", "Visualizer", "Exporter"],
|
||||
"marketplace": "Coming Q2 2027",
|
||||
},
|
||||
},
|
||||
"enterprise_integrations": {
|
||||
"gis": ["ArcGIS", "QGIS", "Google Earth Engine"],
|
||||
"bi": ["Tableau", "Power BI", "Looker"],
|
||||
"crm": ["Salesforce", "HubSpot"],
|
||||
"erp": ["SAP", "Oracle"],
|
||||
"communication": ["Slack", "Teams", "Email"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── SYSTEM OVERVIEW ──────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/architecture")
|
||||
async def get_architecture_overview():
|
||||
"""
|
||||
Komplett arkitekturöversikt
|
||||
"""
|
||||
return {
|
||||
"system_name": "Landvex Intelligence Fusion Engine (LIFE)",
|
||||
"version": "3.0.0",
|
||||
"tagline": "AI-drivet operativsystem för verklighetsintelligens",
|
||||
"description": "Genom att kontinuerligt integrera geospatiala data, crowdsourcade observationer, offentliga datakällor och maskininlärning skapar LIFE en levande, spårbar och förklarbar digital representation av den fysiska världen. Plattformen omvandlar observerbara signaler till evidens, evidens till kunskap och kunskap till beslutsunderlag.",
|
||||
"layers": [
|
||||
{"number": 1, "name": "Acquisition", "function": "Datainsamling", "sources": 10},
|
||||
{"number": 2, "name": "Normalization", "function": "Standardisering", "processes": 6},
|
||||
{"number": 3, "name": "Evidence", "function": "Evidenshantering", "components": 5},
|
||||
{"number": 4, "name": "Intelligence", "function": "AI-analys", "engines": 7},
|
||||
{"number": 5, "name": "Knowledge", "function": "Kunskapsbyggande", "graphs": 6},
|
||||
{"number": 6, "name": "Decision", "function": "Beslutsstöd", "interfaces": 7},
|
||||
],
|
||||
"core_blocks": [
|
||||
{"number": 7, "name": "Decision Engine", "function": "Rekommendationsgenerering"},
|
||||
{"number": 8, "name": "Learning Engine", "function": "Självförbättring"},
|
||||
{"number": 9, "name": "Integration Layer", "function": "Extern anslutning"},
|
||||
],
|
||||
"strategic_positioning": "LIFE är ett AI-drivet operativsystem för verklighetsintelligens. Biståndsanalys är en tillämpning, inte identitet. Samma motor kan användas för infrastruktur, miljö, försäkring, kommunal tillsyn, katastrofhantering, fastighetsförvaltning och andra områden där beslutsfattare behöver kontinuerlig, evidensbaserad lägesbild.",
|
||||
"applications": [
|
||||
"Development aid monitoring",
|
||||
"Infrastructure management",
|
||||
"Urban planning",
|
||||
"Environmental monitoring",
|
||||
"Insurance risk assessment",
|
||||
"Municipal supervision",
|
||||
"Disaster response",
|
||||
"Property management",
|
||||
"Supply chain tracking",
|
||||
"Security analysis",
|
||||
],
|
||||
"metrics": {
|
||||
"projects_monitored": 1250,
|
||||
"observations_processed": 45000,
|
||||
"data_sources": 10,
|
||||
"active_streams": 45,
|
||||
"ai_engines": 7,
|
||||
"knowledge_graphs": 6,
|
||||
"api_endpoints": 120,
|
||||
"daily_insights": 450,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
"""
|
||||
LIFE v7 — Reality Intelligence Ecosystem
|
||||
|
||||
Fokus: Adoption. Ett tekniskt ramverk blir en standard först när andra använder det.
|
||||
|
||||
Tre komponenter:
|
||||
1. Reference Implementations (öppna referensimplementationer)
|
||||
2. Conformance Program (certifiering)
|
||||
3. Public Specification (öppen specifikation)
|
||||
|
||||
Strategisk avgränsning:
|
||||
- RIS (Reality Intelligence Standard) — den öppna specifikationen
|
||||
- LIFE — referensimplementationen av standarden
|
||||
- Landvex — företaget som utvecklar och kommersialiserar LIFE
|
||||
|
||||
Product Definition:
|
||||
"Landvex utvecklar LIFE, en Reality Intelligence Platform och referensimplementation
|
||||
av Reality Intelligence Standard (RIS). Genom en gemensam informationsmodell,
|
||||
spårbar evidens och förklarbara analyser gör LIFE det möjligt att samla in,
|
||||
verifiera, dela och använda observationer från den fysiska världen på ett
|
||||
konsekvent och interoperabelt sätt över olika domäner och organisationer."
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from app.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/life-ecosystem", tags=["life_ecosystem"])
|
||||
|
||||
|
||||
# ─── COMPONENT 1: REFERENCE IMPLEMENTATIONS ───────────────────────────────────
|
||||
|
||||
@router.get("/public/reference-implementations")
|
||||
async def get_reference_implementations():
|
||||
"""
|
||||
Reference Implementations — Öppna referensimplementationer som sänker tröskeln
|
||||
för externa utvecklare.
|
||||
"""
|
||||
return {
|
||||
"component": 1,
|
||||
"name": "Reference Implementations",
|
||||
"description": "Öppna referensimplementationer av RIS som sänker tröskeln för externa utvecklare",
|
||||
"implementations": {
|
||||
"reference_server": {
|
||||
"name": "LIFE Reference Server",
|
||||
"description": "Komplett serverimplementation av RIS-004 (Reality Exchange Protocol)",
|
||||
"language": "Python (FastAPI)",
|
||||
"license": "Apache 2.0",
|
||||
"repository": "https://github.com/landvex/life-reference-server",
|
||||
"features": [
|
||||
"Full RIS-001 till RIS-006 implementation",
|
||||
"PostgreSQL + Neo4j + TimescaleDB",
|
||||
"JWT-autentisering",
|
||||
"Event-driven arkitektur",
|
||||
"Horisontell skalning",
|
||||
],
|
||||
"deployment": {
|
||||
"docker": "docker run landvex/life-reference-server",
|
||||
"kubernetes": "Helm chart tillgänglig",
|
||||
"aws": "CloudFormation template",
|
||||
"local": "docker-compose up",
|
||||
},
|
||||
"status": "Beta",
|
||||
"latest_version": "0.9.2",
|
||||
},
|
||||
"reference_client": {
|
||||
"name": "LIFE Reference Client",
|
||||
"description": "Exempelapplikation som visar hur man bygger ovanpå RIS",
|
||||
"language": "TypeScript / React",
|
||||
"license": "Apache 2.0",
|
||||
"repository": "https://github.com/landvex/life-reference-client",
|
||||
"features": [
|
||||
"Interaktiv karta med Reality Objects",
|
||||
"Observation submission",
|
||||
"Evidence viewer med confidence scores",
|
||||
"Decision support dashboard",
|
||||
"Collaboration workspace",
|
||||
],
|
||||
"deployment": {
|
||||
"static": "Byggs till statiska filer",
|
||||
"cdn": "Kan hostas på valfri CDN",
|
||||
},
|
||||
"status": "Alpha",
|
||||
"latest_version": "0.5.1",
|
||||
},
|
||||
"reference_sdk": {
|
||||
"name": "LIFE Reference SDK",
|
||||
"description": "SDK:er för olika programmeringsspråk",
|
||||
"languages": {
|
||||
"python": {
|
||||
"repository": "https://github.com/landvex/life-sdk-python",
|
||||
"package": "pip install life-sdk",
|
||||
"status": "Stable",
|
||||
"version": "1.2.0",
|
||||
},
|
||||
"javascript": {
|
||||
"repository": "https://github.com/landvex/life-sdk-js",
|
||||
"package": "npm install @landvex/life-sdk",
|
||||
"status": "Stable",
|
||||
"version": "1.1.0",
|
||||
},
|
||||
"java": {
|
||||
"repository": "https://github.com/landvex/life-sdk-java",
|
||||
"package": "Maven Central: ai.landvex:life-sdk",
|
||||
"status": "Beta",
|
||||
"version": "0.8.0",
|
||||
},
|
||||
"go": {
|
||||
"repository": "https://github.com/landvex/life-sdk-go",
|
||||
"package": "go get github.com/landvex/life-sdk-go",
|
||||
"status": "Beta",
|
||||
"version": "0.7.0",
|
||||
},
|
||||
"rust": {
|
||||
"repository": "https://github.com/landvex/life-sdk-rust",
|
||||
"package": "cargo add life-sdk",
|
||||
"status": "Alpha",
|
||||
"version": "0.4.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"example_data": {
|
||||
"name": "LIFE Example Data",
|
||||
"description": "Exempeldataset för att testa och lära sig RIS",
|
||||
"datasets": [
|
||||
{
|
||||
"name": "Somalia Infrastructure",
|
||||
"objects": 1250,
|
||||
"observations": 45000,
|
||||
"format": "RIS-001 / RIS-002",
|
||||
"download": "https://data.life.ai/examples/somalia-infrastructure.tar.gz",
|
||||
},
|
||||
{
|
||||
"name": "Sweden Road Network",
|
||||
"objects": 500000,
|
||||
"observations": 2000000,
|
||||
"format": "RIS-001 / RIS-002",
|
||||
"download": "https://data.life.ai/examples/sweden-roads.tar.gz",
|
||||
},
|
||||
{
|
||||
"name": "Global Climate Events",
|
||||
"objects": 10000,
|
||||
"observations": 500000,
|
||||
"format": "RIS-001 / RIS-002",
|
||||
"download": "https://data.life.ai/examples/climate-events.tar.gz",
|
||||
},
|
||||
],
|
||||
"license": "CC BY 4.0",
|
||||
},
|
||||
"example_apis": {
|
||||
"name": "LIFE Example APIs",
|
||||
"description": "Exempel på hur man bygger API:er ovanpå RIS",
|
||||
"examples": [
|
||||
{
|
||||
"name": "Municipal Infrastructure API",
|
||||
"description": "API för kommunal infrastrukturhantering",
|
||||
"repository": "https://github.com/landvex/examples/municipal-api",
|
||||
},
|
||||
{
|
||||
"name": "Insurance Risk Assessment API",
|
||||
"description": "API för försäkringsriskbedömning",
|
||||
"repository": "https://github.com/landvex/examples/insurance-api",
|
||||
},
|
||||
{
|
||||
"name": "Aid Project Monitoring API",
|
||||
"description": "API för biståndsprojektövervakning",
|
||||
"repository": "https://github.com/landvex/examples/aid-api",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"getting_started": {
|
||||
"quickstart": "https://docs.life.ai/quickstart",
|
||||
"tutorials": [
|
||||
"Build your first Reality Object",
|
||||
"Submit observations via API",
|
||||
"Build a custom domain module",
|
||||
"Deploy a LIFE instance",
|
||||
],
|
||||
"community": {
|
||||
"discord": "https://discord.gg/life-standard",
|
||||
"forum": "https://forum.life.ai",
|
||||
"github_discussions": "https://github.com/landvex/life-standard/discussions",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── COMPONENT 2: CONFORMANCE PROGRAM ─────────────────────────────────────────
|
||||
|
||||
@router.get("/public/conformance-program")
|
||||
async def get_conformance_program():
|
||||
"""
|
||||
Conformance Program — Certifieringsprogram för att ge tydlig kvalitetsnivå
|
||||
för integrationer.
|
||||
"""
|
||||
return {
|
||||
"component": 2,
|
||||
"name": "Conformance Program",
|
||||
"description": "Certifieringsprogram som ger tydlig kvalitetsnivå för RIS-integrationer",
|
||||
"levels": {
|
||||
"ris_compatible": {
|
||||
"name": "RIS Compatible",
|
||||
"badge": "🥉",
|
||||
"description": "Grundläggande kompatibilitet med RIS",
|
||||
"requirements": [
|
||||
"Implementerar RIS-001 (Reality Object Standard)",
|
||||
"Kan läsa och skriva RIS-002 (Evidence Standard)",
|
||||
"Använder RIS-004 (Reality Exchange Protocol) för kommunikation",
|
||||
"Dokumenterad API-specifikation",
|
||||
],
|
||||
"testing": [
|
||||
"Automatiska kompatibilitetstester",
|
||||
"Validering av objektstruktur",
|
||||
"API-kontraktsverifiering",
|
||||
],
|
||||
"validity": "12 månader",
|
||||
"renewal": "Automatisk vid passande tester",
|
||||
"fee": "Gratis",
|
||||
},
|
||||
"ris_certified": {
|
||||
"name": "RIS Certified",
|
||||
"badge": "🥈",
|
||||
"description": "Fullständig implementation av alla RIS-standarder",
|
||||
"requirements": [
|
||||
"Alla krav för RIS Compatible",
|
||||
"Implementerar RIS-003 (Confidence Standard)",
|
||||
"Implementerar RIS-005 (Decision Standard)",
|
||||
"Implementerar RIS-006 (Learning Standard)",
|
||||
"Mänsklig granskning av implementation",
|
||||
"Dokumenterad säkerhetsarkitektur",
|
||||
],
|
||||
"testing": [
|
||||
"Alla automatiska tester",
|
||||
"Säkerhetsgranskning",
|
||||
"Prestandatestning",
|
||||
"Interoperabilitetstestning med referensimplementation",
|
||||
],
|
||||
"validity": "12 månader",
|
||||
"renewal": "Omgranskning varje år",
|
||||
"fee": "$5,000 / år",
|
||||
},
|
||||
"ris_enterprise_certified": {
|
||||
"name": "RIS Enterprise Certified",
|
||||
"badge": "🥇",
|
||||
"description": "Enterprise-klar implementation med högsta kvalitetsnivå",
|
||||
"requirements": [
|
||||
"Alla krav för RIS Certified",
|
||||
"Implementerar alla Governance Layer-komponenter (v5)",
|
||||
"SOC 2 Type II-certifiering",
|
||||
"ISO 27001-certifiering",
|
||||
"24/7 supportavtal",
|
||||
"SLA-garantier",
|
||||
"Penetrationstestning",
|
||||
],
|
||||
"testing": [
|
||||
"Alla tester för RIS Certified",
|
||||
"Oberoende säkerhetsgranskning",
|
||||
"Lasttestning",
|
||||
"Disaster recovery-testning",
|
||||
"Compliance-granskning",
|
||||
],
|
||||
"validity": "12 månader",
|
||||
"renewal": "Omfattande årlig granskning",
|
||||
"fee": "$25,000 / år",
|
||||
"benefits": [
|
||||
"Listad på LIFE Marketplace",
|
||||
"Prioriterad teknisk support",
|
||||
"Tidig tillgång till nya standarder",
|
||||
"Deltagande i standardiseringsarbete",
|
||||
],
|
||||
},
|
||||
},
|
||||
"certification_process": {
|
||||
"steps": [
|
||||
{"step": 1, "name": "Self-assessment", "description": "Utför självbedömning med verktyg", "duration": "1-2 dagar"},
|
||||
{"step": 2, "name": "Application", "description": "Skicka in ansökan med dokumentation", "duration": "1 dag"},
|
||||
{"step": 3, "name": "Automated Testing", "description": "Kör automatiska kompatibilitetstester", "duration": "1-2 dagar"},
|
||||
{"step": 4, "name": "Review", "description": "Mänsklig granskning (för Certified+)", "duration": "1-2 veckor"},
|
||||
{"step": 5, "name": "Certification", "description": "Utfärdande av certifikat och badge", "duration": "1 dag"},
|
||||
],
|
||||
"tools": {
|
||||
"self_assessment": "https://tools.life.ai/conformance/self-assessment",
|
||||
"automated_testing": "https://tools.life.ai/conformance/testing",
|
||||
"documentation_template": "https://docs.life.ai/conformance/template",
|
||||
},
|
||||
},
|
||||
"certified_products": {
|
||||
"count": 12,
|
||||
"examples": [
|
||||
{"name": "MunicipalityX Infrastructure", "level": "RIS Enterprise Certified", "domain": "Cities"},
|
||||
{"name": "AidTracker Pro", "level": "RIS Certified", "domain": "Aid"},
|
||||
{"name": "RiskMap Analytics", "level": "RIS Certified", "domain": "Insurance"},
|
||||
{"name": "GreenWatch", "level": "RIS Compatible", "domain": "Environment"},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── COMPONENT 3: PUBLIC SPECIFICATION ────────────────────────────────────────
|
||||
|
||||
@router.get("/public/public-specification")
|
||||
async def get_public_specification():
|
||||
"""
|
||||
Public Specification — Öppen specifikation som gör att andra kan implementera
|
||||
standarden oberoende av LIFE-plattformen.
|
||||
"""
|
||||
return {
|
||||
"component": 3,
|
||||
"name": "Public Specification",
|
||||
"description": "Öppen specifikation som gör att andra kan implementera RIS oberoende av LIFE-plattformen",
|
||||
"specification": {
|
||||
"title": "Reality Intelligence Standard (RIS) Specification",
|
||||
"version": "1.0.0",
|
||||
"status": "Public Draft",
|
||||
"license": "CC BY 4.0",
|
||||
"url": "https://standard.life.ai/spec/v1.0.0",
|
||||
"download": {
|
||||
"pdf": "https://standard.life.ai/spec/v1.0.0.pdf",
|
||||
"html": "https://standard.life.ai/spec/v1.0.0.html",
|
||||
"markdown": "https://standard.life.ai/spec/v1.0.0.md",
|
||||
},
|
||||
},
|
||||
"contents": {
|
||||
"terminology": {
|
||||
"section": 1,
|
||||
"title": "Terminology",
|
||||
"description": "Definierade termer och begrepp",
|
||||
"terms": [
|
||||
{"term": "Reality Object", "definition": "En entitet i den fysiska världen som kan observeras och representeras digitalt"},
|
||||
{"term": "Evidence", "definition": "Data som stöder eller motsäger ett påstående om ett Reality Object"},
|
||||
{"term": "Confidence", "definition": "Graden av säkerhet i en bedömning, uttryckt som ett värde mellan 0 och 100"},
|
||||
{"term": "Observation", "definition": "En mätning eller iakttagelse av ett Reality Object vid en specifik tid och plats"},
|
||||
{"term": "Fusion", "definition": "Processen att kombinera data från flera källor till en enhetlig representation"},
|
||||
{"term": "Decision Object", "definition": "En strukturerad representation av ett beslutsstöd med observation, evidens, hypotes och rekommendation"},
|
||||
],
|
||||
},
|
||||
"object_model": {
|
||||
"section": 2,
|
||||
"title": "Object Model",
|
||||
"description": "RIS-001: Reality Object Standard",
|
||||
"specification_url": "https://standard.life.ai/spec/v1.0.0#object-model",
|
||||
},
|
||||
"api_contracts": {
|
||||
"section": 3,
|
||||
"title": "API Contracts",
|
||||
"description": "RIS-004: Reality Exchange Protocol",
|
||||
"endpoints": [
|
||||
{
|
||||
"path": "/objects",
|
||||
"methods": ["GET", "POST", "PUT", "DELETE"],
|
||||
"specification": "https://standard.life.ai/spec/v1.0.0#objects-endpoint",
|
||||
},
|
||||
{
|
||||
"path": "/observations",
|
||||
"methods": ["GET", "POST"],
|
||||
"specification": "https://standard.life.ai/spec/v1.0.0#observations-endpoint",
|
||||
},
|
||||
{
|
||||
"path": "/evidence",
|
||||
"methods": ["GET", "POST"],
|
||||
"specification": "https://standard.life.ai/spec/v1.0.0#evidence-endpoint",
|
||||
},
|
||||
{
|
||||
"path": "/relationships",
|
||||
"methods": ["GET", "POST", "DELETE"],
|
||||
"specification": "https://standard.life.ai/spec/v1.0.0#relationships-endpoint",
|
||||
},
|
||||
{
|
||||
"path": "/events",
|
||||
"methods": ["GET", "POST"],
|
||||
"specification": "https://standard.life.ai/spec/v1.0.0#events-endpoint",
|
||||
},
|
||||
{
|
||||
"path": "/hypotheses",
|
||||
"methods": ["GET", "POST"],
|
||||
"specification": "https://standard.life.ai/spec/v1.0.0#hypotheses-endpoint",
|
||||
},
|
||||
{
|
||||
"path": "/predictions",
|
||||
"methods": ["GET"],
|
||||
"specification": "https://standard.life.ai/spec/v1.0.0#predictions-endpoint",
|
||||
},
|
||||
{
|
||||
"path": "/actions",
|
||||
"methods": ["GET", "POST"],
|
||||
"specification": "https://standard.life.ai/spec/v1.0.0#actions-endpoint",
|
||||
},
|
||||
{
|
||||
"path": "/missions",
|
||||
"methods": ["GET", "POST", "PUT"],
|
||||
"specification": "https://standard.life.ai/spec/v1.0.0#missions-endpoint",
|
||||
},
|
||||
],
|
||||
},
|
||||
"evidence_model": {
|
||||
"section": 4,
|
||||
"title": "Evidence Model",
|
||||
"description": "RIS-002: Evidence Standard",
|
||||
"specification_url": "https://standard.life.ai/spec/v1.0.0#evidence-model",
|
||||
},
|
||||
"confidence_model": {
|
||||
"section": 5,
|
||||
"title": "Confidence Model",
|
||||
"description": "RIS-003: Confidence Standard",
|
||||
"specification_url": "https://standard.life.ai/spec/v1.0.0#confidence-model",
|
||||
},
|
||||
"decision_model": {
|
||||
"section": 6,
|
||||
"title": "Decision Model",
|
||||
"description": "RIS-005: Decision Standard",
|
||||
"specification_url": "https://standard.life.ai/spec/v1.0.0#decision-model",
|
||||
},
|
||||
"learning_model": {
|
||||
"section": 7,
|
||||
"title": "Learning Model",
|
||||
"description": "RIS-006: Learning Standard",
|
||||
"specification_url": "https://standard.life.ai/spec/v1.0.0#learning-model",
|
||||
},
|
||||
"examples": {
|
||||
"section": 8,
|
||||
"title": "Examples",
|
||||
"description": "Exempel på implementation och användning",
|
||||
"examples": [
|
||||
{"name": "Creating a Reality Object", "url": "https://standard.life.ai/examples/create-object"},
|
||||
{"name": "Submitting Evidence", "url": "https://standard.life.ai/examples/submit-evidence"},
|
||||
{"name": "Querying with Confidence", "url": "https://standard.life.ai/examples/query-confidence"},
|
||||
{"name": "Building a Decision", "url": "https://standard.life.ai/examples/build-decision"},
|
||||
],
|
||||
},
|
||||
"versioning_policy": {
|
||||
"section": 9,
|
||||
"title": "Versioning Policy",
|
||||
"description": "Hur standarden versionshanteras",
|
||||
"policy": {
|
||||
"semantic_versioning": "MAJOR.MINOR.PATCH",
|
||||
"major_changes": "Brytande förändringar, kräver migration",
|
||||
"minor_changes": "Nya funktioner, bakåtkompatibla",
|
||||
"patch_changes": "Buggfixar, bakåtkompatibla",
|
||||
"deprecation": "Funktioner markeras deprecated minst 1 major version innan borttagning",
|
||||
"support_window": "2 major versioner stöds samtidigt",
|
||||
},
|
||||
},
|
||||
"backward_compatibility": {
|
||||
"section": 10,
|
||||
"title": "Backward Compatibility",
|
||||
"description": "Garantier för bakåtkompatibilitet",
|
||||
"guarantees": [
|
||||
"Inga borttagna fält inom samma major version",
|
||||
"Nya fält är alltid valfria",
|
||||
"API-endpoints föråldras gradvis",
|
||||
"Migration guides vid major bumps",
|
||||
],
|
||||
},
|
||||
},
|
||||
"governance": {
|
||||
"standard_body": "Reality Intelligence Standards Board (RISB)",
|
||||
"membership": [
|
||||
{"organization": "Landvex", "role": "Founding Member", "seats": 2},
|
||||
{"organization": "Open Community", "role": "Elected Representatives", "seats": 3},
|
||||
{"organization": "Academic Institutions", "role": "Advisory", "seats": 2},
|
||||
{"organization": "Enterprise Partners", "role": "Advisory", "seats": 2},
|
||||
],
|
||||
"decision_process": "RFC-baserad process med öppen kommentarsperiod",
|
||||
"meeting_frequency": "Kvartalsvis",
|
||||
"public_minutes": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── STRATEGIC SEPARATION ─────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/strategic-separation")
|
||||
async def get_strategic_separation():
|
||||
"""
|
||||
Tydlig avgränsning mellan RIS, LIFE och Landvex
|
||||
"""
|
||||
return {
|
||||
"title": "Strategic Separation of Concerns",
|
||||
"description": "Tydlig avgränsning mellan standard, implementation och företag",
|
||||
"entities": {
|
||||
"ris": {
|
||||
"name": "Reality Intelligence Standard (RIS)",
|
||||
"type": "Open Standard",
|
||||
"analogy": "Som HTTP för webben",
|
||||
"ownership": "Community-governed (RISB)",
|
||||
"license": "CC BY 4.0",
|
||||
"cost": "Gratis att använda",
|
||||
"purpose": "Definiera HUR verklighetsintelligens representeras, valideras, utbyts och används",
|
||||
"scope": [
|
||||
"Objektmodell (RIS-001)",
|
||||
"Evidensstandard (RIS-002)",
|
||||
"Konfidensstandard (RIS-003)",
|
||||
"Utbytesprotokoll (RIS-004)",
|
||||
"Beslutsstandard (RIS-005)",
|
||||
"Lärandestandard (RIS-006)",
|
||||
"Gemensam ontologi",
|
||||
],
|
||||
"governance": "Reality Intelligence Standards Board",
|
||||
"development": "Öppen process med RFC:er",
|
||||
},
|
||||
"life": {
|
||||
"name": "Landvex Intelligence Fusion Engine (LIFE)",
|
||||
"type": "Reference Implementation",
|
||||
"analogy": "Som Apache HTTP Server för HTTP",
|
||||
"ownership": "Landvex (open source)",
|
||||
"license": "Apache 2.0",
|
||||
"cost": "Gratis att använda, betalt stöd tillgängligt",
|
||||
"purpose": "Referensimplementation av RIS som andra kan bygga vidare på",
|
||||
"scope": [
|
||||
"Komplett serverimplementation",
|
||||
"SDK:er för flera språk",
|
||||
"Webb-dashboard",
|
||||
"Mobile apps",
|
||||
"AI-motorer",
|
||||
"Datainsamlingsverktyg",
|
||||
],
|
||||
"governance": "Landvex med community contributions",
|
||||
"development": "Open source på GitHub",
|
||||
},
|
||||
"landvex": {
|
||||
"name": "Landvex AB",
|
||||
"type": "Commercial Company",
|
||||
"analogy": "Som Cloudflare för webben — bygger på öppna standarder men erbjuder kommersiella tjänster",
|
||||
"ownership": "Privat företag",
|
||||
"license": "Proprietär för kommersiella tjänster",
|
||||
"cost": "Betalda tjänster och support",
|
||||
"purpose": "Utveckla, kommersialisera och erbjuda tjänster ovanpå LIFE och RIS",
|
||||
"scope": [
|
||||
"LIFE Platform hosting (SaaS)",
|
||||
"Managed LIFE instances",
|
||||
"Professional support",
|
||||
"Custom development",
|
||||
"Training and certification",
|
||||
"QUIXZOOM Network",
|
||||
"Domain-specific modules",
|
||||
],
|
||||
"governance": "Företagsledning",
|
||||
"development": "Proprietär utveckling med öppna komponenter",
|
||||
},
|
||||
},
|
||||
"relationship": {
|
||||
"description": "Tre nivåer av öppenhet",
|
||||
"levels": [
|
||||
{
|
||||
"level": "Standard (RIS)",
|
||||
"openness": "Fullständigt öppen",
|
||||
"anyone_can": "Implementera, använda, modifiera",
|
||||
"cost": "Gratis",
|
||||
},
|
||||
{
|
||||
"level": "Implementation (LIFE)",
|
||||
"openness": "Open source",
|
||||
"anyone_can": "Använda, modifiera, distribuera",
|
||||
"cost": "Gratis (Apache 2.0)",
|
||||
},
|
||||
{
|
||||
"level": "Services (Landvex)",
|
||||
"openness": "Proprietär",
|
||||
"anyone_can": "Köpa tjänster",
|
||||
"cost": "Betalda tjänster",
|
||||
},
|
||||
],
|
||||
},
|
||||
"ecosystem_analogy": {
|
||||
"description": "Jämförelse med framgångsrika ekosystem",
|
||||
"examples": [
|
||||
{
|
||||
"standard": "HTTP / HTML / CSS",
|
||||
"implementation": "Apache, Nginx, Chrome, Firefox",
|
||||
"company": "Cloudflare, Fastly, Google",
|
||||
},
|
||||
{
|
||||
"standard": "Kubernetes (CNCF)",
|
||||
"implementation": "OpenShift, Rancher, EKS",
|
||||
"company": "Red Hat, SUSE, AWS",
|
||||
},
|
||||
{
|
||||
"standard": "OpenAPI",
|
||||
"implementation": "Swagger, Postman",
|
||||
"company": "SmartBear, Postman Inc",
|
||||
},
|
||||
{
|
||||
"standard": "RIS (Reality Intelligence Standard)",
|
||||
"implementation": "LIFE (Landvex Intelligence Fusion Engine)",
|
||||
"company": "Landvex AB",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── FINAL PRODUCT STORY ──────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/product-story")
|
||||
async def get_product_story():
|
||||
"""
|
||||
Slutlig produktberättelse
|
||||
"""
|
||||
return {
|
||||
"title": "Landvex LIFE — Reality Intelligence Platform",
|
||||
"tagline": "Reality Intelligence Standard (RIS) + Reference Implementation",
|
||||
"one_sentence": "Landvex utvecklar LIFE, en Reality Intelligence Platform och referensimplementation av Reality Intelligence Standard (RIS). Genom en gemensam informationsmodell, spårbar evidens och förklarbara analyser gör LIFE det möjligt att samla in, verifiera, dela och använda observationer från den fysiska världen på ett konsekvent och interoperabelt sätt över olika domäner och organisationer.",
|
||||
"evolution": {
|
||||
"v1": {"focus": "Aid Intelligence", "key": "Biståndsövervakning"},
|
||||
"v2": {"focus": "Multi-signal", "key": "Evidence Before Opinion"},
|
||||
"v3": {"focus": "Core Architecture", "key": "9 block, AI-operativsystem"},
|
||||
"v4": {"focus": "Platform", "key": "4 nivåer, SDK, Object Model"},
|
||||
"v5": {"focus": "Enterprise", "key": "5 pelare, governance, simulation"},
|
||||
"v6": {"focus": "Standard", "key": "6 RIS, ontologi, ekosystem"},
|
||||
"v7": {"focus": "Ecosystem", "key": "Adoption, reference implementations, conformance"},
|
||||
},
|
||||
"positioning": {
|
||||
"what_we_are": "Ett teknologiskt ramverk och öppen standard för Reality Intelligence",
|
||||
"what_we_enable": "Andra att bygga Reality Intelligence-lösningar",
|
||||
"what_makes_us_different": [
|
||||
"Reality Intelligence (inte bara text)",
|
||||
"Evidence Before Opinion (spårbarhet)",
|
||||
"Multi-source fusion (geospatial + fält + AI)",
|
||||
"Open standard (RIS)",
|
||||
"Reference implementation (LIFE)",
|
||||
"Ecosystem approach (adoption fokus)",
|
||||
],
|
||||
"target_users": [
|
||||
"Development agencies",
|
||||
"Governments",
|
||||
"Infrastructure operators",
|
||||
"Insurance companies",
|
||||
"Urban planners",
|
||||
"Environmental organizations",
|
||||
"Security agencies",
|
||||
"Researchers",
|
||||
"Technology partners",
|
||||
],
|
||||
},
|
||||
"business_model": {
|
||||
"open_components": [
|
||||
"RIS Standard (CC BY 4.0)",
|
||||
"LIFE Reference Server (Apache 2.0)",
|
||||
"LIFE SDK (Apache 2.0)",
|
||||
"Example Data (CC BY 4.0)",
|
||||
],
|
||||
"commercial_components": [
|
||||
"LIFE Platform SaaS",
|
||||
"Managed instances",
|
||||
"Professional support",
|
||||
"Custom development",
|
||||
"Training and certification",
|
||||
"QUIXZOOM Network access",
|
||||
],
|
||||
"revenue_streams": [
|
||||
"SaaS subscriptions",
|
||||
"Support contracts",
|
||||
"Professional services",
|
||||
"Certification fees",
|
||||
"Data services",
|
||||
],
|
||||
},
|
||||
"long_term_vision": "LIFE blir den dominerande standarden för hur organisationer representerar, validerar, utbyter och använder verklighetsintelligens — på samma sätt som HTTP blev standarden för webben.",
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
"""
|
||||
Landvex Intelligence Fusion Engine (LIFE)
|
||||
Continuous Multi-Source Intelligence Platform
|
||||
|
||||
Core Principle: Evidence Before Opinion
|
||||
- First gather observable signals
|
||||
- Then weigh independent sources
|
||||
- Identify patterns and statistical associations
|
||||
- Only then generate cautious hypotheses about possible causes
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from app.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/life", tags=["life_engine"])
|
||||
|
||||
|
||||
class EvidenceCategory(str, Enum):
|
||||
OFFICIAL_REPORTING = "official_reporting"
|
||||
SATELLITE = "satellite"
|
||||
QUIXZOOM = "quixzoom"
|
||||
NEWS = "news"
|
||||
OPEN_DATA = "open_data"
|
||||
HISTORICAL = "historical"
|
||||
ENVIRONMENTAL = "environmental"
|
||||
INFRASTRUCTURE = "infrastructure"
|
||||
TEMPORAL = "temporal"
|
||||
CORROBORATION = "corroboration"
|
||||
|
||||
|
||||
class ConfidenceLevel(str, Enum):
|
||||
VERY_HIGH = "very_high" # 90-100%
|
||||
HIGH = "high" # 75-89%
|
||||
MODERATE = "moderate" # 50-74%
|
||||
LOW = "low" # 25-49%
|
||||
VERY_LOW = "very_low" # 0-24%
|
||||
|
||||
|
||||
class EventType(str, Enum):
|
||||
NATURAL_DISASTER = "natural_disaster"
|
||||
CONFLICT = "conflict"
|
||||
INFRASTRUCTURE_FAILURE = "infrastructure_failure"
|
||||
POLITICAL_INSTABILITY = "political_instability"
|
||||
DISEASE_OUTBREAK = "disease_outbreak"
|
||||
CONSTRUCTION_DELAY = "construction_delay"
|
||||
ENVIRONMENTAL_INCIDENT = "environmental_incident"
|
||||
|
||||
|
||||
# ─── PHILOSOPHY ───────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/philosophy")
|
||||
async def get_life_philosophy():
|
||||
"""Evidence Before Opinion - core architecture principle."""
|
||||
return {
|
||||
"engine_name": "Landvex Intelligence Fusion Engine (LIFE)",
|
||||
"version": "1.0.0",
|
||||
"core_principle": "Evidence Before Opinion",
|
||||
"principle_description": "The system never starts with a conclusion and looks for support. Instead, every analysis builds from observable signals upward.",
|
||||
"process": [
|
||||
{
|
||||
"step": 1,
|
||||
"name": "Gather Facts",
|
||||
"description": "Collect observable signals from all available sources"
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"name": "Weigh Sources",
|
||||
"description": "Evaluate independent sources and their reliability"
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"name": "Identify Patterns",
|
||||
"description": "Detect statistical associations and correlations"
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"name": "Generate Hypotheses",
|
||||
"description": "Form cautious hypotheses about possible causes"
|
||||
},
|
||||
],
|
||||
"commitment": "Every insight answers: What evidence supports this? Which sources contributed? What observations disagree? How confident is the estimate?",
|
||||
}
|
||||
|
||||
|
||||
# ─── DATA SOURCES ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/data-sources")
|
||||
async def get_data_sources():
|
||||
"""All continuous data acquisition sources."""
|
||||
return {
|
||||
"categories": [
|
||||
{
|
||||
"name": "Official Project Information",
|
||||
"sources": [
|
||||
"Development agency project portals",
|
||||
"Government project databases",
|
||||
"Public procurement portals",
|
||||
"Budget publications",
|
||||
"Tender databases",
|
||||
"Evaluation reports",
|
||||
"Annual reports",
|
||||
"Audit reports",
|
||||
"Parliamentary documents",
|
||||
],
|
||||
"update_frequency": "Daily",
|
||||
"reliability": "High",
|
||||
},
|
||||
{
|
||||
"name": "News Intelligence",
|
||||
"sources": [
|
||||
"International news",
|
||||
"National news",
|
||||
"Regional news",
|
||||
"Local newspapers",
|
||||
"RSS feeds",
|
||||
"Development news",
|
||||
"Humanitarian news",
|
||||
"Infrastructure news",
|
||||
"Agriculture news",
|
||||
"Health news",
|
||||
"Education news",
|
||||
"Economic news",
|
||||
"Environmental news",
|
||||
"Security news",
|
||||
"Energy news",
|
||||
"Transportation news",
|
||||
],
|
||||
"update_frequency": "Real-time",
|
||||
"reliability": "Medium",
|
||||
},
|
||||
{
|
||||
"name": "Public Communications",
|
||||
"sources": [
|
||||
"Press releases",
|
||||
"Official websites",
|
||||
"Public blogs",
|
||||
"Open social media posts from institutions",
|
||||
"Project announcements",
|
||||
"Public progress updates",
|
||||
"Conference presentations",
|
||||
"Research publications",
|
||||
],
|
||||
"update_frequency": "Daily",
|
||||
"reliability": "Medium",
|
||||
},
|
||||
{
|
||||
"name": "Geospatial Intelligence",
|
||||
"sources": [
|
||||
"Satellite imagery (Sentinel-2, Landsat)",
|
||||
"Night light intensity",
|
||||
"Land cover change",
|
||||
"Road detection",
|
||||
"Construction detection",
|
||||
"Flood monitoring",
|
||||
"Drought monitoring",
|
||||
"Vegetation analysis",
|
||||
"River changes",
|
||||
"Urban expansion",
|
||||
"Environmental degradation",
|
||||
"Wildfire detection",
|
||||
"Infrastructure growth",
|
||||
],
|
||||
"update_frequency": "Every 5 days",
|
||||
"reliability": "Very High",
|
||||
},
|
||||
{
|
||||
"name": "QUIXZOOM Reality Network",
|
||||
"sources": [
|
||||
"Field observations",
|
||||
"Mission results",
|
||||
"Photographic evidence",
|
||||
"Video",
|
||||
"Audio",
|
||||
"GPS tracks",
|
||||
"Infrastructure inspections",
|
||||
"Community observations",
|
||||
"Temporal comparisons",
|
||||
],
|
||||
"update_frequency": "On-demand",
|
||||
"reliability": "High",
|
||||
},
|
||||
{
|
||||
"name": "Open Data",
|
||||
"sources": [
|
||||
"Population data",
|
||||
"Weather",
|
||||
"Climate",
|
||||
"Transportation",
|
||||
"Electricity",
|
||||
"Internet coverage",
|
||||
"Water availability",
|
||||
"Health indicators",
|
||||
"Education statistics",
|
||||
"Economic indicators",
|
||||
"Commodity prices",
|
||||
"Agricultural production",
|
||||
"Migration statistics",
|
||||
"Conflict datasets",
|
||||
],
|
||||
"update_frequency": "Weekly to Monthly",
|
||||
"reliability": "High",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ─── EVENT DETECTION ──────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/events")
|
||||
async def list_detected_events(
|
||||
project_id: Optional[str] = None,
|
||||
event_type: Optional[EventType] = None,
|
||||
country: Optional[str] = None,
|
||||
):
|
||||
"""Automatically detected meaningful events."""
|
||||
events = [
|
||||
{
|
||||
"id": "evt-001",
|
||||
"type": "natural_disaster",
|
||||
"subtype": "flood",
|
||||
"title": "Severe flooding in Mogadishu region",
|
||||
"description": "Heavy rainfall caused flooding affecting infrastructure accessibility",
|
||||
"location": {"country": "Somalia", "region": "Banadir", "lat": 2.0469, "lng": 45.3182},
|
||||
"date": "2026-06-15",
|
||||
"affected_projects": ["proj-001"],
|
||||
"severity": "high",
|
||||
"confidence": 92,
|
||||
"sources": ["Satellite", "News", "Open data"],
|
||||
"detected_at": "2026-06-16T08:00:00Z",
|
||||
},
|
||||
{
|
||||
"id": "evt-002",
|
||||
"type": "construction_delay",
|
||||
"subtype": "weather_related",
|
||||
"title": "Construction delay due to seasonal rainfall",
|
||||
"description": "Road rehabilitation project delayed due to extended rainy season",
|
||||
"location": {"country": "Nigeria", "region": "Lagos", "lat": 6.5244, "lng": 3.3792},
|
||||
"date": "2026-05-20",
|
||||
"affected_projects": ["proj-045"],
|
||||
"severity": "medium",
|
||||
"confidence": 78,
|
||||
"sources": ["Satellite", "Field observation", "Weather data"],
|
||||
"detected_at": "2026-05-25T10:00:00Z",
|
||||
},
|
||||
{
|
||||
"id": "evt-003",
|
||||
"type": "conflict",
|
||||
"subtype": "regional_insecurity",
|
||||
"title": "Regional insecurity affecting project operations",
|
||||
"description": "Increased security incidents in project area affecting staff access",
|
||||
"location": {"country": "Ethiopia", "region": "Tigray", "lat": 14.1628, "lng": 38.2906},
|
||||
"date": "2026-04-10",
|
||||
"affected_projects": ["proj-089"],
|
||||
"severity": "high",
|
||||
"confidence": 85,
|
||||
"sources": ["News", "Security reports", "Field observation"],
|
||||
"detected_at": "2026-04-12T14:00:00Z",
|
||||
},
|
||||
]
|
||||
|
||||
if country:
|
||||
events = [e for e in events if e["location"]["country"].lower() == country.lower()]
|
||||
if event_type:
|
||||
events = [e for e in events if e["type"] == event_type.value]
|
||||
|
||||
return {
|
||||
"events": events,
|
||||
"total": len(events),
|
||||
"detection_method": "Automated multi-source fusion with human verification",
|
||||
}
|
||||
|
||||
|
||||
# ─── CONTEXT GRAPH ────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/projects/{project_id}/context")
|
||||
async def get_project_context(project_id: str):
|
||||
"""Continuously updated contextual graph for a project."""
|
||||
return {
|
||||
"project_id": project_id,
|
||||
"context": {
|
||||
"political_environment": {
|
||||
"stability_score": 65,
|
||||
"confidence": 78,
|
||||
"factors": ["Democratic governance", "Regional tensions", "Election upcoming"],
|
||||
},
|
||||
"economic_conditions": {
|
||||
"gdp_growth": 4.2,
|
||||
"inflation": 8.5,
|
||||
"currency_stability": "moderate",
|
||||
"confidence": 82,
|
||||
},
|
||||
"climate": {
|
||||
"current_season": "rainy",
|
||||
"rainfall_vs_average": 120,
|
||||
"temperature_vs_average": 102,
|
||||
"drought_risk": "low",
|
||||
"flood_risk": "high",
|
||||
"confidence": 90,
|
||||
},
|
||||
"security": {
|
||||
"overall_risk": "medium",
|
||||
"recent_incidents": 3,
|
||||
"trend": "stable",
|
||||
"confidence": 75,
|
||||
},
|
||||
"infrastructure": {
|
||||
"road_accessibility": 70,
|
||||
"power_availability": 45,
|
||||
"internet_coverage": 60,
|
||||
"water_access": 55,
|
||||
"confidence": 80,
|
||||
},
|
||||
"population": {
|
||||
"local_population": 45000,
|
||||
"displacement": "low",
|
||||
"growth_rate": 2.8,
|
||||
"confidence": 85,
|
||||
},
|
||||
"nearby_projects": [
|
||||
{"id": "proj-015", "name": "Regional Health Clinic", "distance_km": 5.2},
|
||||
{"id": "proj-023", "name": "Road Improvement", "distance_km": 12.0},
|
||||
],
|
||||
"funding_ecosystem": {
|
||||
"total_aid_in_region_usd": 45000000,
|
||||
"major_donors": ["World Bank", "UNICEF", "GIZ"],
|
||||
"coordination_level": "moderate",
|
||||
},
|
||||
},
|
||||
"last_updated": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# ─── CAUSAL ANALYSIS ──────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/projects/{project_id}/causal-analysis")
|
||||
async def get_causal_analysis(project_id: str):
|
||||
"""Causal analysis layer - transparent statistical associations."""
|
||||
return {
|
||||
"project_id": project_id,
|
||||
"disclaimer": "These are statistical associations and model-generated hypotheses, not proven causation.",
|
||||
"observed_associations": [
|
||||
{
|
||||
"id": "assoc-001",
|
||||
"observation": "Reduced accessibility following severe flooding",
|
||||
"factors": [
|
||||
{"name": "Heavy rainfall", "contribution": 0.45, "confidence": 92},
|
||||
{"name": "Poor drainage infrastructure", "contribution": 0.30, "confidence": 78},
|
||||
{"name": "Road surface quality", "contribution": 0.25, "confidence": 65},
|
||||
],
|
||||
"statistical_strength": "strong",
|
||||
"evidence_sources": ["Satellite", "Weather data", "Field observation"],
|
||||
"hypothesis": "Flooding disproportionately affects projects in areas with inadequate drainage",
|
||||
"uncertainty": "Drainage quality is estimated from satellite and may not reflect recent improvements",
|
||||
},
|
||||
{
|
||||
"id": "assoc-002",
|
||||
"observation": "Construction delays associated with seasonal rainfall",
|
||||
"factors": [
|
||||
{"name": "Rainy season duration", "contribution": 0.60, "confidence": 85},
|
||||
{"name": "Soil conditions", "contribution": 0.25, "confidence": 70},
|
||||
{"name": "Equipment availability", "contribution": 0.15, "confidence": 55},
|
||||
],
|
||||
"statistical_strength": "moderate",
|
||||
"evidence_sources": ["Satellite time-series", "Weather data", "Project reports"],
|
||||
"hypothesis": "Projects in tropical regions experience predictable seasonal delays",
|
||||
"uncertainty": "Equipment availability data is limited",
|
||||
},
|
||||
],
|
||||
"distinguish": {
|
||||
"observed_facts": "Flooding occurred on June 15, accessibility reduced by 40%",
|
||||
"statistical_associations": "Projects in flood-prone areas 3x more likely to experience delays",
|
||||
"model_hypotheses": "Drainage investment may reduce weather-related delays by 50%",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── MULTI-SIGNAL CONFIDENCE ─────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/projects/{project_id}/confidence")
|
||||
async def get_confidence_score(project_id: str):
|
||||
"""Multi-signal confidence engine combining evidence from multiple sources."""
|
||||
return {
|
||||
"project_id": project_id,
|
||||
"overall_confidence": 82,
|
||||
"confidence_level": "high",
|
||||
"evidence_breakdown": [
|
||||
{
|
||||
"category": "official_reporting",
|
||||
"source": "UNICEF quarterly report",
|
||||
"contribution": 15,
|
||||
"weight": 0.15,
|
||||
"reliability": 85,
|
||||
"agreement_with_others": 72,
|
||||
},
|
||||
{
|
||||
"category": "satellite",
|
||||
"source": "Sentinel-2 imagery",
|
||||
"contribution": 25,
|
||||
"weight": 0.25,
|
||||
"reliability": 95,
|
||||
"agreement_with_others": 88,
|
||||
},
|
||||
{
|
||||
"category": "quixzoom",
|
||||
"source": "Field observations (12 contributors)",
|
||||
"contribution": 25,
|
||||
"weight": 0.25,
|
||||
"reliability": 88,
|
||||
"agreement_with_others": 85,
|
||||
},
|
||||
{
|
||||
"category": "news",
|
||||
"source": "Regional news monitoring",
|
||||
"contribution": 10,
|
||||
"weight": 0.10,
|
||||
"reliability": 60,
|
||||
"agreement_with_others": 65,
|
||||
},
|
||||
{
|
||||
"category": "open_data",
|
||||
"source": "Weather, population, economic data",
|
||||
"contribution": 15,
|
||||
"weight": 0.15,
|
||||
"reliability": 90,
|
||||
"agreement_with_others": 80,
|
||||
},
|
||||
{
|
||||
"category": "historical",
|
||||
"source": "Similar projects in region",
|
||||
"contribution": 10,
|
||||
"weight": 0.10,
|
||||
"reliability": 75,
|
||||
"agreement_with_others": 70,
|
||||
},
|
||||
],
|
||||
"convergence_analysis": {
|
||||
"sources_agree": 5,
|
||||
"sources_disagree": 1,
|
||||
"primary_disagreement": "Official reporting shows 95% completion vs observed 85%",
|
||||
"confidence_increase": "Multiple independent sources converging on 82-88% completion",
|
||||
},
|
||||
"methodology": "Confidence increases as multiple independent sources converge. Disagreement reduces confidence but highlights areas for additional verification.",
|
||||
}
|
||||
|
||||
|
||||
# ─── PREDICTIVE INTELLIGENCE ─────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/projects/{project_id}/predictions")
|
||||
async def get_predictions(project_id: str):
|
||||
"""Predictive intelligence with uncertainty intervals."""
|
||||
return {
|
||||
"project_id": project_id,
|
||||
"disclaimer": "Predictions are estimates based on current evidence and historical patterns. They are not certainties.",
|
||||
"predictions": [
|
||||
{
|
||||
"id": "pred-001",
|
||||
"type": "completion_date",
|
||||
"prediction": "Project will complete by August 15, 2026",
|
||||
"confidence": 72,
|
||||
"uncertainty_interval": "August 1 - August 30, 2026",
|
||||
"factors": [
|
||||
{"factor": "Current progress rate", "impact": "positive", "strength": 0.7},
|
||||
{"factor": "Seasonal weather forecast", "impact": "negative", "strength": 0.3},
|
||||
{"factor": "Resource availability", "impact": "neutral", "strength": 0.5},
|
||||
],
|
||||
"assumptions": [
|
||||
"No major weather disruptions",
|
||||
"Funding continues as planned",
|
||||
"Material supply remains stable",
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "pred-002",
|
||||
"type": "operational_sustainability",
|
||||
"prediction": "75% probability of sustained operation for 3+ years",
|
||||
"confidence": 68,
|
||||
"uncertainty_interval": "65-85%",
|
||||
"factors": [
|
||||
{"factor": "Maintenance plan quality", "impact": "positive", "strength": 0.8},
|
||||
{"factor": "Local capacity", "impact": "positive", "strength": 0.6},
|
||||
{"factor": "Funding continuity", "impact": "uncertain", "strength": 0.4},
|
||||
],
|
||||
"assumptions": [
|
||||
"Local government maintains commitment",
|
||||
"No major economic shocks",
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "pred-003",
|
||||
"type": "risk_assessment",
|
||||
"prediction": "Medium risk of 2-4 week delay due to rainy season",
|
||||
"confidence": 78,
|
||||
"uncertainty_interval": "Low-High",
|
||||
"factors": [
|
||||
{"factor": "Historical weather patterns", "impact": "negative", "strength": 0.8},
|
||||
{"factor": "Drainage infrastructure", "impact": "positive", "strength": 0.5},
|
||||
{"factor": "Contingency planning", "impact": "positive", "strength": 0.6},
|
||||
],
|
||||
"recommended_actions": [
|
||||
"Accelerate indoor work before rainy season",
|
||||
"Pre-position materials on-site",
|
||||
"Develop drainage improvement plan",
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ─── EXPLAINABILITY ───────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/insights/{insight_id}/explain")
|
||||
async def explain_insight(insight_id: str):
|
||||
"""Every insight answers: What evidence? Which sources? What disagrees?"""
|
||||
return {
|
||||
"insight_id": insight_id,
|
||||
"explainability": {
|
||||
"what_evidence_supports_this": [
|
||||
"12 field observations from 8 independent contributors",
|
||||
"Sentinel-2 satellite imagery from 6 dates",
|
||||
"Official quarterly report (June 2026)",
|
||||
"Regional weather data showing 120% average rainfall",
|
||||
],
|
||||
"which_sources_contributed": [
|
||||
{"source": "QUIXZOOM field observations", "weight": 0.30, "reliability": 88},
|
||||
{"source": "Satellite imagery", "weight": 0.25, "reliability": 95},
|
||||
{"source": "Official reporting", "weight": 0.20, "reliability": 85},
|
||||
{"source": "Open weather data", "weight": 0.15, "reliability": 90},
|
||||
{"source": "News monitoring", "weight": 0.10, "reliability": 60},
|
||||
],
|
||||
"what_observations_disagree": [
|
||||
{
|
||||
"observation": "Official report claims 95% completion",
|
||||
"conflicting_evidence": "Field and satellite observations show 82-88%",
|
||||
"possible_explanations": [
|
||||
"Different measurement methodologies",
|
||||
"Reporting date vs observation date lag",
|
||||
"Interior work not visible from exterior",
|
||||
],
|
||||
},
|
||||
],
|
||||
"how_confident_is_the_estimate": {
|
||||
"overall_confidence": 82,
|
||||
"confidence_level": "high",
|
||||
"primary_uncertainties": [
|
||||
"Interior completion percentage is estimated",
|
||||
"Official reporting may use different metrics",
|
||||
],
|
||||
},
|
||||
"what_assumptions_influence_the_model": [
|
||||
"Field observations are representative of overall progress",
|
||||
"Satellite imagery accurately reflects ground conditions",
|
||||
"Weather patterns follow historical trends",
|
||||
],
|
||||
"what_additional_evidence_would_increase_confidence": [
|
||||
"Interior photographs from multiple angles",
|
||||
"Construction timeline from contractor",
|
||||
"Material delivery receipts",
|
||||
"Independent engineering assessment",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── DASHBOARD ────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/dashboard")
|
||||
async def get_life_dashboard(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""LIFE admin dashboard."""
|
||||
return {
|
||||
"engine": "Landvex Intelligence Fusion Engine",
|
||||
"version": "1.0.0",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"data_sources": {
|
||||
"total": 6,
|
||||
"active": 6,
|
||||
"streams_monitored": 45,
|
||||
},
|
||||
"projects": {
|
||||
"monitored": 1250,
|
||||
"with_context_graph": 1250,
|
||||
"with_causal_analysis": 890,
|
||||
"with_predictions": 1250,
|
||||
},
|
||||
"events_detected": {
|
||||
"last_24h": 12,
|
||||
"last_7d": 89,
|
||||
"last_30d": 340,
|
||||
"by_type": {
|
||||
"natural_disaster": 45,
|
||||
"conflict": 23,
|
||||
"infrastructure_failure": 67,
|
||||
"construction_delay": 120,
|
||||
"political_instability": 15,
|
||||
"environmental_incident": 70,
|
||||
},
|
||||
},
|
||||
"confidence_distribution": {
|
||||
"very_high": 320,
|
||||
"high": 580,
|
||||
"moderate": 280,
|
||||
"low": 55,
|
||||
"very_low": 15,
|
||||
},
|
||||
"recent_fusion_results": [
|
||||
{
|
||||
"project": "Mogadishu Primary School",
|
||||
"insight": "Construction delay likely due to flooding + poor drainage",
|
||||
"confidence": 78,
|
||||
"sources_fused": 5,
|
||||
},
|
||||
{
|
||||
"project": "Kampala Water Supply",
|
||||
"insight": "Operational sustainability high based on maintenance records + community feedback",
|
||||
"confidence": 88,
|
||||
"sources_fused": 6,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
"""
|
||||
Landvex Intelligence Fusion Engine (LIFE) v2
|
||||
Operativ intelligensmotor med 7 kärnkomponenter:
|
||||
1. Evidence Graph (EG)
|
||||
2. Evidence Lineage
|
||||
3. Temporal Reality Engine
|
||||
4. Contradiction Knowledge Base
|
||||
5. Reality DNA
|
||||
6. Reality Memory
|
||||
7. Reality Hypothesis Engine
|
||||
|
||||
Core Principle: Evidence Before Opinion
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
|
||||
from app.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/life-v2", tags=["life_engine_v2"])
|
||||
|
||||
|
||||
# ─── 1. EVIDENCE GRAPH (EG) ──────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/evidence-graph/{project_id}")
|
||||
async def get_evidence_graph(project_id: str):
|
||||
"""
|
||||
Evidence Graph - Varje observation är en nod i en graf.
|
||||
Observation → Object → Project → Programme → Organisation → Region → Country
|
||||
"""
|
||||
return {
|
||||
"project_id": project_id,
|
||||
"graph_structure": {
|
||||
"description": "Hierarchical evidence graph where every observation is a traceable node",
|
||||
"levels": [
|
||||
{
|
||||
"level": "Observation",
|
||||
"description": "Individual data points (photos, measurements, reports)",
|
||||
"example": "Satellite image showing construction progress on 2026-07-01",
|
||||
"count": 156,
|
||||
},
|
||||
{
|
||||
"level": "Object",
|
||||
"description": "Physical entities being observed",
|
||||
"example": "School building, water pump, road segment",
|
||||
"count": 12,
|
||||
},
|
||||
{
|
||||
"level": "Project",
|
||||
"description": "Development project being monitored",
|
||||
"example": "Mogadishu Primary School Renovation",
|
||||
"count": 1,
|
||||
},
|
||||
{
|
||||
"level": "Programme",
|
||||
"description": "Funding programme or initiative",
|
||||
"example": "UNICEF Education Programme 2025-2027",
|
||||
"count": 1,
|
||||
},
|
||||
{
|
||||
"level": "Organisation",
|
||||
"description": "Implementing or funding organization",
|
||||
"example": "UNICEF, Somali Education Consortium",
|
||||
"count": 2,
|
||||
},
|
||||
{
|
||||
"level": "Region",
|
||||
"description": "Geographic region",
|
||||
"example": "East Africa, Banadir Region",
|
||||
"count": 2,
|
||||
},
|
||||
{
|
||||
"level": "Country",
|
||||
"description": "National level",
|
||||
"example": "Somalia",
|
||||
"count": 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
"example_trace": {
|
||||
"conclusion": "Construction is 85% complete",
|
||||
"evidence_chain": [
|
||||
{
|
||||
"node_type": "Observation",
|
||||
"id": "obs-001",
|
||||
"data": "Field photo showing exterior walls complete",
|
||||
"source": "QUIXZOOM contributor C-123",
|
||||
"confidence": 92,
|
||||
},
|
||||
{
|
||||
"node_type": "Observation",
|
||||
"id": "obs-002",
|
||||
"data": "Satellite imagery shows roof structure present",
|
||||
"source": "Sentinel-2",
|
||||
"confidence": 95,
|
||||
},
|
||||
{
|
||||
"node_type": "Object",
|
||||
"id": "obj-001",
|
||||
"name": "School Building Block A",
|
||||
"aggregated_confidence": 93,
|
||||
},
|
||||
{
|
||||
"node_type": "Project",
|
||||
"id": project_id,
|
||||
"name": "Mogadishu Primary School Renovation",
|
||||
"aggregated_confidence": 85,
|
||||
},
|
||||
],
|
||||
},
|
||||
"query_capabilities": [
|
||||
"Trace any conclusion back to original observations",
|
||||
"Find all observations supporting a specific claim",
|
||||
"Identify evidence gaps at any level",
|
||||
"Compare confidence across different branches",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ─── 2. EVIDENCE LINEAGE ─────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/evidence-lineage/{observation_id}")
|
||||
async def get_evidence_lineage(observation_id: str):
|
||||
"""
|
||||
Evidence Lineage - Komplett ursprung för varje datapunkt.
|
||||
Source → Collected → Validated → Cross-validated → AI confidence → Human verification → Historical revisions → Current confidence
|
||||
"""
|
||||
return {
|
||||
"observation_id": observation_id,
|
||||
"lineage": {
|
||||
"source": {
|
||||
"type": "QUIXZOOM field observation",
|
||||
"contributor_id": "C-123",
|
||||
"contributor_reputation": 94,
|
||||
"collection_device": "iPhone 14 Pro",
|
||||
"gps_accuracy": "±3 meters",
|
||||
"timestamp": "2026-07-03T14:30:00Z",
|
||||
},
|
||||
"collected": {
|
||||
"raw_data": "Original image + metadata",
|
||||
"checksum": "sha256:a1b2c3...",
|
||||
"collection_conditions": "Daylight, clear weather",
|
||||
"collector_notes": "Building exterior appears complete, interior work visible through windows",
|
||||
},
|
||||
"validated": {
|
||||
"ai_validation": {
|
||||
"model": "Landvex Construction Progress v2.1",
|
||||
"confidence": 88,
|
||||
"detected_features": ["walls", "roof", "windows", "scaffolding"],
|
||||
},
|
||||
"geospatial_validation": {
|
||||
"matches_expected_location": True,
|
||||
"location_deviation": "0.5 meters",
|
||||
},
|
||||
"temporal_validation": {
|
||||
"timestamp_verified": True,
|
||||
"sequence_consistent": True,
|
||||
},
|
||||
},
|
||||
"cross_validated": {
|
||||
"supporting_observations": [
|
||||
{"id": "obs-003", "source": "Satellite", "agreement": 95},
|
||||
{"id": "obs-004", "source": "Another contributor", "agreement": 82},
|
||||
],
|
||||
"conflicting_observations": [],
|
||||
"cross_validation_score": 91,
|
||||
},
|
||||
"ai_confidence": {
|
||||
"initial_score": 85,
|
||||
"after_validation": 88,
|
||||
"after_cross_validation": 91,
|
||||
"factors": [
|
||||
{"factor": "Image quality", "impact": +3},
|
||||
{"factor": "Multiple angles available", "impact": +5},
|
||||
{"factor": "Limited interior visibility", "impact": -2},
|
||||
],
|
||||
},
|
||||
"human_verification": {
|
||||
"verified_by": "Analyst-007",
|
||||
"verification_date": "2026-07-04",
|
||||
"verification_method": "Visual inspection + cross-reference with satellite",
|
||||
"human_confidence": 90,
|
||||
"notes": "Exterior completion confirmed. Interior estimate based on visible progress.",
|
||||
},
|
||||
"historical_revisions": [
|
||||
{
|
||||
"date": "2026-07-03",
|
||||
"confidence": 85,
|
||||
"change": "Initial upload",
|
||||
},
|
||||
{
|
||||
"date": "2026-07-04",
|
||||
"confidence": 88,
|
||||
"change": "AI validation complete",
|
||||
},
|
||||
{
|
||||
"date": "2026-07-04",
|
||||
"confidence": 91,
|
||||
"change": "Cross-validation with satellite complete",
|
||||
},
|
||||
],
|
||||
"current_confidence": {
|
||||
"score": 91,
|
||||
"level": "high",
|
||||
"last_updated": "2026-07-04T10:00:00Z",
|
||||
"next_review": "2026-07-11",
|
||||
},
|
||||
},
|
||||
"value": "When someone questions an analysis, show exactly how this observation was collected, validated, and verified.",
|
||||
}
|
||||
|
||||
|
||||
# ─── 3. TEMPORAL REALITY ENGINE ──────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/temporal/{project_id}")
|
||||
async def get_temporal_reality(project_id: str):
|
||||
"""
|
||||
Temporal Reality Engine - Tidsmaskin för att se hur verkligheten förändras.
|
||||
"""
|
||||
return {
|
||||
"project_id": project_id,
|
||||
"temporal_capabilities": [
|
||||
"What did the area look like six months ago?",
|
||||
"What has changed?",
|
||||
"How fast?",
|
||||
"When did the change begin?",
|
||||
],
|
||||
"timeline": [
|
||||
{
|
||||
"date": "2025-09-01",
|
||||
"description": "Project start",
|
||||
"satellite_image": "https://cdn.landvex.com/sat/2025-09-01/proj-001.jpg",
|
||||
"observations": 0,
|
||||
"ai_analysis": "Bare ground, no construction activity",
|
||||
"confidence": 95,
|
||||
},
|
||||
{
|
||||
"date": "2025-12-01",
|
||||
"description": "Foundation complete",
|
||||
"satellite_image": "https://cdn.landvex.com/sat/2025-12-01/proj-001.jpg",
|
||||
"observations": 12,
|
||||
"ai_analysis": "Foundation visible, materials on site",
|
||||
"confidence": 92,
|
||||
},
|
||||
{
|
||||
"date": "2026-03-01",
|
||||
"description": "Structure rising",
|
||||
"satellite_image": "https://cdn.landvex.com/sat/2026-03-01/proj-001.jpg",
|
||||
"observations": 45,
|
||||
"ai_analysis": "Walls visible, roof framework started",
|
||||
"confidence": 90,
|
||||
},
|
||||
{
|
||||
"date": "2026-06-01",
|
||||
"description": "Roof complete",
|
||||
"satellite_image": "https://cdn.landvex.com/sat/2026-06-01/proj-001.jpg",
|
||||
"observations": 89,
|
||||
"ai_analysis": "Roof complete, exterior walls finished",
|
||||
"confidence": 93,
|
||||
},
|
||||
{
|
||||
"date": "2026-07-03",
|
||||
"description": "Current state",
|
||||
"satellite_image": "https://cdn.landvex.com/sat/2026-07-03/proj-001.jpg",
|
||||
"observations": 156,
|
||||
"ai_analysis": "Exterior 95% complete, interior work ongoing",
|
||||
"confidence": 88,
|
||||
},
|
||||
],
|
||||
"change_detection": {
|
||||
"total_changes_detected": 23,
|
||||
"significant_changes": [
|
||||
{
|
||||
"date": "2025-12-15",
|
||||
"type": "construction_start",
|
||||
"description": "First visible construction activity",
|
||||
"confidence": 95,
|
||||
},
|
||||
{
|
||||
"date": "2026-02-20",
|
||||
"type": "acceleration",
|
||||
"description": "Construction pace increased 40%",
|
||||
"confidence": 82,
|
||||
},
|
||||
{
|
||||
"date": "2026-06-15",
|
||||
"type": "flooding_impact",
|
||||
"description": "Accessibility reduced due to flooding",
|
||||
"confidence": 92,
|
||||
},
|
||||
],
|
||||
},
|
||||
"temporal_queries": {
|
||||
"what_did_it_look_like_6_months_ago": {
|
||||
"date": "2026-01-03",
|
||||
"description": "Foundation complete, walls starting to rise",
|
||||
"satellite_image": "https://cdn.landvex.com/sat/2026-01-03/proj-001.jpg",
|
||||
},
|
||||
"what_has_changed": {
|
||||
"changes": [
|
||||
"Walls completed (0% → 100%)",
|
||||
"Roof added (0% → 100%)",
|
||||
"Windows installed (0% → 80%)",
|
||||
"Interior work started (0% → 60%)",
|
||||
],
|
||||
"time_period": "6 months",
|
||||
},
|
||||
"how_fast": {
|
||||
"average_progress_per_month": "12%",
|
||||
"fastest_month": "March 2026 (18%)",
|
||||
"slowest_month": "June 2026 (5%, due to flooding)",
|
||||
},
|
||||
"when_did_change_begin": {
|
||||
"construction_start": "2025-12-10",
|
||||
"acceleration": "2026-02-15",
|
||||
"flooding_impact": "2026-06-15",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── 4. CONTRADICTION KNOWLEDGE BASE ─────────────────────────────────────────
|
||||
|
||||
@router.get("/public/contradictions")
|
||||
async def get_contradiction_knowledge_base():
|
||||
"""
|
||||
Contradiction Knowledge Base - Alla identifierade avvikelser lagras för AI-träning.
|
||||
"""
|
||||
return {
|
||||
"description": "All identified discrepancies are stored to train AI on real-world project development patterns",
|
||||
"total_contradictions": 1247,
|
||||
"categories": {
|
||||
"schedule_variance": 342,
|
||||
"progress_reporting": 289,
|
||||
"operational_status": 198,
|
||||
"budget_execution": 156,
|
||||
"maintenance_gap": 134,
|
||||
"location_discrepancy": 67,
|
||||
"other": 61,
|
||||
},
|
||||
"example_entries": [
|
||||
{
|
||||
"id": "contr-001",
|
||||
"type": "schedule_variance",
|
||||
"project": "Mogadishu Primary School",
|
||||
"official_report": "95% complete (June 2026)",
|
||||
"satellite": "88% complete (July 2026)",
|
||||
"quixzoom": "85% complete (July 2026)",
|
||||
"news": "No relevant reports",
|
||||
"public_procurement": "Materials delivered through June",
|
||||
"confidence": 78,
|
||||
"resolution": "Pending follow-up observation",
|
||||
"lessons_learned": "Interior work often underreported in official progress",
|
||||
},
|
||||
{
|
||||
"id": "contr-002",
|
||||
"type": "operational_status",
|
||||
"project": "Nairobi Health Clinic",
|
||||
"official_report": "Fully operational",
|
||||
"satellite": "Activity detected",
|
||||
"quixzoom": "Reduced hours observed, staff shortage",
|
||||
"news": "Healthcare worker strike reported",
|
||||
"public_procurement": "No recent medical supply contracts",
|
||||
"confidence": 85,
|
||||
"resolution": "Partial - strike ended, staffing still reduced",
|
||||
"lessons_learned": "Operational status should include staffing levels",
|
||||
},
|
||||
],
|
||||
"ai_training_value": {
|
||||
"description": "After several years, AI can be trained on hundreds of thousands of real-world examples",
|
||||
"current_training_set": 1247,
|
||||
"projected_2027": 5000,
|
||||
"projected_2028": 15000,
|
||||
"use_cases": [
|
||||
"Predict likely discrepancy types by project category",
|
||||
"Identify early warning signals",
|
||||
"Benchmark reporting accuracy by organization type",
|
||||
"Improve confidence scoring models",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── 5. REALITY DNA ──────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/reality-dna/{project_id}")
|
||||
async def get_reality_dna(project_id: str):
|
||||
"""
|
||||
Reality DNA - Projektets fingeravtryck som kan jämföras med liknande projekt.
|
||||
"""
|
||||
return {
|
||||
"project_id": project_id,
|
||||
"dna_profile": {
|
||||
"description": "Unique fingerprint of project characteristics for benchmarking",
|
||||
"dimensions": [
|
||||
{
|
||||
"name": "Activity",
|
||||
"score": 78,
|
||||
"description": "Level of observable activity",
|
||||
"indicators": ["Worker presence", "Equipment operation", "Material delivery"],
|
||||
},
|
||||
{
|
||||
"name": "Maintenance",
|
||||
"score": 65,
|
||||
"description": "Observable maintenance quality",
|
||||
"indicators": ["Physical condition", "Repair frequency", "Upkeep standards"],
|
||||
},
|
||||
{
|
||||
"name": "Infrastructure",
|
||||
"score": 82,
|
||||
"description": "Infrastructure completeness and quality",
|
||||
"indicators": ["Construction progress", "Material quality", "Specification compliance"],
|
||||
},
|
||||
{
|
||||
"name": "Community Usage",
|
||||
"score": 70,
|
||||
"description": "Actual utilization by community",
|
||||
"indicators": ["Visitor counts", "Usage patterns", "User feedback"],
|
||||
},
|
||||
{
|
||||
"name": "Traffic",
|
||||
"score": 75,
|
||||
"description": "Transportation and accessibility",
|
||||
"indicators": ["Road condition", "Transport frequency", "Accessibility"],
|
||||
},
|
||||
{
|
||||
"name": "Economic Activity",
|
||||
"score": 60,
|
||||
"description": "Economic enablement",
|
||||
"indicators": ["Local employment", "Market activity", "Income effects"],
|
||||
},
|
||||
{
|
||||
"name": "Environmental Status",
|
||||
"score": 72,
|
||||
"description": "Environmental impact",
|
||||
"indicators": ["Resource efficiency", "Pollution", "Ecosystem health"],
|
||||
},
|
||||
{
|
||||
"name": "Safety",
|
||||
"score": 80,
|
||||
"description": "Observable safety conditions",
|
||||
"indicators": ["Structural integrity", "Safety equipment", "Hazard presence"],
|
||||
},
|
||||
{
|
||||
"name": "Operational Continuity",
|
||||
"score": 68,
|
||||
"description": "Uninterrupted service delivery",
|
||||
"indicators": ["Uptime", "Service consistency", "Disruption frequency"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"benchmarking": {
|
||||
"similar_projects": [
|
||||
{
|
||||
"project_id": "proj-089",
|
||||
"name": "Kismayo School Construction",
|
||||
"similarity_score": 87,
|
||||
"comparison": {
|
||||
"activity": "+5%",
|
||||
"maintenance": "-3%",
|
||||
"infrastructure": "+2%",
|
||||
"community_usage": "+8%",
|
||||
},
|
||||
},
|
||||
{
|
||||
"project_id": "proj-156",
|
||||
"name": "Garowe Education Center",
|
||||
"similarity_score": 82,
|
||||
"comparison": {
|
||||
"activity": "-2%",
|
||||
"maintenance": "+4%",
|
||||
"infrastructure": "-1%",
|
||||
"community_usage": "+5%",
|
||||
},
|
||||
},
|
||||
],
|
||||
"category_average": {
|
||||
"activity": 75,
|
||||
"maintenance": 68,
|
||||
"infrastructure": 80,
|
||||
"community_usage": 72,
|
||||
"traffic": 73,
|
||||
"economic_activity": 65,
|
||||
"environmental_status": 70,
|
||||
"safety": 78,
|
||||
"operational_continuity": 70,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── 6. REALITY MEMORY ───────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/reality-memory/{project_id}")
|
||||
async def get_reality_memory(project_id: str):
|
||||
"""
|
||||
Reality Memory - LIFE kastar aldrig bort något.
|
||||
Varje observation, AI-bedömning och förändring sparas.
|
||||
"""
|
||||
return {
|
||||
"project_id": project_id,
|
||||
"memory_principles": [
|
||||
"Every observation is preserved",
|
||||
"Every AI judgment is preserved",
|
||||
"Every change is preserved",
|
||||
"Full reproducibility at any point in time",
|
||||
],
|
||||
"storage_stats": {
|
||||
"total_observations": 45000,
|
||||
"total_ai_judgments": 12500,
|
||||
"total_changes_detected": 8900,
|
||||
"total_revisions": 23400,
|
||||
"storage_size_tb": 2.4,
|
||||
"retention_period": "Permanent",
|
||||
},
|
||||
"time_travel": {
|
||||
"description": "Reconstruct how the system reasoned at any point in time",
|
||||
"example_queries": [
|
||||
{
|
||||
"query": "What did we know on March 15, 2026?",
|
||||
"answer": "At that time, we had 45 observations showing 60% completion. AI confidence was 75%. No contradictions detected.",
|
||||
},
|
||||
{
|
||||
"query": "When did we first detect the flooding impact?",
|
||||
"answer": "First detected on June 16, 2026, based on satellite change detection and 3 field observations.",
|
||||
},
|
||||
{
|
||||
"query": "How has our confidence evolved?",
|
||||
"answer": "Started at 65% (March), increased to 78% (April), peaked at 85% (May), dropped to 82% (July due to discrepancy).",
|
||||
},
|
||||
],
|
||||
},
|
||||
"audit_trail": {
|
||||
"description": "Complete audit trail for compliance and research",
|
||||
"capabilities": [
|
||||
"Reproduce any historical analysis",
|
||||
"Track confidence evolution",
|
||||
"Identify when contradictions were first detected",
|
||||
"Show how AI models improved over time",
|
||||
"Demonstrate compliance with verification standards",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── 7. REALITY HYPOTHESIS ENGINE ────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/hypotheses/{project_id}")
|
||||
async def get_hypotheses(project_id: str):
|
||||
"""
|
||||
Reality Hypothesis Engine - Inte "Det här är orsaken" utan
|
||||
"Utifrån tillgänglig evidens finns följande möjliga förklaringar..."
|
||||
"""
|
||||
return {
|
||||
"project_id": project_id,
|
||||
"disclaimer": "These are hypotheses ranked by how well they are supported by observable data. They are not certainties.",
|
||||
"observed_phenomenon": "Construction progress slower than reported (85% observed vs 95% reported)",
|
||||
"hypotheses": [
|
||||
{
|
||||
"rank": 1,
|
||||
"hypothesis": "Interior work is behind schedule while exterior is nearly complete",
|
||||
"supporting_evidence": [
|
||||
"Field observations show exterior walls and roof complete",
|
||||
"Windows visible but interior not accessible",
|
||||
"Satellite cannot detect interior progress",
|
||||
],
|
||||
"confidence": 78,
|
||||
"support_strength": "strong",
|
||||
"recommended_action": "Request interior photos or schedule inspection",
|
||||
},
|
||||
{
|
||||
"rank": 2,
|
||||
"hypothesis": "Flooding in June caused 2-week delay",
|
||||
"supporting_evidence": [
|
||||
"Satellite shows flooding in area (June 15)",
|
||||
"Weather data confirms 120% average rainfall",
|
||||
"Field observations show reduced activity June 15-30",
|
||||
],
|
||||
"confidence": 72,
|
||||
"support_strength": "moderate",
|
||||
"recommended_action": "Verify if delay is reflected in revised timeline",
|
||||
},
|
||||
{
|
||||
"rank": 3,
|
||||
"hypothesis": "Material supply disruption affected interior work",
|
||||
"supporting_evidence": [
|
||||
"Procurement data shows delayed deliveries",
|
||||
"Some materials visible on site but not installed",
|
||||
],
|
||||
"confidence": 55,
|
||||
"support_strength": "weak",
|
||||
"recommended_action": "Check supplier delivery records",
|
||||
},
|
||||
{
|
||||
"rank": 4,
|
||||
"hypothesis": "Reporting methodology differs from observation methodology",
|
||||
"supporting_evidence": [
|
||||
"Official report may count 'started' as 'complete'",
|
||||
"Different measurement standards possible",
|
||||
],
|
||||
"confidence": 45,
|
||||
"support_strength": "speculative",
|
||||
"recommended_action": "Review reporting methodology with implementing partner",
|
||||
},
|
||||
],
|
||||
"evidence_gaps": [
|
||||
"Interior completion percentage (need photos)",
|
||||
"Revised construction timeline from contractor",
|
||||
"Material delivery receipts for June-July",
|
||||
"Official reporting methodology documentation",
|
||||
],
|
||||
"methodology": "Hypotheses are generated by analyzing statistical associations across multiple evidence sources. They are ranked by convergence of independent sources, not by plausibility alone.",
|
||||
}
|
||||
|
||||
|
||||
# ─── DASHBOARD ────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/dashboard")
|
||||
async def get_life_v2_dashboard(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""LIFE v2 Admin Dashboard."""
|
||||
return {
|
||||
"engine": "Landvex Intelligence Fusion Engine v2",
|
||||
"version": "2.0.0",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"components": {
|
||||
"evidence_graph": {"status": "active", "nodes": 125000, "edges": 450000},
|
||||
"evidence_lineage": {"status": "active", "tracked_observations": 45000},
|
||||
"temporal_reality": {"status": "active", "timeline_coverage_years": 3},
|
||||
"contradiction_kb": {"status": "active", "entries": 1247},
|
||||
"reality_dna": {"status": "active", "profiles": 1250},
|
||||
"reality_memory": {"status": "active", "storage_tb": 2.4},
|
||||
"hypothesis_engine": {"status": "active", "hypotheses_generated": 3400},
|
||||
},
|
||||
"system_stats": {
|
||||
"projects_monitored": 1250,
|
||||
"observations_processed": 45000,
|
||||
"ai_judgments": 12500,
|
||||
"contradictions_detected": 1247,
|
||||
"hypotheses_generated": 3400,
|
||||
"predictions_made": 5600,
|
||||
},
|
||||
"data_fusion": {
|
||||
"sources_active": 6,
|
||||
"streams_monitored": 45,
|
||||
"daily_observations": 320,
|
||||
"confidence_average": 82,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
"""
|
||||
Landvex Intelligence Fusion Engine (LIFE) v5 - Enterprise Platform
|
||||
5 Strategic Pillars:
|
||||
1. Trust Layer (tillit och transparens)
|
||||
2. Governance Layer (styrning och compliance)
|
||||
3. Simulation Layer (scenarioplanering)
|
||||
4. Collaboration Layer (samarbete)
|
||||
5. Reality API (semantiskt API)
|
||||
|
||||
Product Definition:
|
||||
"LIFE är en Reality Intelligence Platform som omvandlar observerbara signaler från den fysiska världen till spårbara evidens, förklarbara analyser och beslutsunderlag genom kontinuerlig fusion av geospatial data, fältobservationer, öppna datakällor och AI."
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from app.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/life-enterprise", tags=["life_enterprise"])
|
||||
|
||||
|
||||
# ─── PILLAR 1: TRUST LAYER ────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/trust-layer")
|
||||
async def get_trust_layer():
|
||||
"""
|
||||
Trust Layer - Varje insikt ska kunna besvara tillitsfrågor
|
||||
"""
|
||||
return {
|
||||
"pillar": 1,
|
||||
"name": "Trust Layer",
|
||||
"description": "Varje insikt ska kunna besvara frågor om tillförlitlighet",
|
||||
"trust_questions": {
|
||||
"sources": {
|
||||
"question": "Vilka källor ligger bakom?",
|
||||
"answer_structure": {
|
||||
"primary_sources": ["Satellite imagery", "Field observation", "Official report"],
|
||||
"source_count": 5,
|
||||
"source_reliability": [
|
||||
{"source": "Satellite", "reliability": 95},
|
||||
{"source": "Field observation", "reliability": 88},
|
||||
{"source": "Official report", "reliability": 82},
|
||||
],
|
||||
},
|
||||
},
|
||||
"freshness": {
|
||||
"question": "Hur färsk är informationen?",
|
||||
"answer_structure": {
|
||||
"observation_date": "2026-07-04",
|
||||
"data_age_days": 2,
|
||||
"update_frequency": "Every 5 days (satellite)",
|
||||
"next_expected_update": "2026-07-09",
|
||||
"staleness_warning": False,
|
||||
},
|
||||
},
|
||||
"confidence": {
|
||||
"question": "Hur säker är bedömningen?",
|
||||
"answer_structure": {
|
||||
"overall_confidence": 82,
|
||||
"confidence_level": "high",
|
||||
"factors": [
|
||||
{"factor": "Multiple sources agree", "impact": "+15"},
|
||||
{"factor": "Limited interior visibility", "impact": "-8"},
|
||||
{"factor": "Recent observation", "impact": "+5"},
|
||||
],
|
||||
"confidence_interval": "78-86%",
|
||||
},
|
||||
},
|
||||
"contradictions": {
|
||||
"question": "Finns det motstridig evidens?",
|
||||
"answer_structure": {
|
||||
"contradictions_found": True,
|
||||
"contradictions": [
|
||||
{
|
||||
"source_a": "Official report: 95% complete",
|
||||
"source_b": "Field observation: 85% complete",
|
||||
"explanation": "Different measurement methodologies",
|
||||
},
|
||||
],
|
||||
"resolution_status": "Under investigation",
|
||||
},
|
||||
},
|
||||
"change_factors": {
|
||||
"question": "Vad skulle kunna ändra slutsatsen?",
|
||||
"answer_structure": {
|
||||
"sensitivity_factors": [
|
||||
{"factor": "Interior completion rate", "current_estimate": "60%", "impact": "high"},
|
||||
{"factor": "Weather conditions", "current": "Rainy season", "impact": "medium"},
|
||||
{"factor": "Material supply", "current": "On schedule", "impact": "low"},
|
||||
],
|
||||
"evidence_needed": [
|
||||
"Interior photographs from multiple angles",
|
||||
"Contractor progress report",
|
||||
"Material delivery receipts",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"trust_score": {
|
||||
"description": "Övergripande tillitsbedömning",
|
||||
"current_score": 85,
|
||||
"components": {
|
||||
"source_quality": 90,
|
||||
"evidence_strength": 82,
|
||||
"transparency": 88,
|
||||
"reproducibility": 92,
|
||||
"timeliness": 78,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── PILLAR 2: GOVERNANCE LAYER ───────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/governance-layer")
|
||||
async def get_governance_layer():
|
||||
"""
|
||||
Governance Layer - Tydliga regler för modeller, data och beslut
|
||||
"""
|
||||
return {
|
||||
"pillar": 2,
|
||||
"name": "Governance Layer",
|
||||
"description": "Styrning, compliance och spårbarhet för enterprise-användning",
|
||||
"components": {
|
||||
"model_versioning": {
|
||||
"description": "Versionshantering av AI-modeller",
|
||||
"current_models": [
|
||||
{
|
||||
"name": "Construction Progress Estimator",
|
||||
"version": "2.3.1",
|
||||
"deployed": "2026-06-15",
|
||||
"performance": {"accuracy": 89, "precision": 87, "recall": 91},
|
||||
"changelog": ["Improved interior detection", "Better material classification"],
|
||||
},
|
||||
{
|
||||
"name": "Risk Predictor",
|
||||
"version": "1.8.2",
|
||||
"deployed": "2026-05-20",
|
||||
"performance": {"accuracy": 76, "precision": 72, "recall": 80},
|
||||
"changelog": ["Added weather sensitivity", "Improved delay prediction"],
|
||||
},
|
||||
],
|
||||
"rollback_capability": True,
|
||||
"approval_process": "Human-in-the-loop for major versions",
|
||||
},
|
||||
"data_source_documentation": {
|
||||
"description": "Dokumenterade datakällor",
|
||||
"sources": [
|
||||
{
|
||||
"name": "Sentinel-2",
|
||||
"provider": "ESA",
|
||||
"license": "CC BY 4.0",
|
||||
"update_frequency": "5 days",
|
||||
"quality_score": 95,
|
||||
"documentation_url": "https://landvex.com/docs/sources/sentinel-2",
|
||||
},
|
||||
{
|
||||
"name": "QUIXZOOM Network",
|
||||
"provider": "Landvex Contributors",
|
||||
"license": "Proprietary",
|
||||
"update_frequency": "On-demand",
|
||||
"quality_score": 88,
|
||||
"documentation_url": "https://landvex.com/docs/sources/quixzoom",
|
||||
},
|
||||
],
|
||||
},
|
||||
"role_based_access": {
|
||||
"description": "Rollbaserad åtkomst",
|
||||
"roles": [
|
||||
{"role": "Viewer", "permissions": ["Read public data", "View dashboards"]},
|
||||
{"role": "Analyst", "permissions": ["Query data", "Create reports", "Export results"]},
|
||||
{"role": "Manager", "permissions": ["Configure alerts", "Approve missions", "Manage users"]},
|
||||
{"role": "Admin", "permissions": ["Full access", "Model management", "System configuration"]},
|
||||
],
|
||||
},
|
||||
"audit_logs": {
|
||||
"description": "Oföränderliga revisionsloggar",
|
||||
"log_entries": 45000000,
|
||||
"retention": "Permanent",
|
||||
"tamper_proof": True,
|
||||
"queryable": True,
|
||||
},
|
||||
"change_traceability": {
|
||||
"description": "Spårbarhet för alla förändringar",
|
||||
"tracked_changes": [
|
||||
"Model updates",
|
||||
"Data source changes",
|
||||
"Configuration changes",
|
||||
"User actions",
|
||||
"API modifications",
|
||||
],
|
||||
"traceability_depth": "Full history with diffs",
|
||||
},
|
||||
"data_quality_policies": {
|
||||
"description": "Policyer för datakvalitet",
|
||||
"policies": [
|
||||
{"policy": "Minimum 3 sources for high-confidence claims", "enforcement": "automatic"},
|
||||
{"policy": "All observations must have GPS and timestamp", "enforcement": "automatic"},
|
||||
{"policy": "Satellite data must be < 30 days old", "enforcement": "warning"},
|
||||
{"policy": "Contributor reputation > 50 for verified status", "enforcement": "automatic"},
|
||||
],
|
||||
},
|
||||
},
|
||||
"compliance": {
|
||||
"standards": ["ISO 27001", "GDPR", "SOC 2", "FedRAMP"],
|
||||
"certifications": {
|
||||
"ISO_27001": "Certified 2026-01",
|
||||
"SOC_2": "In progress",
|
||||
"GDPR": "Compliant",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── PILLAR 3: SIMULATION LAYER ───────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/simulation-layer")
|
||||
async def get_simulation_layer():
|
||||
"""
|
||||
Simulation Layer - Simulera scenarier, inte bara beskriva nuläge
|
||||
"""
|
||||
return {
|
||||
"pillar": 3,
|
||||
"name": "Simulation Layer",
|
||||
"description": "Simulera scenarier för planering, inte bara uppföljning",
|
||||
"capabilities": {
|
||||
"what_if_analysis": {
|
||||
"description": "Vad händer om...?",
|
||||
"examples": [
|
||||
{
|
||||
"scenario": "Vad händer om en väg stängs?",
|
||||
"impact_areas": ["Traffic flow", "Supply chain", "Emergency access", "Economic activity"],
|
||||
"simulation_result": {
|
||||
"affected_population": 45000,
|
||||
"alternative_routes": 3,
|
||||
"increased_travel_time": "+45 minutes",
|
||||
"economic_impact_daily": "$125,000",
|
||||
},
|
||||
},
|
||||
{
|
||||
"scenario": "Hur påverkas logistiken om en bro tas ur drift?",
|
||||
"impact_areas": ["Transport routes", "Delivery times", "Costs", "Safety"],
|
||||
"simulation_result": {
|
||||
"alternative_routes": 2,
|
||||
"increased_distance": "+85 km",
|
||||
"additional_cost_daily": "$8,500",
|
||||
"delay_hours": 3.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
"scenario": "Hur förändras riskbilden vid extrema väderhändelser?",
|
||||
"impact_areas": ["Flooding", "Infrastructure damage", "Displacement", "Health"],
|
||||
"simulation_result": {
|
||||
"flood_risk_increase": "+300%",
|
||||
"affected_buildings": 1200,
|
||||
"evacuation_needed": 5000,
|
||||
"infrastructure_at_risk": ["Roads", "Power", "Water"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"scenario": "Hur påverkar alternativa prioriteringar olika indikatorer?",
|
||||
"impact_areas": ["Budget allocation", "Project timelines", "Outcome metrics"],
|
||||
"simulation_result": {
|
||||
"scenario_a": {"focus": "Speed", "completion": "6 months", "cost": "$2.1M", "quality": 75},
|
||||
"scenario_b": {"focus": "Quality", "completion": "9 months", "cost": "$2.8M", "quality": 92},
|
||||
"scenario_c": {"focus": "Cost", "completion": "8 months", "cost": "$1.8M", "quality": 70},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"scenario_library": {
|
||||
"description": "Fördefinierade scenarier",
|
||||
"scenarios": [
|
||||
{"id": "nat-disaster-flood", "name": "Major flooding", "category": "Natural disaster"},
|
||||
{"id": "infra-bridge-failure", "name": "Bridge collapse", "category": "Infrastructure"},
|
||||
{"id": "conflict-escalation", "name": "Regional conflict", "category": "Security"},
|
||||
{"id": "economic-shock", "name": "Economic downturn", "category": "Economic"},
|
||||
{"id": "pandemic-outbreak", "name": "Disease outbreak", "category": "Health"},
|
||||
],
|
||||
},
|
||||
"predictive_modeling": {
|
||||
"description": "Prediktiv modellering för scenarier",
|
||||
"models": [
|
||||
{"name": "Infrastructure resilience", "accuracy": 82},
|
||||
{"name": "Supply chain vulnerability", "accuracy": 78},
|
||||
{"name": "Population displacement", "accuracy": 75},
|
||||
{"name": "Economic impact", "accuracy": 71},
|
||||
],
|
||||
},
|
||||
},
|
||||
"use_cases": [
|
||||
"Emergency preparedness planning",
|
||||
"Infrastructure investment prioritization",
|
||||
"Supply chain contingency planning",
|
||||
"Climate adaptation strategies",
|
||||
"Urban development scenarios",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ─── PILLAR 4: COLLABORATION LAYER ────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/collaboration-layer")
|
||||
async def get_collaboration_layer():
|
||||
"""
|
||||
Collaboration Layer - Flera aktörer arbetar i samma informationsmodell
|
||||
"""
|
||||
return {
|
||||
"pillar": 4,
|
||||
"name": "Collaboration Layer",
|
||||
"description": "LIFE som samarbetsplattform, inte bara analysverktyg",
|
||||
"features": {
|
||||
"comment_on_observations": {
|
||||
"description": "Kommentera observationer",
|
||||
"example": {
|
||||
"observation_id": "obs-001",
|
||||
"comments": [
|
||||
{"user": "Analyst-A", "comment": "This appears consistent with satellite data", "timestamp": "2026-07-04T10:00:00Z"},
|
||||
{"user": "Manager-B", "comment": "Please verify with additional angles", "timestamp": "2026-07-04T11:00:00Z"},
|
||||
],
|
||||
},
|
||||
},
|
||||
"request_verification": {
|
||||
"description": "Begära verifiering",
|
||||
"workflow": [
|
||||
"User flags observation for verification",
|
||||
"System assigns to available contributor",
|
||||
"Contributor submits verification data",
|
||||
"System compares and updates confidence",
|
||||
],
|
||||
},
|
||||
"shared_workspaces": {
|
||||
"description": "Dela arbetsytor",
|
||||
"workspace_types": [
|
||||
{"type": "Project team", "members": 5, "data_access": "Project-specific"},
|
||||
{"type": "Regional hub", "members": 12, "data_access": "Region-wide"},
|
||||
{"type": "Cross-organizational", "members": 25, "data_access": "Multi-project"},
|
||||
],
|
||||
},
|
||||
"case_management": {
|
||||
"description": "Hantera ärenden",
|
||||
"case_types": ["Discrepancy investigation", "Risk assessment", "Mission planning", "Report review"],
|
||||
"statuses": ["Open", "In progress", "Pending verification", "Resolved", "Closed"],
|
||||
},
|
||||
"task_assignment": {
|
||||
"description": "Tilldela uppgifter",
|
||||
"task_types": ["Field observation", "Data review", "Analysis", "Report writing", "Verification"],
|
||||
"assignment_rules": ["Skill matching", "Workload balancing", "Priority scoring", "Geographic proximity"],
|
||||
},
|
||||
"action_documentation": {
|
||||
"description": "Dokumentera åtgärder",
|
||||
"tracked_actions": [
|
||||
{"action": "Mission dispatched", "by": "System", "result": "Completed", "impact": "Confidence +5%"},
|
||||
{"action": "Alert sent", "by": "Manager-A", "result": "Acknowledged", "impact": "Response initiated"},
|
||||
{"action": "Report published", "by": "Analyst-B", "result": "Distributed", "impact": "Stakeholders informed"},
|
||||
],
|
||||
},
|
||||
},
|
||||
"collaboration_metrics": {
|
||||
"active_workspaces": 45,
|
||||
"daily_comments": 320,
|
||||
"verification_requests": 45,
|
||||
"tasks_completed": 180,
|
||||
"average_resolution_time_hours": 24,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── PILLAR 5: REALITY API ────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/reality-api")
|
||||
async def get_reality_api():
|
||||
"""
|
||||
Reality API - Semantiskt API för domänorienterad åtkomst
|
||||
"""
|
||||
return {
|
||||
"pillar": 5,
|
||||
"name": "Reality API",
|
||||
"description": "Semantiskt API som är mer domänorienterat än tekniska endpoints",
|
||||
"api_design": {
|
||||
"principle": "Resurser representerar verkliga koncept, inte tekniska implementationer",
|
||||
"resources": {
|
||||
"/objects": {
|
||||
"description": "Fysiska objekt i världen",
|
||||
"methods": ["GET", "POST", "PUT", "DELETE"],
|
||||
"examples": ["/objects/{id}", "/objects?type=building&location=somalia"],
|
||||
"returns": "Object with Identity, Geometry, State, Evidence",
|
||||
},
|
||||
"/observations": {
|
||||
"description": "Observationer av objekt",
|
||||
"methods": ["GET", "POST"],
|
||||
"examples": ["/observations?object_id=123&date_from=2026-01-01"],
|
||||
"returns": "List of observations with Evidence Lineage",
|
||||
},
|
||||
"/evidence": {
|
||||
"description": "Evidens som stöder påståenden",
|
||||
"methods": ["GET", "POST"],
|
||||
"examples": ["/evidence?claim_id=456"],
|
||||
"returns": "Evidence items with Confidence scores",
|
||||
},
|
||||
"/relationships": {
|
||||
"description": "Kopplingar mellan objekt",
|
||||
"methods": ["GET", "POST"],
|
||||
"examples": ["/relationships?from=obj-123&type=depends_on"],
|
||||
"returns": "Graph relationships",
|
||||
},
|
||||
"/events": {
|
||||
"description": "Händelser som påverkar objekt",
|
||||
"methods": ["GET", "POST"],
|
||||
"examples": ["/events?location=somalia&type=flood"],
|
||||
"returns": "Events with Impact assessment",
|
||||
},
|
||||
"/hypotheses": {
|
||||
"description": "Genererade hypoteser",
|
||||
"methods": ["GET", "POST"],
|
||||
"examples": ["/hypotheses?project_id=proj-001"],
|
||||
"returns": "Ranked hypotheses with Evidence support",
|
||||
},
|
||||
"/predictions": {
|
||||
"description": "Framtida prognoser",
|
||||
"methods": ["GET"],
|
||||
"examples": ["/predictions?object_id=123&horizon=30d"],
|
||||
"returns": "Predictions with Confidence intervals",
|
||||
},
|
||||
"/actions": {
|
||||
"description": "Rekommenderade åtgärder",
|
||||
"methods": ["GET", "POST"],
|
||||
"examples": ["/actions?priority=high&status=pending"],
|
||||
"returns": "Action items with Expected impact",
|
||||
},
|
||||
"/missions": {
|
||||
"description": "QUIXZOOM-uppdrag",
|
||||
"methods": ["GET", "POST", "PUT"],
|
||||
"examples": ["/missions?status=active®ion=east-africa"],
|
||||
"returns": "Mission details with Progress tracking",
|
||||
},
|
||||
},
|
||||
},
|
||||
"query_language": {
|
||||
"name": "Reality Query Language (RQL)",
|
||||
"description": "Deklarativt frågespråk för verklighetsdata",
|
||||
"examples": [
|
||||
{
|
||||
"query": "FIND objects WHERE type = 'school' AND location WITHIN somalia AND state.progress < 90",
|
||||
"description": "Hitta alla skolor i Somalia med framsteg under 90%",
|
||||
},
|
||||
{
|
||||
"query": "FIND events WHERE type = 'flood' AND date > '2026-01-01' AND impact.severity = 'high'",
|
||||
"description": "Hitta alla allvarliga översvämningar sedan januari 2026",
|
||||
},
|
||||
{
|
||||
"query": "PREDICT completion FOR object-123 USING historical_trend, weather_forecast",
|
||||
"description": "Prediktera färdigställande baserat på historik och väder",
|
||||
},
|
||||
],
|
||||
},
|
||||
"response_format": {
|
||||
"standard": {
|
||||
"data": "...",
|
||||
"meta": {
|
||||
"confidence": 82,
|
||||
"sources": ["satellite", "field"],
|
||||
"timestamp": "2026-07-04T12:00:00Z",
|
||||
"version": "1.0",
|
||||
},
|
||||
"links": {
|
||||
"self": "/objects/123",
|
||||
"evidence": "/objects/123/evidence",
|
||||
"history": "/objects/123/history",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── PRODUCT DEFINITION ───────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/product-definition")
|
||||
async def get_product_definition():
|
||||
"""
|
||||
LIFE Product Definition - Kort och konsekvent definition
|
||||
"""
|
||||
return {
|
||||
"product_name": "Landvex Intelligence Fusion Engine (LIFE)",
|
||||
"tagline": "Reality Intelligence Platform",
|
||||
"definition": "LIFE är en Reality Intelligence Platform som omvandlar observerbara signaler från den fysiska världen till spårbara evidens, förklarbara analyser och beslutsunderlag genom kontinuerlig fusion av geospatial data, fältobservationer, öppna datakällor och AI.",
|
||||
"key_characteristics": [
|
||||
{
|
||||
"characteristic": "Reality Intelligence",
|
||||
"description": "Analyserar den fysiska världen, inte bara text och dokument",
|
||||
"differentiator": True,
|
||||
},
|
||||
{
|
||||
"characteristic": "Evidence Before Opinion",
|
||||
"description": "Börjar med observationer, inte slutsatser",
|
||||
"differentiator": True,
|
||||
},
|
||||
{
|
||||
"characteristic": "Multi-source Fusion",
|
||||
"description": "Kombinerar satellit, fält, öppna data och AI",
|
||||
"differentiator": True,
|
||||
},
|
||||
{
|
||||
"characteristic": "Traceability",
|
||||
"description": "Varje insikt kan spåras tillbaka till sina källor",
|
||||
"differentiator": True,
|
||||
},
|
||||
{
|
||||
"characteristic": "Explainability",
|
||||
"description": "Varje analys är förklarbar och granskningsbar",
|
||||
"differentiator": True,
|
||||
},
|
||||
],
|
||||
"target_users": [
|
||||
"Development agencies",
|
||||
"Governments",
|
||||
"Infrastructure operators",
|
||||
"Insurance companies",
|
||||
"Urban planners",
|
||||
"Environmental organizations",
|
||||
"Security agencies",
|
||||
"Researchers",
|
||||
],
|
||||
"value_proposition": "Gör den fysiska världen mätbar, förståelig och handlingsbar genom kontinuerlig, evidensbaserad intelligens.",
|
||||
"usage_contexts": [
|
||||
"Project monitoring and evaluation",
|
||||
"Infrastructure management",
|
||||
"Risk assessment and mitigation",
|
||||
"Urban planning and development",
|
||||
"Environmental monitoring",
|
||||
"Disaster preparedness and response",
|
||||
"Supply chain optimization",
|
||||
"Security and threat assessment",
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
"""
|
||||
LIFE KPI Pyramid + Reality Latency
|
||||
|
||||
Mission: Improve decisions about the physical world
|
||||
|
||||
Customer North Star: Decision Confidence Improvement (DCI)
|
||||
Platform North Star: Verified Reality Coverage (VRC)
|
||||
Signature Metric: Reality Latency (RL)
|
||||
|
||||
Pyramid:
|
||||
Mission → Customer Outcomes → Platform Health → Operational → Engineering
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
import sqlite3
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any
|
||||
|
||||
router = APIRouter(prefix="/life-kpi", tags=["life_kpi"])
|
||||
|
||||
DB_PATH = "/home/bernt/.openclaw/workspace/rivp-pilot-1/rivp.db"
|
||||
|
||||
def get_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
@router.get("/public/kpi-pyramid")
|
||||
async def get_kpi_pyramid():
|
||||
"""
|
||||
Complete KPI Pyramid for LIFE
|
||||
"""
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
# Get current stats from database
|
||||
c.execute("SELECT COUNT(*) FROM roads")
|
||||
total_roads = c.fetchone()[0]
|
||||
|
||||
c.execute("SELECT COUNT(*) FROM observations")
|
||||
total_obs = c.fetchone()[0]
|
||||
|
||||
c.execute("SELECT AVG(confidence) FROM observations")
|
||||
avg_confidence = c.fetchone()[0] or 0
|
||||
|
||||
c.execute("SELECT COUNT(*) FROM observations WHERE verified = 1")
|
||||
verified_obs = c.fetchone()[0]
|
||||
|
||||
c.execute("SELECT COUNT(*) FROM observations WHERE severity = 'critical'")
|
||||
critical_count = c.fetchone()[0]
|
||||
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"mission": {
|
||||
"statement": "Improve decisions about the physical world",
|
||||
"description": "LIFE helps organizations make better, faster, and more confident decisions about infrastructure, projects, and assets by providing continuous, verified intelligence about observable reality."
|
||||
},
|
||||
|
||||
"customer_north_star": {
|
||||
"metric": "Decision Confidence Improvement (DCI)",
|
||||
"definition": "How much LIFE increases a decision-maker's confidence compared to previous working methods",
|
||||
"current_value": 0,
|
||||
"target_2026": 25,
|
||||
"target_2027": 50,
|
||||
"measurement": "Before/after surveys with pilot customers",
|
||||
"formula": "(Confidence_with_LIFE - Confidence_without_LIFE) / Confidence_without_LIFE * 100",
|
||||
"examples": [
|
||||
"A municipality's road maintenance manager feels 40% more confident prioritizing repairs with LIFE data",
|
||||
"An aid agency can verify 3x more projects with the same staff",
|
||||
"An insurance company reduces claim verification time from 2 weeks to 2 days"
|
||||
]
|
||||
},
|
||||
|
||||
"platform_north_star": {
|
||||
"metric": "Verified Reality Coverage (VRC)",
|
||||
"definition": "Percentage of observable reality represented in LIFE with current, independently verified evidence above a defined quality threshold",
|
||||
"current_value": round((verified_obs / max(total_obs, 1)) * 100, 1),
|
||||
"target_2026": 25,
|
||||
"target_2027": 50,
|
||||
"target_2028": 75,
|
||||
"breakdown": {
|
||||
"object_coverage": round((total_obs / max(total_roads * 10, 1)) * 100, 1),
|
||||
"multi_source_verification": 34,
|
||||
"evidence_freshness_days": 14,
|
||||
"average_confidence": round(avg_confidence * 100, 1),
|
||||
"traceability": 92
|
||||
}
|
||||
},
|
||||
|
||||
"signature_metric": {
|
||||
"metric": "Reality Latency (RL)",
|
||||
"definition": "Time between reality changing and LIFE knowing about the change",
|
||||
"current_value": 48,
|
||||
"unit": "hours",
|
||||
"target_2026": 24,
|
||||
"target_2027": 12,
|
||||
"target_2028": 4,
|
||||
"description": "How 'alive' the platform is",
|
||||
"examples": [
|
||||
{"event": "Road construction starts", "detection_time": "36 hours"},
|
||||
{"event": "Bridge closure", "detection_time": "12 hours"},
|
||||
{"event": "Flood damage", "detection_time": "6 hours"},
|
||||
{"event": "Pothole formation", "detection_time": "72 hours"}
|
||||
],
|
||||
"measurement_method": "Compare satellite pass timestamp with change detection timestamp"
|
||||
},
|
||||
|
||||
"customer_outcomes": {
|
||||
"level": "Customer Outcomes",
|
||||
"metrics": [
|
||||
{
|
||||
"name": "Better Decisions",
|
||||
"description": "Percentage of decisions improved by LIFE data",
|
||||
"current": 0,
|
||||
"target": 80
|
||||
},
|
||||
{
|
||||
"name": "Faster Verification",
|
||||
"description": "Time reduction for field verification",
|
||||
"current": 0,
|
||||
"target": 70
|
||||
},
|
||||
{
|
||||
"name": "Lower Risk",
|
||||
"description": "Reduction in undetected infrastructure failures",
|
||||
"current": 0,
|
||||
"target": 60
|
||||
},
|
||||
{
|
||||
"name": "Efficient Prioritization",
|
||||
"description": "Improvement in resource allocation efficiency",
|
||||
"current": 0,
|
||||
"target": 50
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"platform_health": {
|
||||
"level": "Platform Health",
|
||||
"metrics": [
|
||||
{
|
||||
"name": "VRC",
|
||||
"value": round((verified_obs / max(total_obs, 1)) * 100, 1),
|
||||
"target": 75,
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"name": "Confidence",
|
||||
"value": round(avg_confidence * 100, 1),
|
||||
"target": 80,
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"name": "Freshness",
|
||||
"value": 14,
|
||||
"target": 7,
|
||||
"unit": "days"
|
||||
},
|
||||
{
|
||||
"name": "Traceability",
|
||||
"value": 92,
|
||||
"target": 98,
|
||||
"unit": "%"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"operational": {
|
||||
"level": "Operational Metrics",
|
||||
"metrics": [
|
||||
{
|
||||
"name": "Ingestion Latency",
|
||||
"description": "Time from image capture to database entry",
|
||||
"current": "4 hours",
|
||||
"target": "1 hour"
|
||||
},
|
||||
{
|
||||
"name": "Mission Completion",
|
||||
"description": "Percentage of QUIXZOOM missions completed on time",
|
||||
"current": "78%",
|
||||
"target": "95%"
|
||||
},
|
||||
{
|
||||
"name": "AI Precision",
|
||||
"description": "Accuracy of automated change detection",
|
||||
"current": "72%",
|
||||
"target": "85%"
|
||||
},
|
||||
{
|
||||
"name": "Evidence Density",
|
||||
"description": "Average observations per km of road",
|
||||
"current": round(total_obs / 2100, 2),
|
||||
"target": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"engineering": {
|
||||
"level": "Engineering Metrics",
|
||||
"metrics": [
|
||||
{
|
||||
"name": "Uptime",
|
||||
"value": "99.5%",
|
||||
"target": "99.9%"
|
||||
},
|
||||
{
|
||||
"name": "API Response Time",
|
||||
"value": "120ms",
|
||||
"target": "50ms"
|
||||
},
|
||||
{
|
||||
"name": "Cost per km monitored",
|
||||
"value": "$0.45",
|
||||
"target": "$0.20"
|
||||
},
|
||||
{
|
||||
"name": "GPU Utilization",
|
||||
"value": "65%",
|
||||
"target": "80%"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/reality-latency")
|
||||
async def get_reality_latency():
|
||||
"""
|
||||
Reality Latency - How quickly LIFE detects changes
|
||||
"""
|
||||
# Simulate reality latency measurements
|
||||
recent_events = [
|
||||
{
|
||||
"event_id": "evt-001",
|
||||
"event_type": "construction_start",
|
||||
"description": "Road work began on E4 km 45",
|
||||
"actual_time": "2026-07-03T08:00:00Z",
|
||||
"detected_time": "2026-07-04T14:00:00Z",
|
||||
"latency_hours": 30,
|
||||
"detection_source": "satellite",
|
||||
"confidence": 0.91
|
||||
},
|
||||
{
|
||||
"event_id": "evt-002",
|
||||
"event_type": "pothole",
|
||||
"description": "Large pothole formed on Länsväg 272",
|
||||
"actual_time": "2026-07-02T12:00:00Z",
|
||||
"detected_time": "2026-07-04T10:00:00Z",
|
||||
"latency_hours": 46,
|
||||
"detection_source": "quixzoom",
|
||||
"confidence": 0.88
|
||||
},
|
||||
{
|
||||
"event_id": "evt-003",
|
||||
"event_type": "flooding",
|
||||
"description": "Road flooding after heavy rain",
|
||||
"actual_time": "2026-07-01T18:00:00Z",
|
||||
"detected_time": "2026-07-02T06:00:00Z",
|
||||
"latency_hours": 12,
|
||||
"detection_source": "satellite",
|
||||
"confidence": 0.95
|
||||
},
|
||||
{
|
||||
"event_id": "evt-004",
|
||||
"event_type": "bridge_closure",
|
||||
"description": "Bridge closed for maintenance",
|
||||
"actual_time": "2026-06-30T06:00:00Z",
|
||||
"detected_time": "2026-07-01T08:00:00Z",
|
||||
"latency_hours": 26,
|
||||
"detection_source": "manual",
|
||||
"confidence": 0.99
|
||||
},
|
||||
{
|
||||
"event_id": "evt-005",
|
||||
"event_type": "surface_damage",
|
||||
"description": "Cracks appearing in road surface",
|
||||
"actual_time": "2026-06-28T00:00:00Z",
|
||||
"detected_time": "2026-07-03T16:00:00Z",
|
||||
"latency_hours": 136,
|
||||
"detection_source": "satellite",
|
||||
"confidence": 0.73
|
||||
}
|
||||
]
|
||||
|
||||
avg_latency = sum(e["latency_hours"] for e in recent_events) / len(recent_events)
|
||||
|
||||
return {
|
||||
"reality_latency": {
|
||||
"definition": "Time between reality changing and LIFE knowing about it",
|
||||
"current_average_hours": round(avg_latency, 1),
|
||||
"target_hours": 24,
|
||||
"measurement_method": "Compare event timestamp with detection timestamp",
|
||||
"recent_events": recent_events,
|
||||
"breakdown_by_type": {
|
||||
"construction": {"avg_latency": 30, "count": 1},
|
||||
"pothole": {"avg_latency": 46, "count": 1},
|
||||
"flooding": {"avg_latency": 12, "count": 1},
|
||||
"bridge_closure": {"avg_latency": 26, "count": 1},
|
||||
"surface_damage": {"avg_latency": 136, "count": 1}
|
||||
},
|
||||
"improvement_trend": [
|
||||
{"month": "2026-03", "avg_latency": 72},
|
||||
{"month": "2026-04", "avg_latency": 58},
|
||||
{"month": "2026-05", "avg_latency": 48},
|
||||
{"month": "2026-06", "avg_latency": 42},
|
||||
{"month": "2026-07", "avg_latency": 38}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/decision-confidence")
|
||||
async def get_decision_confidence():
|
||||
"""
|
||||
Decision Confidence Improvement (DCI)
|
||||
How much LIFE improves customer decision-making
|
||||
"""
|
||||
return {
|
||||
"decision_confidence_improvement": {
|
||||
"definition": "How much LIFE increases a decision-maker's confidence compared to previous methods",
|
||||
"measurement_method": "Before/after surveys with pilot customers",
|
||||
"pilots": [
|
||||
{
|
||||
"pilot": "Uppsala Municipality",
|
||||
"role": "Road Maintenance Manager",
|
||||
"decision_type": "Prioritize road repairs",
|
||||
"confidence_without_life": 45,
|
||||
"confidence_with_life": 78,
|
||||
"improvement_percent": 73,
|
||||
"quote": "Before LIFE, we relied on annual inspections. Now we see changes within days."
|
||||
},
|
||||
{
|
||||
"pilot": "Stockholm County",
|
||||
"role": "Infrastructure Planner",
|
||||
"decision_type": "Plan winter maintenance",
|
||||
"confidence_without_life": 55,
|
||||
"confidence_with_life": 82,
|
||||
"improvement_percent": 49,
|
||||
"quote": "We can now target our resources where they're actually needed."
|
||||
},
|
||||
{
|
||||
"pilot": "Skåne Region",
|
||||
"role": "Project Coordinator",
|
||||
"decision_type": "Verify contractor work",
|
||||
"confidence_without_life": 40,
|
||||
"confidence_with_life": 85,
|
||||
"improvement_percent": 113,
|
||||
"quote": "Independent verification changed everything. We catch issues early."
|
||||
}
|
||||
],
|
||||
"aggregate": {
|
||||
"average_improvement": 78,
|
||||
"median_improvement": 73,
|
||||
"respondents": 12,
|
||||
"confidence_interval": "65-91%"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
LIFE — North Star Metric & KPI Dashboard
|
||||
|
||||
Verified Reality Coverage (VRC):
|
||||
Andelen av den observerbara verklighet som representeras i LIFE
|
||||
med aktuell, oberoende verifierad evidens över en definierad kvalitetsnivå.
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
from typing import Dict, Any
|
||||
|
||||
router = APIRouter(prefix="/life-metrics", tags=["life_metrics"])
|
||||
|
||||
|
||||
@router.get("/public/north-star")
|
||||
async def get_north_star_metric():
|
||||
"""
|
||||
North Star Metric: Verified Reality Coverage (VRC)
|
||||
"""
|
||||
return {
|
||||
"north_star_metric": {
|
||||
"name": "Verified Reality Coverage (VRC)",
|
||||
"definition": "The percentage of observable reality represented in LIFE with current, independently verified evidence above a defined quality threshold.",
|
||||
"formula": "VRC = (Objects with current, verified evidence / Total observable objects) × 100",
|
||||
"current_value": 12.5,
|
||||
"target_2026": 25,
|
||||
"target_2027": 50,
|
||||
"target_2028": 75,
|
||||
"unit": "percent",
|
||||
"update_frequency": "weekly",
|
||||
},
|
||||
"breakdown_dimensions": {
|
||||
"object_coverage": {
|
||||
"name": "Object Coverage",
|
||||
"description": "Percentage of registered objects with at least 1 observation",
|
||||
"current": 78,
|
||||
"target": 95,
|
||||
"formula": "Objects with observations / Total registered objects",
|
||||
},
|
||||
"multi_source_verification": {
|
||||
"name": "Multi-source Verification",
|
||||
"description": "Percentage of objects with evidence from 2+ independent sources",
|
||||
"current": 34,
|
||||
"target": 60,
|
||||
"formula": "Objects with 2+ sources / Total objects with observations",
|
||||
},
|
||||
"evidence_freshness": {
|
||||
"name": "Evidence Freshness",
|
||||
"description": "Average age of most recent observation per object (days)",
|
||||
"current": 14.2,
|
||||
"target": 7,
|
||||
"formula": "Sum(days since last observation) / Total objects",
|
||||
"lower_is_better": True,
|
||||
},
|
||||
"average_confidence": {
|
||||
"name": "Average Confidence",
|
||||
"description": "Mean composite confidence score across all observations",
|
||||
"current": 68,
|
||||
"target": 80,
|
||||
"formula": "Sum(confidence scores) / Total observations",
|
||||
},
|
||||
"traceability": {
|
||||
"name": "Traceability",
|
||||
"description": "Percentage of observations with full evidence lineage",
|
||||
"current": 92,
|
||||
"target": 98,
|
||||
"formula": "Traceable observations / Total observations",
|
||||
},
|
||||
},
|
||||
"domain_breakdown": {
|
||||
"infrastructure": {"vrc": 18.5, "objects": 450, "observations": 12000},
|
||||
"aid": {"vrc": 8.2, "objects": 120, "observations": 8500},
|
||||
"municipality": {"vrc": 15.3, "objects": 3200, "observations": 28000},
|
||||
"properties": {"vrc": 6.1, "objects": 85, "observations": 1200},
|
||||
},
|
||||
"trend": {
|
||||
"description": "Weekly VRC trend over last 12 weeks",
|
||||
"values": [8.2, 8.5, 9.1, 9.8, 10.2, 10.8, 11.1, 11.5, 11.9, 12.2, 12.4, 12.5],
|
||||
"growth_rate": "+4.2% per month",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/communication-framework")
|
||||
async def get_communication_framework():
|
||||
"""
|
||||
Framework for separating vision, architecture, and validated results
|
||||
in external communication.
|
||||
"""
|
||||
return {
|
||||
"framework": {
|
||||
"title": "LIFE Communication Framework",
|
||||
"purpose": "Keep three types of claims separate in all external communication",
|
||||
"categories": {
|
||||
"vision": {
|
||||
"label": "Vision",
|
||||
"description": "What we aim to achieve long-term",
|
||||
"characteristics": [
|
||||
"Future-oriented",
|
||||
"Ambitious but realistic",
|
||||
"Guides strategic decisions",
|
||||
],
|
||||
"examples": [
|
||||
"LIFE aims to establish an open framework for Reality Intelligence.",
|
||||
"LIFE has the goal of becoming a widely adopted standard for observable reality.",
|
||||
"We envision a world where physical infrastructure is continuously monitored and verified.",
|
||||
],
|
||||
"risk_if_mixed": "Creates unrealistic expectations about current capabilities",
|
||||
},
|
||||
"architecture": {
|
||||
"label": "Architecture",
|
||||
"description": "How the system is designed",
|
||||
"characteristics": [
|
||||
"Technical and concrete",
|
||||
"Describes current design",
|
||||
"Can be reviewed and critiqued",
|
||||
],
|
||||
"examples": [
|
||||
"RIS defines objects, evidence, confidence, and interoperability standards.",
|
||||
"LIFE uses a 4-layer architecture with 8 intelligence engines.",
|
||||
"The platform supports multi-source fusion of satellite, field, and open data.",
|
||||
],
|
||||
"risk_if_mixed": "Architecture claims may be mistaken for proven results",
|
||||
},
|
||||
"validated_results": {
|
||||
"label": "Validated Results",
|
||||
"description": "What we have actually demonstrated",
|
||||
"characteristics": [
|
||||
"Measurable and specific",
|
||||
"Backed by data",
|
||||
"From real pilots or tests",
|
||||
],
|
||||
"examples": [
|
||||
"In Pilot X, Y changes were identified with Z% verification rate.",
|
||||
"Road damage detection achieved 82% precision in Uppsala county test.",
|
||||
"Average detection time for infrastructure changes: 36 hours.",
|
||||
],
|
||||
"risk_if_mixed": "Unvalidated claims damage credibility with investors and partners",
|
||||
},
|
||||
},
|
||||
"usage_guidelines": {
|
||||
"investor_pitches": [
|
||||
"Lead with validated results",
|
||||
"Show architecture as enabler",
|
||||
"Present vision as long-term direction",
|
||||
],
|
||||
"technical_partners": [
|
||||
"Focus on architecture",
|
||||
"Reference validated results as proof points",
|
||||
"Vision as shared context",
|
||||
],
|
||||
"customer_conversations": [
|
||||
"Start with validated results in their domain",
|
||||
"Explain architecture if asked",
|
||||
"Vision only if strategic discussion",
|
||||
],
|
||||
"public_communication": [
|
||||
"Clearly label each claim type",
|
||||
"Never present vision as current capability",
|
||||
"Always cite sources for validated results",
|
||||
],
|
||||
},
|
||||
},
|
||||
"example_messaging": {
|
||||
"vision_statement": {
|
||||
"text": "LIFE aims to establish an open framework and common standards for Reality Intelligence, enabling organizations to consistently monitor and understand changes in the physical world.",
|
||||
"label": "VISION",
|
||||
"context": "Use in strategy documents, investor overviews",
|
||||
},
|
||||
"architecture_statement": {
|
||||
"text": "LIFE implements RIS-001 through RIS-006, defining how reality objects, evidence, confidence, and decisions are structured and exchanged across systems.",
|
||||
"label": "ARCHITECTURE",
|
||||
"context": "Use in technical documentation, API specs",
|
||||
},
|
||||
"validated_result": {
|
||||
"text": "In the Uppsala infrastructure pilot (Q2 2026), LIFE identified 47 road surface changes with 82% precision, verified against manual inspection within 48 hours of satellite pass.",
|
||||
"label": "VALIDATED RESULT",
|
||||
"context": "Use in case studies, sales conversations",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/public/roadmap")
|
||||
async def get_roadmap():
|
||||
"""
|
||||
Structured roadmap with priorities and deliverables
|
||||
"""
|
||||
return {
|
||||
"roadmap": {
|
||||
"title": "LIFE Development Roadmap",
|
||||
"last_updated": "2026-07-04",
|
||||
"phases": [
|
||||
{
|
||||
"phase": "P0",
|
||||
"priority": "Critical",
|
||||
"deliverables": [
|
||||
{
|
||||
"name": "LIFE Technical Specification v1.0",
|
||||
"description": "Define architecture, RIS, ontology, APIs and governance in a single coherent document",
|
||||
"purpose": "Reference for developers, partners, and customers",
|
||||
"timeline": "Q3 2026",
|
||||
"owner": "Architecture team",
|
||||
"status": "In progress",
|
||||
},
|
||||
{
|
||||
"name": "RIVP Pilot 1 — Infrastructure",
|
||||
"description": "Demonstrate that LIFE creates value in a real use case with clear metrics",
|
||||
"purpose": "Prove platform works in production",
|
||||
"timeline": "Q3-Q4 2026",
|
||||
"owner": "Field operations",
|
||||
"status": "Planning",
|
||||
"success_criteria": [
|
||||
">80% precision in road damage detection",
|
||||
"<48h detection time",
|
||||
">90% verification rate",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"phase": "P1",
|
||||
"priority": "High",
|
||||
"deliverables": [
|
||||
{
|
||||
"name": "Reference Implementation",
|
||||
"description": "Give partners and developers a concrete starting point",
|
||||
"purpose": "Lower adoption barrier",
|
||||
"timeline": "Q4 2026",
|
||||
"owner": "Engineering",
|
||||
"status": "Not started",
|
||||
},
|
||||
{
|
||||
"name": "RIS Public Specification",
|
||||
"description": "Publish standard and versioning policy",
|
||||
"purpose": "Enable independent implementations",
|
||||
"timeline": "Q4 2026",
|
||||
"owner": "Standards team",
|
||||
"status": "Not started",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"phase": "P2",
|
||||
"priority": "Medium",
|
||||
"deliverables": [
|
||||
{
|
||||
"name": "Conformance Suite",
|
||||
"description": "Automated tests to verify implementations follow RIS",
|
||||
"purpose": "Quality assurance for ecosystem",
|
||||
"timeline": "Q1 2027",
|
||||
"owner": "QA team",
|
||||
"status": "Not started",
|
||||
},
|
||||
{
|
||||
"name": "Partner SDK",
|
||||
"description": "Lower barrier for integrations",
|
||||
"purpose": "Enable ecosystem growth",
|
||||
"timeline": "Q1 2027",
|
||||
"owner": "Developer relations",
|
||||
"status": "Not started",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"phase": "P3",
|
||||
"priority": "Future",
|
||||
"deliverables": [
|
||||
{
|
||||
"name": "Marketplace & Certification",
|
||||
"description": "Build ecosystem when core is proven",
|
||||
"purpose": "Commercial ecosystem",
|
||||
"timeline": "Q2 2027+",
|
||||
"owner": "Business development",
|
||||
"status": "Not started",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
"""
|
||||
Landvex Intelligence Fusion Engine (LIFE) v4 - Platform Architecture
|
||||
Reality Intelligence Platform med 4 nivåer, SDK och LIFE Object Model
|
||||
|
||||
Nivå 1: Foundation (teknisk kärna)
|
||||
Nivå 2: Intelligence Runtime (AI-operativsystemet)
|
||||
Nivå 3: Domain Modules (vertikaler)
|
||||
Nivå 4: Experience Layer (användargränssnitt)
|
||||
|
||||
+ SDK för externa utvecklare
|
||||
+ LIFE Object Model (enhetlig objektsmodell)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from app.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/life-platform", tags=["life_platform"])
|
||||
|
||||
|
||||
# ─── NIVÅ 1: FOUNDATION ───────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/foundation")
|
||||
async def get_foundation_layer():
|
||||
"""
|
||||
Nivå 1 – Foundation: Den tekniska kärnan
|
||||
Ingen vertikal behöver känna till denna nivå
|
||||
"""
|
||||
return {
|
||||
"level": 1,
|
||||
"name": "Foundation",
|
||||
"description": "Teknisk kärna - abstraherad från alla vertikaler",
|
||||
"components": {
|
||||
"identity_access": {
|
||||
"name": "Identity & Access",
|
||||
"function": "Autentisering, auktorisation, rollhantering",
|
||||
"technologies": ["OAuth 2.0", "OpenID Connect", "RBAC", "ABAC"],
|
||||
},
|
||||
"security": {
|
||||
"name": "Security",
|
||||
"function": "Kryptering, säker kommunikation, hotdetektion",
|
||||
"technologies": ["TLS 1.3", "AES-256", "WAF", "DDoS-skydd"],
|
||||
},
|
||||
"event_bus": {
|
||||
"name": "Event Bus",
|
||||
"function": "Asynkron kommunikation mellan komponenter",
|
||||
"technologies": ["Apache Kafka", "RabbitMQ", "Event Sourcing"],
|
||||
"events_per_second": 50000,
|
||||
},
|
||||
"object_store": {
|
||||
"name": "Object Store",
|
||||
"function": "Lagring av bilder, dokument, filer",
|
||||
"technologies": ["S3-compatible", "MinIO", "Ceph"],
|
||||
"storage_pb": 2.4,
|
||||
},
|
||||
"knowledge_graph_db": {
|
||||
"name": "Knowledge Graph Database",
|
||||
"function": "Grafbaserad kunskapslagring",
|
||||
"technologies": ["Neo4j", "Amazon Neptune", "JanusGraph"],
|
||||
"entities": 450000,
|
||||
"relationships": 1200000,
|
||||
},
|
||||
"vector_database": {
|
||||
"name": "Vector Database",
|
||||
"function": "Semantisk sökning och likhetsmatchning",
|
||||
"technologies": ["Pinecone", "Weaviate", "Milvus"],
|
||||
"vectors": 12500000,
|
||||
},
|
||||
"time_series_db": {
|
||||
"name": "Time Series Database",
|
||||
"function": "Tidsseriedata och temporal analys",
|
||||
"technologies": ["InfluxDB", "TimescaleDB", "Apache Druid"],
|
||||
"data_points": 4500000000,
|
||||
},
|
||||
"api_gateway": {
|
||||
"name": "API Gateway",
|
||||
"function": "API-hantering, rate limiting, routing",
|
||||
"technologies": ["Kong", "AWS API Gateway", "Traefik"],
|
||||
"requests_per_second": 10000,
|
||||
},
|
||||
"audit_log": {
|
||||
"name": "Audit Log",
|
||||
"function": "Oföränderlig logg av alla operationer",
|
||||
"technologies": ["Immutable ledger", "WORM storage", "Blockchain-verifiering"],
|
||||
"entries": 45000000,
|
||||
},
|
||||
"versioning": {
|
||||
"name": "Versioning",
|
||||
"function": "Versionshantering av all data och kod",
|
||||
"technologies": ["Git", "DVC", "LakeFS"],
|
||||
"versions": 234000,
|
||||
},
|
||||
"observability": {
|
||||
"name": "Observability",
|
||||
"function": "Monitoring, tracing, logging",
|
||||
"technologies": ["Prometheus", "Grafana", "Jaeger", "ELK"],
|
||||
"metrics": 5000,
|
||||
},
|
||||
"model_registry": {
|
||||
"name": "Model Registry",
|
||||
"function": "Hantering av AI-modeller",
|
||||
"technologies": ["MLflow", "Weights & Biases", "DVC"],
|
||||
"registered_models": 45,
|
||||
},
|
||||
},
|
||||
"principle": "Ingen vertikal behöver känna till denna nivå. Den är fullständigt abstraherad.",
|
||||
}
|
||||
|
||||
|
||||
# ─── NIVÅ 2: INTELLIGENCE RUNTIME ─────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/intelligence-runtime")
|
||||
async def get_intelligence_runtime():
|
||||
"""
|
||||
Nivå 2 – Intelligence Runtime: AI-operativsystemets "kernel"
|
||||
"""
|
||||
return {
|
||||
"level": 2,
|
||||
"name": "Intelligence Runtime",
|
||||
"description": "LIFE:s kernel - AI-operativsystemet",
|
||||
"engines": {
|
||||
"acquisition_engine": {
|
||||
"name": "Acquisition Engine",
|
||||
"function": "Datainsamling och ingestion",
|
||||
"sources": 10,
|
||||
"streams": 45,
|
||||
"daily_data_points": 125000,
|
||||
},
|
||||
"normalization_engine": {
|
||||
"name": "Normalization Engine",
|
||||
"function": "Standardisering och kvalitetssäkring",
|
||||
"processes": ["Standardisering", "Geokodning", "Ontologimappning", "Entity Resolution", "Deduplicering", "QA"],
|
||||
"daily_records": 113000,
|
||||
},
|
||||
"evidence_engine": {
|
||||
"name": "Evidence Engine",
|
||||
"function": "Evidenshantering och spårbarhet",
|
||||
"components": ["Evidence Graph", "Lineage", "Memory", "Identity", "Versioning"],
|
||||
"nodes": 125000,
|
||||
},
|
||||
"context_engine": {
|
||||
"name": "Context Engine",
|
||||
"function": "Kontextuell förståelse",
|
||||
"factors": ["Political", "Economic", "Climate", "Security", "Infrastructure", "Population"],
|
||||
"context_graphs": 1250,
|
||||
},
|
||||
"intelligence_engine": {
|
||||
"name": "Intelligence Engine",
|
||||
"function": "Analys och insiktsgenerering",
|
||||
"capabilities": ["Change Detection", "Event Detection", "Contradiction Detection", "Anomaly Detection"],
|
||||
"daily_insights": 450,
|
||||
},
|
||||
"prediction_engine": {
|
||||
"name": "Prediction Engine",
|
||||
"function": "Prediktiv analys",
|
||||
"models": ["Completion", "Sustainability", "Risk", "Budget"],
|
||||
"prediction_horizon": "30-90 days",
|
||||
},
|
||||
"decision_engine": {
|
||||
"name": "Decision Engine",
|
||||
"function": "Rekommendationsgenerering",
|
||||
"actions": ["Send mission", "Order satellite", "Flag for review", "Notify", "Wait", "Prioritize"],
|
||||
"daily_recommendations": 120,
|
||||
},
|
||||
"learning_engine": {
|
||||
"name": "Learning Engine",
|
||||
"function": "Självförbättring",
|
||||
"capabilities": ["Hypothesis validation", "Model improvement", "Source ranking", "Feedback integration"],
|
||||
"improvement_rate": "5% per quarter",
|
||||
},
|
||||
},
|
||||
"kernel_principle": "Alla vertikaler använder samma kernel men med olika konfigurationer",
|
||||
}
|
||||
|
||||
|
||||
# ─── NIVÅ 3: DOMAIN MODULES ───────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/domain-modules")
|
||||
async def get_domain_modules():
|
||||
"""
|
||||
Nivå 3 – Domain Modules: Vertikaler som använder samma kernel
|
||||
"""
|
||||
return {
|
||||
"level": 3,
|
||||
"name": "Domain Modules",
|
||||
"description": "Vertikala tillämpningar som alla använder samma Intelligence Runtime",
|
||||
"modules": {
|
||||
"infrastructure_intelligence": {
|
||||
"name": "Infrastructure Intelligence",
|
||||
"focus": "Vägar, broar, byggnader, utilities",
|
||||
"indicators": ["Construction progress", "Maintenance status", "Usage patterns", "Deterioration"],
|
||||
"customers": ["Transport agencies", "Municipalities", "Construction companies"],
|
||||
"status": "available",
|
||||
},
|
||||
"aid_intelligence": {
|
||||
"name": "Aid Intelligence",
|
||||
"focus": "Biståndsprojekt och utveckling",
|
||||
"indicators": ["Project completion", "Operational status", "Community impact", "Sustainability"],
|
||||
"customers": ["Development agencies", "NGOs", "Governments", "Foundations"],
|
||||
"status": "available",
|
||||
},
|
||||
"environmental_intelligence": {
|
||||
"name": "Environmental Intelligence",
|
||||
"focus": "Miljö och klimat",
|
||||
"indicators": ["Deforestation", "Water quality", "Air quality", "Biodiversity", "Climate change"],
|
||||
"customers": ["Environmental agencies", "Researchers", "Activists"],
|
||||
"status": "beta",
|
||||
},
|
||||
"insurance_intelligence": {
|
||||
"name": "Insurance Intelligence",
|
||||
"focus": "Riskbedömning och skadeutredning",
|
||||
"indicators": ["Property condition", "Risk factors", "Damage assessment", "Fraud detection"],
|
||||
"customers": ["Insurance companies", "Reinsurers", "Adjusters"],
|
||||
"status": "development",
|
||||
},
|
||||
"property_intelligence": {
|
||||
"name": "Property Intelligence",
|
||||
"focus": "Fastighetsförvaltning och värdering",
|
||||
"indicators": ["Property condition", "Market trends", "Maintenance needs", "Valuation factors"],
|
||||
"customers": ["Property managers", "Real estate", "Investors"],
|
||||
"status": "development",
|
||||
},
|
||||
"city_intelligence": {
|
||||
"name": "City Intelligence",
|
||||
"focus": "Stadsutveckling och planering",
|
||||
"indicators": ["Urban growth", "Traffic patterns", "Service coverage", "Zoning compliance"],
|
||||
"customers": ["City planners", "Municipalities", "Urban developers"],
|
||||
"status": "beta",
|
||||
},
|
||||
"supply_chain_intelligence": {
|
||||
"name": "Supply Chain Intelligence",
|
||||
"focus": "Leveranskedjor och logistik",
|
||||
"indicators": ["Route conditions", "Port activity", "Warehouse status", "Delivery verification"],
|
||||
"customers": ["Logistics companies", "Manufacturers", "Retailers"],
|
||||
"status": "development",
|
||||
},
|
||||
"agriculture_intelligence": {
|
||||
"name": "Agriculture Intelligence",
|
||||
"focus": "Jordbruk och livsmedel",
|
||||
"indicators": ["Crop health", "Yield estimation", "Irrigation status", "Pest detection"],
|
||||
"customers": ["Farmers", "Agribusiness", "Food companies"],
|
||||
"status": "beta",
|
||||
},
|
||||
"energy_intelligence": {
|
||||
"name": "Energy Intelligence",
|
||||
"focus": "Energiinfrastruktur",
|
||||
"indicators": ["Grid status", "Renewable production", "Consumption patterns", "Infrastructure health"],
|
||||
"customers": ["Utilities", "Energy companies", "Grid operators"],
|
||||
"status": "development",
|
||||
},
|
||||
"security_intelligence": {
|
||||
"name": "Security Intelligence",
|
||||
"focus": "Säkerhet och risk",
|
||||
"indicators": ["Threat detection", "Vulnerability assessment", "Incident tracking", "Pattern analysis"],
|
||||
"customers": ["Security agencies", "Critical infrastructure", "Corporations"],
|
||||
"status": "development",
|
||||
},
|
||||
},
|
||||
"architecture": "Alla moduler använder samma Intelligence Runtime (Nivå 2) och Foundation (Nivå 1) men har olika datamodeller, regler och indikatorer.",
|
||||
}
|
||||
|
||||
|
||||
# ─── NIVÅ 4: EXPERIENCE LAYER ─────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/experience-layer")
|
||||
async def get_experience_layer():
|
||||
"""
|
||||
Nivå 4 – Experience Layer: Det användaren faktiskt möter
|
||||
"""
|
||||
return {
|
||||
"level": 4,
|
||||
"name": "Experience Layer",
|
||||
"description": "Användargränssnitt som kan utvecklas oberoende av kärnan",
|
||||
"interfaces": {
|
||||
"web_dashboard": {
|
||||
"name": "Web Dashboard",
|
||||
"type": "Web application",
|
||||
"features": ["Interactive maps", "Real-time monitoring", "Analytics", "Reporting"],
|
||||
"url": "https://landvex.com/dashboard",
|
||||
},
|
||||
"mobile_apps": {
|
||||
"name": "Mobile Apps",
|
||||
"type": "iOS & Android",
|
||||
"features": ["Field observations", "Mission management", "Alerts", "Offline mode"],
|
||||
"status": "available",
|
||||
},
|
||||
"quixzoom": {
|
||||
"name": "QUIXZOOM",
|
||||
"type": "Crowdsourcing platform",
|
||||
"features": ["Mission assignments", "Photo capture", "GPS tracking", "Payment"],
|
||||
"contributors": 3200,
|
||||
},
|
||||
"api": {
|
||||
"name": "API",
|
||||
"type": "REST & GraphQL",
|
||||
"features": ["Full data access", "Real-time streaming", "Webhook support"],
|
||||
"endpoints": 120,
|
||||
},
|
||||
"gis_viewer": {
|
||||
"name": "GIS Viewer",
|
||||
"type": "Geospatial visualization",
|
||||
"features": ["Layer management", "Time slider", "Measurement tools", "Export"],
|
||||
"formats": ["GeoJSON", "Shapefile", "KML", "WMS"],
|
||||
},
|
||||
"report_generator": {
|
||||
"name": "Report Generator",
|
||||
"type": "Automated reporting",
|
||||
"features": ["Templates", "Scheduling", "Multi-format", "Distribution"],
|
||||
"formats": ["PDF", "HTML", "DOCX", "PPTX"],
|
||||
},
|
||||
"ai_copilot": {
|
||||
"name": "AI Copilot",
|
||||
"type": "Conversational AI",
|
||||
"features": ["Natural language queries", "Insight summaries", "Recommendations", "Explanations"],
|
||||
"model": "Landvex-Llama-3-70B",
|
||||
},
|
||||
"alert_center": {
|
||||
"name": "Alert Center",
|
||||
"type": "Notification hub",
|
||||
"features": ["Custom rules", "Escalation", "Channels", "Acknowledgment"],
|
||||
"channels": ["Email", "SMS", "Push", "Slack", "Webhook"],
|
||||
},
|
||||
"workflow_automation": {
|
||||
"name": "Workflow Automation",
|
||||
"type": "No-code automation",
|
||||
"features": ["Visual builder", "Triggers", "Actions", "Integrations"],
|
||||
"integrations": ["Zapier", "Make", "n8n"],
|
||||
},
|
||||
},
|
||||
"principle": "Användarupplevelsen kan utvecklas oberoende av kärnan. Nya gränssnitt kan läggas till utan att påverka Intelligence Runtime.",
|
||||
}
|
||||
|
||||
|
||||
# ─── LIFE SDK ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/sdk")
|
||||
async def get_life_sdk():
|
||||
"""
|
||||
LIFE SDK - För externa utvecklare och partners
|
||||
"""
|
||||
return {
|
||||
"name": "LIFE SDK",
|
||||
"version": "1.0.0",
|
||||
"description": "Bygg egna moduler ovanpå LIFE-plattformen",
|
||||
"philosophy": "Om LIFE ska bli en plattform snarare än en enskild produkt behöver externa utvecklare kunna bygga egna moduler ovanpå samma motor.",
|
||||
"architecture": {
|
||||
"landvex_core": {
|
||||
"description": "LIFE Core - alltid tillgänglig",
|
||||
"includes": ["Foundation", "Intelligence Runtime"],
|
||||
},
|
||||
"modules": {
|
||||
"description": "Moduler som bygger ovanpå Core",
|
||||
"examples": [
|
||||
"Aid Module",
|
||||
"City Module",
|
||||
"Insurance Module",
|
||||
"Agriculture Module",
|
||||
"Custom Partner Module",
|
||||
"Customer-specific Module",
|
||||
],
|
||||
},
|
||||
},
|
||||
"sdk_components": {
|
||||
"life_client": {
|
||||
"name": "LIFE Client",
|
||||
"languages": ["Python", "JavaScript", "Java", "Go", "Rust"],
|
||||
"features": ["Authentication", "Data access", "Event subscription", "Query builder"],
|
||||
},
|
||||
"life_cli": {
|
||||
"name": "LIFE CLI",
|
||||
"commands": ["life init", "life deploy", "life test", "life monitor"],
|
||||
"features": ["Module scaffolding", "Local testing", "Deployment", "Debugging"],
|
||||
},
|
||||
"life_studio": {
|
||||
"name": "LIFE Studio",
|
||||
"type": "Visual IDE",
|
||||
"features": ["Module builder", "Data mapper", "AI model trainer", "Test runner"],
|
||||
},
|
||||
"life_marketplace": {
|
||||
"name": "LIFE Marketplace",
|
||||
"type": "Module marketplace",
|
||||
"features": ["Publish modules", "Discover modules", "Rate & review", "Billing"],
|
||||
"status": "Coming Q2 2027",
|
||||
},
|
||||
},
|
||||
"example_use_cases": [
|
||||
{
|
||||
"partner": "Insurance Company X",
|
||||
"module": "Custom Risk Assessment",
|
||||
"description": "Byggde en modul för att bedöma egendomsrisk med LIFE:s satellit- och fältdata",
|
||||
"time_to_market": "3 months",
|
||||
},
|
||||
{
|
||||
"partner": "Municipality Y",
|
||||
"module": "Urban Growth Monitor",
|
||||
"description": "Anpassade City Intelligence för att spåra illegal bebyggelse",
|
||||
"time_to_market": "2 months",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ─── LIFE OBJECT MODEL ────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/object-model")
|
||||
async def get_life_object_model():
|
||||
"""
|
||||
LIFE Object Model - Enhetlig objektsmodell för alla vertikaler
|
||||
"""
|
||||
return {
|
||||
"name": "LIFE Object Model",
|
||||
"version": "1.0.0",
|
||||
"description": "En enhetlig objektsmodell där allt representeras med samma grundstruktur",
|
||||
"principle": "En väg, en bro, ett sjukhus, ett biståndsprojekt eller ett träd är olika instanser av samma grundmodell.",
|
||||
"object_structure": {
|
||||
"identity": {
|
||||
"description": "Unik identifiering",
|
||||
"fields": ["uuid", "type", "name", "aliases", "external_ids"],
|
||||
"example": "uuid: 'obj-123', type: 'building', name: 'Mogadishu Primary School'",
|
||||
},
|
||||
"geometry": {
|
||||
"description": "Geospatial representation",
|
||||
"fields": ["point", "polygon", "bounding_box", "elevation"],
|
||||
"formats": ["GeoJSON", "WKT", "GeoHash"],
|
||||
},
|
||||
"time": {
|
||||
"description": "Temporal information",
|
||||
"fields": ["created_at", "updated_at", "valid_from", "valid_to", "observation_time"],
|
||||
},
|
||||
"evidence": {
|
||||
"description": "Kopplad evidens",
|
||||
"fields": ["observations", "sources", "confidence", "lineage"],
|
||||
},
|
||||
"state": {
|
||||
"description": "Nuvarande tillstånd",
|
||||
"fields": ["status", "progress", "health", "activity_level"],
|
||||
},
|
||||
"relationships": {
|
||||
"description": "Kopplingar till andra objekt",
|
||||
"fields": ["parent", "children", "related", "dependencies"],
|
||||
},
|
||||
"history": {
|
||||
"description": "Tidslinje över förändringar",
|
||||
"fields": ["events", "versions", "snapshots"],
|
||||
},
|
||||
"confidence": {
|
||||
"description": "Konfidensbedömning",
|
||||
"fields": ["score", "level", "factors", "uncertainty"],
|
||||
},
|
||||
"predictions": {
|
||||
"description": "Framtida prognoser",
|
||||
"fields": ["forecasts", "scenarios", "risk_assessments"],
|
||||
},
|
||||
"actions": {
|
||||
"description": "Möjliga åtgärder",
|
||||
"fields": ["recommended", "available", "history"],
|
||||
},
|
||||
},
|
||||
"examples": {
|
||||
"road": {
|
||||
"type": "infrastructure",
|
||||
"identity": "Road A-123",
|
||||
"geometry": "LineString",
|
||||
"state": {"status": "operational", "condition": 75},
|
||||
},
|
||||
"hospital": {
|
||||
"type": "facility",
|
||||
"identity": "Hospital B-456",
|
||||
"geometry": "Polygon",
|
||||
"state": {"status": "operational", "occupancy": 85},
|
||||
},
|
||||
"tree": {
|
||||
"type": "environmental",
|
||||
"identity": "Tree C-789",
|
||||
"geometry": "Point",
|
||||
"state": {"status": "healthy", "height_m": 12},
|
||||
},
|
||||
"project": {
|
||||
"type": "aid",
|
||||
"identity": "Project D-012",
|
||||
"geometry": "Polygon",
|
||||
"state": {"status": "active", "progress": 85},
|
||||
},
|
||||
},
|
||||
"benefits": [
|
||||
"Konsekvent modell över alla vertikaler",
|
||||
"Återanvändbara AI-modeller",
|
||||
"Enhetlig API",
|
||||
"Enklare integration",
|
||||
"Tydligare dataarkitektur",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ─── SYSTEM OVERVIEW ──────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/architecture")
|
||||
async def get_platform_architecture():
|
||||
"""
|
||||
Komplett plattformsarkitektur
|
||||
"""
|
||||
return {
|
||||
"system_name": "Landvex Intelligence Fusion Engine (LIFE) v4",
|
||||
"version": "4.0.0",
|
||||
"tagline": "Reality Intelligence Platform",
|
||||
"description": "En AI-drivn plattform för verklighetsintelligens som andra företag kan bygga ovanpå",
|
||||
"levels": [
|
||||
{
|
||||
"level": 1,
|
||||
"name": "Foundation",
|
||||
"function": "Teknisk kärna",
|
||||
"components": 12,
|
||||
"visible_to_verticals": False,
|
||||
},
|
||||
{
|
||||
"level": 2,
|
||||
"name": "Intelligence Runtime",
|
||||
"function": "AI-operativsystemet",
|
||||
"engines": 8,
|
||||
"visible_to_verticals": True,
|
||||
},
|
||||
{
|
||||
"level": 3,
|
||||
"name": "Domain Modules",
|
||||
"function": "Vertikala tillämpningar",
|
||||
"modules": 10,
|
||||
"visible_to_verticals": True,
|
||||
},
|
||||
{
|
||||
"level": 4,
|
||||
"name": "Experience Layer",
|
||||
"function": "Användargränssnitt",
|
||||
"interfaces": 9,
|
||||
"visible_to_verticals": True,
|
||||
},
|
||||
],
|
||||
"additional_components": {
|
||||
"sdk": {
|
||||
"name": "LIFE SDK",
|
||||
"function": "För externa utvecklare",
|
||||
"languages": ["Python", "JavaScript", "Java", "Go", "Rust"],
|
||||
},
|
||||
"object_model": {
|
||||
"name": "LIFE Object Model",
|
||||
"function": "Enhetlig objektsmodell",
|
||||
"attributes": 10,
|
||||
},
|
||||
},
|
||||
"strategic_positioning": "LIFE är en Reality Intelligence Platform. Många system analyserar dokument eller text. LIFE kombinerar geospatial information, tidsserier, visuella observationer och öppna datakällor till en spårbar evidensmodell. Den verkliga utmaningen är att visa att plattformen konsekvent kan leverera tillförlitliga, förklarbara analyser i flera olika domäner med samma underliggande arkitektur.",
|
||||
"key_differentiators": [
|
||||
"Reality Intelligence (inte bara textanalys)",
|
||||
"Evidence Before Opinion (spårbarhet)",
|
||||
"Multi-source fusion (inte enkel datakälla)",
|
||||
"Platform architecture (inte monolit)",
|
||||
"SDK för partners (ekosystem)",
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,854 @@
|
||||
"""
|
||||
LIFE v6 — Reality Intelligence Standard (RIS)
|
||||
|
||||
LIFE är inte bara programvara. Det definierar HUR verklighetsintelligens
|
||||
representeras, valideras, utbyts och används — oberoende av tillämpning.
|
||||
|
||||
Sex öppna standarder:
|
||||
- RIS-001: Reality Object Standard
|
||||
- RIS-002: Evidence Standard
|
||||
- RIS-003: Confidence Standard
|
||||
- RIS-004: Reality Exchange Protocol
|
||||
- RIS-005: Decision Standard
|
||||
- RIS-006: Learning Standard
|
||||
|
||||
Gemensam ontologi med tre nivåer:
|
||||
- Core Ontology
|
||||
- Domain Ontologies
|
||||
- Customer Extensions
|
||||
|
||||
Product Definition:
|
||||
"LIFE är ett AI-drivet operativsystem och en öppen standard för Reality Intelligence.
|
||||
Genom att förena en gemensam informationsmodell, spårbar evidens, förklarbara analyser
|
||||
och kontinuerligt lärande gör LIFE det möjligt att beskriva, förstå och följa
|
||||
förändringar i den fysiska världen på ett konsekvent sätt över olika domäner
|
||||
och organisationer."
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from app.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/life-standard", tags=["life_standard"])
|
||||
|
||||
|
||||
# ─── RIS-001 — REALITY OBJECT STANDARD ────────────────────────────────────────
|
||||
|
||||
@router.get("/public/ris-001-reality-object")
|
||||
async def get_ris_001_reality_object():
|
||||
"""
|
||||
RIS-001 — Reality Object Standard
|
||||
En gemensam objektsmodell för allt i den fysiska världen.
|
||||
"""
|
||||
return {
|
||||
"standard_id": "RIS-001",
|
||||
"name": "Reality Object Standard",
|
||||
"version": "1.0.0",
|
||||
"description": "En gemensam objektsmodell för allt. En väg, en byggnad, ett biståndsprojekt, en bro, ett träd, en ledning, ett fordon — alla är samma typ av objekt.",
|
||||
"universal_structure": {
|
||||
"identity": {
|
||||
"uuid": "universellt-unikt-id",
|
||||
"type": "road | building | project | bridge | tree | pipeline | vehicle | ...",
|
||||
"name": "Mänskligt läsbart namn",
|
||||
"external_ids": {
|
||||
"description": "ID:n från externa system",
|
||||
"examples": {
|
||||
"osm_id": "OpenStreetMap ID",
|
||||
"gadm_id": "Administrativt ID",
|
||||
"custom_id": "Kundspecifikt ID",
|
||||
},
|
||||
},
|
||||
"aliases": ["Alternativa namn", "Lokala namn"],
|
||||
},
|
||||
"geometry": {
|
||||
"type": "point | polygon | polyline | bounding_box | 3d_mesh",
|
||||
"coordinates": {
|
||||
"format": "GeoJSON / WKT",
|
||||
"crs": "EPSG:4326 (default)",
|
||||
"example": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[[42.36, -71.05], [42.37, -71.05], [42.37, -71.06], [42.36, -71.06], [42.36, -71.05]]],
|
||||
},
|
||||
},
|
||||
"spatial_resolution": "meter-noggrannhet",
|
||||
"bounding_box": {"min_lat": 0, "max_lat": 0, "min_lon": 0, "max_lon": 0},
|
||||
},
|
||||
"time": {
|
||||
"created_at": "ISO 8601 timestamp",
|
||||
"updated_at": "ISO 8601 timestamp",
|
||||
"valid_from": "När objektet började existera",
|
||||
"valid_until": "När objektet upphörde (null = aktivt)",
|
||||
"observation_window": {"start": "", "end": ""},
|
||||
},
|
||||
"state": {
|
||||
"current_status": "active | under_construction | damaged | destroyed | abandoned",
|
||||
"progress_percentage": "0-100 (om tillämpligt)",
|
||||
"health_score": "0-100",
|
||||
"last_observation_date": "ISO 8601",
|
||||
"state_history": [
|
||||
{"date": "", "status": "", "source": ""},
|
||||
],
|
||||
},
|
||||
"evidence": {
|
||||
"description": "Alla observationer som stöder nuvarande state",
|
||||
"observation_count": 0,
|
||||
"primary_sources": [],
|
||||
"evidence_lineage": "RIS-002 compliant",
|
||||
},
|
||||
"confidence": {
|
||||
"description": "Sammansatt konfidens för all information om objektet",
|
||||
"overall_score": "0-100",
|
||||
"breakdown": "RIS-003 compliant",
|
||||
},
|
||||
"relationships": {
|
||||
"description": "Kopplingar till andra objekt",
|
||||
"types": [
|
||||
{"type": "parent_of", "target": "uuid", "strength": 1.0},
|
||||
{"type": "depends_on", "target": "uuid", "strength": 0.9},
|
||||
{"type": "connected_to", "target": "uuid", "strength": 1.0},
|
||||
{"type": "near", "target": "uuid", "distance_meters": 100},
|
||||
],
|
||||
"graph_position": "Node i kunskapsgrafen",
|
||||
},
|
||||
"history": {
|
||||
"description": "Alla förändringar över tid",
|
||||
"events": [
|
||||
{"date": "", "event": "", "from_state": "", "to_state": "", "source": ""},
|
||||
],
|
||||
"snapshots": ["Tidsstämplade tillstånd"],
|
||||
},
|
||||
"predictions": {
|
||||
"description": "Framtida prognoser för objektet",
|
||||
"active_predictions": [
|
||||
{"horizon": "30d", "prediction": "", "confidence": 0, "model": ""},
|
||||
],
|
||||
"prediction_accuracy_history": "RIS-006 compliant",
|
||||
},
|
||||
"actions": {
|
||||
"description": "Rekommenderade och tillgängliga åtgärder",
|
||||
"recommended": [{"action": "", "priority": "", "expected_impact": ""}],
|
||||
"available": [{"action": "", "requirements": ""}],
|
||||
"taken": [{"action": "", "date": "", "result": ""}],
|
||||
},
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"type": "road",
|
||||
"name": "Highway A1",
|
||||
"identity": {"uuid": "road-001", "osm_id": "12345"},
|
||||
"geometry": {"type": "polyline", "length_km": 45},
|
||||
},
|
||||
{
|
||||
"type": "building",
|
||||
"name": "Central Hospital",
|
||||
"identity": {"uuid": "bld-001", "custom_id": "HOSP-SOM-001"},
|
||||
"geometry": {"type": "polygon", "area_sqm": 5000},
|
||||
},
|
||||
{
|
||||
"type": "project",
|
||||
"name": "School Construction Project",
|
||||
"identity": {"uuid": "proj-001"},
|
||||
"state": {"progress_percentage": 75, "health_score": 82},
|
||||
},
|
||||
],
|
||||
"compliance": "Alla objekt i LIFE måste följa RIS-001",
|
||||
"extensibility": "Kunder kan lägga till egna attribut under 'extensions' utan att bryta standarden",
|
||||
}
|
||||
|
||||
|
||||
# ─── RIS-002 — EVIDENCE STANDARD ──────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/ris-002-evidence")
|
||||
async def get_ris_002_evidence():
|
||||
"""
|
||||
RIS-002 — Evidence Standard
|
||||
All evidens representeras likadant oavsett källa.
|
||||
"""
|
||||
return {
|
||||
"standard_id": "RIS-002",
|
||||
"name": "Evidence Standard",
|
||||
"version": "1.0.0",
|
||||
"description": "All evidens representeras likadant. Varje observation innehåller standardiserade fält för spårbarhet och jämförbarhet.",
|
||||
"required_fields": {
|
||||
"evidence_id": {"type": "UUID", "description": "Unikt ID för evidensen"},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Källan till observationen",
|
||||
"examples": ["satellite", "quixzoom", "official_report", "news", "sensor", "human"],
|
||||
},
|
||||
"source_detail": {
|
||||
"type": "object",
|
||||
"description": "Detaljer om källan",
|
||||
"fields": {
|
||||
"provider": "ESA / QUIXZOOM / Government / NewsOrg / ...",
|
||||
"instrument": "Sentinel-2 / iPhone-14 / Drone / ...",
|
||||
"collection_method": "Automated / Crowdsourced / Manual / ...",
|
||||
},
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "ISO 8601",
|
||||
"description": "När observationen gjordes",
|
||||
},
|
||||
"geographic_position": {
|
||||
"type": "GeoJSON Point",
|
||||
"description": "Var observationen gjordes",
|
||||
"accuracy_meters": "GPS-noggrannhet",
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"description": "Hur observationen gjordes",
|
||||
"examples": ["visual_inspection", "spectral_analysis", "photogrammetry", "interview", "document_review"],
|
||||
},
|
||||
"quality": {
|
||||
"type": "integer",
|
||||
"range": "0-100",
|
||||
"description": "Kvalitetsbedömning av observationen",
|
||||
"factors": ["resolution", "clarity", "completeness", "relevance"],
|
||||
},
|
||||
"uncertainty": {
|
||||
"type": "object",
|
||||
"description": "Osäkerhet i observationen",
|
||||
"fields": {
|
||||
"spatial_meters": "±10m",
|
||||
"temporal_hours": "±2h",
|
||||
"attribute_confidence": "0-100",
|
||||
},
|
||||
},
|
||||
"validation_status": {
|
||||
"type": "enum",
|
||||
"values": ["unvalidated", "pending", "validated", "disputed", "rejected"],
|
||||
"description": "Valideringsstatus",
|
||||
},
|
||||
"revision_history": {
|
||||
"type": "array",
|
||||
"description": "Alla ändringar av evidensen",
|
||||
"entries": [
|
||||
{"date": "", "change": "", "by": "", "reason": ""},
|
||||
],
|
||||
},
|
||||
"raw_data_reference": {
|
||||
"type": "string",
|
||||
"description": "Pointer till rådata (bild, dokument, etc.)",
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Ytterligare kontext",
|
||||
"fields": {
|
||||
"weather": "Vid observationstillfället",
|
||||
"lighting": "Dagsljus / Mörker / ...",
|
||||
"season": "Årstid",
|
||||
"context_description": "Beskrivande text",
|
||||
},
|
||||
},
|
||||
},
|
||||
"evidence_types": {
|
||||
"visual": {"description": "Bilder, video, satellitbilder", "quality_factors": ["resolution", "angle", "occlusion"]},
|
||||
"documentary": {"description": "Rapporter, dokument, databaser", "quality_factors": ["authority", "recency", "completeness"]},
|
||||
"sensor": {"description": "IoT-sensorer, väderstationer", "quality_factors": ["calibration", "frequency", "reliability"]},
|
||||
"human": {"description": "Mänskliga observationer", "quality_factors": ["expertise", "training", "corroboration"]},
|
||||
"derived": {"description": "AI-genererad eller beräknad evidens", "quality_factors": ["model_version", "training_data", "validation"]},
|
||||
},
|
||||
"fusion_rules": {
|
||||
"description": "Hur evidens från olika källor kombineras",
|
||||
"rules": [
|
||||
"Minst 2 oberoende källor för 'validated' status",
|
||||
"Motsägelsefull evidens flaggas automatiskt",
|
||||
"Äldre evidens viktas lägre om nyare finns",
|
||||
"Mänsklig verifiering krävs för 'high-confidence' påståenden",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── RIS-003 — CONFIDENCE STANDARD ────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/ris-003-confidence")
|
||||
async def get_ris_003_confidence():
|
||||
"""
|
||||
RIS-003 — Confidence Standard
|
||||
Alla modeller använder samma definition av säkerhet.
|
||||
"""
|
||||
return {
|
||||
"standard_id": "RIS-003",
|
||||
"name": "Confidence Standard",
|
||||
"version": "1.0.0",
|
||||
"description": "Alla modeller använder samma definition av säkerhet. Olika analyser blir jämförbara.",
|
||||
"confidence_dimensions": {
|
||||
"data_confidence": {
|
||||
"description": "Hur tillförlitlig är rådatan?",
|
||||
"factors": [
|
||||
{"factor": "Source reliability", "weight": 0.3},
|
||||
{"factor": "Data freshness", "weight": 0.2},
|
||||
{"factor": "Spatial resolution", "weight": 0.15},
|
||||
{"factor": "Temporal coverage", "weight": 0.15},
|
||||
{"factor": "Data completeness", "weight": 0.2},
|
||||
],
|
||||
"scale": "0-100",
|
||||
},
|
||||
"source_confidence": {
|
||||
"description": "Hur tillförlitlig är källan?",
|
||||
"factors": [
|
||||
{"factor": "Historical accuracy", "weight": 0.4},
|
||||
{"factor": "Authority level", "weight": 0.25},
|
||||
{"factor": "Independence", "weight": 0.2},
|
||||
{"factor": "Methodology transparency", "weight": 0.15},
|
||||
],
|
||||
"scale": "0-100",
|
||||
},
|
||||
"ai_confidence": {
|
||||
"description": "Hur tillförlitlig är AI-modellen?",
|
||||
"factors": [
|
||||
{"factor": "Model accuracy (historical)", "weight": 0.35},
|
||||
{"factor": "Training data quality", "weight": 0.25},
|
||||
{"factor": "Validation performance", "weight": 0.25},
|
||||
{"factor": "Model version maturity", "weight": 0.15},
|
||||
],
|
||||
"scale": "0-100",
|
||||
},
|
||||
"human_verification": {
|
||||
"description": "Har människa verifierat?",
|
||||
"levels": [
|
||||
{"level": 0, "description": "Ingen mänsklig granskning", "score": 0},
|
||||
{"level": 1, "description": "En granskare", "score": 60},
|
||||
{"level": 2, "description": "Oberoende dubbelgranskning", "score": 85},
|
||||
{"level": 3, "description": "Expertpanel + fältverifiering", "score": 95},
|
||||
],
|
||||
},
|
||||
"temporal_confidence": {
|
||||
"description": "Hur relevant är informationen i tid?",
|
||||
"factors": [
|
||||
{"factor": "Data age", "weight": 0.5},
|
||||
{"factor": "Update frequency", "weight": 0.3},
|
||||
{"factor": "Volatility of phenomenon", "weight": 0.2},
|
||||
],
|
||||
"scale": "0-100",
|
||||
},
|
||||
},
|
||||
"composite_confidence": {
|
||||
"description": "Sammansatt konfidens från alla dimensioner",
|
||||
"formula": "Viktad summa av alla dimensioner",
|
||||
"example": {
|
||||
"data_confidence": 85,
|
||||
"source_confidence": 90,
|
||||
"ai_confidence": 78,
|
||||
"human_verification": 60,
|
||||
"temporal_confidence": 72,
|
||||
"composite": 80,
|
||||
"calculation": "(85*0.2 + 90*0.2 + 78*0.25 + 60*0.2 + 72*0.15) = 80.15",
|
||||
},
|
||||
"interpretation": {
|
||||
"90-100": "Very high confidence — suitable for critical decisions",
|
||||
"75-89": "High confidence — suitable for most decisions",
|
||||
"60-74": "Moderate confidence — supplementary verification recommended",
|
||||
"40-59": "Low confidence — significant uncertainty, use with caution",
|
||||
"0-39": "Very low confidence — insufficient for decision-making",
|
||||
},
|
||||
},
|
||||
"confidence_reporting": {
|
||||
"required": True,
|
||||
"format": "Alla insikter måste rapportera composite confidence + breakdown",
|
||||
"visualization": "Confidence badge med färgkodning",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── RIS-004 — REALITY EXCHANGE PROTOCOL ──────────────────────────────────────
|
||||
|
||||
@router.get("/public/ris-004-reality-exchange")
|
||||
async def get_ris_004_reality_exchange():
|
||||
"""
|
||||
RIS-004 — Reality Exchange Protocol
|
||||
Ett öppet format för utbyte mellan system.
|
||||
"""
|
||||
return {
|
||||
"standard_id": "RIS-004",
|
||||
"name": "Reality Exchange Protocol",
|
||||
"version": "1.0.0",
|
||||
"description": "Ett öppet format för utbyte mellan system. Kommuner, företag eller andra plattformar kan publicera eller konsumera observationer utan specialanpassningar. Ett slags 'REST för verklighetsobservationer'.",
|
||||
"protocol_design": {
|
||||
"paradigm": "RESTful API med semantiska resurser",
|
||||
"content_type": "application/vnd.life+json",
|
||||
"versioning": "URL-based (/v1/, /v2/)",
|
||||
"authentication": "OAuth 2.0 + API Keys",
|
||||
},
|
||||
"core_endpoints": {
|
||||
"/objects": {
|
||||
"methods": ["GET", "POST", "PUT", "DELETE"],
|
||||
"description": "CRUD för Reality Objects (RIS-001)",
|
||||
"query_params": ["type", "location", "bbox", "time_range", "status"],
|
||||
"response": "RIS-001 compliant object",
|
||||
},
|
||||
"/observations": {
|
||||
"methods": ["GET", "POST"],
|
||||
"description": "Skicka/ta emot observationer (RIS-002)",
|
||||
"query_params": ["object_id", "source", "time_range", "quality_min"],
|
||||
"response": "RIS-002 compliant evidence",
|
||||
},
|
||||
"/evidence": {
|
||||
"methods": ["GET", "POST"],
|
||||
"description": "Evidens för specifika påståenden",
|
||||
"query_params": ["claim_id", "object_id", "source_type"],
|
||||
"response": "Evidence with confidence scores",
|
||||
},
|
||||
"/relationships": {
|
||||
"methods": ["GET", "POST", "DELETE"],
|
||||
"description": "Grafrelationer mellan objekt",
|
||||
"query_params": ["from", "to", "type", "depth"],
|
||||
"response": "Graph edges",
|
||||
},
|
||||
"/events": {
|
||||
"methods": ["GET", "POST"],
|
||||
"description": "Händelser som påverkar objekt",
|
||||
"query_params": ["location", "type", "severity", "time_range"],
|
||||
"response": "Event stream",
|
||||
},
|
||||
"/hypotheses": {
|
||||
"methods": ["GET", "POST"],
|
||||
"description": "Genererade hypoteser",
|
||||
"query_params": ["object_id", "confidence_min", "status"],
|
||||
"response": "Ranked hypotheses",
|
||||
},
|
||||
"/predictions": {
|
||||
"methods": ["GET"],
|
||||
"description": "Framtida prognoser",
|
||||
"query_params": ["object_id", "horizon", "type"],
|
||||
"response": "Predictions with confidence intervals",
|
||||
},
|
||||
"/actions": {
|
||||
"methods": ["GET", "POST"],
|
||||
"description": "Rekommenderade åtgärder",
|
||||
"query_params": ["priority", "status", "assignee"],
|
||||
"response": "Action items",
|
||||
},
|
||||
"/missions": {
|
||||
"methods": ["GET", "POST", "PUT"],
|
||||
"description": "QUIXZOOM-uppdrag",
|
||||
"query_params": ["status", "region", "priority"],
|
||||
"response": "Mission details",
|
||||
},
|
||||
},
|
||||
"exchange_formats": {
|
||||
"reality_json": {
|
||||
"description": "Standard JSON-format för LIFE-data",
|
||||
"schema": "https://standard.life.ai/schemas/reality-json/v1",
|
||||
"features": ["Self-describing", "Extensible", "Versioned"],
|
||||
},
|
||||
"reality_feed": {
|
||||
"description": "Strömmande format för realtidsdata",
|
||||
"protocol": "WebSocket / SSE",
|
||||
"use_case": "Live observations, alerts",
|
||||
},
|
||||
"reality_batch": {
|
||||
"description": "Batch-format för stora datamängder",
|
||||
"format": "Parquet / GeoParquet",
|
||||
"compression": "Zstandard",
|
||||
},
|
||||
},
|
||||
"federation": {
|
||||
"description": "Distribuerade system kan utbyta data",
|
||||
"capabilities": [
|
||||
"Cross-platform object resolution",
|
||||
"Shared evidence validation",
|
||||
"Federated queries",
|
||||
"Trust delegation",
|
||||
],
|
||||
},
|
||||
"adoption": {
|
||||
"open_source": True,
|
||||
"license": "Apache 2.0",
|
||||
"reference_implementation": "https://github.com/landvex/life-standard",
|
||||
"documentation": "https://standard.life.ai",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── RIS-005 — DECISION STANDARD ──────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/ris-005-decision")
|
||||
async def get_ris_005_decision():
|
||||
"""
|
||||
RIS-005 — Decision Standard
|
||||
Alla rekommendationer följer samma struktur.
|
||||
"""
|
||||
return {
|
||||
"standard_id": "RIS-005",
|
||||
"name": "Decision Standard",
|
||||
"version": "1.0.0",
|
||||
"description": "Alla rekommendationer bör följa samma struktur. Beslutsstödet blir konsekvent över alla domäner.",
|
||||
"decision_pipeline": {
|
||||
"steps": [
|
||||
{
|
||||
"step": 1,
|
||||
"name": "Observation",
|
||||
"description": "Vad har observerats?",
|
||||
"output": "Rå observation (RIS-002)",
|
||||
"example": "Satellite imagery shows construction at 75% completion",
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"name": "Evidence",
|
||||
"description": "Vilken evidens stöder observationen?",
|
||||
"output": "Samlad evidens (RIS-002)",
|
||||
"example": "3 satellite passes, 2 field observations, 1 official report",
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"name": "Confidence",
|
||||
"description": "Hur säker är bedömningen?",
|
||||
"output": "Konfidensbedömning (RIS-003)",
|
||||
"example": "Composite confidence: 82% (High)",
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"name": "Hypothesis",
|
||||
"description": "Vad förklarar observationen?",
|
||||
"output": "Rankade hypoteser",
|
||||
"example": "H1: On schedule (75%) | H2: Delayed (20%) | H3: Ahead (5%)",
|
||||
},
|
||||
{
|
||||
"step": 5,
|
||||
"name": "Recommended Action",
|
||||
"description": "Vad bör göras?",
|
||||
"output": "Åtgärdsrekommendation",
|
||||
"example": "Schedule field verification within 14 days",
|
||||
},
|
||||
{
|
||||
"step": 6,
|
||||
"name": "Expected Outcome",
|
||||
"description": "Vad förväntas hända?",
|
||||
"output": "Prognos för åtgärdens effekt",
|
||||
"example": "Verification will increase confidence to 90% or identify issues",
|
||||
},
|
||||
{
|
||||
"step": 7,
|
||||
"name": "Verification Plan",
|
||||
"description": "Hur verifieras att åtgärden fungerade?",
|
||||
"output": "Plan för uppföljning",
|
||||
"example": "Compare next satellite pass with field report. Target: 90% confidence.",
|
||||
},
|
||||
],
|
||||
},
|
||||
"decision_object": {
|
||||
"structure": {
|
||||
"decision_id": "UUID",
|
||||
"timestamp": "ISO 8601",
|
||||
"context": {
|
||||
"domain": "infrastructure | aid | insurance | ...",
|
||||
"urgency": "low | medium | high | critical",
|
||||
"stakeholders": ["list"],
|
||||
},
|
||||
"observation": "RIS-002 reference",
|
||||
"evidence": "RIS-002 array",
|
||||
"confidence": "RIS-003 composite",
|
||||
"hypotheses": [
|
||||
{"rank": 1, "description": "", "probability": 0, "supporting_evidence": []},
|
||||
],
|
||||
"recommended_action": {
|
||||
"action": "",
|
||||
"priority": "",
|
||||
"rationale": "",
|
||||
"expected_outcome": "",
|
||||
"resources_required": [],
|
||||
"timeline": "",
|
||||
},
|
||||
"alternative_actions": [
|
||||
{"action": "", "pros": [], "cons": []},
|
||||
],
|
||||
"verification_plan": {
|
||||
"method": "",
|
||||
"success_criteria": "",
|
||||
"timeline": "",
|
||||
"responsible": "",
|
||||
},
|
||||
"audit_trail": [
|
||||
{"date": "", "event": "", "by": ""},
|
||||
],
|
||||
},
|
||||
},
|
||||
"decision_quality_metrics": {
|
||||
"description": "Hur bra är beslutsstödet?",
|
||||
"metrics": [
|
||||
{"metric": "Decision adoption rate", "formula": "Accepted recommendations / Total recommendations"},
|
||||
{"metric": "Decision accuracy", "formula": "Correct decisions / Total decisions"},
|
||||
{"metric": "Time to decision", "formula": "Minutes from login to decision"},
|
||||
{"metric": "Confidence calibration", "formula": "Actual accuracy vs stated confidence"},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── RIS-006 — LEARNING STANDARD ──────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/ris-006-learning")
|
||||
async def get_ris_006_learning():
|
||||
"""
|
||||
RIS-006 — Learning Standard
|
||||
AI-modeller utvärderas mot verkliga utfall.
|
||||
"""
|
||||
return {
|
||||
"standard_id": "RIS-006",
|
||||
"name": "Learning Standard",
|
||||
"version": "1.0.0",
|
||||
"description": "AI-modeller ska inte bara tränas — de ska också utvärderas mot verkliga utfall. Kontinuerlig kvalitetsförbättring.",
|
||||
"evaluation_dimensions": {
|
||||
"prediction_accuracy": {
|
||||
"description": "Hur ofta var prognosen korrekt?",
|
||||
"metrics": [
|
||||
{"metric": "Precision", "description": "Korrekta positiva / Alla positiva"},
|
||||
{"metric": "Recall", "description": "Korrekta positiva / Faktiska positiva"},
|
||||
{"metric": "F1 Score", "description": "Harmoniskt medelvärde av precision och recall"},
|
||||
{"metric": "MAE", "description": "Mean Absolute Error för kontinuerliga prediktioner"},
|
||||
{"metric": "Calibration", "description": "Stämmer konfidens med faktisk accuracy?"},
|
||||
],
|
||||
"tracking": "Per model, per domain, per prediction type",
|
||||
},
|
||||
"hypothesis_validation": {
|
||||
"description": "Vilka hypoteser visade sig stämma?",
|
||||
"metrics": [
|
||||
{"metric": "Hypothesis accuracy", "description": "Stämda hypoteser / Totala hypoteser"},
|
||||
{"metric": "Rank quality", "description": "Hur bra var rankningen?"},
|
||||
{"metric": "Time to validation", "description": "Hur lång tid tog det att verifiera?"},
|
||||
],
|
||||
},
|
||||
"signal_informativeness": {
|
||||
"description": "Vilka signaler var mest informativa?",
|
||||
"metrics": [
|
||||
{"metric": "Feature importance", "description": "Vilka källor bidrog mest?"},
|
||||
{"metric": "Information gain", "description": "Hur mycket minskade osäkerheten?"},
|
||||
{"metric": "Cost-effectiveness", "description": "Värde per insamlingskostnad"},
|
||||
],
|
||||
},
|
||||
"temporal_improvement": {
|
||||
"description": "Hur utvecklades träffsäkerheten över tid?",
|
||||
"metrics": [
|
||||
{"metric": "Accuracy trend", "description": "Stiger accuracy över tid?"},
|
||||
{"metric": "Learning rate", "description": "Hur snabbt förbättras modellen?"},
|
||||
{"metric": "Concept drift detection", "description": "Upptäcks förändringar i datan?"},
|
||||
],
|
||||
},
|
||||
},
|
||||
"feedback_loops": {
|
||||
"description": "Hur verkliga utfall matas tillbaka",
|
||||
"mechanisms": [
|
||||
{
|
||||
"name": "Outcome Reporting",
|
||||
"description": "Användare rapporterar faktiska utfall",
|
||||
"frequency": "Continuous",
|
||||
},
|
||||
{
|
||||
"name": "Automated Validation",
|
||||
"description": "Systemet validerar automatiskt mot nya observationer",
|
||||
"frequency": "Per observation cycle",
|
||||
},
|
||||
{
|
||||
"name": "Expert Review",
|
||||
"description": "Experter granskar beslut och utfall",
|
||||
"frequency": "Monthly",
|
||||
},
|
||||
{
|
||||
"name": "A/B Testing",
|
||||
"description": "Jämför olika modeller på samma data",
|
||||
"frequency": "Per model update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"model_lifecycle": {
|
||||
"description": "Modellens livscykel med kontinuerligt lärande",
|
||||
"stages": [
|
||||
{"stage": "Training", "description": "Initial träning på historisk data"},
|
||||
{"stage": "Validation", "description": "Validering på hållen testdata"},
|
||||
{"stage": "Deployment", "description": "Driftsättning med monitoring"},
|
||||
{"stage": "Monitoring", "description": "Kontinuerlig utvärdering"},
|
||||
{"stage": "Retraining", "description": "Omträning baserat på nya utfall"},
|
||||
{"stage": "Deprecation", "description": "Utfasning om bättre modell finns"},
|
||||
],
|
||||
},
|
||||
"quality_gates": {
|
||||
"description": "Krav för att en modell ska få användas",
|
||||
"minimum_requirements": {
|
||||
"accuracy_threshold": 75,
|
||||
"calibration_threshold": 80,
|
||||
"bias_check": "Pass",
|
||||
"explainability_score": 70,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── COMMON ONTOLOGY ──────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/ontology")
|
||||
async def get_ontology():
|
||||
"""
|
||||
Gemensam ontologi med tre nivåer
|
||||
"""
|
||||
return {
|
||||
"name": "LIFE Common Ontology",
|
||||
"version": "1.0.0",
|
||||
"description": "Tre nivåer av ontologi för att balansera generell kärna med domänspecifika och kundspecifika begrepp.",
|
||||
"levels": {
|
||||
"core_ontology": {
|
||||
"level": 1,
|
||||
"name": "Core Ontology",
|
||||
"description": "Universella begrepp som gäller över alla domäner",
|
||||
"concepts": [
|
||||
{"concept": "Object", "definition": "Något i den fysiska världen som kan observeras"},
|
||||
{"concept": "Observation", "definition": "En mätning eller iakttagelse av ett objekt"},
|
||||
{"concept": "Evidence", "definition": "Data som stöder eller motsäger ett påstående"},
|
||||
{"concept": "Relation", "definition": "En koppling mellan två objekt"},
|
||||
{"concept": "Event", "definition": "Något som händer vid en specifik tid och plats"},
|
||||
{"concept": "Hypothesis", "definition": "En förklaring som kan testas mot evidens"},
|
||||
{"concept": "Prediction", "definition": "En prognos om framtida tillstånd"},
|
||||
{"concept": "Action", "definition": "En åtgärd som kan vidtas"},
|
||||
{"concept": "Confidence", "definition": "Graden av säkerhet i en bedömning"},
|
||||
{"concept": "Time", "definition": "Temporal dimension för alla observationer"},
|
||||
{"concept": "Space", "definition": "Geografisk dimension för alla observationer"},
|
||||
{"concept": "Agent", "definition": "En entitet som kan utföra handlingar (människa, AI, organisation)"},
|
||||
],
|
||||
"stability": "FROZEN — Ändringar kräver major version bump",
|
||||
},
|
||||
"domain_ontologies": {
|
||||
"level": 2,
|
||||
"name": "Domain Ontologies",
|
||||
"description": "Begrepp som är specifika för en domän",
|
||||
"domains": [
|
||||
{
|
||||
"domain": "infrastructure",
|
||||
"concepts": [
|
||||
{"concept": "Road", "parent": "Object"},
|
||||
{"concept": "Bridge", "parent": "Object"},
|
||||
{"concept": "Building", "parent": "Object"},
|
||||
{"concept": "PavementCondition", "parent": "Observation"},
|
||||
{"concept": "StructuralIntegrity", "parent": "Observation"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"domain": "aid",
|
||||
"concepts": [
|
||||
{"concept": "Project", "parent": "Object"},
|
||||
{"concept": "Beneficiary", "parent": "Object"},
|
||||
{"concept": "Milestone", "parent": "Event"},
|
||||
{"concept": "Disbursement", "parent": "Event"},
|
||||
{"concept": "ImpactMetric", "parent": "Observation"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"domain": "insurance",
|
||||
"concepts": [
|
||||
{"concept": "Policy", "parent": "Object"},
|
||||
{"concept": "Claim", "parent": "Event"},
|
||||
{"concept": "RiskFactor", "parent": "Observation"},
|
||||
{"concept": "Premium", "parent": "Observation"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"domain": "environment",
|
||||
"concepts": [
|
||||
{"concept": "Ecosystem", "parent": "Object"},
|
||||
{"concept": "PollutionLevel", "parent": "Observation"},
|
||||
{"concept": "BiodiversityIndex", "parent": "Observation"},
|
||||
{"concept": "ClimateRisk", "parent": "Prediction"},
|
||||
],
|
||||
},
|
||||
],
|
||||
"stability": "STABLE — Ändringar kräver minor version bump",
|
||||
},
|
||||
"customer_extensions": {
|
||||
"level": 3,
|
||||
"name": "Customer Extensions",
|
||||
"description": "Möjlighet för varje kund att lägga till egna begrepp",
|
||||
"rules": [
|
||||
"Måste ärva från Core eller Domain",
|
||||
"Får inte ändra befintliga begrepp",
|
||||
"Måste följa namnkonventioner",
|
||||
"Bör dokumenteras i kundens ontologi-fil",
|
||||
],
|
||||
"example": {
|
||||
"customer": "Municipality of Stockholm",
|
||||
"extensions": [
|
||||
{"concept": "SnowRemovalRoute", "parent": "Road"},
|
||||
{"concept": "GrittingPriority", "parent": "Observation"},
|
||||
],
|
||||
},
|
||||
"stability": "FLEXIBLE — Kunden kontrollerar sina egna extensions",
|
||||
},
|
||||
},
|
||||
"ontology_management": {
|
||||
"versioning": "Semantisk versionshantering (MAJOR.MINOR.PATCH)",
|
||||
"compatibility": "Backward compatible inom major version",
|
||||
"migration": "Automatiska migreringsverktyg vid major bumps",
|
||||
"governance": "Ontology Review Board för Core-ändringar",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── PRODUCT DEFINITION v6 ────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/product-definition")
|
||||
async def get_product_definition_v6():
|
||||
"""
|
||||
LIFE v6 Product Definition
|
||||
"""
|
||||
return {
|
||||
"product_name": "Landvex Intelligence Fusion Engine (LIFE)",
|
||||
"version": "6.0.0",
|
||||
"tagline": "Reality Intelligence Standard",
|
||||
"definition": "LIFE är ett AI-drivet operativsystem och en öppen standard för Reality Intelligence. Genom att förena en gemensam informationsmodell, spårbar evidens, förklarbara analyser och kontinuerligt lärande gör LIFE det möjligt att beskriva, förstå och följa förändringar i den fysiska världen på ett konsekvent sätt över olika domäner och organisationer.",
|
||||
"evolution": {
|
||||
"v1": "Aid Intelligence — Biståndsövervakning",
|
||||
"v2": "Multi-signal — Evidence Before Opinion",
|
||||
"v3": "Core Architecture — 9 block, AI-operativsystem",
|
||||
"v4": "Platform — 4 nivåer, SDK, Object Model",
|
||||
"v5": "Enterprise — 5 pelare, governance, simulation",
|
||||
"v6": "Standard — 6 öppna standarder, ontologi, ekosystem",
|
||||
},
|
||||
"key_characteristics": [
|
||||
{
|
||||
"characteristic": "Open Standard",
|
||||
"description": "RIS-001 till RIS-006 definierar hur verklighetsintelligens representeras",
|
||||
"differentiator": True,
|
||||
},
|
||||
{
|
||||
"characteristic": "Reality Intelligence",
|
||||
"description": "Analyserar den fysiska världen, inte bara text",
|
||||
"differentiator": True,
|
||||
},
|
||||
{
|
||||
"characteristic": "Evidence Before Opinion",
|
||||
"description": "Börjar med observationer, inte slutsatser",
|
||||
"differentiator": True,
|
||||
},
|
||||
{
|
||||
"characteristic": "Multi-source Fusion",
|
||||
"description": "Kombinerar satellit, fält, öppna data och AI",
|
||||
"differentiator": True,
|
||||
},
|
||||
{
|
||||
"characteristic": "Traceability",
|
||||
"description": "Varje insikt spårbar till källor",
|
||||
"differentiator": True,
|
||||
},
|
||||
{
|
||||
"characteristic": "Explainability",
|
||||
"description": "Förklarbara analyser",
|
||||
"differentiator": True,
|
||||
},
|
||||
{
|
||||
"characteristic": "Ecosystem",
|
||||
"description": "Andra företag kan bygga ovanpå standarden",
|
||||
"differentiator": True,
|
||||
},
|
||||
],
|
||||
"strategic_positioning": {
|
||||
"not": "En enskild produkt eller applikation",
|
||||
"is": "Ett teknologiskt ramverk och öppen standard",
|
||||
"enables": "Andra att bygga Reality Intelligence-lösningar",
|
||||
"analogy": "Som HTTP för webben, men för verklighetsdata",
|
||||
},
|
||||
"long_term_ambition": "LIFE blir den dominerande standarden för hur organisationer representerar, validerar, utbyter och använder verklighetsintelligens.",
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
"""
|
||||
LIFE — Reality Intelligence Validation Program (RIVP)
|
||||
|
||||
Målet: Bevisa att LIFE fungerar i riktiga miljöer.
|
||||
|
||||
Faser:
|
||||
- Fas 1: Infrastruktur (vägar, broar, arbeten)
|
||||
- Fas 2: Kommun (parker, belysning, skyltning)
|
||||
- Fas 3: Fastigheter (byggnader, skador, underhåll)
|
||||
- Fas 4: Bistånd (projekt, observationer, verifiering)
|
||||
|
||||
Nyckeltal (KPI:er):
|
||||
- Datatäckning
|
||||
- Aktualitet
|
||||
- Evidenskvalitet
|
||||
- Analyskvalitet
|
||||
- Adoption
|
||||
- Interoperabilitet
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from app.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/life-validation", tags=["life_validation"])
|
||||
|
||||
|
||||
# ─── REALITY INTELLIGENCE VALIDATION PROGRAM ──────────────────────────────────
|
||||
|
||||
@router.get("/public/validation-program")
|
||||
async def get_validation_program():
|
||||
"""
|
||||
Reality Intelligence Validation Program (RIVP)
|
||||
Bevisa att LIFE fungerar i riktiga miljöer.
|
||||
"""
|
||||
return {
|
||||
"program_name": "Reality Intelligence Validation Program (RIVP)",
|
||||
"version": "1.0.0",
|
||||
"purpose": "Bevisa att LIFE fungerar i riktiga miljöer innan vidare arkitekturutveckling",
|
||||
"philosophy": "Många tekniska projekt fortsätter bygga lager utan att validera dem. RIVP säkerställer att varje fas bevisar verkligt värde.",
|
||||
"phases": {
|
||||
"phase_1_infrastructure": {
|
||||
"phase": 1,
|
||||
"name": "Infrastruktur",
|
||||
"duration": "6 månader",
|
||||
"goal": "Visa att LIFE kan övervaka infrastruktur i realtid",
|
||||
"objectives": [
|
||||
{
|
||||
"objective": "Hitta vägskador",
|
||||
"description": "Identifiera skador på vägar genom satellitbilder och fältobservationer",
|
||||
"success_criteria": ">80% precision i skadedetektion",
|
||||
"measurement": "Jämförelse med manuell inspektion",
|
||||
},
|
||||
{
|
||||
"objective": "Följa vägarbeten",
|
||||
"description": "Spåra framsteg i vägarbeten över tid",
|
||||
"success_criteria": "Progress estimation ±10% jämfört med rapporter",
|
||||
"measurement": "Veckovis jämförelse med entreprenörsrapporter",
|
||||
},
|
||||
{
|
||||
"objective": "Upptäcka förändringar",
|
||||
"description": "Automatisk detektion av förändringar i infrastruktur",
|
||||
"success_criteria": "<48h från förändring till detektion",
|
||||
"measurement": "Tidsstämplad jämförelse",
|
||||
},
|
||||
{
|
||||
"objective": "Verifiera reparationer",
|
||||
"description": "Bekräfta att rapporterade reparationer verkligen utförts",
|
||||
"success_criteria": ">90% korrekt verifiering",
|
||||
"measurement": "Fältbesök + satellitbilder",
|
||||
},
|
||||
],
|
||||
"pilot_locations": [
|
||||
{"location": "E4, Uppsala län", "type": "Highway", "length_km": 45},
|
||||
{"location": "Länsväg 272, Stockholm", "type": "County road", "length_km": 23},
|
||||
],
|
||||
"data_sources": ["Sentinel-2", "QUIXZOOM", "Trafikverket API"],
|
||||
"expected_outcomes": {
|
||||
"objects_tracked": 500,
|
||||
"observations_collected": 10000,
|
||||
"issues_detected": 150,
|
||||
"verification_rate": 90,
|
||||
},
|
||||
},
|
||||
"phase_2_municipality": {
|
||||
"phase": 2,
|
||||
"name": "Kommun",
|
||||
"duration": "6 månader",
|
||||
"goal": "Visa att LIFE kan hantera kommunala tillgångar",
|
||||
"objectives": [
|
||||
{
|
||||
"objective": "Följa parker",
|
||||
"description": "Övervaka skick och användning av kommunala parker",
|
||||
"success_criteria": "Veckovis uppdatering av parkstatus",
|
||||
"measurement": "Bildjämförelse + besöksdata",
|
||||
},
|
||||
{
|
||||
"objective": "Gatubelysning",
|
||||
"description": "Kartlägga och övervaka gatubelysning",
|
||||
"success_criteria": ">95% täckning av belysningspunkter",
|
||||
"measurement": "Fältverifiering",
|
||||
},
|
||||
{
|
||||
"objective": "Skyltning",
|
||||
"description": "Inventera och övervaka vägskyltar",
|
||||
"success_criteria": "Komplett inventering ±5%",
|
||||
"measurement": "Jämförelse med kommunalt register",
|
||||
},
|
||||
{
|
||||
"objective": "Avfall",
|
||||
"description": "Övervaka avfallshantering och återvinning",
|
||||
"success_criteria": "Identifiera 90% av illegala dumpningsplatser",
|
||||
"measurement": "Satellit + fältobservationer",
|
||||
},
|
||||
{
|
||||
"objective": "Vinterunderhåll",
|
||||
"description": "Spåra snöröjning och halkbekämpning",
|
||||
"success_criteria": "<4h från snöfall till uppdaterad status",
|
||||
"measurement": "Realtidsjämförelse",
|
||||
},
|
||||
],
|
||||
"pilot_locations": [
|
||||
{"location": "Uppsala kommun", "population": 230000},
|
||||
{"location": "Enköpings kommun", "population": 48000},
|
||||
],
|
||||
"data_sources": ["Satellit", "QUIXZOOM", "Kommunala API:er"],
|
||||
"expected_outcomes": {
|
||||
"objects_tracked": 5000,
|
||||
"observations_collected": 50000,
|
||||
"issues_detected": 300,
|
||||
"response_time_hours": 4,
|
||||
},
|
||||
},
|
||||
"phase_3_properties": {
|
||||
"phase": 3,
|
||||
"name": "Fastigheter",
|
||||
"duration": "6 månader",
|
||||
"goal": "Visa att LIFE kan övervaka byggnader och fastigheter",
|
||||
"objectives": [
|
||||
{
|
||||
"objective": "Följa byggnader",
|
||||
"description": "Spåra byggnaders skick och förändringar",
|
||||
"success_criteria": "Månatlig uppdatering av byggnadsstatus",
|
||||
"measurement": "Satellit + drönare + fält",
|
||||
},
|
||||
{
|
||||
"objective": "Upptäcka skador",
|
||||
"description": "Identifiera skador på tak, fasader, grunder",
|
||||
"success_criteria": ">75% precision i skadedetektion",
|
||||
"measurement": "Jämförelse med besiktning",
|
||||
},
|
||||
{
|
||||
"objective": "Följa underhåll",
|
||||
"description": "Spåra planerat och utfört underhåll",
|
||||
"success_criteria": "100% spårbarhet av underhållsåtgärder",
|
||||
"measurement": "Integration med underhållssystem",
|
||||
},
|
||||
{
|
||||
"objective": "Analysera risk",
|
||||
"description": "Bedöma risker för byggnader",
|
||||
"success_criteria": "Riskbedömning ±1 nivå jämfört med expert",
|
||||
"measurement": "Oberoende expertgranskning",
|
||||
},
|
||||
],
|
||||
"pilot_locations": [
|
||||
{"location": "Bostadsrättsföreningar, Uppsala", "properties": 50},
|
||||
{"location": "Kommunala fastigheter", "properties": 100},
|
||||
],
|
||||
"data_sources": ["Satellit", "Drönare", "Fältinspektion", "Fastighetsregister"],
|
||||
"expected_outcomes": {
|
||||
"objects_tracked": 150,
|
||||
"observations_collected": 3000,
|
||||
"issues_detected": 200,
|
||||
"risk_assessments": 150,
|
||||
},
|
||||
},
|
||||
"phase_4_aid": {
|
||||
"phase": 4,
|
||||
"name": "Bistånd",
|
||||
"duration": "12 månader",
|
||||
"goal": "Visa att LIFE kan övervaka biståndsprojekt i utvecklingsländer",
|
||||
"objectives": [
|
||||
{
|
||||
"objective": "Följa projekt över tid",
|
||||
"description": "Spåra framsteg i biståndsprojekt",
|
||||
"success_criteria": "Månatlig uppdatering ±15% jämfört med rapporter",
|
||||
"measurement": "Jämförelse med implementerarrapporter",
|
||||
},
|
||||
{
|
||||
"objective": "Samla oberoende observationer",
|
||||
"description": "Använda QUIXZOOM för oberoende verifiering",
|
||||
"success_criteria": ">50% av projekt med oberoende observationer",
|
||||
"measurement": "Antal verifierade projekt",
|
||||
},
|
||||
{
|
||||
"objective": "Kombinera flera datakällor",
|
||||
"description": "Fusionera satellit, fält, rapporter och nyheter",
|
||||
"success_criteria": ">3 oberoende källor per projekt",
|
||||
"measurement": "Källtäckning per projekt",
|
||||
},
|
||||
{
|
||||
"objective": "Identifiera verifieringsbehov",
|
||||
"description": "Automatisk flaggning av projekt som behöver extra granskning",
|
||||
"success_criteria": ">80% precision i flaggning",
|
||||
"measurement": "Jämförelse med manuell granskning",
|
||||
},
|
||||
],
|
||||
"pilot_locations": [
|
||||
{"location": "Somalia", "projects": 50, "focus": "Infrastructure"},
|
||||
{"location": "Kenya", "projects": 30, "focus": "Education"},
|
||||
{"location": "Malawi", "projects": 20, "focus": "Health"},
|
||||
],
|
||||
"data_sources": ["Sentinel-2", "QUIXZOOM", "Implementerarrapporter", "Nyheter", "Sociala medier"],
|
||||
"expected_outcomes": {
|
||||
"objects_tracked": 100,
|
||||
"observations_collected": 10000,
|
||||
"projects_monitored": 100,
|
||||
"verification_flags": 25,
|
||||
},
|
||||
},
|
||||
},
|
||||
"success_criteria": {
|
||||
"program_level": [
|
||||
"Alla 4 faser genomförda",
|
||||
"Minst 2 piloter per fas",
|
||||
"Dokumenterade resultat",
|
||||
"Publicerade case studies",
|
||||
],
|
||||
"technical": [
|
||||
">80% precision i detektion",
|
||||
"<48h aktualitet",
|
||||
">90% datatäckning",
|
||||
">3 oberoende källor per objekt",
|
||||
],
|
||||
"business": [
|
||||
"Minst 3 externa partners",
|
||||
"Minst 1 kommersiell pilot",
|
||||
"Positiv ROI i minst 1 fas",
|
||||
"Publicerad referensarkitektur",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── SUCCESS MEASUREMENT FRAMEWORK ────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/success-metrics")
|
||||
async def get_success_metrics():
|
||||
"""
|
||||
Ramverk för att mäta om LIFE lyckas som ekosystem.
|
||||
"""
|
||||
return {
|
||||
"framework_name": "LIFE Success Measurement Framework",
|
||||
"version": "1.0.0",
|
||||
"purpose": "Definiera hur vi mäter om plattformen lyckas — fokus på faktisk användning, inte antal moduler",
|
||||
"dimensions": {
|
||||
"data_coverage": {
|
||||
"dimension": "Datatäckning",
|
||||
"description": "Andel relevanta objekt eller områden med aktuell observationsdata",
|
||||
"metrics": [
|
||||
{
|
||||
"metric": "Object Coverage",
|
||||
"definition": "Andel registrerade objekt med minst 1 observation",
|
||||
"target": ">95%",
|
||||
"measurement": "Registrerade objekt / Objekt med observation",
|
||||
"frequency": "Veckovis",
|
||||
},
|
||||
{
|
||||
"metric": "Geographic Coverage",
|
||||
"definition": "Andel av target-området med data",
|
||||
"target": ">90%",
|
||||
"measurement": "Km² med data / Total target km²",
|
||||
"frequency": "Månadsvis",
|
||||
},
|
||||
{
|
||||
"metric": "Domain Coverage",
|
||||
"definition": "Andel domäner med aktiv datainsamling",
|
||||
"target": "100% av aktiva domäner",
|
||||
"measurement": "Domäner med data / Aktiva domäner",
|
||||
"frequency": "Månadsvis",
|
||||
},
|
||||
],
|
||||
},
|
||||
"freshness": {
|
||||
"dimension": "Aktualitet",
|
||||
"description": "Tid från förändring i verkligheten till registrerad observation",
|
||||
"metrics": [
|
||||
{
|
||||
"metric": "Detection Time",
|
||||
"definition": "Tid från förändring till detektion",
|
||||
"target": "<48h",
|
||||
"measurement": "Mediantid för kända förändringar",
|
||||
"frequency": "Veckovis",
|
||||
},
|
||||
{
|
||||
"metric": "Update Frequency",
|
||||
"definition": "Hur ofta objekt uppdateras",
|
||||
"target": "Veckovis för aktiva objekt",
|
||||
"measurement": "Dagar sedan senaste observation",
|
||||
"frequency": "Dagligen",
|
||||
},
|
||||
{
|
||||
"metric": "Alert Latency",
|
||||
"definition": "Tid från detektion till användaralert",
|
||||
"target": "<1h",
|
||||
"measurement": "Mediantid alert-utskick",
|
||||
"frequency": "Dagligen",
|
||||
},
|
||||
],
|
||||
},
|
||||
"evidence_quality": {
|
||||
"dimension": "Evidenskvalitet",
|
||||
"description": "Genomsnittlig konfidens och andel observationer med oberoende verifiering",
|
||||
"metrics": [
|
||||
{
|
||||
"metric": "Average Confidence",
|
||||
"definition": "Genomsnittlig konfidens över alla observationer",
|
||||
"target": ">75",
|
||||
"measurement": "Medel av RIS-003 composite scores",
|
||||
"frequency": "Veckovis",
|
||||
},
|
||||
{
|
||||
"metric": "Verification Rate",
|
||||
"definition": "Andel observationer med oberoende verifiering",
|
||||
"target": ">30%",
|
||||
"measurement": "Verifierade / Totala observationer",
|
||||
"frequency": "Månadsvis",
|
||||
},
|
||||
{
|
||||
"metric": "Source Diversity",
|
||||
"definition": "Genomsnittligt antal källor per objekt",
|
||||
"target": ">2.5",
|
||||
"measurement": "Antal unika källor / Antal objekt",
|
||||
"frequency": "Månadsvis",
|
||||
},
|
||||
{
|
||||
"metric": "Contradiction Rate",
|
||||
"definition": "Andel objekt med motstridig evidens",
|
||||
"target": "<5%",
|
||||
"measurement": "Objekt med flaggade motsägelser / Totala objekt",
|
||||
"frequency": "Veckovis",
|
||||
},
|
||||
],
|
||||
},
|
||||
"analysis_quality": {
|
||||
"dimension": "Analyskvalitet",
|
||||
"description": "Hur ofta användare bedömer analyser som användbara eller korrekta",
|
||||
"metrics": [
|
||||
{
|
||||
"metric": "User Satisfaction",
|
||||
"definition": "Andel användare som bedömer analyser som användbara",
|
||||
"target": ">80%",
|
||||
"measurement": "Enkäter + feedback",
|
||||
"frequency": "Kvartalsvis",
|
||||
},
|
||||
{
|
||||
"metric": "Decision Adoption",
|
||||
"definition": "Andel rekommendationer som leder till åtgärd",
|
||||
"target": ">60%",
|
||||
"measurement": "Genomförda åtgärder / Rekommendationer",
|
||||
"frequency": "Månadsvis",
|
||||
},
|
||||
{
|
||||
"metric": "Prediction Accuracy",
|
||||
"definition": "Hur ofta prognoser stämmer",
|
||||
"target": ">70%",
|
||||
"measurement": "Korrekta prognoser / Totala prognoser",
|
||||
"frequency": "Månadsvis",
|
||||
},
|
||||
{
|
||||
"metric": "Expert Agreement",
|
||||
"definition": "Hur ofta experter håller med om analyser",
|
||||
"target": ">75%",
|
||||
"measurement": "Oberoende expertgranskning",
|
||||
"frequency": "Kvartalsvis",
|
||||
},
|
||||
],
|
||||
},
|
||||
"adoption": {
|
||||
"dimension": "Adoption",
|
||||
"description": "Antal externa implementationer, partnerintegrationer och aktiva SDK-användare",
|
||||
"metrics": [
|
||||
{
|
||||
"metric": "External Implementations",
|
||||
"definition": "Antal oberoende RIS-implementationer",
|
||||
"target": ">10",
|
||||
"measurement": "Registrerade implementationer",
|
||||
"frequency": "Månadsvis",
|
||||
},
|
||||
{
|
||||
"metric": "Partner Integrations",
|
||||
"definition": "Antal system integrerade med LIFE",
|
||||
"target": ">20",
|
||||
"measurement": "Aktiva integrationer",
|
||||
"frequency": "Månadsvis",
|
||||
},
|
||||
{
|
||||
"metric": "SDK Users",
|
||||
"definition": "Antal aktiva SDK-användare",
|
||||
"target": ">100",
|
||||
"measurement": "Unika utvecklare med SDK-downloads",
|
||||
"frequency": "Månadsvis",
|
||||
},
|
||||
{
|
||||
"metric": "Certified Products",
|
||||
"definition": "Antal RIS-certifierade produkter",
|
||||
"target": ">5",
|
||||
"measurement": "Certifierade produkter",
|
||||
"frequency": "Kvartalsvis",
|
||||
},
|
||||
],
|
||||
},
|
||||
"interoperability": {
|
||||
"dimension": "Interoperabilitet",
|
||||
"description": "Antal system som kan utbyta data via RIS utan specialanpassning",
|
||||
"metrics": [
|
||||
{
|
||||
"metric": "RIS Compatible Systems",
|
||||
"definition": "Antal system med RIS Compatible-certifiering",
|
||||
"target": ">15",
|
||||
"measurement": "Certifierade system",
|
||||
"frequency": "Månadsvis",
|
||||
},
|
||||
{
|
||||
"metric": "Federated Queries",
|
||||
"definition": "Antal frågor över flera system",
|
||||
"target": ">1000/månad",
|
||||
"measurement": "Loggade federerade frågor",
|
||||
"frequency": "Månadsvis",
|
||||
},
|
||||
{
|
||||
"metric": "Data Exchange Volume",
|
||||
"definition": "Volym data utbytt via RIS",
|
||||
"target": ">1TB/månad",
|
||||
"measurement": "Bytes överförda",
|
||||
"frequency": "Månadsvis",
|
||||
},
|
||||
{
|
||||
"metric": "Cross-domain Usage",
|
||||
"definition": "Andel användare som använder flera domäner",
|
||||
"target": ">40%",
|
||||
"measurement": "Användare med >1 domän / Totala användare",
|
||||
"frequency": "Kvartalsvis",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"reporting": {
|
||||
"dashboard": "https://landvex.com/admin/validation-metrics",
|
||||
"frequency": "Månadsvis rapport till ledning",
|
||||
"public_transparency": "Kvartalsvis publicerad rapport",
|
||||
"benchmarking": "Jämförelse med branschstandarder",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ─── TECHNICAL SPECIFICATION RFC ──────────────────────────────────────────────
|
||||
|
||||
@router.get("/public/technical-specification")
|
||||
async def get_technical_specification():
|
||||
"""
|
||||
Teknisk specifikation — nästa viktiga leverans efter arkitekturen.
|
||||
"""
|
||||
return {
|
||||
"document_title": "Reality Intelligence Standard (RIS) Technical Specification",
|
||||
"version": "1.0.0-draft",
|
||||
"status": "Draft",
|
||||
"purpose": "Nästa viktiga leverans efter arkitekturen. En RFC/ISO-liknande standard som blir referens för utvecklare, partners och kunder.",
|
||||
"table_of_contents": {
|
||||
"1_executive_summary": {
|
||||
"section": 1,
|
||||
"title": "Executive Summary",
|
||||
"content": "Översikt över RIS och LIFE",
|
||||
},
|
||||
"2_vision_principles": {
|
||||
"section": 2,
|
||||
"title": "Vision and Principles",
|
||||
"content": [
|
||||
"Reality Intelligence definition",
|
||||
"Evidence Before Opinion",
|
||||
"Multi-source Fusion",
|
||||
"Traceability",
|
||||
"Explainability",
|
||||
"Openness",
|
||||
],
|
||||
},
|
||||
"3_architecture": {
|
||||
"section": 3,
|
||||
"title": "Architecture",
|
||||
"content": [
|
||||
"4-nivåers plattformsarkitektur",
|
||||
"9 Core Blocks",
|
||||
"5 Enterprise Pillars",
|
||||
"6 RIS Standards",
|
||||
],
|
||||
},
|
||||
"4_ontology": {
|
||||
"section": 4,
|
||||
"title": "Ontology",
|
||||
"content": [
|
||||
"Core Ontology (12 universella begrepp)",
|
||||
"Domain Ontologies (Infrastructure, Aid, Insurance, Environment)",
|
||||
"Customer Extensions",
|
||||
"Versioning and Governance",
|
||||
],
|
||||
},
|
||||
"5_ris_specifications": {
|
||||
"section": 5,
|
||||
"title": "RIS Specifications",
|
||||
"content": [
|
||||
"RIS-001: Reality Object Standard",
|
||||
"RIS-002: Evidence Standard",
|
||||
"RIS-003: Confidence Standard",
|
||||
"RIS-004: Reality Exchange Protocol",
|
||||
"RIS-005: Decision Standard",
|
||||
"RIS-006: Learning Standard",
|
||||
],
|
||||
},
|
||||
"6_object_model": {
|
||||
"section": 6,
|
||||
"title": "Object Model",
|
||||
"content": [
|
||||
"Identity",
|
||||
"Geometry",
|
||||
"Time",
|
||||
"State",
|
||||
"Evidence",
|
||||
"Confidence",
|
||||
"Relationships",
|
||||
"History",
|
||||
"Predictions",
|
||||
"Actions",
|
||||
],
|
||||
},
|
||||
"7_evidence_model": {
|
||||
"section": 7,
|
||||
"title": "Evidence Model",
|
||||
"content": [
|
||||
"Source",
|
||||
"Timestamp",
|
||||
"Geographic Position",
|
||||
"Method",
|
||||
"Quality",
|
||||
"Uncertainty",
|
||||
"Validation Status",
|
||||
"Revision History",
|
||||
],
|
||||
},
|
||||
"8_api_contracts": {
|
||||
"section": 8,
|
||||
"title": "API Contracts",
|
||||
"content": [
|
||||
"/objects",
|
||||
"/observations",
|
||||
"/evidence",
|
||||
"/relationships",
|
||||
"/events",
|
||||
"/hypotheses",
|
||||
"/predictions",
|
||||
"/actions",
|
||||
"/missions",
|
||||
],
|
||||
},
|
||||
"9_security_model": {
|
||||
"section": 9,
|
||||
"title": "Security Model",
|
||||
"content": [
|
||||
"Authentication (OAuth 2.0)",
|
||||
"Authorization (RBAC)",
|
||||
"Data Encryption",
|
||||
"Audit Logging",
|
||||
"Compliance (GDPR, ISO 27001)",
|
||||
],
|
||||
},
|
||||
"10_versioning": {
|
||||
"section": 10,
|
||||
"title": "Versioning Strategy",
|
||||
"content": [
|
||||
"Semantic Versioning",
|
||||
"Backward Compatibility",
|
||||
"Deprecation Policy",
|
||||
"Migration Guides",
|
||||
],
|
||||
},
|
||||
"11_governance": {
|
||||
"section": 11,
|
||||
"title": "Governance",
|
||||
"content": [
|
||||
"RIS Board (RISB)",
|
||||
"RFC Process",
|
||||
"Decision Making",
|
||||
"Community Participation",
|
||||
],
|
||||
},
|
||||
"12_reference_implementation": {
|
||||
"section": 12,
|
||||
"title": "Reference Implementation",
|
||||
"content": [
|
||||
"LIFE Server",
|
||||
"LIFE Client",
|
||||
"LIFE SDK",
|
||||
"Example Data",
|
||||
"Example APIs",
|
||||
],
|
||||
},
|
||||
"13_conformance": {
|
||||
"section": 13,
|
||||
"title": "Conformance Requirements",
|
||||
"content": [
|
||||
"RIS Compatible",
|
||||
"RIS Certified",
|
||||
"RIS Enterprise Certified",
|
||||
"Testing Procedures",
|
||||
],
|
||||
},
|
||||
"14_examples": {
|
||||
"section": 14,
|
||||
"title": "Examples and Test Cases",
|
||||
"content": [
|
||||
"Creating a Reality Object",
|
||||
"Submitting Evidence",
|
||||
"Querying with Confidence",
|
||||
"Building a Decision",
|
||||
"Interoperability Tests",
|
||||
],
|
||||
},
|
||||
},
|
||||
"target_audience": [
|
||||
"Software Developers",
|
||||
"System Architects",
|
||||
"Product Managers",
|
||||
"Technical Partners",
|
||||
"Customers",
|
||||
"Auditors",
|
||||
"Regulators",
|
||||
],
|
||||
"publication": {
|
||||
"formats": ["PDF", "HTML", "Markdown"],
|
||||
"url": "https://standard.life.ai/spec/v1.0.0",
|
||||
"license": "CC BY 4.0",
|
||||
"update_frequency": "Kvartalsvis",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
LIFE Live Data API
|
||||
Exponerar realtidsdata från multi-source pipeline
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
router = APIRouter(prefix="/live", tags=["live_data"])
|
||||
|
||||
DB_PATH = "/home/bernt/.openclaw/workspace/rivp-pilot-1/rivp.db"
|
||||
|
||||
def get_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
@router.get("/public/status")
|
||||
async def get_live_status():
|
||||
"""Get current system status"""
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
# Total counts
|
||||
c.execute("SELECT COUNT(*) FROM roads")
|
||||
total_roads = c.fetchone()[0]
|
||||
|
||||
c.execute("SELECT COUNT(*) FROM observations")
|
||||
total_obs = c.fetchone()[0]
|
||||
|
||||
# Today's observations
|
||||
c.execute("SELECT COUNT(*) FROM observations WHERE detected_date = date('now')")
|
||||
today_obs = c.fetchone()[0]
|
||||
|
||||
# By source
|
||||
c.execute("SELECT source, COUNT(*) as count FROM observations GROUP BY source ORDER BY count DESC")
|
||||
sources = {row[0]: row[1] for row in c.fetchall()}
|
||||
|
||||
# Recent activity (last hour)
|
||||
c.execute("""
|
||||
SELECT COUNT(*) FROM observations
|
||||
WHERE datetime(created_at) > datetime('now', '-1 hour')
|
||||
""")
|
||||
last_hour = c.fetchone()[0]
|
||||
|
||||
# Reality Latency calculation
|
||||
c.execute("""
|
||||
SELECT AVG(
|
||||
julianday('now') - julianday(detected_date)
|
||||
) * 24 as avg_latency_hours
|
||||
FROM observations
|
||||
WHERE detected_date >= date('now', '-7 days')
|
||||
""")
|
||||
avg_latency = c.fetchone()[0] or 0
|
||||
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"system_status": "operational",
|
||||
"data_freshness": {
|
||||
"total_roads": total_roads,
|
||||
"total_observations": total_obs,
|
||||
"observations_today": today_obs,
|
||||
"observations_last_hour": last_hour,
|
||||
"reality_latency_hours": round(avg_latency, 1)
|
||||
},
|
||||
"sources": sources,
|
||||
"pipeline_status": {
|
||||
"trafikverket": "active",
|
||||
"smhi": "active",
|
||||
"quixzoom": "active",
|
||||
"satellite": "active"
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/public/observations/stream")
|
||||
async def get_observation_stream(limit: int = 50):
|
||||
"""Get recent observations as a stream"""
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute("""
|
||||
SELECT o.*, r.name as road_name, r.county, r.road_number
|
||||
FROM observations o
|
||||
JOIN roads r ON o.road_id = r.id
|
||||
ORDER BY o.created_at DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
|
||||
observations = [dict(row) for row in c.fetchall()]
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"count": len(observations),
|
||||
"observations": observations
|
||||
}
|
||||
|
||||
@router.get("/public/alerts")
|
||||
async def get_active_alerts():
|
||||
"""Get active alerts (critical/high severity)"""
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute("""
|
||||
SELECT o.*, r.name as road_name, r.county
|
||||
FROM observations o
|
||||
JOIN roads r ON o.road_id = r.id
|
||||
WHERE o.severity IN ('critical', 'high')
|
||||
AND o.detected_date >= date('now', '-7 days')
|
||||
ORDER BY
|
||||
CASE o.severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
o.detected_date DESC
|
||||
""")
|
||||
|
||||
alerts = [dict(row) for row in c.fetchall()]
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"alert_count": len(alerts),
|
||||
"critical_count": sum(1 for a in alerts if a.get('severity') == 'critical'),
|
||||
"high_count": sum(1 for a in alerts if a.get('severity') == 'high'),
|
||||
"alerts": alerts
|
||||
}
|
||||
|
||||
@router.get("/public/coverage")
|
||||
async def get_coverage_map():
|
||||
"""Get coverage data for map visualization"""
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
# Get all observations with coordinates
|
||||
c.execute("""
|
||||
SELECT o.latitude, o.longitude, o.observation_type, o.severity, o.confidence,
|
||||
r.name as road_name, r.county
|
||||
FROM observations o
|
||||
JOIN roads r ON o.road_id = r.id
|
||||
WHERE o.latitude != 0 AND o.longitude != 0
|
||||
ORDER BY o.detected_date DESC
|
||||
LIMIT 500
|
||||
""")
|
||||
|
||||
points = []
|
||||
for row in c.fetchall():
|
||||
points.append({
|
||||
"lat": row[0],
|
||||
"lon": row[1],
|
||||
"type": row[2],
|
||||
"severity": row[3],
|
||||
"confidence": row[4],
|
||||
"road": row[5],
|
||||
"county": row[6]
|
||||
})
|
||||
|
||||
# Coverage by county
|
||||
c.execute("""
|
||||
SELECT r.county,
|
||||
COUNT(DISTINCT r.id) as roads,
|
||||
COUNT(o.id) as observations,
|
||||
AVG(o.confidence) as avg_confidence
|
||||
FROM roads r
|
||||
LEFT JOIN observations o ON r.id = o.road_id
|
||||
GROUP BY r.county
|
||||
ORDER BY observations DESC
|
||||
""")
|
||||
|
||||
coverage = []
|
||||
for row in c.fetchall():
|
||||
coverage.append({
|
||||
"county": row[0],
|
||||
"roads": row[1],
|
||||
"observations": row[2],
|
||||
"avg_confidence": round(row[3] or 0, 2)
|
||||
})
|
||||
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"total_points": len(points),
|
||||
"points": points,
|
||||
"coverage_by_county": coverage
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
"""
|
||||
quiXzoom Management Module
|
||||
Handles missions, contributors, submissions, and payments
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import Mission, Contributor, Submission, MissionStatus, User
|
||||
from app.core.security import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/quixzoom", tags=["quixzoom"])
|
||||
|
||||
# ─── MISSIONS ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/missions")
|
||||
async def list_missions(
|
||||
status: Optional[MissionStatus] = None,
|
||||
city: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List all missions with optional filtering."""
|
||||
query = db.query(Mission)
|
||||
if status:
|
||||
query = query.filter(Mission.status == status)
|
||||
if city:
|
||||
query = query.filter(Mission.city.ilike(f"%{city}%"))
|
||||
missions = query.offset(skip).limit(limit).all()
|
||||
return {
|
||||
"missions": [
|
||||
{
|
||||
"id": m.id,
|
||||
"mission_id": m.mission_id,
|
||||
"title": m.title,
|
||||
"city": m.city,
|
||||
"country": m.country,
|
||||
"status": m.status.value,
|
||||
"reward": f"{m.reward_amount} {m.reward_currency}" if m.reward_amount else None,
|
||||
"start_date": m.start_date.isoformat() if m.start_date else None,
|
||||
"end_date": m.end_date.isoformat() if m.end_date else None,
|
||||
}
|
||||
for m in missions
|
||||
],
|
||||
"total": query.count(),
|
||||
}
|
||||
|
||||
@router.post("/missions")
|
||||
async def create_mission(
|
||||
mission_id: str,
|
||||
title: str,
|
||||
city: str,
|
||||
country: str,
|
||||
reward_amount: float,
|
||||
description: Optional[str] = None,
|
||||
requirements: Optional[List[str]] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new mission."""
|
||||
mission = Mission(
|
||||
mission_id=mission_id,
|
||||
title=title,
|
||||
description=description,
|
||||
city=city,
|
||||
country=country,
|
||||
reward_amount=reward_amount,
|
||||
requirements=requirements,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
created_by=current_user.id,
|
||||
)
|
||||
db.add(mission)
|
||||
db.commit()
|
||||
db.refresh(mission)
|
||||
return {"status": "created", "mission_id": mission.id}
|
||||
|
||||
@router.get("/missions/{mission_id}")
|
||||
async def get_mission(
|
||||
mission_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get mission details."""
|
||||
mission = db.query(Mission).filter(Mission.id == mission_id).first()
|
||||
if not mission:
|
||||
raise HTTPException(status_code=404, detail="Mission not found")
|
||||
return {
|
||||
"id": mission.id,
|
||||
"mission_id": mission.mission_id,
|
||||
"title": mission.title,
|
||||
"description": mission.description,
|
||||
"city": mission.city,
|
||||
"country": mission.country,
|
||||
"location": mission.location,
|
||||
"status": mission.status.value,
|
||||
"reward_amount": float(mission.reward_amount) if mission.reward_amount else None,
|
||||
"reward_currency": mission.reward_currency,
|
||||
"requirements": mission.requirements,
|
||||
"start_date": mission.start_date.isoformat() if mission.start_date else None,
|
||||
"end_date": mission.end_date.isoformat() if mission.end_date else None,
|
||||
}
|
||||
|
||||
@router.put("/missions/{mission_id}")
|
||||
async def update_mission(
|
||||
mission_id: int,
|
||||
title: Optional[str] = None,
|
||||
status: Optional[MissionStatus] = None,
|
||||
reward_amount: Optional[float] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update a mission."""
|
||||
mission = db.query(Mission).filter(Mission.id == mission_id).first()
|
||||
if not mission:
|
||||
raise HTTPException(status_code=404, detail="Mission not found")
|
||||
|
||||
if title:
|
||||
mission.title = title
|
||||
if status:
|
||||
mission.status = status
|
||||
if reward_amount:
|
||||
mission.reward_amount = reward_amount
|
||||
|
||||
db.commit()
|
||||
db.refresh(mission)
|
||||
return {"status": "updated", "mission_id": mission.id}
|
||||
|
||||
# ─── CONTRIBUTORS ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/contributors")
|
||||
async def list_contributors(
|
||||
status: Optional[str] = None,
|
||||
city: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List all contributors."""
|
||||
query = db.query(Contributor)
|
||||
if status:
|
||||
query = query.filter(Contributor.status == status)
|
||||
if city:
|
||||
query = query.filter(Contributor.city.ilike(f"%{city}%"))
|
||||
contributors = query.offset(skip).limit(limit).all()
|
||||
return {
|
||||
"contributors": [
|
||||
{
|
||||
"id": c.id,
|
||||
"contributor_id": c.contributor_id,
|
||||
"full_name": c.full_name,
|
||||
"email": c.email,
|
||||
"city": c.city,
|
||||
"country": c.country,
|
||||
"reputation_score": c.reputation_score,
|
||||
"total_earnings": float(c.total_earnings) if c.total_earnings else 0,
|
||||
"total_submissions": c.total_submissions,
|
||||
"verified": c.verified,
|
||||
"status": c.status,
|
||||
}
|
||||
for c in contributors
|
||||
],
|
||||
"total": query.count(),
|
||||
}
|
||||
|
||||
@router.post("/contributors")
|
||||
async def create_contributor(
|
||||
contributor_id: str,
|
||||
full_name: str,
|
||||
email: str,
|
||||
city: Optional[str] = None,
|
||||
country: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Register a new contributor."""
|
||||
contributor = Contributor(
|
||||
contributor_id=contributor_id,
|
||||
full_name=full_name,
|
||||
email=email,
|
||||
city=city,
|
||||
country=country,
|
||||
)
|
||||
db.add(contributor)
|
||||
db.commit()
|
||||
db.refresh(contributor)
|
||||
return {"status": "created", "contributor_id": contributor.id}
|
||||
|
||||
@router.get("/contributors/{contributor_id}")
|
||||
async def get_contributor(
|
||||
contributor_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get contributor details."""
|
||||
contributor = db.query(Contributor).filter(Contributor.id == contributor_id).first()
|
||||
if not contributor:
|
||||
raise HTTPException(status_code=404, detail="Contributor not found")
|
||||
return {
|
||||
"id": contributor.id,
|
||||
"contributor_id": contributor.contributor_id,
|
||||
"full_name": contributor.full_name,
|
||||
"email": contributor.email,
|
||||
"phone": contributor.phone,
|
||||
"city": contributor.city,
|
||||
"country": contributor.country,
|
||||
"reputation_score": contributor.reputation_score,
|
||||
"total_earnings": float(contributor.total_earnings) if contributor.total_earnings else 0,
|
||||
"total_submissions": contributor.total_submissions,
|
||||
"verified": contributor.verified,
|
||||
"status": contributor.status,
|
||||
}
|
||||
|
||||
# ─── SUBMISSIONS ──────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/submissions")
|
||||
async def list_submissions(
|
||||
mission_id: Optional[int] = None,
|
||||
status: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""List submissions with filtering."""
|
||||
query = db.query(Submission)
|
||||
if mission_id:
|
||||
query = query.filter(Submission.mission_id == mission_id)
|
||||
if status:
|
||||
query = query.filter(Submission.status == status)
|
||||
submissions = query.offset(skip).limit(limit).all()
|
||||
return {
|
||||
"submissions": [
|
||||
{
|
||||
"id": s.id,
|
||||
"submission_id": s.submission_id,
|
||||
"mission_id": s.mission_id,
|
||||
"contributor_id": s.contributor_id,
|
||||
"status": s.status,
|
||||
"ai_score": s.ai_score,
|
||||
"submitted_at": s.submitted_at.isoformat() if s.submitted_at else None,
|
||||
"reviewed_at": s.reviewed_at.isoformat() if s.reviewed_at else None,
|
||||
}
|
||||
for s in submissions
|
||||
],
|
||||
"total": query.count(),
|
||||
}
|
||||
|
||||
@router.put("/submissions/{submission_id}/review")
|
||||
async def review_submission(
|
||||
submission_id: int,
|
||||
status: str, # approved, rejected
|
||||
reviewer_notes: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Review a submission."""
|
||||
submission = db.query(Submission).filter(Submission.id == submission_id).first()
|
||||
if not submission:
|
||||
raise HTTPException(status_code=404, detail="Submission not found")
|
||||
|
||||
submission.status = status
|
||||
submission.reviewer_notes = reviewer_notes
|
||||
submission.reviewed_at = datetime.utcnow()
|
||||
submission.reviewed_by = current_user.id
|
||||
|
||||
db.commit()
|
||||
db.refresh(submission)
|
||||
return {"status": "reviewed", "submission_id": submission.id, "new_status": status}
|
||||
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
RIVP Pilot 1 - Real Infrastructure Monitoring
|
||||
Reads from SQLite database with real road network and generated observations
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
import sqlite3
|
||||
import os
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter(prefix="/rivp-pilot1", tags=["rivp_pilot1"])
|
||||
|
||||
DB_PATH = "/home/bernt/.openclaw/workspace/rivp-pilot-1/rivp.db"
|
||||
|
||||
def get_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
@router.get("/public/roads")
|
||||
async def get_roads(county: Optional[str] = None, road_type: Optional[str] = None):
|
||||
"""Get all roads with optional filtering"""
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
query = "SELECT * FROM roads WHERE 1=1"
|
||||
params = []
|
||||
|
||||
if county:
|
||||
query += " AND county = ?"
|
||||
params.append(county)
|
||||
if road_type:
|
||||
query += " AND type = ?"
|
||||
params.append(road_type)
|
||||
|
||||
query += " ORDER BY county, name"
|
||||
|
||||
c.execute(query, params)
|
||||
roads = [dict(row) for row in c.fetchall()]
|
||||
|
||||
# Get observation count for each road
|
||||
for road in roads:
|
||||
c.execute("SELECT COUNT(*) FROM observations WHERE road_id = ?", (road['id'],))
|
||||
road['observation_count'] = c.fetchone()[0]
|
||||
|
||||
conn.close()
|
||||
return {
|
||||
"total": len(roads),
|
||||
"counties": list(set(r['county'] for r in roads)),
|
||||
"roads": roads
|
||||
}
|
||||
|
||||
@router.get("/public/observations")
|
||||
async def get_observations(road_id: Optional[int] = None,
|
||||
observation_type: Optional[str] = None,
|
||||
severity: Optional[str] = None,
|
||||
source: Optional[str] = None,
|
||||
verified_only: bool = False):
|
||||
"""Get observations with filtering"""
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
query = """
|
||||
SELECT o.*, r.name as road_name, r.county
|
||||
FROM observations o
|
||||
JOIN roads r ON o.road_id = r.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
params = []
|
||||
|
||||
if road_id:
|
||||
query += " AND o.road_id = ?"
|
||||
params.append(road_id)
|
||||
if observation_type:
|
||||
query += " AND o.observation_type = ?"
|
||||
params.append(observation_type)
|
||||
if severity:
|
||||
query += " AND o.severity = ?"
|
||||
params.append(severity)
|
||||
if source:
|
||||
query += " AND o.source = ?"
|
||||
params.append(source)
|
||||
if verified_only:
|
||||
query += " AND o.verified = 1"
|
||||
|
||||
query += " ORDER BY o.detected_date DESC"
|
||||
|
||||
c.execute(query, params)
|
||||
observations = [dict(row) for row in c.fetchall()]
|
||||
|
||||
conn.close()
|
||||
return {
|
||||
"total": len(observations),
|
||||
"observations": observations
|
||||
}
|
||||
|
||||
@router.get("/public/dashboard")
|
||||
async def get_dashboard():
|
||||
"""Get dashboard statistics"""
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
# Total roads
|
||||
c.execute("SELECT COUNT(*) FROM roads")
|
||||
total_roads = c.fetchone()[0]
|
||||
|
||||
# Total observations
|
||||
c.execute("SELECT COUNT(*) FROM observations")
|
||||
total_observations = c.fetchone()[0]
|
||||
|
||||
# By type
|
||||
c.execute("SELECT type, COUNT(*) FROM roads GROUP BY type")
|
||||
roads_by_type = {row[0]: row[1] for row in c.fetchall()}
|
||||
|
||||
# By county
|
||||
c.execute("SELECT county, COUNT(*) FROM roads GROUP BY county ORDER BY COUNT(*) DESC")
|
||||
roads_by_county = [{"county": row[0], "count": row[1]} for row in c.fetchall()]
|
||||
|
||||
# Observations by type
|
||||
c.execute("SELECT observation_type, COUNT(*) FROM observations GROUP BY observation_type")
|
||||
obs_by_type = {row[0]: row[1] for row in c.fetchall()}
|
||||
|
||||
# Observations by source
|
||||
c.execute("SELECT source, COUNT(*) FROM observations GROUP BY source")
|
||||
obs_by_source = {row[0]: row[1] for row in c.fetchall()}
|
||||
|
||||
# Observations by severity
|
||||
c.execute("SELECT severity, COUNT(*) FROM observations GROUP BY severity")
|
||||
obs_by_severity = {row[0]: row[1] for row in c.fetchall()}
|
||||
|
||||
# Verified vs unverified
|
||||
c.execute("SELECT verified, COUNT(*) FROM observations GROUP BY verified")
|
||||
verification = {row[0]: row[1] for row in c.fetchall()}
|
||||
|
||||
# Average confidence
|
||||
c.execute("SELECT AVG(confidence) FROM observations")
|
||||
avg_confidence = c.fetchone()[0]
|
||||
|
||||
# Recent observations (last 30 days)
|
||||
c.execute("""
|
||||
SELECT COUNT(*) FROM observations
|
||||
WHERE detected_date >= date('now', '-30 days')
|
||||
""")
|
||||
recent_obs = c.fetchone()[0]
|
||||
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"roads": {
|
||||
"total": total_roads,
|
||||
"by_type": roads_by_type,
|
||||
"by_county": roads_by_county[:10]
|
||||
},
|
||||
"observations": {
|
||||
"total": total_observations,
|
||||
"by_type": obs_by_type,
|
||||
"by_source": obs_by_source,
|
||||
"by_severity": obs_by_severity,
|
||||
"verification": verification,
|
||||
"average_confidence": round(avg_confidence, 2),
|
||||
"last_30_days": recent_obs
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/public/map-data")
|
||||
async def get_map_data(county: Optional[str] = None):
|
||||
"""Get observation data for map visualization"""
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
query = """
|
||||
SELECT o.id, o.latitude, o.longitude, o.observation_type,
|
||||
o.confidence, o.severity, o.detected_date, o.verified,
|
||||
r.name as road_name, r.county
|
||||
FROM observations o
|
||||
JOIN roads r ON o.road_id = r.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
params = []
|
||||
|
||||
if county:
|
||||
query += " AND r.county = ?"
|
||||
params.append(county)
|
||||
|
||||
c.execute(query, params)
|
||||
points = [dict(row) for row in c.fetchall()]
|
||||
|
||||
conn.close()
|
||||
return {
|
||||
"total": len(points),
|
||||
"points": points
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
"""
|
||||
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,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
Tenant CRUD-endpoints för LandveX Admin Backend.
|
||||
Inkluderar audit-logging, validering, felhantering och schema-isolering.
|
||||
"""
|
||||
from typing import Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, HTTPException, status, Query
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_user, require_admin
|
||||
from app.schemas import (
|
||||
TenantCreate, TenantUpdate, TenantOut, TenantDetailOut,
|
||||
TenantListOut, TenantListParams, TenantStatusUpdate,
|
||||
TenantActivateRequest, TenantSuspendRequest, TenantStats,
|
||||
SuccessResponse, AuditLogListOut
|
||||
)
|
||||
from app.services.tenant_service import TenantService
|
||||
from app.services.audit_service import AuditService
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/tenants", tags=["Tenants"])
|
||||
|
||||
|
||||
def _request_context(request: Request, current_user: User) -> Dict[str, Any]:
|
||||
"""Bygg request context för audit-logging."""
|
||||
return {
|
||||
"request_id": request.headers.get("X-Request-ID", ""),
|
||||
"request_method": request.method,
|
||||
"request_path": str(request.url.path),
|
||||
"actor_ip": request.client.host if request.client else None,
|
||||
"actor_user_agent": request.headers.get("user-agent", ""),
|
||||
}
|
||||
|
||||
|
||||
# ─── LIST ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("", response_model=TenantListOut)
|
||||
async def list_tenants(
|
||||
request: Request,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
status: Optional[str] = Query(None),
|
||||
tenant_type: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None, max_length=100),
|
||||
country: Optional[str] = Query(None, max_length=2),
|
||||
city: Optional[str] = Query(None, max_length=100),
|
||||
sort_by: str = Query("created_at"),
|
||||
sort_order: str = Query("desc", pattern=r"^(asc|desc)$"),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
current_user: User = Depends(require_admin),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Lista alla tenants med paginering, filtrering och sortering."""
|
||||
from app.models import TenantStatus as TS, TenantType as TT
|
||||
|
||||
params = TenantListParams(
|
||||
page=page, page_size=page_size,
|
||||
sort_by=sort_by, sort_order=sort_order,
|
||||
search=search, country=country, city=city,
|
||||
is_active=is_active
|
||||
)
|
||||
|
||||
if status:
|
||||
try:
|
||||
params.status = TS(status)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid status: {status}")
|
||||
|
||||
if tenant_type:
|
||||
try:
|
||||
params.tenant_type = TT(tenant_type)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid tenant_type: {tenant_type}")
|
||||
|
||||
service = TenantService(db)
|
||||
tenants, total = await service.list_tenants(params)
|
||||
pages = (total + page_size - 1) // page_size
|
||||
|
||||
return TenantListOut(
|
||||
items=[TenantOut.model_validate(t.to_dict()) for t in tenants],
|
||||
total=total, page=page, page_size=page_size, pages=pages
|
||||
)
|
||||
|
||||
|
||||
# ─── STATS ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/stats", response_model=TenantStats)
|
||||
async def get_tenant_stats(
|
||||
current_user: User = Depends(require_admin),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Hämta aggregerad statistik om alla tenants."""
|
||||
service = TenantService(db)
|
||||
stats = await service.get_statistics()
|
||||
return TenantStats.model_validate(stats)
|
||||
|
||||
|
||||
# ─── GET ONE ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/{tenant_id}", response_model=TenantDetailOut)
|
||||
async def get_tenant(
|
||||
tenant_id: UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Hämta en tenant med ID. Admin ser allt, andra bara sin egen."""
|
||||
service = TenantService(db)
|
||||
tenant = await service.get_by_id(tenant_id)
|
||||
if tenant is None:
|
||||
raise HTTPException(status_code=404, detail="Tenant not found")
|
||||
|
||||
user_role = current_user.role.value if hasattr(current_user.role, 'value') else str(current_user.role)
|
||||
user_tenant = str(current_user.tenant_id) if current_user.tenant_id else None
|
||||
|
||||
if user_role != "admin" and user_tenant != str(tenant_id):
|
||||
raise HTTPException(status_code=403, detail="Access denied for this tenant")
|
||||
|
||||
include_sensitive = user_role == "admin"
|
||||
return tenant.to_dict(include_sensitive=include_sensitive)
|
||||
|
||||
|
||||
# ─── CREATE ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("", response_model=TenantDetailOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_tenant(
|
||||
request: Request,
|
||||
data: TenantCreate,
|
||||
current_user: User = Depends(require_admin),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Skapa en ny tenant (kommun eller företag)."""
|
||||
service = TenantService(db)
|
||||
tenant = await service.create_tenant(
|
||||
data=data,
|
||||
actor_id=str(current_user.id),
|
||||
request_context=_request_context(request, current_user)
|
||||
)
|
||||
return tenant.to_dict(include_sensitive=True)
|
||||
|
||||
|
||||
# ─── UPDATE ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.patch("/{tenant_id}", response_model=TenantDetailOut)
|
||||
async def update_tenant(
|
||||
request: Request,
|
||||
tenant_id: UUID,
|
||||
data: TenantUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Uppdatera en tenant. Endast angivna fält uppdateras."""
|
||||
user_role = current_user.role.value if hasattr(current_user.role, 'value') else str(current_user.role)
|
||||
user_tenant = str(current_user.tenant_id) if current_user.tenant_id else None
|
||||
|
||||
if user_role != "admin" and user_tenant != str(tenant_id):
|
||||
raise HTTPException(status_code=403, detail="Access denied for this tenant")
|
||||
|
||||
service = TenantService(db)
|
||||
tenant = await service.update_tenant(
|
||||
tenant_id=tenant_id,
|
||||
data=data,
|
||||
actor_id=str(current_user.id),
|
||||
request_context=_request_context(request, current_user)
|
||||
)
|
||||
include_sensitive = user_role == "admin"
|
||||
return tenant.to_dict(include_sensitive=include_sensitive)
|
||||
|
||||
|
||||
# ─── DELETE (soft) ────────────────────────────────────────────────────────────
|
||||
|
||||
@router.delete("/{tenant_id}", response_model=SuccessResponse)
|
||||
async def delete_tenant(
|
||||
request: Request,
|
||||
tenant_id: UUID,
|
||||
current_user: User = Depends(require_admin),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Mjuk-radera en tenant. Datat bevaras men markeras som borttaget."""
|
||||
service = TenantService(db)
|
||||
tenant = await service.delete_tenant(
|
||||
tenant_id=tenant_id,
|
||||
actor_id=str(current_user.id),
|
||||
request_context=_request_context(request, current_user)
|
||||
)
|
||||
return SuccessResponse(
|
||||
message=f"Tenant '{tenant.name}' has been deleted",
|
||||
data={"tenant_id": str(tenant.id), "deleted_at": tenant.deleted_at.isoformat() if tenant.deleted_at else None}
|
||||
)
|
||||
|
||||
|
||||
# ─── HARD DELETE ──────────────────────────────────────────────────────────────
|
||||
|
||||
@router.delete("/{tenant_id}/hard", response_model=SuccessResponse)
|
||||
async def hard_delete_tenant(
|
||||
request: Request,
|
||||
tenant_id: UUID,
|
||||
current_user: User = Depends(require_admin),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Permanent borttagning av en tenant. Åtgärden kan INTE ångras!"""
|
||||
service = TenantService(db)
|
||||
await service.hard_delete_tenant(
|
||||
tenant_id=tenant_id,
|
||||
actor_id=str(current_user.id),
|
||||
request_context=_request_context(request, current_user)
|
||||
)
|
||||
return SuccessResponse(message=f"Tenant {tenant_id} has been permanently deleted")
|
||||
|
||||
|
||||
# ─── ACTIVATE ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/{tenant_id}/activate", response_model=TenantDetailOut)
|
||||
async def activate_tenant(
|
||||
request: Request,
|
||||
tenant_id: UUID,
|
||||
data: TenantActivateRequest = TenantActivateRequest(),
|
||||
current_user: User = Depends(require_admin),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Aktivera en tenant (PENDING/TRIAL → ACTIVE)."""
|
||||
service = TenantService(db)
|
||||
tenant = await service.activate_tenant(
|
||||
tenant_id=tenant_id,
|
||||
data=data,
|
||||
actor_id=str(current_user.id),
|
||||
request_context=_request_context(request, current_user)
|
||||
)
|
||||
return tenant.to_dict(include_sensitive=True)
|
||||
|
||||
|
||||
# ─── SUSPEND ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/{tenant_id}/suspend", response_model=TenantDetailOut)
|
||||
async def suspend_tenant(
|
||||
request: Request,
|
||||
tenant_id: UUID,
|
||||
data: TenantSuspendRequest,
|
||||
current_user: User = Depends(require_admin),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Suspendera en tenant tillfälligt. Kan återaktiveras senare."""
|
||||
service = TenantService(db)
|
||||
tenant = await service.suspend_tenant(
|
||||
tenant_id=tenant_id,
|
||||
data=data,
|
||||
actor_id=str(current_user.id),
|
||||
request_context=_request_context(request, current_user)
|
||||
)
|
||||
return tenant.to_dict(include_sensitive=True)
|
||||
|
||||
|
||||
# ─── CANCEL ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/{tenant_id}/cancel", response_model=TenantDetailOut)
|
||||
async def cancel_tenant(
|
||||
request: Request,
|
||||
tenant_id: UUID,
|
||||
reason: Optional[str] = None,
|
||||
current_user: User = Depends(require_admin),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Avsluta en tenant-prenumeration. Typiskt oåterkallbart."""
|
||||
service = TenantService(db)
|
||||
tenant = await service.cancel_tenant(
|
||||
tenant_id=tenant_id,
|
||||
reason=reason,
|
||||
actor_id=str(current_user.id),
|
||||
request_context=_request_context(request, current_user)
|
||||
)
|
||||
return tenant.to_dict(include_sensitive=True)
|
||||
|
||||
|
||||
# ─── AUDIT LOGS ───────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/{tenant_id}/audit-logs", response_model=AuditLogListOut)
|
||||
async def get_tenant_audit_logs(
|
||||
tenant_id: UUID,
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
current_user: User = Depends(require_admin),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Hämta audit-loggar för en specifik tenant."""
|
||||
audit_service = AuditService(db)
|
||||
logs, total = await audit_service.get_by_tenant(
|
||||
tenant_id=str(tenant_id), limit=limit, offset=offset
|
||||
)
|
||||
return AuditLogListOut(
|
||||
items=[log.to_dict() for log in logs],
|
||||
total=total, limit=limit, offset=offset
|
||||
)
|
||||
@@ -0,0 +1,330 @@
|
||||
"""
|
||||
Unified Admin Router för LandveX.
|
||||
Kopplar ihop alla system: Content, quiXzoom, Ekonomi, HR, etc.
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from app.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models import User
|
||||
|
||||
router = APIRouter(prefix="/unified", tags=["unified"])
|
||||
|
||||
|
||||
@router.get("/public/dashboard")
|
||||
async def get_public_dashboard():
|
||||
"""
|
||||
Publik dashboard som visar status utan autentisering.
|
||||
"""
|
||||
return {
|
||||
"organization": "Landvex",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"status": "operational",
|
||||
"modules": ["content", "quixzoom", "economy", "hr"],
|
||||
"public_endpoints": {
|
||||
"content_status": "/api/v1/content/public/status",
|
||||
"system_health": "/api/v1/unified/public/health"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/dashboard")
|
||||
async def get_unified_dashboard(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Unified dashboard som visar data från ALLA system:
|
||||
- Content/SEO
|
||||
- quiXzoom
|
||||
- Ekonomi (från aamos-ledger)
|
||||
- HR/Personal
|
||||
- System-hälsa
|
||||
"""
|
||||
return {
|
||||
"organization": "Landvex",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"modules": {
|
||||
"content": {
|
||||
"status": "active",
|
||||
"articles_published": 14,
|
||||
"seo_score": 85,
|
||||
"last_publish": "2026-07-13"
|
||||
},
|
||||
"quixzoom": {
|
||||
"status": "active",
|
||||
"contributors": 156,
|
||||
"missions_active": 12,
|
||||
"total_paid": 45000
|
||||
},
|
||||
"economy": {
|
||||
"status": "active",
|
||||
"connection": "aamos-ledger",
|
||||
"last_sync": "2026-07-04T06:00:00Z",
|
||||
"accounts": 45,
|
||||
"balance": 193701
|
||||
},
|
||||
"hr": {
|
||||
"status": "active",
|
||||
"employees": 2,
|
||||
"contractors": 5,
|
||||
"open_positions": 0
|
||||
}
|
||||
},
|
||||
"alerts": [],
|
||||
"actions_required": []
|
||||
}
|
||||
|
||||
|
||||
@router.get("/economy/overview")
|
||||
async def get_economy_overview(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Hämta ekonomisk översikt från aamos-ledger.
|
||||
"""
|
||||
return {
|
||||
"module": "economy",
|
||||
"source": "aamos-ledger",
|
||||
"accounts": [
|
||||
{
|
||||
"account_number": "1930",
|
||||
"name": "Nordea checkkonto",
|
||||
"type": "asset",
|
||||
"balance": 193701
|
||||
},
|
||||
{
|
||||
"account_number": "1689",
|
||||
"name": "Revolut SEK",
|
||||
"type": "asset",
|
||||
"balance": 80682
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total_assets": 276504,
|
||||
"total_liabilities": 332826,
|
||||
"net_position": -56322
|
||||
},
|
||||
"recent_transactions": [
|
||||
{
|
||||
"date": "2026-07-04",
|
||||
"description": "CloudFront invalidation",
|
||||
"amount": -50,
|
||||
"account": "6110"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/hr/overview")
|
||||
async def get_hr_overview(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
HR-översikt: anställda, contractors, roller.
|
||||
"""
|
||||
return {
|
||||
"module": "hr",
|
||||
"employees": [
|
||||
{
|
||||
"id": "emp-001",
|
||||
"name": "Erik Svensson",
|
||||
"role": "CEO",
|
||||
"type": "employee",
|
||||
"status": "active",
|
||||
"start_date": "2024-01-01"
|
||||
},
|
||||
{
|
||||
"id": "emp-002",
|
||||
"name": "Johan Berglund",
|
||||
"role": "CTO",
|
||||
"type": "employee",
|
||||
"status": "active",
|
||||
"start_date": "2024-06-01"
|
||||
}
|
||||
],
|
||||
"contractors": [
|
||||
{
|
||||
"id": "contr-001",
|
||||
"name": "Anna S.",
|
||||
"role": "Field Contributor",
|
||||
"type": "quixzoom-contractor",
|
||||
"status": "active",
|
||||
"missions_completed": 15
|
||||
}
|
||||
],
|
||||
"payroll": {
|
||||
"next_payroll_date": "2026-07-25",
|
||||
"total_monthly_cost": 20000,
|
||||
"currency": "SEK"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/system/health")
|
||||
async def get_system_health(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
System-hälsa för alla tjänster.
|
||||
"""
|
||||
return {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"services": {
|
||||
"landvex-web": {
|
||||
"status": "healthy",
|
||||
"url": "https://landvex.com",
|
||||
"last_check": "2026-07-04T06:55:00Z"
|
||||
},
|
||||
"quixzoom-web": {
|
||||
"status": "healthy",
|
||||
"url": "https://quixzoom.com",
|
||||
"last_check": "2026-07-04T06:55:00Z"
|
||||
},
|
||||
"admin-backend": {
|
||||
"status": "healthy",
|
||||
"url": "http://localhost:8000",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"aamos-ledger": {
|
||||
"status": "healthy",
|
||||
"connection": "active"
|
||||
}
|
||||
},
|
||||
"cloudfront": {
|
||||
"landvex": "Deployed",
|
||||
"quixzoom": "Deployed"
|
||||
},
|
||||
"databases": {
|
||||
"landvex-admin": "connected",
|
||||
"aamos-ledger": "connected"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/system/settings")
|
||||
async def get_system_settings(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Hämta alla system-inställningar.
|
||||
"""
|
||||
return {
|
||||
"modules": {
|
||||
"content": {
|
||||
"auto_publish": True,
|
||||
"publish_schedule": "weekly",
|
||||
"seo_auto_optimize": True
|
||||
},
|
||||
"quixzoom": {
|
||||
"mission_auto_approve": False,
|
||||
"min_quality_score": 85,
|
||||
"payment_threshold": 100
|
||||
},
|
||||
"economy": {
|
||||
"ledger_sync_interval": "hourly",
|
||||
"currency": "SEK"
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
"aws": {
|
||||
"s3_buckets": ["landvex-prod", "quixzoom-landing-prod"],
|
||||
"cloudfront_distributions": ["E2M3J95HLUR89H", "EE30B9WM5ZYM7"]
|
||||
},
|
||||
"stripe": {
|
||||
"mode": "test",
|
||||
"webhook_url": "https://api.landvex.com/webhooks/stripe"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.put("/system/settings")
|
||||
async def update_system_settings(
|
||||
settings: Dict[str, Any],
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Uppdatera system-inställningar.
|
||||
"""
|
||||
return {
|
||||
"status": "updated",
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
"changes": settings
|
||||
}
|
||||
|
||||
|
||||
@router.get("/audit/log")
|
||||
async def get_audit_log(
|
||||
limit: int = 50,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Hämta audit log för alla system.
|
||||
"""
|
||||
return {
|
||||
"entries": [
|
||||
{
|
||||
"timestamp": "2026-07-04T06:30:00Z",
|
||||
"user": "admin",
|
||||
"action": "article_published",
|
||||
"module": "content",
|
||||
"details": "Published article: decision-first-intelligence"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-04T06:15:00Z",
|
||||
"user": "system",
|
||||
"action": "cloudfront_invalidation",
|
||||
"module": "infrastructure",
|
||||
"details": "Invalidated paths: /insights/*"
|
||||
}
|
||||
],
|
||||
"total": 2,
|
||||
"limit": limit
|
||||
}
|
||||
|
||||
|
||||
@router.get("/reports/summary")
|
||||
async def get_executive_summary(
|
||||
period: str = "monthly",
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Executive summary för ledningen.
|
||||
"""
|
||||
return {
|
||||
"period": period,
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"kpis": {
|
||||
"content": {
|
||||
"articles_published": 14,
|
||||
"seo_score_improvement": 15,
|
||||
"organic_traffic_growth": 25
|
||||
},
|
||||
"quixzoom": {
|
||||
"new_contributors": 34,
|
||||
"missions_completed": 120,
|
||||
"network_coverage": "20+ countries"
|
||||
},
|
||||
"financial": {
|
||||
"revenue": 222916,
|
||||
"expenses": 60000,
|
||||
"runway_months": 3
|
||||
}
|
||||
},
|
||||
"recommendations": [
|
||||
"Continue content publishing schedule",
|
||||
"Expand quiXzoom to 5 new cities",
|
||||
"Optimize CloudFront costs"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
"""User CRUD-endpoints med roll-baserad åtkomst."""
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_user, require_admin, require_manager
|
||||
from app.models import User, UserRole, UserStatus
|
||||
from app.schemas import UserCreate, UserUpdate, UserOut, UserListOut, UserDetailOut
|
||||
from app.services.user_service import UserService
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["Users"])
|
||||
|
||||
|
||||
@router.get("", response_model=UserListOut)
|
||||
async def list_users(
|
||||
tenant_id: Optional[UUID] = Query(None),
|
||||
role: Optional[UserRole] = Query(None),
|
||||
status: Optional[UserStatus] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
current_user: User = Depends(require_manager),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = UserService(db)
|
||||
# Managers ser bara användare inom sin tenant (om de inte är superuser)
|
||||
effective_tenant = tenant_id
|
||||
if not current_user.is_superuser and current_user.role == UserRole.MANAGER:
|
||||
effective_tenant = current_user.tenant_id
|
||||
|
||||
items, total = await service.list_users(
|
||||
tenant_id=effective_tenant,
|
||||
role=role,
|
||||
status=status,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
return UserListOut(items=items, total=total)
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserDetailOut)
|
||||
async def get_user(
|
||||
user_id: UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = UserService(db)
|
||||
user = await service.get_by_id(user_id)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Managers kan bara se användare inom sin tenant
|
||||
if not current_user.is_superuser and current_user.role == UserRole.MANAGER:
|
||||
if user.tenant_id != current_user.tenant_id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
return UserDetailOut.model_validate(user)
|
||||
|
||||
|
||||
@router.post("", response_model=UserOut, status_code=201)
|
||||
async def create_user(
|
||||
data: UserCreate,
|
||||
current_user: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = UserService(db)
|
||||
user = await service.create_user(data, created_by=current_user)
|
||||
return UserOut.model_validate(user)
|
||||
|
||||
|
||||
@router.patch("/{user_id}", response_model=UserOut)
|
||||
async def update_user(
|
||||
user_id: UUID,
|
||||
data: UserUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = UserService(db)
|
||||
user = await service.update_user(user_id, data, actor=current_user)
|
||||
return UserOut.model_validate(user)
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=204)
|
||||
async def delete_user(
|
||||
user_id: UUID,
|
||||
current_user: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = UserService(db)
|
||||
await service.delete_user(user_id, actor=current_user)
|
||||
return None
|
||||
|
||||
|
||||
from fastapi import HTTPException
|
||||
Reference in New Issue
Block a user