summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Biz/PodcastItLater/Web.py1161
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,