left4me/l4d2web/routes/page_routes.py
mwiegand d14ed9c117
feat(web): blueprint-prefilled create-server flow + empty-state CTA
- Per-row "Create server" link on /blueprints navigates to
  /servers?blueprint_id=<id>; that page validates the param against
  the user's owned blueprints, pre-selects the option, and auto-opens
  the create modal.
- /servers empty-blueprint state now shows an actionable
  "Create a blueprint first ->" link (styled like the primary button)
  pointing at /blueprints, replacing the silent disabled "+ Create"
  button + muted hint.
- Drop the "Reassign blueprint" form on the server detail page
  along with the unused POST /servers/<id> form route. The JSON
  PATCH /servers/<id> endpoint is retained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:47:33 +02:00

216 lines
6.8 KiB
Python

import json
from flask import Blueprint, Response, redirect, render_template, request
from sqlalchemy import select
from l4d2web.auth import current_user, require_admin, require_login
from l4d2web.db import session_scope
from l4d2web.models import Blueprint as BlueprintModel
from l4d2web.models import BlueprintOverlay, Job, Overlay, Server, User
bp = Blueprint("pages", __name__)
@bp.get("/dashboard")
@require_login
def dashboard() -> str:
return render_template("dashboard.html")
@bp.get("/admin")
@require_admin
def admin_home() -> str:
return render_template("admin.html")
@bp.post("/admin/install")
@require_admin
def enqueue_runtime_install() -> Response:
user = current_user()
assert user is not None
with session_scope() as db:
db.add(Job(user_id=user.id, server_id=None, operation="install", state="queued"))
return redirect("/admin/jobs")
@bp.get("/admin/users")
@require_admin
def admin_users() -> str:
with session_scope() as db:
users = db.scalars(select(User).order_by(User.username)).all()
return render_template("admin_users.html", users=users)
@bp.get("/admin/jobs")
@require_admin
def admin_jobs() -> str:
with session_scope() as db:
rows = db.execute(
select(Job, User, Server)
.join(User, User.id == Job.user_id)
.outerjoin(Server, Server.id == Job.server_id)
.order_by(Job.created_at.desc())
).all()
return render_template("admin_jobs.html", rows=rows)
@bp.get("/servers")
@require_login
def servers_page() -> str:
user = current_user()
assert user is not None
with session_scope() as db:
rows = db.execute(
select(Server, BlueprintModel)
.join(BlueprintModel, BlueprintModel.id == Server.blueprint_id)
.where(Server.user_id == user.id)
.order_by(Server.name)
).all()
blueprints = db.scalars(
select(BlueprintModel).where(BlueprintModel.user_id == user.id).order_by(BlueprintModel.name)
).all()
prefill_blueprint_id: int | None = None
raw_prefill = request.args.get("blueprint_id")
if raw_prefill:
try:
candidate = int(raw_prefill)
except ValueError:
candidate = None
if candidate is not None and any(b.id == candidate for b in blueprints):
prefill_blueprint_id = candidate
return render_template(
"servers.html",
rows=rows,
blueprints=blueprints,
prefill_blueprint_id=prefill_blueprint_id,
)
@bp.get("/servers/<int:server_id>")
@require_login
def server_detail(server_id: int):
user = current_user()
assert user is not None
with session_scope() as db:
server = db.scalar(select(Server).where(Server.id == server_id, Server.user_id == user.id))
if server is None:
return Response(status=404)
blueprint = db.scalar(select(BlueprintModel).where(BlueprintModel.id == server.blueprint_id))
recent_job_rows = db.execute(
select(Job, User, Server)
.join(User, User.id == Job.user_id)
.outerjoin(Server, Server.id == Job.server_id)
.where(Job.server_id == server.id)
.order_by(Job.created_at.desc())
.limit(5)
).all()
return render_template(
"server_detail.html",
server=server,
blueprint=blueprint,
recent_job_rows=recent_job_rows,
)
@bp.get("/servers/<int:server_id>/jobs")
@require_login
def server_jobs_page(server_id: int):
user = current_user()
assert user is not None
with session_scope() as db:
server = db.scalar(select(Server).where(Server.id == server_id, Server.user_id == user.id))
if server is None:
return Response(status=404)
rows = db.execute(
select(Job, User, Server)
.join(User, User.id == Job.user_id)
.outerjoin(Server, Server.id == Job.server_id)
.where(Job.server_id == server.id)
.order_by(Job.created_at.desc())
).all()
return render_template("server_jobs.html", server=server, rows=rows)
@bp.get("/overlays")
@require_login
def overlays() -> str:
with session_scope() as db:
overlays = db.scalars(select(Overlay).order_by(Overlay.name)).all()
return render_template("overlays.html", overlays=overlays)
@bp.get("/overlays/<int:overlay_id>")
@require_login
def overlay_detail(overlay_id: int):
with session_scope() as db:
overlay = db.scalar(select(Overlay).where(Overlay.id == overlay_id))
if overlay is None:
return Response(status=404)
using_blueprints = db.scalars(
select(BlueprintModel)
.join(BlueprintOverlay, BlueprintOverlay.blueprint_id == BlueprintModel.id)
.where(BlueprintOverlay.overlay_id == overlay.id)
.order_by(BlueprintModel.name)
).all()
return render_template(
"overlay_detail.html",
overlay=overlay,
using_blueprints=using_blueprints,
)
@bp.get("/blueprints")
@require_login
def blueprints_page() -> str:
user = current_user()
assert user is not None
with session_scope() as db:
blueprints = db.scalars(
select(BlueprintModel).where(BlueprintModel.user_id == user.id).order_by(BlueprintModel.name)
).all()
return render_template("blueprints.html", blueprints=blueprints)
@bp.get("/blueprints/<int:blueprint_id>")
@require_login
def blueprint_page(blueprint_id: int):
user = current_user()
assert user is not None
with session_scope() as db:
blueprint = db.scalar(select(BlueprintModel).where(BlueprintModel.id == blueprint_id))
if blueprint is None:
return Response(status=404)
if blueprint.user_id != user.id:
return Response(status=403)
selected_overlays = db.scalars(
select(Overlay)
.join(BlueprintOverlay, BlueprintOverlay.overlay_id == Overlay.id)
.where(BlueprintOverlay.blueprint_id == blueprint.id)
.order_by(BlueprintOverlay.position)
).all()
position_rows = db.execute(
select(BlueprintOverlay.overlay_id, BlueprintOverlay.position)
.where(BlueprintOverlay.blueprint_id == blueprint.id)
).all()
all_overlays = db.scalars(select(Overlay).order_by(Overlay.name)).all()
overlay_positions = {overlay_id: position + 1 for overlay_id, position in position_rows}
return render_template(
"blueprint_detail.html",
blueprint=blueprint,
selected_overlays=selected_overlays,
all_overlays=all_overlays,
selected_overlay_ids={overlay.id for overlay in selected_overlays},
overlay_positions=overlay_positions,
arguments=json.loads(blueprint.arguments),
config_lines=json.loads(blueprint.config),
)