2026-03-15 02:57:22 +07:00
|
|
|
import logging
|
2026-03-16 00:04:01 +07:00
|
|
|
import sys
|
|
|
|
|
import asyncio
|
2026-03-15 00:35:59 +07:00
|
|
|
from contextlib import asynccontextmanager
|
2026-03-06 20:14:50 +07:00
|
|
|
|
2026-03-15 02:57:22 +07:00
|
|
|
logging.basicConfig(level=logging.INFO, format='%(levelname)s %(name)s: %(message)s')
|
|
|
|
|
|
2026-03-16 00:04:01 +07:00
|
|
|
# On Windows, asyncio defaults to SelectorEventLoop which does NOT support
|
|
|
|
|
# create_subprocess_exec (raises NotImplementedError). Force ProactorEventLoop.
|
|
|
|
|
if sys.platform == 'win32':
|
|
|
|
|
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
|
|
|
|
|
2026-03-03 10:20:49 +07:00
|
|
|
from fastapi import FastAPI
|
|
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
2026-03-06 20:14:50 +07:00
|
|
|
from sqlalchemy import text
|
|
|
|
|
|
2026-04-29 05:24:39 +07:00
|
|
|
from app.api.routes import compile, compile_chip, libraries
|
2026-03-07 09:46:36 +07:00
|
|
|
from app.api.routes.admin import router as admin_router
|
2026-03-06 20:14:50 +07:00
|
|
|
from app.api.routes.auth import router as auth_router
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 05:46:52 +07:00
|
|
|
from app.api.routes.metrics import router as metrics_router
|
2026-03-06 20:14:50 +07:00
|
|
|
from app.api.routes.projects import router as projects_router
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
from app.database.session import Base, async_engine
|
|
|
|
|
|
|
|
|
|
# Import models so SQLAlchemy registers them before create_all
|
|
|
|
|
import app.models.user # noqa: F401
|
|
|
|
|
import app.models.project # noqa: F401
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 05:46:52 +07:00
|
|
|
import app.models.usage_event # noqa: F401
|
2026-03-06 20:14:50 +07:00
|
|
|
|
|
|
|
|
|
2026-04-16 10:57:30 +07:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _asyncio_exception_handler(loop: asyncio.AbstractEventLoop, context: dict) -> None:
|
|
|
|
|
"""Prevent unhandled asyncio task exceptions from killing the uvicorn process.
|
|
|
|
|
|
|
|
|
|
Normally uvicorn re-raises unhandled task exceptions at the event-loop level,
|
|
|
|
|
which can crash the whole process. The main culprit is a race condition in
|
|
|
|
|
websockets <12.0 (legacy/protocol.py AssertionError during keepalive ping).
|
|
|
|
|
Upgrading websockets>=12.0 is the primary fix; this handler is a safety net.
|
|
|
|
|
"""
|
|
|
|
|
exc = context.get("exception")
|
|
|
|
|
msg = context.get("message", "")
|
|
|
|
|
if exc is not None:
|
|
|
|
|
logger.error("Unhandled asyncio task exception (swallowed): %s — %r", msg, exc)
|
|
|
|
|
else:
|
|
|
|
|
# No exception object — let default handler deal with it
|
|
|
|
|
loop.default_exception_handler(context)
|
|
|
|
|
|
|
|
|
|
|
2026-03-06 20:14:50 +07:00
|
|
|
@asynccontextmanager
|
|
|
|
|
async def lifespan(_app: FastAPI):
|
2026-04-16 10:57:30 +07:00
|
|
|
asyncio.get_event_loop().set_exception_handler(_asyncio_exception_handler)
|
2026-03-06 20:14:50 +07:00
|
|
|
async with async_engine.begin() as conn:
|
|
|
|
|
await conn.run_sync(Base.metadata.create_all)
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 05:46:52 +07:00
|
|
|
# Lightweight auto-migrations for legacy DBs. Each statement is wrapped
|
|
|
|
|
# in try/except so re-runs after the column already exists are no-ops.
|
|
|
|
|
legacy_migrations = [
|
|
|
|
|
"ALTER TABLE users ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT 0",
|
|
|
|
|
# Phase: usage metrics
|
|
|
|
|
"ALTER TABLE users ADD COLUMN total_compiles INTEGER NOT NULL DEFAULT 0",
|
|
|
|
|
"ALTER TABLE users ADD COLUMN total_compile_errors INTEGER NOT NULL DEFAULT 0",
|
|
|
|
|
"ALTER TABLE users ADD COLUMN total_runs INTEGER NOT NULL DEFAULT 0",
|
|
|
|
|
"ALTER TABLE users ADD COLUMN last_active_at DATETIME",
|
|
|
|
|
"ALTER TABLE projects ADD COLUMN compile_count INTEGER NOT NULL DEFAULT 0",
|
|
|
|
|
"ALTER TABLE projects ADD COLUMN compile_error_count INTEGER NOT NULL DEFAULT 0",
|
|
|
|
|
"ALTER TABLE projects ADD COLUMN run_count INTEGER NOT NULL DEFAULT 0",
|
|
|
|
|
"ALTER TABLE projects ADD COLUMN update_count INTEGER NOT NULL DEFAULT 0",
|
|
|
|
|
"ALTER TABLE projects ADD COLUMN last_compiled_at DATETIME",
|
|
|
|
|
"ALTER TABLE projects ADD COLUMN last_run_at DATETIME",
|
|
|
|
|
# Country tracking (CF-IPCountry)
|
|
|
|
|
"ALTER TABLE users ADD COLUMN signup_country VARCHAR(2)",
|
|
|
|
|
"ALTER TABLE users ADD COLUMN last_country VARCHAR(2)",
|
|
|
|
|
"ALTER TABLE usage_events ADD COLUMN country VARCHAR(2)",
|
|
|
|
|
]
|
|
|
|
|
for stmt in legacy_migrations:
|
|
|
|
|
try:
|
|
|
|
|
await conn.execute(text(stmt))
|
|
|
|
|
except Exception:
|
|
|
|
|
pass # Column already exists
|
2026-03-06 20:14:50 +07:00
|
|
|
yield
|
|
|
|
|
|
2026-03-03 10:20:49 +07:00
|
|
|
|
|
|
|
|
app = FastAPI(
|
|
|
|
|
title="Arduino Emulator API",
|
|
|
|
|
description="Compilation and project management API",
|
2026-03-06 20:14:50 +07:00
|
|
|
version="1.0.0",
|
|
|
|
|
lifespan=lifespan,
|
2026-03-12 01:55:18 +07:00
|
|
|
# Moved from /docs to /api/docs so the frontend /docs/* documentation
|
|
|
|
|
# routes are served by the React SPA without any nginx conflict.
|
|
|
|
|
docs_url="/api/docs",
|
|
|
|
|
redoc_url="/api/redoc",
|
|
|
|
|
openapi_url="/api/openapi.json",
|
2026-03-03 10:20:49 +07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# CORS for local development
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
CORSMiddleware,
|
2026-03-06 20:14:50 +07:00
|
|
|
allow_origins=[
|
|
|
|
|
"http://localhost:5173",
|
|
|
|
|
"http://localhost:5174",
|
|
|
|
|
"http://localhost:5175",
|
|
|
|
|
settings.FRONTEND_URL,
|
|
|
|
|
],
|
2026-03-03 10:20:49 +07:00
|
|
|
allow_credentials=True,
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Include routers
|
|
|
|
|
app.include_router(compile.router, prefix="/api/compile", tags=["compilation"])
|
2026-04-29 05:24:39 +07:00
|
|
|
app.include_router(compile_chip.router, prefix="/api/compile-chip", tags=["custom-chips"])
|
2026-03-05 08:05:23 +07:00
|
|
|
app.include_router(libraries.router, prefix="/api/libraries", tags=["libraries"])
|
2026-03-06 20:14:50 +07:00
|
|
|
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
|
|
|
|
|
app.include_router(projects_router, prefix="/api", tags=["projects"])
|
feat(metrics): add usage analytics dashboard with country tracking
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 05:46:52 +07:00
|
|
|
app.include_router(metrics_router, prefix="/api/metrics", tags=["metrics"])
|
2026-03-07 09:46:36 +07:00
|
|
|
app.include_router(admin_router, prefix="/api/admin", tags=["admin"])
|
2026-03-03 10:20:49 +07:00
|
|
|
|
2026-03-12 18:17:29 +07:00
|
|
|
# WebSockets
|
|
|
|
|
from app.api.routes import simulation
|
|
|
|
|
app.include_router(simulation.router, prefix="/api/simulation", tags=["simulation"])
|
2026-03-03 10:20:49 +07:00
|
|
|
|
2026-04-01 06:53:56 +07:00
|
|
|
# IoT Gateway — HTTP proxy for ESP32 web servers
|
|
|
|
|
from app.api.routes import iot_gateway
|
|
|
|
|
app.include_router(iot_gateway.router, prefix="/api/gateway", tags=["iot-gateway"])
|
|
|
|
|
|
2026-03-03 10:20:49 +07:00
|
|
|
@app.get("/")
|
|
|
|
|
def root():
|
|
|
|
|
return {
|
|
|
|
|
"message": "Arduino Emulator API",
|
|
|
|
|
"version": "1.0.0",
|
2026-03-12 01:55:18 +07:00
|
|
|
"docs": "/api/docs",
|
2026-03-03 10:20:49 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/health")
|
|
|
|
|
def health_check():
|
|
|
|
|
return {"status": "healthy"}
|
2026-04-09 12:04:41 +07:00
|
|
|
|