046f5e5efd
- api.py: include 'audio' (on/off) field in playlist JSON served to players, derived from the muted toggle in the manage playlist page - players.py: include 'audio' field in legacy player playlist builder - api.py: auto-flip deployment_status pending/deploying -> deployed when a player sends feedback (player is confirmed running) - players_list.html: poll deployment status for pending/failed badges so the players list live-updates once a player starts sending feedback - .dockerignore: exclude runtime data/ volume dir from build context
360 lines
10 KiB
HTML
360 lines
10 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Players - DigiServer v2{% endblock %}
|
||
|
||
{% block content %}
|
||
<style>
|
||
body.dark-mode h1 {
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
.players-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.players-table thead tr {
|
||
background: #f8f9fa;
|
||
text-align: left;
|
||
}
|
||
|
||
body.dark-mode .players-table thead tr {
|
||
background: #1a202c;
|
||
}
|
||
|
||
.players-table th {
|
||
padding: 12px;
|
||
border-bottom: 2px solid #dee2e6;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
}
|
||
|
||
body.dark-mode .players-table th {
|
||
border-bottom-color: #4a5568;
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
.players-table tbody tr {
|
||
border-bottom: 1px solid #dee2e6;
|
||
}
|
||
|
||
body.dark-mode .players-table tbody tr {
|
||
border-bottom-color: #4a5568;
|
||
}
|
||
|
||
.players-table tbody tr:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
body.dark-mode .players-table tbody tr:hover {
|
||
background: #2d3748;
|
||
}
|
||
|
||
.players-table td {
|
||
padding: 12px;
|
||
color: #2d3748;
|
||
}
|
||
|
||
body.dark-mode .players-table td {
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
.players-table td strong {
|
||
color: #2d3748;
|
||
}
|
||
|
||
body.dark-mode .players-table td strong {
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
.players-table code {
|
||
background: #f8f9fa;
|
||
padding: 2px 6px;
|
||
border-radius: 3px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
body.dark-mode .players-table code {
|
||
background: #1a202c;
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
.status-badge {
|
||
padding: 3px 8px;
|
||
border-radius: 3px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: white;
|
||
}
|
||
|
||
.status-badge.online {
|
||
background: #28a745;
|
||
}
|
||
|
||
.status-badge.offline {
|
||
background: #6c757d;
|
||
}
|
||
|
||
.text-muted {
|
||
color: #6c757d;
|
||
}
|
||
|
||
body.dark-mode .text-muted {
|
||
color: #718096;
|
||
}
|
||
|
||
.info-box {
|
||
background: #d1ecf1;
|
||
border: 1px solid #bee5eb;
|
||
color: #0c5460;
|
||
padding: 15px;
|
||
border-radius: 5px;
|
||
}
|
||
|
||
body.dark-mode .info-box {
|
||
background: #1a365d;
|
||
border-color: #2c5282;
|
||
color: #90cdf4;
|
||
}
|
||
|
||
.info-box a {
|
||
color: #0c5460;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
body.dark-mode .info-box a {
|
||
color: #90cdf4;
|
||
}
|
||
|
||
/* Deployment status */
|
||
.deploy-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 3px 8px;
|
||
border-radius: 3px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
.deploy-badge.pending {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
body.dark-mode .deploy-badge.pending {
|
||
background: #4a3800;
|
||
color: #fbbf24;
|
||
}
|
||
.deploy-badge.deployed {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
body.dark-mode .deploy-badge.deployed {
|
||
background: #1a4d2e;
|
||
color: #86efac;
|
||
}
|
||
.deploy-badge.failed {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
body.dark-mode .deploy-badge.failed {
|
||
background: #4a1a1a;
|
||
color: #fc8181;
|
||
}
|
||
.deploy-badge.deploying {
|
||
background: #cce5ff;
|
||
color: #004085;
|
||
animation: pulse-bg 1.5s ease-in-out infinite;
|
||
}
|
||
body.dark-mode .deploy-badge.deploying {
|
||
background: #1a365d;
|
||
color: #90cdf4;
|
||
}
|
||
.deploy-badge .spinner {
|
||
display: inline-block;
|
||
width: 12px;
|
||
height: 12px;
|
||
border: 2px solid rgba(0,64,133,0.3);
|
||
border-radius: 50%;
|
||
border-top-color: #004085;
|
||
animation: deploy-spin 0.8s linear infinite;
|
||
}
|
||
body.dark-mode .deploy-badge .spinner {
|
||
border-color: rgba(144,205,244,0.3);
|
||
border-top-color: #90cdf4;
|
||
}
|
||
@keyframes deploy-spin { to { transform: rotate(360deg); } }
|
||
@keyframes pulse-bg {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.6; }
|
||
}
|
||
.deploy-timestamp {
|
||
font-size: 11px;
|
||
color: #6c757d;
|
||
display: block;
|
||
margin-top: 2px;
|
||
}
|
||
body.dark-mode .deploy-timestamp {
|
||
color: #718096;
|
||
}
|
||
.deploy-tooltip {
|
||
cursor: help;
|
||
border-bottom: 1px dashed #aaa;
|
||
}
|
||
</style>
|
||
<div class="container">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h1>Players</h1>
|
||
<a href="{{ url_for('players.add_player') }}" class="btn btn-success">+ Add New Player</a>
|
||
</div>
|
||
|
||
{% if players %}
|
||
<div class="card">
|
||
<table class="players-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Hostname</th>
|
||
<th>Location</th>
|
||
<th>Orientation</th>
|
||
<th>Status</th>
|
||
<th>Deployment</th>
|
||
<th>Last Seen</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for player in players %}
|
||
<tr id="player-row-{{ player.id }}">
|
||
<td>
|
||
<strong>{{ player.name }}</strong>
|
||
</td>
|
||
<td>
|
||
<code>{{ player.hostname }}</code>
|
||
</td>
|
||
<td>
|
||
{{ player.location or '-' }}
|
||
</td>
|
||
<td>
|
||
{{ player.orientation or 'Landscape' }}
|
||
</td>
|
||
<td>
|
||
{% if player.is_online %}
|
||
<span class="status-badge online">Online</span>
|
||
{% else %}
|
||
<span class="status-badge offline">Offline</span>
|
||
{% endif %}
|
||
</td>
|
||
<td id="deploy-cell-{{ player.id }}">
|
||
{% include "players/_deploy_badge.html" %}
|
||
</td>
|
||
<td>
|
||
{% if player.last_seen %}
|
||
{{ player.last_seen | localtime }}
|
||
{% else %}
|
||
<span class="text-muted">Never</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<a href="{{ url_for('players.manage_player', player_id=player.id) }}"
|
||
class="btn btn-info btn-sm" title="Manage Player">
|
||
⚙️ Manage
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<div class="info-box">
|
||
ℹ️ No players yet. <a href="{{ url_for('players.add_player') }}">Add your first player</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<script>
|
||
// ── Deployment status polling ────────────────────────────────────────────
|
||
(function() {
|
||
var POLL_INTERVAL = 5000; // 5 seconds
|
||
var polling = false;
|
||
|
||
// Initial check: any badge that isn't yet "Deployed" yet (deploying,
|
||
// pending, or failed). We keep polling so that players which send
|
||
// feedback auto-transition from "pending" to "deployed".
|
||
var activeBadges = document.querySelectorAll(
|
||
'.deploy-badge.deploying, .deploy-badge.pending, .deploy-badge.failed'
|
||
);
|
||
if (activeBadges.length > 0) {
|
||
polling = true;
|
||
schedulePoll();
|
||
}
|
||
|
||
function schedulePoll() {
|
||
setTimeout(pollDeploymentStatus, POLL_INTERVAL);
|
||
}
|
||
|
||
function pollDeploymentStatus() {
|
||
if (!polling) return;
|
||
|
||
fetch('{{ url_for("players.deployment_status") }}')
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
var anyActive = false;
|
||
|
||
for (var playerId in data) {
|
||
if (!data.hasOwnProperty(playerId)) continue;
|
||
var status = data[playerId];
|
||
var cell = document.getElementById('deploy-cell-' + playerId);
|
||
if (!cell) continue;
|
||
|
||
var ds = status.deployment_status;
|
||
var lds = status.last_deployment_status;
|
||
var msg = status.last_deployment_message || '';
|
||
var ts = status.last_deployment_at
|
||
? new Date(status.last_deployment_at + 'Z').toLocaleString()
|
||
: '';
|
||
|
||
if (ds === 'deployed') {
|
||
cell.innerHTML = '<span class="deploy-badge deployed" title="' + escapeHtml(msg) + '">\u2705 Deployed<span class="deploy-timestamp">' + ts + '</span></span>';
|
||
} else if (ds === 'failed') {
|
||
// Terminal state - no need to keep polling for this one
|
||
cell.innerHTML = '<span class="deploy-badge failed deploy-tooltip" title="' + escapeHtml(msg) + '">\u274c Failed<span class="deploy-timestamp">' + ts + '</span></span>';
|
||
} else if (ds === 'deploying') {
|
||
anyActive = true;
|
||
if (!cell.querySelector('.deploying')) {
|
||
cell.innerHTML = '<span class="deploy-badge deploying"><span class="spinner"></span>Deploying...</span>';
|
||
}
|
||
} else {
|
||
// pending / null - deployment not confirmed yet.
|
||
// Keep polling: the player may start sending feedback,
|
||
// which flips the status to "deployed" on the server.
|
||
anyActive = true;
|
||
if (!cell.querySelector('.pending')) {
|
||
cell.innerHTML = '<span class="deploy-badge pending" title="Awaiting deployment">\u23f3 Pending</span>';
|
||
}
|
||
}
|
||
}
|
||
|
||
if (anyActive) {
|
||
schedulePoll();
|
||
} else {
|
||
polling = false;
|
||
}
|
||
})
|
||
.catch(function() {
|
||
// Retry
|
||
if (polling) schedulePoll();
|
||
});
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
var div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
})();
|
||
</script>
|
||
|
||
{% endblock %}
|