268 lines
7.9 KiB
Python
268 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
spr init — Web wizard
|
|
|
|
Tiny standalone FastAPI server for room setup.
|
|
Run it, configure in browser, stop it.
|
|
|
|
Usage:
|
|
python -m init.web # http://localhost:9000
|
|
python -m init.web --port 9001
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.responses import HTMLResponse
|
|
from pydantic import BaseModel
|
|
|
|
from init.core import (
|
|
CFG_DIR,
|
|
SPR_ROOT,
|
|
BACKEND_FRAMEWORKS,
|
|
FRONTEND_FRAMEWORKS,
|
|
DATA_FILES,
|
|
clone_room,
|
|
generate_layer0,
|
|
generate_layer1,
|
|
generate_layer2,
|
|
generate_layer3,
|
|
generate_layer4,
|
|
generate_layer5,
|
|
generate_layer6,
|
|
list_rooms,
|
|
next_free_port,
|
|
)
|
|
|
|
log = logging.getLogger("init")
|
|
|
|
app = FastAPI(title="Soleprint Room Setup")
|
|
|
|
WIZARD_HTML = Path(__file__).parent / "wizard.html"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API models
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class Layer2Data(BaseModel):
|
|
app_name: str = ""
|
|
backend_path: str = ""
|
|
backend_framework: str = "django"
|
|
frontend_path: str | None = None
|
|
frontend_framework: str | None = None
|
|
|
|
|
|
class Layer5Data(BaseModel):
|
|
test_url: str = "http://localhost:8000"
|
|
|
|
|
|
class GenerateRequest(BaseModel):
|
|
room: str
|
|
room_type: str = "standalone" # standalone | managed
|
|
port: int = 0
|
|
layers: list[int] = [0, 1] # which layers to generate
|
|
layer2: Layer2Data | None = None
|
|
layer5: Layer5Data | None = None
|
|
# data selections — which items to include in each data file
|
|
data_selections: dict[str, list[dict]] | None = None
|
|
|
|
|
|
class CloneRequest(BaseModel):
|
|
source: str
|
|
target: str
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Routes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
def index():
|
|
if WIZARD_HTML.exists():
|
|
return WIZARD_HTML.read_text()
|
|
raise HTTPException(404, "wizard.html not found")
|
|
|
|
|
|
@app.get("/api/rooms")
|
|
def get_rooms():
|
|
"""List existing rooms."""
|
|
return {"rooms": list_rooms()}
|
|
|
|
|
|
@app.get("/api/defaults")
|
|
def get_defaults():
|
|
"""Return available components from standalone room's data/*.json."""
|
|
standalone_data = CFG_DIR / "standalone" / "data"
|
|
defaults = {}
|
|
for name in DATA_FILES:
|
|
path = standalone_data / f"{name}.json"
|
|
if path.exists():
|
|
try:
|
|
defaults[name] = json.loads(path.read_text())
|
|
except json.JSONDecodeError:
|
|
defaults[name] = {"items": []}
|
|
else:
|
|
defaults[name] = {"items": []}
|
|
return {
|
|
"data": defaults,
|
|
"backend_frameworks": BACKEND_FRAMEWORKS,
|
|
"frontend_frameworks": FRONTEND_FRAMEWORKS,
|
|
"next_port": next_free_port(),
|
|
}
|
|
|
|
|
|
@app.post("/api/generate")
|
|
def generate(req: GenerateRequest):
|
|
"""Generate room files for selected layers."""
|
|
room = req.room.strip()
|
|
if not room:
|
|
raise HTTPException(400, "Room name is required")
|
|
|
|
room_dir = CFG_DIR / room
|
|
if room_dir.exists():
|
|
raise HTTPException(409, f"Room '{room}' already exists in cfg/")
|
|
|
|
port = req.port or next_free_port()
|
|
is_managed = req.room_type == "managed"
|
|
created = []
|
|
|
|
try:
|
|
# Layer 0: always
|
|
if 0 in req.layers:
|
|
config = generate_layer0(room_dir, room, port, is_managed)
|
|
|
|
# Apply data selections if provided
|
|
if req.data_selections:
|
|
data_dir = room_dir / "data"
|
|
for name, items in req.data_selections.items():
|
|
path = data_dir / f"{name}.json"
|
|
if path.exists():
|
|
path.write_text(json.dumps({"items": items}, indent=2) + "\n")
|
|
created.append("config + data")
|
|
else:
|
|
# Need config for later layers
|
|
config = generate_layer0(room_dir, room, port, is_managed)
|
|
created.append("config + data")
|
|
|
|
if 1 in req.layers:
|
|
generate_layer1(room_dir, room, port)
|
|
created.append("docker")
|
|
|
|
app_info = None
|
|
if 2 in req.layers and is_managed and req.layer2:
|
|
l2 = req.layer2
|
|
has_frontend = bool(l2.frontend_path)
|
|
app_info = generate_layer2(
|
|
room_dir, room, config,
|
|
l2.app_name or room, l2.backend_path, l2.backend_framework,
|
|
l2.frontend_path if has_frontend else None,
|
|
l2.frontend_framework if has_frontend else None,
|
|
)
|
|
created.append("managed app")
|
|
|
|
if 3 in req.layers and is_managed and app_info:
|
|
generate_layer3(room_dir, room, app_info["backend_framework"])
|
|
created.append("link")
|
|
|
|
app_name = app_info["app_name"] if app_info else None
|
|
if 4 in req.layers:
|
|
generate_layer4(room_dir, room, app_name)
|
|
created.append("scripts")
|
|
|
|
if 5 in req.layers:
|
|
url = req.layer5.test_url if req.layer5 else "http://localhost:8000"
|
|
generate_layer5(room_dir, room, url)
|
|
created.append("tester")
|
|
|
|
if 6 in req.layers and is_managed and app_info and app_info["has_frontend"]:
|
|
generate_layer6(room_dir, room, app_info["app_name"])
|
|
created.append("nginx")
|
|
|
|
except Exception as e:
|
|
raise HTTPException(500, f"Generation failed: {e}")
|
|
|
|
return {
|
|
"room": room,
|
|
"port": port,
|
|
"created": created,
|
|
"path": str(room_dir.relative_to(SPR_ROOT)),
|
|
"build_command": f"python build.py --cfg {room}",
|
|
}
|
|
|
|
|
|
@app.post("/api/clone")
|
|
def clone(req: CloneRequest):
|
|
"""Clone an existing room as a variant."""
|
|
if not req.source.strip() or not req.target.strip():
|
|
raise HTTPException(400, "Source and target room names required")
|
|
|
|
ok = clone_room(req.source.strip(), req.target.strip())
|
|
if not ok:
|
|
raise HTTPException(400, "Clone failed — check source exists and target doesn't")
|
|
|
|
return {
|
|
"source": req.source,
|
|
"target": req.target,
|
|
"path": f"cfg/{req.target}",
|
|
"build_command": f"python build.py --cfg {req.target}",
|
|
}
|
|
|
|
|
|
@app.get("/api/preview")
|
|
def preview(room: str = "myroom", room_type: str = "standalone"):
|
|
"""Return directory tree preview for a room configuration."""
|
|
is_managed = room_type == "managed"
|
|
tree = [f"cfg/{room}/"]
|
|
tree.append(" config.json")
|
|
tree.append(" data/")
|
|
for f in DATA_FILES:
|
|
tree.append(f" {f}.json")
|
|
tree.append(" soleprint/")
|
|
tree.append(" docker-compose.yml")
|
|
tree.append(" .env")
|
|
if is_managed:
|
|
tree.append(f" docker-compose.yml")
|
|
tree.append(f" Dockerfile.backend")
|
|
tree.append(f" .env")
|
|
tree.append(f" {room}/")
|
|
tree.append(f" dumps/")
|
|
tree.append(" link/")
|
|
tree.append(" main.py")
|
|
tree.append(" adapters/")
|
|
tree.append(" soleprint/")
|
|
tree.append(" nginx/")
|
|
tree.append(" local.conf")
|
|
tree.append(" docker-compose.nginx.yml")
|
|
tree.append(" ctrl/")
|
|
tree.append(" start.sh")
|
|
tree.append(" stop.sh")
|
|
tree.append(" status.sh")
|
|
tree.append(" logs.sh")
|
|
return {"tree": tree}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main():
|
|
import uvicorn
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
|
|
parser = argparse.ArgumentParser(description="Soleprint room setup wizard (web)")
|
|
parser.add_argument("--port", type=int, default=9000)
|
|
parser.add_argument("--host", default="127.0.0.1")
|
|
args = parser.parse_args()
|
|
|
|
log.info("Room setup wizard: http://%s:%d", args.host, args.port)
|
|
uvicorn.run(app, host=args.host, port=args.port, log_level="warning")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|