Files
digiserver-v2/app/templates/players/players_list.html
T
ske087 05194a19b6 Add edited media report page and fix image versioning
- 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
2026-07-21 09:56:14 +03:00

350 lines
9.8 KiB
HTML
Raw 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: 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 %}