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)",
|
feat: persist multi-board projects + add auto-save
The project save/load pipeline only persisted a single `board_type`, so
multi-board workspaces silently lost every board except the active one
on save, and wires referencing the dropped boards' IDs orphaned to the
canvas corner on reload. An audit of the production backup found 74/306
projects (24%) with at least one orphaned wire and 174/301 non-trivial
projects whose code was still the default Blink template — strong signal
that users save once and never re-save.
Backend
- Add `boards_json` column on `projects` with idempotent ALTER TABLE in
the lifespan migration list.
- New `FileGroup` schema + `file_groups` array on
ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat.
- `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via
`read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on
read; legacy single-list `files` only updates the active group, leaving
other boards' files intact.
- `_persist_files_from_body` honors file_groups → files → code priority.
Frontend
- `useSimulatorStore.addBoard` accepts an optional `explicitId` so
saved board IDs can be restored verbatim (wires reference IDs literally).
- New `loadProjectState({boards, fileGroups, components, wires,
activeBoardId})` action: tears down current boards, recreates from the
payload, restores file groups atomically, recalculates wire positions
on the next frame, and refreshes the Interconnect.
- `useEditorStore.replaceFileGroups` for atomic multi-group restore.
- `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through
`buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects
by synthesising a default board from `board_type`).
Auto-save (#useAutoSaveProject hook)
- 2.5s debounced silent PUT triggered ONLY when an authenticated user
has a `currentProject` with a UUID. State hash detects real changes
vs. UI-only churn; baseline is reset on project load so the just-loaded
state isn't immediately re-saved.
- `beforeunload` flush via `fetch keepalive: true` (supports PUT +
credentials, survives unload).
- Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error).
Backfill script (one-off, idempotent)
- `backend/scripts/backfill_boards_2026_05.py` populates `boards_json`
for legacy projects. Heuristic per project, based on which board IDs
the wires reference:
Case A — wires only ref 'arduino-uno' but board_type ≠ uno:
rename id→board_type and rewrite wire endpoints.
Case B — single-board normal: keep verbatim.
Case C — multi-board: recreate one board per distinct ref, infer
kind by stripping trailing -N suffix.
Also moves any flat files into the active board's group subdir.
Stdlib-only, runs from host or `docker exec`.
Docker
- `Dockerfile.standalone` now copies `backend/scripts/` into the image
so the backfill is callable via `docker exec velxio-app python
/app/scripts/backfill_boards_2026_05.py --apply`.
Verified locally on the restored production backup (363 projects):
33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans.
Re-running the script after apply skips all 363 (idempotent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
|
|
|
# Multi-board persistence (replaces single board_type as the source of truth)
|
|
|
|
|
"ALTER TABLE projects ADD COLUMN boards_json TEXT NOT NULL DEFAULT '[]'",
|
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
|
|
|
]
|
|
|
|
|
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
|
|
|
|