feat: add dark mode, navigation, and Web Vitals tracking to marketplace
Backend: - Simplify DatabaseConfig: remove effective_url property and project root finder - Update to Pydantic v2 model_config (replace nested Config class) - Add web_vitals router to main.py and __init__.py - Fix ExplorerService datetime handling (ensure timezone-naive comparisons) - Fix status_label extraction to handle both enum and string job states Frontend (Marketplace): - Add dark mode toggle with system preference detection
This commit is contained in:
@@ -18,35 +18,12 @@ class DatabaseConfig(BaseSettings):
|
||||
max_overflow: int = 20
|
||||
pool_pre_ping: bool = True
|
||||
|
||||
@property
|
||||
def effective_url(self) -> str:
|
||||
"""Get the effective database URL."""
|
||||
if self.url:
|
||||
return self.url
|
||||
# Auto-generate SQLite URL based on environment
|
||||
if self.adapter == "sqlite":
|
||||
project_root = self._find_project_root()
|
||||
db_path = project_root / "data" / "coordinator.db"
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
return f"sqlite:///{db_path}"
|
||||
elif self.adapter == "postgresql":
|
||||
return "postgresql://localhost:5432/aitbc_coordinator"
|
||||
return "sqlite:///:memory:"
|
||||
|
||||
@staticmethod
|
||||
def _find_project_root() -> Path:
|
||||
"""Find project root by looking for .git directory."""
|
||||
current = Path(__file__).resolve()
|
||||
while current.parent != current:
|
||||
if (current / ".git").exists():
|
||||
return current
|
||||
current = current.parent
|
||||
return Path(__file__).resolve().parents[3]
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
case_sensitive = False
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=False,
|
||||
extra="allow"
|
||||
)
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
@@ -116,7 +93,10 @@ class Settings(BaseSettings):
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""Get the database URL (backward compatibility)."""
|
||||
return self.database.effective_url
|
||||
if self.database.url:
|
||||
return self.database.url
|
||||
# Default SQLite path for backward compatibility
|
||||
return f"sqlite:///./aitbc_coordinator.db"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -19,6 +19,7 @@ from .routers import (
|
||||
zk_applications,
|
||||
explorer,
|
||||
payments,
|
||||
web_vitals,
|
||||
)
|
||||
from .routers.governance import router as governance
|
||||
from .routers.partners import router as partners
|
||||
@@ -75,6 +76,7 @@ def create_app() -> FastAPI:
|
||||
app.include_router(governance, prefix="/v1")
|
||||
app.include_router(partners, prefix="/v1")
|
||||
app.include_router(explorer, prefix="/v1")
|
||||
app.include_router(web_vitals, prefix="/v1")
|
||||
|
||||
# Add Prometheus metrics endpoint
|
||||
metrics_app = make_asgi_app()
|
||||
|
||||
@@ -11,6 +11,7 @@ from .users import router as users
|
||||
from .exchange import router as exchange
|
||||
from .marketplace_offers import router as marketplace_offers
|
||||
from .payments import router as payments
|
||||
from .web_vitals import router as web_vitals
|
||||
# from .registry import router as registry
|
||||
|
||||
__all__ = ["client", "miner", "admin", "marketplace", "marketplace_gpu", "explorer", "services", "users", "exchange", "marketplace_offers", "payments", "registry"]
|
||||
__all__ = ["client", "miner", "admin", "marketplace", "marketplace_gpu", "explorer", "services", "users", "exchange", "marketplace_offers", "payments", "web_vitals", "registry"]
|
||||
|
||||
53
apps/coordinator-api/src/app/routers/web_vitals.py
Normal file
53
apps/coordinator-api/src/app/routers/web_vitals.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Web Vitals API endpoint for collecting performance metrics
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict, Any, Optional
|
||||
import logging
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class WebVitalsEntry(BaseModel):
|
||||
name: str
|
||||
startTime: Optional[float] = None
|
||||
duration: Optional[float] = None
|
||||
|
||||
class WebVitalsMetric(BaseModel):
|
||||
name: str
|
||||
value: float
|
||||
id: str
|
||||
delta: Optional[float] = None
|
||||
entries: List[WebVitalsEntry] = []
|
||||
url: Optional[str] = None
|
||||
timestamp: Optional[str] = None
|
||||
|
||||
@router.post("/web-vitals")
|
||||
async def collect_web_vitals(metric: WebVitalsMetric):
|
||||
"""
|
||||
Collect Web Vitals performance metrics from the frontend.
|
||||
This endpoint receives Core Web Vitals (LCP, FID, CLS, TTFB, FCP) for monitoring.
|
||||
"""
|
||||
try:
|
||||
# Log the metric for monitoring/analysis
|
||||
logging.info(f"Web Vitals - {metric.name}: {metric.value}ms (ID: {metric.id}) from {metric.url or 'unknown'}")
|
||||
|
||||
# In a production setup, you might:
|
||||
# - Store in database for trend analysis
|
||||
# - Send to monitoring service (DataDog, New Relic, etc.)
|
||||
# - Trigger alerts for poor performance
|
||||
|
||||
# For now, just acknowledge receipt
|
||||
return {"status": "received", "metric": metric.name, "value": metric.value}
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing web vitals metric: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to process metric")
|
||||
|
||||
# Health check for web vitals endpoint
|
||||
@router.get("/web-vitals/health")
|
||||
async def web_vitals_health():
|
||||
"""Health check for web vitals collection endpoint"""
|
||||
return {"status": "healthy", "service": "web-vitals"}
|
||||
@@ -70,7 +70,8 @@ class ExplorerService:
|
||||
items: list[TransactionSummary] = []
|
||||
for index, job in enumerate(jobs):
|
||||
height = _DEFAULT_HEIGHT_BASE + offset + index
|
||||
status_label = _STATUS_LABELS.get(job.state, job.state.value.title())
|
||||
state_val = job.state.value if hasattr(job.state, "value") else job.state
|
||||
status_label = _STATUS_LABELS.get(job.state) or state_val.title()
|
||||
|
||||
# Try to get payment amount from receipt
|
||||
value_str = "0"
|
||||
@@ -118,14 +119,26 @@ class ExplorerService:
|
||||
}
|
||||
)
|
||||
|
||||
def touch(address: Optional[str], tx_id: str, when: datetime, earned: float = 0.0, spent: float = 0.0) -> None:
|
||||
def _ensure_dt(val: object) -> datetime:
|
||||
if isinstance(val, datetime):
|
||||
return val.replace(tzinfo=None)
|
||||
if isinstance(val, str):
|
||||
try:
|
||||
dt = datetime.fromisoformat(val.replace("Z", "+00:00"))
|
||||
return dt.replace(tzinfo=None)
|
||||
except ValueError:
|
||||
return datetime.min
|
||||
return datetime.min
|
||||
|
||||
def touch(address: Optional[str], tx_id: str, when: object, earned: float = 0.0, spent: float = 0.0) -> None:
|
||||
if not address:
|
||||
return
|
||||
entry = address_map[address]
|
||||
entry["address"] = address
|
||||
entry["tx_count"] = int(entry["tx_count"]) + 1
|
||||
if when > entry["last_active"]:
|
||||
entry["last_active"] = when
|
||||
when_dt = _ensure_dt(when)
|
||||
if when_dt > _ensure_dt(entry["last_active"]):
|
||||
entry["last_active"] = when_dt
|
||||
# Track earnings and spending
|
||||
entry["earned"] = float(entry["earned"]) + earned
|
||||
entry["spent"] = float(entry["spent"]) + spent
|
||||
|
||||
Reference in New Issue
Block a user