diff --git a/asteroid.asd b/asteroid.asd
index 7a4d6ac..a42e30a 100644
--- a/asteroid.asd
+++ b/asteroid.asd
@@ -45,7 +45,8 @@
(:module :parenscript
:components ((:file "auth-ui")
(:file "front-page")
- (:file "profile")))
+ (:file "profile")
+ (:file "users")))
(:file "stream-media")
(:file "user-management")
(:file "playlist-management")
diff --git a/asteroid.lisp b/asteroid.lisp
index 5ef76e7..7cf4c4a 100644
--- a/asteroid.lisp
+++ b/asteroid.lisp
@@ -511,6 +511,18 @@
(format t "ERROR generating profile.js: ~a~%" e)
(format nil "// Error generating JavaScript: ~a~%" e))))
+ ;; Serve ParenScript-compiled users.js
+ ((string= path "js/users.js")
+ (format t "~%=== SERVING PARENSCRIPT users.js ===~%")
+ (setf (content-type *response*) "application/javascript")
+ (handler-case
+ (let ((js (generate-users-js)))
+ (format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
+ (if js js "// Error: No JavaScript generated"))
+ (error (e)
+ (format t "ERROR generating users.js: ~a~%" e)
+ (format nil "// Error generating JavaScript: ~a~%" e))))
+
;; Serve regular static file
(t
(serve-file (merge-pathnames (format nil "static/~a" path)
diff --git a/parenscript/users.lisp b/parenscript/users.lisp
new file mode 100644
index 0000000..504319f
--- /dev/null
+++ b/parenscript/users.lisp
@@ -0,0 +1,203 @@
+;;;; users.lisp - ParenScript version of users.js
+;;;; User management page for admins
+
+(in-package #:asteroid)
+
+(defparameter *users-js*
+ (ps:ps*
+ '(progn
+
+ ;; Load user stats
+ (defun load-user-stats ()
+ (ps:chain
+ (fetch "/api/asteroid/user-stats")
+ (then (lambda (response) (ps:chain response (json))))
+ (then (lambda (result)
+ (let ((data (or (ps:@ result data) result)))
+ (when (and (= (ps:@ data status) "success") (ps:@ data stats))
+ (let ((stats (ps:@ data stats)))
+ (setf (ps:@ (ps:chain document (get-element-by-id "total-users")) text-content)
+ (or (ps:getprop stats "total-users") 0))
+ (setf (ps:@ (ps:chain document (get-element-by-id "active-users")) text-content)
+ (or (ps:getprop stats "active-users") 0))
+ (setf (ps:@ (ps:chain document (get-element-by-id "admin-users")) text-content)
+ (or (ps:getprop stats "admins") 0))
+ (setf (ps:@ (ps:chain document (get-element-by-id "dj-users")) text-content)
+ (or (ps:getprop stats "djs") 0)))))))
+ (catch (lambda (error)
+ (ps:chain console (error "Error loading user stats:" error))))))
+
+ ;; Load users list
+ (defun load-users ()
+ (ps:chain
+ (fetch "/api/asteroid/users")
+ (then (lambda (response) (ps:chain response (json))))
+ (then (lambda (result)
+ (let ((data (or (ps:@ result data) result)))
+ (when (= (ps:@ data status) "success")
+ (show-users-table (ps:@ data users))
+ (setf (ps:@ (ps:chain document (get-element-by-id "users-list-section")) style display) "block")))))
+ (catch (lambda (error)
+ (ps:chain console (error "Error loading users:" error))
+ (alert "Error loading users. Please try again.")))))
+
+ ;; Show users table
+ (defun show-users-table (users)
+ (let ((container (ps:chain document (get-element-by-id "users-container")))
+ (users-html (ps:chain users
+ (map (lambda (user)
+ (+ "
"
+ "| " (ps:@ user username) " | "
+ "" (ps:@ user email) " | "
+ ""
+ ""
+ " | "
+ "" (if (ps:@ user active) "✅ Active" "❌ Inactive") " | "
+ "" (if (ps:getprop user "last-login")
+ (ps:chain (ps:new (-date (* (ps:getprop user "last-login") 1000))) (to-locale-string))
+ "Never") " | "
+ ""
+ (if (ps:@ user active)
+ (+ "")
+ (+ ""))
+ " | "
+ "
")))
+ (join ""))))
+ (setf (ps:@ container inner-h-t-m-l)
+ (+ ""
+ ""
+ ""
+ "| Username | "
+ "Email | "
+ "Role | "
+ "Status | "
+ "Last Login | "
+ "Actions | "
+ "
"
+ ""
+ ""
+ users-html
+ ""
+ "
"
+ ""))))
+
+ (defun hide-users-table ()
+ (setf (ps:@ (ps:chain document (get-element-by-id "users-list-section")) style display) "none"))
+
+ ;; Update user role
+ (defun update-user-role (user-id new-role)
+ (let ((form-data (ps:new (-form-data))))
+ (ps:chain form-data (append "role" new-role))
+
+ (ps:chain
+ (fetch (+ "/api/asteroid/users/" user-id "/role")
+ (ps:create :method "POST" :body form-data))
+ (then (lambda (response) (ps:chain response (json))))
+ (then (lambda (result)
+ (if (= (ps:@ result status) "success")
+ (progn
+ (load-user-stats)
+ (alert "User role updated successfully"))
+ (alert (+ "Error updating user role: " (ps:@ result message))))))
+ (catch (lambda (error)
+ (ps:chain console (error "Error updating user role:" error))
+ (alert "Error updating user role. Please try again."))))))
+
+ ;; Deactivate user
+ (defun deactivate-user (user-id)
+ (when (not (confirm "Are you sure you want to deactivate this user?"))
+ (return))
+
+ (ps:chain
+ (fetch (+ "/api/asteroid/users/" user-id "/deactivate")
+ (ps:create :method "POST"))
+ (then (lambda (response) (ps:chain response (json))))
+ (then (lambda (result)
+ (if (= (ps:@ result status) "success")
+ (progn
+ (load-users)
+ (load-user-stats)
+ (alert "User deactivated successfully"))
+ (alert (+ "Error deactivating user: " (ps:@ result message))))))
+ (catch (lambda (error)
+ (ps:chain console (error "Error deactivating user:" error))
+ (alert "Error deactivating user. Please try again.")))))
+
+ ;; Activate user
+ (defun activate-user (user-id)
+ (ps:chain
+ (fetch (+ "/api/asteroid/users/" user-id "/activate")
+ (ps:create :method "POST"))
+ (then (lambda (response) (ps:chain response (json))))
+ (then (lambda (result)
+ (if (= (ps:@ result status) "success")
+ (progn
+ (load-users)
+ (load-user-stats)
+ (alert "User activated successfully"))
+ (alert (+ "Error activating user: " (ps:@ result message))))))
+ (catch (lambda (error)
+ (ps:chain console (error "Error activating user:" error))
+ (alert "Error activating user. Please try again.")))))
+
+ ;; Toggle create user form
+ (defun toggle-create-user-form ()
+ (let ((form (ps:chain document (get-element-by-id "create-user-form"))))
+ (if (= (ps:@ form style display) "none")
+ (progn
+ (setf (ps:@ form style display) "block")
+ (setf (ps:@ (ps:chain document (get-element-by-id "new-username")) value) "")
+ (setf (ps:@ (ps:chain document (get-element-by-id "new-email")) value) "")
+ (setf (ps:@ (ps:chain document (get-element-by-id "new-password")) value) "")
+ (setf (ps:@ (ps:chain document (get-element-by-id "new-role")) value) "listener"))
+ (setf (ps:@ form style display) "none"))))
+
+ ;; Create new user
+ (defun create-new-user (event)
+ (ps:chain event (prevent-default))
+
+ (let ((username (ps:@ (ps:chain document (get-element-by-id "new-username")) value))
+ (email (ps:@ (ps:chain document (get-element-by-id "new-email")) value))
+ (password (ps:@ (ps:chain document (get-element-by-id "new-password")) value))
+ (role (ps:@ (ps:chain document (get-element-by-id "new-role")) value))
+ (form-data (ps:new (-form-data))))
+
+ (ps:chain form-data (append "username" username))
+ (ps:chain form-data (append "email" email))
+ (ps:chain form-data (append "password" password))
+ (ps:chain form-data (append "role" role))
+
+ (ps:chain
+ (fetch "/api/asteroid/users/create"
+ (ps:create :method "POST" :body form-data))
+ (then (lambda (response) (ps:chain response (json))))
+ (then (lambda (result)
+ (let ((data (or (ps:@ result data) result)))
+ (if (= (ps:@ data status) "success")
+ (progn
+ (alert (+ "User \"" username "\" created successfully!"))
+ (toggle-create-user-form)
+ (load-user-stats)
+ (load-users))
+ (alert (+ "Error creating user: " (or (ps:@ data message) (ps:@ result message))))))))
+ (catch (lambda (error)
+ (ps:chain console (error "Error creating user:" error))
+ (alert "Error creating user. Please try again."))))))
+
+ ;; Initialize on page load
+ (ps:chain document
+ (add-event-listener
+ "DOMContentLoaded"
+ load-user-stats))
+
+ ;; Update user stats every 30 seconds
+ (set-interval load-user-stats 30000)))
+ "Compiled JavaScript for users management - generated at load time")
+
+(defun generate-users-js ()
+ "Return the pre-compiled JavaScript for users page"
+ *users-js*)
diff --git a/static/js/users.js b/static/js/users.js.original
similarity index 100%
rename from static/js/users.js
rename to static/js/users.js.original