Files
digiserver-v2/app/templates/players/manage_player.html
2025-11-15 01:26:12 +02:00

513 lines
20 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 %}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;
}
</style>
<div style="margin-bottom: 2rem;">
<h1 style="display: inline-block; margin-right: 1rem; 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="float: right; 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>
<!-- 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.strftime('%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.strftime('%Y-%m-%d %H:%M') }}</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 class="info-box neutral">
<h4 style="margin: 0 0 1rem 0; font-size: 0.95rem; color: #495057;">🔑 Player Credentials</h4>
<div class="credential-item">
<span class="credential-label">Hostname</span>
<div class="credential-value">{{ player.hostname }}</div>
</div>
<div class="credential-item">
<span class="credential-label">Auth Code</span>
<div class="credential-value">{{ player.auth_code }}</div>
</div>
<div class="credential-item">
<span class="credential-label">Quick Connect Code (Hashed)</span>
<div class="credential-value" style="font-size: 0.75rem;">{{ player.quickconnect_code or 'Not set' }}</div>
<small style="display: block; margin-top: 0.25rem; color: #6c757d;">⚠️ This is the hashed version for security</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.strftime('%Y-%m-%d %H:%M') }}
</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 %}
</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.strftime('%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.strftime('%Y-%m-%d %H:%M') 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.strftime('%Y-%m-%d %H:%M:%S') }}
{% else %}
Never
{% endif %}
</p>
</div>
</div>
</div>
{% endblock %}