This PR breaks the large web and player bootstrap files into smaller modules with clearer ownership.

Web changes:

Split shared helpers, bootstrap logic, route groups, and upload-sync behavior out of web.js.
Kept web.js focused on wiring and server startup.
Fixed screen playlist reassignment so changing a screen’s playlist now triggers a refresh.
Fixed single-slide playlist refresh behavior so updates do not get stuck behind the current slide.
Player changes:

Split websocket/runtime handling into runtime.js.
Split playlist assembly and revision hashing into playlist.js.
Split onboarding and player HTTP routes into dedicated modules.
Split render utilities and template loading into render-helpers.js.
Kept player.js mostly as startup/orchestration.
Validation:

Rebuilt both services with Docker Compose.
Smoke-checked web and player routes after the refactor.
Verified get_errors was clean on the touched modules.
This commit is contained in:
2026-07-20 23:58:27 +01:00
parent 480ccdbe9c
commit 2ea8d389fa
321 changed files with 12687 additions and 7080 deletions
+48 -31
View File
@@ -3,38 +3,55 @@
<h2>Edit screen</h2>
<p>Update the display name and assigned playlist.</p>
</div>
<a class="button-link" href="/admin/screens">Back to screens</a>
</div>
<div class="card">
<h3>Edit screen</h3>
<form id="screen-edit-form" class="screen-edit-form" method="post" action="/admin/screens/{{screen.id}}" data-async-save data-async-save-reload>
<label>Name
<input name="name" value="{{screen.name}}" required />
</label>
<label>Slug
<input name="slug" value="{{screen.slug}}" />
</label>
<label>Playlist
<select name="playlist_id">
<option value="">-- not assigned --</option>
{{#each playlists}}
<option value="{{id}}" {{#if (eq ../screen.playlist_id id)}}selected{{/if}}>{{name}}</option>
{{/each}}
</select>
</label>
</form>
<div class="screen-edit-actions">
<button type="submit" form="screen-edit-form">Save changes</button>
<form class="inline-form" method="post" action="/admin/screens/{{screen.id}}/delete" data-confirm-message="Delete this screen?">
<button class="danger" type="submit">Delete screen</button>
</form>
<div class="row g-4">
<div class="col-12 col-xl-8">
<div class="card card-outline card-primary admin-form-card h-100">
<div class="card-header">
<h3 class="card-title">Screen details</h3>
</div>
<form id="screen-edit-form" method="post" action="/admin/screens/{{screen.id}}" data-async-save data-async-save-close-url="/admin/screens" data-async-save-new-url="/admin/screens/new">
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-6">
<label for="screen-name" class="form-label">Name</label>
<input id="screen-name" name="name" class="form-control" value="{{screen.name}}" required />
</div>
<div class="col-12 col-md-6">
<label for="screen-slug" class="form-label">Slug</label>
<input id="screen-slug" name="slug" class="form-control" value="{{screen.slug}}" />
</div>
<div class="col-12">
<label for="screen-playlist" class="form-label">Playlist</label>
<select id="screen-playlist" name="playlist_id" class="form-select">
<option value="">-- not assigned --</option>
{{#each playlists}}
<option value="{{id}}" {{#if (eq ../screen.playlist_id id)}}selected{{/if}}>{{name}}</option>
{{/each}}
</select>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
<div class="btn-group screen-actions-group" role="group" aria-label="Screen actions">
{{{saveActionButtons formId="screen-edit-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
<a class="btn btn-warning" href="/admin/screens" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
<button type="submit" class="btn btn-danger" form="screen-delete-form">Delete</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="card">
<h3>Player URL</h3>
<p><a href="{{playerUrl screen.slug}}" target="_blank">{{playerUrl screen.slug}}</a></p>
</div>
<div class="col-12 col-xl-4">
<div class="card card-outline card-secondary admin-form-card h-100">
<div class="card-header">
<h3 class="card-title">Player URL</h3>
</div>
<div class="card-body">
<p class="mb-0"><a href="{{playerUrl screen.slug}}" target="_blank">{{playerUrl screen.slug}}</a></p>
</div>
</div>
</div>
</div>