2026-03-30 13:11:37 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-03-30 13:23:36 +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 (
|
|
|
|
|
header_action_link,
|
|
|
|
|
inline_link,
|
|
|
|
|
input_field,
|
|
|
|
|
muted_action_link,
|
|
|
|
|
page_shell,
|
|
|
|
|
section_card,
|
|
|
|
|
select_field,
|
|
|
|
|
status_badge,
|
|
|
|
|
table_section,
|
|
|
|
|
textarea_field,
|
|
|
|
|
toggle_field,
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-30 13:23:36 +02:00
|
|
|
PANGEA_CONTENT_FORMATS = (
|
|
|
|
|
"WTF_0",
|
|
|
|
|
"TEXT_ONLY",
|
|
|
|
|
"WTF_1",
|
|
|
|
|
"MOBILE_1",
|
|
|
|
|
"MOBILE_2",
|
|
|
|
|
"MOBILE_3",
|
|
|
|
|
"WTF_2",
|
|
|
|
|
"XML_TX",
|
|
|
|
|
"JSON",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
PANGEA_CONTENT_TYPES = (
|
|
|
|
|
"articles",
|
|
|
|
|
"audioclips",
|
|
|
|
|
"videoclips",
|
|
|
|
|
"breakingnews",
|
|
|
|
|
"mostpopular",
|
|
|
|
|
"topstories",
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-30 13:11:37 +02:00
|
|
|
|
2026-03-30 13:49:00 +02:00
|
|
|
def _value(source: Mapping[str, object] | None, key: str, default: str = "") -> str:
|
|
|
|
|
if source is None:
|
|
|
|
|
return default
|
|
|
|
|
return str(source.get(key, default))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _checked(source: Mapping[str, object] | None, key: str, default: bool) -> bool:
|
|
|
|
|
if source is None:
|
|
|
|
|
return default
|
|
|
|
|
value = source.get(key, default)
|
|
|
|
|
return bool(value)
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 17:25:37 +02:00
|
|
|
def _action_button(
|
|
|
|
|
*,
|
|
|
|
|
label: str,
|
|
|
|
|
tone: str = "default",
|
|
|
|
|
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",
|
|
|
|
|
}
|
|
|
|
|
attributes: dict[str, str] = {}
|
|
|
|
|
if post_path is not None:
|
|
|
|
|
attributes["data-on:pointerdown"] = f"@post('{post_path}')"
|
|
|
|
|
return h.button(
|
|
|
|
|
attributes,
|
|
|
|
|
type="button",
|
|
|
|
|
class_=(
|
|
|
|
|
"inline-flex items-center whitespace-nowrap rounded-full px-3 py-1.5 "
|
|
|
|
|
f"text-sm font-semibold transition {classes[tone]}"
|
|
|
|
|
),
|
|
|
|
|
)[label]
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 13:23:36 +02:00
|
|
|
def _source_row(source: Mapping[str, object]) -> tuple[Node, ...]:
|
2026-03-30 13:11:37 +02:00
|
|
|
return (
|
|
|
|
|
h.div[
|
2026-03-30 13:23:36 +02:00
|
|
|
h.div(class_="font-semibold text-slate-950")[str(source["name"])],
|
|
|
|
|
h.p(class_="mt-1 font-mono text-xs text-slate-500")[str(source["slug"])],
|
2026-03-30 13:11:37 +02:00
|
|
|
],
|
|
|
|
|
h.p(class_="font-medium whitespace-nowrap text-slate-900")[
|
2026-03-30 13:23:36 +02:00
|
|
|
str(source["source_type"])
|
2026-03-30 13:11:37 +02:00
|
|
|
],
|
|
|
|
|
h.p(class_="max-w-sm truncate font-mono text-xs text-slate-600")[
|
2026-03-30 13:23:36 +02:00
|
|
|
str(source["upstream"])
|
|
|
|
|
],
|
|
|
|
|
h.p(class_="font-medium whitespace-nowrap text-slate-900")[
|
|
|
|
|
str(source["schedule"])
|
2026-03-30 13:11:37 +02:00
|
|
|
],
|
|
|
|
|
h.div(class_="min-w-32 whitespace-normal")[
|
2026-03-30 13:23:36 +02:00
|
|
|
status_badge(
|
|
|
|
|
label=str(source["state"]),
|
|
|
|
|
tone=str(source["state_tone"]),
|
|
|
|
|
),
|
|
|
|
|
h.p(class_="mt-2 text-xs text-slate-500")[str(source["last_run"])],
|
2026-03-30 13:11:37 +02:00
|
|
|
],
|
|
|
|
|
h.div(class_="flex flex-nowrap items-center gap-3")[
|
2026-03-30 13:49:00 +02:00
|
|
|
inline_link(
|
|
|
|
|
href=f"/sources/{source['slug']}/edit", label="Edit", tone="amber"
|
|
|
|
|
),
|
2026-03-30 13:11:37 +02:00
|
|
|
inline_link(href="/runs", label="View runs"),
|
2026-03-30 17:25:37 +02:00
|
|
|
_action_button(
|
|
|
|
|
label="Delete",
|
|
|
|
|
tone="danger",
|
|
|
|
|
post_path=f"/actions/sources/{source['slug']}/delete",
|
|
|
|
|
),
|
2026-03-30 13:11:37 +02:00
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 13:23:36 +02:00
|
|
|
def sources_table(
|
|
|
|
|
*, sources: tuple[Mapping[str, object], ...] | None = None
|
|
|
|
|
) -> Renderable:
|
2026-03-30 13:37:25 +02:00
|
|
|
rows = tuple(_source_row(source) for source in (sources or ()))
|
2026-03-30 13:11:37 +02:00
|
|
|
return table_section(
|
|
|
|
|
eyebrow="Inventory",
|
|
|
|
|
title="Sources",
|
2026-03-30 15:28:56 +02:00
|
|
|
empty_message="No sources yet.",
|
2026-03-30 13:11:37 +02:00
|
|
|
headers=("Source", "Type", "Upstream", "Schedule", "Job state", "Actions"),
|
|
|
|
|
rows=rows,
|
|
|
|
|
actions=header_action_link(href="/sources/create", label="Create source"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 13:23:36 +02:00
|
|
|
def sources_page(
|
2026-03-30 18:26:02 +02:00
|
|
|
*,
|
|
|
|
|
sources: tuple[Mapping[str, object], ...] | None = None,
|
|
|
|
|
running_count: int = 0,
|
2026-03-30 13:23:36 +02:00
|
|
|
) -> Renderable:
|
2026-03-30 18:26:02 +02:00
|
|
|
source_items = sources or ()
|
2026-03-30 13:11:37 +02:00
|
|
|
return page_shell(
|
|
|
|
|
current_path="/sources",
|
|
|
|
|
eyebrow="Source management",
|
|
|
|
|
title="Sources",
|
2026-03-30 18:26:02 +02:00
|
|
|
source_count=len(source_items),
|
|
|
|
|
running_count=running_count,
|
|
|
|
|
content=sources_table(sources=source_items),
|
2026-03-30 13:11:37 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 13:49:00 +02:00
|
|
|
def source_form(
|
|
|
|
|
*,
|
|
|
|
|
mode: str,
|
|
|
|
|
action_path: str,
|
|
|
|
|
source: Mapping[str, object] | None = None,
|
|
|
|
|
) -> Renderable:
|
|
|
|
|
source_type = _value(source, "source_type", "pangea")
|
|
|
|
|
slug = _value(source, "slug")
|
|
|
|
|
title = "Source and job setup" if mode == "create" else "Edit source"
|
|
|
|
|
eyebrow = "Create" if mode == "create" else "Edit"
|
|
|
|
|
status_label = "New source" if mode == "create" else "Existing source"
|
|
|
|
|
submit_label = "Create source" if mode == "create" else "Save changes"
|
|
|
|
|
initial_signals = "{sourceType: 'pangea'}"
|
|
|
|
|
if mode == "edit":
|
|
|
|
|
initial_signals = f"{{sourceType: '{source_type}', sourceSlug: '{slug}'}}"
|
|
|
|
|
|
2026-03-30 13:11:37 +02:00
|
|
|
return section_card(
|
|
|
|
|
content=(
|
|
|
|
|
h.div(
|
|
|
|
|
class_="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between"
|
|
|
|
|
)[
|
|
|
|
|
h.div[
|
|
|
|
|
h.p(
|
|
|
|
|
class_="text-xs font-semibold uppercase tracking-[0.22em] text-amber-600"
|
2026-03-30 13:49:00 +02:00
|
|
|
)[eyebrow],
|
|
|
|
|
h.h2(class_="mt-2 text-xl font-semibold text-slate-950")[title],
|
2026-03-30 13:11:37 +02:00
|
|
|
],
|
2026-03-30 13:49:00 +02:00
|
|
|
status_badge(label=status_label, tone="scheduled"),
|
2026-03-30 13:11:37 +02:00
|
|
|
],
|
|
|
|
|
h.form(
|
2026-03-30 13:23:36 +02:00
|
|
|
{
|
|
|
|
|
"data-signals": "{_formError: '', _formSuccess: ''}",
|
2026-03-30 13:49:00 +02:00
|
|
|
"data-signals__ifmissing": initial_signals,
|
2026-03-30 13:23:36 +02:00
|
|
|
"data-on:submit": f"@post('{action_path}')",
|
|
|
|
|
},
|
2026-03-30 13:11:37 +02:00
|
|
|
class_="mt-5 space-y-6",
|
|
|
|
|
)[
|
2026-03-30 13:23:36 +02:00
|
|
|
h.div(
|
|
|
|
|
{
|
|
|
|
|
"data-show": "$_formError !== ''",
|
|
|
|
|
"data-text": "$_formError",
|
|
|
|
|
},
|
|
|
|
|
class_="rounded-2xl bg-rose-50 px-4 py-3 text-sm font-medium text-rose-800",
|
|
|
|
|
),
|
|
|
|
|
h.div(
|
|
|
|
|
{
|
|
|
|
|
"data-show": "$_formSuccess !== ''",
|
|
|
|
|
"data-text": "$_formSuccess",
|
|
|
|
|
},
|
|
|
|
|
class_="rounded-2xl bg-emerald-100 px-4 py-3 text-sm font-medium text-emerald-800",
|
|
|
|
|
),
|
2026-03-30 13:11:37 +02:00
|
|
|
h.div(class_="grid gap-4 md:grid-cols-2")[
|
|
|
|
|
input_field(
|
|
|
|
|
label="Source name",
|
|
|
|
|
field_id="source-name",
|
2026-03-30 13:49:00 +02:00
|
|
|
value=_value(source, "name"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="sourceName",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
input_field(
|
|
|
|
|
label="Slug",
|
|
|
|
|
field_id="source-slug",
|
2026-03-30 13:49:00 +02:00
|
|
|
value=slug,
|
2026-03-30 13:11:37 +02:00
|
|
|
help_text="Immutable after creation.",
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="sourceSlug",
|
2026-03-30 13:49:00 +02:00
|
|
|
disabled=mode == "edit",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
h.div[
|
|
|
|
|
h.label(
|
|
|
|
|
for_="source-type",
|
|
|
|
|
class_="block text-sm font-medium text-slate-900",
|
|
|
|
|
)["Source type"],
|
|
|
|
|
h.select(
|
|
|
|
|
{"data-bind": "sourceType"},
|
|
|
|
|
id="source-type",
|
|
|
|
|
name="source-type",
|
|
|
|
|
class_="mt-2 block w-full rounded-2xl border-0 bg-white px-3.5 py-2.5 text-sm text-slate-900 shadow-sm ring-1 ring-slate-200 focus:outline-hidden focus:ring-2 focus:ring-amber-500",
|
|
|
|
|
)[
|
2026-03-30 13:49:00 +02:00
|
|
|
h.option(value="feed", selected=source_type == "feed")[
|
|
|
|
|
"feed"
|
|
|
|
|
],
|
|
|
|
|
h.option(value="pangea", selected=source_type == "pangea")[
|
|
|
|
|
"pangea"
|
|
|
|
|
],
|
2026-03-30 13:11:37 +02:00
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
h.div(
|
|
|
|
|
{"data-show": "$sourceType === 'feed'"},
|
|
|
|
|
class_="space-y-4 rounded-[1.5rem] bg-stone-50 p-5",
|
|
|
|
|
)[
|
|
|
|
|
h.div[
|
|
|
|
|
h.p(
|
|
|
|
|
class_="text-xs font-semibold uppercase tracking-[0.22em] text-amber-600"
|
|
|
|
|
)["Feed source options"],
|
|
|
|
|
h.h3(class_="mt-2 text-lg font-semibold text-slate-950")[
|
|
|
|
|
"Feed settings"
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
h.div(class_="grid gap-4 md:grid-cols-2")[
|
|
|
|
|
input_field(
|
|
|
|
|
label="Feed URL",
|
|
|
|
|
field_id="feed-url",
|
2026-03-30 13:49:00 +02:00
|
|
|
value=_value(source, "feed_url"),
|
2026-03-30 13:11:37 +02:00
|
|
|
placeholder="https://example.com/feed.xml",
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="feedUrl",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
h.div(
|
|
|
|
|
{"data-show": "$sourceType === 'pangea'"},
|
|
|
|
|
class_="space-y-4 rounded-[1.5rem] bg-stone-50 p-5",
|
|
|
|
|
)[
|
|
|
|
|
h.div[
|
|
|
|
|
h.p(
|
|
|
|
|
class_="text-xs font-semibold uppercase tracking-[0.22em] text-amber-600"
|
|
|
|
|
)["Pangea source options"],
|
|
|
|
|
h.h3(class_="mt-2 text-lg font-semibold text-slate-950")[
|
|
|
|
|
"Pangea settings"
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
h.div(class_="grid gap-4 lg:grid-cols-3")[
|
|
|
|
|
input_field(
|
|
|
|
|
label="Pangea domain",
|
|
|
|
|
field_id="pangea-domain",
|
2026-03-30 13:49:00 +02:00
|
|
|
value=_value(source, "pangea_domain"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="pangeaDomain",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
input_field(
|
|
|
|
|
label="Category name",
|
|
|
|
|
field_id="pangea-category",
|
2026-03-30 13:49:00 +02:00
|
|
|
value=_value(source, "pangea_category"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="pangeaCategory",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
select_field(
|
|
|
|
|
label="Content format",
|
|
|
|
|
field_id="content-format",
|
2026-03-30 13:23:36 +02:00
|
|
|
options=PANGEA_CONTENT_FORMATS,
|
2026-03-30 13:49:00 +02:00
|
|
|
selected=_value(source, "content_format", "MOBILE_3"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="contentFormat",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
2026-03-30 13:23:36 +02:00
|
|
|
select_field(
|
2026-03-30 13:11:37 +02:00
|
|
|
label="Content type",
|
|
|
|
|
field_id="content-type",
|
2026-03-30 13:23:36 +02:00
|
|
|
options=PANGEA_CONTENT_TYPES,
|
2026-03-30 13:49:00 +02:00
|
|
|
selected=_value(source, "content_type", "articles"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="contentType",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
input_field(
|
|
|
|
|
label="Max articles",
|
|
|
|
|
field_id="max-articles",
|
2026-03-30 13:49:00 +02:00
|
|
|
value=_value(source, "max_articles", "10"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="maxArticles",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
input_field(
|
|
|
|
|
label="Oldest article (days)",
|
|
|
|
|
field_id="oldest-article",
|
2026-03-30 13:49:00 +02:00
|
|
|
value=_value(source, "oldest_article", "3"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="oldestArticle",
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
h.div(class_="grid gap-4 lg:grid-cols-3")[
|
|
|
|
|
toggle_field(
|
|
|
|
|
label="Only newest",
|
|
|
|
|
description="Limit Pangea syncs to the newest material available in the selected category.",
|
|
|
|
|
signal_name="onlyNewest",
|
2026-03-30 13:49:00 +02:00
|
|
|
checked=_checked(source, "only_newest", True),
|
2026-03-30 13:23:36 +02:00
|
|
|
),
|
|
|
|
|
toggle_field(
|
|
|
|
|
label="Include authors",
|
|
|
|
|
description="Carry author bylines into mirrored output where upstream data exists.",
|
|
|
|
|
signal_name="includeAuthors",
|
2026-03-30 13:49:00 +02:00
|
|
|
checked=_checked(source, "include_authors", True),
|
2026-03-30 13:23:36 +02:00
|
|
|
),
|
|
|
|
|
toggle_field(
|
|
|
|
|
label="Exclude media",
|
|
|
|
|
description="Skip image and media attachment mirroring for this source.",
|
|
|
|
|
signal_name="excludeMedia",
|
2026-03-30 13:49:00 +02:00
|
|
|
checked=_checked(source, "exclude_media", False),
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
2026-03-30 13:37:25 +02:00
|
|
|
toggle_field(
|
|
|
|
|
label="Include content",
|
|
|
|
|
description="Store article body content in mirrored output when the upstream provides it.",
|
|
|
|
|
signal_name="includeContent",
|
2026-03-30 13:49:00 +02:00
|
|
|
checked=_checked(source, "include_content", True),
|
2026-03-30 13:37:25 +02:00
|
|
|
),
|
2026-03-30 13:11:37 +02:00
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
h.div(class_="grid gap-4 lg:grid-cols-2")[
|
|
|
|
|
textarea_field(
|
|
|
|
|
label="Notes",
|
|
|
|
|
field_id="source-notes",
|
2026-03-30 13:49:00 +02:00
|
|
|
value=_value(source, "notes"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="sourceNotes",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
textarea_field(
|
|
|
|
|
label="Spider arguments",
|
|
|
|
|
field_id="spider-arguments",
|
2026-03-30 15:51:48 +02:00
|
|
|
value=_value(source, "spider_arguments"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="spiderArguments",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
h.div(
|
|
|
|
|
class_="grid gap-6 xl:grid-cols-[minmax(0,1.3fr)_minmax(20rem,0.9fr)]"
|
|
|
|
|
)[
|
|
|
|
|
h.div(class_="rounded-[1.5rem] bg-stone-50 p-5")[
|
|
|
|
|
h.div[
|
|
|
|
|
h.h3(class_="text-lg font-semibold text-slate-950")[
|
|
|
|
|
"Cron schedule"
|
|
|
|
|
],
|
|
|
|
|
h.p(class_="mt-1 text-sm text-slate-600")[
|
|
|
|
|
"Stored in UTC and displayed in the browser timezone."
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
h.div(class_="mt-5 grid gap-4 sm:grid-cols-2 xl:grid-cols-5")[
|
|
|
|
|
input_field(
|
|
|
|
|
label="Minute",
|
|
|
|
|
field_id="cron-minute",
|
2026-03-30 13:49:00 +02:00
|
|
|
value=_value(source, "cron_minute", "*/30"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="cronMinute",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
input_field(
|
|
|
|
|
label="Hour",
|
|
|
|
|
field_id="cron-hour",
|
2026-03-30 13:49:00 +02:00
|
|
|
value=_value(source, "cron_hour", "*"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="cronHour",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
input_field(
|
|
|
|
|
label="Day of month",
|
|
|
|
|
field_id="cron-day-of-month",
|
2026-03-30 13:49:00 +02:00
|
|
|
value=_value(source, "cron_day_of_month", "*"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="cronDayOfMonth",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
input_field(
|
|
|
|
|
label="Day of week",
|
|
|
|
|
field_id="cron-day-of-week",
|
2026-03-30 13:49:00 +02:00
|
|
|
value=_value(source, "cron_day_of_week", "*"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="cronDayOfWeek",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
input_field(
|
|
|
|
|
label="Month",
|
|
|
|
|
field_id="cron-month",
|
2026-03-30 13:49:00 +02:00
|
|
|
value=_value(source, "cron_month", "*"),
|
2026-03-30 13:23:36 +02:00
|
|
|
signal_name="cronMonth",
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
h.div(class_="rounded-[1.5rem] bg-stone-50 p-5")[
|
|
|
|
|
h.p(
|
|
|
|
|
class_="text-xs font-semibold uppercase tracking-[0.22em] text-amber-600"
|
|
|
|
|
)["Job defaults"],
|
|
|
|
|
h.h3(class_="mt-2 text-lg font-semibold text-slate-950")[
|
|
|
|
|
"Initial job state"
|
|
|
|
|
],
|
|
|
|
|
h.div(class_="mt-5 grid gap-4")[
|
|
|
|
|
toggle_field(
|
|
|
|
|
label="Job enabled",
|
|
|
|
|
description="Scheduler will consider the new job immediately after creation.",
|
|
|
|
|
signal_name="jobEnabled",
|
2026-03-30 13:49:00 +02:00
|
|
|
checked=_checked(source, "enabled", True),
|
2026-03-30 13:11:37 +02:00
|
|
|
),
|
2026-03-30 18:26:02 +02:00
|
|
|
toggle_field(
|
|
|
|
|
label="Convert images",
|
|
|
|
|
description="Normalize mirrored images through the image conversion pipeline for this source.",
|
|
|
|
|
signal_name="convertImages",
|
|
|
|
|
checked=_checked(source, "convert_images", True),
|
|
|
|
|
),
|
|
|
|
|
toggle_field(
|
|
|
|
|
label="Convert video",
|
|
|
|
|
description="Run mirrored videos through the video conversion pipeline for this source.",
|
|
|
|
|
signal_name="convertVideo",
|
|
|
|
|
checked=_checked(source, "convert_video", True),
|
|
|
|
|
),
|
2026-03-30 13:11:37 +02:00
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
h.div(
|
|
|
|
|
class_="flex flex-wrap justify-end gap-3 border-t border-slate-200 pt-6"
|
|
|
|
|
)[
|
|
|
|
|
muted_action_link(href="/sources", label="Cancel"),
|
|
|
|
|
h.button(
|
2026-03-30 13:23:36 +02:00
|
|
|
type="submit",
|
2026-03-30 13:11:37 +02:00
|
|
|
class_="rounded-full bg-slate-950 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-slate-800",
|
2026-03-30 13:49:00 +02:00
|
|
|
)[submit_label],
|
2026-03-30 13:11:37 +02:00
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 18:26:02 +02:00
|
|
|
def create_source_page(
|
|
|
|
|
*,
|
|
|
|
|
action_path: str = "/actions/sources/create",
|
|
|
|
|
source_count: int = 0,
|
|
|
|
|
running_count: int = 0,
|
|
|
|
|
) -> Renderable:
|
2026-03-30 13:11:37 +02:00
|
|
|
actions = (
|
|
|
|
|
muted_action_link(href="/sources", label="Back to sources"),
|
|
|
|
|
header_action_link(href="/runs", label="View runs"),
|
|
|
|
|
)
|
|
|
|
|
return page_shell(
|
|
|
|
|
current_path="/sources/create",
|
|
|
|
|
eyebrow="Source creation",
|
|
|
|
|
title="Create source",
|
2026-03-30 13:49:00 +02:00
|
|
|
actions=actions,
|
2026-03-30 18:26:02 +02:00
|
|
|
source_count=source_count,
|
|
|
|
|
running_count=running_count,
|
2026-03-30 13:49:00 +02:00
|
|
|
content=source_form(mode="create", action_path=action_path),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def edit_source_page(
|
|
|
|
|
*,
|
|
|
|
|
slug: str,
|
|
|
|
|
source: Mapping[str, object],
|
|
|
|
|
action_path: str,
|
2026-03-30 18:26:02 +02:00
|
|
|
source_count: int = 0,
|
|
|
|
|
running_count: int = 0,
|
2026-03-30 13:49:00 +02:00
|
|
|
) -> Renderable:
|
|
|
|
|
actions = (
|
|
|
|
|
muted_action_link(href="/sources", label="Back to sources"),
|
|
|
|
|
header_action_link(href="/runs", label="View runs"),
|
|
|
|
|
)
|
|
|
|
|
return page_shell(
|
|
|
|
|
current_path=f"/sources/{slug}/edit",
|
|
|
|
|
eyebrow="Source editing",
|
|
|
|
|
title="Edit source",
|
2026-03-30 13:11:37 +02:00
|
|
|
actions=actions,
|
2026-03-30 18:26:02 +02:00
|
|
|
source_count=source_count,
|
|
|
|
|
running_count=running_count,
|
2026-03-30 13:49:00 +02:00
|
|
|
content=source_form(mode="edit", action_path=action_path, source=source),
|
2026-03-30 13:11:37 +02:00
|
|
|
)
|