* docs: add procedural/maintenance editor redesign design Collapsible sections, fixed-height layout, drag-to-reorder steps, maintenance schedule section, and step list UX improvements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add procedural editor redesign implementation plan 7 tasks across 7 phases: collapsible sections, fixed-height layout, step list improvements, drag-to-reorder, maintenance schedule section. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: restructure procedural editor with collapsible sections and fixed-height layout Convert scrolling document layout to fixed-height editor with accordion-mode collapsible sections for Details and Intake Form. Step list now gets all remaining height with independent scrolling. Add CollapsibleEditorSection component with ARIA attributes (aria-expanded, aria-controls). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add step count with time estimate header and auto-scroll to new steps Remove outer card wrapper from StepList (now rendered in scrolling container). Header shows total estimated minutes when steps have time estimates. Auto-scrolls to newly added steps using ref + scrollIntoView. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add drag-to-reorder steps with @dnd-kit Wrap step list in DndContext + SortableContext. Each step/section header gets a SortableStepWrapper with useSortable. Drag handles have accessible labels and keyboard support. procedure_end stays non-draggable and always last. Expanded steps are disabled for dragging. Array-index reorder only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add MaintenanceScheduleSection with schedule builder and summary Schedule draft state is local UI only (not in store). Hydrates form from existing schedule on load. Includes getScheduleSummary helper for collapsed section display. Two-stage save: tree first, schedule second. Schedule failure shows actionable error without rolling back tree save. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: wire maintenance schedule section into procedural editor Add collapsible Schedule section for maintenance flows with accordion integration. Schedule summary shows frequency, time, and target count when collapsed. New maintenance flows default to schedule section expanded. Two-stage save preserved: tree saved first, schedule managed independently. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve lint issues in maintenance schedule and editor page Move getScheduleSummary to scheduleUtils.ts to satisfy react-refresh only-export-components rule. Add onScheduleLoaded to useEffect deps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add design and implementation revision documents Revision docs correct original plans: schedule persistence via API endpoints (not tree_structure), array-index reorder (no display_order), store minimum-one-step invariant, accordion mode, ARIA requirements, and two-stage save orchestration with failure handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: auto-seed PR environments with SEED_ON_DEPLOY flag Release command now runs migrations + seeds test users when SEED_ON_DEPLOY=true. Tree seeding runs as a background task on startup via HTTP API. Everything is idempotent and non-fatal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add httpx to requirements for PR environment seeding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: seed all flow types (v2, procedural, maintenance) on deploy Runs seed_trees, seed_trees_v2, seed_procedural_flows, and seed_maintenance_flows sequentially as background tasks when SEED_ON_DEPLOY=true. Each script failure is non-fatal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: trigger redeploy for full seed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
6.5 KiB
Python
179 lines
6.5 KiB
Python
import asyncio
|
|
import logging
|
|
import os
|
|
from contextlib import asynccontextmanager
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from slowapi import _rate_limit_exceeded_handler
|
|
from slowapi.errors import RateLimitExceeded
|
|
|
|
from app.core.config import settings
|
|
from app.core.database import init_db, async_session_maker
|
|
from app.core.logging_config import setup_logging
|
|
from app.core.middleware import RequestLoggingMiddleware, ErrorLoggingMiddleware
|
|
from app.core.rate_limit import limiter
|
|
from app.api.router import api_router
|
|
from app.core.scheduler import scheduler, load_all_schedules
|
|
|
|
# Initialize logging configuration
|
|
setup_logging()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _configure_seed_module(mod: object, api_url: str, email: str, password: str) -> None:
|
|
"""Set globals on a seed script module."""
|
|
mod.API_BASE_URL = api_url # type: ignore[attr-defined]
|
|
mod.ADMIN_EMAIL = email # type: ignore[attr-defined]
|
|
mod.ADMIN_PASSWORD = password # type: ignore[attr-defined]
|
|
|
|
|
|
async def _seed_trees_background() -> None:
|
|
"""Background task: seed all flows via HTTP API after server is ready."""
|
|
await asyncio.sleep(5) # Wait for server to be fully ready
|
|
port = os.environ.get("PORT", "8000")
|
|
api_url = f"http://127.0.0.1:{port}/api/v1"
|
|
email = "admin@resolutionflow.example.com"
|
|
password = "TestPass123!"
|
|
|
|
try:
|
|
import httpx
|
|
# Login to verify admin user exists
|
|
async with httpx.AsyncClient(base_url=api_url, timeout=30) as client:
|
|
login_resp = await client.post("/auth/login/json", json={"email": email, "password": password})
|
|
if login_resp.status_code != 200:
|
|
logger.warning("[seed] Could not login as admin — skipping flow seeding")
|
|
return
|
|
|
|
token = login_resp.json()["access_token"]
|
|
# Check if trees already exist
|
|
trees_resp = await client.get("/trees", headers={"Authorization": f"Bearer {token}"})
|
|
if trees_resp.status_code == 200 and len(trees_resp.json()) > 0:
|
|
logger.info(f"[seed] {len(trees_resp.json())} flows already exist — skipping flow seeding")
|
|
return
|
|
|
|
# No flows yet — run all seed scripts
|
|
seed_scripts = [
|
|
("seed_trees (Tier 1)", "scripts.seed_trees", "seed_database"),
|
|
("seed_trees_v2 (AD/M365/Networking)", "scripts.seed_trees_v2", "seed_database"),
|
|
("seed_procedural_flows", "scripts.seed_procedural_flows", "seed_procedural_flows"),
|
|
("seed_maintenance_flows", "scripts.seed_maintenance_flows", "seed_maintenance_flows"),
|
|
]
|
|
|
|
for label, module_path, func_name in seed_scripts:
|
|
try:
|
|
import importlib
|
|
mod = importlib.import_module(module_path)
|
|
_configure_seed_module(mod, api_url, email, password)
|
|
seed_fn = getattr(mod, func_name)
|
|
logger.info(f"[seed] Running {label}...")
|
|
await seed_fn()
|
|
logger.info(f"[seed] {label} complete!")
|
|
except Exception as e:
|
|
logger.warning(f"[seed] {label} failed (non-fatal): {e}")
|
|
|
|
logger.info("[seed] All flow seeding complete!")
|
|
except Exception as e:
|
|
logger.warning(f"[seed] Flow seeding failed (non-fatal): {e}")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Application lifespan handler."""
|
|
# Startup
|
|
logger.info("Starting ResolutionFlow API server...")
|
|
logger.info(f"Environment: {'Development' if settings.DEBUG else 'Production'}")
|
|
logger.info(f"ALLOW_RAILWAY_ORIGINS: {settings.ALLOW_RAILWAY_ORIGINS}")
|
|
# Note: In production, use Alembic migrations instead of init_db
|
|
# await init_db()
|
|
|
|
# Start maintenance schedule runner
|
|
scheduler.start()
|
|
async with async_session_maker() as db:
|
|
await load_all_schedules(db)
|
|
|
|
# Auto-seed trees in background on PR environments
|
|
seed_task = None
|
|
if settings.SEED_ON_DEPLOY:
|
|
logger.info("[seed] SEED_ON_DEPLOY=true — scheduling background tree seeding")
|
|
seed_task = asyncio.create_task(_seed_trees_background())
|
|
|
|
yield
|
|
|
|
# Shutdown
|
|
if seed_task and not seed_task.done():
|
|
seed_task.cancel()
|
|
scheduler.shutdown(wait=False)
|
|
logger.info("Shutting down ResolutionFlow API server...")
|
|
|
|
|
|
app = FastAPI(
|
|
title=settings.APP_NAME,
|
|
description="ResolutionFlow - Take the path MOST traveled. Guided troubleshooting with automatic documentation.",
|
|
version="1.0.0",
|
|
docs_url="/api/docs",
|
|
redoc_url="/api/redoc",
|
|
openapi_url="/api/openapi.json",
|
|
lifespan=lifespan
|
|
)
|
|
|
|
app.state.limiter = limiter
|
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
|
|
# Add logging middleware (BEFORE CORS to log all requests)
|
|
app.add_middleware(ErrorLoggingMiddleware)
|
|
app.add_middleware(RequestLoggingMiddleware)
|
|
|
|
# Configure CORS
|
|
# Note: When ALLOW_RAILWAY_ORIGINS is True, we use allow_origin_regex for Railway domains
|
|
# PLUS the explicit allowed_origins list (for custom domains like app.resolutionflow.com)
|
|
if settings.ALLOW_RAILWAY_ORIGINS:
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.allowed_origins,
|
|
allow_origin_regex=r"https://.*\.up\.railway\.app",
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
expose_headers=["X-Redaction-Mode", "X-Redaction-Summary"],
|
|
)
|
|
else:
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.allowed_origins,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
expose_headers=["X-Redaction-Mode", "X-Redaction-Summary"],
|
|
)
|
|
|
|
# Include API router
|
|
app.include_router(api_router, prefix=settings.API_V1_PREFIX)
|
|
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
"""Root endpoint."""
|
|
return {
|
|
"message": "ResolutionFlow API",
|
|
"docs": "/api/docs",
|
|
"version": "1.0.0"
|
|
}
|
|
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
"""Health check endpoint."""
|
|
return {"status": "healthy"}
|
|
|
|
|
|
if settings.DEBUG:
|
|
@app.get("/debug/cors")
|
|
async def debug_cors():
|
|
"""Debug endpoint to check CORS configuration."""
|
|
return {
|
|
"allow_railway_origins": settings.ALLOW_RAILWAY_ORIGINS,
|
|
"cors_mode": "regex + list" if settings.ALLOW_RAILWAY_ORIGINS else "list",
|
|
"allowed_origins": settings.allowed_origins,
|
|
"railway_regex": r"https://.*\.up\.railway\.app" if settings.ALLOW_RAILWAY_ORIGINS else None
|
|
}
|