summaryrefslogtreecommitdiff
path: root/Biz/PodcastItLater/Admin.py
diff options
context:
space:
mode:
authorBen Sima <ben@bsima.me>2025-09-05 13:03:06 -0400
committerBen Sima (aider) <ben@bsima.me>2025-09-05 13:03:06 -0400
commit3b107ebe47556124702240fd934129ed1ec04375 (patch)
treef5be939cdab5f5a7d3da6f15bda22de88fcdb530 /Biz/PodcastItLater/Admin.py
parent91750395b5047dfb99f5d9b7b49d336b2bfb38e8 (diff)
Refactor Admin view components and improve code organization
Diffstat (limited to 'Biz/PodcastItLater/Admin.py')
-rw-r--r--Biz/PodcastItLater/Admin.py1565
1 files changed, 481 insertions, 1084 deletions
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",