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:
glenneth 2025-12-17 14:27:16 +03:00
parent bafc43c9f4
commit f8e37ac02a
3 changed files with 190 additions and 6 deletions

View File

@ -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")

View File

@ -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."
(list :enabled *scheduler-enabled*
:running *scheduler-running*
:current-playlist (get-current-scheduled-playlist)
:schedule (get-schedule)))
(let ((time-info (get-server-time-info)))
(list :enabled *scheduler-enabled*
:running *scheduler-running*
:current-playlist (get-current-scheduled-playlist)
: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))))

View File

@ -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>