Crossfade timing, scheduler fix, playback resume, auth noise cleanup

- Delay metadata/track-change notification by 1s after crossfade completes
- Log 'Loading next:' instead of 'Now playing:' during crossfade prep
- Add diagnostic logging: track duration check, crossfade trigger time
- harmony-load-playlist defaults to skip=nil: scheduler queues tracks
  without interrupting current playback
- Save current track to .playback-state.lisp on each track change,
  resume from saved position on restart
- Replace ~50 format-t debug statements in auth with log:info/log:warn
- Remove password hash logging for security
- Add .playback-state.lisp to .gitignore
This commit is contained in:
Glenn Thompson 2026-03-04 00:23:25 +03:00
parent 6e23efe1e4
commit d66d4fe46c
6 changed files with 137 additions and 123 deletions

1
.gitignore vendored
View File

@ -66,3 +66,4 @@ playlists/stream-queue.m3u
/test-postgres-db.lisp /test-postgres-db.lisp
/userdump.csv /userdump.csv
.envrc .envrc
.playback-state.lisp

View File

@ -1572,14 +1572,16 @@
(handler-case (handler-case
(progn (progn
(start-harmony-streaming) (start-harmony-streaming)
;; Load the current playlist and start playing ;; Load the current playlist and start playing (resume from saved position)
(let ((playlist-path (get-stream-queue-path))) (let ((playlist-path (get-stream-queue-path)))
(when (probe-file playlist-path) (when (probe-file playlist-path)
(let ((file-list (m3u-to-file-list playlist-path))) (let* ((file-list (m3u-to-file-list playlist-path))
(when file-list (resumed-list (when file-list (resume-from-saved-state file-list))))
(cl-streamer/harmony:play-list *harmony-pipeline* file-list (when resumed-list
(cl-streamer/harmony:play-list *harmony-pipeline* resumed-list
:crossfade-duration 3.0) :crossfade-duration 3.0)
(format t "~A tracks loaded from stream-queue.m3u~%" (length file-list)))))) (format t "~A tracks loaded from stream-queue.m3u (~A remaining after resume)~%"
(length file-list) (length resumed-list))))))
(format t "📡 Stream: ~a/asteroid.mp3~%" *stream-base-url*) (format t "📡 Stream: ~a/asteroid.mp3~%" *stream-base-url*)
(format t "📡 Stream: ~a/asteroid.aac~%" *stream-base-url*)) (format t "📡 Stream: ~a/asteroid.aac~%" *stream-base-url*))
(error (e) (error (e)

View File

@ -14,27 +14,21 @@
(if user (if user
(progn (progn
;; Login successful - store user ID in session ;; Login successful - store user ID in session
(format t "Login successful for user: ~a~%" (dm:field user "username")) (log:info "Login successful for user: ~a" (dm:field user "username"))
(handler-case (handler-case
(progn
(let* ((user-id (dm:id user)) (let* ((user-id (dm:id user))
(user-role (dm:field user "role")) (user-role (dm:field user "role"))
(redirect-path (cond (redirect-path (cond
;; Admin users go to admin dashboard
((string-equal user-role "admin") "/admin") ((string-equal user-role "admin") "/admin")
;; All other users go to their profile
(t "/profile")))) (t "/profile"))))
(format t "User ID from DB: ~a~%" user-id)
(format t "User role: ~a, redirecting to: ~a~%" user-role redirect-path)
(setf (session:field "user-id") user-id) (setf (session:field "user-id") user-id)
(format t "User ID #~a persisted in session.~%" (session:field "user-id")) (radiance:redirect redirect-path))
(radiance:redirect redirect-path)))
(error (e) (error (e)
(format t "Session error: ~a~%" e) (log:warn "Session error during login: ~a" e)
"Login successful but session error occurred"))) "Login successful but session error occurred")))
;; Login failed - show form with error ;; Login failed - show form with error
(progn (progn
(format t "Login unsuccessful for user: ~a~%" username) (log:info "Login failed for user: ~a" username)
(clip:process-to-string (clip:process-to-string
(load-template "login") (load-template "login")
:title "Asteroid Radio - Login" :title "Asteroid Radio - Login"
@ -168,7 +162,7 @@
(define-api asteroid/user/activate (user-id active) () (define-api asteroid/user/activate (user-id active) ()
"API endpoint for setting the active state of an user account" "API endpoint for setting the active state of an user account"
(format t "Activation of user: #~a set to ~a~%" user-id active) (log:info "Activation of user: #~a set to ~a" user-id active)
(require-role :admin) (require-role :admin)
(with-error-handling (with-error-handling
(let ((user (when user-id (let ((user (when user-id
@ -200,7 +194,7 @@
(define-api asteroid/user/role (user-id role) () (define-api asteroid/user/role (user-id role) ()
"API endpoint for setting the access role of an user account" "API endpoint for setting the access role of an user account"
(format t "Role of user: #~a set to ~a~%" user-id role) (log:info "Role of user: #~a set to ~a" user-id role)
(require-role :admin) (require-role :admin)
(with-error-handling (with-error-handling
(let ((user (when user-id (let ((user (when user-id

View File

@ -282,7 +282,9 @@
(update-all-mounts-metadata pipeline display-title) (update-all-mounts-metadata pipeline display-title)
(notify-track-change pipeline track-info)) (notify-track-change pipeline track-info))
(let ((voice (harmony:play path :mixer mixer :on-end on-end))) (let ((voice (harmony:play path :mixer mixer :on-end on-end)))
(if update-metadata
(log:info "Now playing: ~A" display-title) (log:info "Now playing: ~A" display-title)
(log:info "Loading next: ~A" display-title))
(values voice display-title track-info)))) (values voice display-title track-info))))
(defun voice-remaining-seconds (voice) (defun voice-remaining-seconds (voice)
@ -363,12 +365,21 @@
:name "cl-streamer-fadeout"))) :name "cl-streamer-fadeout")))
(volume-ramp voice 1.0 fade-in) (volume-ramp voice 1.0 fade-in)
(bt:join-thread fade-thread)) (bt:join-thread fade-thread))
;; Crossfade done — now update metadata & notify ;; Crossfade done — brief pause so listeners perceive
;; the new track before UI updates
(sleep 1.0)
(update-all-mounts-metadata pipeline display-title) (update-all-mounts-metadata pipeline display-title)
(notify-track-change pipeline track-info)) (notify-track-change pipeline track-info))
;; Wait for track to approach its end (or skip) ;; Wait for track to approach its end (or skip)
(setf (pipeline-skip-flag pipeline) nil) (setf (pipeline-skip-flag pipeline) nil)
(sleep 0.5) (sleep 0.5)
;; Log initial track duration info
(let ((initial-remaining (voice-remaining-seconds voice)))
(log:info "Track duration check: remaining=~A pos=~A total=~A sr=~A"
initial-remaining
(ignore-errors (mixed:frame-position voice))
(ignore-errors (mixed:frame-count voice))
(ignore-errors (mixed:samplerate voice))))
(loop while (and (pipeline-running-p pipeline) (loop while (and (pipeline-running-p pipeline)
(not (mixed:done-p voice)) (not (mixed:done-p voice))
(not (pipeline-skip-flag pipeline))) (not (pipeline-skip-flag pipeline)))
@ -376,7 +387,8 @@
when (and remaining when (and remaining
(<= remaining crossfade-duration) (<= remaining crossfade-duration)
(not (mixed:done-p voice))) (not (mixed:done-p voice)))
do (setf prev-voice voice) do (log:info "Crossfade trigger: ~,1Fs remaining" remaining)
(setf prev-voice voice)
(return) (return)
do (sleep 0.1)) do (sleep 0.1))
;; Handle skip ;; Handle skip

View File

@ -18,6 +18,60 @@
(defvar *harmony-aac-encoder* nil (defvar *harmony-aac-encoder* nil
"AAC encoder instance.") "AAC encoder instance.")
(defvar *harmony-state-file*
(merge-pathnames ".playback-state.lisp" (asdf:system-source-directory :asteroid))
"File to persist current playback position across restarts.")
;;; ---- Playback State Persistence ----
(defun save-playback-state (track-file-path)
"Save the current track file path to the state file.
Called on each track change so we can resume after restart."
(handler-case
(with-open-file (s *harmony-state-file*
:direction :output
:if-exists :supersede
:if-does-not-exist :create)
(prin1 (list :track-file track-file-path
:timestamp (get-universal-time))
s))
(error (e)
(log:warn "Could not save playback state: ~A" e))))
(defun load-playback-state ()
"Load the saved playback state. Returns plist or NIL."
(handler-case
(when (probe-file *harmony-state-file*)
(with-open-file (s *harmony-state-file* :direction :input)
(read s nil nil)))
(error (e)
(log:warn "Could not load playback state: ~A" e)
nil)))
(defun resume-from-saved-state (file-list)
"Given a playlist FILE-LIST, find the saved track and return the list
starting from the NEXT track after it. Returns the full list if no
saved state or track not found."
(let ((state (load-playback-state)))
(if state
(let* ((saved-file (getf state :track-file))
(pos (position saved-file file-list :test #'string=)))
(if pos
(let ((remaining (nthcdr (1+ pos) file-list)))
(if remaining
(progn
(log:info "Resuming after track ~A (~A of ~A)"
(file-namestring saved-file) (1+ pos) (length file-list))
remaining)
;; Was the last track — start from beginning
(progn
(log:info "Last saved track was final in playlist, starting from beginning")
file-list)))
(progn
(log:info "Saved track not found in current playlist, starting from beginning")
file-list)))
file-list)))
;;; ---- M3U Playlist Loading ---- ;;; ---- M3U Playlist Loading ----
(defun m3u-to-file-list (m3u-path) (defun m3u-to-file-list (m3u-path)
@ -55,6 +109,9 @@
:track-id track-id) :track-id track-id)
:curated) :curated)
(setf *last-known-track-curated* display-title)) (setf *last-known-track-curated* display-title))
;; Persist current track for resume-on-restart
(when file-path
(save-playback-state file-path))
(log:info "Track change: ~A (track-id: ~A)" display-title track-id))) (log:info "Track change: ~A (track-id: ~A)" display-title track-id)))
(defun find-track-by-file-path (file-path) (defun find-track-by-file-path (file-path)
@ -163,9 +220,11 @@
;;; ---- Playlist Control (replaces Liquidsoap commands) ---- ;;; ---- Playlist Control (replaces Liquidsoap commands) ----
(defun harmony-load-playlist (m3u-path) (defun harmony-load-playlist (m3u-path &key (skip nil))
"Load and start playing an M3U playlist through the Harmony pipeline. "Load and start playing an M3U playlist through the Harmony pipeline.
Converts Docker paths to host paths and feeds them to play-list." Converts Docker paths to host paths and feeds them to play-list.
When SKIP is T, immediately crossfade to the new playlist.
When SKIP is NIL (default), queue tracks to play after the current track."
(when *harmony-pipeline* (when *harmony-pipeline*
(let ((file-list (m3u-to-file-list m3u-path))) (let ((file-list (m3u-to-file-list m3u-path)))
(when file-list (when file-list
@ -175,9 +234,11 @@
(mapcar (lambda (path) (mapcar (lambda (path)
(list :file path)) (list :file path))
file-list)) file-list))
;; Skip current track to trigger crossfade into new playlist ;; Only skip if explicitly requested
(cl-streamer/harmony:pipeline-skip *harmony-pipeline*) (when skip
(log:info "Loaded playlist ~A (~A tracks)" m3u-path (length file-list)) (cl-streamer/harmony:pipeline-skip *harmony-pipeline*))
(log:info "Loaded playlist ~A (~A tracks~A)" m3u-path (length file-list)
(if skip ", skipping to start" ""))
(length file-list))))) (length file-list)))))
(defun harmony-skip-track () (defun harmony-skip-track ()

View File

@ -20,53 +20,38 @@
(dm:field user "last-login") nil) (dm:field user "last-login") nil)
(handler-case (handler-case
(db:with-transaction () (db:with-transaction ()
(format t "Inserting user data: ~a~%" user)
(let ((result (dm:insert user))) (let ((result (dm:insert user)))
(format t "Insert result: ~a~%" result) (log:info "User created: ~a (~a)" username role)
(format t "User created: ~a (~a)~%" username role)
t)) t))
(error (e) (error (e)
(format t "Error creating user ~a: ~a~%" username e) (log:warn "Error creating user ~a: ~a" username e)
nil)))) nil))))
(defun find-user-by-username (username) (defun find-user-by-username (username)
"Find a user by username" "Find a user by username"
(format t "Searching for user: ~a~%" username)
(let ((user (dm:get-one "USERS" (db:query (:= 'username username))))) (let ((user (dm:get-one "USERS" (db:query (:= 'username username)))))
(when user user))
(format t "Found user '~a' with id #~a~%" username (dm:id user))
user)))
(defun find-user-by-id (user-id) (defun find-user-by-id (user-id)
"Find a user by ID" "Find a user by ID"
(format t "Looking for user with ID: ~a (type: ~a)~%" user-id (type-of user-id))
(let* ((user-id (if (stringp user-id) (let* ((user-id (if (stringp user-id)
(parse-integer user-id) (parse-integer user-id)
user-id)) user-id))
(user (dm:get-one "USERS" (db:query (:= '_id user-id))))) (user (dm:get-one "USERS" (db:query (:= '_id user-id)))))
(when user user))
(format t "Found user '~a' with id #~a~%"
(dm:field user "username")
(dm:id user))
user)))
(defun authenticate-user (username password) (defun authenticate-user (username password)
"Authenticate a user with username and password" "Authenticate a user with username and password"
(format t "Attempting to authenticate user: ~a~%" username) (log:info "Authentication attempt for user: ~a" username)
(let ((user (find-user-by-username username))) (let ((user (find-user-by-username username)))
(format t "User found: ~a~%" (if user "YES" "NO"))
(when user (when user
(let ((user-active (dm:field user "active")) (let ((user-active (dm:field user "active"))
(user-password (dm:field user "password-hash"))) (user-password (dm:field user "password-hash")))
(handler-case (handler-case
(progn (verify-password password user-password)
(format t "User active: ~a~%" user-active)
(format t "Password hash from DB: ~a~%" user-password)
(format t "Password verification: ~a~%"
(verify-password password user-password)))
(error (e) (error (e)
(format t "Error during user data access: ~a~%" e))) (log:warn "Error during user authentication: ~a" e)))
(when (and (= 1 user-active) (when (and (= 1 user-active)
(verify-password password user-password)) (verify-password password user-password))
;; Update last login using data-model (database agnostic) ;; Update last login using data-model (database agnostic)
@ -77,7 +62,7 @@
;; Use data-model-save to normalize all timestamp fields before saving ;; Use data-model-save to normalize all timestamp fields before saving
(data-model-save user)) (data-model-save user))
(error (e) (error (e)
(format t "Warning: Could not update last-login: ~a~%" e))) (log:warn "Could not update last-login: ~a" e)))
user))))) user)))))
(defun hash-password (password) (defun hash-password (password)
@ -89,9 +74,6 @@
(defun verify-password (password hash) (defun verify-password (password hash)
"Verify a password against its hash" "Verify a password against its hash"
(let ((computed-hash (hash-password password))) (let ((computed-hash (hash-password password)))
(format t "Computed hash: ~a~%" computed-hash)
(format t "Stored hash: ~a~%" hash)
(format t "Match: ~a~%" (string= computed-hash hash))
(string= computed-hash hash))) (string= computed-hash hash)))
(defun reset-user-password (username new-password) (defun reset-user-password (username new-password)
@ -101,33 +83,26 @@
(handler-case (handler-case
(let ((new-hash (hash-password new-password)) (let ((new-hash (hash-password new-password))
(user-id (dm:id user))) (user-id (dm:id user)))
(format t "Resetting password for user: ~a (ID: ~a, type: ~a)~%" username user-id (type-of user-id)) (log:info "Resetting password for user: ~a" username)
(format t "Old hash: ~a~%" (dm:field user "password-hash"))
(format t "New hash: ~a~%" new-hash)
;; Try direct update with uppercase field name to match stored case ;; Try direct update with uppercase field name to match stored case
(setf (dm:field user "password-hash") new-hash) (setf (dm:field user "password-hash") new-hash)
;; (dm:save user) ;; (dm:save user)
(data-model-save user) (data-model-save user)
;; Verify the update worked ;; Verify the update worked
(let ((updated-user (find-user-by-username username))) (let ((updated-user (find-user-by-username username)))
(format t "Verification - fetching user again...~%")
(let ((updated-hash (dm:field updated-user "password-hash"))) (let ((updated-hash (dm:field updated-user "password-hash")))
(format t "Updated password hash in DB: ~a~%" updated-hash) (if (string= updated-hash new-hash)
(format t "Expected hash: ~a~%" new-hash)
(let ((match (string= updated-hash new-hash)))
(format t "Password update match: ~a~%" match)
(if match
(progn (progn
(format t "Password reset successful for user: ~a~%" username) (log:info "Password reset successful for user: ~a" username)
t) t)
(progn (progn
(format t "Password reset FAILED - hash didn't update~%") (log:warn "Password reset FAILED for user: ~a" username)
nil)))))) nil)))))
(error (e) (error (e)
(format t "Error resetting password for ~a: ~a~%" username e) (log:warn "Error resetting password for ~a: ~a" username e)
nil)) nil))
(progn (progn
(format t "User not found: ~a~%" username) (log:warn "User not found for password reset: ~a" username)
nil)))) nil))))
(defun user-has-role-p (user role) (defun user-has-role-p (user role)
@ -135,7 +110,6 @@
(when user (when user
(let* ((role-value (dm:field user "role")) (let* ((role-value (dm:field user "role"))
(user-role (intern (string-upcase role-value) :keyword))) (user-role (intern (string-upcase role-value) :keyword)))
(format t "User role: ~a, checking against: ~a~%" user-role role)
(or (eq user-role role) (or (eq user-role role)
(and (eq role :listener) (member user-role '(:dj :admin))) (and (eq role :listener) (member user-role '(:dj :admin)))
(and (eq role :dj) (eq user-role :admin)))))) (and (eq role :dj) (eq user-role :admin))))))
@ -144,13 +118,10 @@
"Get the currently authenticated user from session" "Get the currently authenticated user from session"
(handler-case (handler-case
(let ((user-id (session:field "user-id"))) (let ((user-id (session:field "user-id")))
(format t "Session user-id: ~a~%" user-id)
(when user-id (when user-id
(let ((user (find-user-by-id user-id))) (find-user-by-id user-id)))
(format t "Found user: ~a~%" (if user "YES" "NO"))
user)))
(error (e) (error (e)
(format t "Error getting current user: ~a~%" e) (log:warn "Error getting current user: ~a" e)
nil))) nil)))
(defun get-current-user-id () (defun get-current-user-id ()
@ -179,8 +150,6 @@
;; Check for "api/" anywhere in the path ;; Check for "api/" anywhere in the path
(is-api-request (if api t (or (search "/api/" uri) (is-api-request (if api t (or (search "/api/" uri)
(search "api/" uri))))) (search "api/" uri)))))
(format t "Authentication check - User ID: ~a, URI: ~a, Is API: ~a~%"
user-id uri (if is-api-request "YES" "NO"))
(if user-id (if user-id
t ; Authenticated - return T to continue t ; Authenticated - return T to continue
;; Not authenticated - emit error and signal to stop processing ;; Not authenticated - emit error and signal to stop processing
@ -188,14 +157,11 @@
(if is-api-request (if is-api-request
;; API request - return JSON error with 401 status using api-output ;; API request - return JSON error with 401 status using api-output
(progn (progn
(format t "Authentication failed - returning JSON 401~%")
(api-output `(("status" . "error") (api-output `(("status" . "error")
("message" . "Authentication required")) ("message" . "Authentication required"))
:status 401)) :status 401))
;; Page request - redirect to login ;; Page request - redirect to login
(progn (radiance:redirect "/login"))))))
(format t "Authentication failed - redirecting to login~%")
(radiance:redirect "/login")))))))
(defun require-role (role &key (api nil)) (defun require-role (role &key (api nil))
"Require user to have a specific role. "Require user to have a specific role.
@ -209,22 +175,14 @@
(is-api-request (if api t (or (search "/api/" uri) (is-api-request (if api t (or (search "/api/" uri)
(and (>= (length uri) 4) (and (>= (length uri) 4)
(string= "api/" (subseq uri 0 4))))))) (string= "api/" (subseq uri 0 4)))))))
(format t "Current user for role check: ~a~%" (if current-user "FOUND" "NOT FOUND"))
(format t "Request URI: ~a, Is API: ~a~%" uri (if is-api-request "YES" "NO"))
(when current-user
(format t "User has role ~a: ~a~%" role (user-has-role-p current-user role)))
(if (and current-user (user-has-role-p current-user role)) (if (and current-user (user-has-role-p current-user role))
t ; Authorized - return T to continue t ; Authorized - return T to continue
;; Not authorized - emit error ;; Not authorized - emit error
(if is-api-request (if is-api-request
;; API request - return NIL (caller will handle JSON error) ;; API request - return NIL (caller will handle JSON error)
(progn nil
(format t "Role check failed - authorization denied~%")
nil)
;; Page request - redirect to login (redirect doesn't return) ;; Page request - redirect to login (redirect doesn't return)
(progn (radiance:redirect "/asteroid/login")))))
(format t "Role check failed - redirecting to login~%")
(radiance:redirect "/asteroid/login"))))))
(defun update-user-role (user-id new-role) (defun update-user-role (user-id new-role)
"Update a user's role" "Update a user's role"
@ -236,9 +194,9 @@
;; (dm:save user) ;; (dm:save user)
(data-model-save user) (data-model-save user)
t) t)
(format t "Could not find user with id #~a~%" user-id))) (log:warn "Could not find user with id #~a" user-id)))
(error (e) (error (e)
(format t "Error updating user role: ~a~%" e) (log:warn "Error updating user role: ~a" e)
nil))) nil)))
(defun deactivate-user (user-id) (defun deactivate-user (user-id)
@ -248,10 +206,10 @@
(setf (dm:field user "active") 0) (setf (dm:field user "active") 0)
;; (dm:save user) ;; (dm:save user)
(data-model-save user) (data-model-save user)
(format t "Deactivated user ~a~%" user-id) (log:info "Deactivated user ~a" user-id)
t) t)
(error (e) (error (e)
(format t "Error deactivating user: ~a~%" e) (log:warn "Error deactivating user: ~a" e)
nil))) nil)))
(defun activate-user (user-id) (defun activate-user (user-id)
@ -261,21 +219,15 @@
(setf (dm:field user "active") 1) (setf (dm:field user "active") 1)
;; (dm:save user) ;; (dm:save user)
(data-model-save user) (data-model-save user)
(format t "Activated user ~a~%" user-id) (log:info "Activated user ~a" user-id)
t) t)
(error (e) (error (e)
(format t "Error activating user: ~a~%" e) (log:warn "Error activating user: ~a" e)
nil))) nil)))
(defun get-all-users () (defun get-all-users ()
"Get all users from database" "Get all users from database"
(format t "Getting all users from database...~%") (dm:get "USERS" (db:query :all)))
(let ((users (dm:get "USERS" (db:query :all))))
(format t "Total users in database: ~a~%" (length users))
(dolist (user users)
(format t "User: ~a~%" (dm:field user "username"))
(format t "User _id field: ~a (type: ~a)~%" (dm:id user) (type-of (dm:id user))))
users))
(defun get-user-stats () (defun get-user-stats ()
"Get user statistics" "Get user statistics"
@ -300,27 +252,20 @@
(string= role "admin"))) (string= role "admin")))
(get-all-users)))) (get-all-users))))
(unless existing-admins (unless existing-admins
(format t "~%Creating default admin user...~%") (log:info "Creating default admin user (admin / asteroid123) - change password after first login!")
(format t "Username: admin~%")
(format t "Password: asteroid123~%")
(format t "Please change this password after first login!~%~%")
(create-user "admin" "admin@asteroid.radio" "asteroid123" :role :admin :active t)))) (create-user "admin" "admin@asteroid.radio" "asteroid123" :role :admin :active t))))
(defun initialize-user-system () (defun initialize-user-system ()
"Initialize the user management system" "Initialize the user management system"
(format t "Initializing user management system...~%") (log:info "Initializing user management system")
;; Skip database check at startup - database queries hang with current setup
(format t "Skipping admin creation check - database already initialized~%")
(format t "User management initialization complete.~%")
;; Try immediate initialization first ;; Try immediate initialization first
#+nil #+nil
(handler-case (handler-case
(progn (progn
(format t "Setting up user management...~%")
(create-default-admin) (create-default-admin)
(format t "User management initialization complete.~%")) (log:info "User management initialization complete."))
(error (e) (error (e)
(format t "Database not ready, will retry in background: ~a~%" e) (log:info "Database not ready, will retry in background: ~a" e)
;; Fallback to delayed initialization ;; Fallback to delayed initialization
(bt:make-thread (bt:make-thread
(lambda () (lambda ()
@ -329,12 +274,11 @@
(sleep 3)) ; Give database more time to initialize (sleep 3)) ; Give database more time to initialize
(handler-case (handler-case
(progn (progn
(format t "Retrying user management setup...~%")
(create-default-admin) (create-default-admin)
(format t "User management initialization complete.~%") (log:info "User management initialization complete.")
(return)) (return))
(error (e) (error (e)
(format t "Error initializing user system: ~a~%" e))))) (log:warn "Error initializing user system: ~a" e)))))
:name "user-init")))) :name "user-init"))))
(defun dump-users (users) (defun dump-users (users)