Add multi-site Caddy helpers and document usage

- add startup/shutdown scripts that render a Caddyfile from JSON config and run health checks

- add Python utilities and a sample sites.json for declarative multi-site configuration

- document the workflow and ignore generated Caddy state artifacts

- normalize double-quote style across challenge workflow controllers, nodes, and tests
This commit is contained in:
Joey Yakimowich-Payne 2025-10-15 22:03:56 -06:00
commit 6038fc25f5
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
26 changed files with 1018 additions and 168 deletions

View file

@ -22,16 +22,18 @@ class ChallengeListApi(Resource):
select(Site).where(Site.app_id == c.app_id, Site.status == "normal")
).scalar_one_or_none()
site_code = site.code if site else None
items.append({
"id": c.id,
"name": c.name,
"description": c.description,
"goal": c.goal,
"app_id": c.app_id,
"workflow_id": c.workflow_id,
"app_mode": app.mode if app else None,
"app_site_code": site_code,
})
items.append(
{
"id": c.id,
"name": c.name,
"description": c.description,
"goal": c.goal,
"app_id": c.app_id,
"workflow_id": c.workflow_id,
"app_mode": app.mode if app else None,
"app_site_code": site_code,
}
)
return {"result": "success", "data": items}
@ -73,28 +75,27 @@ class ChallengeLeaderboardApi(Resource):
if not challenge:
return {"result": "not_found"}, 404
scoring_strategy = challenge.scoring_strategy or 'highest_rating'
scoring_strategy = challenge.scoring_strategy or "highest_rating"
# Build query based on scoring strategy
q = db.session.query(ChallengeAttempt).filter(
ChallengeAttempt.challenge_id == str(challenge_id),
ChallengeAttempt.succeeded.is_(True)
ChallengeAttempt.challenge_id == str(challenge_id), ChallengeAttempt.succeeded.is_(True)
)
# Apply sorting based on strategy
if scoring_strategy == 'first':
if scoring_strategy == "first":
# Earliest successful attempt wins
q = q.order_by(ChallengeAttempt.created_at.asc())
elif scoring_strategy == 'fastest':
elif scoring_strategy == "fastest":
# Lowest elapsed_ms wins
q = q.order_by(ChallengeAttempt.elapsed_ms.asc().nullslast(), ChallengeAttempt.created_at.asc())
elif scoring_strategy == 'fewest_tokens':
elif scoring_strategy == "fewest_tokens":
# Lowest tokens_total wins
q = q.order_by(ChallengeAttempt.tokens_total.asc().nullslast(), ChallengeAttempt.created_at.asc())
elif scoring_strategy == 'highest_rating':
elif scoring_strategy == "highest_rating":
# Highest judge_rating wins, ties broken by earliest
q = q.order_by(ChallengeAttempt.judge_rating.desc().nullslast(), ChallengeAttempt.created_at.asc())
elif scoring_strategy == 'custom':
elif scoring_strategy == "custom":
# Custom score field (computed by plugin)
q = q.order_by(ChallengeAttempt.score.desc().nullslast(), ChallengeAttempt.created_at.asc())
else:
@ -116,5 +117,3 @@ class ChallengeLeaderboardApi(Resource):
for r in rows
]
return {"result": "success", "data": data}

View file

@ -82,4 +82,3 @@ class RedBlueLeaderboardApi(Resource):
"blue_ratio": (float(blue or 0.0) / total) if total else 0.0,
}
return {"result": "success", "data": data}

View file

@ -8,15 +8,15 @@ from extensions.ext_database import db
from services.account_service import RegisterService
@web_ns.route('/register')
@web_ns.route("/register")
class WebRegisterApi(Resource):
def post(self):
payload = request.get_json(force=True) or {}
email = payload.get('email')
name = payload.get('name') or 'Player'
password = payload.get('password')
email = payload.get("email")
name = payload.get("name") or "Player"
password = payload.get("password")
if not email or not password:
return {'result': 'bad_request'}, 400
return {"result": "bad_request"}, 400
account = RegisterService.register(
email=email,
name=name,
@ -25,6 +25,4 @@ class WebRegisterApi(Resource):
create_workspace_required=False,
)
db.session.commit()
return {'result': 'success', 'data': {'account_id': account.id}}, 201
return {"result": "success", "data": {"account_id": account.id}}, 201