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:
+61
-31
@@ -734,75 +734,105 @@ def receive_edited_media():
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
player = request.player
|
player = request.player
|
||||||
|
|
||||||
# Check if file is present
|
# Check if file is present
|
||||||
if 'image_file' not in request.files:
|
if 'image_file' not in request.files:
|
||||||
return jsonify({'error': 'No image file provided'}), 400
|
return jsonify({'error': 'No image file provided'}), 400
|
||||||
|
|
||||||
file = request.files['image_file']
|
file = request.files['image_file']
|
||||||
if file.filename == '':
|
if file.filename == '':
|
||||||
return jsonify({'error': 'No file selected'}), 400
|
return jsonify({'error': 'No file selected'}), 400
|
||||||
|
|
||||||
# Get metadata
|
# Get metadata
|
||||||
import json
|
import json
|
||||||
metadata_str = request.form.get('metadata')
|
metadata_str = request.form.get('metadata')
|
||||||
if not metadata_str:
|
if not metadata_str:
|
||||||
return jsonify({'error': 'No metadata provided'}), 400
|
return jsonify({'error': 'No metadata provided'}), 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
metadata = json.loads(metadata_str)
|
metadata = json.loads(metadata_str)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return jsonify({'error': 'Invalid metadata JSON'}), 400
|
return jsonify({'error': 'Invalid metadata JSON'}), 400
|
||||||
|
|
||||||
# Validate required metadata fields
|
# Validate required metadata fields
|
||||||
required_fields = ['time_of_modification', 'original_name', 'new_name', 'version']
|
required_fields = ['time_of_modification', 'original_name', 'new_name', 'version']
|
||||||
for field in required_fields:
|
for field in required_fields:
|
||||||
if field not in metadata:
|
if field not in metadata:
|
||||||
return jsonify({'error': f'Missing required field: {field}'}), 400
|
return jsonify({'error': f'Missing required field: {field}'}), 400
|
||||||
|
|
||||||
# Import required modules
|
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from app.models.player_edit import PlayerEdit
|
from app.models.player_edit import PlayerEdit
|
||||||
|
|
||||||
# Find the original content by filename
|
|
||||||
original_name = metadata['original_name']
|
original_name = metadata['original_name']
|
||||||
|
version = metadata['version']
|
||||||
|
new_filename = metadata['new_name']
|
||||||
|
|
||||||
|
# ── Content lookup ───────────────────────────────────────────────
|
||||||
|
# If the player sends the current filename (pointing to edited_media/...),
|
||||||
|
# we find the content by its ID embedded in the path. Otherwise do a
|
||||||
|
# direct filename match.
|
||||||
content = Content.query.filter_by(filename=original_name).first()
|
content = Content.query.filter_by(filename=original_name).first()
|
||||||
|
if not content:
|
||||||
|
# Try to extract content_id from a path like "edited_media/<id>/file"
|
||||||
|
import re
|
||||||
|
m = re.match(r'edited_media/(\d+)/', original_name)
|
||||||
|
if m:
|
||||||
|
content = db.session.get(Content, int(m.group(1)))
|
||||||
|
if not content:
|
||||||
|
# Last resort – look up the most recent PlayerEdit for this
|
||||||
|
# content and use its content_id.
|
||||||
|
fallback_edit = PlayerEdit.query.filter_by(new_name=original_name)\
|
||||||
|
.order_by(PlayerEdit.created_at.desc()).first()
|
||||||
|
if fallback_edit:
|
||||||
|
content = db.session.get(Content, fallback_edit.content_id)
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
log_action('warning', f'Player {player.name} tried to edit non-existent content: {original_name}')
|
log_action('warning', f'Player {player.name} tried to edit non-existent content: {original_name}')
|
||||||
return jsonify({'error': f'Original content not found: {original_name}'}), 404
|
return jsonify({'error': f'Original content not found: {original_name}'}), 404
|
||||||
|
|
||||||
# Create versioned folder structure: edited_media/<content_id>/
|
# ── Versionized folder ───────────────────────────────────────────
|
||||||
base_upload_dir = os.path.join(current_app.root_path, 'static', 'uploads')
|
base_upload_dir = os.path.join(current_app.root_path, 'static', 'uploads')
|
||||||
edited_media_dir = os.path.join(base_upload_dir, 'edited_media', str(content.id))
|
edited_media_dir = os.path.join(base_upload_dir, 'edited_media', str(content.id))
|
||||||
os.makedirs(edited_media_dir, exist_ok=True)
|
os.makedirs(edited_media_dir, exist_ok=True)
|
||||||
|
|
||||||
# Save the edited file with version suffix
|
# On the very first edit (v1) move the original file into the
|
||||||
version = metadata['version']
|
# versionized folder so it is never orphaned.
|
||||||
new_filename = metadata['new_name']
|
is_first_edit = PlayerEdit.query.filter_by(content_id=content.id).count() == 0
|
||||||
|
if is_first_edit:
|
||||||
|
orig_upload = os.path.join(base_upload_dir, content.filename)
|
||||||
|
# content.filename might already be an edited_media/ path if
|
||||||
|
# this is a re-process; only move if it's a plain filename.
|
||||||
|
if os.path.isfile(orig_upload) and not content.filename.startswith('edited_media/'):
|
||||||
|
orig_stored = f"original_{content.filename}"
|
||||||
|
shutil.move(orig_upload, os.path.join(edited_media_dir, orig_stored))
|
||||||
|
log_action('info', f'Moved original file "{content.filename}" to versionized folder as "{orig_stored}"')
|
||||||
|
|
||||||
|
# ── Save the edited file ─────────────────────────────────────────
|
||||||
edited_file_path = os.path.join(edited_media_dir, new_filename)
|
edited_file_path = os.path.join(edited_media_dir, new_filename)
|
||||||
file.save(edited_file_path)
|
file.save(edited_file_path)
|
||||||
|
|
||||||
# Save metadata JSON file
|
# Side-car metadata JSON
|
||||||
metadata_filename = f"{os.path.splitext(new_filename)[0]}_metadata.json"
|
metadata_filename = f"{os.path.splitext(new_filename)[0]}_metadata.json"
|
||||||
metadata_path = os.path.join(edited_media_dir, metadata_filename)
|
metadata_path = os.path.join(edited_media_dir, metadata_filename)
|
||||||
with open(metadata_path, 'w') as f:
|
with open(metadata_path, 'w') as f:
|
||||||
json.dump(metadata, f, indent=2)
|
json.dump(metadata, f, indent=2)
|
||||||
|
|
||||||
# Update the content record to reference the edited version path
|
# ── Point Content.filename to the latest edit ────────────────────
|
||||||
# Keep original filename unchanged, point to edited_media folder
|
# This tells the player to download the latest edited version.
|
||||||
old_filename = content.filename
|
old_filename = content.filename
|
||||||
content.filename = f"edited_media/{content.id}/{new_filename}"
|
content.filename = f"edited_media/{content.id}/{new_filename}"
|
||||||
|
|
||||||
# Create edit record
|
# ── Create edit record ───────────────────────────────────────────
|
||||||
time_of_mod = None
|
time_of_mod = None
|
||||||
if metadata.get('time_of_modification'):
|
if metadata.get('time_of_modification'):
|
||||||
try:
|
try:
|
||||||
time_of_mod = datetime.fromisoformat(metadata['time_of_modification'].replace('Z', '+00:00'))
|
time_of_mod = datetime.fromisoformat(metadata['time_of_modification'].replace('Z', '+00:00'))
|
||||||
except:
|
except:
|
||||||
time_of_mod = datetime.utcnow()
|
time_of_mod = datetime.utcnow()
|
||||||
|
|
||||||
# Auto-create PlayerUser record if user code is provided
|
# Auto-create PlayerUser record if user code is provided
|
||||||
user_code = metadata.get('user_card_data')
|
user_code = metadata.get('user_card_data')
|
||||||
log_action('debug', f'Metadata user code: {user_code}')
|
log_action('debug', f'Metadata user code: {user_code}')
|
||||||
@@ -817,7 +847,7 @@ def receive_edited_media():
|
|||||||
log_action('debug', f'PlayerUser already exists for code: {user_code}')
|
log_action('debug', f'PlayerUser already exists for code: {user_code}')
|
||||||
else:
|
else:
|
||||||
log_action('debug', 'No user code in metadata')
|
log_action('debug', 'No user code in metadata')
|
||||||
|
|
||||||
edit_record = PlayerEdit(
|
edit_record = PlayerEdit(
|
||||||
player_id=player.id,
|
player_id=player.id,
|
||||||
content_id=content.id,
|
content_id=content.id,
|
||||||
@@ -830,22 +860,22 @@ def receive_edited_media():
|
|||||||
edited_file_path=edited_file_path
|
edited_file_path=edited_file_path
|
||||||
)
|
)
|
||||||
db.session.add(edit_record)
|
db.session.add(edit_record)
|
||||||
|
|
||||||
# Update playlist version to force player refresh
|
# ── Update playlist version to force player refresh ──────────────
|
||||||
playlist = None
|
playlist = None
|
||||||
if player.playlist_id:
|
if player.playlist_id:
|
||||||
from app.models.playlist import Playlist
|
from app.models.playlist import Playlist
|
||||||
playlist = db.session.get(Playlist, player.playlist_id)
|
playlist = db.session.get(Playlist, player.playlist_id)
|
||||||
if playlist:
|
if playlist:
|
||||||
playlist.version += 1
|
playlist.version += 1
|
||||||
|
|
||||||
# Clear playlist cache
|
# Clear playlist cache
|
||||||
cache.delete_memoized(get_cached_playlist, player.id)
|
cache.delete_memoized(get_cached_playlist, player.id)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
log_action('info', f'Player {player.name} uploaded edited media: {old_filename} -> {new_filename} (v{version})')
|
log_action('info', f'Player {player.name} uploaded edited media: {old_filename} -> {new_filename} (v{version})')
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': 'Edited media received and processed',
|
'message': 'Edited media received and processed',
|
||||||
|
|||||||
+102
-13
@@ -28,10 +28,23 @@ def list():
|
|||||||
status_info = get_player_status_info(player.id)
|
status_info = get_player_status_info(player.id)
|
||||||
player_statuses[player.id] = status_info
|
player_statuses[player.id] = status_info
|
||||||
|
|
||||||
|
# Build a JSON-safe dict of deployment statuses for the polling JS
|
||||||
|
import json
|
||||||
|
from datetime import datetime as dt
|
||||||
|
player_statuses_json = {}
|
||||||
|
for player in players:
|
||||||
|
player_statuses_json[str(player.id)] = {
|
||||||
|
'deployment_status': player.deployment_status,
|
||||||
|
'last_deployment_status': player.last_deployment_status,
|
||||||
|
'last_deployment_at': player.last_deployment_at.isoformat() if player.last_deployment_at else None,
|
||||||
|
'last_deployment_message': player.last_deployment_message,
|
||||||
|
}
|
||||||
|
|
||||||
return render_template('players/players_list.html',
|
return render_template('players/players_list.html',
|
||||||
players=players,
|
players=players,
|
||||||
playlists=playlists,
|
playlists=playlists,
|
||||||
player_statuses=player_statuses)
|
player_statuses=player_statuses,
|
||||||
|
player_statuses_json=json.dumps(player_statuses_json))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_action('error', f'Error loading players list: {str(e)}')
|
log_action('error', f'Error loading players list: {str(e)}')
|
||||||
flash('Error loading players list.', 'danger')
|
flash('Error loading players list.', 'danger')
|
||||||
@@ -148,6 +161,13 @@ def add_player():
|
|||||||
host = f"{detected_ip}:{port_part}" if port_part else detected_ip
|
host = f"{detected_ip}:{port_part}" if port_part else detected_ip
|
||||||
server_url = f"{flask_request.scheme}://{host}"
|
server_url = f"{flask_request.scheme}://{host}"
|
||||||
|
|
||||||
|
# Mark deployment as "in progress" immediately so the UI polling picks it up
|
||||||
|
from datetime import datetime
|
||||||
|
new_player.deployment_status = 'deploying'
|
||||||
|
new_player.last_deployment_at = datetime.utcnow()
|
||||||
|
new_player.last_deployment_message = 'Deployment in progress...'
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
# Generate API key for player authentication
|
# Generate API key for player authentication
|
||||||
import hashlib
|
import hashlib
|
||||||
api_key = hashlib.sha256(f'{name}:{hostname}'.encode()).hexdigest()[:32]
|
api_key = hashlib.sha256(f'{name}:{hostname}'.encode()).hexdigest()[:32]
|
||||||
@@ -173,19 +193,30 @@ def add_player():
|
|||||||
log_action('error', f'Failed to initiate background deployment for player "{name}": {str(deploy_err)}')
|
log_action('error', f'Failed to initiate background deployment for player "{name}": {str(deploy_err)}')
|
||||||
|
|
||||||
# Flash detailed success message
|
# Flash detailed success message
|
||||||
success_msg = f'''
|
|
||||||
Player "{name}" created successfully!<br>
|
|
||||||
<strong>Auth Code:</strong> <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px;">{auth_code}</code><br>
|
|
||||||
<strong>Hostname:</strong> {hostname}<br>
|
|
||||||
<strong>Quick Connect:</strong> {quickconnect_code}<br>
|
|
||||||
'''
|
|
||||||
|
|
||||||
if deployment_initiated:
|
if deployment_initiated:
|
||||||
success_msg += f'<strong style="color: #0275d8;">⌛ Deployment in Progress</strong> Deploying to {ssh_hostname} in background...<br>'
|
success_msg = f'''
|
||||||
success_msg += '<small>Check player status to see deployment completion</small><br>'
|
Player "{name}" created successfully!<br>
|
||||||
|
<strong>Auth Code:</strong> <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px;">{auth_code}</code><br>
|
||||||
success_msg += '<small>Configure the player with these credentials in app_config.json</small>'
|
<strong>Hostname:</strong> {hostname}<br>
|
||||||
flash(success_msg, 'success')
|
<strong>Quick Connect:</strong> {quickconnect_code}<br>
|
||||||
|
<br>
|
||||||
|
<strong style="color: #0275d8;">⌛ Deployment in Progress</strong><br>
|
||||||
|
Deploying to <strong>{ssh_hostname}</strong> in background...<br>
|
||||||
|
<small>The deployment status will update automatically on the players list.</small>
|
||||||
|
'''
|
||||||
|
flash(success_msg, 'success')
|
||||||
|
else:
|
||||||
|
success_msg = f'''
|
||||||
|
Player "{name}" created successfully!<br>
|
||||||
|
<strong>Auth Code:</strong> <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px;">{auth_code}</code><br>
|
||||||
|
<strong>Hostname:</strong> {hostname}<br>
|
||||||
|
<strong>Quick Connect:</strong> {quickconnect_code}<br>
|
||||||
|
<br>
|
||||||
|
<small>Configure the player with these credentials in app_config.json</small>
|
||||||
|
'''
|
||||||
|
flash(success_msg, 'success')
|
||||||
|
if deploy_player:
|
||||||
|
flash('Player was created but deployment could not be started. You can deploy manually from the Manage page.', 'warning')
|
||||||
|
|
||||||
return redirect(url_for('players.list'))
|
return redirect(url_for('players.list'))
|
||||||
|
|
||||||
@@ -453,6 +484,40 @@ def edited_media(player_id: int):
|
|||||||
return redirect(url_for('players.manage_player', player_id=player_id))
|
return redirect(url_for('players.manage_player', player_id=player_id))
|
||||||
|
|
||||||
|
|
||||||
|
@players_bp.route('/<int:player_id>/edited-media-report')
|
||||||
|
@login_required
|
||||||
|
def edited_media_report(player_id: int):
|
||||||
|
"""Display a tabular report of all edited media from this player."""
|
||||||
|
try:
|
||||||
|
player = Player.query.get_or_404(player_id)
|
||||||
|
|
||||||
|
from app.models.player_edit import PlayerEdit
|
||||||
|
from app.models.player_user import PlayerUser
|
||||||
|
|
||||||
|
edited_media = PlayerEdit.query.filter_by(player_id=player_id)\
|
||||||
|
.order_by(PlayerEdit.created_at.desc())\
|
||||||
|
.all()
|
||||||
|
|
||||||
|
# Build user display name mapping
|
||||||
|
user_mappings = {}
|
||||||
|
for edit in edited_media:
|
||||||
|
if edit.user and edit.user not in user_mappings:
|
||||||
|
player_user = PlayerUser.query.filter_by(user_code=edit.user).first()
|
||||||
|
if player_user and player_user.user_name:
|
||||||
|
user_mappings[edit.user] = player_user.user_name
|
||||||
|
else:
|
||||||
|
user_mappings[edit.user] = edit.user
|
||||||
|
|
||||||
|
return render_template('players/edited_media_report.html',
|
||||||
|
player=player,
|
||||||
|
edited_media=edited_media,
|
||||||
|
user_mappings=user_mappings)
|
||||||
|
except Exception as e:
|
||||||
|
log_action('error', f'Error loading edited media report for player {player_id}: {str(e)}')
|
||||||
|
flash('Error loading edited media report.', 'danger')
|
||||||
|
return redirect(url_for('players.manage_player', player_id=player_id))
|
||||||
|
|
||||||
|
|
||||||
@players_bp.route('/<int:player_id>/fullscreen')
|
@players_bp.route('/<int:player_id>/fullscreen')
|
||||||
def player_fullscreen(player_id: int):
|
def player_fullscreen(player_id: int):
|
||||||
"""Display player fullscreen view (no authentication required for players)."""
|
"""Display player fullscreen view (no authentication required for players)."""
|
||||||
@@ -598,6 +663,30 @@ def bulk_assign_playlist():
|
|||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@players_bp.route('/deployment-status')
|
||||||
|
@login_required
|
||||||
|
def deployment_status():
|
||||||
|
"""Return deployment status for all players (used by polling JS)."""
|
||||||
|
try:
|
||||||
|
players = Player.query.with_entities(
|
||||||
|
Player.id, Player.deployment_status,
|
||||||
|
Player.last_deployment_status, Player.last_deployment_at,
|
||||||
|
Player.last_deployment_message
|
||||||
|
).all()
|
||||||
|
data = {}
|
||||||
|
for p in players:
|
||||||
|
data[p.id] = {
|
||||||
|
'deployment_status': p.deployment_status,
|
||||||
|
'last_deployment_status': p.last_deployment_status,
|
||||||
|
'last_deployment_at': p.last_deployment_at.isoformat() if p.last_deployment_at else None,
|
||||||
|
'last_deployment_message': p.last_deployment_message,
|
||||||
|
}
|
||||||
|
return jsonify(data)
|
||||||
|
except Exception as e:
|
||||||
|
log_action('error', f'Error fetching deployment status: {str(e)}')
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@players_bp.route('/<int:player_id>/playlist/reorder', methods=['POST'])
|
@players_bp.route('/<int:player_id>/playlist/reorder', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def reorder_playlist(player_id: int):
|
def reorder_playlist(player_id: int):
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -646,13 +646,22 @@ document.addEventListener('keydown', function(event) {
|
|||||||
Edited Media on the Player
|
Edited Media on the Player
|
||||||
</h2>
|
</h2>
|
||||||
{% if edited_media %}
|
{% if edited_media %}
|
||||||
<a href="{{ url_for('players.edited_media', player_id=player.id) }}"
|
<div style="display: flex; gap: 0.5rem;">
|
||||||
class="btn"
|
<a href="{{ url_for('players.edited_media', player_id=player.id) }}"
|
||||||
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;"
|
class="btn"
|
||||||
onmouseover="this.style.background='#6d28d9'"
|
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;"
|
||||||
onmouseout="this.style.background='#7c3aed'">
|
onmouseover="this.style.background='#6d28d9'"
|
||||||
📋 View All Edited Media
|
onmouseout="this.style.background='#7c3aed'">
|
||||||
</a>
|
📋 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<p style="color: #6c757d; font-size: 0.9rem; margin-top: 0.5rem;">Latest 3 edited files with their most recent versions</p>
|
<p style="color: #6c757d; font-size: 0.9rem; margin-top: 0.5rem;">Latest 3 edited files with their most recent versions</p>
|
||||||
|
|||||||
@@ -125,6 +125,82 @@
|
|||||||
body.dark-mode .info-box a {
|
body.dark-mode .info-box a {
|
||||||
color: #90cdf4;
|
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>
|
</style>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||||
@@ -142,13 +218,14 @@
|
|||||||
<th>Location</th>
|
<th>Location</th>
|
||||||
<th>Orientation</th>
|
<th>Orientation</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Deployment</th>
|
||||||
<th>Last Seen</th>
|
<th>Last Seen</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for player in players %}
|
{% for player in players %}
|
||||||
<tr>
|
<tr id="player-row-{{ player.id }}">
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ player.name }}</strong>
|
<strong>{{ player.name }}</strong>
|
||||||
</td>
|
</td>
|
||||||
@@ -168,6 +245,9 @@
|
|||||||
<span class="status-badge offline">Offline</span>
|
<span class="status-badge offline">Offline</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td id="deploy-cell-{{ player.id }}">
|
||||||
|
{% include "players/_deploy_badge.html" %}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if player.last_seen %}
|
{% if player.last_seen %}
|
||||||
{{ player.last_seen | localtime }}
|
{{ player.last_seen | localtime }}
|
||||||
@@ -192,4 +272,78 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -62,6 +62,17 @@ def background_player_deployment(
|
|||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.utils.logger import log_action
|
from app.utils.logger import log_action
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Mark deployment as "in progress" immediately so the UI can show live status
|
||||||
|
player = Player.query.get(player_id)
|
||||||
|
if player:
|
||||||
|
player.deployment_status = 'deploying'
|
||||||
|
player.last_deployment_at = datetime.utcnow()
|
||||||
|
player.last_deployment_status = None
|
||||||
|
player.last_deployment_message = 'Deployment in progress...'
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Execute deployment
|
# Execute deployment
|
||||||
result = deploy_player_to_host(
|
result = deploy_player_to_host(
|
||||||
@@ -79,10 +90,8 @@ def background_player_deployment(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Update player with deployment status
|
# Update player with deployment status
|
||||||
from datetime import datetime
|
|
||||||
player = Player.query.get(player_id)
|
player = Player.query.get(player_id)
|
||||||
if player:
|
if player:
|
||||||
player.last_deployment_at = datetime.utcnow()
|
|
||||||
if result.get('success'):
|
if result.get('success'):
|
||||||
player.deployment_status = 'deployed'
|
player.deployment_status = 'deployed'
|
||||||
player.last_deployment_status = 'success'
|
player.last_deployment_status = 'success'
|
||||||
@@ -98,4 +107,10 @@ def background_player_deployment(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Background deployment error for player '{player_name}': {str(e)}", exc_info=True)
|
logger.error(f"Background deployment error for player '{player_name}': {str(e)}", exc_info=True)
|
||||||
|
player = Player.query.get(player_id)
|
||||||
|
if player:
|
||||||
|
player.deployment_status = 'failed'
|
||||||
|
player.last_deployment_status = 'failed'
|
||||||
|
player.last_deployment_message = f'Deployment crashed: {str(e)}'
|
||||||
|
db.session.commit()
|
||||||
log_action('error', f'Background deployment error for player "{player_name}": {str(e)}')
|
log_action('error', f'Background deployment error for player "{player_name}": {str(e)}')
|
||||||
|
|||||||
Reference in New Issue
Block a user