summaryrefslogtreecommitdiff
path: root/Biz/PodcastItLater/Web.py
diff options
context:
space:
mode:
authorBen Sima <ben@bsima.me>2025-11-16 03:57:51 -0500
committerBen Sima <ben@bsima.me>2025-11-16 03:57:51 -0500
commit468ee3c4dc005a139ea2b8ac157c61d0ee4422d9 (patch)
tree3fa6b33194187237bfe19bce9b5eb86175925166 /Biz/PodcastItLater/Web.py
parentf74ee8bc380f07e597b638a719e7bbfe9461a031 (diff)
Add admin metrics dashboard
- Added Core.Database.get_metrics_summary() for aggregate stats - Added Core.Database.get_episode_metric_events() for raw event data - Created MetricsDashboard component with summary cards and top episodes tables - Added /admin/metrics route with admin authentication - Added metrics link to admin dropdown menu - Added comprehensive tests for metrics functionality - Fixed type errors in Admin.py by adding MetricCardAttrs - All tests passing (48 tests total in Web.py) - Completed epic t-ga8V8O (24/24 tasks)
Diffstat (limited to 'Biz/PodcastItLater/Web.py')
-rw-r--r--Biz/PodcastItLater/Web.py132
1 files changed, 126 insertions, 6 deletions
diff --git a/Biz/PodcastItLater/Web.py b/Biz/PodcastItLater/Web.py
index b41f31d..60818e9 100644
--- a/Biz/PodcastItLater/Web.py
+++ b/Biz/PodcastItLater/Web.py
@@ -1611,6 +1611,7 @@ def cancel_queue_item(request: Request, job_id: int) -> Response:
app.delete("/queue/{job_id}")(Admin.delete_queue_item)
app.get("/admin/users")(Admin.admin_users)
+app.get("/admin/metrics")(Admin.admin_metrics)
app.post("/admin/users/{user_id}/status")(Admin.update_user_status)
app.post("/admin/episode/{episode_id}/toggle-public")(
Admin.toggle_episode_public,
@@ -2145,6 +2146,124 @@ class TestAdminInterface(BaseWebTest):
self.assertIn("PROCESSING: 1", response.text)
+class TestMetricsDashboard(BaseWebTest):
+ """Test metrics dashboard functionality."""
+
+ def setUp(self) -> None:
+ """Set up test client with logged-in admin user."""
+ super().setUp()
+
+ # Create and login admin user
+ self.user_id, _ = Core.Database.create_user(
+ "ben@bensima.com",
+ )
+ Core.Database.update_user_status(
+ self.user_id,
+ "active",
+ )
+ self.client.post("/login", data={"email": "ben@bensima.com"})
+
+ def test_metrics_page_requires_admin(self) -> None:
+ """Verify non-admin users cannot access metrics."""
+ # Create non-admin user
+ user_id, _ = Core.Database.create_user("user@example.com")
+ Core.Database.update_user_status(user_id, "active")
+
+ # Login as non-admin
+ self.client.get("/logout")
+ self.client.post("/login", data={"email": "user@example.com"})
+
+ # Try to access metrics
+ response = self.client.get("/admin/metrics")
+
+ # Should redirect
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.headers["Location"], "/?error=forbidden")
+
+ def test_metrics_page_requires_login(self) -> None:
+ """Verify unauthenticated users are redirected."""
+ self.client.get("/logout")
+
+ response = self.client.get("/admin/metrics")
+
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.headers["Location"], "/")
+
+ def test_metrics_displays_summary(self) -> None:
+ """Verify metrics summary is displayed."""
+ # Create test episode
+ episode_id = Core.Database.create_episode(
+ title="Test Episode",
+ audio_url="http://example.com/audio.mp3",
+ content_length=1000,
+ duration=300,
+ )
+ Core.Database.add_episode_to_user(self.user_id, episode_id)
+
+ # Track some events
+ Core.Database.track_episode_metric(episode_id, "played")
+ Core.Database.track_episode_metric(episode_id, "played")
+ Core.Database.track_episode_metric(episode_id, "downloaded")
+ Core.Database.track_episode_metric(episode_id, "added", self.user_id)
+
+ # Get metrics page
+ response = self.client.get("/admin/metrics")
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("Episode Metrics", response.text)
+ self.assertIn("Total Episodes", response.text)
+ self.assertIn("Total Plays", response.text)
+ self.assertIn("Total Downloads", response.text)
+ self.assertIn("Total Adds", response.text)
+
+ def test_metrics_shows_top_episodes(self) -> None:
+ """Verify top episodes tables are displayed."""
+ # Create test episodes
+ episode1 = Core.Database.create_episode(
+ title="Popular Episode",
+ audio_url="http://example.com/popular.mp3",
+ content_length=1000,
+ duration=300,
+ author="Test Author",
+ )
+ Core.Database.add_episode_to_user(self.user_id, episode1)
+
+ episode2 = Core.Database.create_episode(
+ title="Less Popular Episode",
+ audio_url="http://example.com/less.mp3",
+ content_length=1000,
+ duration=300,
+ )
+ Core.Database.add_episode_to_user(self.user_id, episode2)
+
+ # Track events - more for episode1
+ for _ in range(5):
+ Core.Database.track_episode_metric(episode1, "played")
+ for _ in range(2):
+ Core.Database.track_episode_metric(episode2, "played")
+
+ for _ in range(3):
+ Core.Database.track_episode_metric(episode1, "downloaded")
+ Core.Database.track_episode_metric(episode2, "downloaded")
+
+ # Get metrics page
+ response = self.client.get("/admin/metrics")
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("Most Played", response.text)
+ self.assertIn("Most Downloaded", response.text)
+ self.assertIn("Popular Episode", response.text)
+
+ def test_metrics_empty_state(self) -> None:
+ """Verify metrics page works with no data."""
+ response = self.client.get("/admin/metrics")
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("Episode Metrics", response.text)
+ # Should show 0 for counts
+ self.assertIn("Total Episodes", response.text)
+
+
class TestJobCancellation(BaseWebTest):
"""Test job cancellation functionality."""
@@ -2499,10 +2618,11 @@ class TestEpisodeDeduplication(BaseWebTest):
for url in similar_urls:
url_hash = Core.hash_url(url)
- episode = Core.Database.find_episode_by_url_hash(url_hash)
+ episode = Core.Database.get_episode_by_url_hash(url_hash)
self.assertIsNotNone(episode)
- self.assertEqual(episode["id"], self.episode_id) # type: ignore[index]
+ if episode is not None:
+ self.assertEqual(episode["id"], self.episode_id)
def test_add_existing_episode_to_user_feed(self) -> None:
"""Should add existing episode to new user's feed."""
@@ -2551,7 +2671,7 @@ class TestMetricsTracking(BaseWebTest):
)
# Verify metric was recorded
- metrics = Core.Database.get_episode_metrics(self.episode_id)
+ metrics = Core.Database.get_episode_metric_events(self.episode_id)
self.assertEqual(len(metrics), 1)
self.assertEqual(metrics[0]["event_type"], "added")
self.assertEqual(metrics[0]["user_id"], self.user_id)
@@ -2564,7 +2684,7 @@ class TestMetricsTracking(BaseWebTest):
self.user_id,
)
- metrics = Core.Database.get_episode_metrics(self.episode_id)
+ metrics = Core.Database.get_episode_metric_events(self.episode_id)
self.assertEqual(len(metrics), 1)
self.assertEqual(metrics[0]["event_type"], "played")
@@ -2576,7 +2696,7 @@ class TestMetricsTracking(BaseWebTest):
user_id=None,
)
- metrics = Core.Database.get_episode_metrics(self.episode_id)
+ metrics = Core.Database.get_episode_metric_events(self.episode_id)
self.assertEqual(len(metrics), 1)
self.assertEqual(metrics[0]["event_type"], "played")
self.assertIsNone(metrics[0]["user_id"])
@@ -2594,7 +2714,7 @@ class TestMetricsTracking(BaseWebTest):
self.assertEqual(response.status_code, 200)
# Verify metric was recorded
- metrics = Core.Database.get_episode_metrics(self.episode_id)
+ metrics = Core.Database.get_episode_metric_events(self.episode_id)
played_metrics = [m for m in metrics if m["event_type"] == "played"]
self.assertGreater(len(played_metrics), 0)