diff options
| author | Ben Sima <ben@bsima.me> | 2025-09-03 15:34:35 -0400 |
|---|---|---|
| committer | Ben Sima (aider) <ben@bsima.me> | 2025-09-03 15:34:35 -0400 |
| commit | a4617cb64294dc04ab21942378d8cf3aa03195fa (patch) | |
| tree | 0c246e10e8a8f728641fe46fb2a115209f6fd655 /Biz/PodcastItLater/Web.py | |
| parent | caeb14975063d0e2e046265261474ff5aed21aef (diff) | |
Refactor Admin View for HTMX Auto-Update
Diffstat (limited to 'Biz/PodcastItLater/Web.py')
| -rw-r--r-- | Biz/PodcastItLater/Web.py | 1161 |
1 files changed, 795 insertions, 366 deletions
diff --git a/Biz/PodcastItLater/Web.py b/Biz/PodcastItLater/Web.py index 2bfd448..6fc3a26 100644 --- a/Biz/PodcastItLater/Web.py +++ b/Biz/PodcastItLater/Web.py @@ -376,415 +376,451 @@ class AdminView(Component[AnyChildren, AdminViewAttrs]): ), pages.Body( layouts.Center( - 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( + layouts.Stack( + html.h1("PodcastItLater Admin - Queue Status"), 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() - ], + html.a( + "← Back to Home", + href="/", + style={"color": "#007cba"}, + ), style={"margin-bottom": "20px"}, ), - ), - # Queue Items Table - html.div( - html.h2("Queue Items"), + # Status Summary 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.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.td( - str(item["id"]), - style={"padding": "10px"}, + html.th( + "ID", + style={ + "padding": "10px", + "text-align": "left", + }, ), - html.td( - html.div( - item["url"][ - :TITLE_TRUNCATE_LENGTH - ] - + ( - "..." - if ( - len(item["url"]) - > TITLE_TRUNCATE_LENGTH # noqa: E501 - ) - else "" - ), - title=item["url"], - style={ - "max-width": ( - "300px" - ), - "overflow": ( - "hidden" - ), - "text-overflow": ( - "ellipsis" - ), - }, - ), - style={"padding": "10px"}, + html.th( + "URL", + style={ + "padding": "10px", + "text-align": "left", + }, ), - html.td( - item["email"] or "-", - style={"padding": "10px"}, + html.th( + "Email", + style={ + "padding": "10px", + "text-align": "left", + }, ), - html.td( - html.span( - item["status"], - style={ - "color": ( - AdminView._get_status_color( - item[ - "status" - ], - ) - ), - }, - ), - style={"padding": "10px"}, + html.th( + "Status", + style={ + "padding": "10px", + "text-align": "left", + }, ), - html.td( - str( - item.get( - "retry_count", - 0, - ), - ), - style={"padding": "10px"}, + html.th( + "Retries", + style={ + "padding": "10px", + "text-align": "left", + }, ), - html.td( - item["created_at"], - style={"padding": "10px"}, + html.th( + "Created", + style={ + "padding": "10px", + "text-align": "left", + }, ), - html.td( - html.div( - item["error_message"][ - :ERROR_TRUNCATE_LENGTH - ] - + "..." - if item["error_message"] - and len( - item[ - "error_message" - ], - ) - > ERROR_TRUNCATE_LENGTH - else item[ - "error_message" - ] - or "-", - title=item[ - "error_message" - ] - or "", + 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={ - "max-width": ( - "200px" - ), - "overflow": ( - "hidden" - ), - "text-overflow": ( - "ellipsis" - ), + "padding": "10px", }, ), - style={"padding": "10px"}, - ), - html.td( - html.div( - html.button( - "Retry", - hx_post=f"/queue/{item['id']}/retry", - hx_target="body", - hx_swap="outerHTML", + html.td( + html.div( + item["url"][ + :TITLE_TRUNCATE_LENGTH + ] + + ( + "..." + if ( + len( + item[ + "url" + ], + ) + > TITLE_TRUNCATE_LENGTH # noqa: E501 + ) + else "" + ), + title=item["url"], style={ - "margin-right": ( # noqa: E501 - "5px" + "max-width": ( + "300px" ), - "padding": ( - "5px 10px" + "overflow": ( + "hidden" ), - "background": ( - "#28a745" + "text-overflow": ( # noqa: E501 + "ellipsis" ), + }, + ), + style={ + "padding": "10px", + }, + ), + html.td( + item["email"] or "-", + style={ + "padding": "10px", + }, + ), + html.td( + html.span( + item["status"], + style={ "color": ( - "white" - ), - "border": ( - "none" - ), - "cursor": ( - "pointer" - ), - "border-radius": ( # noqa: E501 - "3px" + AdminView.get_status_color( + item[ + "status" + ], + ) ), }, - 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?" + ), + style={ + "padding": "10px", + }, + ), + html.td( + str( + item.get( + "retry_count", + 0, ), - hx_target="body", - hx_swap="outerHTML", + ), + style={ + "padding": "10px", + }, + ), + html.td( + item["created_at"], + style={ + "padding": "10px", + }, + ), + html.td( + html.div( + item[ + "error_message" + ][ + :ERROR_TRUNCATE_LENGTH + ] + + "..." + if item[ + "error_message" + ] + and len( + item[ + "error_message" + ], + ) + > ( + ERROR_TRUNCATE_LENGTH + ) + else item[ + "error_message" + ] + or "-", + title=item[ + "error_message" + ] + or "", style={ - "padding": ( - "5px 10px" - ), - "background": ( - "#dc3545" - ), - "color": ( - "white" + "max-width": ( + "200px" ), - "border": ( - "none" + "overflow": ( + "hidden" ), - "cursor": ( - "pointer" + "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 ), - "border-radius": ( # noqa: E501 - "3px" + 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={ - "display": "flex", - "gap": "5px", + "padding": "10px", }, ), - style={"padding": "10px"}, - ), - ) - for item in queue_items - ], + ) + for item in queue_items + ], + ), + style={ + "width": "100%", + "border-collapse": "collapse", + "border": "1px solid #ddd", + }, ), style={ - "width": "100%", - "border-collapse": "collapse", - "border": "1px solid #ddd", + "overflow-x": "auto", + "margin-bottom": "30px", }, ), - style={ - "overflow-x": "auto", - "margin-bottom": "30px", - }, ), - ), - # Episodes Table - html.div( - html.h2("Completed Episodes"), + # Episodes Table 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.h2("Completed Episodes"), + html.div( + html.table( + html.thead( html.tr( - html.td( - str(episode["id"]), - style={"padding": "10px"}, + html.th( + "ID", + style={ + "padding": "10px", + "text-align": "left", + }, ), - html.td( - episode["title"][ - :TITLE_TRUNCATE_LENGTH - ] - + ( - "..." - if len(episode["title"]) - > TITLE_TRUNCATE_LENGTH - else "" - ), - style={"padding": "10px"}, + html.th( + "Title", + style={ + "padding": "10px", + "text-align": "left", + }, ), - html.td( - html.a( - "Listen", - href=episode[ - "audio_url" - ], - target="_blank", - style={ - "color": "#007cba", - }, - ), - style={"padding": "10px"}, + html.th( + "Audio URL", + style={ + "padding": "10px", + "text-align": "left", + }, ), - html.td( - f"{episode['duration']}s" - if episode["duration"] - else "-", - style={"padding": "10px"}, + html.th( + "Duration", + style={ + "padding": "10px", + "text-align": "left", + }, ), - html.td( - ( - f"{episode['content_length']:,} chars" # noqa: E501 - ) - if episode["content_length"] - else "-", - style={"padding": "10px"}, + html.th( + "Content Length", + style={ + "padding": "10px", + "text-align": "left", + }, ), - html.td( - episode["created_at"], - style={"padding": "10px"}, + html.th( + "Created", + style={ + "padding": "10px", + "text-align": "left", + }, ), - ) - for episode in episodes - ], + ), + ), + html.tbody( + *[ + html.tr( + html.td( + str(episode["id"]), + style={ + "padding": "10px", + }, + ), + html.td( + episode["title"][ + :TITLE_TRUNCATE_LENGTH + ] + + ( + "..." + if len( + episode[ + "title" + ], + ) + > ( + 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={ - "width": "100%", - "border-collapse": "collapse", - "border": "1px solid #ddd", - }, + style={"overflow-x": "auto"}, ), - style={"overflow-x": "auto"}, ), - ), - html.style(""" + html.style(""" body { font-family: Arial, sans-serif; max-width: 1200px; @@ -797,17 +833,20 @@ class AdminView(Component[AnyChildren, AdminViewAttrs]): tbody tr:nth-child(even) { background: #f8f9fa; } tbody tr:hover { background: #e9ecef; } """), + ), + id="admin-content", + hx_get="/admin", + hx_trigger="every 10s", + hx_swap="innerHTML", + hx_target="#admin-content", ), ), htmx_version="1.9.10", - hx_get="/admin", - hx_trigger="every 10s", - hx_swap="outerHTML", ), ) @staticmethod - def _get_status_color(status: str) -> str: + def get_status_color(status: str) -> str: """Get color for status display.""" return { "pending": "#ffa500", @@ -1197,7 +1236,7 @@ def queue_status(request: Request) -> QueueStatus: # noqa: ARG001 @app.get("/admin") -def admin_queue_status(request: Request) -> AdminView | Response: +def admin_queue_status(request: Request) -> AdminView | Response | html.div: """Return admin view showing all queue items and episodes.""" # Check if user is logged in user_id = request.session.get("user_id") @@ -1230,6 +1269,396 @@ def admin_queue_status(request: Request) -> AdminView | Response: get_database_path(), ) + # 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 + 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"][ + :TITLE_TRUNCATE_LENGTH + ] + + ( + "..." + if ( + len(item["url"]) + > TITLE_TRUNCATE_LENGTH + ) + else "" + ), + title=item["url"], + style={ + "max-width": ("300px"), + "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"][ + :ERROR_TRUNCATE_LENGTH + ] + + "..." + if item["error_message"] + and len( + item["error_message"], + ) + > 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"][ + :TITLE_TRUNCATE_LENGTH + ] + + ( + "..." + if len(episode["title"]) + > (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; } + """), + ), + hx_get="/admin", + hx_trigger="every 10s", + hx_swap="innerHTML", + ) + return AdminView( queue_items=all_queue_items, episodes=all_episodes, |
