workshop_routes: add per-overlay refresh endpoint

POST /overlays/{id}/refresh lets the overlay owner (or any admin)
re-fetch fresh Steam metadata for all items and enqueue a rebuild.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-11 23:03:48 +02:00
parent 81c6863cca
commit f5094c2d9d
No known key found for this signature in database
2 changed files with 193 additions and 0 deletions

View file

@ -120,6 +120,60 @@ def remove_item(overlay_id: int, item_id: int) -> Response:
return redirect(f"/jobs/{job_id}") return redirect(f"/jobs/{job_id}")
@bp.post("/overlays/<int:overlay_id>/refresh")
@require_login
def refresh_overlay(overlay_id: int) -> Response:
user = current_user()
assert user is not None
with session_scope() as db:
overlay, err = _check_workshop_overlay_access(overlay_id, user, db)
if err is not None:
return err
steam_ids = list(
db.scalars(
select(WorkshopItem.steam_id)
.join(
OverlayWorkshopItem,
OverlayWorkshopItem.workshop_item_id == WorkshopItem.id,
)
.where(OverlayWorkshopItem.overlay_id == overlay_id)
).all()
)
if not steam_ids:
return Response("overlay has no items", status=400)
try:
metas = steam_workshop.fetch_metadata_batch(steam_ids, mode="refresh")
except Exception as exc:
return Response(f"steam api error: {exc}", status=502)
metas_by_id = {m.steam_id: m for m in metas}
with session_scope() as db:
overlay, err = _check_workshop_overlay_access(overlay_id, user, db)
if err is not None:
return err
for steam_id in steam_ids:
wi = db.scalar(select(WorkshopItem).where(WorkshopItem.steam_id == steam_id))
if wi is None:
continue
meta = metas_by_id.get(steam_id)
if meta is None:
wi.last_error = "steam returned no entry for this item"
continue
wi.title = meta.title
wi.filename = meta.filename
wi.file_url = meta.file_url
wi.file_size = meta.file_size
wi.time_updated = meta.time_updated
wi.preview_url = meta.preview_url
wi.last_error = "" if meta.result == 1 else f"steam result {meta.result}"
job = enqueue_build_overlay(db, overlay_id=overlay_id, user_id=user.id)
job_id = job.id
return redirect(f"/jobs/{job_id}")
@bp.post("/admin/workshop/refresh") @bp.post("/admin/workshop/refresh")
@require_admin @require_admin
def admin_refresh() -> Response: def admin_refresh() -> Response:

View file

@ -283,3 +283,142 @@ def test_other_user_cannot_modify_workshop_overlay(overlay_for):
headers={"X-CSRF-Token": "test-token"}, headers={"X-CSRF-Token": "test-token"},
) )
assert response.status_code == 403 assert response.status_code == 403
def test_overlay_refresh_owner_can_refresh_and_enqueues_build(overlay_for):
app, login, user_id, _admin_id, overlay_id = overlay_for
user_client = login(user_id)
with _patch_steam([_meta("1001")]):
user_client.post(
f"/overlays/{overlay_id}/items",
data={"input": "1001", "input_mode": "items"},
headers={"X-CSRF-Token": "test-token"},
)
with session_scope() as session:
for job in session.query(Job).filter_by(operation="build_overlay", state="queued"):
job.state = "succeeded"
fresh_meta = steam_workshop.WorkshopMetadata(
steam_id="1001", title="Item 1001 (updated)", filename="1001.vpk",
file_url="https://example.com/1001.vpk", file_size=99, time_updated=1800000000,
preview_url="https://example.com/preview-1001.jpg", consumer_app_id=550, result=1,
)
with patch.object(steam_workshop, "fetch_metadata_batch", return_value=[fresh_meta]) as fetch:
response = user_client.post(
f"/overlays/{overlay_id}/refresh",
headers={"X-CSRF-Token": "test-token"},
)
assert response.status_code == 302
assert response.headers["Location"].startswith("/jobs/")
fetch.assert_called_once_with(["1001"], mode="refresh")
with session_scope() as session:
wi = session.query(WorkshopItem).filter_by(steam_id="1001").one()
assert wi.title == "Item 1001 (updated)"
assert wi.time_updated == 1800000000
new_jobs = session.query(Job).filter_by(
operation="build_overlay", overlay_id=overlay_id, state="queued"
).all()
assert len(new_jobs) == 1
def test_overlay_refresh_returns_400_when_overlay_empty(overlay_for):
app, login, user_id, _admin_id, overlay_id = overlay_for
user_client = login(user_id)
with patch.object(steam_workshop, "fetch_metadata_batch") as fetch:
response = user_client.post(
f"/overlays/{overlay_id}/refresh",
headers={"X-CSRF-Token": "test-token"},
)
assert response.status_code == 400
fetch.assert_not_called()
def test_overlay_refresh_forbidden_for_non_owner(overlay_for, env_user):
app, login, _user_id, _admin_id, overlay_id = overlay_for
with session_scope() as session:
bob = User(username="bob", password_digest=hash_password("x"), admin=False)
session.add(bob)
session.flush()
bob_id = bob.id
bob_client = login(bob_id)
response = bob_client.post(
f"/overlays/{overlay_id}/refresh",
headers={"X-CSRF-Token": "test-token"},
)
assert response.status_code == 403
def test_overlay_refresh_admin_can_refresh_anyone(overlay_for):
app, login, user_id, admin_id, overlay_id = overlay_for
user_client = login(user_id)
with _patch_steam([_meta("1001")]):
user_client.post(
f"/overlays/{overlay_id}/items",
data={"input": "1001", "input_mode": "items"},
headers={"X-CSRF-Token": "test-token"},
)
admin_client = login(admin_id)
with _patch_steam([_meta("1001")]):
response = admin_client.post(
f"/overlays/{overlay_id}/refresh",
headers={"X-CSRF-Token": "test-token"},
)
assert response.status_code == 302
def test_overlay_refresh_502_on_steam_error(overlay_for):
app, login, user_id, _admin_id, overlay_id = overlay_for
user_client = login(user_id)
with _patch_steam([_meta("1001")]):
user_client.post(
f"/overlays/{overlay_id}/items",
data={"input": "1001", "input_mode": "items"},
headers={"X-CSRF-Token": "test-token"},
)
with session_scope() as session:
for job in session.query(Job).filter_by(operation="build_overlay", state="queued"):
job.state = "succeeded"
baseline_job_count = session.query(Job).filter_by(
operation="build_overlay", overlay_id=overlay_id, state="queued"
).count()
with patch.object(steam_workshop, "fetch_metadata_batch", side_effect=Exception("boom")):
response = user_client.post(
f"/overlays/{overlay_id}/refresh",
headers={"X-CSRF-Token": "test-token"},
)
assert response.status_code == 502
assert b"steam api error" in response.data
with session_scope() as session:
n = session.query(Job).filter_by(
operation="build_overlay", overlay_id=overlay_id, state="queued"
).count()
assert n == baseline_job_count
def test_overlay_refresh_missing_item_records_last_error(overlay_for):
app, login, user_id, _admin_id, overlay_id = overlay_for
user_client = login(user_id)
with _patch_steam([_meta("1001")]):
user_client.post(
f"/overlays/{overlay_id}/items",
data={"input": "1001", "input_mode": "items"},
headers={"X-CSRF-Token": "test-token"},
)
with session_scope() as session:
for job in session.query(Job).filter_by(operation="build_overlay", state="queued"):
job.state = "succeeded"
with patch.object(steam_workshop, "fetch_metadata_batch", return_value=[]):
response = user_client.post(
f"/overlays/{overlay_id}/refresh",
headers={"X-CSRF-Token": "test-token"},
)
assert response.status_code == 302
with session_scope() as session:
wi = session.query(WorkshopItem).filter_by(steam_id="1001").one()
assert "no entry" in wi.last_error