republisher/repub/pages/dashboard.py

225 lines
7.1 KiB
Python
Raw Normal View History

2026-03-30 12:13:04 +02:00
from __future__ import annotations
2026-03-30 14:02:39 +02:00
from collections.abc import Mapping
from typing import cast
2026-03-30 14:02:39 +02:00
2026-03-30 12:13:04 +02:00
import htpy as h
from htpy import Node, Renderable
from repub.components import (
action_button,
app_shell,
2026-03-30 13:11:37 +02:00
inline_link,
muted_action_link,
2026-03-30 12:13:04 +02:00
stat_card,
status_badge,
2026-03-30 15:21:39 +02:00
table_section,
2026-03-30 12:13:04 +02:00
)
from repub.pages.runs import live_work_section, relative_time_formatter_script
2026-03-30 12:13:04 +02:00
2026-06-02 10:18:59 +02:00
def dashboard_header(
*, path_prefix: str = "/admin", reader_app_url: str | None = None
) -> Renderable:
2026-03-30 13:11:37 +02:00
return h.section[
h.div(
class_="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
)[
h.div[
h.h1(class_="text-3xl font-semibold tracking-tight text-slate-950")[
"Republisher"
2026-03-30 12:13:04 +02:00
],
],
2026-03-30 13:11:37 +02:00
h.div(class_="flex flex-wrap gap-2")[
2026-06-02 10:18:59 +02:00
(
muted_action_link(
href=reader_app_url,
label="Open AnyNews",
target="_blank",
rel="noopener noreferrer",
)
if reader_app_url is not None
else None
),
muted_action_link(
href=f"{path_prefix}/publisher",
label="Publisher View",
),
2026-03-30 12:13:04 +02:00
],
2026-03-30 13:11:37 +02:00
]
2026-03-30 12:13:04 +02:00
]
2026-03-30 14:02:39 +02:00
def operational_snapshot(*, snapshot: Mapping[str, str] | None = None) -> Renderable:
values = snapshot or {
"running_now": "0",
"upcoming_today": "0",
"failures_24h": "0",
"artifact_footprint": "0 B",
}
2026-03-30 12:13:04 +02:00
return h.section[
2026-03-30 13:11:37 +02:00
h.div(class_="mb-3 flex items-end justify-between gap-4")[
2026-03-30 12:13:04 +02:00
h.div[
h.p(
2026-03-30 13:11:37 +02:00
class_="text-xs font-semibold uppercase tracking-[0.22em] text-slate-500"
2026-03-30 12:13:04 +02:00
)["Overview"],
2026-03-30 13:11:37 +02:00
h.h2(class_="mt-1 text-xl font-semibold tracking-tight text-slate-950")[
"Operational snapshot"
],
],
2026-03-30 12:13:04 +02:00
],
2026-03-30 13:11:37 +02:00
h.dl(class_="grid gap-3 md:grid-cols-2 xl:grid-cols-4")[
2026-03-30 12:34:38 +02:00
stat_card(
2026-03-30 13:11:37 +02:00
label="Running now",
2026-03-30 14:02:39 +02:00
value=values["running_now"],
detail="Currently active job executions.",
2026-03-30 12:34:38 +02:00
),
2026-03-30 12:13:04 +02:00
stat_card(
2026-03-30 13:11:37 +02:00
label="Upcoming today",
2026-03-30 14:02:39 +02:00
value=values["upcoming_today"],
detail="Enabled jobs that are ready for their next run.",
2026-03-30 12:13:04 +02:00
),
stat_card(
2026-03-30 13:11:37 +02:00
label="Failures in 24h",
2026-03-30 14:02:39 +02:00
value=values["failures_24h"],
detail="Recent failed executions recorded by the scheduler.",
2026-03-30 13:11:37 +02:00
),
stat_card(
2026-03-30 14:02:39 +02:00
label="Artifact footprint",
value=values["artifact_footprint"],
2026-03-30 15:04:41 +02:00
detail="Current artifact size under the output path.",
2026-03-30 12:13:04 +02:00
),
],
]
def _source_feed_row(
source_feed: Mapping[str, object], *, show_feed_url: bool
) -> tuple[Node, ...]:
2026-03-30 15:21:39 +02:00
last_updated_iso = source_feed.get("last_updated_iso")
last_updated = (
h.time(
datetime=str(last_updated_iso),
title=str(last_updated_iso),
class_="font-medium text-slate-900",
)[str(source_feed["last_updated"])]
if last_updated_iso is not None
else h.p(class_="font-medium text-slate-900")[str(source_feed["last_updated"])]
)
next_run_iso = source_feed.get("next_run_at")
next_run = (
h.time(
{
"data-next-run-at": str(next_run_iso),
"title": str(next_run_iso),
},
datetime=str(next_run_iso),
class_="font-medium text-slate-900",
)[str(source_feed["next_run"])]
if next_run_iso is not None
else h.p(class_="font-medium text-slate-900")[str(source_feed["next_run"])]
)
feed_url_cells = (
(
h.div(class_="min-w-64")[
inline_link(
href=str(source_feed["feed_href"]),
label=str(source_feed["feed_href"]),
tone="amber",
)
],
)
if show_feed_url
else ()
)
2026-03-30 15:21:39 +02:00
return (
h.div[
h.div(class_="font-semibold text-slate-950")[str(source_feed["source"])],
h.p(class_="mt-0.5 font-mono text-[11px] text-slate-500")[
str(source_feed["slug"])
],
],
*feed_url_cells,
2026-03-30 15:21:39 +02:00
status_badge(
label=str(source_feed["feed_status_label"]),
tone=str(source_feed["feed_status_tone"]),
),
last_updated,
next_run,
action_button(
label="Run now",
disabled=bool(source_feed["run_disabled"]),
post_path=cast(str | None, source_feed.get("run_post_path")),
),
2026-03-30 15:21:39 +02:00
)
def published_feeds_table(
2026-06-02 10:18:59 +02:00
*,
source_feeds: tuple[Mapping[str, object], ...] | None = None,
manage_sources_href: str | None = "/admin/sources",
show_heading: bool = True,
show_feed_url: bool = True,
2026-03-30 15:21:39 +02:00
) -> Renderable:
rows = tuple(
_source_feed_row(source_feed, show_feed_url=show_feed_url)
for source_feed in (source_feeds or ())
)
feed_url_headers = ("Feed URL",) if show_feed_url else ()
2026-03-30 15:21:39 +02:00
return table_section(
2026-06-02 10:18:59 +02:00
eyebrow="Published feeds" if show_heading else None,
title="Published feeds" if show_heading else None,
2026-03-30 15:28:56 +02:00
empty_message="No feeds have been published yet.",
headers=(
"Source",
*feed_url_headers,
"Status",
"Last updated",
"Next run",
"Actions",
),
2026-03-30 15:21:39 +02:00
rows=rows,
2026-06-02 10:18:59 +02:00
actions=(
muted_action_link(href=manage_sources_href, label="Manage sources")
if manage_sources_href is not None
else None
),
2026-03-30 15:21:39 +02:00
)
2026-03-30 13:11:37 +02:00
def dashboard_page() -> Renderable:
2026-03-30 14:02:39 +02:00
return dashboard_page_with_data()
def dashboard_page_with_data(
*,
2026-06-02 10:18:59 +02:00
current_path: str = "/admin",
path_prefix: str = "/admin",
2026-03-30 14:02:39 +02:00
snapshot: Mapping[str, str] | None = None,
running_executions: tuple[Mapping[str, object], ...] | None = None,
queued_executions: tuple[Mapping[str, object], ...] | None = None,
2026-03-30 15:21:39 +02:00
source_feeds: tuple[Mapping[str, object], ...] | None = None,
2026-06-02 10:18:59 +02:00
reader_app_url: str | None = None,
2026-03-30 14:02:39 +02:00
) -> Renderable:
2026-03-30 18:26:02 +02:00
running_items = running_executions or ()
queued_items = queued_executions or ()
2026-03-30 18:26:02 +02:00
source_items = source_feeds or ()
return app_shell(
2026-06-02 10:18:59 +02:00
current_path=current_path,
source_count=len(source_items),
running_count=len(running_items),
content=(
2026-06-02 10:18:59 +02:00
dashboard_header(path_prefix=path_prefix, reader_app_url=reader_app_url),
operational_snapshot(snapshot=snapshot),
live_work_section(
running_executions=running_items,
queued_executions=queued_items,
),
2026-06-02 10:18:59 +02:00
published_feeds_table(
source_feeds=source_items,
manage_sources_href=f"{path_prefix}/sources",
),
relative_time_formatter_script(),
2026-03-30 18:26:02 +02:00
),
)