summaryrefslogtreecommitdiff
path: root/Biz/PodcastItLater/Admin
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-12-17 13:29:40 -0500
committerBen Sima <ben@bensima.com>2025-12-17 13:29:40 -0500
commitab01b34bf563990e0f491ada646472aaade97610 (patch)
tree5e46a1a157bb846b0c3a090a83153c788da2b977 /Biz/PodcastItLater/Admin
parente112d3ce07fa24f31a281e521a554cc881a76c7b (diff)
parent337648981cc5a55935116141341521f4fce83214 (diff)
Merge Ava deployment changes
Diffstat (limited to 'Biz/PodcastItLater/Admin')
-rw-r--r--Biz/PodcastItLater/Admin/Core.py95
-rw-r--r--Biz/PodcastItLater/Admin/Handlers.py28
-rw-r--r--Biz/PodcastItLater/Admin/Views.py107
-rw-r--r--Biz/PodcastItLater/Admin/__init__.py1
4 files changed, 221 insertions, 10 deletions
diff --git a/Biz/PodcastItLater/Admin/Core.py b/Biz/PodcastItLater/Admin/Core.py
new file mode 100644
index 0000000..10ea7f6
--- /dev/null
+++ b/Biz/PodcastItLater/Admin/Core.py
@@ -0,0 +1,95 @@
+"""
+PodcastItLater Admin Interface.
+
+Admin pages and functionality for managing users and queue items.
+"""
+
+# : out podcastitlater-admin
+# : dep ludic
+# : dep httpx
+# : dep starlette
+# : dep pytest
+# : dep pytest-asyncio
+# : dep pytest-mock
+
+import Biz.PodcastItLater.Admin.Handlers as Handlers
+import Biz.PodcastItLater.Admin.Views as Views
+import sys
+
+# Re-export all symbols for backward compatibility
+ActionButtons = Views.ActionButtons
+ActionButtonsAttrs = Views.ActionButtonsAttrs
+AdminUsers = Views.AdminUsers
+AdminUsersAttrs = Views.AdminUsersAttrs
+AdminView = Views.AdminView
+AdminViewAttrs = Views.AdminViewAttrs
+EpisodeTableRow = Views.EpisodeTableRow
+EpisodeTableRowAttrs = Views.EpisodeTableRowAttrs
+MetricCard = Views.MetricCard
+MetricCardAttrs = Views.MetricCardAttrs
+MetricsAttrs = Views.MetricsAttrs
+MetricsDashboard = Views.MetricsDashboard
+QueueTableRow = Views.QueueTableRow
+QueueTableRowAttrs = Views.QueueTableRowAttrs
+StatusBadge = Views.StatusBadge
+StatusBadgeAttrs = Views.StatusBadgeAttrs
+TopEpisodesTable = Views.TopEpisodesTable
+TopEpisodesTableAttrs = Views.TopEpisodesTableAttrs
+TruncatedText = Views.TruncatedText
+TruncatedTextAttrs = Views.TruncatedTextAttrs
+UserTableRow = Views.UserTableRow
+UserTableRowAttrs = Views.UserTableRowAttrs
+create_table_header = Views.create_table_header
+AdminFeedback = Views.AdminFeedback
+AdminFeedbackAttrs = Views.AdminFeedbackAttrs
+
+admin_feedback = Handlers.admin_feedback
+admin_metrics = Handlers.admin_metrics
+admin_queue_status = Handlers.admin_queue_status
+admin_users = Handlers.admin_users
+delete_queue_item = Handlers.delete_queue_item
+retry_queue_item = Handlers.retry_queue_item
+toggle_episode_public = Handlers.toggle_episode_public
+update_user_status = Handlers.update_user_status
+
+__all__ = [
+ "ActionButtons",
+ "ActionButtonsAttrs",
+ "AdminFeedback",
+ "AdminFeedbackAttrs",
+ "AdminUsers",
+ "AdminUsersAttrs",
+ "AdminView",
+ "AdminViewAttrs",
+ "EpisodeTableRow",
+ "EpisodeTableRowAttrs",
+ "MetricCard",
+ "MetricCardAttrs",
+ "MetricsAttrs",
+ "MetricsDashboard",
+ "QueueTableRow",
+ "QueueTableRowAttrs",
+ "StatusBadge",
+ "StatusBadgeAttrs",
+ "TopEpisodesTable",
+ "TopEpisodesTableAttrs",
+ "TruncatedText",
+ "TruncatedTextAttrs",
+ "UserTableRow",
+ "UserTableRowAttrs",
+ "admin_feedback",
+ "admin_metrics",
+ "admin_queue_status",
+ "admin_users",
+ "create_table_header",
+ "delete_queue_item",
+ "retry_queue_item",
+ "toggle_episode_public",
+ "update_user_status",
+]
+
+
+def main() -> None:
+ """Admin tests are currently in Web."""
+ if "test" in sys.argv:
+ sys.exit(0)
diff --git a/Biz/PodcastItLater/Admin/Handlers.py b/Biz/PodcastItLater/Admin/Handlers.py
index b98c551..9e06fe6 100644
--- a/Biz/PodcastItLater/Admin/Handlers.py
+++ b/Biz/PodcastItLater/Admin/Handlers.py
@@ -296,3 +296,31 @@ def admin_metrics(request: Request) -> Views.MetricsDashboard | Response:
metrics = Core.Database.get_metrics_summary()
return Views.MetricsDashboard(metrics=metrics, user=user)
+
+
+def admin_feedback(request: Request) -> Views.AdminFeedback | Response:
+ """Admin feedback view."""
+ # Check if user is logged in and is admin
+ user_id = request.session.get("user_id")
+ if not user_id:
+ return Response(
+ "",
+ status_code=302,
+ headers={"Location": "/"},
+ )
+
+ user = Core.Database.get_user_by_id(
+ user_id,
+ )
+ if not user or not Core.is_admin(user):
+ return Response(
+ "",
+ status_code=302,
+ headers={"Location": "/?error=forbidden"},
+ )
+
+ # Get feedback data
+ feedback = Core.Database.get_feedback(limit=100)
+ count = Core.Database.get_feedback_count()
+
+ return Views.AdminFeedback(feedback=feedback, count=count, user=user)
diff --git a/Biz/PodcastItLater/Admin/Views.py b/Biz/PodcastItLater/Admin/Views.py
index 7cc71a5..057c5e0 100644
--- a/Biz/PodcastItLater/Admin/Views.py
+++ b/Biz/PodcastItLater/Admin/Views.py
@@ -263,16 +263,16 @@ class MetricsDashboard(Component[AnyChildren, MetricsAttrs]):
"Most Played",
classes=["card-title", "mb-0"],
),
- classes=["card-header", "bg-white"],
+ classes=["card-header"],
),
TopEpisodesTable(
episodes=metrics["most_played"],
metric_name="Plays",
count_key="play_count",
),
- classes=["card", "shadow-sm"],
+ classes=["card", "shadow-sm", "mb-3"],
),
- classes=["col-lg-4"],
+ classes=["col-12"],
),
html.div(
html.div(
@@ -288,16 +288,16 @@ class MetricsDashboard(Component[AnyChildren, MetricsAttrs]):
"Most Downloaded",
classes=["card-title", "mb-0"],
),
- classes=["card-header", "bg-white"],
+ classes=["card-header"],
),
TopEpisodesTable(
episodes=metrics["most_downloaded"],
metric_name="Downloads",
count_key="download_count",
),
- classes=["card", "shadow-sm"],
+ classes=["card", "shadow-sm", "mb-3"],
),
- classes=["col-lg-4"],
+ classes=["col-12"],
),
html.div(
html.div(
@@ -313,16 +313,16 @@ class MetricsDashboard(Component[AnyChildren, MetricsAttrs]):
"Most Added to Feeds",
classes=["card-title", "mb-0"],
),
- classes=["card-header", "bg-white"],
+ classes=["card-header"],
),
TopEpisodesTable(
episodes=metrics["most_added"],
metric_name="Adds",
count_key="add_count",
),
- classes=["card", "shadow-sm"],
+ classes=["card", "shadow-sm", "mb-3"],
),
- classes=["col-lg-4"],
+ classes=["col-12"],
),
classes=["row", "g-3"],
),
@@ -742,3 +742,92 @@ class AdminView(Component[AnyChildren, AdminViewAttrs]):
),
),
)
+
+
+class AdminFeedbackAttrs(Attrs):
+ """Attributes for AdminFeedback component."""
+
+ feedback: list[dict[str, typing.Any]]
+ count: int
+ user: dict[str, typing.Any]
+
+
+class AdminFeedback(Component[AnyChildren, AdminFeedbackAttrs]):
+ """Admin feedback listing page."""
+
+ @override
+ def render(self) -> UI.PageLayout:
+ feedback = self.attrs["feedback"]
+ count = self.attrs["count"]
+ user = self.attrs["user"]
+
+ rows = [
+ html.tr(
+ html.td(html.small(fb["id"][:8], classes=["text-muted"])),
+ html.td(fb.get("email") or "-"),
+ html.td(
+ html.span(
+ fb.get("source") or "-",
+ classes=["badge", "bg-secondary"],
+ ),
+ ),
+ html.td(str(fb.get("rating") or "-")),
+ html.td(
+ TruncatedText(
+ text=fb.get("use_case") or "-",
+ max_length=50,
+ ),
+ ),
+ html.td(
+ TruncatedText(
+ text=fb.get("feedback_text") or "-",
+ max_length=50,
+ ),
+ ),
+ html.td(
+ html.small(
+ fb["created_at"],
+ classes=["text-muted"],
+ ),
+ ),
+ )
+ for fb in feedback
+ ]
+
+ return UI.PageLayout(
+ html.div(
+ html.h2(
+ html.i(classes=["bi", "bi-chat-heart-fill", "me-2"]),
+ f"Feedback ({count})",
+ classes=["mb-4"],
+ ),
+ html.div(
+ html.table(
+ create_table_header([
+ "ID",
+ "Email",
+ "Source",
+ "Rating",
+ "Use Case",
+ "Feedback",
+ "Created",
+ ]),
+ html.tbody(*rows),
+ classes=["table", "table-hover", "table-sm"],
+ ),
+ classes=["table-responsive"],
+ )
+ if rows
+ else html.div(
+ html.p(
+ "No feedback yet.",
+ classes=["text-muted", "text-center", "py-5"],
+ ),
+ ),
+ ),
+ user=user,
+ current_page="admin",
+ page_title="Admin Feedback - PodcastItLater",
+ error=None,
+ meta_tags=[],
+ )
diff --git a/Biz/PodcastItLater/Admin/__init__.py b/Biz/PodcastItLater/Admin/__init__.py
deleted file mode 100644
index 04e3e32..0000000
--- a/Biz/PodcastItLater/Admin/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""PodcastItLater Admin package."""