05194a19b6
- New route GET /players/<id>/edited-media-report with tabular report - New template edited_media_report.html with User/Filename/Date/Link columns - Launch Report button (green) next to View All Edited Media button - Fix: on first edit, move original file to versionized folder as original_<name> - Fix: content lookup fallback via PlayerEdit records and path regex - Keep Content.filename pointing to latest edit for player playlist
350 lines
9.8 KiB
HTML
350 lines
9.8 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: are there any "Deploying..." badges?
|
||
var deployingBadges = document.querySelectorAll('.deploy-badge.deploying');
|
||
if (deployingBadges.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 anyDeploying = 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') {
|
||
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') {
|
||
anyDeploying = true;
|
||
if (!cell.querySelector('.deploying')) {
|
||
cell.innerHTML = '<span class="deploy-badge deploying"><span class="spinner"></span>Deploying...</span>';
|
||
}
|
||
} else {
|
||
// No deployment or not started (pending, null, etc.)
|
||
// Don't set anyDeploying = false here — we only care about active deployments
|
||
}
|
||
}
|
||
|
||
if (anyDeploying) {
|
||
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 %}
|