from __future__ import annotations 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, ) SOURCES: tuple[dict[str, str], ...] = ( { "name": "Guardian feed mirror", "slug": "guardian-feed", "source_type": "Feed", "upstream": "https://guardianproject.info/feed.xml", "schedule": "Every 30 minutes", "last_run": "Succeeded 53m ago", "state": "Enabled", "state_tone": "scheduled", }, { "name": "Pangea mobile articles", "slug": "pangea-mobile", "source_type": "Pangea", "upstream": "guardianproject.info / News", "schedule": "Every 4 hours", "last_run": "Running now", "state": "Enabled", "state_tone": "running", }, { "name": "Podcast enclosure mirror", "slug": "podcast-audio", "source_type": "Feed", "upstream": "https://guardianproject.info/podcast/podcast.xml", "schedule": "Paused", "last_run": "Failed 2h ago", "state": "Disabled", "state_tone": "idle", }, ) def _source_row(source: dict[str, str]) -> tuple[Node, ...]: return ( h.div[ h.div(class_="font-semibold text-slate-950")[source["name"]], h.p(class_="mt-1 font-mono text-xs text-slate-500")[source["slug"]], ], h.p(class_="font-medium whitespace-nowrap text-slate-900")[ source["source_type"] ], h.p(class_="max-w-sm truncate font-mono text-xs text-slate-600")[ source["upstream"] ], h.p(class_="font-medium whitespace-nowrap text-slate-900")[source["schedule"]], h.div(class_="min-w-32 whitespace-normal")[ status_badge(label=source["state"], tone=source["state_tone"]), h.p(class_="mt-2 text-xs text-slate-500")[source["last_run"]], ], h.div(class_="flex flex-nowrap items-center gap-3")[ inline_link(href="/sources/create", label="Edit", tone="amber"), inline_link(href="/runs", label="View runs"), ], ) def sources_table() -> Renderable: rows = tuple(_source_row(source) for source in SOURCES) return table_section( eyebrow="Inventory", title="Sources", subtitle="Configured feed and Pangea sources live here as tables, with clear schedule and job state visibility instead of card-based CRUD.", headers=("Source", "Type", "Upstream", "Schedule", "Job state", "Actions"), rows=rows, actions=header_action_link(href="/sources/create", label="Create source"), ) def sources_page() -> Renderable: return page_shell( current_path="/sources", eyebrow="Source management", title="Sources", description="Configured feed and Pangea sources live here as tables, with clear schedule and job state visibility instead of card-based CRUD.", actions=header_action_link(href="/sources/create", label="Create source"), content=sources_table(), ) def create_source_form() -> Renderable: 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" )["Create"], h.h2(class_="mt-2 text-xl font-semibold text-slate-950")[ "Source and job setup" ], h.p(class_="mt-2 max-w-3xl text-sm text-slate-600")[ "The create flow lives on its own page and creates the source plus its paired job record. This pass is visual only, but the fields already reflect the intended shape." ], ], status_badge(label="New source", tone="scheduled"), ], h.form( {"data-signals__ifmissing": "{sourceType: 'pangea'}"}, class_="mt-5 space-y-6", )[ h.div(class_="grid gap-4 md:grid-cols-2")[ input_field( label="Source name", field_id="source-name", value="Pangea mobile articles", ), input_field( label="Slug", field_id="source-slug", value="pangea-mobile", help_text="Immutable after creation.", ), 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", )[ h.option(value="feed")["feed"], h.option(value="pangea", selected=True)["pangea"], ], ], ], 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.p(class_="mt-2 text-sm text-slate-600")[ "Shown only when the source type is set to feed." ], ], h.div(class_="grid gap-4 md:grid-cols-2")[ input_field( label="Feed URL", field_id="feed-url", placeholder="https://example.com/feed.xml", ), ], ], 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.p(class_="mt-2 text-sm text-slate-600")[ "Shown only when the source type is set to pangea." ], ], h.div(class_="grid gap-4 lg:grid-cols-3")[ input_field( label="Pangea domain", field_id="pangea-domain", value="guardianproject.info", ), input_field( label="Category name", field_id="pangea-category", value="News", ), select_field( label="Content format", field_id="content-format", options=("MOBILE_3", "MOBILE_2", "WEB"), selected="MOBILE_3", ), input_field( label="Content type", field_id="content-type", value="articles", ), input_field( label="Max articles", field_id="max-articles", value="10", ), input_field( label="Oldest article (days)", field_id="oldest-article", value="3", ), ], ], h.div(class_="grid gap-4 lg:grid-cols-2")[ textarea_field( label="Notes", field_id="source-notes", value="Primary Pangea mobile article mirror for the operator landing page.", ), textarea_field( label="Spider arguments", field_id="spider-arguments", value="language=en,download_media=true", ), ], 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", value="15", ), input_field( label="Hour", field_id="cron-hour", value="*/4", ), input_field( label="Day of month", field_id="cron-day-of-month", value="*", ), input_field( label="Day of week", field_id="cron-day-of-week", value="1-6", ), input_field( label="Month", field_id="cron-month", value="*", ), ], ], 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", checked=True, ), toggle_field( label="Only newest", description="Limit Pangea syncs to the newest material available in the selected category.", signal_name="onlyNewest", checked=True, ), toggle_field( label="Include authors", description="Carry author bylines into mirrored output where upstream data exists.", signal_name="includeAuthors", checked=True, ), toggle_field( label="Exclude media", description="Skip image and media attachment mirroring for this source.", signal_name="excludeMedia", checked=False, ), ], ], ], 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( type="button", class_="rounded-full bg-slate-950 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-slate-800", )["Create source"], ], ], ) ) def create_source_page() -> Renderable: 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", description="Dedicated create page for the source form. The list page stays focused on scanning existing sources, while this page handles the new source and job configuration flow.", actions=actions, content=create_source_form(), )