republisher/repub/pages/dashboard.py

266 lines
9.3 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
2026-03-30 12:13:04 +02:00
import htpy as h
from htpy import Node, Renderable
from repub.components import (
2026-03-30 13:11:37 +02:00
admin_sidebar,
header_action_link,
inline_button,
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
)
2026-03-30 14:02:39 +02:00
def _text(values: Mapping[str, object], key: str) -> str:
return str(values[key])
def _running_execution_row(execution: Mapping[str, object]) -> tuple[Node, ...]:
status_tone = "running" if _text(execution, "status") != "Succeeded" else "done"
2026-03-30 13:11:37 +02:00
return (
h.div[
2026-03-30 14:02:39 +02:00
h.div(class_="font-semibold text-slate-950")[_text(execution, "source")],
2026-03-30 13:11:37 +02:00
h.p(class_="mt-0.5 font-mono text-[11px] text-slate-500")[
2026-03-30 14:02:39 +02:00
_text(execution, "slug")
2026-03-30 12:13:04 +02:00
],
2026-03-30 13:11:37 +02:00
],
h.div[
2026-03-30 14:02:39 +02:00
h.p(class_="font-medium text-slate-900")[
f"#{_text(execution, 'execution_id')}"
],
2026-03-30 13:11:37 +02:00
h.p(class_="mt-0.5 text-[11px] text-slate-500")[
2026-03-30 14:02:39 +02:00
f"job {_text(execution, 'job_id')}"
2026-03-30 12:13:04 +02:00
],
],
2026-03-30 13:11:37 +02:00
h.div[
2026-03-30 14:02:39 +02:00
h.p(class_="font-medium text-slate-900")[_text(execution, "started_at")],
h.p(class_="mt-0.5 text-[11px] text-slate-500")[
_text(execution, "runtime")
],
2026-03-30 13:11:37 +02:00
],
2026-03-30 14:02:39 +02:00
status_badge(label=_text(execution, "status"), tone=status_tone),
2026-03-30 13:11:37 +02:00
h.div(class_="min-w-56 whitespace-normal")[
2026-03-30 14:02:39 +02:00
h.p(class_="font-medium text-slate-900")[_text(execution, "stats")],
h.p(class_="mt-0.5 text-[11px] text-slate-500")[_text(execution, "worker")],
2026-03-30 13:11:37 +02:00
],
h.div(class_="flex flex-nowrap items-center gap-3")[
inline_link(
2026-03-30 14:02:39 +02:00
href=_text(execution, "log_href"),
2026-03-30 13:11:37 +02:00
label="View log",
tone="amber",
),
inline_button(label="Stop", tone="danger"),
],
)
2026-03-30 12:13:04 +02:00
2026-03-30 13:11:37 +02:00
def dashboard_header() -> Renderable:
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.p(class_="mt-1 text-sm text-slate-600")[
"Operational status and live executions."
2026-03-30 12:13:04 +02:00
],
],
2026-03-30 13:11:37 +02:00
h.div(class_="flex flex-wrap gap-2")[
header_action_link(href="/sources/create", label="Create source"),
muted_action_link(href="/sources", label="View sources"),
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 14:02:39 +02:00
h.p(class_="text-xs text-slate-500")["Live values from the database"],
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
),
],
]
2026-03-30 14:02:39 +02:00
def running_executions_table(
*, running_executions: tuple[Mapping[str, object], ...] | None = None
) -> Renderable:
rows = tuple(
_running_execution_row(execution) for execution in (running_executions or ())
)
2026-03-30 13:11:37 +02:00
headers = ("Source", "Execution", "Started", "Status", "Stats", "Actions")
2026-03-30 12:13:04 +02:00
2026-03-30 13:11:37 +02:00
def render_row(row: tuple[Node, ...]) -> Renderable:
first_cell, *other_cells = row
return h.tr(class_="align-top")[
h.td(class_="py-3 pr-6 pl-4 text-sm font-medium text-slate-950 sm:pl-4")[
first_cell
2026-03-30 12:13:04 +02:00
],
2026-03-30 13:11:37 +02:00
(
h.td(
class_="px-3 py-3 align-top text-sm whitespace-nowrap text-slate-600"
)[cell]
for cell in other_cells
),
]
2026-03-30 12:13:04 +02:00
2026-03-30 13:11:37 +02:00
return h.section[
h.div(class_="mb-3 flex items-end justify-between gap-4")[
2026-03-30 12:13:04 +02:00
h.div[
h.p(
class_="text-xs font-semibold uppercase tracking-[0.22em] text-amber-600"
2026-03-30 13:11:37 +02:00
)["Live work"],
h.h2(class_="mt-1 text-xl font-semibold text-slate-950")[
"Running executions"
2026-03-30 12:13:04 +02:00
],
2026-03-30 13:11:37 +02:00
h.p(class_="mt-1 text-sm text-slate-600")[
"Dashboard keeps only the in-flight executions visible here. The full run history lives on the Runs page."
2026-03-30 12:13:04 +02:00
],
],
2026-03-30 13:11:37 +02:00
muted_action_link(href="/runs", label="Open runs"),
2026-03-30 12:13:04 +02:00
],
2026-03-30 13:11:37 +02:00
h.div(
class_="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-slate-200"
2026-03-30 12:13:04 +02:00
)[
2026-03-30 13:11:37 +02:00
h.div(class_="overflow-x-auto")[
h.table(
class_="w-full min-w-[70rem] divide-y divide-slate-200 table-auto"
)[
h.thead(class_="bg-stone-50")[
h.tr[
(
h.th(
scope="col",
class_="px-3 py-2.5 text-left text-[11px] font-semibold uppercase tracking-[0.18em] whitespace-nowrap text-slate-500 first:pl-4",
)[header]
for header in headers
)
]
],
h.tbody(class_="divide-y divide-slate-200 bg-white")[
(render_row(row) for row in rows)
],
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 15:21:39 +02:00
def _source_feed_row(source_feed: Mapping[str, object]) -> tuple[Node, ...]:
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"])]
)
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"])
],
],
h.div(class_="min-w-64")[
inline_link(
href=str(source_feed["feed_href"]),
label=str(source_feed["feed_href"]),
tone="amber",
)
],
status_badge(
label=str(source_feed["feed_status_label"]),
tone=str(source_feed["feed_status_tone"]),
),
last_updated,
h.p(class_="font-medium text-slate-900")[
str(source_feed["artifact_footprint"])
],
)
def published_feeds_table(
*, source_feeds: tuple[Mapping[str, object], ...] | None = None
) -> Renderable:
rows = tuple(_source_feed_row(source_feed) for source_feed in (source_feeds or ()))
return table_section(
eyebrow="Published feeds",
title="Published feeds",
subtitle="Per-source public feed paths under /feeds, with current availability and disk usage.",
headers=("Source", "Feed URL", "Status", "Last updated", "Disk usage"),
rows=rows,
actions=muted_action_link(href="/sources", label="Manage sources"),
)
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(
*,
snapshot: Mapping[str, str] | None = None,
running_executions: tuple[Mapping[str, object], ...] | None = None,
2026-03-30 15:21:39 +02:00
source_feeds: tuple[Mapping[str, object], ...] | None = None,
2026-03-30 14:02:39 +02:00
) -> Renderable:
2026-03-30 12:27:45 +02:00
return h.main(
id="morph",
class_="min-h-screen lg:grid lg:grid-cols-[18rem_minmax(0,1fr)]",
)[
2026-03-30 13:11:37 +02:00
admin_sidebar(current_path="/"),
h.div(class_="px-4 py-4 sm:px-5 lg:px-6 lg:py-5")[
h.div(class_="mx-auto max-w-7xl space-y-5")[
dashboard_header(),
2026-03-30 14:02:39 +02:00
operational_snapshot(snapshot=snapshot),
running_executions_table(running_executions=running_executions),
2026-03-30 15:21:39 +02:00
published_feeds_table(source_feeds=source_feeds),
2026-03-30 12:27:45 +02:00
]
2026-03-30 12:13:04 +02:00
],
2026-03-30 12:27:45 +02:00
]