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
This commit is contained in:
2026-07-21 09:56:14 +03:00
parent 2af04e1db3
commit 05194a19b6
7 changed files with 718 additions and 54 deletions
+25
View File
@@ -0,0 +1,25 @@
{% if player.deployment_status == 'deploying' %}
<span class="deploy-badge deploying" id="deploying-badge-{{ player.id }}">
<span class="spinner"></span>Deploying...
</span>
{% elif player.deployment_status == 'deployed' %}
<span class="deploy-badge deployed" title="{{ player.last_deployment_message or 'Deployed successfully' }}">
✅ Deployed
{% if player.last_deployment_at %}
<span class="deploy-timestamp">{{ player.last_deployment_at | localtime }}</span>
{% endif %}
</span>
{% elif player.deployment_status == 'failed' %}
<span class="deploy-badge failed deploy-tooltip" title="{{ player.last_deployment_message or 'Deployment failed' }}">
❌ Failed
{% if player.last_deployment_at %}
<span class="deploy-timestamp">{{ player.last_deployment_at | localtime }}</span>
{% endif %}
</span>
{% elif player.deployment_status == 'pending' %}
<span class="deploy-badge pending" title="Awaiting deployment">
⏳ Pending
</span>
{% else %}
<span class="text-muted"></span>
{% endif %}
@@ -0,0 +1,342 @@
{% extends "base.html" %}
{% block title %}Edited Media Report - {{ player.name }}{% endblock %}
{% block content %}
<style>
.report-container {
margin-bottom: 2rem;
}
.report-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.report-header h1 {
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.5rem;
}
.report-summary {
display: flex;
gap: 1.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.summary-stat {
background: #f8f9fa;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 0.75rem 1.25rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
body.dark-mode .summary-stat {
background: #1a202c;
border-color: #4a5568;
}
.summary-stat .stat-value {
font-size: 1.4rem;
font-weight: 700;
color: #7c3aed;
}
.summary-stat .stat-label {
font-size: 0.85rem;
color: #64748b;
}
.report-table-wrapper {
overflow-x: auto;
border: 1px solid #e2e8f0;
border-radius: 10px;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
body.dark-mode .report-table-wrapper {
background: #1a202c;
border-color: #4a5568;
}
.report-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.report-table thead {
background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
color: white;
}
.report-table thead th {
padding: 0.85rem 1rem;
text-align: left;
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
}
.report-table tbody tr {
border-bottom: 1px solid #e2e8f0;
transition: background 0.15s;
}
body.dark-mode .report-table tbody tr {
border-bottom-color: #4a5568;
}
.report-table tbody tr:hover {
background: rgba(124, 58, 237, 0.05);
}
body.dark-mode .report-table tbody tr:hover {
background: rgba(124, 58, 237, 0.12);
}
.report-table tbody tr:nth-child(even) {
background: #f8fafc;
}
body.dark-mode .report-table tbody tr:nth-child(even) {
background: #162032;
}
.report-table tbody td {
padding: 0.75rem 1rem;
vertical-align: middle;
}
.report-table .col-user {
font-weight: 600;
color: #1a202c;
}
body.dark-mode .report-table .col-user {
color: #e2e8f0;
}
.report-table .col-filename {
font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace;
font-size: 0.82rem;
color: #475569;
word-break: break-all;
}
body.dark-mode .report-table .col-filename {
color: #cbd5e1;
}
.report-table .col-date {
color: #64748b;
white-space: nowrap;
font-size: 0.85rem;
}
.report-table .col-link a {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.4rem 0.8rem;
background: #7c3aed;
color: white;
text-decoration: none;
border-radius: 5px;
font-size: 0.8rem;
font-weight: 500;
transition: background 0.2s, transform 0.15s;
white-space: nowrap;
}
.report-table .col-link a:hover {
background: #6d28d9;
transform: translateY(-1px);
}
body.dark-mode .report-table .col-link a {
background: #6d28d9;
}
body.dark-mode .report-table .col-link a:hover {
background: #5b21b6;
}
.report-table .col-version {
text-align: center;
}
.version-badge {
display: inline-block;
padding: 0.2rem 0.55rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
background: #ede9fe;
color: #7c3aed;
}
body.dark-mode .version-badge {
background: rgba(124, 58, 237, 0.2);
color: #a78bfa;
}
.no-data {
text-align: center;
padding: 4rem 2rem;
color: #6c757d;
}
.no-data .icon {
font-size: 3rem;
margin-bottom: 0.75rem;
}
.no-data p {
margin: 0.25rem 0;
}
.print-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: #64748b;
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.2s;
text-decoration: none;
}
.print-btn:hover {
background: #475569;
}
@media print {
body {
background: white !important;
color: black !important;
}
.report-table-wrapper {
border: 1px solid #ccc !important;
box-shadow: none !important;
}
.report-table thead {
background: #333 !important;
}
.btn, .print-btn, nav, .back-link {
display: none !important;
}
}
</style>
<div class="report-container">
<div class="report-header">
<a href="{{ url_for('players.manage_player', player_id=player.id) }}"
class="btn back-link"
style="background: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 6px; display: inline-flex; align-items: center; gap: 0.5rem;">
← Back to Player
</a>
<h1>
<img src="{{ url_for('static', filename='icons/edit.svg') }}" alt="" style="width: 28px; height: 28px;">
Edited Media Report — {{ player.name }}
</h1>
<button class="print-btn" onclick="window.print()">
🖨️ Print / PDF
</button>
</div>
{% if edited_media %}
{% set total_edits = edited_media|length %}
{% set unique_files = edited_media|map(attribute='content_id')|unique|list|length %}
{% set unique_users = edited_media|selectattr('user')|map(attribute='user')|unique|list|length %}
<div class="report-summary">
<div class="summary-stat">
<span class="stat-value">{{ total_edits }}</span>
<span class="stat-label">Total Edits</span>
</div>
<div class="summary-stat">
<span class="stat-value">{{ unique_files }}</span>
<span class="stat-label">Files Edited</span>
</div>
<div class="summary-stat">
<span class="stat-value">{{ unique_users }}</span>
<span class="stat-label">Editors</span>
</div>
</div>
<div class="report-table-wrapper">
<table class="report-table">
<thead>
<tr>
<th style="width: 22%;">User</th>
<th style="width: 30%;">Edited File</th>
<th style="width: 15%;">Version</th>
<th style="width: 18%;">Date</th>
<th style="width: 15%;">Link</th>
</tr>
</thead>
<tbody>
{% for edit in edited_media %}
<tr>
<td class="col-user">
{% if edit.user %}
{% set display_name = user_mappings.get(edit.user, edit.user) %}
👤 {{ display_name }}
{% else %}
<span style="color: #94a3b8;"></span>
{% endif %}
</td>
<td class="col-filename">
📄 {{ edit.new_name }}
</td>
<td class="col-version">
<span class="version-badge">v{{ edit.version }}</span>
</td>
<td class="col-date">
{% if edit.time_of_modification %}
{{ edit.time_of_modification | localtime('%Y-%m-%d %H:%M') }}
{% elif edit.created_at %}
{{ edit.created_at | localtime('%Y-%m-%d %H:%M') }}
{% else %}
<span style="color: #94a3b8;"></span>
{% endif %}
</td>
<td class="col-link">
<a href="{{ url_for('static', filename='uploads/edited_media/' ~ edit.content_id ~ '/' ~ edit.new_name) }}"
target="_blank"
title="Open {{ edit.new_name }}">
🔗 Open File
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="no-data">
<div class="icon">📋</div>
<p style="font-size: 1.1rem; font-weight: 500;">No edited media found</p>
<p>This player has not submitted any edited media yet.</p>
</div>
{% endif %}
</div>
{% endblock %}
+16 -7
View File
@@ -646,13 +646,22 @@ document.addEventListener('keydown', function(event) {
Edited Media on the Player
</h2>
{% if edited_media %}
<a href="{{ url_for('players.edited_media', player_id=player.id) }}"
class="btn"
style="background: #7c3aed; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 6px; font-size: 0.9rem; display: inline-flex; align-items: center; gap: 0.5rem; transition: background 0.2s;"
onmouseover="this.style.background='#6d28d9'"
onmouseout="this.style.background='#7c3aed'">
📋 View All Edited Media
</a>
<div style="display: flex; gap: 0.5rem;">
<a href="{{ url_for('players.edited_media', player_id=player.id) }}"
class="btn"
style="background: #7c3aed; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 6px; font-size: 0.9rem; display: inline-flex; align-items: center; gap: 0.5rem; transition: background 0.2s;"
onmouseover="this.style.background='#6d28d9'"
onmouseout="this.style.background='#7c3aed'">
📋 View All Edited Media
</a>
<a href="{{ url_for('players.edited_media_report', player_id=player.id) }}"
class="btn"
style="background: #059669; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 6px; font-size: 0.9rem; display: inline-flex; align-items: center; gap: 0.5rem; transition: background 0.2s;"
onmouseover="this.style.background='#047857'"
onmouseout="this.style.background='#059669'">
📊 Launch Report
</a>
</div>
{% endif %}
</div>
<p style="color: #6c757d; font-size: 0.9rem; margin-top: 0.5rem;">Latest 3 edited files with their most recent versions</p>
+155 -1
View File
@@ -125,6 +125,82 @@
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;">
@@ -142,13 +218,14 @@
<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>
<tr id="player-row-{{ player.id }}">
<td>
<strong>{{ player.name }}</strong>
</td>
@@ -168,6 +245,9 @@
<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 }}
@@ -192,4 +272,78 @@
</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 %}