"""Server-rendered HTML. One embedded stylesheet, no JavaScript beyond a confirm() on the destructive buttons.""" from __future__ import annotations import html from datetime import date from .. import util from ..settings import DEFAULTS, EDITABLE, MASKED_KEYS STYLE = """ :root { --bg: #14161a; --panel: #1c1f26; --line: #2c313b; --text: #e6e8ec; --muted: #99a0ae; --accent: #6aa9ff; --warn: #ffb454; --bad: #ff6b6b; --good: #6ade9b; } * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; } main { max-width: 62rem; margin: 0 auto; padding: 1.5rem 1rem 4rem; } h1 { font-size: 1.4rem; margin: 0; } h2 { font-size: 1.05rem; margin: 2rem 0 .75rem; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; } header { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; border-bottom: 1px solid var(--line); padding-bottom: .75rem; } header .sub { color: var(--muted); font-size: .85rem; } a { color: var(--accent); } .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 1rem; } table { width: 100%; border-collapse: collapse; } th, td { text-align: left; padding: .55rem .5rem; border-bottom: 1px solid var(--line); vertical-align: middle; } th { color: var(--muted); font-weight: 600; font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; } td.num { text-align: right; font-variant-numeric: tabular-nums; } .muted { color: var(--muted); } .badge { display: inline-block; padding: .1rem .45rem; border-radius: 999px; font-size: .75rem; border: 1px solid currentColor; } .badge.warn { color: var(--warn); } .badge.good { color: var(--good); } .badge.bad { color: var(--bad); } input[type=text], input[type=password], input[type=number], select { background: #12141a; color: var(--text); border: 1px solid var(--line); border-radius: 7px; padding: .45rem .55rem; font: inherit; width: 100%; } button { background: var(--accent); color: #0b1017; border: 0; border-radius: 7px; padding: .5rem .9rem; font: inherit; font-weight: 600; cursor: pointer; } button.secondary { background: #2b3140; color: var(--text); } button.danger { background: transparent; color: var(--bad); border: 1px solid var(--bad); font-weight: 500; padding: .3rem .6rem; } button.link { background: transparent; color: var(--accent); border: 0; padding: .3rem .4rem; font-weight: 500; } form.inline { display: inline; } .row { display: flex; gap: .6rem; align-items: center; } .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: .85rem; } label { display: block; font-size: .82rem; color: var(--muted); margin-bottom: .25rem; } .field { margin-bottom: .3rem; } .error { color: var(--bad); font-size: .8rem; margin-top: .2rem; } .flash { border-radius: 8px; padding: .6rem .8rem; margin-bottom: 1rem; border: 1px solid; } .flash.ok { color: var(--good); border-color: var(--good); } .flash.bad { color: var(--bad); border-color: var(--bad); } .login { max-width: 21rem; margin: 6rem auto; } footer { margin-top: 2.5rem; color: var(--muted); font-size: .8rem; } @media (max-width: 40rem) { th.hide, td.hide { display: none; } } """ def _e(value) -> str: return html.escape("" if value is None else str(value), quote=True) def page(title: str, body: str) -> bytes: return f""" {_e(title)}
{body}
""".encode("utf-8") def login_page(error: str | None = None) -> bytes: alert = f'
{_e(error)}
' if error else "" body = f"""

youtube-automate

Sign in to manage subscriptions.

{alert}
""" return page("Sign in — youtube-automate", body) def _channel_row(channel: dict, csrf: str) -> str: failures = channel["consecutive_poll_failures"] if failures > 2: badge = f'{failures} failed polls' elif channel["last_poll_ok"] == 0: badge = 'last poll failed' else: badge = "" retention = channel["retention_days"] retention_value = "" if retention is None else str(retention) placeholder = f"default ({channel['global_retention']})" return f""" {_e(channel['title'])} {badge}
{_e(channel['handle'] or channel['channel_id'])} {channel['downloaded']} {_e(util.human_bytes(channel['bytes']))} {_e(channel['latest'] or '—')} {_e(channel['last_polled_at'] or 'never')}
""" def _settings_form(values: dict, errors: dict, csrf: str) -> str: fields = [] for key in EDITABLE: value = values.get(key, DEFAULTS[key]) error = errors.get(key) if key in MASKED_KEYS and value: shown, placeholder = "", "stored — leave blank to keep" else: shown, placeholder = value, "" input_type = "password" if key in MASKED_KEYS else "text" fields.append( f"""
{f'
{_e(error)}
' if error else ''}
""" ) return f"""
{''.join(fields)}
""" def index_page( *, channels: list[dict], settings_values: dict, settings_errors: dict, csrf: str, flash: tuple[str, str] | None = None, add_error: str | None = None, queue_depth: int = 0, ) -> bytes: flash_html = "" if flash: kind, message = flash flash_html = f'
{_e(message)}
' if channels: rows = "".join(_channel_row(channel, csrf) for channel in channels) table = f"""
{rows}
ChannelOn diskSize Latest uploadLast poll Retention (days)
""" else: table = '
No channels yet. Add one below.
' add_error_html = f'
{_e(add_error)}
' if add_error else "" body = f"""

youtube-automate

{len(channels)} channel(s) · {queue_depth} queued ·
{flash_html}

Channels

{table}

Add a channel

{add_error_html}

Settings

{_settings_form(settings_values, settings_errors, csrf)} """ return page("youtube-automate", body)