288 lines
12 KiB
Plaintext
288 lines
12 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<title data-text="title">Asteroid Radio - Admin Dashboard</title>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||
<script src="/asteroid/static/js/admin.js"></script>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>🎛️ ADMIN DASHBOARD</h1>
|
||
<div class="nav">
|
||
<a href="/asteroid">Home</a>
|
||
<a href="/asteroid/player">Player</a>
|
||
<a href="/asteroid/profile">Profile</a>
|
||
<a href="/asteroid/admin/users">👥 Users</a>
|
||
<a href="/asteroid/logout" class="btn-logout">Logout</a>
|
||
</div>
|
||
|
||
<!-- System Status -->
|
||
<div class="admin-section">
|
||
<h2>System Status</h2>
|
||
<div class="admin-grid">
|
||
<div class="status-card">
|
||
<h3>Server Status</h3>
|
||
<p class="status-good" data-text="server-status">🟢 Running</p>
|
||
</div>
|
||
<div class="status-card">
|
||
<h3>Database Status</h3>
|
||
<p class="status-good" data-text="database-status">🟢 Connected</p>
|
||
</div>
|
||
<div class="status-card">
|
||
<h3>Liquidsoap Status</h3>
|
||
<p class="status-error" data-text="liquidsoap-status">🔴 Not Running</p>
|
||
</div>
|
||
<div class="status-card">
|
||
<h3>Icecast Status</h3>
|
||
<p class="status-error" data-text="icecast-status">🔴 Not Running</p>
|
||
<button id="icecast-restart" class="btn btn-danger btn-sm" style="margin-top: 8px;">🔄 Restart</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Listener Statistics -->
|
||
<div class="admin-section">
|
||
<h2>📊 Current Listeners</h2>
|
||
<table class="listener-stats-table" style="table-layout: fixed; width: 100%;">
|
||
<colgroup>
|
||
<col style="width: 25%;">
|
||
<col style="width: 25%;">
|
||
<col style="width: 25%;">
|
||
<col style="width: 25%;">
|
||
</colgroup>
|
||
<thead>
|
||
<tr>
|
||
<th>🎵 MP3</th>
|
||
<th>🎧 AAC</th>
|
||
<th>📱 Low</th>
|
||
<th>🎲 Shuffle</th>
|
||
<th>📈 Total</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td style="text-align: center;"><span class="stat-number" id="listeners-mp3">0</span></td>
|
||
<td style="text-align: center;"><span class="stat-number" id="listeners-aac">0</span></td>
|
||
<td style="text-align: center;"><span class="stat-number" id="listeners-low">0</span></td>
|
||
<td style="text-align: center;"><span class="stat-number" id="listeners-shuffle">0</span></td>
|
||
<td style="text-align: center;"><span class="stat-number" id="listeners-total">0</span></td>
|
||
</tr>
|
||
<tr class="stat-peak-row">
|
||
<td style="text-align: center;">Peak: <span id="peak-mp3">0</span></td>
|
||
<td style="text-align: center;">Peak: <span id="peak-aac">0</span></td>
|
||
<td style="text-align: center;">Peak: <span id="peak-low">0</span></td>
|
||
<td style="text-align: center;">Peak: <span id="peak-shuffle">0</span></td>
|
||
<td style="text-align: center;">Updated: <span id="stats-updated">--</span></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="admin-controls" style="margin-top: 10px;">
|
||
<button id="refresh-stats" class="btn btn-secondary" onclick="refreshListenerStats()">🔄 Refresh</button>
|
||
<span id="stats-status" style="margin-left: 15px;"></span>
|
||
</div>
|
||
|
||
<!-- Geo Stats -->
|
||
<h3 style="margin-top: 20px;">🌍 Listener Locations (Last 7 Days)</h3>
|
||
<div id="geo-stats-container">
|
||
<table class="listener-stats-table" id="geo-stats-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Country</th>
|
||
<th>Listeners</th>
|
||
<th>Minutes</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="geo-stats-body">
|
||
<tr><td colspan="3" style="color: #888;">Loading...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Music Library Management -->
|
||
<div class="admin-section">
|
||
<h2>Music Library Management</h2>
|
||
|
||
<!-- Music Library Info -->
|
||
<div class="upload-section">
|
||
<h3>Music Library</h3>
|
||
<div class="upload-info">
|
||
<p>The music library is mounted from your local filesystem into the Liquidsoap container.</p>
|
||
<p><strong>To add music:</strong></p>
|
||
<ol>
|
||
<li>Add files to your music library directory (set via <code>MUSIC_LIBRARY</code> env var)</li>
|
||
<li>Click "Scan Library" to index new tracks into the database</li>
|
||
</ol>
|
||
<p><em>Supported formats: MP3, FLAC, OGG, WAV, OPUS</em></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin-controls">
|
||
<button id="scan-library" class="btn btn-primary">🔍 Scan Library</button>
|
||
<button id="refresh-tracks" class="btn btn-secondary">🔄 Refresh Track List</button>
|
||
<span id="scan-status" style="margin-left: 15px; font-weight: bold;"></span>
|
||
</div>
|
||
|
||
<div class="track-stats">
|
||
<p>Total Tracks: <span id="track-count" data-text="track-count">0</span></p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Track Management -->
|
||
<div class="admin-section">
|
||
<h2>Track Management</h2>
|
||
<div class="track-controls">
|
||
<input type="text" id="track-search" placeholder="Search tracks..." class="search-input">
|
||
<select id="sort-tracks" class="sort-select">
|
||
<option value="title">Sort by Title</option>
|
||
<option value="artist">Sort by Artist</option>
|
||
<option value="album">Sort by Album</option>
|
||
</select>
|
||
<select id="tracks-per-page" class="sort-select" onchange="changeTracksPerPage()">
|
||
<option value="10">10 per page</option>
|
||
<option value="20" selected>20 per page</option>
|
||
<option value="50">50 per page</option>
|
||
<option value="100">100 per page</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="tracks-container" class="tracks-list">
|
||
<div class="loading">Loading tracks...</div>
|
||
</div>
|
||
|
||
<!-- Pagination Controls -->
|
||
<div id="pagination-controls" style="display: none; margin-top: 20px; text-align: center;">
|
||
<button onclick="goToPage(1)" class="btn btn-secondary">« First</button>
|
||
<button onclick="previousPage()" class="btn btn-secondary">‹ Prev</button>
|
||
<span id="page-info" style="margin: 0 15px; font-weight: bold;">Page 1 of 1</span>
|
||
<button onclick="nextPage()" class="btn btn-secondary">Next ›</button>
|
||
<button onclick="goToLastPage()" class="btn btn-secondary">Last »</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stream Queue Management -->
|
||
<div class="admin-section">
|
||
<h2>🎵 Stream Queue Management</h2>
|
||
<p>Manage the live stream playback queue. Liquidsoap watches <code>stream-queue.m3u</code> and reloads automatically.</p>
|
||
|
||
<!-- Playlist Selection -->
|
||
<div class="playlist-controls" style="margin-bottom: 20px; padding: 15px; background: #2a2a2a; border-radius: 8px;">
|
||
<h3 style="margin-top: 0;">📋 Load Playlist</h3>
|
||
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
||
<select id="playlist-select" class="sort-select" style="min-width: 250px;">
|
||
<option value="">-- Select a playlist --</option>
|
||
</select>
|
||
<button id="load-playlist-btn" class="btn btn-success">📂 Load Selected</button>
|
||
<button id="refresh-playlists-btn" class="btn btn-secondary">🔄 Refresh List</button>
|
||
</div>
|
||
<p style="margin: 10px 0 0 0; font-size: 0.9em; color: #888;">
|
||
Loading a playlist will copy it to <code>stream-queue.m3u</code> and Liquidsoap will start playing it.
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Queue Controls -->
|
||
<div class="queue-controls" style="margin-bottom: 15px;">
|
||
<button id="refresh-queue" class="btn btn-secondary">🔄 Refresh Queue</button>
|
||
<button id="save-queue-btn" class="btn btn-primary">💾 Save Queue</button>
|
||
<button id="clear-queue-btn" class="btn btn-warning">🗑️ Clear Queue</button>
|
||
<button id="add-random-tracks" class="btn btn-info">🎲 Add 10 Random</button>
|
||
</div>
|
||
|
||
<!-- Save As -->
|
||
<div style="margin-bottom: 15px; display: flex; gap: 10px; align-items: center;">
|
||
<input type="text" id="save-as-name" placeholder="New playlist name..." class="search-input" style="max-width: 250px;">
|
||
<button id="save-as-btn" class="btn btn-success">💾 Save As New Playlist</button>
|
||
</div>
|
||
|
||
<!-- Queue Status -->
|
||
<div id="queue-status" style="margin-bottom: 15px; padding: 10px; background: #1a1a1a; border-radius: 4px;">
|
||
<span id="queue-count">0</span> tracks in queue
|
||
</div>
|
||
|
||
<!-- Queue Contents -->
|
||
<div id="stream-queue-container" class="queue-list" style="max-height: 400px; overflow-y: auto;">
|
||
<div class="loading">Loading queue...</div>
|
||
</div>
|
||
|
||
<div class="queue-actions" style="margin-top: 20px;">
|
||
<h3>Add Tracks to Queue</h3>
|
||
<input type="text" id="queue-track-search" placeholder="Search tracks to add..." class="search-input">
|
||
<div id="queue-track-results" class="track-results"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Liquidsoap Stream Control -->
|
||
<div class="admin-section">
|
||
<h2>📡 Stream Control (Liquidsoap)</h2>
|
||
<p>Control the live audio stream. Commands are sent directly to Liquidsoap.</p>
|
||
|
||
<!-- Status Display -->
|
||
<div id="liquidsoap-status" style="margin-bottom: 20px; padding: 15px; background: #1a1a1a; border-radius: 8px;">
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
||
<div>
|
||
<strong>Uptime:</strong> <span id="ls-uptime">--</span>
|
||
</div>
|
||
<div>
|
||
<strong>Remaining:</strong> <span id="ls-remaining">--</span>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: 10px;">
|
||
<strong>Now Playing:</strong> <span id="ls-metadata">--</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Control Buttons -->
|
||
<div class="queue-controls" style="margin-bottom: 15px;">
|
||
<button id="ls-refresh-status" class="btn btn-secondary">🔄 Refresh Status</button>
|
||
<button id="ls-skip" class="btn btn-warning">⏭️ Skip Track</button>
|
||
<button id="ls-reload" class="btn btn-info">📂 Reload Playlist</button>
|
||
<button id="ls-restart" class="btn btn-danger">🔄 Restart Container</button>
|
||
</div>
|
||
|
||
<p style="font-size: 0.9em; color: #888;">
|
||
<strong>Skip Track:</strong> Immediately skip to the next track in the playlist.<br>
|
||
<strong>Reload Playlist:</strong> Force Liquidsoap to re-read stream-queue.m3u.<br>
|
||
<strong>Restart Container:</strong> Restart the Liquidsoap Docker container (causes brief stream interruption).
|
||
</p>
|
||
</div>
|
||
|
||
<!-- User Management -->
|
||
<div class="admin-section">
|
||
<div class="card">
|
||
<h3>👥 User Management</h3>
|
||
<p>Manage user accounts, roles, and permissions.</p>
|
||
<div class="controls">
|
||
<a href="/asteroid/admin/users" class="btn btn-primary">👥 Manage Users</a>
|
||
</div>
|
||
|
||
<!-- Admin Password Reset Form -->
|
||
<div class="form-section" style="margin-top: 20px;">
|
||
<h4>🔒 Reset User Password</h4>
|
||
<form id="admin-reset-password-form" onsubmit="return resetUserPassword(event)">
|
||
<div class="form-group">
|
||
<label for="reset-username">Username:</label>
|
||
<input type="text" id="reset-username" name="username" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="reset-new-password">New Password:</label>
|
||
<input type="password" id="reset-new-password" name="new-password" required minlength="8">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="reset-confirm-password">Confirm Password:</label>
|
||
<input type="password" id="reset-confirm-password" name="confirm-password" required minlength="8">
|
||
</div>
|
||
<div id="reset-password-message" class="message"></div>
|
||
<button type="submit" class="btn btn-primary">Reset Password</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Listener stats, geo stats, and password reset now handled by ParenScript in admin.lisp -->
|
||
</body>
|
||
</html>
|