Each linked overlay gets a checkbox on the blueprint detail page that opts
its server.cfg in as exec server_overlay_<id>. The web app builds the
spec with {path, alias} per overlay and prepends exec server_overlay_<id>
lines to the blueprint config in lowest-overlay-first order. The host
stages those copies in the overlayfs upper layer before mounting (avoids
copy-up writes against a sandbox-uid file). A live preview block above the
Config textarea shows what gets auto-executed.
Schema:
- alembic 0007: BlueprintOverlay.expose_server_cfg BOOLEAN
Spec contract:
- l4d2host OverlayRef(path, alias?). load_spec accepts both bare-string
and {path, alias} entries.
Side effects folded in (same file in l4d2_facade):
- start_server auto-initializes; the manual Initialize step is no longer
needed before Start.
- initialize_server no longer runs blueprint builders — builds happen on
overlay save, not on every server Start.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
80 lines
4.1 KiB
HTML
80 lines
4.1 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Blueprint {{ blueprint.name }} | left4me{% endblock %}
|
||
|
||
{% block content %}
|
||
<section class="panel">
|
||
<div class="page-heading">
|
||
<h1>Blueprint: {{ blueprint.name }}</h1>
|
||
</div>
|
||
<form method="post" action="/blueprints/{{ blueprint.id }}" class="stack">
|
||
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
|
||
<label><span class="section-title">Name</span><input name="name" value="{{ blueprint.name }}" required></label>
|
||
<label><span class="section-title">Arguments</span><textarea name="arguments" rows="2" spellcheck="false">{{ arguments | join('\n') }}</textarea></label>
|
||
<span class="section-title">Overlays</span>
|
||
<div class="overlay-picker">
|
||
<ol class="overlay-picker-list" data-overlay-list>
|
||
{% for overlay in selected_overlays %}
|
||
<li class="overlay-picker-row" draggable="true" data-overlay-id="{{ overlay.id }}" data-overlay-name="{{ overlay.name }}">
|
||
<span class="overlay-picker-handle" aria-hidden="true">⋮⋮</span>
|
||
<span class="overlay-picker-name">{{ overlay.name }}</span>
|
||
<label class="overlay-picker-expose" title="Auto-load this overlay's server.cfg before your blueprint config">
|
||
<input type="checkbox" name="expose_server_cfg_ids" value="{{ overlay.id }}"
|
||
{% if overlay_expose_state.get(overlay.id) %}checked{% endif %}>
|
||
exec <code>server.cfg</code>
|
||
</label>
|
||
<button type="button" class="overlay-picker-remove" data-action="remove" aria-label="Remove {{ overlay.name }}">×</button>
|
||
<input type="hidden" name="overlay_ids" value="{{ overlay.id }}">
|
||
</li>
|
||
{% endfor %}
|
||
</ol>
|
||
<p class="overlay-picker-empty muted" data-overlay-empty {% if selected_overlays %}hidden{% endif %}>No overlays selected. Pick one below to add.</p>
|
||
<div class="overlay-picker-add">
|
||
<select data-overlay-add aria-label="Add overlay">
|
||
<option value="">Add overlay…</option>
|
||
{% for overlay in available_overlays %}
|
||
<option value="{{ overlay.id }}" data-overlay-name="{{ overlay.name }}">{{ overlay.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
{% set exposed = [] %}
|
||
{# Source `exec` is last-wins. First overlay in the list = topmost =
|
||
highest precedence, so its exec runs LAST. Iterate the picker list in
|
||
reverse to render the preview in actual execution order. #}
|
||
{% for overlay in selected_overlays | reverse %}{% if overlay_expose_state.get(overlay.id) %}{{ exposed.append(overlay) or '' }}{% endif %}{% endfor %}
|
||
<label><span class="section-title">Config</span>
|
||
<div class="config-shell">
|
||
{% if exposed %}
|
||
<pre class="config-preview" aria-label="Auto-loaded overlay configs">{% for o in exposed %}exec {{ o.name }}.cfg
|
||
{% endfor %}</pre>
|
||
{% endif %}
|
||
<textarea name="config" rows="8" spellcheck="false">{{ config_lines | join('\n') }}</textarea>
|
||
</div>
|
||
</label>
|
||
<button type="submit">Save blueprint</button>
|
||
</form>
|
||
</section>
|
||
|
||
<div class="page-footer-actions">
|
||
<button type="button" class="danger-outline" data-modal-open="delete-blueprint-modal">Delete blueprint</button>
|
||
</div>
|
||
|
||
<dialog id="delete-blueprint-modal" class="modal" aria-labelledby="delete-blueprint-title">
|
||
<div class="modal-header">
|
||
<h2 id="delete-blueprint-title">Delete blueprint "{{ blueprint.name }}"?</h2>
|
||
<button type="button" class="modal-close" data-modal-close aria-label="Close">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>This cannot be undone. Blueprints in use by a server cannot be deleted.</p>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="button-secondary" data-modal-close>Cancel</button>
|
||
<form method="post" action="/blueprints/{{ blueprint.id }}/delete" class="inline-form">
|
||
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
|
||
<button class="danger" type="submit">Delete</button>
|
||
</form>
|
||
</div>
|
||
</dialog>
|
||
<script src="{{ url_for('static', filename='js/blueprint-overlay-picker.js') }}" defer></script>
|
||
{% endblock %}
|