feat: procedural editor redesign with collapsible sections and DnD (#84)

* 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>
This commit was merged in pull request #84.
This commit is contained in:
chihlasm
2026-02-19 08:39:25 -05:00
committed by GitHub
parent 51243130e5
commit 9462d8b15a
15 changed files with 2217 additions and 157 deletions

View File

@@ -72,6 +72,9 @@ class Settings(BaseSettings):
"""Check if Stripe is configured."""
return self.STRIPE_SECRET_KEY is not None and self.STRIPE_WEBHOOK_SECRET is not None
# Deployment auto-seed test data on PR environments
SEED_ON_DEPLOY: bool = False
# CORS - set FRONTEND_URL in production (e.g., https://patherly.up.railway.app)
CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://localhost:5173", "http://localhost:5174"]
FRONTEND_URL: Optional[str] = None

View File

@@ -1,4 +1,6 @@
import asyncio
import logging
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
@@ -18,6 +20,62 @@ 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."""
@@ -33,9 +91,17 @@ async def lifespan(app: FastAPI):
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...")

View File

@@ -7,4 +7,4 @@ healthcheckPath = "/health"
healthcheckTimeout = 100
restartPolicyType = "on_failure"
restartPolicyMaxRetries = 3
releaseCommand = "alembic upgrade head"
releaseCommand = "python -m scripts.release"

View File

@@ -28,6 +28,9 @@ stripe==14.3.0
# Email
resend==2.21.0
# HTTP client (seed scripts, internal API calls)
httpx>=0.27.0
# Utilities
python-dotenv==1.0.1
croniter>=2.0.0

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""
Railway release command — runs migrations + optional seed data.
Set SEED_ON_DEPLOY=true in Railway env vars to auto-seed test users
on PR environments. Seeding is idempotent (skips existing records).
Usage (called by railway.toml releaseCommand):
python -m scripts.release
"""
import asyncio
import subprocess
import sys
from app.core.config import settings
def run_migrations() -> None:
"""Run alembic upgrade head."""
print("\n[release] Running database migrations...")
result = subprocess.run(
["alembic", "upgrade", "head"],
capture_output=False,
)
if result.returncode != 0:
print("[release] ERROR: Migrations failed!")
sys.exit(1)
print("[release] Migrations complete.")
async def seed_test_data() -> None:
"""Seed test users (direct DB, no HTTP needed)."""
print("\n[release] Seeding test users...")
from scripts.seed_test_users import main as seed_users
await seed_users()
print("[release] Test users seeded.")
def main() -> None:
run_migrations()
if settings.SEED_ON_DEPLOY:
print("[release] SEED_ON_DEPLOY=true — seeding test data...")
asyncio.run(seed_test_data())
else:
print("[release] SEED_ON_DEPLOY not set — skipping seed data.")
print("\n[release] Release complete!")
if __name__ == "__main__":
main()