From 9e65e80276aeb33c0f917d005e621a18158fffee Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Tue, 2 Dec 2025 15:51:42 -0500 Subject: Fix Admin.py imports for bild local dep detection - Change 'from Biz.X import Y' to 'import Biz.X as X' style - bild only recognizes 'import X as Y' for local dep detection - Add setuptools to Python deps (required by newer nixpkgs) Amp-Thread-ID: https://ampcode.com/threads/T-fe8328a9-7709-4544-9d31-b099f04aa120 Co-authored-by: Amp --- Biz/PodcastItLater/Admin/__init__.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Biz/PodcastItLater/Admin/__init__.py (limited to 'Biz/PodcastItLater/Admin') 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.""" -- cgit v1.2.3 From 0baab1972e30c0e4629e67152838e660b02a2537 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Mon, 15 Dec 2025 08:47:02 -0500 Subject: t-265.6: Add feedback collection endpoint for PIL - Add feedback table with migration in Core.py - Add FeedbackForm and FeedbackPage UI components - Add /feedback GET/POST routes and /api/feedback JSON endpoint - Add admin feedback view at /admin/feedback - Create Omni/Agent/Tools/Feedback.hs with feedback_list tool - Wire feedback tool into Telegram agent --- Biz/PodcastItLater/Admin/Handlers.py | 28 ++++++++++++ Biz/PodcastItLater/Admin/Views.py | 89 ++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) (limited to 'Biz/PodcastItLater/Admin') 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..7834340 100644 --- a/Biz/PodcastItLater/Admin/Views.py +++ b/Biz/PodcastItLater/Admin/Views.py @@ -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=[], + ) -- cgit v1.2.3 From a7dcb30c7a465d9fce72b7fc3e605470b2b59814 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Tue, 16 Dec 2025 08:06:09 -0500 Subject: feat(deploy): Complete mini-PaaS deployment system (t-266) - Add Omni/Deploy/ with Manifest, Deployer, Systemd, Caddy modules - Manifest CLI: show, update, add-service, list, rollback commands - Deployer: polls S3 manifest, pulls closures, manages systemd units - Caddy integration for dynamic reverse proxy routes - bild: auto-cache to S3, outputs STORE_PATH for push.sh - push.sh: supports both NixOS and service deploys - Biz.nix: simplified to base OS + deployer only - Services (podcastitlater-web/worker) now deployer-managed - Documentation: README.md with operations guide --- Biz/PodcastItLater/Admin/Core.py | 95 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 Biz/PodcastItLater/Admin/Core.py (limited to 'Biz/PodcastItLater/Admin') 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) -- cgit v1.2.3 From 32c2bb198007ab85095c14be544cfca9d083a7cd Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Wed, 17 Dec 2025 11:00:29 -0500 Subject: Improve PodcastItLater homepage and dark mode support Homepage changes: - New marketing-focused landing page for logged-out users - Hero: 'Your Reading List, as a Podcast' with clear value prop - How It Works: 3-step process cards (paste URL, convert, listen) - Social proof: 'Join others' section with public feed - Login form moved below social proof for better conversion flow Dark mode fixes: - Remove bg-light from Current Plan card on /account page - Remove bg-white from metrics card headers on /admin/metrics Layout improvements: - Stack metrics tables vertically (full-width) instead of horizontal - Add spacing between metric cards for better readability --- Biz/PodcastItLater/Admin/Views.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'Biz/PodcastItLater/Admin') diff --git a/Biz/PodcastItLater/Admin/Views.py b/Biz/PodcastItLater/Admin/Views.py index 7834340..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"], ), -- cgit v1.2.3