2026-03-30 13:11:37 +02:00
from __future__ import annotations
2026-03-30 14:02:39 +02:00
from collections . abc import Mapping
2026-03-30 13:11:37 +02:00
import htpy as h
from htpy import Node , Renderable
from repub . components import (
inline_link ,
muted_action_link ,
page_shell ,
section_card ,
status_badge ,
table_section ,
)
2026-03-30 14:02:39 +02:00
def _action_button (
* ,
label : str ,
tone : str = " default " ,
disabled : bool = False ,
post_path : str | None = None ,
) - > Renderable :
classes = {
" default " : " bg-stone-100 text-slate-700 hover:bg-stone-200 " ,
" danger " : " bg-rose-50 text-rose-700 hover:bg-rose-100 " ,
}
class_name = (
" cursor-not-allowed bg-slate-100 text-slate-400 " if disabled else classes [ tone ]
)
attributes : dict [ str , str ] = { }
if post_path is not None and not disabled :
attributes [ " data-on:pointerdown " ] = f " @post( ' { post_path } ' ) "
return h . button (
attributes ,
type = " button " ,
disabled = disabled ,
class_ = (
" inline-flex items-center whitespace-nowrap rounded-full px-3 py-1.5 "
f " text-sm font-semibold transition { class_name } "
) ,
) [ label ]
2026-03-30 13:11:37 +02:00
2026-03-30 14:02:39 +02:00
def _text ( values : Mapping [ str , object ] , key : str ) - > str :
return str ( values [ key ] )
def _maybe_text ( values : Mapping [ str , object ] , key : str ) - > str | None :
value = values . get ( key )
if value in { None , " " } :
return None
return str ( value )
2026-03-30 13:11:37 +02:00
2026-03-30 14:02:39 +02:00
def _flag ( values : Mapping [ str , object ] , key : str ) - > bool :
return bool ( values [ key ] )
def _running_row ( execution : Mapping [ str , object ] ) - > tuple [ Node , . . . ] :
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 " ) ] ,
h . p ( class_ = " mt-1 font-mono text-xs text-slate-500 " ) [
_text ( execution , " slug " )
] ,
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 ' ) } "
] ,
h . p ( class_ = " mt-1 text-xs text-slate-500 " ) [
f " job { _text ( execution , ' job_id ' ) } "
] ,
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-1 text-xs 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 = " running " ) ,
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-1 text-xs 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 " ,
) ,
2026-03-30 14:02:39 +02:00
_action_button (
label = " Stop " ,
tone = " danger " ,
post_path = _maybe_text ( execution , " cancel_post_path " ) ,
) ,
2026-03-30 13:11:37 +02:00
] ,
)
2026-03-30 14:02:39 +02:00
def _upcoming_row ( job : Mapping [ str , object ] ) - > tuple [ Node , . . . ] :
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 ( job , " source " ) ] ,
h . p ( class_ = " mt-1 font-mono text-xs text-slate-500 " ) [ _text ( job , " slug " ) ] ,
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 ( job , " next_run " ) ] ,
h . p ( class_ = " mt-1 text-xs text-slate-500 " ) [ f " job { _text ( job , ' job_id ' ) } " ] ,
2026-03-30 13:11:37 +02:00
] ,
2026-03-30 14:02:39 +02:00
h . p ( class_ = " font-mono text-xs text-slate-600 " ) [ _text ( job , " schedule " ) ] ,
2026-03-30 13:11:37 +02:00
status_badge (
2026-03-30 14:02:39 +02:00
label = _text ( job , " enabled_label " ) ,
tone = _text ( job , " enabled_tone " ) ,
2026-03-30 13:11:37 +02:00
) ,
h . p ( class_ = " max-w-40 whitespace-normal text-sm text-slate-500 " ) [
2026-03-30 14:02:39 +02:00
_text ( job , " run_reason " )
2026-03-30 13:11:37 +02:00
] ,
h . div ( class_ = " flex flex-nowrap items-center gap-2 " ) [
2026-03-30 14:02:39 +02:00
_action_button (
label = " Run now " ,
disabled = _flag ( job , " run_disabled " ) ,
post_path = _maybe_text ( job , " run_post_path " ) ,
) ,
_action_button (
label = _text ( job , " toggle_label " ) ,
post_path = _maybe_text ( job , " toggle_post_path " ) ,
) ,
_action_button (
label = " Delete " ,
tone = " danger " ,
post_path = _maybe_text ( job , " delete_post_path " ) ,
) ,
2026-03-30 13:11:37 +02:00
] ,
)
2026-03-30 14:02:39 +02:00
def _completed_row ( execution : Mapping [ str , object ] ) - > tuple [ Node , . . . ] :
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 " ) ] ,
h . p ( class_ = " mt-1 font-mono text-xs text-slate-500 " ) [
_text ( execution , " slug " )
] ,
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 ' ) } "
] ,
h . p ( class_ = " mt-1 text-xs text-slate-500 " ) [
f " job { _text ( execution , ' job_id ' ) } "
] ,
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 , " ended_at " ) ] ,
h . p ( class_ = " mt-1 text-xs text-slate-500 " ) [ _text ( execution , " summary " ) ] ,
2026-03-30 13:11:37 +02:00
] ,
status_badge (
2026-03-30 14:02:39 +02:00
label = _text ( execution , " status " ) ,
tone = _text ( execution , " status_tone " ) ,
2026-03-30 13:11:37 +02:00
) ,
h . div ( class_ = " min-w-48 whitespace-normal " ) [
2026-03-30 14:02:39 +02:00
h . p ( class_ = " font-medium text-slate-900 " ) [ _text ( execution , " stats " ) ]
2026-03-30 13:11:37 +02:00
] ,
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 " ,
) ,
)
2026-03-30 14:02:39 +02:00
def runs_page (
* ,
running_executions : tuple [ Mapping [ str , object ] , . . . ] | None = None ,
upcoming_jobs : tuple [ Mapping [ str , object ] , . . . ] | None = None ,
completed_executions : tuple [ Mapping [ str , object ] , . . . ] | None = None ,
) - > Renderable :
running_items = running_executions or ( )
upcoming_items = upcoming_jobs or ( )
completed_items = completed_executions or ( )
running_rows = tuple ( _running_row ( execution ) for execution in running_items )
upcoming_rows = tuple ( _upcoming_row ( job ) for job in upcoming_items )
completed_rows = tuple ( _completed_row ( execution ) for execution in completed_items )
2026-03-30 13:11:37 +02:00
return page_shell (
current_path = " /runs " ,
eyebrow = " Execution control " ,
title = " Runs " ,
description = " Running executions first, then the schedule queue, then completed history. Logs are routed through app URLs instead of direct file serving. " ,
actions = muted_action_link ( href = " /sources " , label = " Back to sources " ) ,
content = (
table_section (
eyebrow = " Live work " ,
title = " Running job executions " ,
subtitle = " Operators can inspect the live log stream, request a graceful stop, and escalate to a hard kill after the 15 second deadline if needed. " ,
headers = (
" Source " ,
" Execution " ,
" Started " ,
" Status " ,
" Stats " ,
" Actions " ,
) ,
rows = running_rows ,
) ,
table_section (
eyebrow = " Queue " ,
title = " Upcoming jobs " ,
2026-03-30 14:02:39 +02:00
subtitle = " Scheduled work shows enable or disable state, run-now affordances, and destructive delete controls. Deleting removes the source-linked job and its execution history. " ,
2026-03-30 13:11:37 +02:00
headers = (
" Source " ,
" Next run " ,
" Cron " ,
" State " ,
" Run now " ,
" Actions " ,
) ,
rows = upcoming_rows ,
) ,
table_section (
eyebrow = " History " ,
title = " Completed job executions " ,
subtitle = " Recent execution history keeps the summary counters visible and links back to the plain text log view. " ,
headers = (
" Source " ,
" Execution " ,
" Ended " ,
" Status " ,
" Summary " ,
" Log " ,
) ,
rows = completed_rows ,
) ,
) ,
)
2026-03-30 14:02:39 +02:00
def execution_logs_page (
* ,
job_id : int ,
execution_id : int ,
log_view : Mapping [ str , object ] | None = None ,
) - > Renderable :
if log_view is None :
log_view = {
" title " : f " Job { job_id } / execution { execution_id } " ,
" description " : " Plain text log view routed through the app. " ,
" status_label " : " Unavailable " ,
" status_tone " : " failed " ,
" log_text " : " " ,
" error_message " : " Execution log is only available from persisted job runs. " ,
}
error_message = _maybe_text ( log_view , " error_message " )
error_notice = (
h . div (
class_ = " mt-3 rounded-2xl bg-rose-50 px-4 py-3 text-sm font-medium text-rose-800 "
) [
h . p [ " Execution log unavailable " ] ,
h . p ( class_ = " mt-1 font-normal " ) [ error_message ] ,
]
if error_message is not None
else None
)
2026-03-30 13:11:37 +02:00
return page_shell (
current_path = f " /job/ { job_id } /execution/ { execution_id } /logs " ,
eyebrow = " Execution log " ,
2026-03-30 14:02:39 +02:00
title = _text ( log_view , " title " ) ,
description = _text ( log_view , " description " ) ,
2026-03-30 13:11:37 +02:00
actions = muted_action_link ( href = " /runs " , label = " Back to runs " ) ,
content = (
section_card (
content = (
h . div ( class_ = " flex items-end justify-between gap-4 " ) [
h . div [
h . p (
class_ = " text-xs font-semibold uppercase tracking-[0.22em] text-amber-600 "
) [ " Route " ] ,
h . h2 ( class_ = " mt-2 text-xl font-semibold text-slate-950 " ) [
f " /job/ { job_id } /execution/ { execution_id } /logs "
] ,
h . p ( class_ = " mt-2 text-sm text-slate-600 " ) [
2026-03-30 14:02:39 +02:00
_text ( log_view , " description " )
2026-03-30 13:11:37 +02:00
] ,
] ,
2026-03-30 14:02:39 +02:00
status_badge (
label = _text ( log_view , " status_label " ) ,
tone = _text ( log_view , " status_tone " ) ,
) ,
2026-03-30 13:11:37 +02:00
] ,
2026-03-30 14:02:39 +02:00
error_notice ,
2026-03-30 13:11:37 +02:00
h . pre (
class_ = " mt-3 overflow-x-auto rounded-[1.5rem] bg-slate-950 p-5 text-xs leading-6 text-emerald-200 "
2026-03-30 14:02:39 +02:00
) [ _text ( log_view , " log_text " ) ] ,
2026-03-30 13:11:37 +02:00
)
) ,
) ,
)