Ryan Malloy 8abd7719bf Initial SpiceBook MVP: notebook interface for circuit simulation
Phase 1 implementation with ngspice backend and Astro/React frontend:

Backend (FastAPI):
- ngspice subprocess engine with custom .raw file parser
- Notebook CRUD with .spicebook JSON format (filesystem storage)
- Simulation endpoints (standalone + cell-in-notebook)
- SVG waveform export endpoint
- 18 REST API routes, 5 passing tests

Frontend (Astro 5 + React 19):
- Notebook editor as React island with Zustand state management
- CodeMirror 6 with custom SPICE language mode (syntax highlighting
  for dot commands, components, engineering notation, comments)
- uPlot waveform viewer with transient and AC/Bode plot modes
- Markdown cells with edit/preview toggle
- Notebook list with card grid UI
- Dark theme, Tailwind CSS 4, Lucide icons

Infrastructure:
- Docker Compose with dev/prod targets
- Caddy-based frontend prod serving
- 3 example notebooks (RC filter, voltage divider, CE amplifier)
2026-02-13 01:44:38 -07:00

98 lines
2.5 KiB
Python

"""FastAPI application entry point for SpiceBook."""
import logging
import shutil
import sys
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from spicebook.config import settings
from spicebook.routers import notebooks, simulation, waveforms
logger = logging.getLogger("spicebook")
def create_app() -> FastAPI:
application = FastAPI(
title="SpiceBook",
description="Notebook interface for SPICE circuit simulation",
version="2026.02.13",
)
# CORS -- allow dev frontends
application.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:4321",
"http://localhost:3000",
"http://127.0.0.1:4321",
"http://127.0.0.1:3000",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
application.include_router(notebooks.router)
application.include_router(simulation.router)
application.include_router(waveforms.router)
@application.get("/health")
async def health():
return {"status": "ok", "version": "2026.02.13"}
@application.on_event("startup")
async def startup():
_configure_logging()
_validate_ngspice()
_ensure_directories()
return application
def _configure_logging() -> None:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
stream=sys.stderr,
)
def _validate_ngspice() -> None:
"""Check that ngspice is installed and reachable."""
ngspice = shutil.which(str(settings.ngspice_path))
if ngspice is None:
ngspice = shutil.which("ngspice")
if ngspice:
logger.info("ngspice found at %s", ngspice)
else:
logger.warning(
"ngspice not found at '%s' and not on PATH. "
"Simulations will fail until ngspice is installed.",
settings.ngspice_path,
)
def _ensure_directories() -> None:
"""Create notebook directories if they don't exist."""
settings.notebook_dir.mkdir(parents=True, exist_ok=True)
settings.user_dir.mkdir(parents=True, exist_ok=True)
settings.examples_dir.mkdir(parents=True, exist_ok=True)
logger.info("Notebook directory: %s", settings.notebook_dir.resolve())
app = create_app()
def main() -> None:
"""Entry point for `spicebook` CLI command."""
uvicorn.run(
"spicebook.main:app",
host=settings.backend_host,
port=settings.backend_port,
reload=False,
)