639 lines
25 KiB
HTML
639 lines
25 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Manage Player - {{ player.name }}{% endblock %}
|
||
|
||
{% block content %}
|
||
<style>
|
||
.form-group {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 0.5rem;
|
||
font-weight: bold;
|
||
}
|
||
|
||
body.dark-mode .form-group label {
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
.form-control {
|
||
width: 100%;
|
||
padding: 0.5rem;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
body.dark-mode .form-control {
|
||
background: #1a202c;
|
||
border-color: #4a5568;
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
body.dark-mode .form-control:focus {
|
||
border-color: #7c3aed;
|
||
outline: none;
|
||
}
|
||
|
||
.info-box {
|
||
padding: 1rem;
|
||
border-radius: 4px;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.info-box.neutral {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
body.dark-mode .info-box.neutral {
|
||
background: #1a202c;
|
||
border: 1px solid #4a5568;
|
||
}
|
||
|
||
.info-box.success {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
body.dark-mode .info-box.success {
|
||
background: #1a4d2e;
|
||
color: #86efac;
|
||
border: 1px solid #48bb78;
|
||
}
|
||
|
||
.info-box.warning {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
|
||
body.dark-mode .info-box.warning {
|
||
background: #4a3800;
|
||
color: #fbbf24;
|
||
border: 1px solid #ecc94b;
|
||
}
|
||
|
||
.log-item {
|
||
padding: 0.75rem;
|
||
margin-bottom: 0.5rem;
|
||
border-radius: 4px;
|
||
background: #f8f9fa;
|
||
border-left: 4px solid;
|
||
}
|
||
|
||
body.dark-mode .log-item {
|
||
background: #2d3748;
|
||
}
|
||
|
||
.log-item pre {
|
||
background: #fff;
|
||
border: 1px solid #ddd;
|
||
}
|
||
|
||
body.dark-mode .log-item pre {
|
||
background: #1a202c;
|
||
border-color: #4a5568;
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
body.dark-mode h1,
|
||
body.dark-mode h2,
|
||
body.dark-mode h3,
|
||
body.dark-mode h4 {
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
body.dark-mode p {
|
||
color: #a0aec0;
|
||
}
|
||
|
||
body.dark-mode strong {
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
body.dark-mode code {
|
||
background: #1a202c;
|
||
color: #e2e8f0;
|
||
padding: 2px 6px;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
body.dark-mode small {
|
||
color: #718096;
|
||
}
|
||
|
||
.credential-item {
|
||
margin-bottom: 0.75rem;
|
||
padding-bottom: 0.75rem;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
body.dark-mode .credential-item {
|
||
border-bottom-color: #4a5568;
|
||
}
|
||
|
||
.credential-item:last-child {
|
||
margin-bottom: 0;
|
||
padding-bottom: 0;
|
||
border-bottom: none;
|
||
}
|
||
|
||
.credential-label {
|
||
font-weight: 600;
|
||
display: block;
|
||
margin-bottom: 0.25rem;
|
||
font-size: 0.85rem;
|
||
color: #495057;
|
||
}
|
||
|
||
body.dark-mode .credential-label {
|
||
color: #a0aec0;
|
||
}
|
||
|
||
.credential-value {
|
||
font-family: 'Courier New', monospace;
|
||
background: #f8f9fa;
|
||
padding: 0.5rem;
|
||
border-radius: 4px;
|
||
word-break: break-all;
|
||
font-size: 0.85rem;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
body.dark-mode .credential-value {
|
||
background: #0d1117;
|
||
border-color: #4a5568;
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
.status-card {
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.status-card.online {
|
||
background: #d4edda;
|
||
}
|
||
|
||
body.dark-mode .status-card.online {
|
||
background: #1a4d2e;
|
||
border: 1px solid #48bb78;
|
||
}
|
||
|
||
.status-card.offline {
|
||
background: #f8d7da;
|
||
}
|
||
|
||
body.dark-mode .status-card.offline {
|
||
background: #4a1a1a;
|
||
border: 1px solid #dc3545;
|
||
}
|
||
|
||
.status-card.other {
|
||
background: #fff3cd;
|
||
}
|
||
|
||
body.dark-mode .status-card.other {
|
||
background: #4a3800;
|
||
border: 1px solid #ecc94b;
|
||
}
|
||
|
||
.playlist-stats {
|
||
padding: 1rem;
|
||
background: #f8f9fa;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
body.dark-mode .playlist-stats {
|
||
background: #1a202c;
|
||
border: 1px solid #4a5568;
|
||
}
|
||
|
||
body.dark-mode .playlist-stats > div > div:first-child {
|
||
color: #a0aec0;
|
||
}
|
||
|
||
/* Player Logs Dark Mode */
|
||
body.dark-mode .card p[style*="color: #6c757d"] {
|
||
color: #a0aec0 !important;
|
||
}
|
||
|
||
body.dark-mode .card > div[style*="max-height: 500px"] > div[style*="padding: 0.75rem"] {
|
||
background: #2d3748 !important;
|
||
}
|
||
|
||
body.dark-mode .card > div[style*="max-height: 500px"] > div[style*="padding: 0.75rem"] p {
|
||
color: #e2e8f0 !important;
|
||
}
|
||
|
||
body.dark-mode .card > div[style*="max-height: 500px"] > div[style*="padding: 0.75rem"] p[style*="color: #6c757d"] {
|
||
color: #a0aec0 !important;
|
||
}
|
||
|
||
body.dark-mode .card > div[style*="max-height: 500px"] > div[style*="padding: 0.75rem"] small {
|
||
color: #a0aec0 !important;
|
||
}
|
||
|
||
body.dark-mode .card > div[style*="max-height: 500px"] > div[style*="padding: 0.75rem"] details summary {
|
||
color: #f87171 !important;
|
||
}
|
||
|
||
body.dark-mode .card > div[style*="max-height: 500px"] > div[style*="padding: 0.75rem"] pre {
|
||
background: #1a202c !important;
|
||
border-color: #4a5568 !important;
|
||
color: #e2e8f0 !important;
|
||
}
|
||
|
||
body.dark-mode .card > div[style*="text-align: center"] {
|
||
color: #a0aec0 !important;
|
||
}
|
||
|
||
body.dark-mode .card > div[style*="text-align: center"] p {
|
||
color: #a0aec0 !important;
|
||
}
|
||
</style>
|
||
<div style="margin-bottom: 2rem; display: flex; justify-content: space-between; align-items: flex-start;">
|
||
<h1 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
|
||
<img src="{{ url_for('static', filename='icons/monitor.svg') }}" alt="" style="width: 32px; height: 32px;">
|
||
Manage Player: {{ player.name }}
|
||
</h1>
|
||
<a href="{{ url_for('players.list') }}" class="btn" style="display: inline-flex; align-items: center; gap: 0.5rem;">
|
||
<img src="{{ url_for('static', filename='icons/monitor.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||
Back to Players
|
||
</a>
|
||
</div>
|
||
|
||
<!-- Delete Confirmation Modal -->
|
||
<div id="deleteModal" style="display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4);">
|
||
<div style="background-color: #fefefe; margin: 15% auto; padding: 20px; border: 1px solid #888; border-radius: 8px; width: 90%; max-width: 500px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
||
<h2 style="margin-top: 0; color: #dc3545; display: flex; align-items: center; gap: 0.5rem;">
|
||
<span style="font-size: 1.5rem;">⚠️</span>
|
||
Confirm Delete
|
||
</h2>
|
||
<p style="font-size: 1.1rem; margin: 1.5rem 0;">
|
||
Are you sure you want to delete player <strong>"{{ player.name }}"</strong>?
|
||
</p>
|
||
<p style="color: #dc3545; margin: 1rem 0;">
|
||
<strong>Warning:</strong> This action cannot be undone. All feedback logs for this player will also be deleted.
|
||
</p>
|
||
<div style="display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 2rem;">
|
||
<button onclick="closeDeleteModal()" class="btn" style="background: #6c757d;">
|
||
Cancel
|
||
</button>
|
||
<form method="POST" action="{{ url_for('players.delete_player', player_id=player.id) }}" style="margin: 0;">
|
||
<button type="submit" class="btn" style="background: #dc3545;">
|
||
Yes, Delete Player
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
body.dark-mode #deleteModal > div {
|
||
background-color: #1a202c;
|
||
border-color: #4a5568;
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
body.dark-mode #deleteModal h2 {
|
||
color: #f87171;
|
||
}
|
||
|
||
body.dark-mode #deleteModal p {
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
body.dark-mode #deleteModal p strong {
|
||
color: #fbbf24;
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
function confirmDelete() {
|
||
document.getElementById('deleteModal').style.display = 'block';
|
||
}
|
||
|
||
function closeDeleteModal() {
|
||
document.getElementById('deleteModal').style.display = 'none';
|
||
}
|
||
|
||
// Close modal when clicking outside of it
|
||
window.onclick = function(event) {
|
||
const modal = document.getElementById('deleteModal');
|
||
if (event.target == modal) {
|
||
closeDeleteModal();
|
||
}
|
||
}
|
||
|
||
// Close modal with ESC key
|
||
document.addEventListener('keydown', function(event) {
|
||
if (event.key === 'Escape') {
|
||
closeDeleteModal();
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<!-- Player Status Overview -->
|
||
<div class="card status-card {% if player.status == 'online' %}online{% elif player.status == 'offline' %}offline{% else %}other{% endif %}">
|
||
<h3 style="display: flex; align-items: center; gap: 0.5rem;">
|
||
Status:
|
||
{% if player.status == 'online' %}
|
||
<span style="color: #28a745; display: flex; align-items: center; gap: 0.3rem;">
|
||
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="" style="width: 20px; height: 20px; color: #28a745;">
|
||
Online
|
||
</span>
|
||
{% elif player.status == 'offline' %}
|
||
<span style="color: #dc3545; display: flex; align-items: center; gap: 0.3rem;">
|
||
<img src="{{ url_for('static', filename='icons/warning.svg') }}" alt="" style="width: 20px; height: 20px; color: #dc3545;">
|
||
Offline
|
||
</span>
|
||
{% else %}
|
||
<span style="color: #ffc107; display: flex; align-items: center; gap: 0.3rem;">
|
||
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="" style="width: 20px; height: 20px; color: #ffc107;">
|
||
{{ player.status|title }}
|
||
</span>
|
||
{% endif %}
|
||
</h3>
|
||
<p><strong>Hostname:</strong> {{ player.hostname }}</p>
|
||
<p><strong>Last Seen:</strong>
|
||
{% if player.last_seen %}
|
||
{{ player.last_seen | localtime('%Y-%m-%d %H:%M:%S') }}
|
||
{% else %}
|
||
Never
|
||
{% endif %}
|
||
</p>
|
||
<p><strong>Assigned Playlist:</strong>
|
||
{% if current_playlist %}
|
||
<span style="color: #28a745; font-weight: bold;">{{ current_playlist.name }} (v{{ current_playlist.version }})</span>
|
||
{% else %}
|
||
<span style="color: #dc3545;">No playlist assigned</span>
|
||
{% endif %}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Playlist Overview Card -->
|
||
{% if current_playlist %}
|
||
<div class="card" style="margin-bottom: 2rem;">
|
||
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
|
||
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="" style="width: 24px; height: 24px;">
|
||
Current Playlist: {{ current_playlist.name }}
|
||
</h2>
|
||
<div class="playlist-stats" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-top: 1rem;">
|
||
<div>
|
||
<div style="font-size: 0.85rem; color: #6c757d; margin-bottom: 0.25rem;">Total Items</div>
|
||
<div style="font-size: 1.5rem; font-weight: bold;">{{ current_playlist.contents.count() }}</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size: 0.85rem; color: #6c757d; margin-bottom: 0.25rem;">Playlist Version</div>
|
||
<div style="font-size: 1.5rem; font-weight: bold;">v{{ current_playlist.version }}</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size: 0.85rem; color: #6c757d; margin-bottom: 0.25rem;">Last Updated</div>
|
||
<div style="font-size: 1rem; font-weight: bold;">{{ current_playlist.updated_at | localtime }}</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size: 0.85rem; color: #6c757d; margin-bottom: 0.25rem;">Orientation</div>
|
||
<div style="font-size: 1.5rem; font-weight: bold;">{{ current_playlist.orientation }}</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: 1rem; display: flex; gap: 0.5rem;">
|
||
<a href="{{ url_for('content.manage_playlist_content', playlist_id=current_playlist.id) }}"
|
||
class="btn btn-primary" style="display: inline-flex; align-items: center; gap: 0.5rem;">
|
||
<img src="{{ url_for('static', filename='icons/edit.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||
Edit Playlist Content
|
||
</a>
|
||
<a href="{{ url_for('players.player_fullscreen', player_id=player.id) }}"
|
||
class="btn btn-success" style="display: inline-flex; align-items: center; gap: 0.5rem;"
|
||
target="_blank">
|
||
<img src="{{ url_for('static', filename='icons/monitor.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||
View Live Content
|
||
</a>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Three Column Layout -->
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 1.5rem;">
|
||
|
||
<!-- Card 1: Edit Credentials -->
|
||
<div class="card">
|
||
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
|
||
<img src="{{ url_for('static', filename='icons/edit.svg') }}" alt="" style="width: 24px; height: 24px;">
|
||
Edit Credentials
|
||
</h2>
|
||
<form method="POST" style="margin-top: 1rem;">
|
||
<input type="hidden" name="action" value="update_credentials">
|
||
|
||
<div class="form-group">
|
||
<label for="name">Player Name *</label>
|
||
<input type="text" id="name" name="name" value="{{ player.name }}"
|
||
required minlength="3" class="form-control">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="location">Location</label>
|
||
<input type="text" id="location" name="location" value="{{ player.location or '' }}"
|
||
placeholder="e.g., Main Lobby" class="form-control">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="orientation">Orientation</label>
|
||
<select id="orientation" name="orientation" class="form-control">
|
||
<option value="Landscape" {% if player.orientation == 'Landscape' %}selected{% endif %}>Landscape</option>
|
||
<option value="Portrait" {% if player.orientation == 'Portrait' %}selected{% endif %}>Portrait</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="border-top: 1px solid #ddd; margin: 1.5rem 0; padding-top: 1.5rem;">
|
||
<h4 style="margin: 0 0 1rem 0; font-size: 0.95rem;">🔑 Authentication Settings</h4>
|
||
|
||
<div class="form-group">
|
||
<label for="hostname">Hostname *</label>
|
||
<input type="text" id="hostname" name="hostname" value="{{ player.hostname }}"
|
||
required minlength="3" class="form-control"
|
||
placeholder="e.g., tv-terasa">
|
||
<small style="display: block; margin-top: 0.25rem; color: #6c757d;">
|
||
ℹ️ This is the unique identifier for the player
|
||
</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="password">New Password (leave blank to keep current)</label>
|
||
<input type="password" id="password" name="password" class="form-control"
|
||
placeholder="Enter new password">
|
||
<small style="display: block; margin-top: 0.25rem; color: #6c757d;">
|
||
🔒 Optional: Set a new password for player authentication
|
||
</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="quickconnect_code">Quick Connect Code</label>
|
||
<input type="text" id="quickconnect_code" name="quickconnect_code" class="form-control"
|
||
placeholder="e.g., 8887779">
|
||
<small style="display: block; margin-top: 0.25rem; color: #6c757d;">
|
||
🔗 Enter the plain text code (e.g., 8887779) - will be hashed automatically
|
||
</small>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-success" style="width: 100%; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
|
||
<img src="{{ url_for('static', filename='icons/edit.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||
Save Changes
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Card 2: Assign Playlist -->
|
||
<div class="card">
|
||
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
|
||
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="" style="width: 24px; height: 24px;">
|
||
Assign Playlist
|
||
</h2>
|
||
<form method="POST" style="margin-top: 1rem;">
|
||
<input type="hidden" name="action" value="assign_playlist">
|
||
|
||
<div class="form-group">
|
||
<label for="playlist_id">Select Playlist</label>
|
||
<select id="playlist_id" name="playlist_id" class="form-control">
|
||
<option value="">-- No Playlist (Unassign) --</option>
|
||
{% for playlist in playlists %}
|
||
<option value="{{ playlist.id }}"
|
||
{% if player.playlist_id == playlist.id %}selected{% endif %}>
|
||
{{ playlist.name }} (v{{ playlist.version }}) - {{ playlist.contents.count() }} items
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
{% if current_playlist %}
|
||
<div class="info-box success">
|
||
<h4 style="margin: 0 0 0.5rem 0;">Currently Assigned:</h4>
|
||
<p style="margin: 0;"><strong>{{ current_playlist.name }}</strong></p>
|
||
<p style="margin: 0.25rem 0 0 0; font-size: 0.9rem;">Version: {{ current_playlist.version }}</p>
|
||
<p style="margin: 0.25rem 0 0 0; font-size: 0.9rem;">Content Items: {{ current_playlist.contents.count() }}</p>
|
||
<p style="margin: 0.25rem 0 0 0; font-size: 0.9rem; color: #6c757d;">
|
||
Updated: {{ current_playlist.updated_at | localtime }}
|
||
</p>
|
||
</div>
|
||
{% else %}
|
||
<div class="info-box warning">
|
||
<p style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
|
||
<img src="{{ url_for('static', filename='icons/warning.svg') }}" alt="" style="width: 18px; height: 18px;">
|
||
No playlist currently assigned to this player.
|
||
</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<button type="submit" class="btn btn-success" style="width: 100%; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
|
||
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||
Assign Playlist
|
||
</button>
|
||
</form>
|
||
|
||
<div style="margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid #ddd;">
|
||
<h4>Quick Actions:</h4>
|
||
<a href="{{ url_for('content.content_list') }}" class="btn" style="width: 100%; margin-top: 0.5rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
|
||
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||
Create New Playlist
|
||
</a>
|
||
{% if current_playlist %}
|
||
<a href="{{ url_for('content.manage_playlist_content', playlist_id=current_playlist.id) }}"
|
||
class="btn" style="width: 100%; margin-top: 0.5rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
|
||
<img src="{{ url_for('static', filename='icons/edit.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||
Edit Current Playlist
|
||
</a>
|
||
{% endif %}
|
||
<button onclick="confirmDelete()" class="btn" style="width: 100%; margin-top: 0.5rem; background: #dc3545; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
|
||
<span style="font-size: 1.2rem;">🗑️</span>
|
||
Delete Player
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Card 3: Player Logs -->
|
||
<div class="card">
|
||
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
|
||
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="" style="width: 24px; height: 24px;">
|
||
Player Logs
|
||
</h2>
|
||
<p style="color: #6c757d; font-size: 0.9rem;">Recent feedback from the player device</p>
|
||
|
||
<div style="max-height: 500px; overflow-y: auto; margin-top: 1rem;">
|
||
{% if recent_logs %}
|
||
{% for log in recent_logs %}
|
||
<div style="padding: 0.75rem; margin-bottom: 0.5rem; border-left: 4px solid
|
||
{% if log.status == 'error' %}#dc3545
|
||
{% elif log.status == 'warning' %}#ffc107
|
||
{% elif log.status == 'playing' %}#28a745
|
||
{% else %}#17a2b8{% endif %};
|
||
background: #f8f9fa; border-radius: 4px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||
<div style="flex: 1;">
|
||
<strong style="color:
|
||
{% if log.status == 'error' %}#dc3545
|
||
{% elif log.status == 'warning' %}#ffc107
|
||
{% elif log.status == 'playing' %}#28a745
|
||
{% else %}#17a2b8{% endif %};">
|
||
{% if log.status == 'error' %}❌
|
||
{% elif log.status == 'warning' %}⚠️
|
||
{% elif log.status == 'playing' %}▶️
|
||
{% elif log.status == 'restarting' %}🔄
|
||
{% else %}ℹ️{% endif %}
|
||
{{ log.status|upper }}
|
||
</strong>
|
||
<p style="margin: 0.25rem 0 0 0; font-size: 0.9rem;">{{ log.message }}</p>
|
||
{% if log.playlist_version %}
|
||
<p style="margin: 0.25rem 0 0 0; font-size: 0.85rem; color: #6c757d;">
|
||
Playlist v{{ log.playlist_version }}
|
||
</p>
|
||
{% endif %}
|
||
{% if log.error_details %}
|
||
<details style="margin-top: 0.5rem;">
|
||
<summary style="cursor: pointer; font-size: 0.85rem; color: #dc3545;">Error Details</summary>
|
||
<pre style="margin: 0.5rem 0 0 0; padding: 0.5rem; background: #fff; border: 1px solid #ddd; border-radius: 4px; font-size: 0.8rem; overflow-x: auto;">{{ log.error_details }}</pre>
|
||
</details>
|
||
{% endif %}
|
||
</div>
|
||
<small style="color: #6c757d; white-space: nowrap; margin-left: 1rem;">
|
||
{{ log.timestamp | localtime('%m/%d %H:%M') }}
|
||
</small>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% else %}
|
||
<div style="text-align: center; padding: 2rem; color: #6c757d;">
|
||
<p>📭 No logs received yet</p>
|
||
<p style="font-size: 0.9rem;">Logs will appear here once the player starts sending feedback</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Additional Info Section -->
|
||
<div class="card" style="margin-top: 2rem;">
|
||
<h2>ℹ️ Player Information</h2>
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-top: 1rem;">
|
||
<div>
|
||
<p><strong>Player ID:</strong> {{ player.id }}</p>
|
||
<p><strong>Created:</strong> {{ (player.created_at | localtime) if player.created_at else 'N/A' }}</p>
|
||
</div>
|
||
<div>
|
||
<p><strong>Orientation:</strong> {{ player.orientation }}</p>
|
||
<p><strong>Location:</strong> {{ player.location or 'Not set' }}</p>
|
||
</div>
|
||
<div>
|
||
<p><strong>Last Heartbeat:</strong>
|
||
{% if player.last_heartbeat %}
|
||
{{ player.last_heartbeat | localtime('%Y-%m-%d %H:%M:%S') }}
|
||
{% else %}
|
||
Never
|
||
{% endif %}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% endblock %}
|