feat(l4d2-web): script overlay UI
Adds the script type to the create-overlay modal (with an admin-only system-wide checkbox) and a script-section to the detail page: textarea for the bash body, Save / Rebuild / Wipe buttons, last_build_status badge, latest-build-job link, and a Wipe confirm modal. Removes the GlobalOverlaySource block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
be22744d54
commit
d351bcbee5
3 changed files with 129 additions and 11 deletions
|
|
@ -6,7 +6,7 @@
|
|||
<section class="panel">
|
||||
<div class="page-heading">
|
||||
<h1>Overlay: {{ overlay.name }}</h1>
|
||||
{% set can_edit = overlay.type not in ['l4d2center_maps', 'cedapug_maps'] and (g.user.admin or (overlay.type == 'workshop' and overlay.user_id == g.user.id)) %}
|
||||
{% set can_edit = g.user.admin or (overlay.type in ['workshop', 'script'] and overlay.user_id == g.user.id) %}
|
||||
{% if can_edit %}
|
||||
<button type="button" class="danger" data-modal-open="delete-overlay-modal">Delete</button>
|
||||
{% endif %}
|
||||
|
|
@ -27,21 +27,58 @@
|
|||
<tr><th>Type</th><td>{{ overlay.type }}</td></tr>
|
||||
<tr><th>Scope</th><td>{% if overlay.user_id %}private{% else %}system{% endif %}</td></tr>
|
||||
<tr><th>Path</th><td class="muted">{{ overlay.path }}</td></tr>
|
||||
<tr>
|
||||
<th>Last build</th>
|
||||
<td>
|
||||
{% if overlay.last_build_status == 'ok' %}
|
||||
<span class="badge badge-ok">ok</span>
|
||||
{% elif overlay.last_build_status == 'failed' %}
|
||||
<span class="badge badge-error">failed</span>
|
||||
{% else %}
|
||||
<span class="badge badge-muted">never</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
{% if global_source %}
|
||||
{% if overlay.type == 'script' %}
|
||||
<section class="panel">
|
||||
<h2>Global source</h2>
|
||||
<table class="definition-table">
|
||||
<tbody>
|
||||
<tr><th>Source key</th><td>{{ global_source.source_key }}</td></tr>
|
||||
<tr><th>Source URL</th><td><a href="{{ global_source.source_url }}">{{ global_source.source_url }}</a></td></tr>
|
||||
<tr><th>Last refreshed</th><td>{{ global_source.last_refreshed_at or "Never" }}</td></tr>
|
||||
<tr><th>Last error</th><td>{{ global_source.last_error or "None" }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="page-heading">
|
||||
<h2>Script</h2>
|
||||
{% if can_edit %}
|
||||
<div class="inline-form-group">
|
||||
<form method="post" action="/overlays/{{ overlay.id }}/build" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
|
||||
<button type="submit" class="button-secondary">Rebuild</button>
|
||||
</form>
|
||||
<button type="button" class="danger" data-modal-open="wipe-overlay-modal">Wipe</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if can_edit %}
|
||||
<form method="post" action="/overlays/{{ overlay.id }}/script" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
|
||||
<label>Bash script
|
||||
<textarea name="script" rows="20" spellcheck="false">{{ overlay.script or "" }}</textarea>
|
||||
</label>
|
||||
<p class="muted">Runs sandboxed against the overlay directory mounted at <code>/overlay</code>. Saving auto-enqueues a build.</p>
|
||||
<div>
|
||||
<button type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<pre class="script-preview">{{ overlay.script or "" }}</pre>
|
||||
{% endif %}
|
||||
|
||||
{% if latest_build_job %}
|
||||
<p>
|
||||
Latest build: <a href="/jobs/{{ latest_build_job.id }}">job #{{ latest_build_job.id }}</a>
|
||||
— state: <strong>{{ latest_build_job.state }}</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
|
|
@ -118,5 +155,24 @@
|
|||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
{% if overlay.type == 'script' %}
|
||||
<dialog id="wipe-overlay-modal" class="modal" aria-labelledby="wipe-overlay-title">
|
||||
<div class="modal-header">
|
||||
<h2 id="wipe-overlay-title">Wipe overlay "{{ overlay.name }}"?</h2>
|
||||
<button type="button" class="modal-close" data-modal-close aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Empties the overlay directory. Use Rebuild afterward to repopulate.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="button-secondary" data-modal-close>Cancel</button>
|
||||
<form method="post" action="/overlays/{{ overlay.id }}/wipe" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
|
||||
<button class="danger" type="submit">Wipe</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -37,8 +37,12 @@
|
|||
<fieldset class="overlay-type-radio">
|
||||
<legend>Type</legend>
|
||||
<label><input type="radio" name="type" value="workshop" checked> Workshop (downloads from Steam)</label>
|
||||
<label><input type="radio" name="type" value="script"> Script (runs sandboxed bash)</label>
|
||||
</fieldset>
|
||||
<label>Name <input name="name" required></label>
|
||||
{% if g.user and g.user.admin %}
|
||||
<label><input type="checkbox" name="system_wide" value="1"> System-wide (visible to all users)</label>
|
||||
{% endif %}
|
||||
<p class="muted">The path is generated automatically.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
|
|
|||
|
|
@ -510,3 +510,61 @@ def test_non_admin_cannot_view_system_job(tmp_path, monkeypatch) -> None:
|
|||
|
||||
response = user_client.get(f"/jobs/{job_id}")
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_overlay_create_modal_offers_script_type(auth_client_with_server) -> None:
|
||||
response = auth_client_with_server.get("/overlays")
|
||||
text = response.get_data(as_text=True)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert 'value="workshop"' in text
|
||||
assert 'value="script"' in text
|
||||
|
||||
|
||||
def _seed_overlay(name: str, type_: str, user_id: int) -> int:
|
||||
with session_scope() as s:
|
||||
overlay = Overlay(name=name, path="", type=type_, user_id=user_id)
|
||||
s.add(overlay)
|
||||
s.flush()
|
||||
overlay.path = str(overlay.id)
|
||||
s.flush()
|
||||
return overlay.id
|
||||
|
||||
|
||||
def test_overlay_detail_script_section(auth_client_with_server) -> None:
|
||||
with session_scope() as s:
|
||||
user_id = s.query(User).filter_by(username="alice").one().id
|
||||
overlay_id = _seed_overlay("build", "script", user_id)
|
||||
|
||||
response = auth_client_with_server.get(f"/overlays/{overlay_id}")
|
||||
text = response.get_data(as_text=True)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert '<textarea name="script"' in text
|
||||
assert "Rebuild" in text
|
||||
assert "Wipe" in text
|
||||
assert "Last build" in text
|
||||
|
||||
|
||||
def test_overlay_detail_workshop_section_unchanged(auth_client_with_server) -> None:
|
||||
with session_scope() as s:
|
||||
user_id = s.query(User).filter_by(username="alice").one().id
|
||||
overlay_id = _seed_overlay("ws", "workshop", user_id)
|
||||
|
||||
response = auth_client_with_server.get(f"/overlays/{overlay_id}")
|
||||
text = response.get_data(as_text=True)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Workshop items" in text
|
||||
|
||||
|
||||
def test_overlay_detail_no_global_source_block(auth_client_with_server) -> None:
|
||||
with session_scope() as s:
|
||||
user_id = s.query(User).filter_by(username="alice").one().id
|
||||
overlay_id = _seed_overlay("ws", "workshop", user_id)
|
||||
|
||||
response = auth_client_with_server.get(f"/overlays/{overlay_id}")
|
||||
text = response.get_data(as_text=True)
|
||||
|
||||
assert "Global source" not in text
|
||||
assert "source_url" not in text
|
||||
|
|
|
|||
Loading…
Reference in a new issue