diff --git a/asteroid.lisp b/asteroid.lisp index 6329be5..405fd43 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -1281,6 +1281,18 @@ ("total_minutes" . ,(third row)))) stats)))))) +(define-api asteroid/stats/geo/cities (country &optional (days "7")) () + "Get city breakdown for a specific country (admin only)" + (require-role :admin) + (let ((stats (get-geo-stats-by-city country (parse-integer days :junk-allowed t)))) + (api-output `(("status" . "success") + ("country" . ,country) + ("cities" . ,(mapcar (lambda (row) + `(("city" . ,(or (first row) "Unknown")) + ("listeners" . ,(second row)) + ("minutes" . ,(third row)))) + stats)))))) + ;; RADIANCE server management functions (defun start-server (&key (port *server-port*)) diff --git a/listener-stats.lisp b/listener-stats.lisp index 833e4ba..da6bc67 100644 --- a/listener-stats.lisp +++ b/listener-stats.lisp @@ -382,6 +382,22 @@ (log:error "Failed to get geo stats: ~a" e) nil))) +(defun get-geo-stats-by-city (country-code &optional (days 7)) + "Get city breakdown for a specific country for the last N days" + (handler-case + (with-db + (postmodern:query + (format nil "SELECT city, SUM(listener_count) as total_listeners, SUM(listen_minutes) as total_minutes + FROM listener_geo_stats + WHERE date > NOW() - INTERVAL '~a days' + AND country_code = '~a' + GROUP BY city + ORDER BY total_listeners DESC + LIMIT 10" days country-code))) + (error (e) + (log:error "Failed to get city stats for ~a: ~a" country-code e) + nil))) + (defun get-user-listening-stats (user-id) "Get listening statistics for a specific user" (handler-case diff --git a/template/admin.ctml b/template/admin.ctml index 2c078c5..0b8a27d 100644 --- a/template/admin.ctml +++ b/template/admin.ctml @@ -336,6 +336,9 @@ return String.fromCodePoint(...codePoints); } + // Track expanded countries + const expandedCountries = new Set(); + // Fetch and display geo stats function refreshGeoStats() { fetch('/api/asteroid/stats/geo?days=7') @@ -349,12 +352,19 @@ const country = item.country_code || item[0]; const listeners = item.total_listeners || item[1] || 0; const minutes = item.total_minutes || item[2] || 0; - return `
| └ ${city.city} | +${city.listeners} | +${city.minutes} | +