Files
ske087 046f5e5efd Add audio parameter to player playlist and auto-mark deployment status
- 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
2026-08-05 11:43:02 +03:00

360 lines
10 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}