feat: Add admin UI for playlist scheduler with server time display
- Add server time info (UTC) to scheduler status API - Add scheduler section to admin.ctml with: - Server time display (UTC) - Current playlist indicator - Enable/Disable/Load Current buttons - Schedule table showing all time slots - Add ParenScript functions for scheduler controls - Auto-refresh scheduler status every 30 seconds - Highlight active playlist in schedule table
This commit is contained in:
parent
923b1dc4ea
commit
6a80072075
|
|
@ -28,8 +28,11 @@
|
|||
(load-current-queue)
|
||||
(refresh-liquidsoap-status)
|
||||
(setup-stats-refresh)
|
||||
(refresh-scheduler-status)
|
||||
;; Update Liquidsoap status every 10 seconds
|
||||
(set-interval refresh-liquidsoap-status 10000))))
|
||||
(set-interval refresh-liquidsoap-status 10000)
|
||||
;; Update scheduler status every 30 seconds
|
||||
(set-interval refresh-scheduler-status 30000))))
|
||||
|
||||
;; Setup all event listeners
|
||||
(defun setup-event-listeners ()
|
||||
|
|
@ -1123,6 +1126,112 @@
|
|||
(set-interval refresh-listener-stats 30000)
|
||||
(set-interval refresh-geo-stats 60000))
|
||||
|
||||
;; ========================================
|
||||
;; Playlist Scheduler Controls
|
||||
;; ========================================
|
||||
|
||||
;; Refresh scheduler status
|
||||
(defun refresh-scheduler-status ()
|
||||
(ps:chain
|
||||
(fetch "/api/asteroid/scheduler/status")
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (result)
|
||||
(let ((data (or (ps:@ result data) result)))
|
||||
(when (= (ps:@ data status) "success")
|
||||
;; Update server time
|
||||
(let ((time-el (ps:chain document (get-element-by-id "server-utc-time")))
|
||||
(server-time (ps:@ data server_time)))
|
||||
(when (and time-el server-time)
|
||||
(setf (ps:@ time-el text-content) (ps:@ server-time utc))))
|
||||
|
||||
;; Update current playlist
|
||||
(let ((playlist-el (ps:chain document (get-element-by-id "scheduler-current-playlist"))))
|
||||
(when playlist-el
|
||||
(setf (ps:@ playlist-el text-content) (or (ps:@ data current_playlist) "--"))))
|
||||
|
||||
;; Update status indicator
|
||||
(let ((status-el (ps:chain document (get-element-by-id "scheduler-status-indicator"))))
|
||||
(when status-el
|
||||
(if (ps:@ data enabled)
|
||||
(setf (ps:@ status-el inner-h-t-m-l)
|
||||
"<span style=\"color: #00ff00;\">🟢 Enabled</span>")
|
||||
(setf (ps:@ status-el inner-h-t-m-l)
|
||||
"<span style=\"color: #ffaa00;\">🟡 Disabled</span>"))))
|
||||
|
||||
;; Update schedule table
|
||||
(let ((table-body (ps:chain document (get-element-by-id "scheduler-table-body")))
|
||||
(schedule (ps:@ data schedule))
|
||||
(current-hour (when (ps:@ data server_time) (ps:@ data server_time utc_hour))))
|
||||
(when (and table-body schedule)
|
||||
(let ((html ""))
|
||||
(ps:chain schedule
|
||||
(for-each (lambda (entry)
|
||||
(let* ((hour (ps:@ entry hour))
|
||||
(playlist (ps:@ entry playlist))
|
||||
(is-active (and current-hour
|
||||
(>= current-hour hour)
|
||||
(or (not (ps:chain schedule (find (lambda (e) (and (> (ps:@ e hour) hour) (<= (ps:@ e hour) current-hour))))))
|
||||
t))))
|
||||
(setf html
|
||||
(+ html
|
||||
"<tr" (if (= playlist (ps:@ data current_playlist)) " style=\"background: #1a3a1a;\"" "") ">"
|
||||
"<td>" (if (< hour 10) "0" "") hour ":00 UTC</td>"
|
||||
"<td>" playlist "</td>"
|
||||
"<td>" (if (= playlist (ps:@ data current_playlist)) "▶️ Active" "") "</td>"
|
||||
"</tr>"))))))
|
||||
(setf (ps:@ table-body inner-h-t-m-l) html))))))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error loading scheduler status:" error))))))
|
||||
|
||||
;; Enable scheduler
|
||||
(defun enable-scheduler ()
|
||||
(ps:chain
|
||||
(fetch "/api/asteroid/scheduler/enable" (ps:create :method "POST"))
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (result)
|
||||
(let ((data (or (ps:@ result data) result)))
|
||||
(if (= (ps:@ data status) "success")
|
||||
(progn
|
||||
(show-toast "✓ Scheduler enabled")
|
||||
(refresh-scheduler-status))
|
||||
(alert (+ "Error: " (or (ps:@ data message) "Unknown error")))))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error enabling scheduler:" error))
|
||||
(alert "Error enabling scheduler")))))
|
||||
|
||||
;; Disable scheduler
|
||||
(defun disable-scheduler ()
|
||||
(ps:chain
|
||||
(fetch "/api/asteroid/scheduler/disable" (ps:create :method "POST"))
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (result)
|
||||
(let ((data (or (ps:@ result data) result)))
|
||||
(if (= (ps:@ data status) "success")
|
||||
(progn
|
||||
(show-toast "⏸️ Scheduler disabled")
|
||||
(refresh-scheduler-status))
|
||||
(alert (+ "Error: " (or (ps:@ data message) "Unknown error")))))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error disabling scheduler:" error))
|
||||
(alert "Error disabling scheduler")))))
|
||||
|
||||
;; Load current scheduled playlist
|
||||
(defun load-current-scheduled-playlist ()
|
||||
(ps:chain
|
||||
(fetch "/api/asteroid/scheduler/load-current" (ps:create :method "POST"))
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (result)
|
||||
(let ((data (or (ps:@ result data) result)))
|
||||
(if (= (ps:@ data status) "success")
|
||||
(progn
|
||||
(show-toast (+ "✓ Loaded " (ps:@ data playlist)))
|
||||
(refresh-scheduler-status)
|
||||
(load-current-queue))
|
||||
(alert (+ "Error: " (or (ps:@ data message) "Unknown error")))))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error loading scheduled playlist:" error))
|
||||
(alert "Error loading scheduled playlist")))))
|
||||
|
||||
;; Make functions globally accessible for onclick handlers
|
||||
(setf (ps:@ window go-to-page) go-to-page)
|
||||
(setf (ps:@ window previous-page) previous-page)
|
||||
|
|
@ -1140,6 +1249,10 @@
|
|||
(setf (ps:@ window refresh-listener-stats) refresh-listener-stats)
|
||||
(setf (ps:@ window refresh-geo-stats) refresh-geo-stats)
|
||||
(setf (ps:@ window setup-stats-refresh) setup-stats-refresh)
|
||||
(setf (ps:@ window refresh-scheduler-status) refresh-scheduler-status)
|
||||
(setf (ps:@ window enable-scheduler) enable-scheduler)
|
||||
(setf (ps:@ window disable-scheduler) disable-scheduler)
|
||||
(setf (ps:@ window load-current-scheduled-playlist) load-current-scheduled-playlist)
|
||||
))
|
||||
"Compiled JavaScript for admin dashboard - generated at load time")
|
||||
|
||||
|
|
|
|||
|
|
@ -118,12 +118,27 @@
|
|||
"Get the current playlist schedule as a sorted list."
|
||||
(sort (copy-list *playlist-schedule*) #'< :key #'car))
|
||||
|
||||
(defun get-server-time-info ()
|
||||
"Get current server time information in both UTC and local timezone."
|
||||
(let* ((now (local-time:now))
|
||||
(utc-hour (local-time:timestamp-hour now :timezone local-time:+utc-zone+))
|
||||
(utc-minute (local-time:timestamp-minute now :timezone local-time:+utc-zone+)))
|
||||
(list :utc-time (local-time:format-timestring nil now
|
||||
:format '(:year "-" (:month 2) "-" (:day 2) " " (:hour 2) ":" (:min 2) ":" (:sec 2) " UTC")
|
||||
:timezone local-time:+utc-zone+)
|
||||
:utc-hour utc-hour
|
||||
:utc-minute utc-minute
|
||||
:local-time (local-time:format-timestring nil now
|
||||
:format '(:year "-" (:month 2) "-" (:day 2) " " (:hour 2) ":" (:min 2) ":" (:sec 2))))))
|
||||
|
||||
(defun get-scheduler-status ()
|
||||
"Get the current status of the scheduler."
|
||||
(let ((time-info (get-server-time-info)))
|
||||
(list :enabled *scheduler-enabled*
|
||||
:running *scheduler-running*
|
||||
:current-playlist (get-current-scheduled-playlist)
|
||||
:schedule (get-schedule)))
|
||||
:schedule (get-schedule)
|
||||
:server-time time-info)))
|
||||
|
||||
;;; API Endpoints for Admin Interface
|
||||
|
||||
|
|
@ -131,11 +146,15 @@
|
|||
"Get the current scheduler status"
|
||||
(require-role :admin)
|
||||
(with-error-handling
|
||||
(let ((status (get-scheduler-status)))
|
||||
(let* ((status (get-scheduler-status))
|
||||
(time-info (getf status :server-time)))
|
||||
(api-output `(("status" . "success")
|
||||
("enabled" . ,(if (getf status :enabled) t :json-false))
|
||||
("running" . ,(if (getf status :running) t :json-false))
|
||||
("current_playlist" . ,(getf status :current-playlist))
|
||||
("server_time" . (("utc" . ,(getf time-info :utc-time))
|
||||
("utc_hour" . ,(getf time-info :utc-hour))
|
||||
("local" . ,(getf time-info :local-time))))
|
||||
("schedule" . ,(mapcar (lambda (entry)
|
||||
`(("hour" . ,(car entry))
|
||||
("playlist" . ,(cdr entry))))
|
||||
|
|
|
|||
|
|
@ -214,6 +214,58 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Playlist Scheduler -->
|
||||
<div class="admin-section">
|
||||
<h2>⏰ Playlist Scheduler</h2>
|
||||
<p>Automatically load playlists at scheduled times (UTC).</p>
|
||||
|
||||
<!-- Server Time Display -->
|
||||
<div id="scheduler-time" style="margin-bottom: 20px; padding: 15px; background: #1a1a1a; border-radius: 8px;">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px;">
|
||||
<div>
|
||||
<strong>🕐 Server Time (UTC):</strong><br>
|
||||
<span id="server-utc-time" style="font-size: 1.2em; font-family: monospace;">--:--:--</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>📋 Current Playlist:</strong><br>
|
||||
<span id="scheduler-current-playlist" style="font-size: 1.1em;">--</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>⚡ Scheduler Status:</strong><br>
|
||||
<span id="scheduler-status-indicator">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scheduler Controls -->
|
||||
<div class="queue-controls" style="margin-bottom: 15px;">
|
||||
<button id="scheduler-refresh" class="btn btn-secondary" onclick="refreshSchedulerStatus()">🔄 Refresh</button>
|
||||
<button id="scheduler-enable" class="btn btn-success" onclick="enableScheduler()">▶️ Enable</button>
|
||||
<button id="scheduler-disable" class="btn btn-warning" onclick="disableScheduler()">⏸️ Disable</button>
|
||||
<button id="scheduler-load-current" class="btn btn-info" onclick="loadCurrentScheduledPlaylist()">📂 Load Current</button>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Table -->
|
||||
<h3>📅 Schedule</h3>
|
||||
<table class="listener-stats-table" style="margin-top: 10px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time (UTC)</th>
|
||||
<th>Playlist</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="scheduler-table-body">
|
||||
<tr><td colspan="3" style="color: #888;">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p style="margin-top: 15px; font-size: 0.9em; color: #888;">
|
||||
The scheduler automatically loads the appropriate playlist at each scheduled time.
|
||||
Use "Load Current" to manually sync to the correct playlist for the current time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Liquidsoap Stream Control -->
|
||||
<div class="admin-section">
|
||||
<h2>📡 Stream Control (Liquidsoap)</h2>
|
||||
|
|
|
|||
Loading…
Reference in New Issue