From 3b107ebe47556124702240fd934129ed1ec04375 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Fri, 5 Sep 2025 13:03:06 -0400 Subject: Refactor Admin view components and improve code organization --- Biz/PodcastItLater/Admin.py | 1565 +++++++++++++------------------------------ 1 file changed, 481 insertions(+), 1084 deletions(-) (limited to 'Biz/PodcastItLater') diff --git a/Biz/PodcastItLater/Admin.py b/Biz/PodcastItLater/Admin.py index 29e04d9..32732d1 100644 --- a/Biz/PodcastItLater/Admin.py +++ b/Biz/PodcastItLater/Admin.py @@ -38,6 +38,335 @@ class AdminUsersAttrs(Attrs): users: list[dict[str, typing.Any]] +class StatusBadgeAttrs(Attrs): + """Attributes for StatusBadge component.""" + + status: str + count: int | None + + +class StatusBadge(Component[AnyChildren, StatusBadgeAttrs]): + """Display a status badge with optional count.""" + + @override + def render(self) -> html.span: + status = self.attrs["status"] + count = self.attrs.get("count", None) + + text = f"{status.upper()}: {count}" if count is not None else status + + return html.span( + text, + style={ + "margin-right": "20px" if count is not None else "0", + "padding": "5px 10px", + "background": self.get_status_color(status), + "color": "white", + "border-radius": "4px", + "font-weight": "bold" if count is None else "normal", + }, + ) + + @staticmethod + def get_status_color(status: str) -> str: + """Get color for status display.""" + return { + "pending": "#ffa500", + "processing": "#007cba", + "completed": "#28a745", + "active": "#28a745", + "error": "#dc3545", + "cancelled": "#6c757d", + "disabled": "#dc3545", + }.get(status, "#6c757d") + + +class TruncatedTextAttrs(Attrs): + """Attributes for TruncatedText component.""" + + text: str + max_length: int + max_width: str + + +class TruncatedText(Component[AnyChildren, TruncatedTextAttrs]): + """Display truncated text with tooltip.""" + + @override + def render(self) -> html.div: + text = self.attrs["text"] + max_length = self.attrs["max_length"] + max_width = self.attrs.get("max_width", "200px") + + truncated = ( + text[:max_length] + "..." if len(text) > max_length else text + ) + + return html.div( + truncated, + title=text, + style={ + "max-width": max_width, + "overflow": "hidden", + "text-overflow": "ellipsis", + }, + ) + + +class ActionButtonsAttrs(Attrs): + """Attributes for ActionButtons component.""" + + job_id: int + status: str + + +class ActionButtons(Component[AnyChildren, ActionButtonsAttrs]): + """Render action buttons for queue items.""" + + @override + def render(self) -> html.div: + job_id = self.attrs["job_id"] + status = self.attrs["status"] + + buttons = [] + + if status != "completed": + buttons.append( + html.button( + "Retry", + hx_post=f"/queue/{job_id}/retry", + hx_target="body", + hx_swap="outerHTML", + style={ + "margin-right": "5px", + "padding": "5px 10px", + "background": "#28a745", + "color": "white", + "border": "none", + "cursor": "pointer", + "border-radius": "3px", + }, + disabled=status == "completed", + ), + ) + + buttons.append( + html.button( + "Delete", + hx_delete=f"/queue/{job_id}", + hx_confirm="Are you sure you want to delete this queue item?", + hx_target="body", + hx_swap="outerHTML", + style={ + "padding": "5px 10px", + "background": "#dc3545", + "color": "white", + "border": "none", + "cursor": "pointer", + "border-radius": "3px", + }, + ), + ) + + return html.div( + *buttons, + style={"display": "flex", "gap": "5px"}, + ) + + +class QueueTableRowAttrs(Attrs): + """Attributes for QueueTableRow component.""" + + item: dict[str, typing.Any] + + +class QueueTableRow(Component[AnyChildren, QueueTableRowAttrs]): + """Render a single queue table row.""" + + @override + def render(self) -> html.tr: + item = self.attrs["item"] + + return html.tr( + html.td(str(item["id"]), style={"padding": "10px"}), + html.td( + TruncatedText( + text=item["url"], + max_length=Core.TITLE_TRUNCATE_LENGTH, + max_width="300px", + ), + style={"padding": "10px"}, + ), + html.td( + TruncatedText( + text=item.get("title") or "-", + max_length=Core.TITLE_TRUNCATE_LENGTH, + ), + style={"padding": "10px"}, + ), + html.td( + item["email"] or "-", + style={"padding": "10px"}, + ), + html.td( + StatusBadge(status=item["status"]), + style={"padding": "10px"}, + ), + html.td( + str(item.get("retry_count", 0)), + style={"padding": "10px"}, + ), + html.td( + item["created_at"], + style={"padding": "10px"}, + ), + html.td( + TruncatedText( + text=item["error_message"] or "-", + max_length=Core.ERROR_TRUNCATE_LENGTH, + ) + if item["error_message"] + else "-", + style={"padding": "10px"}, + ), + html.td( + ActionButtons(job_id=item["id"], status=item["status"]), + style={"padding": "10px"}, + ), + ) + + +class EpisodeTableRowAttrs(Attrs): + """Attributes for EpisodeTableRow component.""" + + episode: dict[str, typing.Any] + + +class EpisodeTableRow(Component[AnyChildren, EpisodeTableRowAttrs]): + """Render a single episode table row.""" + + @override + def render(self) -> html.tr: + episode = self.attrs["episode"] + + return html.tr( + html.td(str(episode["id"]), style={"padding": "10px"}), + html.td( + TruncatedText( + text=episode["title"], + max_length=Core.TITLE_TRUNCATE_LENGTH, + ), + style={"padding": "10px"}, + ), + html.td( + html.a( + "Listen", + href=episode["audio_url"], + target="_blank", + style={"color": "#007cba"}, + ), + style={"padding": "10px"}, + ), + html.td( + f"{episode['duration']}s" if episode["duration"] else "-", + style={"padding": "10px"}, + ), + html.td( + f"{episode['content_length']:,} chars" + if episode["content_length"] + else "-", + style={"padding": "10px"}, + ), + html.td( + episode["created_at"], + style={"padding": "10px"}, + ), + ) + + +class UserTableRowAttrs(Attrs): + """Attributes for UserTableRow component.""" + + user: dict[str, typing.Any] + + +class UserTableRow(Component[AnyChildren, UserTableRowAttrs]): + """Render a single user table row.""" + + @override + def render(self) -> html.tr: + user = self.attrs["user"] + + return html.tr( + html.td(user["email"], style={"padding": "10px"}), + html.td(user["created_at"], style={"padding": "10px"}), + html.td( + StatusBadge(status=user.get("status", "pending")), + style={"padding": "10px"}, + ), + html.td( + html.select( + html.option( + "Pending", + value="pending", + selected=user.get("status") == "pending", + ), + html.option( + "Active", + value="active", + selected=user.get("status") == "active", + ), + html.option( + "Disabled", + value="disabled", + selected=user.get("status") == "disabled", + ), + name="status", + hx_post=f"/admin/users/{user['id']}/status", + hx_trigger="change", + hx_target="body", + hx_swap="outerHTML", + style={ + "padding": "5px", + "border": "1px solid #ddd", + "border-radius": "3px", + }, + ), + style={"padding": "10px"}, + ), + ) + + +def create_table_header(columns: list[str]) -> html.thead: + """Create a table header with given column names.""" + return html.thead( + html.tr(*[ + html.th( + col, + style={"padding": "10px", "text-align": "left"}, + ) + for col in columns + ]), + ) + + +def create_admin_styles() -> html.style: + """Create common admin page styles.""" + return html.style(""" + body { + font-family: Arial, sans-serif; + max-width: 1200px; + margin: 0 auto; + padding: 20px; + } + h1, h2 { color: #333; } + table { background: white; } + thead { background: #f8f9fa; } + tbody tr:nth-child(even) { background: #f8f9fa; } + tbody tr:hover { background: #e9ecef; } + """) + + class AdminUsers(Component[AnyChildren, AdminUsersAttrs]): """Admin view for managing users.""" @@ -64,156 +393,8 @@ class AdminUsers(Component[AnyChildren, AdminUsersAttrs]): ), style={"margin-bottom": "20px"}, ), - # Users Table - html.div( - html.h2("All Users"), - html.div( - html.table( - html.thead( - html.tr( - html.th( - "Email", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Created At", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Status", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Actions", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - ), - ), - html.tbody( - *[ - html.tr( - html.td( - user["email"], - style={ - "padding": "10px", - }, - ), - html.td( - user["created_at"], - style={ - "padding": "10px", - }, - ), - html.td( - html.span( - user.get( - "status", - "pending", - ).upper(), - style={ - "color": ( - AdminUsers.get_status_color( - user.get( - "status", - "pending", - ), - ) - ), - "font-weight": ( - "bold" - ), - }, - ), - style={ - "padding": "10px", - }, - ), - html.td( - html.select( - html.option( - "Pending", - value="pending", - selected=user.get( - "status", - ) - == "pending", - ), - html.option( - "Active", - value="active", - selected=user.get( - "status", - ) - == "active", - ), - html.option( - "Disabled", - value="disabled", - selected=user.get( - "status", - ) - == "disabled", - ), - name="status", - hx_post=f"/admin/users/{user['id']}/status", - hx_trigger="change", - hx_target="body", - hx_swap="outerHTML", - style={ - "padding": ( - "5px" - ), - "border": ( - "1px solid " - "#ddd" - ), - "border-radius": "3px", # noqa: E501 - }, - ), - style={ - "padding": "10px", - }, - ), - ) - for user in users - ], - ), - style={ - "width": "100%", - "border-collapse": "collapse", - "border": "1px solid #ddd", - }, - ), - style={ - "overflow-x": "auto", - }, - ), - ), - html.style(""" - body { - font-family: Arial, sans-serif; - max-width: 1200px; - margin: 0 auto; - padding: 20px; - } - h1, h2 { color: #333; } - table { background: white; } - thead { background: #f8f9fa; } - tbody tr:nth-child(even) { background: #f8f9fa; } - tbody tr:hover { background: #e9ecef; } - """), + self._render_users_table(users), + create_admin_styles(), ), id="admin-users-content", ), @@ -223,13 +404,30 @@ class AdminUsers(Component[AnyChildren, AdminUsersAttrs]): ) @staticmethod - def get_status_color(status: str) -> str: - """Get color for status display.""" - return { - "pending": "#ffa500", - "active": "#28a745", - "disabled": "#dc3545", - }.get(status, "#6c757d") + def _render_users_table( + users: list[dict[str, typing.Any]], + ) -> html.div: + """Render users table.""" + return html.div( + html.h2("All Users"), + html.div( + html.table( + create_table_header([ + "Email", + "Created At", + "Status", + "Actions", + ]), + html.tbody(*[UserTableRow(user=user) for user in users]), + style={ + "width": "100%", + "border-collapse": "collapse", + "border": "1px solid #ddd", + }, + ), + style={"overflow-x": "auto"}, + ), + ) class AdminViewAttrs(Attrs): @@ -258,522 +456,10 @@ class AdminView(Component[AnyChildren, AdminViewAttrs]): pages.Body( layouts.Center( html.div( - layouts.Stack( - html.h1("PodcastItLater Admin - Queue Status"), - html.div( - html.a( - "← Back to Home", - href="/", - style={"color": "#007cba"}, - ), - html.a( - "Manage Users", - href="/admin/users", - style={ - "color": "#007cba", - "margin-left": "15px", - }, - ), - style={"margin-bottom": "20px"}, - ), - # Status Summary - html.div( - html.h2("Status Summary"), - html.div( - *[ - html.span( - f"{status.upper()}: {count}", - style={ - "margin-right": "20px", - "padding": "5px 10px", - "background": ( - AdminView.get_status_color( - status, - ) - ), - "color": "white", - "border-radius": "4px", - }, - ) - for status, count in ( - status_counts.items() - ) - ], - style={"margin-bottom": "20px"}, - ), - ), - # Queue Items Table - html.div( - html.h2("Queue Items"), - html.div( - html.table( - html.thead( - html.tr( - html.th( - "ID", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "URL", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Title", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Email", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Status", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Retries", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Created", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Error", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Actions", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - ), - ), - html.tbody( - *[ - html.tr( - html.td( - str(item["id"]), - style={ - "padding": "10px", - }, - ), - html.td( - html.div( - item["url"][ - : Core.TITLE_TRUNCATE_LENGTH # noqa: E501 - ] - + ( - "..." - if ( - len( - item[ - "url" - ], - ) - > Core.TITLE_TRUNCATE_LENGTH # noqa: E501 - ) - else "" - ), - title=item["url"], - style={ - "max-width": ( - "300px" - ), - "overflow": ( - "hidden" - ), - "text-overflow": ( # noqa: E501 - "ellipsis" - ), - }, - ), - style={ - "padding": "10px", - }, - ), - html.td( - html.div( - ( - item.get( - "title", - ) - or "-" - )[ - : Core.TITLE_TRUNCATE_LENGTH # noqa: E501 - ] - + ( - "..." - if item.get( - "title", - ) - and len( - item[ - "title" - ], - ) - > ( - Core.TITLE_TRUNCATE_LENGTH - ) - else "" - ), - title=item.get( - "title", - "", - ), - style={ - "max-width": ( - "200px" - ), - "overflow": ( - "hidden" - ), - "text-overflow": ( # noqa: E501 - "ellipsis" - ), - }, - ), - style={ - "padding": "10px", - }, - ), - html.td( - item["email"] or "-", - style={ - "padding": "10px", - }, - ), - html.td( - html.span( - item["status"], - style={ - "color": ( - AdminView.get_status_color( - item[ - "status" - ], - ) - ), - }, - ), - style={ - "padding": "10px", - }, - ), - html.td( - str( - item.get( - "retry_count", - 0, - ), - ), - style={ - "padding": "10px", - }, - ), - html.td( - item["created_at"], - style={ - "padding": "10px", - }, - ), - html.td( - html.div( - item[ - "error_message" - ][ - : Core.ERROR_TRUNCATE_LENGTH # noqa: E501 - ] - + "..." - if item[ - "error_message" - ] - and len( - item[ - "error_message" - ], - ) - > ( - Core.ERROR_TRUNCATE_LENGTH - ) - else item[ - "error_message" - ] - or "-", - title=item[ - "error_message" - ] - or "", - style={ - "max-width": ( - "200px" - ), - "overflow": ( - "hidden" - ), - "text-overflow": "ellipsis", # noqa: E501 - }, - ), - style={ - "padding": "10px", - }, - ), - html.td( - html.div( - html.button( - "Retry", - hx_post=f"/queue/{item['id']}/retry", - hx_target="body", - hx_swap="outerHTML", - style={ - "margin-right": "5px", # noqa: E501 - "padding": "5px 10px", # noqa: E501 - "background": "#28a745", # noqa: E501 - "color": ( - "white" - ), - "border": ( - "none" - ), - "cursor": ( - "pointer" - ), - "border-radius": "3px", # noqa: E501 - }, - disabled=item[ - "status" - ] - == "completed", - ) - if item["status"] - != "completed" - else "", - html.button( - "Delete", - hx_delete=f"/queue/{item['id']}", - hx_confirm=( - "Are you sure you " # noqa: E501 - "want to delete " # noqa: E501 - "this queue item?" # noqa: E501 - ), - hx_target="body", - hx_swap="outerHTML", - style={ - "padding": "5px 10px", # noqa: E501 - "background": "#dc3545", # noqa: E501 - "color": ( - "white" - ), - "border": ( - "none" - ), - "cursor": ( - "pointer" - ), - "border-radius": "3px", # noqa: E501 - }, - ), - style={ - "display": ( - "flex" - ), - "gap": "5px", - }, - ), - style={ - "padding": "10px", - }, - ), - ) - for item in queue_items - ], - ), - style={ - "width": "100%", - "border-collapse": "collapse", - "border": "1px solid #ddd", - }, - ), - style={ - "overflow-x": "auto", - "margin-bottom": "30px", - }, - ), - ), - # Episodes Table - html.div( - html.h2("Completed Episodes"), - html.div( - html.table( - html.thead( - html.tr( - html.th( - "ID", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Title", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Audio URL", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Duration", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Content Length", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Created", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - ), - ), - html.tbody( - *[ - html.tr( - html.td( - str(episode["id"]), - style={ - "padding": "10px", - }, - ), - html.td( - episode["title"][ - : Core.TITLE_TRUNCATE_LENGTH # noqa: E501 - ] - + ( - "..." - if len( - episode[ - "title" - ], - ) - > ( - Core.TITLE_TRUNCATE_LENGTH - ) - else "" - ), - style={ - "padding": "10px", - }, - ), - html.td( - html.a( - "Listen", - href=episode[ - "audio_url" - ], - target="_blank", - style={ - "color": ( - "#007cba" - ), - }, - ), - style={ - "padding": "10px", - }, - ), - html.td( - f"{episode['duration']}s" - if episode["duration"] - else "-", - style={ - "padding": "10px", - }, - ), - html.td( - ( - f"{episode['content_length']:,} chars" # noqa: E501 - ) - if episode[ - "content_length" - ] - else "-", - style={ - "padding": "10px", - }, - ), - html.td( - episode["created_at"], - style={ - "padding": "10px", - }, - ), - ) - for episode in episodes - ], - ), - style={ - "width": "100%", - "border-collapse": "collapse", - "border": "1px solid #ddd", - }, - ), - style={"overflow-x": "auto"}, - ), - ), - html.style(""" - body { - font-family: Arial, sans-serif; - max-width: 1200px; - margin: 0 auto; - padding: 20px; - } - h1, h2 { color: #333; } - table { background: white; } - thead { background: #f8f9fa; } - tbody tr:nth-child(even) { background: #f8f9fa; } - tbody tr:hover { background: #e9ecef; } - """), + AdminView._render_content( + queue_items, + episodes, + status_counts, ), id="admin-content", hx_get="/admin", @@ -787,15 +473,114 @@ class AdminView(Component[AnyChildren, AdminViewAttrs]): ) @staticmethod - def get_status_color(status: str) -> str: - """Get color for status display.""" - return { - "pending": "#ffa500", - "processing": "#007cba", - "completed": "#28a745", - "error": "#dc3545", - "cancelled": "#6c757d", - }.get(status, "#6c757d") + def _render_content( + queue_items: list[dict[str, typing.Any]], + episodes: list[dict[str, typing.Any]], + status_counts: dict[str, int], + ) -> layouts.Stack: + """Render the main content of the admin page.""" + return layouts.Stack( + html.h1("PodcastItLater Admin - Queue Status"), + AdminView.render_navigation(), + AdminView.render_status_summary(status_counts), + AdminView.render_queue_table(queue_items), + AdminView.render_episodes_table(episodes), + create_admin_styles(), + ) + + @staticmethod + def render_navigation() -> html.div: + """Render navigation links.""" + return html.div( + html.a( + "← Back to Home", + href="/", + style={"color": "#007cba"}, + ), + html.a( + "Manage Users", + href="/admin/users", + style={"color": "#007cba", "margin-left": "15px"}, + ), + style={"margin-bottom": "20px"}, + ) + + @staticmethod + def render_status_summary(status_counts: dict[str, int]) -> html.div: + """Render status summary section.""" + return html.div( + html.h2("Status Summary"), + html.div( + *[ + StatusBadge(status=status, count=count) + for status, count in status_counts.items() + ], + style={"margin-bottom": "20px"}, + ), + ) + + @staticmethod + def render_queue_table( + queue_items: list[dict[str, typing.Any]], + ) -> html.div: + """Render queue items table.""" + return html.div( + html.h2("Queue Items"), + html.div( + html.table( + create_table_header([ + "ID", + "URL", + "Title", + "Email", + "Status", + "Retries", + "Created", + "Error", + "Actions", + ]), + html.tbody(*[ + QueueTableRow(item=item) for item in queue_items + ]), + style={ + "width": "100%", + "border-collapse": "collapse", + "border": "1px solid #ddd", + }, + ), + style={"overflow-x": "auto", "margin-bottom": "30px"}, + ), + ) + + @staticmethod + def render_episodes_table( + episodes: list[dict[str, typing.Any]], + ) -> html.div: + """Render episodes table.""" + return html.div( + html.h2("Completed Episodes"), + html.div( + html.table( + create_table_header([ + "ID", + "Title", + "Audio URL", + "Duration", + "Content Length", + "Created", + ]), + html.tbody(*[ + EpisodeTableRow(episode=episode) for episode in episodes + ]), + style={ + "width": "100%", + "border-collapse": "collapse", + "border": "1px solid #ddd", + }, + ), + style={"overflow-x": "auto"}, + ), + ) def admin_queue_status(request: Request) -> AdminView | Response | html.div: @@ -847,409 +632,21 @@ def admin_queue_status(request: Request) -> AdminView | Response | html.div: # Check if this is an HTMX request for auto-update if request.headers.get("HX-Request") == "true": # Return just the content div for HTMX updates + AdminView( + queue_items=all_queue_items, + episodes=all_episodes, + status_counts=status_counts, + ) + content = layouts.Stack( + html.h1("PodcastItLater Admin - Queue Status"), + AdminView.render_navigation(), + AdminView.render_status_summary(status_counts), + AdminView.render_queue_table(all_queue_items), + AdminView.render_episodes_table(all_episodes), + create_admin_styles(), + ) return html.div( - layouts.Stack( - html.h1("PodcastItLater Admin - Queue Status"), - html.div( - html.a( - "← Back to Home", - href="/", - style={"color": "#007cba"}, - ), - style={"margin-bottom": "20px"}, - ), - # Status Summary - html.div( - html.h2("Status Summary"), - html.div( - *[ - html.span( - f"{status.upper()}: {count}", - style={ - "margin-right": "20px", - "padding": "5px 10px", - "background": ( - AdminView.get_status_color( - status, - ) - ), - "color": "white", - "border-radius": "4px", - }, - ) - for status, count in status_counts.items() - ], - style={"margin-bottom": "20px"}, - ), - ), - # Queue Items Table - html.div( - html.h2("Queue Items"), - html.div( - html.table( - html.thead( - html.tr( - html.th( - "ID", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "URL", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Email", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Status", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Retries", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Created", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Error", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Actions", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - ), - ), - html.tbody( - *[ - html.tr( - html.td( - str(item["id"]), - style={"padding": "10px"}, - ), - html.td( - html.div( - item["url"][ - : Core.TITLE_TRUNCATE_LENGTH - ] - + ( - "..." - if ( - len(item["url"]) - > Core.TITLE_TRUNCATE_LENGTH # noqa: E501 - ) - else "" - ), - title=item["url"], - style={ - "max-width": "300px", - "overflow": "hidden", - "text-overflow": "ellipsis", - }, - ), - style={"padding": "10px"}, - ), - html.td( - html.div( - (item.get("title") or "-")[ - : Core.TITLE_TRUNCATE_LENGTH - ] - + ( - "..." - if item.get("title") - and len(item["title"]) - > Core.TITLE_TRUNCATE_LENGTH - else "" - ), - title=item.get("title", ""), - style={ - "max-width": "200px", - "overflow": "hidden", - "text-overflow": "ellipsis", - }, - ), - style={"padding": "10px"}, - ), - html.td( - item["email"] or "-", - style={"padding": "10px"}, - ), - html.td( - html.span( - item["status"], - style={ - "color": ( - AdminView.get_status_color( - item["status"], - ) - ), - }, - ), - style={"padding": "10px"}, - ), - html.td( - str( - item.get( - "retry_count", - 0, - ), - ), - style={"padding": "10px"}, - ), - html.td( - item["created_at"], - style={"padding": "10px"}, - ), - html.td( - html.div( - item["error_message"][ - : Core.ERROR_TRUNCATE_LENGTH - ] - + "..." - if item["error_message"] - and len( - item["error_message"], - ) - > Core.ERROR_TRUNCATE_LENGTH - else item["error_message"] - or "-", - title=item["error_message"] - or "", - style={ - "max-width": ("200px"), - "overflow": ("hidden"), - "text-overflow": ( - "ellipsis" - ), - }, - ), - style={"padding": "10px"}, - ), - html.td( - html.div( - html.button( - "Retry", - hx_post=f"/queue/{item['id']}/retry", - hx_target="body", - hx_swap="outerHTML", - style={ - "margin-right": ("5px"), - "padding": ("5px 10px"), - "background": ( - "#28a745" - ), - "color": ("white"), - "border": ("none"), - "cursor": ("pointer"), - "border-radius": ( - "3px" - ), - }, - disabled=item["status"] - == "completed", - ) - if item["status"] != "completed" - else "", - html.button( - "Delete", - hx_delete=f"/queue/{item['id']}", - hx_confirm=( - "Are you sure " - "you want to " - "delete this " - "queue item?" - ), - hx_target="body", - hx_swap="outerHTML", - style={ - "padding": ("5px 10px"), - "background": ( - "#dc3545" - ), - "color": ("white"), - "border": ("none"), - "cursor": ("pointer"), - "border-radius": ( - "3px" - ), - }, - ), - style={ - "display": "flex", - "gap": "5px", - }, - ), - style={"padding": "10px"}, - ), - ) - for item in all_queue_items - ], - ), - style={ - "width": "100%", - "border-collapse": "collapse", - "border": "1px solid #ddd", - }, - ), - style={ - "overflow-x": "auto", - "margin-bottom": "30px", - }, - ), - ), - # Episodes Table - html.div( - html.h2("Completed Episodes"), - html.div( - html.table( - html.thead( - html.tr( - html.th( - "ID", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Title", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Audio URL", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Duration", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Content Length", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - html.th( - "Created", - style={ - "padding": "10px", - "text-align": "left", - }, - ), - ), - ), - html.tbody( - *[ - html.tr( - html.td( - str(episode["id"]), - style={"padding": "10px"}, - ), - html.td( - episode["title"][ - : Core.TITLE_TRUNCATE_LENGTH - ] - + ( - "..." - if len(episode["title"]) - > (Core.TITLE_TRUNCATE_LENGTH) - else "" - ), - style={"padding": "10px"}, - ), - html.td( - html.a( - "Listen", - href=episode["audio_url"], - target="_blank", - style={ - "color": "#007cba", - }, - ), - style={"padding": "10px"}, - ), - html.td( - f"{episode['duration']}s" - if episode["duration"] - else "-", - style={"padding": "10px"}, - ), - html.td( - ( - f"{episode['content_length']:,} chars" # noqa: E501 - ) - if episode["content_length"] - else "-", - style={"padding": "10px"}, - ), - html.td( - episode["created_at"], - style={"padding": "10px"}, - ), - ) - for episode in all_episodes - ], - ), - style={ - "width": "100%", - "border-collapse": "collapse", - "border": "1px solid #ddd", - }, - ), - style={"overflow-x": "auto"}, - ), - ), - html.style(""" - body { - font-family: Arial, sans-serif; - max-width: 1200px; - margin: 0 auto; - padding: 20px; - } - h1, h2 { color: #333; } - table { background: white; } - thead { background: #f8f9fa; } - tbody tr:nth-child(even) { background: #f8f9fa; } - tbody tr:hover { background: #e9ecef; } - """), - ), + content, hx_get="/admin", hx_trigger="every 10s", hx_swap="innerHTML", -- cgit v1.2.3