From f8e37ac02a659ab14aa1cdde08db86a2272b844b Mon Sep 17 00:00:00 2001 From: glenneth Date: Wed, 17 Dec 2025 14:27:16 +0300 Subject: [PATCH] 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 --- parenscript/admin.lisp | 115 +++++++++++++++++++++++++++++++++++++++- playlist-scheduler.lisp | 29 ++++++++-- template/admin.ctml | 52 ++++++++++++++++++ 3 files changed, 190 insertions(+), 6 deletions(-) diff --git a/parenscript/admin.lisp b/parenscript/admin.lisp index c505634..87a62df 100644 --- a/parenscript/admin.lisp +++ b/parenscript/admin.lisp @@ -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) + "🟒 Enabled") + (setf (ps:@ status-el inner-h-t-m-l) + "🟑 Disabled")))) + + ;; 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 + "" + "" (if (< hour 10) "0" "") hour ":00 UTC" + "" playlist "" + "" (if (= playlist (ps:@ data current_playlist)) "▢️ Active" "") "" + "")))))) + (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") diff --git a/playlist-scheduler.lisp b/playlist-scheduler.lisp index a47e257..6066221 100644 --- a/playlist-scheduler.lisp +++ b/playlist-scheduler.lisp @@ -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)))) diff --git a/template/admin.ctml b/template/admin.ctml index cdbef42..cfbce07 100644 --- a/template/admin.ctml +++ b/template/admin.ctml @@ -214,6 +214,58 @@ + +
+

⏰ Playlist Scheduler

+

Automatically load playlists at scheduled times (UTC).

+ + +
+
+
+ πŸ• Server Time (UTC):
+ --:--:-- +
+
+ πŸ“‹ Current Playlist:
+ -- +
+
+ ⚑ Scheduler Status:
+ -- +
+
+
+ + +
+ + + + +
+ + +

πŸ“… Schedule

+ + + + + + + + + + + +
Time (UTC)PlaylistStatus
Loading...
+ +

+ 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. +

+
+

πŸ“‘ Stream Control (Liquidsoap)