from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from sqlalchemy import desc from pathlib import Path from datetime import datetime, timedelta, timezone from typing import List, Dict, Any from app.seismo.database import get_db from app.seismo.models import UnitHistory, Emitter, RosterUnit router = APIRouter(prefix="/api", tags=["activity"]) PHOTOS_BASE_DIR = Path("data/photos") @router.get("/recent-activity") def get_recent_activity(limit: int = 20, db: Session = Depends(get_db)): """ Get recent activity feed combining unit history changes and photo uploads. Returns a unified timeline of events sorted by timestamp (newest first). """ activities = [] # Get recent history entries history_entries = db.query(UnitHistory)\ .order_by(desc(UnitHistory.changed_at))\ .limit(limit * 2)\ .all() # Get more than needed to mix with photos for entry in history_entries: activity = { "type": "history", "timestamp": entry.changed_at.isoformat(), "timestamp_unix": entry.changed_at.timestamp(), "unit_id": entry.unit_id, "change_type": entry.change_type, "field_name": entry.field_name, "old_value": entry.old_value, "new_value": entry.new_value, "source": entry.source, "notes": entry.notes } activities.append(activity) # Get recent photos if PHOTOS_BASE_DIR.exists(): image_extensions = {".jpg", ".jpeg", ".png", ".gif", ".webp"} photo_activities = [] for unit_dir in PHOTOS_BASE_DIR.iterdir(): if not unit_dir.is_dir(): continue unit_id = unit_dir.name for file_path in unit_dir.iterdir(): if file_path.is_file() and file_path.suffix.lower() in image_extensions: modified_time = file_path.stat().st_mtime photo_activities.append({ "type": "photo", "timestamp": datetime.fromtimestamp(modified_time).isoformat(), "timestamp_unix": modified_time, "unit_id": unit_id, "filename": file_path.name, "photo_url": f"/api/unit/{unit_id}/photo/{file_path.name}" }) activities.extend(photo_activities) # Sort all activities by timestamp (newest first) activities.sort(key=lambda x: x["timestamp_unix"], reverse=True) # Limit to requested number activities = activities[:limit] return { "activities": activities, "total": len(activities) } @router.get("/recent-callins") def get_recent_callins(hours: int = 6, limit: int = None, db: Session = Depends(get_db)): """ Get recent unit call-ins (units that have reported recently). Returns units sorted by most recent last_seen timestamp. Args: hours: Look back this many hours (default: 6) limit: Maximum number of results (default: None = all) """ # Calculate the time threshold time_threshold = datetime.now(timezone.utc) - timedelta(hours=hours) # Query emitters with recent activity, joined with roster info recent_emitters = db.query(Emitter)\ .filter(Emitter.last_seen >= time_threshold)\ .order_by(desc(Emitter.last_seen))\ .all() # Get roster info for all units roster_dict = {r.id: r for r in db.query(RosterUnit).all()} call_ins = [] for emitter in recent_emitters: roster_unit = roster_dict.get(emitter.id) # Calculate time since last seen last_seen_utc = emitter.last_seen.replace(tzinfo=timezone.utc) if emitter.last_seen.tzinfo is None else emitter.last_seen time_diff = datetime.now(timezone.utc) - last_seen_utc # Format time ago if time_diff.total_seconds() < 60: time_ago = "just now" elif time_diff.total_seconds() < 3600: minutes = int(time_diff.total_seconds() / 60) time_ago = f"{minutes}m ago" else: hours_ago = time_diff.total_seconds() / 3600 if hours_ago < 24: time_ago = f"{int(hours_ago)}h {int((hours_ago % 1) * 60)}m ago" else: days = int(hours_ago / 24) time_ago = f"{days}d ago" call_in = { "unit_id": emitter.id, "last_seen": emitter.last_seen.isoformat(), "time_ago": time_ago, "status": emitter.status, "device_type": roster_unit.device_type if roster_unit else "seismograph", "deployed": roster_unit.deployed if roster_unit else False, "note": roster_unit.note if roster_unit and roster_unit.note else "", "location": roster_unit.address if roster_unit and roster_unit.address else (roster_unit.location if roster_unit else "") } call_ins.append(call_in) # Apply limit if specified if limit: call_ins = call_ins[:limit] return { "call_ins": call_ins, "total": len(call_ins), "hours": hours, "time_threshold": time_threshold.isoformat() }