Compare commits

..

No commits in common. "349fa31d8f2c26abef8f358d92cd2ad9407d93a9" and "b415ca9530e881efb1b0cf6e85d1b402f6ab6d0d" have entirely different histories.

9 changed files with 20 additions and 127 deletions

View File

@ -53,14 +53,6 @@
(setf (session:field "user-id") nil)
(radiance:redirect "/"))
;; Helper to convert Common Lisp universal time to Unix epoch
(defun cl-time-to-unix (cl-time)
"Convert Common Lisp universal time to Unix epoch.
CL epoch is 1900-01-01, Unix epoch is 1970-01-01.
Difference is 2208988800 seconds."
(when cl-time
(- cl-time 2208988800)))
;; API: Get all users (admin only)
(define-api asteroid/users () ()
"API endpoint to get all users"
@ -74,8 +66,8 @@
("email" . ,(dm:field user "email"))
("role" . ,(dm:field user "role"))
("active" . ,(= (dm:field user "active") 1))
("created-date" . ,(cl-time-to-unix (dm:field user "created-date")))
("last-login" . ,(cl-time-to-unix (dm:field user "last-login")))))
("created-date" . ,(dm:field user "created-date"))
("last-login" . ,(dm:field user "last-login"))))
users)))))
(error (e)
(api-output `(("status" . "error")

View File

@ -96,9 +96,3 @@
(progn
(setf (header "Content-Type") "text/plain")
"Stream Offline")))))
(define-api asteroid/channel-name () ()
"Get the current curated channel name for live updates.
Returns JSON with the channel name from the current playlist's PHASE header."
(with-error-handling
(api-output `(("channel_name" . ,(get-curated-channel-name))))))

View File

@ -595,36 +595,7 @@
;; Update now playing every 5 seconds
(set-interval update-now-playing 5000)
;; Poll server for channel name changes (works across all listeners)
(let ((last-channel-name nil))
(set-interval
(lambda ()
(ps:chain
(fetch "/api/asteroid/channel-name")
(then (lambda (response)
(if (ps:@ response ok)
(ps:chain response (json))
nil)))
(then (lambda (data)
(when data
(let ((current-channel-name (or (ps:@ data data channel_name)
(ps:@ data channel_name))))
(when (and current-channel-name
(not (= current-channel-name last-channel-name)))
(setf last-channel-name current-channel-name)
;; Update localStorage for cross-window sync
(ps:chain local-storage (set-item "curated-channel-name" current-channel-name))
;; Update channel selector in current document
(let ((channel-selector (ps:chain document (get-element-by-id "stream-channel"))))
(when channel-selector
(let ((curated-option (ps:chain channel-selector (query-selector "option[value='curated']"))))
(when curated-option
(setf (ps:@ curated-option text-content) (+ "🎧 " current-channel-name)))))))))))
(catch (lambda (error)
(ps:chain console (log "Could not fetch channel name:" error))))))
10000)) ;; Poll every 10 seconds
;; Listen for messages from popout window
;; Listen for messages from popout window
(ps:chain window
(add-event-listener
"message"

View File

@ -556,33 +556,20 @@
;; Update quality selector state based on channel
(update-quality-selector-state)
;; Poll server for channel name changes (works across all listeners)
(let ((last-channel-name nil))
;; Check for channel name changes from localStorage periodically
(let ((last-channel-name (ps:chain local-storage (get-item "curated-channel-name"))))
(set-interval
(lambda ()
(ps:chain
(fetch "/api/asteroid/channel-name")
(then (lambda (response)
(if (ps:@ response ok)
(ps:chain response (json))
nil)))
(then (lambda (data)
(when data
(let ((current-channel-name (or (ps:@ data data channel_name)
(ps:@ data channel_name))))
(when (and current-channel-name
(not (= current-channel-name last-channel-name)))
(setf last-channel-name current-channel-name)
;; Update localStorage for popout player sync
(ps:chain local-storage (set-item "curated-channel-name" current-channel-name))
(let ((channel-selector (ps:chain document (get-element-by-id "stream-channel"))))
(when channel-selector
(let ((curated-option (ps:chain channel-selector (query-selector "option[value='curated']"))))
(when curated-option
(setf (ps:@ curated-option text-content) (+ "🎧 " current-channel-name)))))))))))
(catch (lambda (error)
(ps:chain console (log "Could not fetch channel name:" error))))))
10000)) ;; Poll every 10 seconds
(let ((current-channel-name (ps:chain local-storage (get-item "curated-channel-name"))))
(when (and current-channel-name
(not (= current-channel-name last-channel-name)))
(setf last-channel-name current-channel-name)
(let ((channel-selector (ps:chain document (get-element-by-id "stream-channel"))))
(when channel-selector
(let ((curated-option (ps:chain channel-selector (query-selector "option[value='curated']"))))
(when curated-option
(setf (ps:@ curated-option text-content) (+ "🎧 " current-channel-name)))))))))
2000))
;; Start now playing updates
(set-timeout update-mini-now-playing 1000)

View File

@ -57,13 +57,9 @@
"</select>"
"</td>"
"<td>" (if (ps:@ user active) "✅ Active" "❌ Inactive") "</td>"
"<td>" (let ((login-val (ps:getprop user "last-login")))
(if login-val
(let ((date-val (if (> login-val 9999999999)
login-val ; Already milliseconds
(* login-val 1000)))) ; Convert seconds to ms
(ps:chain (ps:new (-date date-val)) (to-locale-string)))
"Never")) "</td>"
"<td>" (if (ps:getprop user "last-login")
(ps:chain (ps:new (-date (* (ps:getprop user "last-login") 1000))) (to-locale-string))
"Never") "</td>"
"<td class=\"user-actions\">"
(if (ps:@ user active)
(+ "<button class=\"btn btn-danger\" onclick=\"deactivateUser('" (ps:@ user id) "')\">Deactivate</button>")

View File

@ -1250,28 +1250,4 @@
(0% :opacity 1)
(50% :opacity 0.3)
(100% :opacity 1))
;; Site footer - subtle, unobtrusive
(.site-footer
:text-align "center"
:padding "20px 0"
:margin-top "30px"
:border-top "1px solid #333"
:font-size "0.85em"
:color "#666"
:display "flex"
:justify-content "center"
:gap "30px"
:flex-wrap "wrap"
(a
:color "#888"
:text-decoration "none"
:transition "color 0.2s ease")
((:and a :hover)
:color "#00ff00")
(.craftering
(a :margin "0 5px")))
) ;; End of let block

View File

@ -93,15 +93,6 @@
</div>
</div>
</main>
<footer class="site-footer">
<span>Listed on <a href="http://www.internet-radio.com/stations/ambient/" target="_blank" rel="noopener">Internet Radio</a></span>
<span class="craftering">
<a href="https://craftering.systemcrafters.net/@asteroid/previous">←</a>
<a href="https://craftering.systemcrafters.net/">craftering</a>
<a href="https://craftering.systemcrafters.net/@asteroid/next">→</a>
</span>
</footer>
</div>
</body>
</html>

View File

@ -126,15 +126,6 @@
</div>
</div>
</main>
<footer class="site-footer">
<span>Listed on <a href="http://www.internet-radio.com/stations/ambient/" target="_blank" rel="noopener">Internet Radio</a></span>
<span class="craftering">
<a href="https://craftering.systemcrafters.net/@asteroid/previous">←</a>
<a href="https://craftering.systemcrafters.net/">craftering</a>
<a href="https://craftering.systemcrafters.net/@asteroid/next">→</a>
</span>
</footer>
</div>
</body>
</html>

View File

@ -70,14 +70,9 @@
(when (and (= 1 user-active)
(verify-password password user-password))
;; Update last login using data-model (database agnostic)
;; Use ISO 8601 format in UTC that PostgreSQL TIMESTAMP can parse
(handler-case
(progn
(setf (dm:field user "last-login")
(local-time:format-timestring nil (local-time:now)
:format '(:year "-" (:month 2) "-" (:day 2) " "
(:hour 2) ":" (:min 2) ":" (:sec 2))
:timezone local-time:+utc-zone+))
(setf (dm:field user "last-login") (format nil "~a" (local-time:now)))
(dm:save user))
(error (e)
(format t "Warning: Could not update last-login: ~a~%" e)))