@kurtbruns/kestrel 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -1
- package/README.md +1 -1
- package/dist/public/dashboard/app.js +148 -148
- package/dist/public/dashboard/app.js.map +3 -3
- package/dist/public/dashboard/index.html +2 -2
- package/dist/public/dashboard/styles.css +23 -0
- package/dist/worker/index.js +2197 -480
- package/docs/README.md +1 -0
- package/docs/get-started/04-ses.md +1 -1
- package/docs/get-started/06-go-live.md +5 -2
- package/docs/reference/01-configuration.md +7 -1
- package/docs/reference/03-import-format.md +103 -0
- package/migrations/0005_imports.sql +74 -0
- package/migrations/0006_deliveries_fewer_indexes.sql +19 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../client/shell.ts", "../../../client/lifecycle.ts", "../../../client/ui/dom.ts", "../../../client/ui/html.ts", "../../../client/access_locked.ts", "../../../client/config_problem.ts", "../../../client/state.ts", "../../../client/auth.ts", "../../../client/api.ts", "../../../shared/publication.ts", "../../../client/brand.ts", "../../../shared/archive_url.ts", "../../../shared/sends.ts", "../../../client/deployment.ts", "../../../client/ui/icons/article.svg", "../../../client/ui/icons/bold.svg", "../../../client/ui/icons/book.svg", "../../../client/ui/icons/branch.svg", "../../../client/ui/icons/check.svg", "../../../client/ui/icons/circle-check.svg", "../../../client/ui/icons/circle-minus.svg", "../../../client/ui/icons/code.svg", "../../../client/ui/icons/copyout.svg", "../../../client/ui/icons/desktop.svg", "../../../client/ui/icons/editable.svg", "../../../client/ui/icons/heading.svg", "../../../client/ui/icons/indent.svg", "../../../client/ui/icons/info.svg", "../../../client/ui/icons/info-filled.svg", "../../../client/ui/icons/italic.svg", "../../../client/ui/icons/kestrel.svg", "../../../client/ui/icons/lines.svg", "../../../client/ui/icons/link.svg", "../../../client/ui/icons/mail.svg", "../../../client/ui/icons/ol.svg", "../../../client/ui/icons/paperclip.svg", "../../../client/ui/icons/phone.svg", "../../../client/ui/icons/plus.svg", "../../../client/ui/icons/preview.svg", "../../../client/ui/icons/quote.svg", "../../../client/ui/icons/readonly.svg", "../../../client/ui/icons/search.svg", "../../../client/ui/icons/send.svg", "../../../client/ui/icons/ul.svg", "../../../client/ui/icons/upload.svg", "../../../client/ui/icons/x.svg", "../../../client/ui/icons.ts", "../../../client/room/shell.ts", "../../../shared/dashboard_link.ts", "../../../shared/cursor.ts", "../../../client/send_state.ts", "../../../shared/email.ts", "../../../client/ui/format.ts", "../../../client/ui/list_controls.ts", "../../../client/ui/widgets.ts", "../../../client/posts/drafts.ts", "../../../client/sends/progress.ts", "../../../client/subscribers/dialogs.ts", "../../../client/ui/copy.ts", "../../../client/dashboard/dashboard.ts", "../../../client/sends/dialogs.ts", "../../../shared/page_policy.ts", "../../../client/ui/email_frame.ts", "../../../client/ui/highlight.ts", "../../../client/posts/autosave.ts", "../../../client/posts/dirty.ts", "../../../client/posts/pending_save.ts", "../../../client/posts/revisions.ts", "../../../client/posts/web.ts", "../../../client/posts/editor.ts", "../../../client/room/docs.ts", "../../../client/room/curl.ts", "../../../client/room/reference.ts", "../../../client/sends/list.ts", "../../../client/sends/record.ts", "../../../client/ui/savebar.ts", "../../../client/settings/page.ts", "../../../client/settings/confirmation.ts", "../../../client/settings/deploy.ts", "../../../client/settings/remake.ts", "../../../shared/email_logo.ts", "../../../client/settings/template.ts", "../../../client/settings/emails.ts", "../../../client/ui/square_image.ts", "../../../client/settings/identity.ts", "../../../client/settings/notifications.ts", "../../../shared/reader_pages.ts", "../../../client/settings/reader_pages.ts", "../../../client/settings/subscribe.ts", "../../../client/settings/settings.ts", "../../../client/subscribers/list.ts", "../../../client/router.ts", "../../../client/main.ts"],
|
|
4
|
-
"sourcesContent": ["// The app shell: the DOM roots every view renders into, and the sidebar's drawer and\n// collapse modes, which installShell() wires when boot calls it.\n\n/** A root the page always carries (public/dashboard/index.html); missing means the wrong page. */\nfunction root(id: string): HTMLElement {\n const el = document.getElementById(id);\n if (!el) {\n throw new Error(`the admin page has no #${id}`);\n }\n return el;\n}\n\nexport const app = root(\"app\");\nexport const identity = root(\"identity\");\nexport const toasts = root(\"toasts\");\n\n// Mobile nav drawer: the hamburger slides the sidebar in; the scrim or any nav\n// click closes it. On desktop the sidebar is always in view and these are inert.\nconst navToggle = document.getElementById(\"navToggle\");\nconst navScrim = document.getElementById(\"navScrim\");\nexport function setNavOpen(open: boolean): void {\n document.body.classList.toggle(\"nav-open\", open);\n navToggle?.setAttribute(\"aria-expanded\", open ? \"true\" : \"false\");\n if (navScrim) {\n navScrim.hidden = !open;\n }\n}\n// Collapsible sidebar. The effective width mode lives in data-nav on <html> (\"full\"\n// vs \"rail\"); CSS keys off it. By default the mode follows the viewport width (the\n// <head> guard in index.html sets it before first paint). The footer chevron toggles a\n// manual override, but that override is deliberately session-only \u2014 held in memory, not\n// stored \u2014 so a plain reload always drops back to the width-based default. That gives a\n// one-keystroke way back to \"auto\" and avoids a saved preference getting stuck fighting\n// the width across sizes. The chevron is hidden below 540, where the sidebar is an\n// off-canvas overlay driven by the hamburger instead.\nconst navCollapse = document.getElementById(\"navCollapse\");\n// null = follow the width; \"collapsed\" / \"expanded\" = manual override for this load.\nlet navOverride: \"collapsed\" | \"expanded\" | null = null;\nfunction computeNavMode(): \"full\" | \"rail\" {\n if (window.innerWidth < 540) {\n return \"full\";\n }\n if (navOverride === \"collapsed\") {\n return \"rail\";\n }\n if (navOverride === \"expanded\") {\n return \"full\";\n }\n return window.innerWidth <= 1024 ? \"rail\" : \"full\";\n}\nfunction applyNavMode(): void {\n const mode = computeNavMode();\n document.documentElement.dataset.nav = mode;\n if (!navCollapse) {\n return;\n }\n const expanded = mode !== \"rail\";\n const label = expanded ? \"Collapse sidebar\" : \"Expand sidebar\";\n navCollapse.setAttribute(\"aria-expanded\", expanded ? \"true\" : \"false\");\n navCollapse.setAttribute(\"aria-label\", label);\n navCollapse.setAttribute(\"title\", label);\n}\nlet navResizeRaf = 0;\nlet navResizeSettle = 0;\n\n/** Wire the drawer, the collapse chevron, and the resize tracking; boot calls this once. */\nexport function installShell(): void {\n navToggle?.addEventListener(\"click\", () =>\n setNavOpen(!document.body.classList.contains(\"nav-open\")),\n );\n navScrim?.addEventListener(\"click\", () => setNavOpen(false));\n document.querySelector(\".sidebar\")?.addEventListener(\"click\", (e) => {\n if (e.target instanceof Element && e.target.closest(\"a\")) {\n setNavOpen(false);\n }\n });\n navCollapse?.addEventListener(\"click\", () => {\n navOverride = document.documentElement.dataset.nav !== \"rail\" ? \"collapsed\" : \"expanded\";\n applyNavMode();\n });\n // Track the responsive default as the window resizes (rAF-coalesced). A session\n // override still wins at 540 and up; below it, the mobile overlay takes over. The\n // nav-resizing class suppresses the sidebar's own transitions for the duration, so a\n // breakpoint cross (rail \u2192 mobile drawer) snaps instead of animating a stray slide.\n window.addEventListener(\"resize\", () => {\n document.body.classList.add(\"nav-resizing\");\n clearTimeout(navResizeSettle);\n navResizeSettle = setTimeout(() => {\n document.body.classList.remove(\"nav-resizing\");\n }, 200);\n if (navResizeRaf) {\n return;\n }\n navResizeRaf = requestAnimationFrame(() => {\n navResizeRaf = 0;\n applyNavMode();\n });\n });\n applyNavMode();\n}\n", "// The view lifecycle: one view is mounted at a time, into its own root under #app, with\n// an AbortSignal that ends with it. Everything a view starts that could outlive it, a\n// timer, a poll, a read that paints, a bar it borrowed, is bound to that signal, so\n// navigating away tears the view down by construction, and the view never has to know\n// whether it is still the one on screen.\n\nimport { app } from \"./shell\";\n\n/**\n * What a mounted view offers the app around it. Every member is optional: a list view\n * offers nothing; the editor offers all three.\n */\nexport interface ViewHandle {\n /** Unsaved work the browser should warn about before the tab unloads. */\n dirty?(): boolean;\n /**\n * Asked once before a navigation away. The view may save in the background here and\n * answer \"leave\"; \"confirm\" has the router ask the reader first, and a declined prompt\n * keeps the view mounted.\n */\n beforeLeave?(): \"leave\" | \"confirm\";\n /** \u2318S / Ctrl-S. */\n manualSave?(): void;\n}\n\n// What a view hands back: a handle, or nothing (the async ones resolve to nothing, so\n// this is `void`, not `undefined`, and the union is the honest type of that).\n// biome-ignore lint/suspicious/noConfusingVoidType: see above\ntype Offered = ViewHandle | void;\n\n/** A view: paints into `root`, binds what it starts to `signal`, and may return a handle. */\nexport type View = (root: HTMLElement, signal: AbortSignal) => Offered | Promise<Offered>;\n\nlet controller: AbortController | null = null;\nlet handle: ViewHandle | null = null;\n\n/**\n * Tear the mounted view down and mount `view` in its place. The previous view's signal\n * aborts first, then a fresh root replaces its markup, so a stale continuation (a read\n * that resolved after the abort) paints into a detached element and nobody sees it. A\n * view re-enters itself the same way (a reload, a retry): the same abort, a fresh root.\n */\nexport function mount(view: View): Promise<void> {\n controller?.abort();\n handle = null;\n const c = new AbortController();\n controller = c;\n const root = document.createElement(\"div\");\n root.className = \"view\";\n app.replaceChildren(root);\n // The view starts now, not in a microtask: a second mount in the same tick must find\n // it started (its signal listeners in place), or the abort would reach a view that\n // never ran and a promise that never settles.\n let outcome: Promise<Offered>;\n try {\n outcome = Promise.resolve(view(root, c.signal));\n } catch (e) {\n outcome = Promise.reject(e);\n }\n return outcome.then(\n (h) => {\n if (!c.signal.aborted && h) {\n handle = h;\n }\n },\n (e) => {\n // A view cut off mid-load is not an error; anything else is the view's own bug.\n if (!c.signal.aborted) {\n throw e;\n }\n },\n );\n}\n\n/** Tear the mounted view down and leave #app empty (the re-auth wall paints its own). */\nexport function unmount(): void {\n controller?.abort();\n controller = null;\n handle = null;\n app.replaceChildren();\n}\n\n/** The mounted view's handle, for the router's guards; null while nothing offers one. */\nexport function mounted(): ViewHandle | null {\n return handle;\n}\n\n/** Run `fn` every `ms` until the signal aborts. */\nexport function every(ms: number, fn: () => void, signal: AbortSignal): void {\n if (signal.aborted) {\n return;\n }\n const id = setInterval(fn, ms);\n signal.addEventListener(\"abort\", () => clearInterval(id), { once: true });\n}\n\n/**\n * Run `fn` once at the wall-clock moment `time`, unless the signal aborts first. For a\n * change that belongs to an exact moment (a review window closing at the fire time), which a\n * once-a-second tick would reach up to a second late. Only for moments close at hand: a\n * timer's delay tops out near 25 days, past which it would fire at once.\n */\nexport function at(time: number, fn: () => void, signal: AbortSignal): void {\n if (signal.aborted) {\n return;\n }\n const cancel = () => clearTimeout(id);\n const id = setTimeout(\n () => {\n signal.removeEventListener(\"abort\", cancel);\n fn();\n },\n Math.max(0, time - Date.now()),\n );\n signal.addEventListener(\"abort\", cancel, { once: true });\n}\n\n// A tick's answer: `false` ends the poll; anything else (nothing, for a tick that always\n// continues) keeps it going.\n// biome-ignore lint/suspicious/noConfusingVoidType: see above\ntype Again = boolean | void;\n\n/**\n * A repeating read that never overlaps itself: `tick` runs `ms` after the previous one\n * finished, until the signal aborts or a tick answers `false`. A tick's own reads take\n * the signal, so one in flight is cut off by navigation; a tick that throws yields to the\n * next. A cut-off read and a refused one (the API answered non-2xx) are the expected\n * failures and stay quiet; anything else is a bug in the tick and is reported, so a\n * broken paint cannot hide inside a poll that keeps retrying.\n */\nexport function poll(ms: number, tick: () => Promise<Again>, signal: AbortSignal): void {\n const arm = () => {\n if (signal.aborted) {\n return;\n }\n const cancel = () => clearTimeout(id);\n const id = setTimeout(async () => {\n signal.removeEventListener(\"abort\", cancel);\n let again: boolean | undefined = true;\n try {\n again = (await tick()) ?? true;\n } catch (e) {\n if (!isExpectedFailure(e)) {\n reportError(e);\n }\n }\n if (again !== false) {\n arm();\n }\n }, ms);\n signal.addEventListener(\"abort\", cancel, { once: true });\n };\n arm();\n}\n\n/** Run `fn` when the signal aborts (at once if it already has). */\nexport function onAbort(signal: AbortSignal, fn: () => void): void {\n if (signal.aborted) {\n fn();\n return;\n }\n signal.addEventListener(\"abort\", fn, { once: true });\n}\n\n// A read cut off by the signal (the DOMException fetch rejects with), or one the API\n// refused (client/api.ts's ApiError, matched by name: importing it here would put\n// lifecycle on a cycle through api and auth).\nfunction isExpectedFailure(e: unknown): boolean {\n return e instanceof Error && (e.name === \"AbortError\" || e.name === \"ApiError\");\n}\n", "// Querying markup a module itself rendered: the ids and classes are the module's own, so\n// an element that is missing is a bug in its template, not a state to handle. These fail\n// loud, naming the selector, instead of returning null for every call site to re-check.\n\n/** The one element a selector matches under `root`, or a loud failure naming it. */\nexport function $<T extends Element = HTMLElement>(\n selector: string,\n root: ParentNode = document,\n): T {\n const el = root.querySelector<T>(selector);\n if (!el) {\n throw new Error(`no element matches ${selector}`);\n }\n return el;\n}\n\n/** Every element a selector matches under `root`, as an array. */\nexport function $$<T extends Element = HTMLElement>(\n selector: string,\n root: ParentNode = document,\n): T[] {\n return [...root.querySelectorAll<T>(selector)];\n}\n", "// Markup as a value. html`\u2026` builds it and escapes every interpolation that is not already\n// markup, so the safe path is the default: a subject, an email, a template a publisher typed\n// can never break out of the element it is shown in. The unsafe path has a name to grep for.\n\nconst ESCAPES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\n/** Escape text for an HTML text node or a quoted attribute value. */\nexport function escapeHtml(value: string | number): string {\n return String(value).replace(/[&<>\"']/g, (c) => ESCAPES[c] ?? c);\n}\n\n/**\n * Markup that is safe to put in the document. Only `html`, `unsafeHtml`, and what they\n * build produce one: the declared private field makes the type nominal, so a look-alike\n * object cannot pass as markup at compile time, and `instanceof` is what the runtime\n * checks, which a look-alike cannot pass either. An object rather than a branded string so\n * nesting can tell markup from text and never escape it twice. Frozen, so what was built is\n * what is set.\n */\nexport class Html {\n private declare readonly brand: undefined;\n readonly markup: string;\n constructor(markup: string) {\n this.markup = markup;\n Object.freeze(this);\n }\n toString(): string {\n return this.markup;\n }\n}\n\n/**\n * What an interpolation may be. Arrays flatten; `null`, `undefined`, and `false` render\n * nothing, so `${cond && html\\`\u2026\\`}` reads plainly. `true` is not admitted: a bare boolean\n * in markup is a bug, not a value.\n */\nexport type Interpolation = Html | string | number | false | null | undefined | Interpolation[];\n\nfunction toMarkup(value: Interpolation): string {\n if (value instanceof Html) {\n return value.markup;\n }\n if (value == null || value === false) {\n return \"\";\n }\n if (Array.isArray(value)) {\n return value.map(toMarkup).join(\"\");\n }\n return escapeHtml(value);\n}\n\n// A literal chunk that ends in `=` (optionally followed by whitespace) puts the next\n// interpolation in an unquoted attribute value, where nothing escaping can do makes it safe.\nconst UNQUOTED_ATTRIBUTE = /=\\s*$/;\n\n/**\n * Build markup. Escaping makes exactly two contexts safe: text, and a quoted attribute\n * value whose meaning is text. It does nothing for a value that is code or a URL: not\n * `href`, `src`, `action`, `formaction` (a `javascript:` URL has no character to escape;\n * check the scheme first), not `srcdoc`, `style`, or an `on*` handler, and never inside\n * `<script>` or `<style>`, an attribute name, or a tag name. An interpolation in an\n * unquoted attribute value throws. A literal with an invalid escape sequence (`\\u`, `\\x`,\n * `\\1`) throws rather than vanish: that is what the cooked string would do.\n */\nexport function html(strings: TemplateStringsArray, ...values: Interpolation[]): Html {\n let out = chunk(strings, 0);\n for (let i = 0; i < values.length; i++) {\n const value = values[i];\n if (!(value instanceof Html) && UNQUOTED_ATTRIBUTE.test(out)) {\n throw new Error(\n \"html: an interpolation in an unquoted attribute value cannot be made safe; quote it\",\n );\n }\n out += toMarkup(value) + chunk(strings, i + 1);\n }\n return new Html(out);\n}\n\nfunction chunk(strings: TemplateStringsArray, i: number): string {\n const s = strings[i];\n if (s === undefined) {\n throw new Error(\n `html: invalid escape sequence in template text near ${JSON.stringify(strings.raw[i])}`,\n );\n }\n return s;\n}\n\n/**\n * Markup the tag did not build: server-rendered HTML the API returns, or markup a module\n * still assembles by hand. The caller vouches for it. Grep for this name when auditing.\n */\nexport function unsafeHtml(markup: string): Html {\n return new Html(markup);\n}\n\n/**\n * Put markup in an element: the one place strings reach innerHTML. The runtime check\n * holds the line where the type checker cannot see (a spec's cast, a value that crossed\n * an `unknown`): a string here is a bug, and it fails loud rather than render the word\n * \"undefined\".\n */\nexport function setHtml(el: Element, markup: Html): void {\n if (!(markup instanceof Html)) {\n throw new Error(\n \"setHtml: expected Html; build it with html`\u2026` or vouch for it with unsafeHtml()\",\n );\n }\n el.innerHTML = markup.markup;\n}\n", "// The wall a deployment with no Access lands on (SPEC \u00A711): until Access is set up, no\n// request can authenticate, and the API's 401 says so, so the editor shows that state, full\n// page, with the guide that sets Access up, instead of a session that never existed.\n\nimport { unmount } from \"./lifecycle\";\nimport { app } from \"./shell\";\nimport { $ } from \"./ui/dom\";\nimport { html, setHtml } from \"./ui/html\";\n\n/**\n * The published Access guide. The editor's own Docs tab is read from the API, which is\n * refusing every call until Access is set up, so the wall links to the project's site.\n */\nexport const ACCESS_GUIDE = \"https://getkestrel.dev/docs/access/\";\n\n/** Whether a 401's body says no Access is configured, rather than this request failing it. */\nexport function accessNotConfigured(data: unknown): boolean {\n if (!data || typeof data !== \"object\") {\n return false;\n }\n const d = data as { error?: unknown; reason?: unknown };\n return d.error === \"unauthorized\" && d.reason === \"access_not_configured\";\n}\n\n/**\n * Replace the app with the explanation: the editor is locked until Access is set up, why,\n * and the guide that does it. The mounted view is torn down first, so nothing keeps calling\n * an API that can only refuse, and the publication chrome hides, as on the sign-in wall.\n * Every call in flight fails the same way, so a wall already up is left as it is.\n */\nexport function showAccessLocked(): void {\n if (app.querySelector(\".access-wall\")) {\n return;\n }\n unmount();\n document.body.classList.add(\"signed-out\");\n setHtml(\n app,\n html`<div class=\"card auth-wall access-wall\"><h2 tabindex=\"-1\">The editor is locked until you set up Access</h2><p>Until Cloudflare Access is in front of the editor, the app can\u2019t tell who is asking, so it lets no one in. On a new deployment, that\u2019s expected. Your public pages work as usual.</p><p class=\"hint\"><a href=\"${ACCESS_GUIDE}\" target=\"_blank\" rel=\"noopener\">Lock the dashboard with Access <span aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\"> (opens in a new tab)</span></a></p><button id=\"access-reload\" class=\"primary\">Reload</button></div>`,\n );\n $(\"#access-reload\", app).onclick = () => location.reload();\n $<HTMLElement>(\".access-wall h2\", app).focus();\n}\n", "// The wall a refused deployment lands on (SPEC \u00A79): while the Worker refuses its\n// configuration, every API call answers `invalid_config` naming the variable, and while its\n// database is behind the release, `schema_behind` naming the migrations and the command that\n// applies them, so the editor shows that once, full page, with the fix, instead of failing\n// call by call.\n\nimport { unmount } from \"./lifecycle\";\nimport { app } from \"./shell\";\nimport { $ } from \"./ui/dom\";\nimport { type Html, html, setHtml } from \"./ui/html\";\n\n/** What the API says of a refused deployment: its words about it, and the variable to fix\n * when it is a setting. A database behind the release names no variable: its words name the\n * migrations and the command. */\nexport interface ConfigRefusal {\n variable?: string;\n message: string;\n}\n\n/** The refusal an error body carries, or null for any other body. */\nexport function configRefusal(data: unknown): ConfigRefusal | null {\n if (!data || typeof data !== \"object\") {\n return null;\n }\n const d = data as { error?: unknown; variable?: unknown; message?: unknown };\n if (d.error === \"schema_behind\" && typeof d.message === \"string\") {\n return { message: d.message };\n }\n if (d.error !== \"invalid_config\" || typeof d.variable !== \"string\") {\n return null;\n }\n return {\n variable: d.variable,\n message: typeof d.message === \"string\" ? d.message : `${d.variable} is refused`,\n };\n}\n\n/**\n * The published Configuration reference. The editor's own Docs tab is read from the API,\n * which is refusing every call until the configuration is fixed, so the wall links to the\n * project's site instead.\n */\nexport const CONFIGURATION_REFERENCE = \"https://getkestrel.dev/docs/configuration/\";\n\n/** Each variable's heading in the Configuration reference, by its GitHub-style anchor; a\n * secret's is the Secrets table. */\nexport const REFERENCE_ANCHORS: Readonly<Record<string, string>> = {\n PROVIDER: \"provider\",\n ALLOW_FAKE_PROVIDER: \"provider\",\n APP_ORIGIN: \"vars\",\n AWS_REGION: \"vars\",\n FROM_ADDRESS: \"from_address-and-sending_domain\",\n SENDING_DOMAIN: \"from_address-and-sending_domain\",\n ARCHIVE_BASE_PATH: \"archive_base_path\",\n SES_MAX_SEND_RATE: \"ses_max_send_rate\",\n SUBREQUEST_BUDGET: \"subrequest_budget\",\n MIN_LEAD_SECONDS: \"min_lead_seconds\",\n ARCHIVE_ORIGIN: \"archive_origin-and-media_public_base\",\n MEDIA_PUBLIC_BASE: \"archive_origin-and-media_public_base\",\n NOTIFY_FROM: \"notify_from\",\n ACCESS_TEAM_DOMAIN: \"secrets\",\n ACCESS_AUD: \"secrets\",\n ACCESS_ALLOWED_EMAILS: \"secrets\",\n AWS_ACCESS_KEY_ID: \"secrets\",\n AWS_SECRET_ACCESS_KEY: \"secrets\",\n SNS_TOPIC_ARN: \"secrets\",\n SES_CONFIGURATION_SET: \"secrets\",\n SIMULATE_SENDS: \"what-the-app-refuses\",\n};\n\n/** The section of the reference listing everything the app refuses, for a variable with no\n * heading of its own. */\nconst REFUSALS_ANCHOR = \"what-the-app-refuses\";\n\n/** The reference's address for one variable, at its own heading when it has one. */\nexport function referenceUrl(variable: string): string {\n return `${CONFIGURATION_REFERENCE}#${REFERENCE_ANCHORS[variable] ?? REFUSALS_ANCHOR}`;\n}\n\n/**\n * Replace the app with the explanation: the server's words about what's wrong, which name\n * the variable, and the reference, which says how to set it wherever the editor runs.\n * The how-to lives in the reference, not here, so it can't drift from it. The mounted view is torn down first, so nothing keeps calling\n * an API that can only refuse, and the publication chrome hides, as on the sign-in wall.\n * Every call in flight fails the same way, so a wall already up is left as it is.\n */\nexport function showConfigProblem(refusal: ConfigRefusal): void {\n if (app.querySelector(\".config-wall\")) {\n return;\n }\n unmount();\n document.body.classList.add(\"signed-out\");\n const { variable } = refusal;\n // A setting links to its reference; a database behind the release has its command in the\n // server's words already, and the reference has nothing to add.\n const heading = variable ? \"A setting needs fixing\" : \"The database needs updating\";\n const reference: Html | string = variable\n ? html`<p class=\"hint\"><a href=\"${referenceUrl(variable)}\" target=\"_blank\" rel=\"noopener\">${variable} in the Configuration reference <span aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\"> (opens in a new tab)</span></a></p>`\n : \"\";\n setHtml(\n app,\n html`<div class=\"card auth-wall config-wall\" role=\"alert\"><h2 tabindex=\"-1\">${heading}</h2><p>Kestrel won\u2019t run until it\u2019s fixed, and nothing is sent in the meantime.</p><p class=\"config-wall-problem\">${refusal.message}</p>${reference}<button id=\"config-reload\" class=\"primary\">Reload</button></div>`,\n );\n $(\"#config-reload\", app).onclick = () => location.reload();\n $<HTMLElement>(\".config-wall h2\", app).focus();\n}\n", "// The app-wide state: the three values every view reads and boot writes, in one object\n// (`appState`) so each cross-module write is visible here. What a view owns lives in the\n// view, for as long as its mount (client/lifecycle.ts).\n\nimport type { SettingsResponse } from \"../shared/settings\";\n\n/** The boot probe's answer (GET /api/whoami): who we are and which auth mode gates this surface. */\nexport interface Session {\n principal: { kind: \"human\" | \"service\"; email?: string };\n auth: { mode: \"access\" | \"dev\" };\n}\n\nexport interface AppState {\n /** The dev token (local dev only; deployed envs authenticate at the edge). */\n token: string;\n /** Set once booted. */\n session: Session | null;\n /**\n * The last GET /api/settings payload, fetched at boot so the sidebar brand and the\n * dashboard's publication and setup cards can read the origins and the From-address\n * fallback without re-fetching on every render.\n */\n appConfig: SettingsResponse | null;\n}\n\n/** localStorage key of the dev token. */\nexport const TOKEN_KEY = \"kestrel_token\";\n\nexport const appState: AppState = {\n token: localStorage.getItem(TOKEN_KEY) || \"\",\n session: null,\n appConfig: null,\n};\n", "// Auth on the client: the boot probe, the dev token, the bearer header, the identity chip,\n// and the re-auth wall a dead session lands on.\n\nimport { accessNotConfigured, showAccessLocked } from \"./access_locked\";\nimport { configRefusal, showConfigProblem } from \"./config_problem\";\nimport { unmount } from \"./lifecycle\";\nimport { app, identity } from \"./shell\";\nimport { appState, type Session, TOKEN_KEY } from \"./state\";\nimport { $ } from \"./ui/dom\";\nimport { html, setHtml } from \"./ui/html\";\n\n/** Remember the dev token; an empty one forgets it, so the next boot mints instead of probing with a dead value. */\nexport function setToken(t: string | null | undefined): void {\n appState.token = (t || \"\").trim();\n try {\n // Clearing (empty token) removes the key rather than storing \"\", so the next\n // boot takes the \"no token \u2192 mint\" path instead of probing with a dead value.\n if (appState.token) {\n localStorage.setItem(TOKEN_KEY, appState.token);\n } else {\n localStorage.removeItem(TOKEN_KEY);\n }\n } catch {\n /* private mode */\n }\n}\n\n/**\n * The dev token goes in Authorization; in Access mode there is no token and the session\n * cookie authenticates instead, so no header is sent.\n */\nexport function authHeaders(): Record<string, string> {\n return appState.token ? { Authorization: `Bearer ${appState.token}` } : {};\n}\n\n/**\n * Render the sidebar identity chip from the session: who is signed in under Access, with\n * the sign-out link, or the local-dev marker when there is no edge.\n */\nexport function renderIdentity(): void {\n const session = appState.session;\n if (session?.auth.mode === \"access\") {\n const p = session.principal;\n const who = p.email || (p.kind === \"service\" ? \"Service token\" : \"Signed in\");\n setHtml(\n identity,\n html`<span class=\"who\" title=\"${who}\">${who}</span><a class=\"ghost\" href=\"/cdn-cgi/access/logout\" title=\"Sign out\"><svg class=\"signout-icon\" aria-hidden=\"true\"><use href=\"#i-signout\"/></svg><span class=\"signout-label\">Sign out</span></a>`,\n );\n } else {\n setHtml(\n identity,\n html`<span class=\"who dev\" title=\"Local dev: sign-in is skipped on localhost\">Local dev</span>`,\n );\n }\n}\n\n/**\n * Access sessions expire at the edge (the request never reaches the app), so the only\n * recovery is a fresh document load that re-triggers the Access login. In dev this\n * shouldn't happen, but a reload re-mints, so the same affordance is safe.\n */\nexport function showReauth(): void {\n // Every call in flight fails the same way, so a wall already up is left as it is: drawn\n // again, it would take focus back from wherever the reader had moved it.\n if (app.querySelector(\".reauth-wall\")) {\n return;\n }\n // A dead token means every background poll (and a pending editor autosave) now 401s \u2014\n // which is what routed us here. Tear the view down so a walled tab goes quiet instead of\n // re-hitting the API on its timers until reload.\n unmount();\n // No identity yet \u2014 hide the publication chrome so the wall stands alone.\n document.body.classList.add(\"signed-out\");\n setHtml(\n app,\n html`<div class=\"card auth-wall reauth-wall\"><h2 tabindex=\"-1\">Session expired</h2><p class=\"hint\">Your access session ended. Sign in again to continue.</p><button id=\"reauth\" class=\"primary\">Sign in</button></div>`,\n );\n $(\"#reauth\", app).onclick = () => location.reload();\n // Focus starts on the heading, as on the other walls, so a screen reader begins there.\n $<HTMLElement>(\".reauth-wall h2\", app).focus();\n}\n\n// Mint a dev token into localStorage. Returns false when deployed, where the endpoint 404s\n// (or is unreachable) and the Access cookie authenticates instead.\nasync function mintDevToken(): Promise<boolean> {\n try {\n const r = await fetch(\"/api/dev/token?kind=human\");\n if (r.ok) {\n setToken(((await r.json()) as { token: string }).token);\n return true;\n }\n } catch {\n /* deployed: the endpoint is absent; the Access cookie authenticates instead */\n }\n return false;\n}\n\n// Probe identity. A network error or an opaque Access redirect can't be recovered here, so\n// it surfaces as a null result (the sign-in wall).\nasync function whoami(): Promise<Response | null> {\n try {\n return await fetch(\"/api/whoami\", { headers: authHeaders(), redirect: \"manual\" });\n } catch {\n return null;\n }\n}\n\n/**\n * The boot probe: who is signed in, minting a dev token first in local development. Answers\n * the session, or null once it has put up the wall the probe landed on: the refused\n * configuration's when the server names a setting to fix (SPEC \u00A79), the locked editor's when\n * the deployment has no Access yet (SPEC \u00A711), since signing in again cannot help with\n * either, and the sign-in wall otherwise.\n */\nexport async function probeSession(): Promise<Session | null> {\n if (!appState.token) {\n await mintDevToken();\n }\n let res = await whoami();\n // Stale stored token in dev: clear it, mint a fresh one, and probe again so a leftover\n // credential self-heals. In Access mode the re-mint fails and `res` stays as it was.\n if (!res?.ok && appState.token) {\n setToken(\"\");\n if (await mintDevToken()) {\n res = await whoami();\n }\n }\n if (res?.ok) {\n return (await res.json()) as Session;\n }\n const data: unknown = res ? await res.json().catch(() => null) : null;\n const refusal = configRefusal(data);\n if (refusal) {\n showConfigProblem(refusal);\n } else if (accessNotConfigured(data)) {\n showAccessLocked();\n } else {\n showReauth();\n }\n return null;\n}\n", "// The one network seam: every call to the HTTP API goes through api(), apiText(), or\n// apiFile(), which attach the credential, route a 401 to re-auth (or to the locked editor,\n// before Access is set up) and a refused deploy configuration to its wall, and turn any\n// other failure into one ApiError carrying the server's message. They call the global\n// fetch, so a spec stands in for the API by scripting fetch and the real functions run\n// over it.\n\nimport type { ApiErrorBody } from \"../shared/errors\";\nimport { accessNotConfigured, showAccessLocked } from \"./access_locked\";\nimport { authHeaders, showReauth } from \"./auth\";\nimport { configRefusal, showConfigProblem } from \"./config_problem\";\n\n/** What a request may carry. `json` sets the body and its content type; `body` is sent as is. */\nexport interface ApiOptions {\n method?: string;\n headers?: Record<string, string>;\n body?: BodyInit | null;\n json?: unknown;\n /**\n * The mounted view's signal, for a read that paints: navigating away cuts the request\n * off, and the view is spared a stale answer. Never for a write (a save the reader\n * started must land whether or not they stayed to watch).\n */\n signal?: AbortSignal;\n}\n\n/**\n * A non-2xx answer. Callers branch on `status` (the editor's conflict handling on 409)\n * and read the parsed body, when there was one, from `data`.\n */\nexport class ApiError extends Error {\n constructor(\n message: string,\n readonly status: number,\n readonly data: unknown = null,\n ) {\n super(message);\n this.name = \"ApiError\";\n }\n\n /** The API's error code (`error` on the body), such as `cursor_ahead`, or null when the\n * body carried none. */\n get code(): string | null {\n const d = this.data as Partial<ApiErrorBody> | null;\n return typeof d?.error === \"string\" ? d.error : null;\n }\n\n /** The request field a refusal is about (`field` on the body), dotted for a nested one,\n * or null when the refusal names none. */\n get field(): string | null {\n const d = this.data as Partial<ApiErrorBody> | null;\n return typeof d?.field === \"string\" ? d.field : null;\n }\n}\n\nconst REAUTH_MESSAGE = \"Not authorized. Sign in again.\";\n\n// A server message written as a bare code (\"internal_error\") or a bare noun (\"post\"), with\n// no space in it, is a word for a program, not a person.\nconst isBareWord = (m: string) => /^[a-z_]+$/i.test(m.trim());\n\n// What fetch rejects with when no answer came: Chrome, Firefox, and Safari's words.\nconst NETWORK_FAILURE =\n /^(Failed to fetch|NetworkError when attempting to fetch resource\\.?|Load failed|The network connection was lost\\.?|The Internet connection appears to be offline\\.?)$/;\n\n/**\n * What to tell the publisher about a failure, in their words: the server's own message when\n * it wrote one for people, otherwise a plain sentence for what happened. A dropped\n * connection, a server error with no message, and a 404 naming only what it couldn't find\n * would otherwise reach the page as \"Failed to fetch\", \"internal_error\", or \"post\".\n */\nexport function describeError(e: unknown): string {\n if (e instanceof ApiError) {\n // Only the server's own message counts as written for people: an ApiError built from\n // a body that isn't the API's (a proxy's page, an empty body) carries the HTTP status\n // text, \"Bad Gateway\", which is no better than a code.\n const written = (e.data as { message?: unknown } | null)?.message;\n if (typeof written === \"string\" && !isBareWord(written)) {\n return written;\n }\n if (e.status === 401) {\n return e.message; // the sign-in wall is already up; this is its one line\n }\n if (e.status === 404) {\n return \"It isn\u2019t there any more. It may have been deleted elsewhere.\";\n }\n if (e.status >= 500) {\n return \"Something went wrong on the server. Try again in a moment.\";\n }\n if (e.status === 429) {\n return \"Too many requests at once. Try again in a moment.\";\n }\n return \"The server couldn\u2019t do that. Reload the page and try again.\";\n }\n // fetch rejects with a TypeError when the request never got an answer: offline, a\n // dropped connection, the server unreachable. Its text differs by browser, so only those\n // exact texts count; a TypeError from a bug keeps its own words.\n if (e instanceof TypeError && NETWORK_FAILURE.test(e.message)) {\n return \"Couldn\u2019t reach Kestrel. Check your connection and try again.\";\n }\n if (e instanceof Error) {\n return e.message;\n }\n return String(e);\n}\n\nasync function send(path: string, opts: ApiOptions): Promise<Response> {\n const headers: Record<string, string> = { ...authHeaders(), ...(opts.headers ?? {}) };\n let body = opts.body;\n if (opts.json !== undefined) {\n headers[\"content-type\"] = \"application/json\";\n body = JSON.stringify(opts.json);\n }\n const res = await fetch(path, {\n method: opts.method ?? \"GET\",\n headers,\n body,\n signal: opts.signal,\n });\n if (res.status === 401) {\n // A deployment with no Access can authenticate no one, which signing in again can't\n // fix, so the 401 says so and the editor shows that instead (SPEC \u00A711).\n const data: unknown = await res.json().catch(() => null);\n if (accessNotConfigured(data)) {\n showAccessLocked();\n } else {\n showReauth();\n }\n throw new ApiError(REAUTH_MESSAGE, 401, data);\n }\n return res;\n}\n\n/** The message an error body carries, when it carries one. */\nfunction messageOf(data: unknown): string | undefined {\n if (data && typeof data === \"object\") {\n const d = data as { message?: unknown; error?: unknown };\n if (typeof d.message === \"string\") {\n return d.message;\n }\n if (typeof d.error === \"string\") {\n return d.error;\n }\n }\n return undefined;\n}\n\n/**\n * The ApiError a non-2xx response stands for, the same from every reader: the server's\n * words from its JSON error body, or, for a body that is not one (a proxy's error page, an\n * empty body), the status text, and the status itself when even that is empty, as it\n * always is over HTTP/2.\n */\nasync function failure(res: Response): Promise<ApiError> {\n const text = await res.text();\n let data: unknown = null;\n try {\n data = text ? JSON.parse(text) : null;\n } catch {\n // Not JSON, so not the API's error body: fall back to the status below.\n }\n // A refused deploy configuration fails every call alike, so it is shown once, full page,\n // rather than as one more failed read.\n const refusal = configRefusal(data);\n if (refusal) {\n showConfigProblem(refusal);\n }\n const fallback = res.statusText || `Request failed (HTTP ${res.status})`;\n return new ApiError(messageOf(data) ?? fallback, res.status, data);\n}\n\n/** Call the API and parse its JSON answer (null for an empty body). Throws ApiError on a non-2xx. */\nexport async function api<T = unknown>(path: string, opts: ApiOptions = {}): Promise<T> {\n const res = await send(path, opts);\n if (!res.ok) {\n throw await failure(res);\n }\n const text = await res.text();\n return (text ? JSON.parse(text) : null) as T;\n}\n\n/**\n * Like api(), but the raw response text, for the endpoints that answer with HTML (the\n * rendered preview). Same 401 guard, which a bare fetch(authHeaders()) would skip.\n */\nexport async function apiText(path: string, opts: ApiOptions = {}): Promise<string> {\n const res = await send(path, opts);\n if (!res.ok) {\n throw await failure(res);\n }\n return res.text();\n}\n\n/** A file the API serves for download: its text, and the name the server gives it in\n * `Content-Disposition`, if any, so a saved file is named as the server names it. */\nexport async function apiFile(\n path: string,\n opts: ApiOptions = {},\n): Promise<{ text: string; filename: string | null }> {\n const res = await send(path, opts);\n if (!res.ok) {\n throw await failure(res);\n }\n const disposition = res.headers.get(\"content-disposition\") ?? \"\";\n const filename = /filename=\"([^\"]+)\"/.exec(disposition)?.[1] ?? null;\n return { text: await res.text(), filename };\n}\n", "// The publication's display name, as every surface shows it: the email, the reader pages,\n// the confirmation email, and the editor's previews of them (SPEC \u00A79). One function, so\n// no two surfaces can name the same publication differently.\n\n/** The display name of a `From:` header: `Birds Weekly <hello@birds.example>` and\n * `\"Birds Weekly\" <hello@birds.example>` give `Birds Weekly`; a bare address, or an\n * address in angle brackets alone, gives \"\". A quoted name's escapes are undone. */\nexport function fromDisplayName(fromAddress: string): string {\n const header = fromAddress.trim();\n const lt = header.lastIndexOf(\"<\");\n if (lt < 0 || !header.endsWith(\">\")) {\n return \"\";\n }\n const display = header.slice(0, lt).trim();\n if (display.length >= 2 && display.startsWith('\"') && display.endsWith('\"')) {\n return display.slice(1, -1).replace(/\\\\(.)/g, \"$1\").trim();\n }\n return display;\n}\n\n/** The name a publication goes by: the one the publisher set, else the sender's display\n * name, else \"\" (never a made-up name, so a surface with none words itself without one). */\nexport function publicationName(name: string, fromAddress: string): string {\n return name || fromDisplayName(fromAddress);\n}\n", "// Publication identity for the sidebar brand: name, tagline, logo, and the From-name\n// fallback.\n\nimport { publicationName } from \"../shared/publication\";\nimport type { SettingsResponse } from \"../shared/settings\";\nimport { appState } from \"./state\";\nimport { html, setHtml } from \"./ui/html\";\n\nexport interface Publication {\n name: string;\n tagline: string;\n logoUrl: string | null;\n}\n\n/**\n * The publication's name / tagline / logo come from the settings surface\n * (settings.publication). Each field falls back sensibly when unset: the name as the\n * email resolves it, and with none a placeholder only the publisher sees, and a neutral\n * initial tile for the logo.\n */\nexport function derivePublication(data: SettingsResponse | null): Publication {\n const p = data?.settings.publication;\n return {\n name: publicationName(p?.name || \"\", data?.deployment.fromAddress ?? \"\") || \"Your publication\",\n tagline: p?.tagline || \"\",\n logoUrl: p?.logoUrl || null,\n };\n}\n\nexport function renderSidebarBrand(): void {\n const pub = derivePublication(appState.appConfig);\n const nameEl = document.getElementById(\"brandName\");\n const tagEl = document.getElementById(\"brandTagline\");\n const logoEl = document.getElementById(\"brandLogo\");\n if (nameEl) {\n nameEl.textContent = pub.name;\n }\n if (tagEl) {\n tagEl.textContent = pub.tagline;\n tagEl.hidden = !pub.tagline;\n }\n if (logoEl) {\n if (pub.logoUrl) {\n setHtml(logoEl, html`<img src=\"${pub.logoUrl}\" alt=\"\">`);\n logoEl.classList.remove(\"brand-logo-placeholder\");\n } else {\n // Neutral placeholder tile: the publication's initial on the accent.\n logoEl.textContent = (pub.name.trim()[0] || \"K\").toUpperCase();\n logoEl.classList.add(\"brand-logo-placeholder\");\n }\n }\n}\n", "// The one spelling of a post's public archive URL, shared by the Worker (emails, the\n// reader surface, the API) and the editor, so the two can never disagree (SPEC \u00A75).\n\n/** The canonical archive URL of a post: `<origin><basePath>/<slug>`. */\nexport function archivePostUrl(origin: string, basePath: string, slug: string): string {\n return `${origin}${basePath}/${slug}`;\n}\n", "// Sends as the API carries them (SPEC \u00A76, \u00A78, \u00A712): one `SendView` of a send, the same on\n// every route (the list, the send, the feed, and every action's answer), the feed the pages\n// follow, and the per-recipient record; beside them, the stored send row the Worker builds\n// the view from, which never goes on the wire as it is.\n// The Worker's routes produce these shapes and the editor consumes them; one definition,\n// so neither can drift.\n\nimport type { PageMeta } from \"./list\";\n\nexport type SendStatus = \"scheduled\" | \"sending\" | \"sent\" | \"canceled\";\n\n/**\n * Why a provider refused a whole batch (SPEC \u00A712): it is `unavailable` (an outage or a\n * rate limit, which clears on its own), or it refuses the `account` (a bad key, an\n * unverified domain, a paused account), which needs the operator.\n */\nexport type HaltReason = \"unavailable\" | \"account\";\n\n/**\n * What a refusal is about, so the advice can name the fix: the access key or its permissions,\n * the sender (an unverified domain or from-address), a spent sending quota, the account\n * paused or suspended by the provider, a rate limit, the provider itself failing, or no\n * provider connected to the deployment at all (SPEC \u00A79).\n */\nexport type HaltCause =\n | \"credentials\"\n | \"sender\"\n | \"quota\"\n | \"suspended\"\n | \"rate_limit\"\n | \"outage\"\n | \"not_connected\";\n\n/**\n * The advice for a send held for want of a provider (SPEC \u00A79), in its parts: the setup step,\n * which a page makes the link every not-connected line carries, and what follows it. The\n * guide's mention stands in for that link where there is none (an email, the API's words).\n * \"Deploy\" is the step's own last one.\n */\nexport const NOT_CONNECTED_ADVICE = {\n step: \"Connect Amazon SES\",\n guide: \" (the setup guide walks through it)\",\n after: \", then deploy.\",\n} as const;\n\n/**\n * What to do about the provider refusing the account, by what the refusal is about\n * (SPEC \u00A712). The fix is always outside the app: nothing in it can change a credential or\n * the provider's view of the account (SPEC \u00A79). One wording for the watch and the notification\n * that tells the publisher, so the two never advise differently. Plain text: a page that\n * links the not-connected step builds it from `NOT_CONNECTED_ADVICE`.\n */\nexport function refusalAdvice(cause: HaltCause | null): string {\n switch (cause) {\n case \"credentials\":\n return \"Replace the SES access key in the deployment's secrets, or give it ses:SendEmail and ses:SendBulkEmail in its IAM policy.\";\n case \"sender\":\n return \"Verify the sending domain or from-address with the provider.\";\n case \"quota\":\n return \"Wait for SES's 24-hour quota to free up, or ask AWS to raise it in the Service Quotas console.\";\n case \"suspended\":\n return \"Settle the account's standing with the provider (for SES, in the SES console).\";\n case \"not_connected\":\n return `${NOT_CONNECTED_ADVICE.step}${NOT_CONNECTED_ADVICE.guide}${NOT_CONNECTED_ADVICE.after}`;\n default:\n return \"Fix the account with the provider.\";\n }\n}\n\n/** The provider's standing refusal of a send, while one lasts. */\nexport interface SendHalt {\n reason: HaltReason;\n cause: HaltCause | null;\n error: string;\n /** How many runs in a row this refusal has halted. */\n retries: number;\n /** When refusals for this reason began; null only if the record lacks it. */\n since: number | null;\n /** When the next retry is due. The sweep leaves the send alone until then; null only if the record lacks it. */\n retry_at: number | null;\n}\n\n/** The eight denormalized progress counters on a send. */\nexport interface SendCounts {\n pending: number;\n in_flight: number;\n accepted: number;\n delivered: number;\n bounced: number;\n complained: number;\n skipped: number;\n unsent: number;\n}\n\n/**\n * A send as stored: its frozen render, its facts, and the counters (the `c_*` columns). The\n * Worker's own row type; the API carries `SendView`, built from it, never this.\n */\nexport interface Send {\n id: string;\n post_id: string;\n status: SendStatus;\n fire_at: number;\n rendered_html: string;\n rendered_text: string;\n subject: string;\n recipient_count: number;\n locked_until: number | null;\n scheduled_at: number;\n started_at: number | null;\n completed_at: number | null;\n /** When the send's first run fixed its audience (SPEC \u00A76); null until it fires. From then on `recipient_count` is the audience at fire. */\n audience_resolved_at: number | null;\n /** When a template or identity change last re-made the frozen render while the send was scheduled; null if never. */\n remade_at: number | null;\n /** When a test of the frozen copy was last sent while the send was scheduled; null if never. */\n tested_at: number | null;\n /** Why the provider refused this send's last batch as a whole (SPEC \u00A712), or null once a batch is answered: `unavailable` retries on its own, `account` needs the operator. */\n halt_reason: HaltReason | null;\n /** What that refusal is about, for the advice shown with it. */\n halt_cause: HaltCause | null;\n /** The provider's own words for that refusal. */\n halt_error: string | null;\n /** When refusals for this reason began. */\n halted_at: number | null;\n /** How many runs in a row this refusal has halted; 0 while the send is not halted. */\n halt_retries: number;\n /** When the sweep next retries the halted send (SPEC \u00A712); null while it is not halted. */\n halt_retry_at: number | null;\n c_pending: number;\n c_in_flight: number;\n c_accepted: number;\n c_delivered: number;\n c_bounced: number;\n c_complained: number;\n c_skipped: number;\n c_unsent: number;\n /**\n * Where the send's last visible change sits in one app-wide sequence (SPEC \u00A78): any\n * change a reader could see (a cancel, a move, a re-make, a counter move, a receipt)\n * gives the send a higher `rev` than every change before it, across all sends. A lease\n * renewal alone does not move it.\n */\n rev: number;\n}\n\n/** A stored send without the large frozen bodies: what a view is built from. */\nexport type SendSummary = Omit<Send, \"rendered_html\" | \"rendered_text\">;\n\n/**\n * How long a send may stay `sending` before it is flagged as in flight too long (SPEC\n * \u00A712): long enough that an ordinary outage's retries never alarm, short enough to be\n * told the same afternoon. The server decides the flag; the editor only words it.\n */\nexport const STUCK_THRESHOLD_MS = 30 * 60 * 1000;\n\n/**\n * The minimum lead a deployment runs with when it sets none (SPEC \u00A76): every send spends\n * at least this long visible and cancelable before it fires (I6). The value in force is the\n * deployment's (`DeploymentView.minLeadMs`); this is only its default.\n */\nexport const DEFAULT_MIN_LEAD_MS = 5 * 60 * 1000;\n\n/**\n * The least minimum lead any deployment may set: one sweep tick. The sweep runs once a\n * minute, so a shorter lead would still be kept, but the tick, not the lead, would decide\n * when a send fires, a precision the system doesn't have (SPEC \u00A76).\n */\nexport const MIN_LEAD_FLOOR_MS = 60 * 1000;\n\n/**\n * The most minimum lead any deployment may set: one day. The lead bounds only the least\n * wait before a send, never how far out one may be scheduled, so a longer one holds back\n * every Send now and is almost certainly milliseconds typed as seconds (SPEC \u00A76).\n */\nexport const MIN_LEAD_CEILING_MS = 24 * 60 * 60 * 1000;\n\n/** A minimum lead in words, \"5 minutes\" or \"90 seconds\", so the server's refusal and the\n * editor's copy name it the same way. */\nexport function formatLead(ms: number): string {\n const seconds = Math.round(ms / 1000);\n if (seconds % 60 === 0) {\n const minutes = seconds / 60;\n return `${minutes} minute${minutes === 1 ? \"\" : \"s\"}`;\n }\n return `${seconds} seconds`;\n}\n\n/** GET /api/sends */\nexport interface SendListResponse {\n sends: SendView[];\n page: PageMeta;\n /**\n * Where this read stands among the changes to sends, to hand back as `since`: `<seq>.<at>`,\n * the change sequence at the read and the server's time of it, both decimal. Two compare\n * by those numbers (`shared/cursor.ts`).\n */\n cursor: string;\n}\n\n/**\n * The live reporting phase, derived from the counters, never stored: waiting in the\n * review window; due (the fire time has passed and the next sweep starts it); handing\n * recipients to the provider (with or without retries); paused between sweep ticks;\n * wedged and awaiting Resolve; dispatched with receipts still arriving; every accepted\n * recipient confirmed; or canceled.\n */\nexport type SendPhase =\n | \"scheduled\"\n | \"due\"\n | \"progressing\"\n | \"retrying\"\n | \"backing-off\"\n | \"needs-attention\"\n | \"settling\"\n | \"complete\"\n | \"canceled\";\n\n/**\n * One send, as every route carries it (SPEC \u00A78): `GET /api/sends` rows, `GET /api/sends/:id`, the\n * feed, and every action's answer, so a client keeps one shape current from any of them and\n * tells two apart by `rev`. The stored facts, less the frozen bodies (at their own route,\n * `links.email_html` and `links.email_text`), with what the server derives from them at\n * `as_of`: the phase, the conditions, the actions, and the next change.\n */\nexport interface SendView {\n id: string;\n post_id: string;\n subject: string;\n /** The stored lifecycle state (SPEC \u00A72); `phase` is the finer reading of it. */\n status: SendStatus;\n /** Where the send's last visible change sits in the change sequence: of two views of one send, the higher `rev` is the newer. */\n rev: number;\n /** The server's clock when the view was read: what the clock derives (phase, conditions, actions, `next_change_at`) is as of then. */\n as_of: number;\n fire_at: number;\n scheduled_at: number;\n started_at: number | null;\n completed_at: number | null;\n /** When a template or identity change last re-made the scheduled email; null if never. */\n remade_at: number | null;\n /** When the scheduled email was last tested; null if never. */\n tested_at: number | null;\n /**\n * Who the send goes to: `count` is an estimate while the send is scheduled (the audience\n * is resolved when it fires, SPEC \u00A76), and the audience at fire once `fixed`, from\n * `fixed_at` on, never growing.\n */\n audience: { count: number; fixed: boolean; fixed_at: number | null };\n /** Every recipient in exactly one bucket, summing to the audience once it is fixed. */\n counts: SendCounts;\n /** Provider hand-off: how far the send loop has gotten. */\n dispatch: {\n /** Recipients the loop has finished with (accepted, unsent, or skipped). */\n done: number;\n percent: number;\n /** Recipients handed off a minute: what a sweep tick carries, averaged over the ticks run so far (null when not sending, or before the first tick has finished). */\n rate_per_min: number | null;\n /** Rough time to finish dispatch, counted in sweep ticks: to the end of the last tick the rest needs, the wait between ticks included. Only while it is handing off (null otherwise, never while paused, nor before the first tick has finished). */\n eta_ms: number | null;\n };\n /** Delivery confirmation, which lags acceptance. */\n delivery: { confirmed: number; percent_of_accepted: number };\n /** The provider, and its standing refusal of this send while one lasts. */\n provider: { name: string; halt: SendHalt | null };\n phase: SendPhase;\n /** What is wrong with the send, or worth knowing, now (SPEC \u00A78, \u00A712), each with its own words and the action that settles it, if any. */\n conditions: SendCondition[];\n /** What the server would accept on the send right now. */\n actions: SendAction[];\n /**\n * The earliest moment the send can change with no one acting on it, by the server's\n * clock: at or before `as_of` while it can move at any moment (due, or sending with work\n * in hand), a later time while it waits on the clock or the sweep (its fire time, a halt's\n * next retry, the in-flight-too-long threshold), or null when only an action or a receipt\n * can change it (wedged, missed, finished).\n */\n next_change_at: number | null;\n /** Where the send's other resources are. `archive` is the published post, absolute, once the send has started; null before. */\n links: {\n self: string;\n email_html: string;\n email_text: string;\n deliveries: string;\n deliveries_csv: string;\n post: string;\n archive: string | null;\n };\n}\n\n/** Something a person can do to a send through the API, exactly as the server would take it now. */\nexport interface SendAction {\n name: \"cancel\" | \"reschedule\" | \"resolve\";\n method: \"POST\";\n /** The route, with the send's id filled in. */\n path: string;\n}\n\n/** What a condition asks of a person: `action` needs one (red), `warn` is worth a look (amber), `info` is worth knowing. */\nexport type ConditionSeverity = \"action\" | \"warn\" | \"info\";\n\n/** What every condition carries. */\ninterface ConditionBase {\n severity: ConditionSeverity;\n /** When the condition began, or null when the record does not say. */\n since: number | null;\n /** The server's words for it, whole sentences, the same wherever it is shown. */\n message: string;\n /** The action that settles it, if a person can take one through the API. */\n action: SendAction | null;\n}\n\n/**\n * One open condition on a send (SPEC \u00A78, \u00A712), derived by the server from the record, the\n * same on every route:\n *\n * - `missed`: still scheduled past the missed tolerance; the sweep that fires it has not run.\n * - `stuck`: still sending past the stuck threshold after it started.\n * - `wedged`: recipients whose delivery is unknown; `count` of them, settled by Resolve.\n * - `refused`: the provider refuses the account; its `cause`, `error`, the `advice` for the\n * fix (outside the app), and `retry_at`. No action: the send resumes on its own.\n * - `provider_unavailable`: the provider is down or rate-limiting; retried at `retry_at`.\n * - `bounce_spike`: a recently sent send's confirmed bounces reached the danger zone.\n * - `remade`: a template or identity change re-made the scheduled email after its last test.\n */\nexport type SendCondition =\n | (ConditionBase & { kind: \"missed\" })\n | (ConditionBase & { kind: \"stuck\" })\n | (ConditionBase & { kind: \"wedged\"; count: number })\n | (ConditionBase & {\n kind: \"refused\";\n cause: HaltCause | null;\n error: string;\n advice: string;\n retry_at: number | null;\n })\n | (ConditionBase & { kind: \"provider_unavailable\"; error: string; retry_at: number | null })\n | (ConditionBase & { kind: \"bounce_spike\"; bounced: number; rate: number })\n | (ConditionBase & { kind: \"remade\"; tested_at: number | null });\n\n/** The kinds of condition. */\nexport type ConditionKind = SendCondition[\"kind\"];\n\n/** A condition in the feed's roll-up: which send it is on, and the condition. */\nexport type FeedCondition = SendCondition & { send_id: string; subject: string };\n\n/**\n * The bounce spike (SPEC \u00A78): a send's confirmed bounces over its audience at fire reaching\n * the provider's danger zone. SES puts a sender under review at a 5% bounce rate; the small\n * absolute floor keeps a tiny audience's noisy rate (one bad address among a handful) from\n * tripping it; and only a send finished within the recent window is read, since an old\n * send's bounces are history, not a risk to act on. Well above the dev simulation's ~2%.\n */\nexport const BOUNCE_SPIKE_RATE = 0.05;\nexport const BOUNCE_SPIKE_MIN = 3;\nexport const BOUNCE_SPIKE_RECENT_MS = 7 * 24 * 60 * 60 * 1000;\n\n/** GET /api/sends/feed: what a client follows to keep up with sends without polling each one. */\nexport interface SendFeedResponse {\n /** The server's clock at the read, so a client times its next read by the server's clock, not its own. */\n now: number;\n /**\n * With `since`, every send that changed after that cursor, whatever its state, those the\n * clock changed with no write included (turned due, missed, in flight too long, or wedged\n * as a lease ran out); without it, every send that can change on its own. Soonest fire first.\n */\n sends: SendView[];\n /** With `since`, every send removed after that cursor (deleted with its post), in sequence order; empty without it. */\n removed: RemovedSend[];\n /** Where this read stands (`<seq>.<at>`, as on `GET /api/sends`), to hand back as `since` on the next. */\n cursor: string;\n /** Whether the read stopped at `limit` changes with more after `cursor`: read again at once from it. */\n more: boolean;\n /** Every open condition across the sends that can have one (scheduled, sending, and recently sent), whether or not they changed: every open problem in one read, most severe first. */\n conditions: FeedCondition[];\n /**\n * When to read again, by the server's clock: soon while a send can move, about once a\n * minute while none can, and never later than just past the next change the clock or the\n * sweep will make. Advisory: reading sooner (right after acting) is always fine.\n */\n read_again_at: number;\n}\n\n/** A send removed since a cursor: which, and where the removal sits in the change sequence. */\nexport interface RemovedSend {\n id: string;\n rev: number;\n}\n\n/** The sent record's per-recipient delivery breakdown (SPEC \u00A78). */\nexport interface DeliveryOutcomes {\n recipients: number;\n delivered: number;\n bounced: number;\n complained: number;\n /** Never accepted by the provider (transport-level; does not itself suppress). */\n unsent: number;\n /** Excluded at send time (unsubscribed or suppressed after the audience froze). */\n skipped: number;\n /** Accepted by the provider, with no delivery event yet (a provider may emit none). */\n accepted: number;\n in_flight: number;\n}\n\n/** GET /api/sends/:id: the send, and its record's outcome breakdown. */\nexport interface SendResponse {\n send: SendView;\n outcomes: DeliveryOutcomes;\n /** Where this read stands among the changes to sends (`<seq>.<at>`), to follow the send from with `GET /api/sends/feed`. */\n cursor: string;\n}\n\n/** The buckets GET /api/sends/:id/deliveries filters by, exactly as the outcomes count them, plus \"all\" and \"failures\" (bounced, complained, or unsent). */\nexport type DeliveryView =\n | \"all\"\n | \"failures\"\n | \"delivered\"\n | \"bounced\"\n | \"complained\"\n | \"unsent\"\n | \"skipped\"\n | \"accepted\"\n | \"in_flight\";\n\n/**\n * One recipient's row for the in-app record: the send-loop status and the later webhook\n * event, the provider detail or error, and for a bounce the frozen hard/soft kind (null\n * when the kind is unknown; rendered as a plain \"Bounce\").\n */\nexport interface DeliveryRecord {\n email: string;\n status: string;\n event: string | null;\n event_detail: string | null;\n event_at: number | null;\n error: string | null;\n attempts: number;\n bounce_kind: string | null;\n}\n\n/** GET /api/sends/:id/deliveries */\nexport interface DeliveryListResponse {\n deliveries: DeliveryRecord[];\n view: DeliveryView;\n page: PageMeta;\n}\n\n/** The publisher's answer for a wedged send's ambiguous recipients (SPEC \u00A712). */\nexport type StuckResolution = \"unsent\" | \"accepted\";\n\n/** POST /api/sends/:id/resolve */\nexport interface ResolveResponse {\n send: SendView;\n /** Where the answer stands among the changes to sends, to follow the send from. */\n cursor: string;\n /** How many ambiguous recipients this action adjudicated. */\n resolved: number;\n /** Whether the send reached its completion gate and finished. */\n completed: boolean;\n}\n\n/** POST /api/sends/:id/cancel and /reschedule */\nexport interface SendActionResponse {\n send: SendView;\n /** Where the answer stands among the changes to sends, to follow the send from. */\n cursor: string;\n /** False when the send already stood as asked (canceled already, or already at that time): nothing was written. */\n changed: boolean;\n}\n\n/**\n * POST /api/posts/:id/schedule and /send: the send that was frozen (SPEC \u00A76). A second\n * send-now for a post already in its window answers with that send and `idempotent`.\n */\nexport interface ScheduleResponse {\n send: SendView;\n /** Where the answer stands among the changes to sends, to follow the send from. */\n cursor: string;\n idempotent?: boolean;\n}\n", "// Reads of the deployment reflection (appState.appConfig.deployment; SPEC \u00A79), fetched once\n// at boot: the build reference, the archive URL, the minimum lead, and the no-provider notes.\n\nimport { archivePostUrl } from \"../shared/archive_url\";\nimport { DEFAULT_MIN_LEAD_MS, formatLead, NOT_CONNECTED_ADVICE } from \"../shared/sends\";\nimport { appState } from \"./state\";\nimport { type Html, html } from \"./ui/html\";\n\n/** The build reference as two link fragments: the version (to its release) and the sha (to its commit). */\nexport interface BuildRefParts {\n version: Html;\n sha: Html;\n}\n\n/**\n * { version, sha } from the read-only deployment reflection (appConfig.deployment.build,\n * resolved at build in src/build.ts): version \u2192 its release (only when this build IS that\n * tagged release), sha \u2192 its commit, both on the repo and never getkestrel.dev. Each\n * degrades to plain text (no dead link) when there's no repo, the build isn't a release,\n * or the sha is \"dev\" (a local build). null until a build is known. Build metadata is\n * quiet, secondary info: roomShell pins it to the rail's bottom-left corner (and, on\n * mobile, sets it as the room's foot); it's also at GET /api/version, where a bug report\n * is filed. Never in the publisher-facing dashboard.\n */\nexport function buildRefParts(): BuildRefParts | null {\n const b = appState.appConfig?.deployment.build;\n if (!b?.version) {\n return null;\n }\n const version = b.tagUrl\n ? html`<a class=\"build-link\" href=\"${b.tagUrl}\" target=\"_blank\" rel=\"noopener\">v${b.version}</a>`\n : html`v${b.version}`;\n const sha = b.commitUrl\n ? html`<a class=\"build-link\" href=\"${b.commitUrl}\" target=\"_blank\" rel=\"noopener\">${b.sha}</a>`\n : html`${b.sha}`;\n return { version, sha };\n}\n\n/**\n * The deployment's minimum lead (SPEC \u00A76), the least time between a request to send and the\n * send firing, which the server enforces on every schedule, send now, and reschedule. The\n * default until the reflection has loaded: the editor never states a lead of its own.\n */\nexport function minLeadMs(): number {\n return appState.appConfig?.deployment.minLeadMs ?? DEFAULT_MIN_LEAD_MS;\n}\n\n/** The minimum lead in words, \"5 minutes\", for the editor's hints and toasts. */\nexport function minLeadText(): string {\n return formatLead(minLeadMs());\n}\n\n/**\n * Why a picked time is refused for being too soon, in the publisher's words (SPEC \u00A76): the\n * lead is what keeps a window to cancel in. Where the dialog offers Send now, it says that\n * is the way to the soonest send.\n */\nexport function tooSoonText(offersSendNow: boolean): string {\n const why = `Pick a time at least ${minLeadText()} from now, so there\u2019s a window to cancel in.`;\n return offersSendNow ? `${why} For the soonest send, use Send now below.` : why;\n}\n\n/** Whether a picked time is inside the minimum lead, checked before asking the server. */\nexport function isTooSoon(fireAt: number, now = Date.now()): boolean {\n return fireAt < now + minLeadMs();\n}\n\n/**\n * The earliest time a send-time picker offers: one lead out, plus the minute the picker\n * can't show (it reads to the minute) and the moment the publisher spends choosing, so the\n * time it proposes is still outside the lead when it reaches the server.\n */\nexport function earliestFireAt(now = Date.now()): Date {\n return new Date(now + minLeadMs() + 60_000);\n}\n\n/**\n * True when the deployment sends through the `fake` stand-in, which records a send but\n * never delivers: local development, the tests, or a hosted demo, each of which names it on\n * purpose (SPEC \u00A79). This is the developer's \"nothing is delivered\" voice. A deployment\n * partway through setup is the other state, not connected (`providerNotConnected`). Read\n * from the read-only deployment reflection fetched once at boot; the copy avoids the\n * internal \"fake transport\" term (docs/DESIGN.md \u00A74).\n */\nexport function noEmailProvider(): boolean {\n return appState.appConfig?.deployment.provider === \"fake\";\n}\n\n/**\n * True when no email provider is connected (SPEC \u00A79): a deployment whose `PROVIDER` is not\n * set yet, which refuses every send. The operator's voice: name the state and point to the\n * step that ends it, labelled \"Connect Amazon SES\" (`connectProviderLink`).\n */\nexport function providerNotConnected(): boolean {\n return appState.appConfig?.deployment.provider === \"none\";\n}\n\n/**\n * True when this is local development's preview of the not-connected state\n * (`SIMULATE_SENDS=not-connected`, SPEC \u00A710) rather than a deployment partway through setup,\n * as the reflection says. Its way out is a restart, not a deploy.\n */\nexport function notConnectedPreview(): boolean {\n return providerNotConnected() && appState.appConfig?.deployment.notConnectedPreview === true;\n}\n\n/** The setup step that connects a provider: Get started's Connect Amazon SES. */\nexport const CONNECT_PROVIDER_HREF = \"#/docs/ses\";\n\n/** The one link every not-connected line carries, worded for any provider. */\nexport function connectProviderLink(cls?: string): Html {\n const classAttr = cls ? html` class=\"${cls}\"` : null;\n return html`<a${classAttr} href=\"${CONNECT_PROVIDER_HREF}\">${NOT_CONNECTED_ADVICE.step}</a>`;\n}\n\n/**\n * What to do about a send held for want of a provider (SPEC \u00A79), as a page says it: the\n * shared advice (`refusalAdvice`) with its step as the link every not-connected line\n * carries, in place of the guide's mention. In the local preview there is nothing to\n * deploy, so it names the restart that ends the state instead.\n */\nexport function heldAdviceHtml(): Html {\n return notConnectedPreview()\n ? html`Restart <code>npm run dev</code> without <code>SIMULATE_SENDS=not-connected</code> to send.`\n : html`${connectProviderLink()}${NOT_CONNECTED_ADVICE.after}`;\n}\n\n/**\n * The one line for sends held while no provider is connected (DESIGN \u00A74): the state, the\n * held sends (`which`, a link to one or to the list of several), the reassurance every\n * refused send carries (SPEC \u00A712), and the fix, once. The dashboard's health block and the\n * Sent page's attention card both say it, in place of the state's line and the send's own.\n */\nexport function heldSendsHtml(which: Html, count: number): Html {\n const one = count === 1;\n return html`No email provider connected, so nothing can be sent and ${which} ${one ? \"is\" : \"are\"} held, with no one marked unsent; ${one ? \"it goes out at its\" : \"they go out at their\"} first retry once one is connected. ${heldAdviceHtml()}`;\n}\n\n/**\n * A page's standing not-connected warning (DESIGN \u00A74): the health block's amber treatment,\n * not dismissible, since the state holds every time the page is read.\n * `cls` names the surface's line; null when a provider is connected.\n */\nexport function notConnectedWarning(cls: string, words: Html): Html | null {\n return providerNotConnected()\n ? html`<div class=\"health amber ${cls}\" role=\"status\"><span class=\"health-dot\">\u26A0\uFE0F</span><div><span>${words}</span></div></div>`\n : null;\n}\n\n/**\n * A scheduled send's line while no provider is connected: its window is real and still\n * cancelable, so the countdown stays, but at the fire time it is held, not sent. Read from\n * the deployment reflection, not from the send, which carries no condition until it fires.\n */\nexport function heldWhenFiresHtml(): Html | null {\n return providerNotConnected()\n ? html`<div class=\"sched-held\">Will be held: no email provider is connected.</div>`\n : null;\n}\n\n/**\n * The standing line a dialog carries while no provider is connected (DESIGN \u00A74): not\n * dismissible, since the state holds every time the dialog opens, and gone\n * once a provider is set. `why` says what the state stops; `after` ends the sentence the\n * link starts. Null otherwise.\n */\nfunction notConnectedLine(why: string, after: string): Html | null {\n return providerNotConnected()\n ? html`<p class=\"hint not-connected\">No email provider connected, so ${why}. ${connectProviderLink()}${after}.</p>`\n : null;\n}\n\n/** The Send-a-test dialogs' standing line; their Send is disabled beside it. */\nexport function notConnectedTestLine(): Html | null {\n return notConnectedLine(\"a test can\u2019t be sent yet\", \" to send one\");\n}\n\n/** The Add subscriber dialog's standing line: adding sends a confirmation, so its Send\n * confirmation is disabled beside it and the address field left out. */\nexport function notConnectedAddLine(): Html | null {\n return notConnectedLine(\"a confirmation can\u2019t be sent yet\", \" to add subscribers\");\n}\n\n/** The Schedule dialog's standing line; its Schedule and Send now are disabled beside it. */\nexport function notConnectedScheduleLine(): Html | null {\n return notConnectedLine(\"this can\u2019t be scheduled yet\", \" to schedule it\");\n}\n\n/**\n * The Reschedule dialog's line. Moving a send already scheduled is still accepted (it sends\n * nothing), so its control stays live; the line says the send is held when it fires.\n */\nexport function notConnectedRescheduleLine(): Html | null {\n return notConnectedLine(\"this send is held when it fires, until one is\", \" to send it\");\n}\n\n/**\n * Suffix a send/test/schedule confirmation so it never reads as a real send on the `fake`\n * stand-in. Tense-neutral (\"nothing is delivered\") so it fits a test that already ran, a\n * send now queued, and a schedule that will fire later alike. A not-connected deployment\n * refuses those acts, so it never gets this far.\n */\nexport function withNoProviderNote(msg: string): string {\n return noEmailProvider() ? `${msg} (local stand-in: nothing is delivered)` : msg;\n}\n\n/** A post's public archive address under this deployment, by the formula the Worker\n * uses (shared/archive_url.ts), or null before the reflection has loaded. */\nexport function postArchiveUrl(slug: string): string | null {\n const d = appState.appConfig?.deployment;\n return d ? archivePostUrl(d.archiveOrigin, d.archiveBasePath, slug) : null;\n}\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M280-280h280v-80H280v80Zm0-160h400v-80H280v80Zm0-160h400v-80H280v80Zm-80 480q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm0-560v560-560Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M272-200v-560h221q65 0 120 40t55 111q0 51-23 78.5T602-491q25 11 55.5 41t30.5 90q0 89-65 124.5T501-200H272Zm121-112h104q48 0 58.5-24.5T566-372q0-11-10.5-35.5T494-432H393v120Zm0-228h93q33 0 48-17t15-38q0-24-17-39t-44-15h-95v109Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2z\"/><path d=\"M4 21V5\"/><path d=\"M9 8h6\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 3v8a4 4 0 0 0 4 4h8\"/><path d=\"m14 11 4 4-4 4\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 6 9 17l-5-5\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"m8 12 3 3 5-6\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M8 12h8\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M320-240 80-480l240-240 57 57-184 184 183 183-56 56Zm320 0-57-57 184-184-183-183 56-56 240 240-240 240Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\"/><path d=\"M5 15V5a2 2 0 0 1 2-2h10\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"12\" rx=\"2\"/><path d=\"M8 20h8\"/><path d=\"M12 16v4\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M360-280v-400h80v160h160v-160h80v400h-80v-160H440v160h-80Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M120-120v-80h720v80H120Zm320-160v-80h400v80H440Zm0-160v-80h400v80H440Zm0-160v-80h400v80H440ZM120-760v-80h720v80H120Zm0 440v-320l160 160-160 160Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 16v-4M12 8h.01\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M440-280h80v-240h-80v240Zm68.5-331.5Q520-623 520-640t-11.5-28.5Q497-680 480-680t-28.5 11.5Q440-657 440-640t11.5 28.5Q463-600 480-600t28.5-11.5ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M200-200v-100h160l120-360H320v-100h400v100H580L460-300h140v100H200Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 360 360\" aria-hidden=\"true\"><path d=\"M290.028 216.064C285.698 218.264 280.838 219.394 275.978 219.344C281.268 225.044 284.128 232.924 283.708 240.694C283.278 248.844 279.388 256.514 274.268 262.874C269.148 269.244 262.808 274.494 256.608 279.814C237.148 296.474 218.248 314.404 195.328 325.854C178.568 334.224 160.148 338.864 143.458 347.374C140.148 349.064 136.898 350.904 133.418 352.224C129.938 353.544 126.178 354.324 122.498 353.794C121.718 353.684 120.938 353.504 120.228 353.184C119.508 352.864 118.848 352.374 118.408 351.734C117.918 351.024 117.708 350.154 117.718 349.304C117.728 348.454 117.948 347.614 118.268 346.824C118.567 346.054 118.967 345.339 119.371 344.618L119.418 344.534C128.788 327.894 140.118 312.454 150.358 296.344C160.598 280.224 169.848 263.174 174.428 244.634C176.258 237.264 177.328 229.644 176.738 222.074C176.138 214.504 173.808 206.964 169.308 200.854C164.588 194.434 157.728 189.874 150.538 186.434C139.628 181.204 127.768 178.284 115.858 176.154C93.9183 172.224 71.6083 170.884 49.4083 168.944C45.8483 168.634 42.2483 168.294 38.8783 167.124C34.7683 165.704 31.0683 163.004 28.7383 159.334C26.4183 155.664 25.5583 151.024 26.7483 146.844C27.6583 143.624 29.7283 140.784 32.3583 138.724C34.9883 136.654 38.1683 135.324 41.4383 134.634C44.7183 133.944 48.0983 133.874 51.4383 134.154C53.2683 134.304 55.0883 134.554 56.8983 134.824C72.2383 137.094 87.3783 140.484 102.548 143.644C115.468 146.334 128.498 148.864 141.688 149.224C154.878 149.574 168.338 147.654 180.178 141.824C196.548 133.764 208.758 118.514 215.298 101.474C224.258 78.094 223.028 52.174 220.398 27.284C219.788 21.484 219.108 15.614 220.058 9.854C220.178 9.124 220.328 8.38398 220.658 7.71398C220.988 7.05398 221.528 6.45399 222.228 6.19399C222.858 5.95399 223.568 6.01399 224.218 6.22399C224.858 6.43399 225.448 6.77399 226.028 7.12399C262.588 29.154 290.508 63.614 310.198 101.484C312.188 105.314 314.108 109.204 315.408 113.314C316.708 117.434 317.358 121.814 316.768 126.084C316.348 129.114 315.268 132.104 313.318 134.454C310.668 137.634 306.688 139.354 302.778 140.744C298.878 142.124 294.808 143.324 291.488 145.804C286.998 149.174 284.328 154.834 284.598 160.444C292.098 161.904 299.448 164.114 306.508 167.014C312.788 169.594 318.998 172.854 323.418 178.014C327.088 182.294 329.288 187.614 331.438 192.844C332.158 194.584 332.878 196.344 333.328 198.184C333.778 200.014 333.958 201.944 333.578 203.794C333.138 205.934 331.948 207.914 330.248 209.294C329.898 208.024 329.258 206.834 328.408 205.834C326.958 204.154 324.928 203.054 322.798 202.454C318.258 201.174 313.328 202.114 309.018 204.044C304.708 205.974 300.898 208.834 297.068 211.584C294.808 213.204 292.508 214.804 290.028 216.064Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 6h11M9 12h11M9 18h11M4 6h1M4 12h1M4 18h1\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm200 160v-80h160q50 0 85-35t35-85q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H520Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"m3 7 9 6 9-6\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M120-80v-60h100v-30h-60v-60h60v-30H120v-60h120q17 0 28.5 11.5T280-280v40q0 17-11.5 28.5T240-200q17 0 28.5 11.5T280-160v40q0 17-11.5 28.5T240-80H120Zm0-280v-110q0-17 11.5-28.5T160-510h60v-30H120v-60h120q17 0 28.5 11.5T280-560v70q0 17-11.5 28.5T240-450h-60v30h100v60H120Zm60-280v-180h-60v-60h120v240h-60Zm180 440v-80h480v80H360Zm0-240v-80h480v80H360Zm0-240v-80h480v80H360Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M720-330q0 104-73 177T470-80q-104 0-177-73t-73-177v-370q0-75 52.5-127.5T400-880q75 0 127.5 52.5T580-700v350q0 46-32 78t-78 32q-46 0-78-32t-32-78v-370h80v370q0 13 8.5 21.5T470-320q13 0 21.5-8.5T500-350v-350q-1-42-29.5-71T400-800q-42 0-71 29t-29 71v370q-1 71 49 120.5T470-160q70 0 119-49.5T640-330v-390h80v390Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"7\" y=\"2\" width=\"10\" height=\"20\" rx=\"2\"/><path d=\"M11 18h2\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"m228-240 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T458-480L320-240h-92Zm360 0 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T818-480L680-240h-92ZM362.5-517.5Q380-535 380-560t-17.5-42.5Q345-620 320-620t-42.5 17.5Q260-585 260-560t17.5 42.5Q295-500 320-500t42.5-17.5Zm360 0Q740-535 740-560t-17.5-42.5Q705-620 680-620t-42.5 17.5Q620-585 620-560t17.5 42.5Q655-500 680-500t42.5-17.5ZM680-560Zm-360 0Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"4\" y=\"11\" width=\"16\" height=\"10\" rx=\"2\"/><path d=\"M8 11V7a4 4 0 0 1 8 0v4\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"6.5\"/><path d=\"m20 20-4.2-4.2\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 3 11 14M22 3l-7 18-4-7-7-4 18-7z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M360-200v-80h480v80H360Zm0-240v-80h480v80H360Zm0-240v-80h480v80H360ZM200-160q-33 0-56.5-23.5T120-240q0-33 23.5-56.5T200-320q33 0 56.5 23.5T280-240q0 33-23.5 56.5T200-160Zm0-240q-33 0-56.5-23.5T120-480q0-33 23.5-56.5T200-560q33 0 56.5 23.5T280-480q0 33-23.5 56.5T200-400Zm-56.5-263.5Q120-687 120-720t23.5-56.5Q167-800 200-800t56.5 23.5Q280-753 280-720t-23.5 56.5Q233-640 200-640t-56.5-23.5Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 16V4M8 8l4-4 4 4\"/><path d=\"M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\"><path d=\"M6 6l12 12M18 6 6 18\"/></svg>\n", "// Icons as files: client/ui/icons/*.svg, each a complete, viewable SVG that the build imports\n// as text and the editor serves inline, so an icon can be opened, previewed, and diffed as\n// the drawing it is. One registry, one renderer; the sidebar's static sprite in index.html\n// is separate because it paints before any script runs.\n\nimport { type Html, unsafeHtml } from \"./html\";\nimport article from \"./icons/article.svg\";\nimport bold from \"./icons/bold.svg\";\nimport book from \"./icons/book.svg\";\nimport branch from \"./icons/branch.svg\";\nimport check from \"./icons/check.svg\";\nimport circleCheck from \"./icons/circle-check.svg\";\nimport circleMinus from \"./icons/circle-minus.svg\";\nimport code from \"./icons/code.svg\";\nimport copyout from \"./icons/copyout.svg\";\nimport desktop from \"./icons/desktop.svg\";\nimport editable from \"./icons/editable.svg\";\nimport heading from \"./icons/heading.svg\";\nimport indent from \"./icons/indent.svg\";\nimport info from \"./icons/info.svg\";\nimport infoFilled from \"./icons/info-filled.svg\";\nimport italic from \"./icons/italic.svg\";\nimport kestrel from \"./icons/kestrel.svg\";\nimport lines from \"./icons/lines.svg\";\nimport link from \"./icons/link.svg\";\nimport mail from \"./icons/mail.svg\";\nimport ol from \"./icons/ol.svg\";\nimport paperclip from \"./icons/paperclip.svg\";\nimport phone from \"./icons/phone.svg\";\nimport plus from \"./icons/plus.svg\";\nimport preview from \"./icons/preview.svg\";\nimport quote from \"./icons/quote.svg\";\nimport readonly from \"./icons/readonly.svg\";\nimport search from \"./icons/search.svg\";\nimport send from \"./icons/send.svg\";\nimport ul from \"./icons/ul.svg\";\nimport upload from \"./icons/upload.svg\";\nimport x from \"./icons/x.svg\";\n\n// The files are the app's own drawings, so vouching for them here is the one place icons\n// become markup; every other use goes through icon().\nconst ICONS = {\n article: unsafeHtml(article),\n bold: unsafeHtml(bold),\n check: unsafeHtml(check),\n code: unsafeHtml(code),\n copyout: unsafeHtml(copyout),\n editable: unsafeHtml(editable),\n heading: unsafeHtml(heading),\n indent: unsafeHtml(indent),\n info: unsafeHtml(info),\n \"info-filled\": unsafeHtml(infoFilled),\n italic: unsafeHtml(italic),\n kestrel: unsafeHtml(kestrel),\n lines: unsafeHtml(lines),\n link: unsafeHtml(link),\n ol: unsafeHtml(ol),\n paperclip: unsafeHtml(paperclip),\n plus: unsafeHtml(plus),\n preview: unsafeHtml(preview),\n quote: unsafeHtml(quote),\n readonly: unsafeHtml(readonly),\n search: unsafeHtml(search),\n send: unsafeHtml(send),\n ul: unsafeHtml(ul),\n upload: unsafeHtml(upload),\n book: unsafeHtml(book),\n branch: unsafeHtml(branch),\n \"circle-check\": unsafeHtml(circleCheck),\n \"circle-minus\": unsafeHtml(circleMinus),\n desktop: unsafeHtml(desktop),\n mail: unsafeHtml(mail),\n phone: unsafeHtml(phone),\n x: unsafeHtml(x),\n} as const;\n\nexport type IconName = keyof typeof ICONS;\n\n/** Whether a string names an icon; for call sites that pick one by a runtime key. */\nexport function isIconName(name: string): name is IconName {\n return Object.hasOwn(ICONS, name);\n}\n\n/** The inline markup of an icon, decorative (aria-hidden) and sized by its container. */\nexport function icon(name: IconName): Html {\n return ICONS[name];\n}\n", "// The reference room shell shared by Docs and the API reference: the top bar, the\n// contents rail, and the surface switch; installRoomBar() wires the bar's one delegate.\n\nimport { buildRefParts } from \"../deployment\";\nimport { app } from \"../shell\";\nimport { appState } from \"../state\";\nimport { type Html, html } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\n\n/** The two surfaces the room switches between. */\nexport type RoomSurface = \"docs\" | \"reference\";\n\n/**\n * The reference room shell shared by Docs / API: a top bar (a rail-width \"\u2190 Dashboard\",\n * the Kestrel mark, the surface switch) over a two-column grid whose left column, the\n * contents rail, lines up exactly under \"\u2190 Dashboard\". Pass rail = null for a surface\n * with no contents rail.\n *\n * On mobile (\u2264720px, styles.css) the same markup collapses to one --bar-h row (a square\n * back arrow, the mark, the tabs) so the bar never grows past the height the in-page\n * anchors and the sticky rail are calibrated for; the \u2715 drops there (\u2190 is the one way\n * back). The body stacks, and each rail decides its own mobile shape (a folded \"On this\n * page\", or a chip row; see the surfaces).\n */\nexport function roomShell(active: RoomSurface, rail: Html | null, main: Html): Html {\n const tab = (view: RoomSurface, label: string) =>\n active === view\n ? html`<a href=\"#/${view}\" data-room=\"${view}\" data-text=\"${label}\" aria-current=\"page\">${label}</a>`\n : html`<a href=\"#/${view}\" data-room=\"${view}\" data-text=\"${label}\">${label}</a>`;\n // The running build (SPEC \u00A79) as quiet metadata (version \u2192 release, sha \u2192 commit),\n // pinned to the rail's bottom-left corner on every surface, so the bar stays identity +\n // nav. On mobile the rail is no longer a column, so the same stamp is the room's foot\n // instead (styles.css shows one or the other). Nothing until a build is known; the build\n // time rides the tooltip. Build info lives in the app's own room (and at GET\n // /api/version), never in the publisher-facing dashboard.\n const parts = buildRefParts();\n const bt = appState.appConfig?.deployment.build.buildTime;\n const built = bt\n ? `Built ${new Date(bt).toLocaleString(undefined, { dateStyle: \"medium\", timeStyle: \"short\" })}`\n : null;\n const stamp = parts\n ? html`${parts.version} <span aria-hidden=\"true\">\u00B7</span> ${parts.sha}`\n : null;\n const railFoot = stamp\n ? built\n ? html`<div class=\"rail-foot\" title=\"${built}\">${stamp}</div>`\n : html`<div class=\"rail-foot\">${stamp}</div>`\n : null;\n const foot = stamp\n ? built\n ? html`<footer class=\"room-foot\" title=\"${built}\">${stamp}</footer>`\n : html`<footer class=\"room-foot\">${stamp}</footer>`\n : null;\n const body =\n rail == null\n ? html`<div class=\"room-body norail\"><div class=\"room-main\">${main}</div></div>`\n : html`<div class=\"room-body\"><nav class=\"room-rail\" aria-label=\"Contents\"><div class=\"rail-inner\">${rail}${railFoot}</div></nav><div class=\"room-main\">${main}</div></div>`;\n return html`<div class=\"room\">\n <header class=\"room-bar\">\n <a class=\"room-back\" href=\"#/dashboard\" aria-label=\"Back to dashboard\"><span aria-hidden=\"true\">\u2190</span><span class=\"room-back-label\">Dashboard</span></a>\n <div class=\"room-nav\">\n <a class=\"room-brand\" href=\"#/docs\">${icon(\"kestrel\")}<span>Kestrel</span></a>\n <nav class=\"room-switch\" aria-label=\"Reference\">${tab(\"docs\", \"Docs\")}${tab(\"reference\", \"API\")}</nav>\n <a class=\"room-close\" href=\"#/dashboard\" title=\"Back to publication\" aria-label=\"Back to publication\"><span aria-hidden=\"true\">\u2715</span></a>\n </div>\n </header>\n ${body}\n ${foot}\n </div>`;\n}\n\n/**\n * A room-bar link to the page you're already on (the active tab, or the wordmark on the\n * docs index) sets the hash to what it already is, so no hashchange fires and nothing\n * re-renders or scrolls. Make it the \"back to the top\" it reads as, so the bar behaves\n * the same whether or not the tap happens to change the hash. Instant, like every other\n * in-page move here (the \"On this page\" links) and like a cross-page click landing at\n * the top of its page. Delegated once on #app, so it survives every re-render of the room;\n * boot calls this once.\n */\nexport function installRoomBar(): void {\n app.addEventListener(\"click\", (ev) => {\n const a = ev.target instanceof Element ? ev.target.closest(\".room-bar a[href^='#/']\") : null;\n if (!a || a.getAttribute(\"href\") !== location.hash) {\n return;\n }\n ev.preventDefault();\n window.scrollTo(0, 0);\n });\n}\n", "// The one spelling of a link into the editor from outside it (a notification email to the\n// publisher), shared by the Worker that writes the link and the editor that opens it, so\n// the two can't disagree.\n//\n// The editor's pages are hash routes (`#/sent/<id>`), but a browser never sends the hash\n// to the server, so a login in between (Cloudflare Access, when the publisher isn't\n// signed in) returns to `/dashboard/` without it and lands on the editor's home. A link\n// from outside carries its route in the query instead, which the login keeps, and the\n// editor turns it into the hash at boot.\n\n/** The query parameter that carries the route. */\nconst PARAM = \"to\";\n\n/** A link from outside the editor to one of its pages: `route` is the hash route without\n * its `#` (`/sent/<id>`), each segment already encoded. The route's slashes stay literal,\n * so the link reads as the page it opens (`\u2026/dashboard/?to=/sent/<id>`). */\nexport function dashboardLink(appOrigin: string, route: string): string {\n return `${appOrigin}/dashboard/?${PARAM}=${encodeURIComponent(route).replaceAll(\"%2F\", \"/\")}`;\n}\n\n/** A route: `/` and then the characters an encoded segment uses, repeated. */\nconst ROUTE = /^(\\/[A-Za-z0-9._~%-]+)+$/;\n\n/** A segment a URL parser reads as `.` or `..`, spelled out or percent-encoded. The editor\n * puts a route's segments into API paths as they are, so one of these would climb out of\n * the path the page meant to read. */\nconst DOT_SEGMENT = /\\/(?:\\.|%2e){1,2}(?=\\/|$)/i;\n\n/** Whether `route` (a hash route without its `#`) is one the editor will open: path-shaped,\n * with no dot segment. The editor checks its hash with this as well as a link's query, so a\n * route typed into the address bar is held to the same rule as one from an email. */\nexport function isDashboardRoute(route: string): boolean {\n return ROUTE.test(route) && !DOT_SEGMENT.test(route);\n}\n\n/** The hash a link's query names (`#/sent/<id>`), or null when it names none. Only a route\n * the editor will open is taken, so a mangled or hand-typed query opens the editor's home\n * rather than a nonsense route. */\nexport function dashboardRouteFromSearch(search: string): string | null {\n const route = new URLSearchParams(search).get(PARAM);\n return route && isDashboardRoute(route) ? `#${route}` : null;\n}\n", "/**\n * The send cursor (SPEC \u00A78): where a read of sends stood, handed to a client so it can\n * later ask for what changed since.\n *\n * It carries two positions. `seq` is the change sequence at the read: a send whose visible\n * state changed afterward has a higher `rev`. `at` is the server's time of the read, for\n * what changes with the clock rather than with a write (a fire time passing, a send\n * becoming stuck, a lease running out), which no `rev` can record.\n *\n * The format is part of the API, not a private detail: `<seq>.<at>`, both decimal\n * integers. Any client may read it and compare two, so a client following sends from\n * several reads (a list, and one send's page) can follow them all from the earlier cursor\n * in one feed read, as the editor does; nothing here is the editor's alone. A cursor that\n * does not parse is refused, never guessed at.\n */\n\n/** A read's position among the changes to sends. */\nexport interface SendCursor {\n seq: number;\n at: number;\n}\n\nconst PATTERN = /^(\\d+)\\.(\\d+)$/;\n\n/** The cursor as the API carries it. */\nexport function encodeSendCursor(cursor: SendCursor): string {\n return `${cursor.seq}.${cursor.at}`;\n}\n\n/** A cursor a client handed back, or null when it is not one this server issued. */\nexport function decodeSendCursor(raw: string): SendCursor | null {\n const match = PATTERN.exec(raw);\n if (!match?.[1] || !match[2]) {\n return null;\n }\n const seq = Number(match[1]);\n const at = Number(match[2]);\n return Number.isSafeInteger(seq) && Number.isSafeInteger(at) ? { seq, at } : null;\n}\n\n/**\n * A cursor at or before both (the smaller `seq` and the smaller `at`): asking what changed\n * since it answers for each of them, and a send reported twice reads the same both times.\n * The rule any client may apply to the documented format; one that does not parse gives way\n * to the other.\n */\nexport function earlierCursor(a: string, b: string): string {\n const x = decodeSendCursor(a);\n const y = decodeSendCursor(b);\n if (!x || !y) {\n return x ? a : b;\n }\n return encodeSendCursor({ seq: Math.min(x.seq, y.seq), at: Math.min(x.at, y.at) });\n}\n", "// The send-state layer: one shared read of the send feed (GET /api/sends/feed), which pages\n// follow instead of polling, so every page tells the same story about a send and keeps up\n// with it, whichever client changed it (docs/DESIGN.md \u00A74). The server says when to read\n// again; the layer keeps to it.\n\nimport { earlierCursor } from \"../shared/cursor\";\nimport type { SendFeedResponse, SendPhase, SendStatus, SendView } from \"../shared/sends\";\nimport { ApiError, api } from \"./api\";\n\n/**\n * Where a send is in its life, as a page reacts to it: in the review window, due (its\n * fire time has passed and the next sweep starts it), sending, sent with receipts still\n * arriving, complete, or canceled. Coarser than the phase: a send that retries, backs off,\n * or needs attention is still sending.\n */\nexport type SendStage = \"scheduled\" | \"due\" | \"sending\" | \"sent\" | \"complete\" | \"canceled\";\n\n/** A send's stage, from its status and derived phase. */\nexport function stageOf(s: { status: SendStatus; phase: SendPhase }): SendStage {\n switch (s.status) {\n case \"scheduled\":\n return s.phase === \"due\" ? \"due\" : \"scheduled\";\n case \"sending\":\n return \"sending\";\n case \"sent\":\n return s.phase === \"complete\" ? \"complete\" : \"sent\";\n default:\n return \"canceled\";\n }\n}\n\n/** A send that moved since a follower last looked; `from` is null for one it had not seen. */\nexport interface StageChange {\n send: SendView;\n from: SendStage | null;\n to: SendStage;\n}\n\n/** What a follower of every send gets from a read that found something changed. */\nexport interface SendsUpdate {\n /** Every send that changed since the follower's last update, soonest fire first. */\n sends: SendView[];\n /** Those whose stage moved, a send the follower had not seen included. */\n changes: StageChange[];\n /** The ids of sends removed since (deleted with their post). */\n removed: string[];\n}\n\n/**\n * What a page does with what the layer reads. `stale` is the server saying the page's\n * cursor is ahead of its database (reset or restored since): nothing after it will ever be\n * reported, so the layer stops following for every page, and each re-reads its own list or\n * send and follows again from that read.\n */\nexport interface SendsFollower {\n update(update: SendsUpdate): void;\n stale(): void;\n}\n\n/** What a page following one send does with what the layer reads: `update` for each read\n * that changed it, `removed` once it is deleted, `stale` as for `SendsFollower`. */\nexport interface SendFollower {\n update(send: SendView, change: StageChange | null): void;\n removed(): void;\n stale(): void;\n}\n\n/** Where a page's own read of sends (`GET /api/sends`) stood: its cursor, and the rows it painted. */\nexport interface ListRead {\n cursor: string;\n sends: readonly { id: string; status: SendStatus; phase: SendPhase }[];\n}\n\n/** Where a page's own read of one send (`GET /api/sends/:id`, or an action's answer) stood. */\nexport interface SendRead {\n cursor: string;\n send: Pick<SendView, \"id\" | \"status\" | \"phase\">;\n}\n\n// A failed read is tried again after this, doubling up to the cap while it keeps failing.\nconst RETRY_MS = 3000;\nconst RETRY_MAX_MS = 60_000;\n// A read that has not answered by this is cut off and counts as failed. Without it, one\n// that never settles (a dropped connection, a dev server restarted mid-request) would hold\n// `reading` for good, and the layer would never read again until the page was reloaded.\nconst READ_TIMEOUT_MS = 30_000;\n\n/** One page's interest in the feed. */\ninterface Follower {\n /** Take a read it was part of. */\n take(res: SendFeedResponse): void;\n /** Its cursor is ahead of the database: it has been dropped, and reads again. */\n stale(): void;\n}\n\nconst followers = new Set<Follower>();\n// Where the layer's last read stood, and the earliest cursor a page joined with since: the\n// next read asks from the earlier of the two, so it answers for every follower (a send\n// reported twice reads the same both times).\nlet cursor: string | null = null;\nlet floor: string | null = null;\nlet timer: ReturnType<typeof setTimeout> | undefined;\nlet reading: AbortController | null = null;\nlet readAgain = false;\nlet readQueued = false;\nlet failures = 0;\n\n// Whatever a follower's handler throws is its own bug: reported, and never allowed to stop\n// the others or the cadence.\nfunction safely(fn: () => void): void {\n try {\n fn();\n } catch (e) {\n reportError(e);\n }\n}\n\nfunction lower(a: string | null, b: string | null): string | null {\n return a && b ? earlierCursor(a, b) : (a ?? b);\n}\n\nfunction arm(ms: number): void {\n clearTimeout(timer);\n timer = undefined;\n if (!followers.size || document.hidden) {\n return;\n }\n timer = setTimeout(read, ms);\n}\n\nfunction read(): void {\n clearTimeout(timer);\n timer = undefined;\n if (!followers.size) {\n return;\n }\n if (reading) {\n readAgain = true; // someone new, or a page that just acted: once more when this lands\n return;\n }\n const since = lower(cursor, floor);\n floor = null;\n const round = [...followers];\n const c = new AbortController();\n reading = c;\n const cutOff = setTimeout(() => c.abort(), READ_TIMEOUT_MS);\n const query = since ? `?since=${encodeURIComponent(since)}` : \"\";\n api<SendFeedResponse>(`/api/sends/feed${query}`, { signal: c.signal }).then(\n (res) => {\n clearTimeout(cutOff);\n if (reading !== c) {\n return; // stopped meanwhile\n }\n reading = null;\n failures = 0;\n cursor = res.cursor;\n for (const f of round) {\n if (followers.has(f)) {\n f.take(res);\n }\n }\n if (readAgain) {\n readAgain = false;\n read();\n return;\n }\n // By the server's clock: its `now` and `read_again_at` are one clock, whatever ours says.\n arm(Math.max(0, res.read_again_at - res.now));\n },\n (err: unknown) => {\n clearTimeout(cutOff);\n if (reading !== c) {\n return; // stopped meanwhile (a cut-off read is still the one out, so it goes on)\n }\n reading = null;\n readAgain = false;\n if (err instanceof ApiError && err.code === \"cursor_ahead\") {\n // The database is behind every cursor the layer holds (a reset or a restore): no\n // read from them would ever report anything. Every page reads its own sends again\n // and follows from that read.\n const dropped = [...followers];\n for (const f of dropped) {\n leave(f);\n }\n for (const f of dropped) {\n safely(() => f.stale());\n }\n return;\n }\n if (err instanceof ApiError && err.status === 400) {\n // A cursor this server no longer reads (one from before a deploy changed its\n // format): start again from what can change on its own.\n cursor = null;\n } else {\n floor = lower(floor, since);\n }\n // A background read that fails stays quiet and is tried again, spaced out: the page\n // loaded from its own read, and the next good read reports everything since.\n failures += 1;\n arm(Math.min(RETRY_MS * 2 ** (failures - 1), RETRY_MAX_MS));\n },\n );\n}\n\n// Followers that join in the same tick (a page wiring several sections) share one read.\nfunction queueRead(): void {\n if (readQueued) {\n return;\n }\n readQueued = true;\n queueMicrotask(() => {\n readQueued = false;\n read();\n });\n}\n\n// Hidden, the layer reads nothing (its timer is dropped, and `arm` sets none); shown again,\n// it reads at once, since anything may have happened meanwhile.\nfunction onVisibility(): void {\n if (document.hidden) {\n clearTimeout(timer);\n timer = undefined;\n } else {\n read();\n }\n}\n\nfunction join(f: Follower, since: string, signal: AbortSignal): void {\n if (signal.aborted) {\n return;\n }\n followers.add(f);\n floor = lower(floor, since);\n if (followers.size === 1) {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n signal.addEventListener(\"abort\", () => leave(f), { once: true });\n queueRead();\n}\n\nfunction leave(f: Follower): void {\n followers.delete(f);\n if (followers.size) {\n return;\n }\n clearTimeout(timer);\n timer = undefined;\n reading?.abort();\n reading = null;\n readAgain = false;\n failures = 0;\n cursor = null;\n floor = null;\n document.removeEventListener(\"visibilitychange\", onVisibility);\n}\n\n/**\n * Follow every send from the page's own list read, for the life of `signal`. The first\n * feed read asks from that read's cursor, so nothing that changed between the two is\n * missed. `follower.update` gets each later read that found a change, with the stage\n * changes since the page's read (a send it did not list reads as first seen) and the sends\n * removed.\n */\nexport function followSends(from: ListRead, follower: SendsFollower, signal: AbortSignal): void {\n const seen = new Map<string, SendStage>(from.sends.map((s) => [s.id, stageOf(s)]));\n join(\n {\n take(res) {\n if (!res.sends.length && !res.removed.length) {\n return;\n }\n const changes: StageChange[] = [];\n for (const send of res.sends) {\n const to = stageOf(send);\n const was = seen.get(send.id) ?? null;\n if (was !== to) {\n changes.push({ send, from: was, to });\n }\n seen.set(send.id, to);\n }\n const removed = res.removed.map((r) => r.id);\n for (const id of removed) {\n seen.delete(id);\n }\n safely(() => follower.update({ sends: res.sends, changes, removed }));\n },\n stale: () => follower.stale(),\n },\n from.cursor,\n signal,\n );\n}\n\n/**\n * Follow one send from the page's own read of it, for the life of `signal`, whatever its\n * state. `follower.update` gets the send from each later read that found it changed, with\n * its stage change when it moved; `follower.removed` is told once it is deleted.\n */\nexport function followSend(from: SendRead, follower: SendFollower, signal: AbortSignal): void {\n const id = from.send.id;\n let seen: SendStage = stageOf(from.send);\n join(\n {\n take(res) {\n if (res.removed.some((r) => r.id === id)) {\n safely(() => follower.removed());\n return;\n }\n const send = res.sends.find((s) => s.id === id);\n if (!send) {\n return;\n }\n const to = stageOf(send);\n const change = to !== seen ? { send, from: seen, to } : null;\n seen = to;\n safely(() => follower.update(send, change));\n },\n stale: () => follower.stale(),\n },\n from.cursor,\n signal,\n );\n}\n\n/**\n * Read now, for a page that has just acted on a send (a cancel, a move, a Resolve), so\n * everything following sends catches up at once instead of at the next read. Nothing when\n * nothing follows.\n */\nexport function readSendsNow(): void {\n if (followers.size) {\n read();\n }\n}\n", "// What a valid email address is, spelled once for the Worker and the editor, so a form\n// never accepts an address the API will refuse, or refuses one it would take.\n\n/** The form an address is stored and compared in: trimmed and lowercased. */\nexport function normalizeEmail(email: string): string {\n return email.trim().toLowerCase();\n}\n\n/**\n * A plausible address: one `@`, something before it, and a dotted domain after it, with\n * no whitespace. Deliberately loose: the confirmation email is the real check (SPEC \u00A77).\n * Pass it a normalized address.\n */\nexport function isValidEmail(email: string): boolean {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);\n}\n", "// Pure formatting and parsing, with no DOM: dates, countdowns, the datetime-local value,\n// and the recipient list.\n\nimport { isValidEmail, normalizeEmail } from \"../../shared/email\";\n\n/** A short local date-time, or an em dash for none. */\nexport const fmt = (ms: number | null | undefined): string =>\n ms\n ? new Date(ms).toLocaleString(undefined, {\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n })\n : \"\u2014\";\n\n/**\n * Split a free-text recipient list (newlines or commas) into unique, normalized\n * addresses, and the entries that aren't addresses, as typed, so the caller can name\n * them instead of quietly sending to fewer people than were listed. Server-side\n * validation is still authoritative; this tidies the Send-test input.\n */\nexport function parseAddresses(text: string | null | undefined): {\n valid: string[];\n invalid: string[];\n} {\n const valid = new Set<string>();\n const invalid: string[] = [];\n for (const part of String(text || \"\").split(/[\\n,]+/)) {\n const a = normalizeEmail(part);\n if (isValidEmail(a)) {\n valid.add(a);\n } else if (a) {\n invalid.push(part.trim());\n }\n }\n return { valid: [...valid], invalid };\n}\n\n/** The toast for Send-test entries that aren't addresses, or null when there are none. */\nexport function invalidAddressesMessage(invalid: string[]): string | null {\n if (!invalid.length) {\n return null;\n }\n return `Not ${invalid.length === 1 ? \"an email address\" : \"email addresses\"}: ${invalid.join(\", \")}`;\n}\n\n/**\n * The dialog error for a test that didn't reach every address: who got it, who didn't,\n * each distinct reason once, and that Send again tries only those. Both test dialogs leave\n * just the failed addresses in their field, so a second press mails no one twice.\n */\nexport function notSentMessage(\n sent: number,\n total: number,\n failed: string[],\n reasons: string[] = [],\n): string {\n const why = reasons.length ? ` ${reasons.join(\" \")}` : \"\";\n return sent\n ? `Sent to ${sent} of ${total}. Not sent to ${failed.join(\", \")}.${why} Send again to try those.`\n : `Not sent.${why} Send again to try.`;\n}\n\n/** A Date as the value of a `datetime-local` input, in local time. */\nexport function toLocalInput(d: Date): string {\n const p = (n: number) => String(n).padStart(2, \"0\");\n return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;\n}\n\n/**\n * A countdown that gets coarser the further out the fire time is: it ticks seconds\n * only inside the last few minutes (the imminent / send-now cancel window), then\n * counts down by the minute, by the hour within a day, and by whole days beyond, so\n * a send scheduled days away reads \"Sends in 2 days\", not a ticking \"47h 47m\".\n */\nexport function untilStr(fireAt: number, due = false): string {\n const d = fireAt - Date.now();\n if (due || d <= 0) {\n // From the fire time the send waits for the sweep's tick on that minute (a fire time is\n // on the minute, SPEC \u00A76), a few seconds; nothing is sending yet, and the card leaves the\n // queue once something is (docs/DESIGN.md \u00A74).\n return \"Preparing to send\u2026\";\n }\n const s = Math.floor(d / 1000);\n const min = Math.floor(s / 60);\n const hr = Math.floor(min / 60);\n if (s < 300) {\n return min > 0 ? `Sends in ${min}m ${String(s % 60).padStart(2, \"0\")}s` : `Sends in ${s}s`;\n }\n if (min < 60) {\n return `Sends in ${min}m`;\n }\n if (hr < 24) {\n const rm = min % 60;\n return rm > 0 ? `Sends in ${hr}h ${rm}m` : `Sends in ${hr}h`;\n }\n const days = Math.round(hr / 24);\n return `Sends in ${days} day${days === 1 ? \"\" : \"s\"}`;\n}\n\n/** A scheduled send past the server's missed tolerance, by how late it now is. */\nexport function lateStr(fireAt: number): string {\n const min = Math.max(0, Math.floor((Date.now() - fireAt) / 60_000));\n return `Missed its fire time \u00B7 ${min} min late`;\n}\n", "// Shared list controls: the query model, toolbar, sortable headers, and pager.\n\nimport type { PageMeta, SortDir } from \"../../shared/list\";\nimport { type Html, html, setHtml } from \"./html\";\n\n// One convention for the admin list views (Posts, Subscribers, Sends): a search-left\n// / filters-right toolbar, clickable sortable column headers, and offset pagination \u2014\n// all driven by a small per-view `state` object whose reload() rebuilds the query and\n// re-fetches. The hash only seeds the initial filter (dashboard deep-links); sort,\n// page, and filter operate in place, matching the `page` envelope the endpoints return\n// (shared/list.ts).\n\n/** The per-view list state every control reads and writes. */\nexport interface ListState {\n status?: string;\n search?: string;\n sort?: string;\n dir?: SortDir;\n /** \"only\" narrows to suppressed subscribers; a deliverability axis, independent of status. */\n suppressed?: \"\" | \"only\";\n /** \"only\" narrows to sends with delivery failures. */\n failures?: \"\" | \"only\";\n offset: number;\n limit: number;\n}\n\n/** What a list toolbar offers; see listToolbar for what each field means. */\nexport interface ToolbarConfig {\n statuses?: { value: string; label: string }[];\n allValue?: string;\n searchPlaceholder?: string;\n suppressible?: boolean;\n failures?: boolean;\n}\n\n/**\n * Build the query string for a list request from the view state. `sort`/`dir` are sent\n * only once a column is chosen (state.sort set), so a view keeps its endpoint's bespoke\n * default order (e.g. posts' scheduled-first) until the reader sorts.\n */\nexport function listQuery(state: ListState): string {\n const p = new URLSearchParams();\n if (state.status) {\n p.set(\"status\", state.status);\n }\n if (state.suppressed) {\n p.set(\"suppressed\", state.suppressed);\n }\n if (state.failures) {\n p.set(\"failures\", state.failures);\n }\n const term = (state.search || \"\").trim();\n if (term) {\n p.set(\"search\", term);\n }\n if (state.sort) {\n p.set(\"sort\", state.sort);\n p.set(\"dir\", state.dir ?? \"desc\");\n }\n p.set(\"limit\", String(state.limit));\n p.set(\"offset\", String(state.offset));\n return p.toString();\n}\n\n/**\n * A filter/search toolbar: search on the left, the status filter pinned right.\n * `cfg.statuses` = [{value,label}]. `cfg.suppressible` (subscribers only) adds a\n * separate \"Suppressed only\" toggle \u2014 suppression is a deliverability flag, not a\n * consent status, so it's its own control (an independent axis you can combine with a\n * status), never an option inside the status dropdown.\n * `cfg.allValue` sets what the \"All statuses\" option means \u2014 normally \"\" (no status\n * filter), but the Drafts view passes \"draft,scheduled\" so \"All\" stays scoped to the\n * two draft-side statuses rather than reaching sent posts. Omit `cfg.statuses` for a\n * search-only toolbar (the Sent list is single-status, so it carries no status filter).\n * `cfg.failures` adds the Sent list's \"With delivery failures\" flag \u2014 a filter, not a sort: it keeps\n * the newest-first order the operator scans by and needs no severity weighting (a summed\n * sort would rank 25 retried unsent recipients above one spam complaint).\n */\nexport function listToolbar(cfg: ToolbarConfig): Html {\n const statusSel = cfg.statuses?.length\n ? html`<select class=\"lt-status\" aria-label=\"Filter by status\"><option value=\"${cfg.allValue || \"\"}\">All statuses</option>${cfg.statuses.map(\n (s) => html`<option value=\"${s.value}\">${s.label}</option>`,\n )}</select>`\n : null;\n const suppressed = cfg.suppressible\n ? html`<label class=\"lt-toggle\"><input type=\"checkbox\" class=\"lt-suppressed\"><span>Suppressed</span></label>`\n : null;\n const failures = cfg.failures\n ? html`<label class=\"lt-toggle\"><input type=\"checkbox\" class=\"lt-failures\"><span>With delivery failures</span></label>`\n : null;\n return html`<div class=\"list-toolbar\">\n <input class=\"lt-search\" type=\"search\" placeholder=\"${cfg.searchPlaceholder || \"Search\u2026\"}\" aria-label=\"Search\" autocomplete=\"off\">\n <div class=\"lt-filters\">${statusSel}${suppressed}${failures}</div>\n </div>`;\n}\n\n/**\n * Wire the toolbar controls (within `root`) to the view's reload, seeding their values\n * from state so a deep-linked filter shows selected. Search is debounced; any change\n * resets to the first page. Status and the suppression toggle are independent axes.\n */\nexport function wireToolbar(root: ParentNode, state: ListState, reload: () => void): void {\n const search = root.querySelector<HTMLInputElement>(\".lt-search\");\n const status = root.querySelector<HTMLSelectElement>(\".lt-status\");\n const suppressed = root.querySelector<HTMLInputElement>(\".lt-suppressed\");\n const failures = root.querySelector<HTMLInputElement>(\".lt-failures\");\n if (search) {\n search.value = state.search || \"\";\n let t: number | undefined;\n search.oninput = () => {\n clearTimeout(t);\n t = setTimeout(() => {\n state.search = search.value;\n state.offset = 0;\n reload();\n }, 250);\n };\n }\n if (status) {\n status.value = state.status || \"\";\n status.onchange = () => {\n state.status = status.value;\n state.offset = 0;\n reload();\n };\n }\n if (suppressed) {\n suppressed.checked = state.suppressed === \"only\";\n suppressed.onchange = () => {\n state.suppressed = suppressed.checked ? \"only\" : \"\";\n state.offset = 0;\n reload();\n };\n }\n if (failures) {\n failures.checked = state.failures === \"only\";\n failures.onchange = () => {\n state.failures = failures.checked ? \"only\" : \"\";\n state.offset = 0;\n reload();\n };\n }\n}\n\n/**\n * A table header cell. A sortable column (given a `key`) renders a button that toggles\n * asc/desc and shows the active direction; other columns are plain labels. `cls` adds a\n * column class (e.g. \"num\" for right-aligned numeric columns).\n */\nexport function th(label: string, key: string | null, state: ListState, cls?: string): Html {\n if (!key) {\n return cls ? html`<th class=\"${cls}\">${label}</th>` : html`<th>${label}</th>`;\n }\n const active = state.sort === key;\n // A fixed-width slot always reserved (empty when unsorted) so the label doesn't\n // shift when the arrow appears; light \u2191/\u2193 to match the app's other arrows.\n const arrow = active ? (state.dir === \"asc\" ? \"\u2191\" : \"\u2193\") : \"\";\n const klass = `${cls ? `${cls} ` : \"\"}sortable${active ? \" sorted\" : \"\"}`;\n return html`<th class=\"${klass}\"><button type=\"button\" class=\"th-sort\" data-sort=\"${key}\">${label}<span class=\"th-arrow\" aria-hidden=\"true\">${arrow}</span></button></th>`;\n}\n\n/**\n * Wire the sortable headers inside a freshly-rendered table. Clicking a column sorts by\n * it (default desc), or flips direction if it is already the sort key; resets to page 1.\n */\nexport function wireSort(container: ParentNode, state: ListState, reload: () => void): void {\n for (const b of container.querySelectorAll<HTMLButtonElement>(\".th-sort\")) {\n b.onclick = () => {\n const key = b.dataset.sort;\n if (state.sort === key) {\n state.dir = state.dir === \"asc\" ? \"desc\" : \"asc\";\n } else {\n state.sort = key;\n state.dir = \"desc\";\n }\n state.offset = 0;\n reload();\n };\n }\n}\n\n/**\n * Offset pager: \"a\u2013b of N\" with Prev/Next. Renders nothing when one page covers all.\n */\nexport function renderPager(\n el: Element,\n state: ListState,\n page: PageMeta | null | undefined,\n reload: () => void,\n): void {\n // Page by the limit the server actually clamped to (page.limit), not the requested one.\n const limit = page?.limit ?? state.limit;\n if (!page || page.total <= limit) {\n setHtml(el, html``);\n return;\n }\n const from = page.offset + 1;\n const to = Math.min(page.offset + limit, page.total);\n const hasPrev = page.offset > 0;\n const hasNext = page.offset + limit < page.total;\n setHtml(\n el,\n html`<div class=\"pager\"><button type=\"button\" class=\"pager-prev\"${hasPrev ? null : DISABLED}>\u2190 Prev</button><span class=\"pager-range muted\">${from}\u2013${to} of ${page.total}</span><button type=\"button\" class=\"pager-next\"${hasNext ? null : DISABLED}>Next \u2192</button></div>`,\n );\n if (hasPrev) {\n const prev = el.querySelector<HTMLButtonElement>(\".pager-prev\");\n if (prev) {\n prev.onclick = () => {\n state.offset = Math.max(0, page.offset - limit);\n reload();\n };\n }\n }\n if (hasNext) {\n const next = el.querySelector<HTMLButtonElement>(\".pager-next\");\n if (next) {\n next.onclick = () => {\n state.offset = page.offset + limit;\n reload();\n };\n }\n }\n}\n\n// A bare attribute is markup, not text: spelled once as markup so it can be interpolated.\nconst DISABLED = html` disabled`;\n", "// Small shared widgets, each independent of the others: the toast, the status badge, the\n// modal, the info tooltip, the row-action menu, the busy state on a button, and the error\n// view with its retry. The tooltip's document\n// listeners are wired by installTooltips() when boot calls it.\n\nimport { toasts } from \"../shell\";\nimport { type Html, html, setHtml } from \"./html\";\nimport { icon } from \"./icons\";\n\n// Long enough to read a sentence-length confirmation (\"Test sent to 2 addresses\").\nconst TOAST_MS = 3600;\n// Matches the .toast.leaving transition in styles.css, so removal trails the collapse.\nconst TOAST_LEAVE_MS = 300;\n// More than this at once is a wall, not a confirmation (DESIGN \u00A75).\nconst MAX_TOASTS = 3;\nconst toastTimers = new WeakMap<HTMLElement, number>();\n\n/**\n * A transient confirmation in the app-wide toast region (DESIGN \u00A75). The same message\n * already showing has its time restarted rather than a second copy stacked beneath it,\n * and past MAX_TOASTS the oldest leaves early, so a burst never builds a tower.\n */\nexport function toast(msg: string): void {\n const live = [...toasts.querySelectorAll<HTMLElement>(\".toast:not(.leaving)\")];\n const same = live.find((t) => t.textContent === msg);\n if (same) {\n lingerToast(same);\n return;\n }\n const t = document.createElement(\"div\");\n t.className = \"toast\";\n t.textContent = msg;\n toasts.appendChild(t);\n requestAnimationFrame(() => t.classList.add(\"show\"));\n lingerToast(t);\n for (const old of live.slice(0, live.length + 1 - MAX_TOASTS)) {\n leaveToast(old);\n }\n}\n\nfunction lingerToast(t: HTMLElement): void {\n clearTimeout(toastTimers.get(t));\n toastTimers.set(\n t,\n window.setTimeout(() => leaveToast(t), TOAST_MS),\n );\n}\n\n// Fade and collapse the toast's height together, so the ones beside it slide into its\n// place rather than jump. The height is pinned and committed first because a transition\n// can't start from auto.\nfunction leaveToast(t: HTMLElement): void {\n clearTimeout(toastTimers.get(t));\n t.style.height = `${t.offsetHeight}px`;\n void t.offsetHeight;\n t.classList.add(\"leaving\");\n t.style.height = \"0\";\n setTimeout(() => t.remove(), TOAST_LEAVE_MS);\n}\n\n/** A status pill; the status doubles as its class. */\nexport const badge = (status: string): Html => html`<span class=\"badge ${status}\">${status}</span>`;\n\nexport interface Modal {\n el: HTMLElement;\n close: () => void;\n /**\n * Say why the dialog's act didn't go through, inside the dialog: beneath `field` when one\n * field is at fault (marked invalid, focused, and cleared as soon as it is edited),\n * otherwise just above the actions, where the reader is looking; a toast at the foot of\n * the screen would be far from the field and gone while the dialog stays open. A second\n * call replaces the first.\n */\n error: (message: string, field?: HTMLInputElement | HTMLTextAreaElement) => void;\n /** Take the error away, as a new try begins. */\n clearError: () => void;\n}\n\nlet fieldErrorSeq = 0;\n// The clear function of the error each field shows now, so a second error replaces the first.\nconst fieldErrors = new WeakMap<HTMLElement, () => void>();\n\n/** The one look of a field or dialog error: an icon and the reason, announced as an alert. */\nfunction errorLine(message: string): HTMLElement {\n const line = document.createElement(\"div\");\n line.className = \"field-error\";\n line.id = `field-error-${++fieldErrorSeq}`;\n line.setAttribute(\"role\", \"alert\");\n const ico = document.createElement(\"span\");\n ico.className = \"field-error-ico\";\n ico.setAttribute(\"aria-hidden\", \"true\");\n ico.textContent = \"!\";\n const text = document.createElement(\"span\");\n text.textContent = message;\n line.append(ico, text);\n return line;\n}\n\n/**\n * Say why a field's value is refused, beneath the field (DESIGN \u00A75): the field is marked\n * invalid and described by the reason, and both go as soon as the field is edited.\n * Returns the function that takes the error away sooner; a second call on the same\n * field replaces the first.\n */\nexport function fieldError(\n field: HTMLInputElement | HTMLTextAreaElement,\n message: string,\n): () => void {\n fieldErrors.get(field)?.();\n const line = errorLine(message);\n const describedBy = field.getAttribute(\"aria-describedby\");\n field.after(line);\n field.setAttribute(\"aria-describedby\", describedBy ? `${describedBy} ${line.id}` : line.id);\n field.classList.add(\"is-invalid\");\n let cleared = false;\n const clear = () => {\n if (cleared) {\n return;\n }\n cleared = true;\n line.remove();\n field.classList.remove(\"is-invalid\");\n if (describedBy === null) {\n field.removeAttribute(\"aria-describedby\");\n } else {\n field.setAttribute(\"aria-describedby\", describedBy);\n }\n field.removeEventListener(\"input\", clear);\n fieldErrors.delete(field);\n };\n field.addEventListener(\"input\", clear);\n fieldErrors.set(field, clear);\n return clear;\n}\n\n/**\n * A modal over a backdrop; a click outside or Escape closes it. The content is markup.\n * The dialog belongs to the view that opened it: `signal` is that view's mount signal, and\n * when it aborts (a navigation, a remount) the dialog closes with it, so it never sits over\n * a page it does not belong to. Only the dialog goes: a write already started from it does\n * not take the signal (no write does) and still lands, its answer arriving after the\n * dialog is gone.\n */\nexport function modal(content: Html, signal: AbortSignal): Modal {\n const back = document.createElement(\"div\");\n back.className = \"modal-backdrop\";\n setHtml(back, html`<div class=\"modal\" role=\"dialog\" aria-modal=\"true\">${content}</div>`);\n const dialog = back.firstElementChild as HTMLElement;\n // Focus goes back where it was when the dialog closes, so a keyboard reader carries on\n // from the control that opened it rather than from the top of the page.\n const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n // Escape and Tab listen on the document and the end of the view on its signal, so\n // close() takes both listeners with it however the dialog closes (a button, the\n // backdrop, Escape, the view ending), and each lives exactly as long as the dialog.\n const onKey = (e: KeyboardEvent) => {\n // With dialogs stacked (a confirmation over the dialog that asked for it), only the top\n // one answers the keyboard: Escape closes it alone, and Tab stays inside it.\n if (back !== [...document.querySelectorAll(\".modal-backdrop\")].pop()) {\n return;\n }\n if (e.key === \"Escape\") {\n close();\n } else if (e.key === \"Tab\") {\n trapTab(dialog, e);\n }\n };\n // One error at a time: a field's (from fieldError) or the dialog's own line.\n let clearCurrent: (() => void) | null = null;\n const clearError = () => {\n clearCurrent?.();\n clearCurrent = null;\n };\n const error = (message: string, field?: HTMLInputElement | HTMLTextAreaElement) => {\n clearError();\n if (field) {\n clearCurrent = fieldError(field, message);\n field.focus();\n return;\n }\n const line = errorLine(message);\n line.classList.add(\"dialog-error\");\n const actions = dialog.querySelector(\".actions\");\n if (actions) {\n actions.before(line);\n } else {\n dialog.append(line);\n }\n clearCurrent = () => line.remove();\n };\n const close = () => {\n // A click on the backdrop has already dropped focus to the page body, so that counts as\n // focus lost with the dialog too.\n const active = document.activeElement;\n const hadFocus = back.contains(active) || !active || active === document.body;\n back.remove();\n document.removeEventListener(\"keydown\", onKey);\n signal.removeEventListener(\"abort\", close);\n if (hadFocus && opener?.isConnected) {\n opener.focus();\n }\n };\n if (signal.aborted) {\n // The view is already gone, so the dialog is never shown.\n return { el: back, close, error, clearError };\n }\n document.body.appendChild(back);\n back.addEventListener(\"click\", (e) => {\n if (e.target === back) {\n close();\n }\n });\n document.addEventListener(\"keydown\", onKey);\n signal.addEventListener(\"abort\", close, { once: true });\n // A caller that focuses a field of its own does so before this runs; otherwise focus\n // lands on the dialog's first control, so the keyboard starts inside it.\n queueMicrotask(() => {\n if (back.isConnected && !dialog.contains(document.activeElement)) {\n focusables(dialog)[0]?.focus();\n }\n });\n return { el: back, close, error, clearError };\n}\n\nconst FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\nfunction focusables(root: HTMLElement): HTMLElement[] {\n return [...root.querySelectorAll<HTMLElement>(FOCUSABLE)].filter((el) => !el.closest(\"[hidden]\"));\n}\n// Tab and Shift+Tab cycle within the dialog: aria-modal says the page behind is inert, so\n// focus must not walk out to it.\nfunction trapTab(dialog: HTMLElement, e: KeyboardEvent): void {\n const els = focusables(dialog);\n const first = els[0];\n const last = els[els.length - 1];\n if (!first || !last) {\n e.preventDefault();\n return;\n }\n const at = document.activeElement;\n if (!dialog.contains(at)) {\n e.preventDefault();\n first.focus();\n } else if (e.shiftKey && at === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && at === last) {\n e.preventDefault();\n first.focus();\n }\n}\n\n// Keep an info tooltip within the viewport. The tip is a CSS pseudo-element\n// anchored to the icon's left edge; pure CSS can't see the viewport, so before it\n// shows we measure the icon and, if the (width-capped) tip would run off the right\n// edge on a narrow screen, slide it left via --tip-x. Delegated so it survives view\n// re-renders; with JS off the tip falls back to left:0. Vertical placement stays in\n// CSS (the .tip-below variant) \u2014 which icons sit near the top is static, not dynamic.\nconst TIP_GUTTER = 8;\nfunction positionInfoTip(el: HTMLElement): void {\n // Measure the rendered tip (laid out even while hidden) so this stays in step\n // with the CSS max-width/padding rather than duplicating them here.\n const tip = getComputedStyle(el, \"::after\");\n const tipW = parseFloat(tip.width) + parseFloat(tip.paddingLeft) + parseFloat(tip.paddingRight);\n if (!Number.isFinite(tipW)) {\n return;\n }\n const iconLeft = el.getBoundingClientRect().left;\n const vw = document.documentElement.clientWidth;\n // Slide left enough to clear the right gutter, but never so far that the left\n // edge crosses the gutter (very narrow screens) and never rightward (shift \u2264 0).\n const shift = Math.min(0, Math.max(vw - TIP_GUTTER - tipW - iconLeft, TIP_GUTTER - iconLeft));\n el.style.setProperty(\"--tip-x\", `${Math.round(shift)}px`);\n}\n/** Wire the tooltip's positioning and dismissal, delegated on the document; boot calls this once. */\nexport function installTooltips(): void {\n // Position before the tip shows on either trigger: pointer hover, or focus \u2014 the\n // latter is how keyboard (Tab) and touch (tap focuses the span) reach it.\n for (const type of [\"pointerover\", \"focusin\"]) {\n document.addEventListener(type, (e) => {\n const el = e.target instanceof Element ? e.target.closest<HTMLElement>(\".info\") : null;\n if (el) {\n positionInfoTip(el);\n }\n });\n }\n // Escape dismisses a focus-shown tip without tabbing away (the pointer tip just\n // needs the mouse to leave).\n document.addEventListener(\"keydown\", (e) => {\n const active = document.activeElement;\n if (e.key === \"Escape\" && active instanceof HTMLElement && active.classList.contains(\"info\")) {\n active.blur();\n }\n });\n}\n\n/**\n * The \u24D8 affordance whose explanation shows as a tooltip. Focusable and role/aria-labelled\n * so it's reachable by keyboard and touch (the tip shows on :focus, not only :hover) and\n * read by screen readers \u2014 the aria-label mirrors the visible tip. `below` drops the tip\n * under the icon (for icons near the page top).\n */\nexport function infoTip(tip: string, { below = false }: { below?: boolean } = {}): Html {\n return html`<span class=\"info${below ? \" tip-below\" : \"\"}\" role=\"img\" tabindex=\"0\" aria-label=\"${tip}\" data-tip=\"${tip}\">${icon(\"info-filled\")}</span>`;\n}\n\nexport interface MenuItem {\n label: string;\n /** A line under the label saying what the pick does, for a menu whose items need it. */\n description?: string;\n danger?: boolean;\n onClick: () => void;\n}\n\n/** Where a menu sits under its trigger: `end` lines up right edges (a row's \u22EF), `start` left edges (a labeled button). */\nexport interface MenuOptions {\n align?: \"start\" | \"end\";\n}\n\n// popover menu, for row actions (\u22EF) and labeled menu buttons. A transparent full-screen overlay (behind\n// the menu) closes it on an outside click \u2014 no document-listener race.\nlet menuEls: HTMLElement[] = [];\nlet menuSignal: AbortSignal | null = null;\nlet menuAnchor: HTMLElement | null = null;\nfunction closeMenu(): void {\n const hadFocus = menuEls.some((e) => e.contains(document.activeElement));\n for (const e of menuEls) {\n e.remove();\n }\n menuEls = [];\n menuSignal?.removeEventListener(\"abort\", closeMenu);\n menuSignal = null;\n menuAnchor?.setAttribute(\"aria-expanded\", \"false\");\n // Back to the \u22EF that opened it, so the keyboard keeps its place in the list. A pick that\n // opens a dialog runs after this, and the dialog returns focus here when it closes.\n if (hadFocus && menuAnchor?.isConnected) {\n menuAnchor.focus();\n }\n menuAnchor = null;\n}\n/**\n * Open a menu under an anchor (a row's \u22EF or a labeled button); any outside click or a pick closes it. Like a\n * modal, it belongs to the view that opened it and closes when `signal`, that view's\n * mount signal, aborts.\n */\nexport function openMenu(\n anchor: Element,\n items: MenuItem[],\n signal: AbortSignal,\n { align = \"end\" }: MenuOptions = {},\n): void {\n closeMenu();\n if (signal.aborted) {\n return;\n }\n const overlay = document.createElement(\"div\");\n overlay.className = \"menu-overlay\";\n overlay.onclick = closeMenu;\n const m = document.createElement(\"div\");\n m.className = \"menu\";\n m.setAttribute(\"role\", \"menu\");\n for (const it of items) {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.setAttribute(\"role\", \"menuitem\");\n b.tabIndex = -1;\n b.className = `menu-item${it.danger ? \" danger-item\" : \"\"}`;\n if (it.description) {\n const name = document.createElement(\"span\");\n name.className = \"menu-item-name\";\n name.textContent = it.label;\n const desc = document.createElement(\"span\");\n desc.className = \"menu-item-desc\";\n desc.textContent = it.description;\n b.append(name, desc);\n m.classList.add(\"menu-described\");\n } else {\n b.textContent = it.label;\n }\n b.onclick = () => {\n closeMenu();\n it.onClick();\n };\n m.appendChild(b);\n }\n m.addEventListener(\"keydown\", (e) => menuKey(m, e));\n document.body.appendChild(overlay);\n document.body.appendChild(m);\n menuEls = [overlay, m];\n menuSignal = signal;\n menuAnchor = anchor instanceof HTMLElement ? anchor : null;\n menuAnchor?.setAttribute(\"aria-expanded\", \"true\");\n signal.addEventListener(\"abort\", closeMenu, { once: true });\n const r = anchor.getBoundingClientRect();\n m.style.top = `${r.bottom + window.scrollY + 4}px`;\n m.style.left = `${(align === \"start\" ? r.left : r.right - m.offsetWidth) + window.scrollX}px`;\n m.querySelector<HTMLElement>(\".menu-item\")?.focus();\n}\n// The menu keys: arrows, Home, and End move between items (wrapping), Escape closes it,\n// and Tab closes it and lets focus move on from the \u22EF, as a menu button does.\nfunction menuKey(m: HTMLElement, e: KeyboardEvent): void {\n const items = [...m.querySelectorAll<HTMLElement>(\".menu-item\")];\n const i = items.indexOf(document.activeElement as HTMLElement);\n const go = (n: number) => {\n e.preventDefault();\n items[(n + items.length) % items.length]?.focus();\n };\n if (e.key === \"ArrowDown\") {\n go(i + 1);\n } else if (e.key === \"ArrowUp\") {\n go(i - 1);\n } else if (e.key === \"Home\") {\n go(0);\n } else if (e.key === \"End\") {\n go(items.length - 1);\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n e.stopPropagation();\n closeMenu();\n } else if (e.key === \"Tab\") {\n closeMenu();\n }\n}\n\n/** Run an action with the button disabled and relabeled, restoring it after, if it is still in the page. */\nexport async function busy<T>(\n btn: HTMLButtonElement,\n label: string | null,\n fn: () => Promise<T>,\n): Promise<T> {\n const orig = btn.textContent;\n btn.disabled = true;\n if (label) {\n btn.textContent = label;\n }\n try {\n return await fn();\n } finally {\n if (btn.isConnected) {\n btn.disabled = false;\n btn.textContent = orig;\n }\n }\n}\n\n/** The error view with a retry, in place of what failed to load. */\nexport function renderError(container: Element, msg: string, retryFn: () => void): void {\n setHtml(\n container,\n html`<div class=\"error\"><span>${msg}</span><button class=\"ghost\" data-retry>Retry</button></div>`,\n );\n const b = container.querySelector<HTMLButtonElement>(\"[data-retry]\");\n if (b) {\n b.onclick = retryFn;\n }\n}\n", "// The drafts list (draft + scheduled), kept current with its posts' sends.\n\nimport type { PostListResponse, PostSavedResponse } from \"../../shared/posts\";\nimport { api, describeError } from \"../api\";\nimport { mount } from \"../lifecycle\";\nimport { followSends } from \"../send_state\";\nimport { $, $$ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { html, setHtml } from \"../ui/html\";\nimport {\n type ListState,\n listQuery,\n listToolbar,\n renderPager,\n th,\n wireSort,\n wireToolbar,\n} from \"../ui/list_controls\";\nimport { badge, busy, type MenuItem, modal, openMenu, renderError, toast } from \"../ui/widgets\";\n\n// The writing side: draft + scheduled only \u2014 a sent post is a frozen record and lives in\n// Sent. \"All statuses\" is scoped to those two, so it never reaches sent.\nconst DRAFT_STATUSES = [\n { value: \"draft\", label: \"Draft\" },\n { value: \"scheduled\", label: \"Scheduled\" },\n];\nconst DRAFTS_SCOPE = \"draft,scheduled\";\n\n/**\n * Create a draft and jump into the editor \u2014 shared by the Posts list, the Dashboard,\n * and the setup checklist so the \"New post\" affordance behaves identically everywhere.\n */\nexport function createNewPost(btn: HTMLButtonElement): Promise<void> {\n return busy(btn, \"Creating\u2026\", async () => {\n try {\n // An empty subject: the editor shows \"Untitled\" as the field's placeholder, so the\n // first thing typed is the whole subject, not an addition to a stand-in.\n const { post } = await api<PostSavedResponse>(\"/api/posts\", { method: \"POST\", json: {} });\n location.hash = `#/edit/${post.id}`;\n } catch (err) {\n toast(describeError(err));\n }\n });\n}\n\nexport async function renderDrafts(root: HTMLElement, signal: AbortSignal): Promise<void> {\n // Default sort left empty so the server keeps its scheduled-first order until the\n // reader clicks a column header.\n const state: ListState = {\n status: DRAFTS_SCOPE,\n search: \"\",\n sort: \"\",\n dir: \"desc\",\n offset: 0,\n limit: 50,\n };\n setHtml(\n root,\n html`<div class=\"spread page-head\"><h1>Drafts</h1><button class=\"primary\" id=\"newPost\">New post</button></div>\n ${listToolbar({ statuses: DRAFT_STATUSES, allValue: DRAFTS_SCOPE, searchPlaceholder: \"Search subject\u2026\" })}\n <div id=\"list\" class=\"muted\">Loading\u2026</div>\n <div id=\"postsPager\"></div>`,\n );\n const newPost = $<HTMLButtonElement>(\"#newPost\", root);\n newPost.onclick = () => createNewPost(newPost);\n const listEl = $(\"#list\", root);\n const pagerEl = $(\"#postsPager\", root);\n\n // The posts on screen and their sends' fire times, so a report of a send knows whether it\n // changes this page; and the active sends shown, for a report of one removed.\n let shownFire = new Map<string, number | null>();\n let shownSends = new Set<string>();\n // Loads can overlap (a send's change reloading while the reader sorts or pages), so only\n // the latest paints. A background reload that fails leaves the list the reader is\n // reading; only a load the reader started shows the error.\n let latest = 0;\n const reload = () => load(false);\n async function load(background: boolean): Promise<PostListResponse | null> {\n const mine = ++latest;\n try {\n const data = await api<PostListResponse>(`/api/posts?${listQuery(state)}`, { signal });\n if (mine !== latest) {\n return null;\n }\n const posts = data.posts;\n shownFire = new Map(posts.map((p) => [p.id, p.fire_at]));\n shownSends = new Set(posts.flatMap((p) => (p.active_send_id ? [p.active_send_id] : [])));\n if (!posts.length) {\n // \"Filtered\" = a real narrowing beyond the default drafts scope (a search, or a\n // single-status pick) \u2014 so a fresh, empty list still reads as an invitation.\n const filtered = state.search || (state.status && state.status !== DRAFTS_SCOPE);\n setHtml(\n listEl,\n html`<p class=\"muted\">${filtered ? \"No drafts match.\" : \"No drafts yet. Create your first draft.\"}</p>`,\n );\n setHtml(pagerEl, html``);\n return data;\n }\n // Cells are named (when / updated) and an empty Scheduled cell is marked, so the\n // \u2264720px layout can stack a row and label its dates from CSS alone.\n setHtml(\n listEl,\n html`<div class=\"table-wrap\"><table class=\"list-table posts-table stacks has-actions\"><colgroup><col><col class=\"c-status\"><col class=\"c-date\"><col class=\"c-date\"><col class=\"c-act\"></colgroup><thead><tr>${th(\"Title\", \"title\", state)}${th(\"Status\", null, state)}${th(\"Scheduled\", \"scheduled\", state)}${th(\"Updated\", \"updated\", state)}<th></th></tr></thead><tbody>${posts.map(\n (p) => {\n // A post whose send is in flight is no longer an editable/cancelable draft \u2014\n // show it as `sending` and route it to the live watch, not the editor.\n const sending = p.active_send_status === \"sending\";\n const href = sending ? `#/sent/${p.active_send_id}` : `#/edit/${p.id}`;\n return html`<tr class=\"clickable\" data-id=\"${p.id}\" data-target=\"${href}\"><td><a href=\"${href}\">${p.subject || html`<em>untitled</em>`}</a></td><td>${sending ? badge(\"sending\") : badge(p.status)}</td><td class=\"muted when${p.fire_at ? \"\" : \" empty\"}\">${p.fire_at ? fmt(p.fire_at) : \"\u2014\"}</td><td class=\"muted updated\">${fmt(p.updated_at)}</td><td class=\"act\"><button class=\"icon\" data-menu=\"${p.id}\" data-status=\"${sending ? \"sending\" : p.status}\" data-target=\"${href}\" aria-label=\"Post actions\" aria-haspopup=\"menu\">\u22EF</button></td></tr>`;\n },\n )}</tbody></table></div>`,\n );\n wireSort(listEl, state, reload);\n for (const tr of $$<HTMLTableRowElement>(\"tr[data-id]\", listEl)) {\n tr.onclick = (e) => {\n const t = e.target;\n if (\n t instanceof Element &&\n t.tagName !== \"A\" &&\n !t.closest(\"[data-menu]\") &&\n tr.dataset.target\n ) {\n location.hash = tr.dataset.target;\n }\n };\n }\n for (const b of $$<HTMLButtonElement>(\"[data-menu]\", listEl)) {\n b.onclick = (e) => {\n e.stopPropagation();\n const st = b.dataset.status;\n const target = b.dataset.target ?? \"\";\n const items: MenuItem[] = [\n {\n label: st === \"sending\" ? \"Watch send\" : \"Open\",\n onClick: () => {\n location.hash = target;\n },\n },\n ];\n if (st === \"draft\" && b.dataset.menu) {\n const id = b.dataset.menu;\n items.push({\n label: \"Delete draft\",\n danger: true,\n onClick: () => confirmDelete(id, reload, signal),\n });\n }\n openMenu(b, items, signal);\n };\n }\n renderPager(pagerEl, state, data.page, reload);\n return data;\n } catch (e) {\n if (!background && mine === latest) {\n // Until the list has first answered, Retry is that first read, which starts following.\n renderError(listEl, describeError(e), () => (following ? reload() : void firstLoad()));\n }\n return null;\n }\n }\n // Follow the listed posts' sends from the first read of the list that answers (DESIGN \u00A74):\n // a send that starts or is canceled, or moves, re-reads the list, whichever client made the\n // change, so a post that has gone out stops reading scheduled. A send's progress alone\n // (counts moving while it sends) changes nothing Drafts shows, so it reads nothing.\n let following = false;\n const follow = (first: PostListResponse) => {\n following = true;\n followSends(\n {\n cursor: first.cursor,\n sends: first.posts.flatMap((p) =>\n p.active_send_id && p.active_send_status\n ? [\n {\n id: p.active_send_id,\n status: p.active_send_status,\n phase: p.active_send_status === \"sending\" ? \"progressing\" : \"scheduled\",\n },\n ]\n : [],\n ),\n },\n {\n update({ sends, changes, removed }) {\n const touched =\n changes.some((c) => shownFire.has(c.send.post_id)) ||\n sends.some(\n (s) =>\n s.status === \"scheduled\" &&\n shownFire.has(s.post_id) &&\n shownFire.get(s.post_id) !== s.fire_at,\n ) ||\n removed.some((id) => shownSends.has(id));\n if (touched) {\n void load(true);\n }\n },\n stale: () => mount(renderDrafts),\n },\n signal,\n );\n };\n const firstLoad = async () => {\n const data = await load(false);\n if (data && !following) {\n follow(data);\n }\n };\n wireToolbar(root, state, reload);\n await firstLoad();\n}\n\nfunction confirmDelete(pid: string, reload: () => unknown, signal: AbortSignal): void {\n const m = modal(\n html`<h3>Delete draft?</h3><p class=\"hint\">This permanently deletes the draft and its revisions. This can\u2019t be undone.</p><div class=\"actions\"><button type=\"button\" id=\"dCancel\">Cancel</button><button type=\"button\" class=\"danger\" id=\"dGo\">Delete</button></div>`,\n signal,\n );\n const go = $<HTMLButtonElement>(\"#dGo\", m.el);\n $(\"#dCancel\", m.el).onclick = m.close;\n go.onclick = () =>\n busy(go, \"Deleting\u2026\", async () => {\n m.clearError();\n try {\n await api(`/api/posts/${pid}`, { method: \"DELETE\" });\n m.close();\n toast(\"Draft deleted\");\n reload();\n } catch (e) {\n m.error(describeError(e));\n }\n });\n}\n", "// What a send's view says, shared by the list, the record, and the dashboard: a page's sends\n// as they stand, its conditions and actions, who needs the operator, the Delivered cell,\n// the in-progress card, the scheduled cards' countdowns, and the small number formats.\n\nimport type {\n ConditionKind,\n SendAction,\n SendCondition,\n SendCounts,\n SendView,\n} from \"../../shared/sends\";\nimport { at, every } from \"../lifecycle\";\nimport { $$ } from \"../ui/dom\";\nimport { lateStr, untilStr } from \"../ui/format\";\nimport { type Html, html } from \"../ui/html\";\n\n/** A percentage clamped to 0\u2013100 and rounded. */\nexport const clampPct = (n: number | null | undefined): number =>\n Math.max(0, Math.min(100, Math.round(n || 0)));\n\n/** A rough duration, coarsening with size: \"45s\", \"12 min\", \"3 hr\"; an em dash for none. */\nexport function fmtDuration(ms: number | null | undefined): string {\n if (ms == null || !Number.isFinite(ms) || ms <= 0) {\n return \"\u2014\";\n }\n const s = Math.round(ms / 1000);\n if (s < 90) {\n return `${s}s`;\n }\n const m = Math.round(s / 60);\n if (m < 90) {\n return `${m} min`;\n }\n return `${Math.round(m / 60)} hr`;\n}\n\n/**\n * A scheduled card's countdown cell, from its `GET /api/sends` row: the fire time, whether the\n * server reads the send due, and whether it is past the missed tolerance. Empty until\n * `countdowns` words it.\n */\nexport function countdownHtml(s: SendView): Html {\n return html`<span class=\"countdown\" data-fire=\"${s.fire_at}\"${s.phase === \"due\" && html` data-due`}${has(s, \"missed\") && html` data-missed`}></span>`;\n}\n\n/**\n * The live countdown cells under `root`, ticking once a second for the life of the mount:\n * \"Sends in \u2026\" before the fire time; \"Preparing to send\u2026\" from the fire time (by the clock,\n * or sooner when the server already reads the send due) until the send starts and its card\n * leaves the queue; and, past the server's missed tolerance, how late it is, in the danger\n * tone. A card's window controls (`data-closes`, Cancel and Reschedule) hide at the fire\n * time, when the review window closes (SPEC \u00A76) and the server would refuse them, rather\n * than wait for the next read. A fire time inside the coming second gets a tick of its own\n * at that moment, so the switch lands when the countdown runs out, not up to a second\n * after. Controls hidden this way stay hidden: a card is re-rendered only from a read, whose\n * `actions` decide what it offers. Returns the tick, for a section that re-renders its\n * cards: a fresh cell is empty until the next tick paints it.\n */\nexport function countdowns(root: ParentNode, signal: AbortSignal): () => void {\n let armed = 0; // the moment an extra tick is set for, so a repeated tick sets it once\n const tick = () => {\n const now = Date.now();\n let next = Number.POSITIVE_INFINITY;\n const watch = (moment: number) => {\n if (moment > now && moment < next) {\n next = moment;\n }\n };\n for (const el of $$<HTMLElement>(\"[data-closes]\", root)) {\n const closes = Number(el.dataset.closes);\n el.hidden = now >= closes;\n watch(closes);\n }\n for (const el of $$<HTMLElement>(\"[data-fire]\", root)) {\n const fire = Number(el.dataset.fire);\n const missed = el.dataset.missed !== undefined;\n el.textContent = missed ? lateStr(fire) : untilStr(fire, el.dataset.due !== undefined);\n el.classList.toggle(\"countdown-missed\", missed);\n watch(fire);\n }\n if (next - now <= 1000 && next !== armed) {\n armed = next;\n at(next, tick, signal);\n }\n };\n every(1000, tick, signal);\n return tick;\n}\n\n/** Whether the server reports an open condition of `kind` on the send. */\nexport const has = (s: { conditions: SendCondition[] }, kind: ConditionKind): boolean =>\n s.conditions.some((c) => c.kind === kind);\n\n/** The send's open condition of `kind`, as the server words it, if any. */\nexport function conditionOf<K extends ConditionKind>(\n s: { conditions: SendCondition[] },\n kind: K,\n): Extract<SendCondition, { kind: K }> | undefined {\n return s.conditions.find((c) => c.kind === kind) as\n | Extract<SendCondition, { kind: K }>\n | undefined;\n}\n\n/** Whether the server would take the action on the send now: a control is offered only then. */\nexport const can = (s: { actions: SendAction[] }, name: SendAction[\"name\"]): boolean =>\n s.actions.some((a) => a.name === name);\n\n/**\n * A page's sends as they stand: each row it read, replaced by the layer's latest report of\n * that send once there is one, then every reported send it did not read. The layer reports\n * every change after the page's read, so a report is never older than the row it replaces;\n * both are the one `SendView`, so nothing is converted.\n */\nexport function sendsNow(\n rows: readonly SendView[],\n reported: ReadonlyMap<string, SendView>,\n): SendView[] {\n const listed = new Set(rows.map((r) => r.id));\n return [\n ...rows.map((r) => reported.get(r.id) ?? r),\n ...[...reported.values()].filter((s) => !listed.has(s.id)),\n ];\n}\n\n/**\n * A send that needs the operator rather than patience (SPEC \u00A712): wedged on ambiguous\n * deliveries, which only Resolve can settle without risking a double-mail (I4), or refused\n * by the provider, which only a fix to the account lifts. Its home is the attention block,\n * not the in-progress cards. The server decides both conditions, so every page agrees.\n */\nexport function needsOperator(s: SendView): boolean {\n return has(s, \"wedged\") || has(s, \"refused\");\n}\n\n/** The counts a Delivered cell reads. */\nexport type DeliveredCounts = Pick<SendCounts, \"delivered\" | \"bounced\" | \"complained\" | \"unsent\">;\n\n/**\n * A send's \"Delivered\" cell in the send lists, from its counts: a list row's, or a live send's as its receipts settle. It reports TRUE delivered\n * (webhook-confirmed, not provider-accepted), so the Sent list and the dashboard's Sent\n * table agree with the record view's \"Delivered\" for the same send, and a bounced or\n * complained recipient is never miscounted as delivered. Any bounce, complaint, or unsent\n * shows as a muted delivery-failure note beneath the count, worst first, so a bad send\n * reads as one at a glance. A clean send prints nothing.\n */\nexport function deliveredCell(c: DeliveredCounts): Html {\n const delivered = c.delivered || 0;\n const kinds: string[] = [];\n if (c.complained) {\n kinds.push(`${c.complained.toLocaleString()} complained`);\n }\n if (c.bounced) {\n kinds.push(`${c.bounced.toLocaleString()} bounced`);\n }\n if (c.unsent) {\n kinds.push(`${c.unsent.toLocaleString()} unsent`);\n }\n // Each kind is one unbreakable unit, so a wrap lands between kinds, never inside one.\n const note = kinds.length\n ? html`<span class=\"muted delivered-note\">${kinds.map((text, i) => html`${i ? \", \" : \"\"}<span class=\"delivered-kind\">${text}</span>`)}</span>`\n : null;\n return html`<span class=\"n\">${delivered.toLocaleString()}</span>${note}`;\n}\n\n/**\n * One in-progress send as a card with a live mini dispatch bar, its counts, and a Watch\n * link, from the layer's read. The bar is accepted over the audience, as the watch's\n * dispatch bar is, and the time to finish is the server's, shown only while the send is\n * handing off, never while it is paused. A send in flight too long says so on its card.\n * The whole card opens the watch; the \"Watch\" link is the keyboard/middle-click target.\n */\nexport function activeRowHtml(s: SendView): Html {\n const c = s.counts;\n const accepted = c.accepted + c.delivered + c.bounced + c.complained;\n const confirmed = c.delivered + c.bounced + c.complained;\n const total = s.audience.count;\n const pct = total > 0 ? (100 * accepted) / total : 0;\n // The server gives a time to finish only while the send is handing off.\n const eta = s.dispatch.eta_ms ? ` \u00B7 ~${fmtDuration(s.dispatch.eta_ms)} left` : \"\";\n const stuck = conditionOf(s, \"stuck\");\n return html`<div class=\"card spread clickable active-card\" data-watch=\"${s.id}\">\n <div class=\"active-main\">\n <a class=\"card-link active-subj\" href=\"#/sent/${s.id}\">${s.subject || html`<em>untitled</em>`}</a>\n <div class=\"active-bar\"><div class=\"active-fill\" style=\"width:${clampPct(pct)}%\"></div></div>\n <div class=\"muted active-stat\">Sending: ${accepted.toLocaleString()} of ${total.toLocaleString()} accepted${\n confirmed ? ` \u00B7 ${confirmed.toLocaleString()} confirmed` : \"\"\n }${eta}</div>\n ${stuck ? html`<div class=\"active-stuck\">${stuck.message}</div>` : null}\n </div>\n <a class=\"ghost-link\" href=\"#/sent/${s.id}\">Watch \u2192</a>\n </div>`;\n}\n", "// The subscriber actions taken from more than one surface: adding one (the list, the\n// dashboard's quick actions) and unsubscribing one (the list's row menu, the dashboard).\n\nimport { isValidEmail, normalizeEmail } from \"../../shared/email\";\nimport type { SubscribeAction, SubscribeResponse, Subscriber } from \"../../shared/subscribers\";\nimport { ApiError, api, describeError } from \"../api\";\nimport { notConnectedAddLine, providerNotConnected } from \"../deployment\";\nimport { $ } from \"../ui/dom\";\nimport { html } from \"../ui/html\";\nimport { busy, modal, toast } from \"../ui/widgets\";\n\n/**\n * Add subscriber \u2192 the normal double opt-in (never an auto-confirm). While no provider is\n * connected the confirmation can't go and the server would refuse, so the dialog says so\n * before the publisher types anything: the standing line, no address field, its act\n * disabled (DESIGN \u00A74).\n */\nexport function addSubscriberModal(onDone: () => void, signal: AbortSignal): void {\n const notConnected = providerNotConnected();\n const m = modal(\n html`<h3>Add subscriber</h3><p class=\"hint\">Starts the normal double opt-in: they get a confirmation email and won\u2019t receive posts until they confirm.</p>${notConnectedAddLine()}${notConnected ? null : html`<label for=\"addEmail\">Email address</label><input type=\"email\" id=\"addEmail\" placeholder=\"person@example.com\">`}<div class=\"actions\"><button type=\"button\" id=\"aCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"aGo\">Send confirmation</button></div>`,\n signal,\n );\n const go = $<HTMLButtonElement>(\"#aGo\", m.el);\n $(\"#aCancel\", m.el).onclick = m.close;\n go.disabled = notConnected;\n if (notConnected) {\n $(\"#aCancel\", m.el).focus();\n return;\n }\n const input = $<HTMLInputElement>(\"#addEmail\", m.el);\n input.focus();\n go.onclick = () =>\n busy(go, \"Adding\u2026\", async () => {\n m.clearError();\n const addr = normalizeEmail(input.value);\n if (!isValidEmail(addr)) {\n m.error(\"Enter a valid email address\", input);\n return;\n }\n try {\n const r = await api<SubscribeResponse>(\"/api/subscribers\", {\n method: \"POST\",\n json: { email: addr },\n });\n m.close();\n toast(addedMessage(addr, r.action));\n onDone();\n } catch (e) {\n // A refusal about the address goes under it; anything else, above the buttons.\n m.error(describeError(e), e instanceof ApiError && e.field === \"email\" ? input : undefined);\n }\n });\n}\n\n/** What the Add told the admin: a confirmation went, or why none was due. A refused\n * confirmation is an API error, shown by the caller as it is. */\nfunction addedMessage(addr: string, action: SubscribeAction): string {\n switch (action) {\n case \"already_confirmed\":\n return `${addr} is already confirmed`;\n case \"suppressed\":\n return `${addr} is suppressed, so no confirmation was sent`;\n case \"recently_sent\":\n return `A confirmation went to ${addr} a few minutes ago; try again later`;\n default:\n return `Confirmation sent to ${addr}`;\n }\n}\n\nexport function confirmUnsubscribe(sub: Subscriber, onDone: () => void, signal: AbortSignal): void {\n const m = modal(\n html`<h3>Unsubscribe this subscriber?</h3><p class=\"hint\">Removes <strong>${sub.email}</strong> from the send audience immediately. They can re-subscribe later through the double opt-in.</p><div class=\"actions\"><button type=\"button\" id=\"uCancel\">Cancel</button><button type=\"button\" class=\"danger\" id=\"uGo\">Unsubscribe</button></div>`,\n signal,\n );\n const go = $<HTMLButtonElement>(\"#uGo\", m.el);\n $(\"#uCancel\", m.el).onclick = m.close;\n go.onclick = () =>\n busy(go, \"Unsubscribing\u2026\", async () => {\n m.clearError();\n try {\n await api(`/api/subscribers/${sub.id}/unsubscribe`, { method: \"POST\" });\n m.close();\n toast(`Unsubscribed ${sub.email}`);\n onDone();\n } catch (e) {\n m.error(describeError(e));\n }\n });\n}\n", "// Copying text out of the app, one way everywhere (DESIGN \u00A77): a quiet Copy and its glyph,\n// which says it worked where it sits, the glyph turning to a check for a moment while the\n// word stays, never with a toast. A code block wears it on its label line (`codeBlock`);\n// a single value (an address) wears it beside the value; either copies the plain text.\n\nimport { type Html, html, setHtml } from \"./html\";\nimport { icon } from \"./icons\";\nimport { toast } from \"./widgets\";\n\nconst COPIED_MS = 1500;\n\n/** The copy control's face: its word, then its glyph, a check once a copy has landed. A\n * button whose word says what it copies (\"Copy HTML\") carries it as `data-copy-word`, so\n * the face it returns to after the check says it again. */\nexport function copyFace(copied = false, word = \"Copy\"): Html {\n return html`<span>${word}</span>${icon(copied ? \"check\" : \"copyout\")}`;\n}\n\n/** A Copy that puts `text` on the clipboard when clicked. `label` names what it copies,\n * for a screen reader (\"Copy curl\"). */\nexport function copyButton(text: string, label: string): Html {\n return html`<button type=\"button\" class=\"copy-btn\" data-copy=\"${text}\" aria-label=\"${label}\">${copyFace()}</button>`;\n}\n\n/** A labelled code block with a Copy of its plain text on the label line. `code` is the\n * block's markup (highlighted, say); `raw` is what the clipboard gets. */\nexport function codeBlock(label: string, code: Html, raw: string): Html {\n return html`<div class=\"block\"><div class=\"block-head\"><span class=\"block-label\">${label}</span>${copyButton(raw, `Copy ${label}`)}</div><pre><code>${code}</code></pre></div>`;\n}\n\nconst pending = new WeakMap<HTMLElement, { label: string; timer: number }>();\n\n/** Copy `text` and say so on `button`: the check, and \"Copied\" as its accessible name,\n * for a moment; a second copy restarts the moment. A clipboard the browser refuses is\n * the one thing said elsewhere, since the button can't show it. */\nexport async function copyWith(button: HTMLElement, text: string): Promise<void> {\n try {\n await navigator.clipboard.writeText(text);\n } catch {\n toast(\"Couldn\u2019t copy to clipboard\");\n return;\n }\n const prior = pending.get(button);\n const label = prior?.label ?? button.getAttribute(\"aria-label\") ?? \"Copy\";\n const word = button.dataset.copyWord;\n window.clearTimeout(prior?.timer);\n setHtml(button, copyFace(true, word));\n button.setAttribute(\"aria-label\", \"Copied\");\n button.classList.add(\"copied\");\n const timer = window.setTimeout(() => {\n setHtml(button, copyFace(false, word));\n button.setAttribute(\"aria-label\", label);\n button.classList.remove(\"copied\");\n pending.delete(button);\n }, COPIED_MS);\n pending.set(button, { label, timer });\n}\n\n/** Every Copy under `root` (a `[data-copy]` button) copies its text when clicked: one\n * listener on the root, so a block painted later is covered too. */\nexport function wireCopy(root: HTMLElement): void {\n root.addEventListener(\"click\", (ev) => {\n const b = ev.target instanceof Element ? ev.target.closest<HTMLElement>(\"[data-copy]\") : null;\n if (b && root.contains(b)) {\n void copyWith(b, b.dataset.copy ?? \"\");\n }\n });\n}\n", "// The dashboard (home): counts, the active send widget, drafts and sent tables, the\n// setup checklist, and quick actions.\n\nimport type { PostListItem, PostListResponse } from \"../../shared/posts\";\nimport type { SendListResponse, SendView } from \"../../shared/sends\";\nimport type { DeploymentView } from \"../../shared/settings\";\nimport type { SubscriberCounts, SubscriberListResponse } from \"../../shared/subscribers\";\nimport { api, describeError } from \"../api\";\nimport { derivePublication, type Publication } from \"../brand\";\nimport {\n CONNECT_PROVIDER_HREF,\n connectProviderLink,\n heldSendsHtml,\n heldWhenFiresHtml,\n providerNotConnected,\n} from \"../deployment\";\nimport { mount } from \"../lifecycle\";\nimport { createNewPost } from \"../posts/drafts\";\nimport { followSends, type SendStage, type SendsUpdate, stageOf } from \"../send_state\";\nimport {\n activeRowHtml,\n conditionOf,\n countdownHtml,\n countdowns,\n deliveredCell,\n has,\n needsOperator,\n sendsNow,\n} from \"../sends/progress\";\nimport { appState } from \"../state\";\nimport { addSubscriberModal } from \"../subscribers/dialogs\";\nimport { copyButton, copyFace, wireCopy } from \"../ui/copy\";\nimport { $, $$ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { badge, renderError } from \"../ui/widgets\";\n\n// The post-login landing and the brand's target (the default route). Built entirely\n// from existing authed endpoints \u2014 GET /api/posts, /sends, /subscribers, and the cached\n// /api/settings \u2014 so it adds no surface and can't touch an invariant. It answers\n// SPEC \u00A78's questions at a glance: is anything wrong, who's on the list, what's\n// scheduled, what went out, and what's still in progress.\n\n/** One line of the health block: red needs a decision, amber a look. */\ninterface HealthAlert {\n level: \"red\" | \"amber\";\n text: string | Html;\n}\n\n// Health (SPEC \u00A78 \"is anything wrong\", \u00A712 loud failure): calm in the common case, loud\n// only when something needs attention. Every line is a condition the server reports on a\n// send as it stands (the page's rows, overlaid by the layer's reports), so a line appears\n// within a read of its condition and stays until the condition clears, and the dashboard\n// keeps no rule of its own: the bounce spike included, which the server reads off each\n// recently sent send. Only the loud kinds are lines here; a re-made send's note and a\n// provider outage short of the stuck threshold are shown where the send is. The one line\n// not about a send is the deployment's: no email provider connected (SPEC \u00A79), an amber\n// standing state with no time, read from the deployment reflection, gone once one is set.\n// When that state is holding a send, the two are said as one red line instead of two.\nfunction computeHealth(live: SendView[]): HealthAlert[] {\n const alerts: HealthAlert[] = [];\n // Missed only past the server's tolerance: an ordinary slow tick is never a miss (\u00A712).\n const missed = live.filter((s) => has(s, \"missed\"));\n if (missed.length) {\n alerts.push({\n level: \"red\",\n text: `${missed.length} scheduled send${missed.length === 1 ? \"\" : \"s\"} passed the fire time without going out.`,\n });\n }\n // A send wedged on ambiguous in-flight rows needs a decision, not just patience: red,\n // linking to where Resolve is (its page; the Sent page for several), and kept out of the\n // in-progress lines below so it isn't reported twice (SPEC \u00A712).\n const wedged = live.flatMap((s) => {\n const c = conditionOf(s, \"wedged\");\n return c ? [{ s, count: c.count }] : [];\n });\n const [firstWedged] = wedged;\n if (firstWedged) {\n const n = wedged.reduce((sum, w) => sum + w.count, 0);\n const noun = n === 1 ? \"delivery\" : \"deliveries\";\n alerts.push({\n level: \"red\",\n text:\n wedged.length === 1\n ? html`<a href=\"#/sent/${firstWedged.s.id}\">${firstWedged.s.subject}</a> has ${n} ambiguous ${noun} awaiting a decision; resolve ${n === 1 ? \"it\" : \"them\"} on its page.`\n : html`${n} ambiguous ${noun} across <a href=\"#/sent\">${wedged.length} sends</a> await a decision; resolve them on the Sent page.`,\n });\n }\n // The provider refusing the account stops every send it touches until the operator\n // fixes the account (SPEC \u00A712): one red line, in the server's words for it.\n // A send held for want of a provider (SPEC \u00A79) is the deployment's state showing on a\n // send, so while the deployment is not connected the two are one line: the state, the\n // held sends, and the fix once, as a link. Red, since a send is waiting on the operator.\n const notConnected = providerNotConnected();\n const allRefused = live.flatMap((s) => {\n const c = conditionOf(s, \"refused\");\n return c ? [{ s, c }] : [];\n });\n const held = notConnected ? allRefused.filter((r) => r.c.cause === \"not_connected\") : [];\n const refused = allRefused.filter((r) => !held.includes(r));\n // One send links to its watch; several, to the Sent page that lists them all.\n const which = (sends: { s: SendView }[]) =>\n sends.length === 1 && sends[0]\n ? html`<a href=\"#/sent/${sends[0].s.id}\">${sends[0].s.subject}</a>`\n : html`<a href=\"#/sent\">${sends.length} sends</a>`;\n if (held.length) {\n alerts.push({\n level: \"red\",\n text: html`<span class=\"dash-not-connected\">${heldSendsHtml(which(held), held.length)}</span>`,\n });\n }\n const [firstRefused] = refused;\n if (firstRefused) {\n alerts.push({ level: \"red\", text: html`${which(refused)}: ${firstRefused.c.message}` });\n }\n // A healthy in-progress send is NOT surfaced here: the active-send widget below is its\n // home (a bar and a Watch link). The health line is loud-only, so it keeps just the\n // *stuck* case: a send the server reports in flight too long.\n const stuck = live.filter((s) => has(s, \"stuck\") && !needsOperator(s));\n const [firstStuck] = stuck;\n if (firstStuck) {\n alerts.push({\n level: \"amber\",\n text:\n stuck.length === 1\n ? html`<a href=\"#/sent/${firstStuck.id}\">${firstStuck.subject}</a>: ${conditionOf(firstStuck, \"stuck\")?.message}`\n : html`<a href=\"#/sent\">${stuck.length} sends</a> have been sending longer than they should; they may be retrying.`,\n });\n }\n // Bounce spike (SPEC \u00A78): the server's reading of a recent send's confirmed bounces over\n // its audience at fire. Read-only reporting: it never throttles or halts a send.\n const spiky = live.flatMap((s) => {\n const c = conditionOf(s, \"bounce_spike\");\n return c ? [{ s, c }] : [];\n });\n const [firstSpike] = spiky;\n if (firstSpike) {\n alerts.push({\n level: \"amber\",\n text: html`<a href=\"#/sent/${firstSpike.s.id}\">${firstSpike.s.subject}</a>: ${firstSpike.c.message}`,\n });\n }\n if (notConnected && !held.length) {\n alerts.push({\n level: \"amber\",\n text: html`<span class=\"dash-not-connected\">No email provider connected, so nothing can be sent yet. ${connectProviderLink()} to start sending.</span>`,\n });\n }\n return alerts;\n}\n\n/** The health block: nothing at all when nothing needs attention. */\nfunction healthHtml(alerts: HealthAlert[]): Html {\n if (!alerts.length) {\n return html``;\n }\n const level = alerts.some((i) => i.level === \"red\") ? \"red\" : \"amber\";\n return html`<div class=\"health ${level}\"><span class=\"health-dot\">\u26A0\uFE0F</span><div>${alerts.map(\n (i) => html`<div>${i.text}</div>`,\n )}</div></div>`;\n}\n\n/** The sends the active-send widget shows: in flight, and not needing the operator (that\n * one's home is its red line). */\nconst activeOf = (live: SendView[]) =>\n live.filter((s) => s.status === \"sending\" && !needsOperator(s));\n\n/** A subscriber-count tile; each deep-links into the roster on its own filter. */\ninterface Tile {\n label: string;\n emph?: boolean;\n v: number;\n filter: string;\n sub?: string;\n}\n\nexport async function renderDashboard(view: HTMLElement, signal: AbortSignal): Promise<void> {\n const remount = () => {\n if (!signal.aborted) {\n mount(renderDashboard); // never over wherever the reader went since\n }\n };\n setHtml(view, html`<div class=\"dash\" id=\"dash\"><p class=\"muted\">Loading\u2026</p></div>`);\n const root = $(\"#dash\", view);\n let posts: PostListItem[];\n let first: SendListResponse;\n let counts: SubscriberCounts;\n try {\n // The health line scans every send and the archive-link slug map needs every post,\n // so ask for a full window rather than the list default (50). Subscribers is only\n // read for its (filter-independent) counts, so its row limit doesn't matter.\n const [p, s, subs] = await Promise.all([\n api<PostListResponse>(\"/api/posts?limit=200\", { signal }),\n api<SendListResponse>(\"/api/sends?limit=200\", { signal }),\n api<SubscriberListResponse>(\"/api/subscribers\", { signal }),\n ]);\n posts = p.posts;\n first = s;\n counts = subs.counts;\n } catch (e) {\n renderError(root, describeError(e), remount);\n return;\n }\n let sends = first.sends;\n // Every send the layer has reported since the page's read, as it now stands.\n const reported = new Map<string, SendView>();\n const pub = derivePublication(appState.appConfig);\n const deployment = appState.appConfig?.deployment ?? null;\n const totalSubs = counts.confirmed + counts.pending + counts.unsubscribed + counts.suppressed;\n\n // First run \u2014 nothing written and no one on the list: replace the body with the\n // onboarding checklist (the shared Getting-started component) rather than a wall\n // of empty tiles.\n if (!posts.length && totalSubs === 0) {\n setHtml(\n root,\n html`<div class=\"dash-head\"><div><h1>${pub.name}</h1>${\n pub.tagline ? html`<p class=\"muted dash-tagline\">${pub.tagline}</p>` : null\n }<p class=\"muted\">Let\u2019s get your first post out the door.</p></div></div>${setupChecklistHtml(pub, deployment)}<section class=\"dash-section\"><h2>API access</h2>${apiConnectCard(false)}</section>`,\n );\n wireDashActions(root, remount, signal);\n return;\n }\n\n // Each tile deep-links into the roster pre-filtered on its criterion\n // (#/subscribers/<filter>), so a count is a way in, not just a number.\n const tiles: Tile[] = [\n {\n label: \"Confirmed\",\n emph: true,\n v: counts.confirmed,\n filter: \"confirmed\",\n },\n { label: \"Pending\", v: counts.pending, filter: \"pending\" },\n { label: \"Unsubscribed\", v: counts.unsubscribed, filter: \"unsubscribed\" },\n { label: \"Suppressed\", v: counts.suppressed, filter: \"suppressed\" },\n ];\n const tilesHtml = html`<div class=\"tiles\">${tiles.map(\n (t) =>\n html`<a class=\"tile${t.emph ? \" tile-emph\" : \"\"}\" href=\"#/subscribers/${t.filter}\"><span class=\"tile-n\">${t.v}</span><span class=\"tile-label\">${t.label}${\n t.sub ? html`<span class=\"tile-sub\">${t.sub}</span>` : null\n }</span></a>`,\n )}</div>`;\n\n const drafts = posts.filter((p) => p.status === \"draft\").slice(0, 5);\n const draftsHtml = drafts.length\n ? html`<div class=\"table-wrap\"><table><tbody>${drafts.map(\n (p) =>\n html`<tr class=\"clickable\" data-id=\"${p.id}\"><td><a href=\"#/edit/${p.id}\">${p.subject || html`<em>untitled</em>`}</a></td><td class=\"muted\">edited ${fmt(p.updated_at)}</td></tr>`,\n )}</tbody></table></div>`\n : html`<p class=\"muted\">No drafts in progress.</p>`;\n\n // The publication's home is the landing page at the app's own origin (SPEC \u00A711); the\n // archive and every post are a click from it, so the card names the one address to share.\n const homeUrl = deployment?.appOrigin || location.origin;\n const pubCardHtml = html`<div class=\"card pub-card\">\n <div class=\"pub-row\"><code class=\"pub-val\">${homeUrl}</code>${copyButton(homeUrl, \"Copy the publication\u2019s address\")}</div>\n <div class=\"pub-foot\"><a href=\"/\" target=\"_blank\" rel=\"noopener\">View publication \u2197</a></div>\n </div>`;\n\n // Has Claude (the `service` principal) edited here? Authorship on any current revision\n // flips the API-access card to \"connected\" (SPEC \u00A74). Uses the author already on each\n // list row, so no extra fetch; a post a human later re-edited no longer counts.\n const claudeConnected = posts.some((p) => p.author === \"service\");\n const apiCardHtml = apiConnectCard(claudeConnected);\n\n const quickHtml = html`<div class=\"row quick-actions\"><button data-act=\"new-post\">New post</button><button data-act=\"add-sub\">Add subscriber</button><button data-nav=\"#/settings\">Edit publication</button></div>`;\n\n // No news is good news: the health block appears only when something needs attention\n // (SPEC \u00A78 / \u00A712, the only thing that ever surfaces loudly). The active-send widget sits\n // with it, a glanceable entry into the watch while a send is in flight.\n setHtml(\n root,\n html`\n <div class=\"dash-head\">\n <div><h1>${pub.name}</h1>${pub.tagline ? html`<p class=\"muted dash-tagline\">${pub.tagline}</p>` : null}</div>\n <button class=\"primary\" data-act=\"new-post\">New post</button>\n </div>\n <div id=\"dashHealth\">${healthHtml(computeHealth(sendsNow(sends, reported)))}</div>\n <div id=\"dashActive\">${dashActiveHtml(activeOf(sendsNow(sends, reported)))}</div>\n <section class=\"dash-section\"><h2>Subscribers</h2>${tilesHtml}</section>\n <div class=\"dash-cols\">\n <section class=\"dash-section\"><h2>Scheduled</h2><div id=\"dashScheduled\">${dashScheduledHtml(scheduledOf(sends))}</div></section>\n <section class=\"dash-section\"><h2>Drafts</h2>${draftsHtml}</section>\n </div>\n <section class=\"dash-section\"><h2>Sent</h2><div id=\"dashSent\">${dashSentHtml(sends)}</div></section>\n <section class=\"dash-section\"><h2>Quick actions</h2>${quickHtml}</section>\n <div class=\"dash-cols\">\n <section class=\"dash-section\"><h2>Publication</h2>${pubCardHtml}</section>\n <section class=\"dash-section\"><h2>API access</h2>${apiCardHtml}</section>\n </div>`,\n );\n\n wireDashActions(root, remount, signal);\n // Row / card clicks open the post (subject links + Cancel opt out \u2014 the same guard\n // the Posts table and the Sends cards use).\n for (const tr of $$<HTMLTableRowElement>(\"tr[data-id]\", root)) {\n tr.onclick = (e) => {\n const t = e.target;\n if (t instanceof Element && t.tagName !== \"A\" && !t.closest(\"button\")) {\n location.hash = `#/edit/${tr.dataset.id}`;\n }\n };\n }\n wireDashSentRows(root);\n wireDashActiveCards(root);\n wireDashScheduledCards(root);\n const tickCountdowns = countdowns(root, signal);\n tickCountdowns();\n\n // From here the send-state layer keeps the send sections current (docs/DESIGN.md \u00A74),\n // following every send from where the page's own read stood. Each report repaints what it\n // carries directly (the health block, the active-send widget, a listed send's Delivered).\n // A stage change, or any change to a scheduled or due send (a cancel, a move, a re-make,\n // a new schedule, the fire time passing, a miss), reads the sends again for the queue,\n // and the Sent table, whoever made it: a send that went due to sent between\n // two reads lands straight in the Sent table, and the other client's move shows within\n // one idle read.\n let reading = 0;\n const readSends = async () => {\n const mine = ++reading;\n try {\n const fresh = await api<SendListResponse>(\"/api/sends?limit=200\", { signal });\n if (mine !== reading) {\n return; // a later change's read is the one to paint\n }\n sends = fresh.sends;\n setHtml($(\"#dashScheduled\", root), dashScheduledHtml(scheduledOf(sends)));\n wireDashScheduledCards(root);\n tickCountdowns(); // the fresh cards are empty until the next tick\n setHtml($(\"#dashSent\", root), dashSentHtml(sends));\n wireDashSentRows(root);\n paintLive();\n } catch {\n /* a background read: the sections keep their last render until the next change */\n }\n };\n const paintLive = () => {\n const now = sendsNow(sends, reported);\n setHtml($(\"#dashHealth\", root), healthHtml(computeHealth(now)));\n setHtml($(\"#dashActive\", root), dashActiveHtml(activeOf(now)));\n wireDashActiveCards(root);\n patchDelivered(root, [...reported.values()]);\n };\n const inQueue = (st: SendStage | null) => st === \"scheduled\" || st === \"due\";\n followSends(\n first,\n {\n update: (u: SendsUpdate) => {\n for (const s of u.sends) {\n reported.set(s.id, s);\n }\n for (const id of u.removed) {\n reported.delete(id);\n }\n paintLive();\n if (u.changes.length || u.removed.length || u.sends.some((s) => inQueue(stageOf(s)))) {\n readSends();\n }\n },\n // The database is behind the page's read (a reset or a restore): read it all again.\n stale: remount,\n },\n signal,\n );\n}\n\n/** The scheduled sends, soonest first. */\nconst scheduledOf = (sends: SendView[]) =>\n sends.filter((s) => s.status === \"scheduled\").sort((a, b) => a.fire_at - b.fire_at);\n\n/** The dashboard's Sent table: the latest five sends that have started, each opening its\n * page (the watch while it sends, the record once sent). */\nfunction dashSentHtml(sends: SendView[]): Html {\n const recent = sends.filter((s) => s.status === \"sent\" || s.status === \"sending\").slice(0, 5);\n if (!recent.length) {\n return html`<p class=\"muted\">No sends yet.</p>`;\n }\n return html`<div class=\"table-wrap\"><table><thead><tr><th>Subject</th><th>Status</th><th class=\"num\">Recipients</th><th class=\"num\">Delivered</th><th></th></tr></thead><tbody>${recent.map(\n (s) => {\n // The published post, which the send's view links once it has started.\n const url = s.links.archive;\n return html`<tr class=\"clickable\" data-send=\"${s.id}\"><td><a href=\"#/sent/${s.id}\">${s.subject}</a></td><td>${badge(s.status)}</td><td class=\"num\">${s.audience.count.toLocaleString()}</td><td class=\"num delivered\">${deliveredCell(s.counts)}</td><td class=\"act\">${\n url\n ? html`<a class=\"ghost-link\" href=\"${url}\" target=\"_blank\" rel=\"noopener\">Archive \u2197</a>`\n : null\n }</td></tr>`;\n },\n )}</tbody></table></div>`;\n}\n// Sent-table rows carry a SEND id (not a post id) and open the send's page.\nfunction wireDashSentRows(root: HTMLElement): void {\n for (const tr of $$<HTMLTableRowElement>(\"tr[data-send]\", root)) {\n tr.onclick = (e) => {\n const t = e.target;\n if (t instanceof Element && t.tagName !== \"A\") {\n location.hash = `#/sent/${tr.dataset.send}`;\n }\n };\n }\n}\n/** A listed send still sending or settling: its Delivered cell follows the layer's counts,\n * so the table moves with its receipts without being read again. */\nfunction patchDelivered(root: HTMLElement, sends: SendView[]): void {\n for (const s of sends) {\n const cell = $$<HTMLTableRowElement>(\"#dashSent tr[data-send]\", root)\n .find((tr) => tr.dataset.send === s.id)\n ?.querySelector(\"td.delivered\");\n if (cell) {\n setHtml(cell, deliveredCell(s.counts));\n }\n }\n}\n\n/** The dashboard's scheduled cards are read-only summaries: the whole card links into the\n * editor, where the schedule is actually managed. A send the server reports as re-made\n * since its last test says so, and the editor, where the test is, says what to do. The\n * Sent page keeps the one-call cancel the review window needs (SPEC \u00A78). */\nfunction dashScheduledHtml(scheduled: SendView[]): Html {\n if (!scheduled.length) {\n return html`<p class=\"muted\">Nothing scheduled.</p>`;\n }\n const held = heldWhenFiresHtml();\n return html`${scheduled.map(\n (s) =>\n html`<div class=\"card spread clickable nextup sched-card\" data-post=\"${s.post_id}\"><div><a class=\"card-link sched-subj\" href=\"#/edit/${s.post_id}\">${s.subject}</a><div class=\"muted\">${countdownHtml(s)} \u00B7 ${fmt(s.fire_at)} \u00B7 ${s.audience.count} recipients</div>${held}${has(s, \"remade\") ? html`<div class=\"sched-remade-mark\">Look changed since your last test</div>` : null}</div></div>`,\n )}`;\n}\nfunction wireDashScheduledCards(root: HTMLElement): void {\n for (const card of $$(\"#dashScheduled .nextup\", root)) {\n card.onclick = (e) => {\n const t = e.target;\n if (t instanceof Element && t.tagName !== \"A\") {\n location.hash = `#/edit/${card.dataset.post}`;\n }\n };\n }\n}\n\n/** The dashboard active-send section (nothing when nothing is in flight). */\nfunction dashActiveHtml(active: SendView[]): Html {\n if (!active.length) {\n return html``;\n }\n return html`<section class=\"dash-section\"><h2>Active send${active.length === 1 ? \"\" : \"s\"}</h2>${active.map(\n activeRowHtml,\n )}</section>`;\n}\nfunction wireDashActiveCards(root: HTMLElement): void {\n for (const card of $$(\"#dashActive .active-card[data-watch]\", root)) {\n card.onclick = (e) => {\n const t = e.target;\n if (t instanceof Element && t.tagName !== \"A\") {\n location.hash = `#/sent/${card.dataset.watch}`;\n }\n };\n }\n}\n\n// Controls shared by the Dashboard and the Getting-started view: hash navigation,\n// \"New post\", \"Add subscriber\", and copy buttons.\nfunction wireDashActions(root: HTMLElement, reload: () => void, signal: AbortSignal): void {\n for (const b of $$(\"[data-nav]\", root)) {\n b.onclick = () => {\n location.hash = b.dataset.nav ?? \"\";\n };\n }\n for (const b of $$<HTMLButtonElement>(\"[data-act='new-post']\", root)) {\n b.onclick = () => createNewPost(b);\n }\n for (const b of $$(\"[data-act='add-sub']\", root)) {\n b.onclick = () => addSubscriberModal(reload, signal);\n }\n wireCopy(root);\n}\n\n// The onboarding checklist, shared by the first-run dashboard and Getting-started.\n// The \"API access\" card. Two states: an invitation to connect Claude, or \u2014 once Claude\n// (the `service` principal, SPEC \u00A74) has edited here \u2014 a plain \"Claude is connected\" note.\n// Shared by the populated dashboard and the first-run state (a card below the setup\n// checklist), so the two can't drift; connecting an agent is optional, so this is never a\n// required setup step. Both states have one shape \u2014 a line, then the card's links stacked\n// at one weight, each led by a glyph for what it is (the guide, the reference) \u2014 because\n// neither link is an action: connecting happens outside the app (an Access service token),\n// so nothing here may look like a button that would do it. Typography-led with no base-URL\n// field: the operator already knows their own origin (it's the Publication card's URL right\n// beside this one), and the connect guide is where that URL is actually used.\nfunction apiConnectCard(connected: boolean): Html {\n if (connected) {\n return html`<div class=\"card pub-card\">\n <p class=\"conn-status\"><span class=\"conn-dot\" aria-hidden=\"true\"></span>Claude is connected.</p>\n <div class=\"pub-foot pub-links\"><a href=\"#/reference\">${icon(\"code\")}API reference \u2192</a><a href=\"#/docs/connect-claude\">${icon(\"article\")}Connection guide \u2192</a></div>\n </div>`;\n }\n return html`<div class=\"card pub-card\">\n <p class=\"pub-note\">Let Claude draft, proofread, and schedule your posts.</p>\n <div class=\"pub-foot pub-links\"><a href=\"#/docs/connect-claude\">${icon(\"article\")}Connect Claude \u2192</a><a href=\"#/reference\">${icon(\"code\")}API reference \u2192</a></div>\n </div>`;\n}\n\n/**\n * The first-run checklist, in the order the steps depend on each other. Writing a post\n * comes late because its New post creates a draft, which ends the first-run page; sharing\n * comes last so a visitor finds a post in the archive. While no provider is connected,\n * connecting one replaces the sending-domain step (connecting SES verifies the domain) and\n * the subscribe step says signups wait for it (the page is closed).\n */\nfunction setupChecklistHtml(pub: Publication, deployment: DeploymentView | null): Html {\n const subscribeUrl = `${deployment?.appOrigin || location.origin}/subscribe`;\n const notConnected = providerNotConnected();\n return html`<div class=\"card setup\">\n <h2 class=\"setup-title\">Set up your publication</h2>\n <ol class=\"setup-steps\">\n <li><div class=\"setup-step-main\"><strong>Name your publication</strong><span class=\"muted\">Currently \u201C${pub.name}\u201D. Set the name, tagline, and brand in Settings.</span></div><button data-nav=\"#/settings\">Settings</button></li>\n ${\n notConnected\n ? html`<li class=\"setup-connect\"><div class=\"setup-step-main\"><strong>Connect Amazon SES</strong><span class=\"muted\">Nothing can be sent, and signups stay closed, until SES is connected.</span></div><button data-nav=\"${CONNECT_PROVIDER_HREF}\">Connect Amazon SES</button></li>`\n : html`<li><div class=\"setup-step-main\"><strong>Confirm your sending domain</strong><span class=\"muted\">SPF, DKIM, and DMARC on your From address. The setup guide walks through it.</span></div><button data-nav=\"#/docs/sending-domain-dns\">Docs</button></li>`\n }\n <li><div class=\"setup-step-main\"><strong>Write your first post</strong><span class=\"muted\">Draft a post in Markdown and preview it exactly as the email.</span></div><button class=\"primary\" data-act=\"new-post\">New post</button></li>\n ${\n notConnected\n ? html`<li class=\"setup-subscribe\"><div class=\"setup-step-main\"><strong>Share your subscribe link</strong><span class=\"muted\">Signups open once an email provider is connected; until then the page says they aren\u2019t open yet.</span></div></li>`\n : html`<li class=\"setup-subscribe\"><div class=\"setup-step-main\"><strong>Share your subscribe link</strong><code class=\"setup-url\">${subscribeUrl}</code></div><button type=\"button\" data-copy=\"${subscribeUrl}\" aria-label=\"Copy the subscribe link\">${copyFace()}</button></li>`\n }\n </ol>\n </div>`;\n}\n", "// The send actions a publisher takes from more than one surface: resolving a wedged send\n// (the list, the watch) and rescheduling (the list's scheduled card, the editor's banner).\n\nimport type { ResolveResponse, SendSummary, StuckResolution } from \"../../shared/sends\";\nimport { ApiError, api, describeError } from \"../api\";\nimport {\n earliestFireAt,\n isTooSoon,\n minLeadText,\n notConnectedRescheduleLine,\n tooSoonText,\n} from \"../deployment\";\nimport { readSendsNow } from \"../send_state\";\nimport { $ } from \"../ui/dom\";\nimport { toLocalInput } from \"../ui/format\";\nimport { html } from \"../ui/html\";\nimport { busy, modal, toast } from \"../ui/widgets\";\n\nconst message = describeError;\n\n/**\n * The one manual step for a wedged send: decide whether the ambiguous batch went out or\n * not. Both outcomes are safe for I4 (neither re-mails this post), so the modal explains\n * the trade-off (record accuracy) rather than warning of a double-send.\n */\nexport function openResolveModal(\n send: Pick<SendSummary, \"id\" | \"c_in_flight\">,\n reload: () => void,\n signal: AbortSignal,\n): void {\n const n = send.c_in_flight || 0;\n const noun = n === 1 ? \"delivery\" : \"deliveries\";\n const m = modal(\n html`<h3>Resolve ${n} ambiguous ${noun}</h3>\n <p class=\"hint\">A transport error left ${n} recipient${n === 1 ? \"\" : \"s\"} in flight: the request went out but the provider never confirmed, so there\u2019s no way to know whether it was accepted. To avoid mailing anyone twice, the send won\u2019t retry ${n === 1 ? \"it\" : \"them\"} on its own, so it can\u2019t finish until you decide. Neither choice re-sends this post.</p>\n <p class=\"hint\">Kestrel can\u2019t tell whether ${n === 1 ? \"this email\" : \"these emails\"} went out, so unless you know, <strong>Assume not sent</strong> is the safe default.</p>\n <p class=\"hint\"><strong>Assume not sent</strong>: recorded as unsent. ${n === 1 ? \"That reader\" : \"Those readers\"} may miss this post but ${n === 1 ? \"gets\" : \"get\"} your next one, and no one is mailed twice.</p>\n <p class=\"hint\"><strong>Assume sent</strong>: recorded as accepted by your provider, with delivery still waiting on its receipt. Choose this when you know otherwise, such as a reader who says they got it.</p>\n <div class=\"actions\"><button type=\"button\" id=\"rCancel\">Cancel</button><button type=\"button\" id=\"rUnsent\">Assume not sent</button><button type=\"button\" class=\"primary\" id=\"rAccepted\">Assume sent</button></div>`,\n signal,\n );\n $(\"#rCancel\", m.el).onclick = m.close;\n const doResolve = (btn: HTMLButtonElement, resolution: StuckResolution, verb: string) =>\n busy(btn, \"Resolving\u2026\", async () => {\n m.clearError();\n try {\n const res = await api<ResolveResponse>(`/api/sends/${send.id}/resolve`, {\n method: \"POST\",\n json: { resolution },\n });\n m.close();\n toast(res.completed ? \"Send completed\" : `Marked ${verb}`);\n reload();\n } catch (e) {\n m.error(message(e));\n }\n });\n const unsent = $<HTMLButtonElement>(\"#rUnsent\", m.el);\n const accepted = $<HTMLButtonElement>(\"#rAccepted\", m.el);\n unsent.onclick = () => doResolve(unsent, \"unsent\", \"not sent\");\n accepted.onclick = () => doResolve(accepted, \"accepted\", \"sent\");\n}\n\n/**\n * Move a scheduled send's fire time without canceling or re-editing: the content stays\n * frozen (I3) and the cancelable review window is preserved (I6); only fire_at moves,\n * via POST /api/sends/:id/reschedule (SPEC \u00A76). The same datetime picker as the Schedule\n * modal, prefilled with the current fire time and floored at the minimum lead. Shared\n * by the editor's scheduled banner and the Sent page's scheduled card (SPEC \u00A78), so\n * `onDone` re-renders whichever surface opened it.\n */\nexport function openRescheduleModal(\n sendId: string,\n currentFireAt: number,\n onDone: () => void,\n signal: AbortSignal,\n): void {\n const minStr = toLocalInput(earliestFireAt());\n const cur = toLocalInput(new Date(currentFireAt));\n const m = modal(\n html`<h3 id=\"rsHead\">Reschedule this post</h3>\n <p class=\"hint\">Move when it sends (at least ${minLeadText()} out). The content stays frozen and the cancelable window is kept. Only the time changes.</p>\n ${notConnectedRescheduleLine()}\n <label for=\"rsWhen\">Send at</label><input type=\"datetime-local\" id=\"rsWhen\" min=\"${minStr}\" value=\"${cur}\">\n <div class=\"actions\"><button type=\"button\" id=\"rsCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"rsGo\">Reschedule</button></div>`,\n signal,\n );\n const box = $(\".modal\", m.el);\n box.setAttribute(\"aria-labelledby\", \"rsHead\");\n const go = $<HTMLButtonElement>(\"#rsGo\", box);\n const when = $<HTMLInputElement>(\"#rsWhen\", box);\n $(\"#rsCancel\", box).onclick = m.close;\n go.onclick = () =>\n busy(go, \"Rescheduling\u2026\", async () => {\n m.clearError();\n const v = when.value;\n const t = v ? new Date(v).getTime() : Number.NaN;\n if (Number.isNaN(t)) {\n m.error(\"Pick a valid date & time\", when);\n return;\n }\n if (isTooSoon(t)) {\n m.error(tooSoonText(false), when);\n return;\n }\n try {\n await api(`/api/sends/${sendId}/reschedule`, {\n method: \"POST\",\n json: { fire_at: new Date(t).toISOString() },\n });\n m.close();\n toast(\"Rescheduled\");\n onDone();\n } catch (e) {\n if (e instanceof ApiError && e.status === 404) {\n // The send went with its post. Say so in words (the API names only \"send\"), and\n // read the feed now so the page showing it follows: the editor goes to Drafts with\n // the same message, which shows as one toast.\n m.close();\n toast(\"This post was deleted elsewhere.\");\n readSendsNow();\n return;\n }\n if (e instanceof ApiError && e.field === \"fire_at\") {\n m.error(e.code === \"fire_at_too_soon\" ? tooSoonText(false) : message(e), when);\n return;\n }\n m.error(message(e));\n }\n });\n when.focus();\n}\n", "// The one content security policy for a page carrying a post's HTML (SPEC \u00A75), shared by\n// the Worker, which sends it as a header on every page it serves, and the editor, which\n// restates it inside a framed copy of such a page, so the two can't drift.\n//\n// What the pages actually load: inline styles (the email's own, the page chrome's\n// <style>), the display font from Google Fonts, and images from anywhere a post may\n// point (the media origin, which may be a separate public bucket domain, or any image the\n// author linked). Nothing else, and no script, frame, plugin, or base rewrite.\n\n/**\n * The policy, as a header value. `formAction` is the one difference between the kinds of\n * page (a post's page submits no form at all; a page the app wrote posts back to itself).\n * `framed` is the copy the editor frames, carried in a <meta> tag: it leaves out\n * `frame-ancestors`, which a <meta> can't carry.\n */\nexport function pagePolicy(formAction: string, opts: { framed?: boolean } = {}): string {\n return [\n \"default-src 'none'\",\n \"script-src 'none'\",\n \"object-src 'none'\",\n \"frame-src 'none'\",\n \"base-uri 'none'\",\n `form-action ${formAction}`,\n opts.framed ? null : \"frame-ancestors 'none'\",\n \"img-src * data:\",\n \"style-src 'unsafe-inline' https://fonts.googleapis.com\",\n \"font-src https://fonts.gstatic.com\",\n ]\n .filter((d) => d !== null)\n .join(\"; \");\n}\n", "// A framed email or page in the editor: the post's Preview tab, its web version, the sample\n// email on the Template page and in Settings, and Settings' reader page previews. Its\n// sandbox, the policy a framed copy carries, and what its links do.\n\nimport { pagePolicy } from \"../../shared/page_policy\";\n\n// A framed preview is a srcdoc document, whose links resolve against the editor's own URL,\n// so a fragment-only link (the sample email's `#unsubscribe` placeholder) names the\n// dashboard rather than a spot in the email: followed, it would open the dashboard or load\n// it into the frame. The click is refused instead. The listener belongs to the editor, not\n// the frame, so it runs even where the frame's sandbox allows no scripts; one shared\n// function, so running the helper again on the same link adds nothing.\nconst stayPut = (e: Event) => e.preventDefault();\n\n/** The sandbox for a frame holding a post's email or page. Same-origin, so the editor can\n * size the frame and set its links to open a new tab (`openLinksInNewTab`); popups, so\n * that new tab can open at all; and popups escaping the sandbox, so the page it opens\n * works as it would from an inbox rather than inheriting this sandbox. Never scripts or\n * forms: the post's markup does nothing inside the editor but show. */\nexport const EMAIL_FRAME_SANDBOX = \"allow-same-origin allow-popups allow-popups-to-escape-sandbox\";\n\n/** Every link in a framed email made to open a new tab, with no opener and no referrer.\n * Followed in the frame, a link would load its target there, and most targets (the app's\n * own pages among them) refuse to be framed, so the frame would go blank and the preview\n * be lost. A fragment-only link does nothing instead (`stayPut`). Run it whenever the\n * frame's content changes. A sandboxed frame also needs `allow-popups` for the new tab to\n * open at all. */\nexport function openLinksInNewTab(doc: Document): void {\n for (const link of doc.querySelectorAll<HTMLAnchorElement | HTMLAreaElement>(\n \"a[href], area[href]\",\n )) {\n if (link.getAttribute(\"href\")?.startsWith(\"#\")) {\n link.addEventListener(\"click\", stayPut);\n link.addEventListener(\"auxclick\", stayPut);\n continue;\n }\n link.target = \"_blank\";\n link.rel = \"noopener noreferrer\";\n }\n}\n\n/** `markup` placed first in the document's <head> (never a <header>), or first in the\n * document when it has none. */\nfunction intoHead(doc: string, markup: string): string {\n const head = /<head(\\s[^>]*)?>/i;\n return head.test(doc) ? doc.replace(head, (tag) => tag + markup) : markup + doc;\n}\n\n/** The framed copy of a page (a post's page, a reader page's preview) under the policy a\n * page that submits no form carries when served (shared/page_policy.ts). The frame's sandbox already keeps script and forms out; the\n * policy is the second lock. Only the framed copy changes, never the page served. */\nexport function framedCopy(doc: string): string {\n return intoHead(\n doc,\n `<meta http-equiv=\"Content-Security-Policy\" content=\"${pagePolicy(\"'none'\", { framed: true })}\">`,\n );\n}\n", "// Syntax highlighting for the template editor (HTML/CSS with template tokens), the post\n// composer (Markdown), and the API reference's examples (JSON and a curl command). String\n// assembly by nature: the source is escaped first, then the escaped text is decorated with\n// spans, so the result is vouched for as markup at the boundary rather than built with the\n// html tag.\n\nimport { escapeHtml as esc, type Html, unsafeHtml } from \"./html\";\n\n// --- template syntax highlighting ---\n// A small tokenizer for the overlay editor: colors the {{ tokens }}, HTML tags and\n// attribute strings, and \u2014 inside the <style> block \u2014 CSS selectors, properties,\n// colors and at-rules. Logic-less templates need nothing heavier, so this stays\n// inside the dashboard's framework-free, no-build ethos (no CodeMirror, no bundler).\nfunction hlTokens(s: string): string {\n return s.replace(/\\{\\{\\s*(?:if\\s+)?[\\w.]+\\s*\\}\\}/g, (m) => `<span class=\"cx-var\">${m}</span>`);\n}\nfunction hlHtml(raw: string): string {\n let s = esc(raw);\n s = s.replace(/"[^&]*?"/g, (m) => `<span class=\"cx-str\">${m}</span>`);\n s = s.replace(\n /(<\\/?)([a-zA-Z][\\w-]*)/g,\n (_m, br, name) => `<span class=\"cx-punct\">${br}</span><span class=\"cx-tag\">${name}</span>`,\n );\n s = s.replace(/(\\/?)>/g, (_m, sl) => `<span class=\"cx-punct\">${sl}></span>`);\n return hlTokens(s);\n}\nfunction hlCss(raw: string): string {\n let s = esc(raw);\n s = s.replace(/"[^&]*?"/g, (m) => `<span class=\"cx-str\">${m}</span>`);\n s = s.replace(/#[0-9a-fA-F]{3,8}\\b/g, (m) => `<span class=\"cx-num\">${m}</span>`);\n s = s.replace(/@[\\w-]+/g, (m) => `<span class=\"cx-at\">${m}</span>`);\n s = s.replace(\n /^(\\s*)(?![@\\s])([^\\n{}<]+?)(\\s*)\\{$/gm,\n (_m, ind, sel, sp) => `${ind}<span class=\"cx-sel\">${sel}</span>${sp}{`,\n );\n s = s.replace(\n /^(\\s*)([a-z-]+)(\\s*:)/gm,\n (_m, ind, prop, colon) => `${ind}<span class=\"cx-prop\">${prop}</span>${colon}`,\n );\n return hlTokens(s);\n}\n/** The template source as highlighted markup for the overlay editor. */\nexport function highlightTemplate(src: string): Html {\n return unsafeHtml(\n String(src)\n .split(/(<style>[\\s\\S]*?<\\/style>)/)\n .map((seg) => {\n const m = seg.match(/^<style>([\\s\\S]*?)<\\/style>$/);\n if (m) {\n return `<span class=\"cx-punct\"><</span><span class=\"cx-tag\">style</span><span class=\"cx-punct\">></span>${hlCss(m[1] ?? \"\")}<span class=\"cx-punct\"></</span><span class=\"cx-tag\">style</span><span class=\"cx-punct\">></span>`;\n }\n return hlHtml(seg);\n })\n .join(\"\"),\n );\n}\n\n// --- post (Markdown) syntax highlighting ---\n// highlightTemplate()'s counterpart for the post editor: the same transparent-textarea-\n// over-highlighted-<pre> overlay, tokenizing Markdown instead of HTML/CSS, in the same\n// framework-free, no-build spirit. The scheme is deliberately minimal (Sublime-style): it\n// colors the *marks* an author scans for \u2014 the #, >, list bullets, the emphasis *asterisks*,\n// a link's URL \u2014 and leaves the CONTENT in the font color, carrying only weight/slant. So a\n// bold word reads as weight, not as a color that could be confused with a heading. It is a\n// light, line-oriented pass, not a full CommonMark parser, over the shared --syntax-* palette.\n\n// Inline constructs within one line, in a single left-to-right pass. One alternation consumes\n// each construct whole so we never re-tokenize inside a span we just emitted (the classic bug\n// from chaining .replace() calls \u2014 an italic pass eating a bold marker). Marks are colored, the\n// content plain: emphasis asterisks are pink and the word carries only weight/slant; a link's\n// text stays plain and only its URL is treated; inline `code` sits on a chip. Order at a given\n// position: `code`, then [links]/images, then **strong** before *em*. Single-underscore _em_ is\n// guarded by \\b so intra-word underscores (snake_case, price_1) stay literal, not emphasized.\nfunction hlMdInline(raw: string): string {\n return esc(raw).replace(\n /(`[^`\\n]+`)|(!?)(\\[[^\\]\\n]*\\])\\(([^)\\n]*)\\)|(\\*\\*|__)([^\\n]+?)\\5|\\*([^*\\n]+?)\\*|\\b_([^_\\n]+?)_\\b/g,\n (_m, code, bang, ltext, lurl, bd, btext, aem, uem) => {\n if (code) {\n return `<span class=\"cx-md-code\">${code}</span>`;\n }\n if (ltext !== undefined) {\n // Link/image: text + brackets stay plain; only the URL is treated (blue, underlined).\n return `${bang || \"\"}${ltext}(<span class=\"cx-md-url\">${lurl}</span>)`;\n }\n if (bd) {\n return `<span class=\"cx-md-mark\">${bd}</span><span class=\"cx-md-strong\">${btext}</span><span class=\"cx-md-mark\">${bd}</span>`;\n }\n // Emphasis: asterisk and underscore share the same treatment; the delimiter is a literal.\n const [d, text] = aem !== undefined ? [\"*\", aem] : [\"_\", uem];\n return `<span class=\"cx-md-mark\">${d}</span><span class=\"cx-md-em\">${text}</span><span class=\"cx-md-mark\">${d}</span>`;\n },\n );\n}\n// One line's block-level construct. Marks are colored and the text stays plain (still inline-\n// tokenized). Falls through to a plain inline pass for prose.\nfunction renderMdLine(line: string): string {\n // ATX heading: crimson # marks, heading text in the font color (bold for hierarchy).\n const h = line.match(/^(\\s{0,3}#{1,6})(\\s.*)?$/);\n if (h) {\n return `<span class=\"cx-md-hmark\">${esc(h[1] ?? \"\")}</span><span class=\"cx-md-htext\">${esc(h[2] || \"\")}</span>`;\n }\n // Thematic break: --- *** ___ (three or more of one mark), alone on the line.\n if (/^\\s{0,3}([-*_])(?:\\s*\\1){2,}\\s*$/.test(line)) {\n return `<span class=\"cx-md-bmark\">${esc(line)}</span>`;\n }\n // Blockquote: > markers colored, the quoted text plain (still inline-tokenized).\n const q = line.match(/^(\\s{0,3})((?:>\\s?)+)(.*)$/);\n if (q) {\n return `${esc(q[1] ?? \"\")}<span class=\"cx-md-bmark\">${esc(q[2] ?? \"\")}</span>${hlMdInline(q[3] ?? \"\")}`;\n }\n // List item: -, *, + (unordered) or 1. / 1) (ordered); colored marker, plain text.\n const li = line.match(/^(\\s*)([-*+]|\\d{1,9}[.)])(\\s+)(.*)$/);\n if (li) {\n return `${li[1]}<span class=\"cx-md-bmark\">${esc(li[2] ?? \"\")}</span>${li[3]}${hlMdInline(li[4] ?? \"\")}`;\n }\n return hlMdInline(line);\n}\n/** The post's Markdown as highlighted markup for the composer overlay, one block row per line. */\nexport function highlightMarkdown(src: string): Html {\n let inFence = false;\n return unsafeHtml(\n String(src)\n .split(\"\\n\")\n .map((line) => {\n let html: string;\n let band = false;\n // Fenced code block: a ``` or ~~~ line toggles the fence. The backticks stay plain and\n // only the language after them is colored. The fence lines themselves stay off the band \u2014\n // only the code between the fences carries the background.\n const fence = line.match(/^(\\s*)(```+|~~~+)(.*)$/);\n if (fence) {\n inFence = !inFence;\n const lang = fence[3] ? `<span class=\"cx-md-bmark\">${esc(fence[3])}</span>` : \"\";\n html = `${fence[1]}${esc(fence[2] ?? \"\")}${lang}`;\n } else if (inFence) {\n band = true;\n html = esc(line);\n } else {\n html = renderMdLine(line);\n }\n // Each source line is its own block row, so the caret aligns line-for-line with the\n // textarea and consecutive fenced rows' backgrounds abut into one continuous band. An\n // empty row is held open to one line by .cx-md-ln's min-height (CSS), not a filler glyph.\n return `<span class=\"cx-md-ln${band ? \" cx-md-band\" : \"\"}\">${html}</span>`;\n })\n .join(\"\"),\n );\n}\n\n// --- API reference examples ---\n// One pass over the raw JSON text: each token is escaped and wrapped, and the whitespace\n// between tokens is escaped as it stands, so nothing is decorated twice.\nconst JSON_TOKEN =\n /(\"(?:\\\\.|[^\"\\\\])*\")(\\s*:)?|(-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)|\\b(true|false|null)\\b|([{}[\\],])/g;\n/** A value as pretty-printed JSON (two-space indent), highlighted on the shared syntax palette. */\nexport function highlightJson(value: unknown): Html {\n const src = JSON.stringify(value, null, 2) ?? \"\";\n let out = \"\";\n let last = 0;\n for (const m of src.matchAll(JSON_TOKEN)) {\n out += esc(src.slice(last, m.index));\n const [whole, str, colon, num, lit] = m;\n if (str !== undefined) {\n out += colon\n ? `<span class=\"cx-prop\">${esc(str)}</span><span class=\"cx-punct\">${esc(colon)}</span>`\n : `<span class=\"cx-str\">${esc(str)}</span>`;\n } else if (num !== undefined) {\n out += `<span class=\"cx-num\">${num}</span>`;\n } else if (lit !== undefined) {\n out += `<span class=\"cx-tag\">${lit}</span>`;\n } else {\n out += `<span class=\"cx-punct\">${esc(whole)}</span>`;\n }\n last = m.index + whole.length;\n }\n return unsafeHtml(out + esc(src.slice(last)));\n}\n/** A curl command with the command, its flags, and the shell variables to fill in marked. */\nexport function highlightCurl(src: string): Html {\n return unsafeHtml(\n esc(src)\n .replace(/^curl\\b/, '<span class=\"cx-tag\">curl</span>')\n .replace(/(^|\\s)(-[A-Za-z])\\b/g, '$1<span class=\"cx-num\">$2</span>')\n .replace(/\\$[A-Z_]+/g, '<span class=\"cx-var\">$&</span>'),\n );\n}\n", "// Autosave timing, apart from the editor that uses it: save after a quiet pause, but never\n// let an edit sit unsaved longer than a hard cap even while the typing never pauses. The\n// idle timer restarts on every edit; the cap timer starts on the first edit after a save\n// and is left alone until a save fires or cancels it. Manual Save stays the primary path;\n// this is the safety net.\n\nexport interface AutosaveTimers {\n /** Quiet pause before a background save. */\n idleMs: number;\n /** Hard cap: no edit stays unsaved longer than this. */\n maxMs: number;\n}\n\nexport const AUTOSAVE_TIMERS: AutosaveTimers = { idleMs: 5000, maxMs: 30000 };\n\nexport interface Autosave {\n /** An edit happened. */\n touch(): void;\n /** Forget the pending save: a save is starting, a conflict paused autosave, or the editor is leaving. */\n cancel(): void;\n /** Whether a save is scheduled. */\n readonly pending: boolean;\n}\n\n/** The two-timer machine; `save` runs once when either timer fires, with both cleared first. */\nexport function createAutosave(\n save: () => void,\n timers: AutosaveTimers = AUTOSAVE_TIMERS,\n): Autosave {\n let idle: number | null = null;\n let cap: number | null = null;\n const cancel = () => {\n if (idle !== null) {\n clearTimeout(idle);\n }\n if (cap !== null) {\n clearTimeout(cap);\n }\n idle = null;\n cap = null;\n };\n const fire = () => {\n cancel();\n save();\n };\n return {\n touch() {\n if (idle !== null) {\n clearTimeout(idle);\n }\n idle = setTimeout(fire, timers.idleMs);\n if (cap === null) {\n cap = setTimeout(fire, timers.maxMs);\n }\n },\n cancel,\n get pending() {\n return idle !== null || cap !== null;\n },\n };\n}\n", "// Whether the editor's fields differ from what was last saved, as one comparison of a\n// snapshot: the fields serialized. \"Dirty\" drives the save-status cue, the navigation\n// guards, and whether an autosave has anything to do.\n\nexport class DirtyTracker<T> {\n #saved: string;\n\n constructor(private readonly read: () => T) {\n this.#saved = JSON.stringify(read());\n }\n\n /** The current values, in the form they are compared and sent. */\n snapshot(): string {\n return JSON.stringify(this.read());\n }\n\n get dirty(): boolean {\n return this.snapshot() !== this.#saved;\n }\n\n /**\n * What is saved now. Pass the snapshot that was actually sent: a save takes time, and an\n * edit typed while it was in flight is not saved by it. Marking the fields as they are\n * when the save lands would show that edit as saved and let a leave flush skip it.\n */\n markSaved(sent: string = this.snapshot()): void {\n this.#saved = sent;\n }\n}\n", "// The editor's saves still in flight, for a view the editor hands over to in the same\n// document. The web version link opens in place in a browser pane with no tabs, and its\n// read of the post must not overtake the save carrying the publisher's last edits.\n\nlet pending: Promise<unknown> = Promise.resolve();\n\n/** Record the editor's save chain as it stands; it settles once every save queued so far has. */\nexport function trackSaves(chain: Promise<unknown>): void {\n pending = chain;\n}\n\n/** Settles, never rejects, once every save the editor has queued has landed or failed. A\n * failed save shows in the editor; the reader here only waits its turn. */\nexport function savesSettled(): Promise<void> {\n return pending.then(\n () => {},\n () => {},\n );\n}\n", "// Optimistic concurrency on the client (SPEC \u00A74): the revision this editor is based on,\n// and what a failed save or a freshness poll means against it. The server rejects a save\n// whose base is not the newest revision (409) rather than clobber another writer's; the\n// editor's job is to tell the publisher, not to decide for them.\n\nimport { ApiError } from \"../api\";\n\n/** Who wrote a revision: an email, \"service\" for Claude, or unknown. */\nexport type Author = string | null;\n\n/** What the editor must surface: another writer's newer revision, or a post no longer editable here. */\nexport type Conflict = { kind: \"stale\"; revision: string; author: Author } | { kind: \"locked\" };\n\n/** Classify a failed save; null when it is a plain failure, not a conflict. */\nexport function conflictFromError(e: unknown): Conflict | null {\n if (!(e instanceof ApiError) || e.status !== 409) {\n return null;\n }\n const d = e.data as { error?: unknown; current_revision?: unknown; author?: unknown } | null;\n if (d?.error === \"stale_revision\" && typeof d.current_revision === \"string\") {\n return {\n kind: \"stale\",\n revision: d.current_revision,\n author: typeof d.author === \"string\" ? d.author : null,\n };\n }\n return { kind: \"locked\" };\n}\n\n/** A freshness poll's answer: the post's status and newest revision, and who wrote it. */\nexport interface PollAnswer {\n status: string;\n revision: string | null;\n author: Author;\n}\n\n/** What was true when the poll was sent, to tell our own save's effect from another writer's. */\nexport interface PollContext {\n saving: boolean;\n baseAtRequest: string | null;\n}\n\nexport class RevisionTracker {\n /** The revision this editor is based on; carried on every save so the server can refuse a stale one. */\n base: string | null;\n /** The newest revision already surfaced, so the poll re-warns only on a genuinely newer one. */\n warnedRevision: string | null = null;\n\n constructor(base: string | null) {\n this.base = base;\n }\n\n /** Our save landed: it is the newest revision, so the poll compares against it. */\n saved(revision: string | null): void {\n this.base = revision;\n }\n\n /** A conflict was shown; a stale one is remembered so the poll does not repeat it. */\n noteWarned(conflict: Conflict): void {\n if (conflict.kind === \"stale\") {\n this.warnedRevision = conflict.revision;\n }\n }\n\n /** The publisher chose to keep editing: adopt the newer revision as the base, so the next save wins. */\n adoptWarned(): void {\n if (this.warnedRevision !== null) {\n this.base = this.warnedRevision;\n }\n this.warnedRevision = null;\n }\n\n clearWarning(): void {\n this.warnedRevision = null;\n }\n\n /**\n * What a poll's answer means. \"ignore\" when our own save moved the base while the poll\n * was in flight (the answer may predate it) or a save is running now; \"locked\" when the\n * post is no longer a draft; a stale conflict when there is a newer revision not yet\n * surfaced; \"fresh\" otherwise.\n */\n decide(answer: PollAnswer, at: PollContext): Conflict | \"ignore\" | \"fresh\" {\n if (at.saving || this.base !== at.baseAtRequest) {\n return \"ignore\";\n }\n if (answer.status !== \"draft\") {\n return { kind: \"locked\" };\n }\n const rev = answer.revision;\n if (rev && rev !== this.base && rev !== this.warnedRevision) {\n return { kind: \"stale\", revision: rev, author: answer.author };\n }\n return \"fresh\";\n }\n}\n", "// A post's web version: the post as its public post page will show it (SPEC \u00A75), for the\n// publisher to proof before it is permanent. It opens in a tab of its own from the editor's\n// Web version link, so it carries no app chrome: one strip in the brand accent above the\n// page says what it is, where the post stands, and where its public page will be, so the\n// copy is never mistaken for the live page, and the frame below holds the page exactly as\n// it will be. The strip is the editor's, outside the frame, so it stays while the post\n// scrolls and the framed page is the public page's bytes.\n// Once the post's send starts, the public page itself is the web version.\n\nimport type { PostResponse } from \"../../shared/posts\";\nimport { ApiError, api, apiText } from \"../api\";\nimport { postArchiveUrl } from \"../deployment\";\nimport { mount, onAbort } from \"../lifecycle\";\nimport { EMAIL_FRAME_SANDBOX, framedCopy, openLinksInNewTab } from \"../ui/email_frame\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { renderError } from \"../ui/widgets\";\nimport { savesSettled } from \"./pending_save\";\n\n/** The editor route (the hash) of a post's web version. */\nexport function webVersionHash(postId: string): string {\n return `#/web/post/${encodeURIComponent(postId)}`;\n}\n\n/** The post id a web-version route names (`#/web/post/<id>`, split into its parts), or null\n * for one that names none, so the router falls through. */\nexport function webPostId(kind: string | undefined, id: string | undefined): string | null {\n if (kind !== \"post\" || !id) {\n return null;\n }\n try {\n return decodeURIComponent(id);\n } catch {\n // A hand-typed or mangled address: no page, never a throw.\n return null;\n }\n}\n\n/** Where a post stands, for the bar: a draft or scheduled, and its public address (null\n * while unknown, so the bar names none). */\nexport interface WebBarState {\n scheduled: boolean;\n url: string | null;\n}\n\n/** An address as the bar shows it: without its scheme, which says nothing to a reader. */\nfunction shortUrl(url: string): string {\n return url.replace(/^https?:\\/\\//, \"\");\n}\n\n/** The bar's words: what the page is, as its tag, then where the post stands and where it\n * will be published (as text, since the address doesn't answer yet), with no date. Null\n * state: the post couldn't be read, so the bar says only what the page is. */\nexport function webBar(state: WebBarState | null): Html {\n const tag = html`<span class=\"web-bar-tag\">Web version</span>`;\n if (state === null) {\n return tag;\n }\n const stage = state.scheduled ? \"Scheduled\" : \"Draft\";\n const at = state.url ? html` \u00B7 will be published at ${shortUrl(state.url)}` : null;\n return html`${tag} <span class=\"web-bar-text\">${stage}${at}</span>`;\n}\n\n/** The same address, spelled one way, or null for one that is not a whole URL. */\nfunction normalUrl(url: string): string | null {\n try {\n return new URL(url).href;\n } catch {\n return null;\n }\n}\n\n/** In the Preview tab of an unpublished post, every link to the post's own public address\n * (its \"View in browser\" link) made to open its web version instead (SPEC \u00A75). That address\n * answers \"Nothing here yet\" until the send starts, so a publisher who follows it to see\n * the post's page would find none; the web version is that page. Only the framed copy\n * changes: the email's bytes, and every test and send of it, keep the public address.\n * Links to anywhere else, another post's page among them, are left as they are. Only\n * the address changes: `openLinksInNewTab` still opens the link in a new tab. */\nexport function previewOpensWebVersion(\n doc: Document,\n archiveUrl: string | null,\n webUrl: string,\n): void {\n const own = archiveUrl ? normalUrl(archiveUrl) : null;\n if (!own) {\n return;\n }\n for (const link of doc.querySelectorAll<HTMLAnchorElement | HTMLAreaElement>(\n \"a[href], area[href]\",\n )) {\n const href = link.getAttribute(\"href\");\n if (href && normalUrl(href) === own) {\n link.setAttribute(\"href\", webUrl);\n }\n }\n}\n\n/** Leave for another page, replacing this one in the history (so Back skips it). */\nexport function leaveFor(url: string): void {\n location.replace(url);\n}\n\n/** The post's read, or null when it fails (the page still shows, under a bar that names no\n * stage or address). */\nasync function readPost(id: string, signal: AbortSignal): Promise<PostResponse | null> {\n try {\n return await api<PostResponse>(`/api/posts/${encodeURIComponent(id)}`, { signal });\n } catch {\n return null;\n }\n}\n\nexport async function renderWebVersion(\n id: string,\n root: HTMLElement,\n signal: AbortSignal,\n): Promise<void> {\n const appTitle = document.title;\n document.title = \"Web version\";\n onAbort(signal, () => {\n document.title = appTitle;\n });\n try {\n // Opened in place from the editor, the click's save may still be going: read after it.\n await savesSettled();\n const [doc, data] = await Promise.all([\n apiText(`/api/posts/${encodeURIComponent(id)}/web`, { signal }),\n readPost(id, signal),\n ]);\n if (signal.aborted) {\n return;\n }\n const url = data ? postArchiveUrl(data.post.slug) : null;\n // Once the send starts the post is public, and its web version is that page: go there.\n if (data && (data.sending || data.post.status === \"sent\") && url) {\n leaveFor(url);\n return;\n }\n const bar = webBar(data ? { scheduled: data.scheduled != null, url } : null);\n setHtml(\n root,\n html`<div class=\"web-page\">\n <div class=\"web-bar\" role=\"note\"><div class=\"web-bar-in\">${bar}</div></div>\n <iframe class=\"web-frame\" sandbox=\"${EMAIL_FRAME_SANDBOX}\" title=\"Web version\" scrolling=\"no\"></iframe>\n </div>`,\n );\n const frame = root.querySelector<HTMLIFrameElement>(\".web-frame\");\n if (!frame) {\n return;\n }\n // A link opens a new tab, as in the Preview tab, and the page's own <title> is the\n // post's subject.\n frame.onload = () => {\n const page = frame.contentDocument;\n if (!page || signal.aborted) {\n return;\n }\n openLinksInNewTab(page);\n if (page.title) {\n document.title = `Web version \u00B7 ${page.title}`;\n }\n // The frame is as tall as the post, so the window scrolls the page and the strip\n // above goes up with it rather than staying pinned. Measured on the body, which is\n // content-sized, and kept fitted as fonts, images, and the window's width change it.\n const fitFrame = () => {\n const h = page.body?.scrollHeight;\n if (h) {\n frame.style.height = `${h}px`;\n }\n };\n fitFrame();\n if (page.body && typeof ResizeObserver !== \"undefined\") {\n const watch = new ResizeObserver(fitFrame);\n watch.observe(page.body);\n onAbort(signal, () => watch.disconnect());\n }\n };\n frame.srcdoc = framedCopy(doc);\n } catch (e) {\n if (signal.aborted) {\n return;\n }\n setHtml(root, html`<div class=\"web-error\"></div>`);\n const box = root.querySelector(\".web-error\");\n if (box) {\n // The API names only what it didn't find (\"post\"), which says nothing on its own.\n const notFound = e instanceof ApiError && e.status === 404;\n const msg = notFound\n ? \"There\u2019s no post here. It may have been deleted.\"\n : e instanceof Error\n ? e.message\n : String(e);\n renderError(box, msg, () => mount((r, s) => renderWebVersion(id, r, s)));\n }\n }\n}\n", "// The post editor: subject/slug, the Markdown composer, autosave with its idle and\n// hard-cap timers, the freshness poll, images, the schedule / send-now dialogs, and a\n// scheduled post's banner, which follows its send until the send page takes over.\n\nimport type { ImageUploadResponse } from \"../../shared/images\";\nimport type {\n PostEditBody,\n PostResponse,\n PostSavedResponse,\n TestSendResponse,\n} from \"../../shared/posts\";\nimport type { ScheduleResponse, SendAction, SendResponse, SendView } from \"../../shared/sends\";\nimport type { SettingsResponse } from \"../../shared/settings\";\nimport type { SubscriberListResponse } from \"../../shared/subscribers\";\nimport { ApiError, api, apiText, describeError } from \"../api\";\nimport {\n connectProviderLink,\n earliestFireAt,\n heldWhenFiresHtml,\n isTooSoon,\n minLeadText,\n notConnectedScheduleLine,\n notConnectedTestLine,\n notConnectedWarning,\n postArchiveUrl,\n providerNotConnected,\n tooSoonText,\n withNoProviderNote,\n} from \"../deployment\";\nimport { at, every, mount, onAbort, poll, type ViewHandle } from \"../lifecycle\";\nimport { followSend, readSendsNow, type StageChange } from \"../send_state\";\nimport { openRescheduleModal } from \"../sends/dialogs\";\nimport { can, conditionOf, has } from \"../sends/progress\";\nimport { $, $$ } from \"../ui/dom\";\nimport { EMAIL_FRAME_SANDBOX, openLinksInNewTab } from \"../ui/email_frame\";\nimport {\n fmt,\n invalidAddressesMessage,\n lateStr,\n notSentMessage,\n parseAddresses,\n toLocalInput,\n untilStr,\n} from \"../ui/format\";\nimport { highlightMarkdown } from \"../ui/highlight\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { type IconName, icon } from \"../ui/icons\";\nimport { busy, fieldError, infoTip, modal, renderError, toast } from \"../ui/widgets\";\nimport { createAutosave } from \"./autosave\";\nimport { DirtyTracker } from \"./dirty\";\nimport { trackSaves } from \"./pending_save\";\nimport { type Author, type Conflict, conflictFromError, RevisionTracker } from \"./revisions\";\nimport { previewOpensWebVersion, webVersionHash } from \"./web\";\n\nconst message = describeError;\n\nconst TOOLBAR: [IconName, string][][] = [\n [\n [\"heading\", \"Heading\"],\n [\"bold\", \"Bold (\u2318B)\"],\n [\"italic\", \"Italic (\u2318I)\"],\n ],\n [\n [\"quote\", \"Quote\"],\n [\"code\", \"Code\"],\n [\"link\", \"Link (\u2318K)\"],\n ],\n [\n [\"ul\", \"Bulleted list\"],\n [\"ol\", \"Numbered list\"],\n [\"indent\", \"Indent\"],\n ],\n];\n\n// A bare attribute is markup, not text: spelled once as markup so it can be interpolated.\nconst READONLY = html` readonly`;\nconst ARIA_DISABLED = html` aria-disabled=\"true\"`;\n\ntype ComposerTab = \"edit\" | \"preview\";\n\n/**\n * How a save ended. `saved` and `unchanged` both mean the server holds what the editor\n * shows. `refused` means it does not: another writer's revision is newer, so the save was\n * rejected (or is paused behind the out-of-date banner), and an action that would freeze\n * the content \u2014 schedule, send now (SPEC \u00A76, I3) \u2014 must not proceed on the publisher's\n * behalf. A plain failure throws instead of answering.\n */\ntype SaveResult = \"saved\" | \"unchanged\" | \"refused\";\n\nexport async function renderEditor(\n id: string,\n root: HTMLElement,\n signal: AbortSignal,\n): Promise<ViewHandle | undefined> {\n // Reload, cancel-schedule, and error-retry re-enter through mount(), which tears this\n // mount down (its autosave, its freshness poll) before the next one starts.\n // (A retry or reload click reaches this from a live root; a re-entry after a write the\n // reader did not stay to watch, such as a cancel that landed after they left, must not\n // mount the editor over wherever they went.)\n const remount = () => {\n if (!signal.aborted) {\n mount((r, s) => renderEditor(id, r, s));\n }\n };\n // The post was deleted elsewhere (another tab, or Claude through the API), found by\n // whichever read or write answers 404 first: say so once and go to Drafts. Deleting a\n // post is deliberate, and autosave keeps what is unsaved to the last few seconds of\n // typing, so the editor doesn't stay open to hold it.\n let gone = false;\n const removed = (msg = \"This post was deleted elsewhere.\") => {\n if (gone) {\n return;\n }\n gone = true;\n toast(msg);\n if (!signal.aborted) {\n handOff(\"#/drafts\");\n }\n };\n setHtml(root, html`<p class=\"muted\">Loading\u2026</p>`);\n let data: PostResponse;\n // A scheduled post's send, as the send routes report it: the banner's countdown and\n // conditions, and the cursor the editor follows the send from.\n let sendRead: SendResponse | null = null;\n try {\n data = await api<PostResponse>(`/api/posts/${id}`, { signal });\n if (signal.aborted) {\n return; // navigated away while loading: the redirects below must not hijack that\n }\n // A sent post is a frozen record, not editable: it opens the sent record view, never\n // the editor. Redirect a stale #/edit link (or a post sent in another tab / by Claude)\n // there instead of a locked editor. A post whose send is in flight is no longer an\n // editable, cancelable scheduled draft but an active send, so its link opens the live\n // watch, not a soft-locked editor with a dead Cancel.\n const home = sendHome(data);\n if (home) {\n handOff(home);\n return;\n }\n } catch (e) {\n // A Retry can only fail again for a post that isn't there (gone since the last read,\n // or a link to one that never was), so it goes to Drafts instead.\n if (isNotFound(e)) {\n removed(\"There\u2019s no post here. It may have been deleted.\");\n return;\n }\n renderError(root, message(e), remount);\n return;\n }\n if (data.scheduled) {\n try {\n sendRead = await api<SendResponse>(`/api/sends/${data.scheduled.id}`, { signal });\n } catch (e) {\n if (signal.aborted) {\n return;\n }\n // A send is deleted only with its post, so the post went between the two reads.\n if (isNotFound(e)) {\n removed();\n return;\n }\n // Any other failure leaves the post readable: the editor opens with its banner from\n // the post's own read, and follows the send once a later read of it succeeds.\n }\n if (signal.aborted || (sendRead && movedOn(sendRead.send, remount))) {\n return;\n }\n }\n const { post, markdown, scheduled } = data;\n\n // The editor now only ever mounts a draft or a scheduled (frozen) post, so `locked`\n // means scheduled \u2014 signaled by the scheduled banner, not a status pill.\n const locked = post.status !== \"draft\";\n // The revision this editor is based on, for optimistic concurrency (SPEC \u00A74).\n // Advanced on each successful save; carried on every save so the server rejects\n // (409) rather than clobbers a newer save from another tab or from Claude.\n const revisions = new RevisionTracker(post.current_revision);\n // A scheduled post keeps its formatting toolbar in view, greyed. The buttons take\n // aria-disabled rather than disabled so a click still reaches applyFormat, whose locked\n // branch nudges the foot line, the way out (DESIGN \u00A73).\n const toolbarHtml = TOOLBAR.map(\n (group, i) =>\n html`${i ? html`<span class=\"sep\"></span>` : null}${group.map(\n ([kind, label]) =>\n html`<button type=\"button\" class=\"tb\" data-fmt=\"${kind}\" title=\"${label}\" aria-label=\"${label}\"${locked ? ARIA_DISABLED : null}>${icon(kind)}</button>`,\n )}`,\n );\n // readonly, not disabled: a scheduled post's text can still be read, selected, and\n // copied; only a change is refused (DESIGN \u00A73).\n const ro = locked ? READONLY : null;\n // The web version's address (SPEC \u00A75), whole, so it opens as its own page. It differs\n // from this one only in the hash, which is what keeps a save safe when the link opens in\n // place (below).\n const webVersionUrl = `${location.origin}${location.pathname}${webVersionHash(id)}`;\n\n setHtml(\n root,\n html`\n <div class=\"editor-head\">\n <a href=\"#/drafts\" class=\"back\">\u2190 Drafts</a>\n <div class=\"editor-head-right\">\n <a class=\"ghost btn\" id=\"webLink\" href=\"${webVersionUrl}\" target=\"_blank\" rel=\"noopener\" aria-label=\"Web version (opens in a new tab)\">Web version \u2197</a>\n </div>\n </div>\n ${locked && scheduled ? html`<div class=\"banner banner-scheduled\"><div class=\"sched-main\"><span id=\"schedWhen\"></span>${heldWhenFiresHtml()}<div class=\"sched-remade\" id=\"schedRemade\" hidden><span id=\"schedRemadeMsg\"></span></div></div><span class=\"row\" id=\"schedControls\"><button type=\"button\" class=\"ghost\" id=\"rescheduleSchedule\">Reschedule</button><button type=\"button\" class=\"ghost\" id=\"cancelSchedule\">Cancel</button></span></div>` : null}\n <div id=\"freshnessBanner\" class=\"banner banner-conflict\" role=\"alert\" hidden></div>\n <div class=\"card\">\n <div class=\"grid2\">\n <div><label for=\"f-subject\">Subject</label><input id=\"f-subject\" value=\"${post.subject}\" placeholder=\"Untitled\"${ro}></div>\n <div>\n <div class=\"label-row\">\n <label for=\"f-slug\">Slug</label>\n ${infoTip(\"The web address of this post\u2019s archive page.\")}\n </div>\n <input id=\"f-slug\" value=\"${post.slug}\"${ro}>\n ${locked ? null : html`<label class=\"slug-auto-toggle\"><input type=\"checkbox\" id=\"f-slug-auto\">Auto-generate from subject</label>`}\n </div>\n </div>\n\n <label for=\"f-markdown\">Body</label>\n <div class=\"composer\">\n <div class=\"composer-head\">\n <div class=\"ctabs\" role=\"tablist\">\n <button type=\"button\" class=\"ctab active\" data-tab=\"edit\" data-text=\"Edit\" role=\"tab\" aria-selected=\"true\"><span class=\"ctab-label\">${icon(\"editable\")}Edit</span></button>\n <button type=\"button\" class=\"ctab\" data-tab=\"preview\" data-text=\"Preview\" role=\"tab\" aria-selected=\"false\"><span class=\"ctab-label\">${icon(\"preview\")}Preview</span></button>\n </div>\n <div class=\"toolbar\" role=\"toolbar\" aria-label=\"Formatting\">${toolbarHtml}</div>\n </div>\n <div class=\"composer-body${locked ? \" locked\" : \"\"}\" id=\"composerBody\">\n <pre class=\"md-hl\" id=\"mdHl\" aria-hidden=\"true\"><code></code></pre>\n <textarea id=\"f-markdown\" class=\"editor\"${locked ? null : html` placeholder=\"Type your post in Markdown\u2026\"`}${ro}>${markdown}</textarea>\n <iframe id=\"previewFrame\" class=\"preview\" sandbox=\"${EMAIL_FRAME_SANDBOX}\" title=\"Email preview\" hidden></iframe>\n </div>\n ${\n locked\n ? html`<div class=\"composer-foot composer-foot-lock\" id=\"lockFoot\" role=\"status\">${icon(\"readonly\")}<span>Cancel the schedule to edit</span></div>`\n : html`<div class=\"composer-foot\" id=\"dropFoot\">${icon(\"paperclip\")}<span>Paste, drop, or click to add images</span></div>`\n }\n <input type=\"file\" id=\"imgInput\" accept=\"image/png,image/jpeg,image/webp,image/gif\" multiple hidden>\n </div>\n <div id=\"warnings\"></div>\n ${locked ? null : notConnectedWarning(\"editor-not-connected\", html`No email provider connected: you can write and preview, not send. ${connectProviderLink()}.`)}\n\n <div class=\"actions-bar\">\n ${\n locked\n ? html`<div class=\"row\"><button type=\"button\" class=\"secondary\" id=\"testBtn\">${icon(\"send\")}<span>Send test email</span></button></div>`\n : html`<div class=\"row\">\n <button type=\"button\" class=\"ghost\" id=\"saveBtn\">Save draft</button>\n <span class=\"save-status\" id=\"saveStatus\" aria-live=\"polite\"></span>\n </div>\n <div class=\"row\">\n <button type=\"button\" class=\"secondary\" id=\"testBtn\">${icon(\"send\")}<span>Send test email</span></button>\n <button type=\"button\" class=\"primary\" id=\"scheduleBtn\">Schedule</button>\n </div>`\n }\n </div>\n </div>`,\n );\n\n const subjectEl = $<HTMLInputElement>(\"#f-subject\");\n const slugEl = $<HTMLInputElement>(\"#f-slug\");\n const ta = $<HTMLTextAreaElement>(\"#f-markdown\");\n const toolbarEl = $(\".toolbar\", root);\n const previewFrame = $<HTMLIFrameElement>(\"#previewFrame\");\n\n // Syntax-highlight overlay: a transparent <textarea> over a highlighted\n // <pre>, kept in scroll sync \u2014 the template editor's scaffolding, tokenizing Markdown.\n // Prose soft-wraps, so both layers share pre-wrap and identical metrics (the caret lands\n // on the colored text); that wrapping is also why there's no line-number gutter \u2014 a\n // number can't track a wrapped line and prose doesn't want one. The highlight is the point.\n const mdHl = $(\"#mdHl\");\n const mdHlCode = $(\"code\", mdHl);\n const syncMdScroll = () => {\n mdHl.scrollTop = ta.scrollTop;\n mdHl.scrollLeft = ta.scrollLeft;\n };\n const paintMarkdown = () => {\n // highlightMarkdown emits one block row per source line (split on \"\\n\"), so the row count \u2014\n // and thus the overlay height \u2014 already tracks the textarea, including a trailing blank line.\n setHtml(mdHlCode, highlightMarkdown(ta.value));\n syncMdScroll();\n };\n ta.addEventListener(\"scroll\", syncMdScroll);\n paintMarkdown(); // paint the initial content (a scheduled post is read-only but still highlighted)\n\n // Whether the slug follows the subject is the post's own (`slug_auto`), never guessed from\n // its shape: `spring-update-2027` set by hand looks just like a numbered `spring-update`.\n // While it follows, a save sends no slug of its own and the server derives and numbers it;\n // once set by hand, the field is what is sent. Either way the field shows only slugs the server\n // answered with, never the editor's own guess, so it can lag a subject edit until the save\n // but never shows an address the post won't get.\n const autoEl = locked ? null : $<HTMLInputElement>(\"#f-slug-auto\");\n const collect = () => ({\n subject: subjectEl.value,\n ...(autoEl?.checked ? { slug_auto: true } : { slug: slugEl.value, slug_auto: false }),\n markdown: ta.value,\n });\n // A save's answer for a slug being typed by hand waits until the field is left (or Enter),\n // so the cursor is never moved under the publisher; it lands only if the field still\n // holds what that save sent.\n let held: { shown: string; slug: string } | null = null;\n const applyHeld = () => {\n if (held && slugEl.value === held.shown) {\n slugEl.value = held.slug;\n refreshDirty(); // the field now matches what was saved\n }\n held = null;\n };\n\n // --- subject validation (SPEC \u00A76: freeze() rejects a subjectless send) ---\n // Surfaced on the field, not by disabling the button. The old guard greyed out\n // Schedule and hung the reason in a `title` on the row \u2014 invisible to keyboard,\n // touch, and screen readers. Instead Schedule stays live and we validate on click:\n // an empty subject puts the input into an error state with the reason directly\n // beneath it (DESIGN \u00A75), tied by aria-describedby and announced (role=alert).\n let clearSubjectErr: (() => void) | null = null;\n function clearSubjectError() {\n clearSubjectErr?.();\n clearSubjectErr = null;\n }\n // Gates both send paths (the scheduled send and the in-modal Send now both open\n // from Schedule): true when a subject is present; otherwise shows the error, moves\n // focus to the field, and returns false so the modal never opens.\n function validateSubject() {\n if (subjectEl.value.trim() === \"\") {\n clearSubjectErr = fieldError(subjectEl, \"Add a subject before you schedule.\");\n subjectEl.focus();\n return false;\n }\n clearSubjectError();\n return true;\n }\n\n // The \"Auto-generate from subject\" box, checked while the slug follows the subject. The\n // slug stays editable throughout. Typing your own slug takes manual control (unchecks);\n // emptying the field, or ticking the box, hands it back to the subject. The slug the\n // subject gives arrives with the save that leaving the subject makes (or Enter, or the\n // autosave).\n if (autoEl) {\n autoEl.checked = post.slug_auto;\n\n // Muted while it tracks the subject; normal color once it's set by hand.\n const reflect = () => slugEl.classList.toggle(\"slug-auto\", autoEl.checked);\n reflect();\n\n // Typing a slug takes manual control; clearing it follows the subject again.\n slugEl.addEventListener(\"input\", () => {\n autoEl.checked = slugEl.value.trim() === \"\";\n reflect();\n });\n // Ticking the box saves at once, so the field shows the subject's slug as soon as the\n // server answers; unticking hands over the field ready to edit.\n autoEl.addEventListener(\"change\", () => {\n reflect();\n markEdited(); // the checkbox changes what a save sends without an input event\n if (autoEl.checked) {\n saveDraft(true).catch(() => {}) /* the save cue says why */;\n } else {\n slugEl.focus();\n slugEl.select();\n }\n });\n // Never leave an empty slug: a field left empty follows the subject again. The save\n // that leaving it makes fills it in; with nothing to save, the post already has the\n // subject's slug, so the field shows that.\n slugEl.addEventListener(\"blur\", () => {\n applyHeld();\n if (slugEl.value.trim() === \"\") {\n autoEl.checked = true;\n reflect();\n markEdited();\n if (!dirty.dirty) {\n slugEl.value = savedSlug;\n }\n }\n });\n\n // Clear the empty-subject error as soon as a real subject is typed; it re-fires\n // on the next Schedule click if the field is still empty (see validateSubject).\n subjectEl.addEventListener(\"input\", () => {\n if (subjectEl.value.trim() !== \"\") {\n clearSubjectError();\n }\n });\n }\n\n // Autosave (./autosave.ts): save after a quiet pause, but never let an edit sit\n // unsaved longer than the hard cap. Manual Save + \u2318S stays the primary path; this is\n // the safety net. Failures surface as a toast, never silently. Cancelled with the mount,\n // so a navigation or the re-auth wall ends it. Declared before any path that can save\n // (the Preview tab saves first), since saveDraft cancels it.\n const mine = createAutosave(() => {\n saveDraft(true).catch(() => {}); // the save cue says why\n });\n onAbort(signal, () => mine.cancel());\n\n // --- tabs ---\n const tabs = $$<HTMLButtonElement>(\".ctab\", root);\n function showTab(name: ComposerTab) {\n for (const t of tabs) {\n const on = t.dataset.tab === name;\n t.classList.toggle(\"active\", on);\n t.setAttribute(\"aria-selected\", on ? \"true\" : \"false\");\n }\n ta.hidden = name !== \"edit\";\n mdHl.hidden = name !== \"edit\"; // the highlight layer travels with the textarea\n previewFrame.hidden = name !== \"preview\";\n toolbarEl.classList.toggle(\"off\", name !== \"edit\");\n // The image foot dims with the toolbar: a click on Preview would insert into a hidden\n // textarea behind a preview that doesn't change. A drop still lands (it switches to Edit).\n root.querySelector(\"#dropFoot\")?.classList.toggle(\"off\", name !== \"edit\");\n if (name === \"edit\") {\n syncMdScroll();\n }\n }\n // The slug the server last saved, which the preview renders with (a dedupe may differ\n // from what the field shows while it is being typed in).\n let savedSlug = post.slug;\n async function showPreview() {\n showTab(\"preview\");\n try {\n if (!locked) {\n await saveDraft(true);\n }\n previewFrame.srcdoc = await apiText(`/api/posts/${id}/preview`);\n previewFrame.onload = () => {\n try {\n const doc = previewFrame.contentDocument;\n if (doc) {\n openLinksInNewTab(doc);\n // Read on each load, so a slug edited since the last preview is the one\n // matched. The editor holds only an unpublished post: once its send starts, the\n // editor hands off to the send page (and a web version opened after that goes\n // on to the public page itself).\n previewOpensWebVersion(doc, postArchiveUrl(savedSlug), webVersionUrl);\n previewFrame.style.height = `${doc.body.scrollHeight + 24}px`;\n }\n } catch {}\n };\n } catch (e) {\n if (signal.aborted || foundGone(e)) {\n return;\n }\n toast(message(e));\n }\n }\n for (const t of tabs) {\n t.onclick = () => (t.dataset.tab === \"preview\" ? showPreview() : showTab(\"edit\"));\n }\n // A scheduled post opens on Preview: the preview is the copy that will send, and the\n // Edit tab is read-only, so it is somewhere the publisher goes deliberately and finds\n // the way out written on it (DESIGN \u00A73).\n if (locked) {\n showPreview();\n }\n\n // --- the lock's nudge ---\n // A scheduled post refuses edits at the browser (readonly) and at the API (SPEC \u00A76). When\n // one is attempted anyway, the foot's \"Cancel the schedule to edit\" pulses once, so the\n // way out is seen where the keystroke landed rather than announced elsewhere (DESIGN \u00A73).\n const lockFoot = locked ? $(\"#lockFoot\") : null;\n let nudgeTimer: number | undefined;\n function nudge() {\n if (!lockFoot) {\n return;\n }\n lockFoot.classList.remove(\"nudge\");\n requestAnimationFrame(() => lockFoot.classList.add(\"nudge\")); // a frame apart, so a second nudge restarts the pulse\n clearTimeout(nudgeTimer);\n nudgeTimer = setTimeout(() => lockFoot.classList.remove(\"nudge\"), 900);\n }\n\n // --- formatting toolbar ---\n function wrapSel(before: string, after: string, placeholder: string) {\n const s = ta.selectionStart;\n const e = ta.selectionEnd;\n const sel = ta.value.slice(s, e) || placeholder;\n ta.value = ta.value.slice(0, s) + before + sel + after + ta.value.slice(e);\n ta.focus();\n ta.selectionStart = s + before.length;\n ta.selectionEnd = s + before.length + sel.length;\n }\n function prefixLines(prefix: string) {\n const s = ta.selectionStart;\n const e = ta.selectionEnd;\n const start = ta.value.lastIndexOf(\"\\n\", s - 1) + 1;\n const out = (ta.value.slice(start, e) || \"\")\n .split(\"\\n\")\n .map((l) => prefix + l)\n .join(\"\\n\");\n ta.value = ta.value.slice(0, start) + out + ta.value.slice(e);\n ta.focus();\n ta.selectionStart = start;\n ta.selectionEnd = start + out.length;\n }\n function applyFormat(kind: string) {\n if (locked) {\n nudge(); // reached by a toolbar click or a Cmd+B / I / K shortcut\n return;\n }\n showTab(\"edit\");\n if (kind === \"bold\") {\n wrapSel(\"**\", \"**\", \"bold text\");\n } else if (kind === \"italic\") {\n wrapSel(\"*\", \"*\", \"italic text\");\n } else if (kind === \"heading\") {\n prefixLines(\"## \");\n } else if (kind === \"quote\") {\n prefixLines(\"> \");\n } else if (kind === \"ul\") {\n prefixLines(\"- \");\n } else if (kind === \"ol\") {\n prefixLines(\"1. \");\n } else if (kind === \"indent\") {\n prefixLines(\" \");\n } else if (kind === \"link\") {\n wrapSel(\"[\", \"](https://)\", \"link text\");\n } else if (kind === \"code\") {\n const s = ta.selectionStart;\n const e = ta.selectionEnd;\n if (s === e || ta.value.slice(s, e).includes(\"\\n\")) {\n wrapSel(\"```\\n\", \"\\n```\", \"code\");\n } else {\n wrapSel(\"`\", \"`\", \"code\");\n }\n }\n markEdited();\n }\n for (const b of $$<HTMLButtonElement>(\".tb[data-fmt]\", root)) {\n b.onclick = () => applyFormat(b.dataset.fmt ?? \"\");\n }\n ta.addEventListener(\"keydown\", (e) => {\n if (!(e.metaKey || e.ctrlKey)) {\n return;\n }\n const k = e.key.toLowerCase();\n if (k === \"b\") {\n e.preventDefault();\n applyFormat(\"bold\");\n } else if (k === \"i\") {\n e.preventDefault();\n applyFormat(\"italic\");\n } else if (k === \"k\") {\n e.preventDefault();\n applyFormat(\"link\");\n }\n });\n\n // --- unsaved-changes tracking + save ---\n // A snapshot of the last-saved field values; the editor is \"dirty\" whenever the\n // current values differ. We reflect that in a save-status indicator beside the\n // button, guard navigation (at the router, through the handle), and autosave.\n let editorDirty = false;\n // The last save errored, or the draft changed elsewhere (the out-of-date banner is up):\n // either way the leave guard prompts instead of silently flushing.\n let saveFailed = false;\n let conflicted = false;\n // Why the last save failed, in the publisher's words, kept on the save cue until a save\n // succeeds: a toast would be gone in seconds and leave the cue reading like ordinary\n // typing. A failed save tries again on its own, spaced out, so a dropped connection\n // recovers without another edit.\n let saveError: string | null = null;\n let retryDelay = 0;\n let retryArmed = false; // one pending retry at a time, however many saves fail meanwhile\n // The save row is a draft's; a scheduled post renders none.\n const saveBtn = locked ? null : $<HTMLButtonElement>(\"#saveBtn\");\n const saveStatus = locked ? null : $(\"#saveStatus\");\n const dirty = new DirtyTracker(collect);\n let saving = false;\n // The autosave editor's counterpart to the shared save bar's dot: same amber\n // \"unsaved\" cue (dot + text), extended to the autosave lifecycle it actually has \u2014\n // Saving\u2026 while a save is in flight, Saved once it lands. Empty while locked\n // (a scheduled post can't be edited here).\n function renderSaveStatus() {\n if (!saveStatus) {\n return;\n }\n if (locked) {\n saveStatus.className = \"save-status\";\n saveStatus.textContent = \"\";\n } else if (saving) {\n saveStatus.className = \"save-status is-saving\";\n saveStatus.textContent = \"Saving\u2026\";\n } else if (saveError && editorDirty) {\n saveStatus.className = \"save-status is-error\";\n saveStatus.textContent = `Not saved: ${saveError}`;\n } else if (editorDirty) {\n saveStatus.className = \"save-status is-dirty\";\n saveStatus.textContent = \"Unsaved changes\";\n } else {\n saveStatus.className = \"save-status is-saved\";\n saveStatus.textContent = \"Saved\";\n }\n }\n function refreshDirty() {\n editorDirty = dirty.dirty;\n if (!editorDirty) {\n // Back to what was last saved (an undo, say): nothing is unsaved, so no failure stands.\n saveError = null;\n saveFailed = false;\n retryDelay = 0;\n }\n renderSaveStatus();\n }\n renderSaveStatus(); // paint the initial state (Saved on a fresh draft; empty if locked)\n function scheduleAutosave() {\n mine.touch();\n }\n // Called after any edit \u2014 typed, formatted, or an inserted image.\n function markEdited() {\n if (locked) {\n return;\n }\n paintMarkdown(); // repaint the highlight overlay from the new textarea value\n refreshDirty();\n if (!conflicted) {\n scheduleAutosave();\n }\n }\n\n // Saves are chained so an autosave and an explicit save can never overlap; a\n // silent save with nothing pending is skipped.\n let saveChain: Promise<unknown> = Promise.resolve();\n function saveDraft(silent: boolean): Promise<SaveResult> {\n const next = saveChain.catch(() => {}).then(() => doSaveDraft(silent));\n saveChain = next;\n trackSaves(saveChain);\n return next;\n }\n async function doSaveDraft(silent: boolean): Promise<SaveResult> {\n if (silent && !dirty.dirty) {\n return \"unchanged\"; // nothing changed since the last save\n }\n if (conflicted) {\n return \"refused\"; // paused until the out-of-date banner is resolved\n }\n mine.cancel(); // a save is starting \u2014 cancel any pending autosave trigger\n saving = true;\n renderSaveStatus(); // reflect Saving\u2026 right away; the finally re-renders when done\n // What this save sends is what it saves: an edit typed while it is in flight stays\n // dirty and goes with the next one.\n const fields = collect();\n let sent = JSON.stringify(fields);\n const slugShown = slugEl.value;\n try {\n const body: PostEditBody = { ...fields, base_revision: revisions.base };\n const { post: u } = await api<PostSavedResponse>(`/api/posts/${id}`, {\n method: \"PUT\",\n json: body,\n });\n // Show the slug the server gave (derived, numbered if taken), unless the field changed\n // while the save was out: that edit is unsaved, and the next save brings its own answer.\n // Focus alone is no reason to drop it, since tabbing through the field changes nothing;\n // a slug being typed by hand only has it wait until the field is left.\n if (u.slug && slugEl.value === slugShown) {\n // A slug set by hand is sent as typed; a following one is the server's to derive.\n const handSet = \"slug\" in fields;\n if (handSet && document.activeElement === slugEl) {\n held = { shown: slugShown, slug: u.slug }; // being typed by hand: see applyHeld\n } else {\n slugEl.value = u.slug;\n }\n if (handSet) {\n sent = JSON.stringify({ ...fields, slug: u.slug }); // what was saved, not what was typed\n }\n }\n if (u.slug) {\n savedSlug = u.slug;\n }\n revisions.saved(u.current_revision); // our save is now the newest; poll against it\n dirty.markSaved(sent);\n saveFailed = false;\n saveError = null;\n retryDelay = 0;\n if (!silent) {\n toast(\"Saved\");\n }\n return \"saved\";\n } catch (e) {\n // A stale-revision 409 isn't a plain failure: another writer got there first.\n // Surface the out-of-date banner (notify, don't clobber) instead of an error toast.\n const conflict = conflictFromError(e);\n if (conflict) {\n showConflict(conflict);\n return \"refused\";\n }\n if (isNotFound(e)) {\n removed();\n return \"refused\";\n }\n saveFailed = true; // the leave guard now prompts rather than silently flushing\n // A refusal naming the subject or slug is said under that field, and the cue points\n // there; the server will refuse the same value again, so it is not retried.\n const named =\n e instanceof ApiError ? { subject: subjectEl, slug: slugEl }[e.field ?? \"\"] : undefined;\n if (named) {\n fieldError(named, describeError(e));\n saveError = \"fix the field marked above.\";\n } else {\n saveError = describeError(e);\n }\n // Only a failure that can pass on its own is tried again: no answer, the server's\n // own trouble, or too many requests at once.\n const transient = !(e instanceof ApiError) || e.status >= 500 || e.status === 429;\n if (transient && !retryArmed) {\n retryArmed = true;\n retryDelay = retryDelay ? Math.min(retryDelay * 2, 60_000) : 5_000;\n at(\n Date.now() + retryDelay,\n () => {\n retryArmed = false;\n if (saveError && !conflicted) {\n saveDraft(true).catch(() => {}); // a failure says so on the cue again\n }\n },\n signal,\n );\n }\n throw e;\n } finally {\n saving = false;\n refreshDirty();\n }\n }\n if (saveBtn) {\n saveBtn.onclick = () => {\n // While the out-of-date banner is up a save is paused (notify, never clobber: SPEC\n // \u00A74), so the button reads as unavailable and a click (or \u2318S) does not pretend to\n // save: it points at the banner, whose Reload or Keep editing is the next step.\n // aria-disabled, not disabled, so the click still lands to do that.\n if (conflicted) {\n freshnessEl.scrollIntoView({ block: \"nearest\" });\n return;\n }\n busy(saveBtn, \"Saving\u2026\", () => saveDraft(false).catch(() => {})).finally(refreshDirty);\n };\n }\n // Save draft follows the banner: unavailable while it asks for a decision, and says why.\n function paintSaveBtn() {\n if (!saveBtn) {\n return;\n }\n if (conflicted) {\n saveBtn.setAttribute(\"aria-disabled\", \"true\");\n saveBtn.title = \"Saving is paused: choose Reload or Keep editing above\";\n } else {\n saveBtn.removeAttribute(\"aria-disabled\");\n saveBtn.removeAttribute(\"title\");\n }\n }\n\n // Leaving the editor saves in the background instead of prompting. Capture the\n // fields NOW (the mount tears down the DOM right after) and send them through\n // the chain so they can't overlap an in-flight save. Deliberately not bound to the\n // signal: a save the reader started must land whether or not they stayed to watch.\n // A save already in flight (a blur's, the Web version link's) may carry these very\n // edits, so the flush asks again once it lands, and reads the base then: a base taken\n // now would be older than that save, and the server would refuse our own edits as\n // another writer's.\n const leaveFlush = () => {\n mine.cancel();\n if (locked || !dirty.dirty) {\n return;\n }\n const fields = collect();\n const sent = JSON.stringify(fields);\n editorDirty = false;\n saveChain = saveChain\n .catch(() => {})\n .then(async () => {\n if (!dirty.dirty) {\n return;\n }\n const body: PostEditBody = { ...fields, base_revision: revisions.base };\n const { post: u } = await api<PostSavedResponse>(`/api/posts/${id}`, {\n method: \"PUT\",\n json: body,\n });\n revisions.saved(u.current_revision);\n dirty.markSaved(sent);\n })\n .catch((e) => {\n if (isNotFound(e)) {\n // The reader has left, and their edits with the page: say what happened to them.\n toast(\"This post was deleted elsewhere, so your last edits weren\u2019t saved.\");\n return;\n }\n toast(\n e instanceof ApiError && e.status === 409\n ? \"Changed elsewhere, so your edits weren\u2019t saved\"\n : `Couldn\u2019t save your changes: ${message(e)}`,\n );\n });\n trackSaves(saveChain);\n };\n const handle: ViewHandle = {\n dirty: () => editorDirty,\n beforeLeave() {\n if (gone) {\n return \"leave\"; // on its way to Drafts: there is nothing left to save to\n }\n if (editorDirty && (saveFailed || conflicted)) {\n return \"confirm\"; // a silent flush would fail (or clobber): the reader decides\n }\n leaveFlush();\n return \"leave\";\n },\n manualSave() {\n if (saveBtn && !saveBtn.disabled) {\n saveBtn.click();\n }\n },\n };\n\n // Typed edits mark dirty; leaving subject/slug, or Enter in one, flushes promptly. The body is\n // left to the idle/cap timers so a toolbar click (which blurs it) doesn't save\n // on every interaction.\n if (!locked) {\n for (const el of [subjectEl, slugEl, ta]) {\n el.addEventListener(\"input\", markEdited);\n }\n for (const el of [subjectEl, slugEl]) {\n el.addEventListener(\n \"blur\",\n () => saveDraft(true).catch(() => {}) /* the save cue says why */,\n );\n // Enter commits a one-line field the way leaving it does, but not the Enter that ends\n // an IME composition, which Safari reports with isComposing false and keyCode 229.\n el.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" && !e.isComposing && e.keyCode !== 229) {\n e.preventDefault();\n saveDraft(true)\n .then(applyHeld) // Enter commits the slug, so show what it saved as\n .catch(() => {}); // the save cue says why\n }\n });\n }\n }\n\n // --- concurrent-edit detection (SPEC \u00A74) ---\n // Another tab, or Claude through the API, can save this draft while it's open\n // here. A save carries base_revision so the server rejects a stale write (409);\n // a light poll warns before the writer invests more effort. We notify, never\n // adopt: Reload takes the other version, Keep editing keeps yours (your next\n // save overwrites it). Re-arm only on a genuinely newer revision.\n const freshnessEl = $(\"#freshnessBanner\");\n const friendlyAuthor = (a: Author) => (a === \"service\" ? \"Claude\" : a || null);\n\n function clearConflict() {\n conflicted = false;\n paintSaveBtn();\n revisions.clearWarning();\n freshnessEl.hidden = true;\n setHtml(freshnessEl, html``);\n }\n function showConflict(conflict: Conflict) {\n if (locked) {\n return;\n }\n conflicted = true; // pauses autosave; makes the leave guard prompt\n saveError = null; // the banner is the story now; an older failure's reason would contradict it\n paintSaveBtn();\n revisions.noteWarned(conflict);\n if (conflict.kind === \"locked\") {\n setHtml(\n freshnessEl,\n html`<span><span aria-hidden=\"true\">\u26A0\uFE0F</span> This draft was scheduled elsewhere and can no longer be edited here.</span><span class=\"row\"><button type=\"button\" class=\"ghost\" id=\"freshReload\">Reload</button></span>`,\n );\n } else {\n const who = friendlyAuthor(conflict.author);\n setHtml(\n freshnessEl,\n html`<span><span aria-hidden=\"true\">\u26A0\uFE0F</span> This draft was changed elsewhere${who ? html`, last edited by <strong>${who}</strong>` : null}. Saving is paused until you choose: Reload to load that version (discards your unsaved edits), or Keep editing to keep yours, and your next save overwrites that version.</span><span class=\"row\"><button type=\"button\" class=\"ghost\" id=\"freshReload\">Reload</button><button type=\"button\" class=\"ghost\" id=\"freshKeep\">Keep editing</button></span>`,\n );\n }\n freshnessEl.hidden = false;\n $<HTMLButtonElement>(\"#freshReload\", freshnessEl).onclick = () => {\n clearConflict();\n remount();\n };\n const keep = freshnessEl.querySelector<HTMLButtonElement>(\"#freshKeep\");\n if (keep) {\n keep.onclick = () => {\n revisions.adoptWarned(); // the newer revision becomes our base \u2014 our next save wins\n clearConflict();\n refreshDirty();\n if (editorDirty) {\n scheduleAutosave();\n }\n };\n }\n }\n\n // A read or write that found the post gone (a 404) answers it here, whichever control\n // made it: the API names only what it didn't find (\"post\"), which says nothing alone.\n function foundGone(e: unknown): boolean {\n if (!isNotFound(e)) {\n return false;\n }\n removed();\n return true;\n }\n\n if (!locked) {\n // Skipped while hidden, saving, or already warned \u2014 poll GET is cheap and only\n // re-warns on a revision we haven't surfaced yet. Read at once when the tab is shown.\n const pollFreshness = async () => {\n if (saving || conflicted || gone || document.hidden) {\n return;\n }\n const baseAtRequest = revisions.base; // to tell our own save landing mid-poll from another writer's\n try {\n const fresh = await api<PostResponse>(`/api/posts/${id}`, { signal });\n const decision = revisions.decide(\n {\n status: fresh.post.status,\n revision: fresh.post.current_revision,\n author: fresh.author,\n },\n { saving, baseAtRequest },\n );\n if (decision !== \"ignore\" && decision !== \"fresh\") {\n showConflict(decision);\n }\n } catch (e) {\n if (isNotFound(e)) {\n removed();\n }\n // Anything else is transient: try again next tick.\n }\n };\n every(10000, pollFreshness, signal);\n readWhenShown(pollFreshness, signal);\n }\n\n // --- web version: the post as its public page will show it (SPEC \u00A75) ---\n // A plain link, so no browser blocks it as a pop-up. The click saves pending edits and\n // lets the link go: a new tab loads the editor and signs in before it reads the page, so\n // the save is ahead of it; opened in place, the page waits for it (pending_save.ts). A\n // refused or failed save shows here as any save does.\n // Where the link opens in place (a browser pane with no tabs), it is a same-document hash\n // change, so nothing unloads to cut the save off, and the leave flush that follows sees\n // it in flight and doesn't send it twice.\n $<HTMLAnchorElement>(\"#webLink\").addEventListener(\"click\", () => {\n if (!locked) {\n saveDraft(true).catch(() => {}) /* the save cue says why */;\n }\n });\n\n if (locked && scheduled) {\n // The send as last reported: this page's read of it (or, until that succeeds, what the\n // post's read says of it), then each read of the send-state layer that found it\n // changed, whichever client changed it (DESIGN \u00A74).\n let current: BannerSend = sendRead?.send ?? {\n ...scheduled,\n phase: \"scheduled\",\n conditions: [],\n };\n const whenEl = $(\"#schedWhen\");\n const controls = $(\"#schedControls\");\n // --- the banner: the cards' countdown, and the review window closing at the fire time ---\n // Before the fire time it counts down on the clock. From then the server refuses Cancel\n // and Reschedule (SPEC \u00A76), so the same line reads \"Preparing to send\u2026\" in place of the\n // countdown and the buttons stay where they are, disabled: the page hands off to the\n // live send within seconds, so nothing needs to move. Past the server's missed\n // tolerance it says how late the send is, from the server's condition.\n // The switch happens on this page's clock, at the fire time itself (a tick of its own\n // when the fire time is inside the coming second), with no read needed. Before it the\n // buttons are the server's to offer (`actions`); from it the clock only disables what\n // was offered, so a read that answered before the fire time cannot bring them back.\n let painted = \"\";\n let armed = 0; // the fire time an exact tick is set for, so it is set once\n let seenOpen = false; // whether this page showed the window open, and so its buttons\n const reschedBtn = $<HTMLButtonElement>(\"#rescheduleSchedule\");\n const cancelBtn = $<HTMLButtonElement>(\"#cancelSchedule\");\n const offers = (name: SendAction[\"name\"]) =>\n current.actions === undefined || can({ actions: current.actions }, name);\n const paintBanner = () => {\n const now = Date.now();\n const due = current.phase === \"due\" || now >= current.fire_at;\n if (!due) {\n seenOpen = true;\n reschedBtn.hidden = !offers(\"reschedule\");\n cancelBtn.hidden = !offers(\"cancel\");\n } else if (!seenOpen) {\n // Opened after the window closed: there is nothing to show disabled.\n reschedBtn.hidden = true;\n cancelBtn.hidden = true;\n }\n reschedBtn.disabled = due;\n cancelBtn.disabled = due;\n controls.hidden = reschedBtn.hidden && cancelBtn.hidden;\n const markup = scheduledBannerHtml(current, due);\n const key = String(markup);\n if (key !== painted) {\n painted = key;\n setHtml(whenEl, markup);\n }\n if (!due && current.fire_at - now <= 1000 && current.fire_at !== armed) {\n armed = current.fire_at;\n at(current.fire_at, paintBanner, signal);\n }\n };\n paintBanner();\n every(1000, paintBanner, signal);\n\n // --- re-made since its last test (SPEC \u00A78): the server's condition, in the banner ---\n // A template or identity change re-made the frozen email, so an earlier test no longer\n // shows what will go out. The server says so until a test of the re-made copy lands,\n // the same fact Claude reads. The banner only says so: Send test email is in the actions\n // bar below, and a second one here would crowd the banner's own controls.\n const remadeEl = $(\"#schedRemade\");\n const paintRemade = () => {\n const remade = conditionOf(current, \"remade\");\n remadeEl.hidden = !remade;\n $(\"#schedRemadeMsg\").textContent = remade?.message ?? \"\";\n };\n paintRemade();\n\n // --- following the send (DESIGN \u00A74) ---\n // A move, a re-make, or a cancel made elsewhere (by Claude or another tab) shows in\n // place, and when the send starts this page hands off to its send page, which is the\n // send's home from then on, saying why the page changed.\n const follow = (from: SendResponse) =>\n followSend(\n from,\n {\n update(send: SendView, change: StageChange | null) {\n current = send;\n if (change?.to === \"sending\" || change?.to === \"sent\" || change?.to === \"complete\") {\n toast(\"Your send started, so this page switched to its live watch.\");\n handOff(`#/sent/${send.id}`);\n return;\n }\n if (change?.to === \"canceled\") {\n toast(\"The schedule was canceled elsewhere, so this post is a draft again.\");\n remount();\n return;\n }\n paintBanner();\n paintRemade();\n },\n removed,\n // The layer's cursor ran ahead of the database: read the post and its send again.\n stale: remount,\n },\n signal,\n );\n if (sendRead) {\n follow(sendRead);\n } else {\n // The send's read failed: try it again, spaced out, and follow from the first that\n // answers. A failure throws, which the poll takes as \"try again\".\n poll(\n 5000,\n async () => {\n const read = await api<SendResponse>(`/api/sends/${scheduled.id}`, { signal });\n if (!movedOn(read.send, remount)) {\n current = read.send;\n paintBanner();\n paintRemade();\n follow(read);\n }\n return false;\n },\n signal,\n );\n }\n\n // --- reschedule (from the scheduled banner): move the fire time, content stays frozen ---\n reschedBtn.onclick = () => openRescheduleModal(current.id, current.fire_at, remount, signal);\n\n // --- cancel schedule (from the scheduled banner) ---\n cancelBtn.onclick = () =>\n busy(cancelBtn, \"Canceling\u2026\", async () => {\n try {\n await api(`/api/sends/${scheduled.id}/cancel`, { method: \"POST\" });\n toast(\"Schedule canceled\");\n remount();\n } catch (e) {\n if (isNotFound(e)) {\n removed(); // the send went with its post before the follow noticed\n } else if (e instanceof ApiError && e.code === \"window_closed\") {\n // The fire time passed while the cancel was out, so the email is going out. The\n // server's clock has closed the window even if this page's has not, so the banner\n // reads as due, and the follow, read at once, hands off to the live watch when\n // the send starts.\n current = { ...current, phase: \"due\" };\n toast(\"Too late to cancel: the send time has passed, so this email is going out.\");\n readSendsNow();\n } else {\n toast(message(e));\n }\n }\n // `busy` hands the button back when it ends; a cancel still answering at the fire\n // time must not re-enable it, so the clock's verdict is painted again at once.\n }).finally(paintBanner);\n }\n\n // --- image upload: drag/drop, paste, click ---\n async function uploadAndInsert(file: File) {\n if (!file.type.startsWith(\"image/\")) {\n return;\n }\n try {\n const fd = new FormData();\n fd.append(\"file\", file);\n const { image } = await api<ImageUploadResponse>(`/api/posts/${id}/images`, {\n method: \"POST\",\n body: fd,\n });\n const s = ta.selectionStart;\n const snippet = `\\n\\n`;\n ta.value = ta.value.slice(0, s) + snippet + ta.value.slice(s);\n ta.selectionStart = ta.selectionEnd = s + snippet.length;\n markEdited();\n toast(\"Image added\");\n } catch (e) {\n if (!foundGone(e)) {\n toast(message(e));\n }\n }\n }\n const composerBody = $(\"#composerBody\");\n if (!locked) {\n const imgInput = $<HTMLInputElement>(\"#imgInput\");\n const foot = $(\"#dropFoot\");\n composerBody.addEventListener(\"dragover\", (e) => {\n e.preventDefault();\n composerBody.classList.add(\"dragover\");\n });\n composerBody.addEventListener(\"dragleave\", (e) => {\n if (e.target === composerBody) {\n composerBody.classList.remove(\"dragover\");\n }\n });\n composerBody.addEventListener(\"drop\", (e) => {\n e.preventDefault();\n composerBody.classList.remove(\"dragover\");\n showTab(\"edit\");\n for (const f of e.dataTransfer?.files ?? []) {\n uploadAndInsert(f);\n }\n });\n ta.addEventListener(\"paste\", (e) => {\n for (const it of e.clipboardData?.items || []) {\n if (it.type.startsWith(\"image/\")) {\n const f = it.getAsFile();\n if (f) {\n e.preventDefault();\n uploadAndInsert(f);\n }\n }\n }\n });\n foot.onclick = () => imgInput.click();\n imgInput.onchange = () => {\n for (const f of imgInput.files ?? []) {\n uploadAndInsert(f);\n }\n imgInput.value = \"\";\n };\n } else {\n // Locked: a key that would change the text, a paste, or a drop is refused (readonly\n // does the refusing; a drop is stopped from opening the file) and nudges the foot.\n // Navigation, selection, and copy keys pass, so reading stays free.\n const wouldEdit = (e: KeyboardEvent) => {\n if (e.metaKey || e.ctrlKey) {\n return [\"x\", \"z\", \"y\"].includes(e.key.toLowerCase()); // cut, undo, redo (paste fires its own event)\n }\n return e.key.length === 1 || e.key === \"Enter\" || e.key === \"Backspace\" || e.key === \"Delete\";\n };\n const fields: HTMLElement[] = [ta, subjectEl, slugEl];\n for (const f of fields) {\n f.addEventListener(\"keydown\", (e) => {\n if (wouldEdit(e)) {\n nudge();\n }\n });\n f.addEventListener(\"paste\", nudge);\n }\n composerBody.addEventListener(\"dragover\", (e) => e.preventDefault());\n composerBody.addEventListener(\"drop\", (e) => {\n e.preventDefault();\n nudge();\n });\n }\n\n const warningsEl = $(\"#warnings\");\n function showWarnings(ws: string[] | null | undefined) {\n setHtml(\n warningsEl,\n ws?.length\n ? html`<div class=\"warnings\"><strong>Warnings:</strong> ${ws.join(\"; \")}</div>`\n : html``,\n );\n }\n\n // --- send test (modal) ---\n // Pre-fills from the default test recipients (Settings) and accepts\n // several \u2014 one per line. Each address is a separate test send through the same\n // per-recipient path as a real send (I5).\n $<HTMLButtonElement>(\"#testBtn\").onclick = () => {\n // A scheduled post's test is its frozen copy, exactly as it will fire (SPEC \u00A75);\n // the dialog is where that is said (DESIGN \u00A73).\n const lead = locked\n ? \"Delivers the frozen copy that will send, exactly as it will fire, so you can check it in a client. One address per line.\"\n : \"Delivers the rendered email to real inboxes so you can check it in a client. One address per line.\";\n // While no provider is connected the server would refuse a test, so the dialog leaves\n // out the recipients and keeps only the standing line (DESIGN \u00A74).\n const notConnected = providerNotConnected();\n const m = modal(\n html`<h3>Send a test</h3><p class=\"hint\">${lead}</p>${notConnectedTestLine()}${notConnected ? null : html`<label for=\"testTo\">Recipients</label><textarea id=\"testTo\" rows=\"3\" placeholder=\"you@example.com\"></textarea><p class=\"hint\" id=\"testDefaultsHint\" hidden></p>`}<div class=\"actions\"><button type=\"button\" id=\"tCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"tGo\">Send test</button></div>`,\n signal,\n );\n const go = $<HTMLButtonElement>(\"#tGo\", m.el);\n $(\"#tCancel\", m.el).onclick = m.close;\n go.disabled = notConnected;\n if (notConnected) {\n $(\"#tCancel\", m.el).focus();\n return;\n }\n const to = $<HTMLTextAreaElement>(\"#testTo\", m.el);\n to.focus();\n // Pre-fill with saved defaults (don't clobber anything already typed).\n api<SettingsResponse>(\"/api/settings\")\n .then((s) => {\n const defaults = s.settings.testRecipients || [];\n if (defaults.length && !to.value.trim()) {\n to.value = defaults.join(\"\\n\");\n m.clearError(); // an empty-field error raised before the defaults arrived no longer holds\n const hint = $(\"#testDefaultsHint\", m.el);\n hint.textContent = \"Pre-filled from your default test recipients (Settings).\";\n hint.hidden = false;\n }\n })\n .catch(() => {});\n go.onclick = () =>\n busy(go, \"Sending\u2026\", async () => {\n m.clearError();\n const { valid: addrs, invalid } = parseAddresses(to.value);\n const notAddresses = invalidAddressesMessage(invalid);\n if (notAddresses) {\n m.error(notAddresses, to);\n return;\n }\n if (!addrs.length) {\n m.error(\"Enter at least one email address\", to);\n return;\n }\n try {\n if (!locked) {\n await saveDraft(true);\n }\n // One request per address, each on its own, so one that fails doesn't stop the\n // rest, and the dialog can name exactly the ones to try again.\n let sent = 0;\n const failed: string[] = [];\n const reasons = new Set<string>(); // each distinct reason once, in the order met\n let lastWarnings: string[] | null = null;\n for (const addr of addrs) {\n try {\n const r = await api<TestSendResponse>(`/api/posts/${id}/test`, {\n method: \"POST\",\n json: { to: addr },\n });\n if (r.sent) {\n sent++;\n } else {\n failed.push(addr);\n }\n lastWarnings = r.warnings;\n } catch (e) {\n // A post that's gone, or a session that ended, ends the loop: every address\n // after it would fail the same way.\n if (isNotFound(e) || (e instanceof ApiError && e.status === 401)) {\n throw e;\n }\n failed.push(addr);\n reasons.add(describeError(e));\n }\n }\n showWarnings(lastWarnings);\n // A test of a scheduled post settles its re-made condition on the server; read the\n // feed now so the banner's line goes with it rather than at the next idle read.\n if (locked && sent > 0) {\n readSendsNow();\n }\n if (failed.length) {\n // The dialog stays open with only the addresses that didn't get it, so Send again\n // tries those and no one gets a second copy.\n to.value = failed.join(\"\\n\");\n m.error(notSentMessage(sent, addrs.length, failed, [...reasons]));\n return;\n }\n m.close();\n toast(withNoProviderNote(`Test sent to ${sent} address${sent === 1 ? \"\" : \"es\"}`));\n } catch (e) {\n if (isNotFound(e)) {\n m.close();\n foundGone(e);\n return;\n }\n m.error(message(e));\n }\n });\n };\n\n // --- schedule (the one send entry point; Send now lives inside as a demoted link) ---\n // Scheduling behind a cancelable review window is the literal default (SPEC \u00A76);\n // sending immediately is the deliberate sub-choice. Both server flows are unchanged\n // (/schedule, /send) \u2014 this is one modal with two views, so the safer path is what\n // the Primary opens and the louder one is a step down.\n const scheduleBtn = locked ? null : $<HTMLButtonElement>(\"#scheduleBtn\");\n if (scheduleBtn) {\n scheduleBtn.onclick = () => {\n if (!validateSubject()) {\n return; // empty subject: the field-level error is now showing; don't open the modal\n }\n const minStr = toLocalInput(earliestFireAt());\n const def = toLocalInput(new Date(Date.now() + 24 * 3600 * 1000));\n // While no provider is connected the server would refuse both views, so the dialog\n // drops the picker and the Send now row and leaves only the standing line (DESIGN \u00A74).\n const notConnected = providerNotConnected();\n const scheduleView = html`<h3 id=\"schHead\">Schedule this post</h3><p class=\"hint\">It sends at the time you pick (at least ${minLeadText()} out), with a cancelable window until then.</p>${notConnectedScheduleLine()}${notConnected ? null : html`<label for=\"schWhen\">Send at</label><input type=\"datetime-local\" id=\"schWhen\" min=\"${minStr}\" value=\"${def}\">`}<div class=\"actions\"><button type=\"button\" id=\"schCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"schGo\">Schedule</button></div>${notConnected ? null : html`<div class=\"altrow\"><span class=\"altrow-note\">Need it sooner? The soonest is ${minLeadText()} out, still cancelable.</span><button type=\"button\" class=\"linkbtn\" id=\"toSendNow\">Send now \u2192</button></div>`}`;\n const m = modal(scheduleView, signal);\n const box = $(\".modal\", m.el);\n // A refused save closes the dialog, and the out-of-date banner it was covering is\n // the whole answer \u2014 so make sure the publisher is actually looking at it: they\n // clicked Schedule at the foot of the post and the banner sits at its head. Only\n // here, never in showConflict: the freshness poll raises the same banner on its own\n // every ten seconds, and scrolling the page out from under someone mid-sentence to\n // announce it would be its own bug. `nearest` leaves an already-visible banner alone.\n const refused = () => {\n m.close();\n freshnessEl.scrollIntoView({ block: \"nearest\" });\n };\n\n const doSchedule = () =>\n busy($<HTMLButtonElement>(\"#schGo\", box), \"Scheduling\u2026\", async () => {\n m.clearError();\n const whenEl = $<HTMLInputElement>(\"#schWhen\", box);\n const t = whenEl.value ? new Date(whenEl.value).getTime() : Number.NaN;\n if (Number.isNaN(t)) {\n m.error(\"Pick a valid date & time\", whenEl);\n return;\n }\n if (isTooSoon(t)) {\n m.error(tooSoonText(true), whenEl);\n return;\n }\n try {\n // A refused save means the server holds another writer's newer revision, so\n // scheduling now would freeze content the publisher never saw (SPEC \u00A76).\n // Close the dialog and let the out-of-date banner behind it be the next step.\n if ((await saveDraft(true)) === \"refused\") {\n refused();\n return;\n }\n await api<ScheduleResponse>(`/api/posts/${id}/schedule`, {\n method: \"POST\",\n json: { fire_at: new Date(t).toISOString() },\n });\n m.close();\n // Stay on the post, re-rendered in its scheduled state: the window is the\n // review and this page is the review surface (SPEC \u00A76, DESIGN \u00A73).\n toast(withNoProviderNote(`Scheduled for ${fmt(t)}. Send yourself a test.`));\n remount();\n } catch (e) {\n if (isNotFound(e)) {\n m.close();\n foundGone(e);\n return;\n }\n // The server's clock is the authority on the lead; its refusal is said the same\n // way as the editor's own check, under the picker.\n if (e instanceof ApiError && e.field === \"fire_at\") {\n m.error(\n e.code === \"fire_at_too_soon\" ? tooSoonText(true) : message(e),\n $<HTMLInputElement>(\"#schWhen\", box),\n );\n return;\n }\n m.error(message(e));\n }\n });\n\n const doSendNow = () =>\n busy($<HTMLButtonElement>(\"#snGo\", box), \"Queuing\u2026\", async () => {\n m.clearError();\n try {\n // As in doSchedule: a refused save must not become a frozen send.\n if ((await saveDraft(true)) === \"refused\") {\n refused();\n return;\n }\n // The toast names the time the server answered, not the lead: the fire time is\n // rounded up to the minute (SPEC \u00A76), so it can be up to a minute past the lead.\n const { send } = await api<ScheduleResponse>(`/api/posts/${id}/send`, {\n method: \"POST\",\n });\n m.close();\n toast(withNoProviderNote(`Sends at ${fmt(send.fire_at)}, cancelable until then.`));\n remount();\n } catch (e) {\n if (isNotFound(e)) {\n m.close();\n foundGone(e);\n return;\n }\n m.error(message(e));\n }\n });\n\n function wireSchedule() {\n box.setAttribute(\"aria-labelledby\", \"schHead\");\n $(\"#schCancel\", box).onclick = m.close;\n $(\"#schGo\", box).onclick = doSchedule;\n // The server would refuse a schedule; the standing line above says why and where to\n // fix it, and the picker and Send now row are left out of the view.\n $<HTMLButtonElement>(\"#schGo\", box).disabled = notConnected;\n if (notConnected) {\n $(\"#schCancel\", box).focus();\n return;\n }\n $(\"#toSendNow\", box).onclick = showSendNow;\n $(\"#schWhen\", box).focus();\n }\n\n async function showSendNow() {\n setHtml(\n box,\n html`<h3 id=\"snHead\">Send now?</h3><p class=\"hint\">Freezes the current draft and sends it to <strong id=\"snWho\">your confirmed subscribers</strong> after a cancelable window of at least ${minLeadText()}, on the minute. You can cancel until it fires.</p><div class=\"altrow altrow-top\"><button type=\"button\" class=\"linkbtn\" id=\"toSchedule\">\u2190 Back to schedule</button></div><div class=\"actions\"><button type=\"button\" id=\"snCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"snGo\">Send now</button></div>`,\n );\n box.setAttribute(\"aria-labelledby\", \"snHead\");\n $(\"#snCancel\", box).onclick = m.close;\n $(\"#snGo\", box).onclick = doSendNow;\n $<HTMLButtonElement>(\"#snGo\", box).disabled = providerNotConnected();\n $(\"#toSchedule\", box).onclick = () => {\n setHtml(box, scheduleView);\n wireSchedule();\n };\n $(\"#snGo\", box).focus();\n // Fill the count once known; the copy reads sensibly until then. A send reaches the\n // confirmed addresses that aren't suppressed (SPEC \u00A76), so ask the server for that set's\n // total rather than the confirmed count, which still includes the suppressed ones.\n try {\n const s = await api<SubscriberListResponse>(\n \"/api/subscribers?status=confirmed&suppressed=hide&limit=1\",\n { signal },\n );\n const n = s.page.total;\n const whoEl = box.querySelector(\"#snWho\"); // gone if the view flipped back meanwhile\n if (whoEl) {\n whoEl.textContent = `${n} confirmed subscriber${n === 1 ? \"\" : \"s\"}`;\n }\n } catch {}\n }\n\n wireSchedule();\n };\n }\n return handle;\n}\n\n/** What the scheduled banner reads of its send. `actions` is absent only until the send's\n * own read answers (the post's read does not carry them). */\ntype BannerSend = Pick<SendView, \"id\" | \"fire_at\" | \"phase\" | \"conditions\" | \"remade_at\"> &\n Partial<Pick<SendView, \"actions\">>;\n\n/**\n * Whether the send has moved on from scheduled, found on the editor's own read of it: once\n * it has started its send page is its home, and once canceled its post is a draft again,\n * which a fresh read of the post shows.\n */\nfunction movedOn(send: SendView, remount: () => void): boolean {\n if (send.status === \"sending\" || send.status === \"sent\") {\n handOff(`#/sent/${send.id}`);\n return true;\n }\n if (send.status === \"canceled\") {\n remount();\n return true;\n }\n return false;\n}\n\n/**\n * The scheduled banner's words for where its send stands (DESIGN \u00A74), in the cards' own\n * formats: the fire time and a countdown while the window is open; from the fire time, the\n * same line with \"Preparing to send\u2026\" in place of the countdown; past the server's missed\n * tolerance, how late it is, in the danger tone.\n */\nfunction scheduledBannerHtml(send: BannerSend, due: boolean): Html {\n if (has(send, \"missed\")) {\n return html`Scheduled for <strong>${fmt(send.fire_at)}</strong> \u00B7 <span class=\"countdown countdown-missed\">${lateStr(send.fire_at)}</span>`;\n }\n if (due) {\n return html`Scheduled for <strong>${fmt(send.fire_at)}</strong> \u00B7 <span class=\"countdown\">${untilStr(send.fire_at, true)}</span>`;\n }\n return html`Scheduled for <strong>${fmt(send.fire_at)}</strong> \u00B7 <span class=\"countdown\">${untilStr(send.fire_at)}</span> \u00B7 cancelable until then.`;\n}\n\n/** Whether a read or write found its post (or its send, which goes with it) gone. */\nfunction isNotFound(e: unknown): boolean {\n return e instanceof ApiError && e.status === 404;\n}\n\n/** Where a post's send now lives, when that is no longer the editor: the watch while it\n * sends, the record once it has sent. */\nfunction sendHome(data: PostResponse): string | null {\n if (data.sending) {\n return `#/sent/${data.sending.id}`;\n }\n if (data.post.status === \"sent\") {\n return data.sent ? `#/sent/${data.sent.id}` : \"#/sent\";\n }\n return null;\n}\n\n/**\n * Move to a send's new home in place of this page's history entry (DESIGN \u00A74): the\n * editor URL the reader opened has become the send page, so Back goes where they came\n * from rather than to an editor that would only hand them forward again.\n */\nfunction handOff(hash: string): void {\n location.replace(hash);\n}\n\n// A tab hidden while a poll ticks skips those ticks; shown again, it reads at once rather\n// than at the next tick, since anything may have happened meanwhile.\nfunction readWhenShown(read: () => unknown, signal: AbortSignal): void {\n document.addEventListener(\n \"visibilitychange\",\n () => {\n if (!document.hidden) {\n read();\n }\n },\n { signal },\n );\n}\n", "// The in-app docs room: the setup guide fetched from the authed /api/docs routes.\n\nimport type { DocFragment, DocsLanding, DocsResponse } from \"../../shared/docs\";\nimport { api, describeError } from \"../api\";\nimport { mount } from \"../lifecycle\";\nimport { appState } from \"../state\";\nimport { $, $$ } from \"../ui/dom\";\nimport { type Html, html, setHtml, unsafeHtml } from \"../ui/html\";\nimport { renderError, toast } from \"../ui/widgets\";\nimport { roomShell } from \"./shell\";\n\n// The setup guide, authored in docs/README.md and its section folders, and served read-only by the authed\n// GET /api/docs route as sanitized HTML fragments. `#/docs` is the index \u2014 an intro over the\n// guide's sections, the main path numbered as steps; `#/docs/:slug` is one doc, and\n// `#/docs/:slug/:anchor` one heading on it; its rail a back-link to the index\n// plus that doc's \"On this page\" (never a tree of all docs). No iframe: the content is\n// trusted (repo markdown, hygiene-passed), so injecting the fragments is safe. Fetched once\n// and cached (the bundle never changes at runtime), so paging is instant.\nlet docsCache: DocsResponse | null = null;\n\n// A doc card's blurb on the index: the doc's own first paragraph, condensed. Derived here\n// (docs carry no front-matter description) so it stays in sync with the doc itself.\nfunction docDescription(doc: DocFragment): string {\n const tmp = document.createElement(\"div\");\n // The API's rendered HTML, parsed only to read its first paragraph; never attached.\n setHtml(tmp, unsafeHtml(doc.html || \"\"));\n const raw = (tmp.querySelector(\"p\")?.textContent || \"\").trim().replace(/\\s+/g, \" \");\n // A paragraph that leads into a list ends in \":\" \u2014 on a card that reads as a cut-off\n // sentence, so it ends in an ellipsis like a truncated blurb does.\n const leadsIn = /[:;,]$/.test(raw);\n const text = raw.replace(/[\\s:;,]+$/, \"\");\n const MAX = 150;\n if (text.length > MAX) {\n return `${text.slice(0, MAX).replace(/[\\s.,;:]+\\S*$/, \"\")}\u2026`;\n }\n return leadsIn ? `${text}\u2026` : text;\n}\n\nexport async function renderDocs(\n slug: string | undefined,\n root: HTMLElement,\n signal: AbortSignal,\n anchor?: string,\n): Promise<void> {\n setHtml(\n root,\n roomShell(\"docs\", null, html`<div class=\"docs-index\"><p class=\"muted\">Loading\u2026</p></div>`),\n );\n if (!docsCache) {\n // The first visit fetches (with the mount's signal, so a tap on API or a different\n // doc while it is in flight cuts it off rather than let it paint a stale room).\n try {\n docsCache = await api<DocsResponse>(\"/api/docs\", { signal });\n } catch (e) {\n if (!signal.aborted) {\n renderError($(\".room-main\", root), describeError(e), () =>\n mount((r, s) => renderDocs(slug, r, s, anchor)),\n );\n }\n return;\n }\n }\n const { landing, docs } = docsCache;\n if (!docs.length) {\n setHtml($(\".room-main\"), html`<p class=\"muted\">No documentation.</p>`);\n return;\n }\n if (slug) {\n renderDocPage(root, landing, docs, slug, anchor ? decodeAnchor(anchor) : undefined);\n } else {\n renderDocsIndex(root, landing, docs);\n }\n}\n\n/** The route's anchor segment as the heading's anchor. A malformed escape in a hand-edited\n * link opens the page at its top rather than failing the room. */\nfunction decodeAnchor(anchor: string): string | undefined {\n try {\n return decodeURIComponent(anchor);\n } catch {\n return undefined;\n }\n}\n\n// The index: the landing page `docs/README.md` lays out (its title, intro, and sections),\n// with a card per doc. A section the README lists as numbered shows numbers, since its pages\n// are steps taken in order; the others are picked from. The cards' titles and blurbs come\n// from the docs themselves. It keeps the\n// room's two-column shape (a doc page's rail is that doc's \"On this page\"); here the rail\n// holds the project's external links \u2014 the reference room is about Kestrel itself, so this\n// is where getkestrel.dev and the source live.\nfunction renderDocsIndex(root: HTMLElement, landing: DocsLanding, docs: DocFragment[]): void {\n const card = (d: DocFragment, n: number | null): Html => {\n const desc = docDescription(d);\n return html`<li><a class=\"doc-card\" href=\"#/docs/${d.slug}\">${n === null ? null : html`<span class=\"doc-card-n\">${String(n).padStart(2, \"0\")}</span>`}<span class=\"doc-card-main\"><span class=\"doc-card-t\">${d.title} <span class=\"doc-card-go\" aria-hidden=\"true\">\u2192</span></span>${desc ? html`<span class=\"doc-card-d\">${desc}</span>` : null}</span></a></li>`;\n };\n const sections = landing.sections.map((sec) => {\n const inSection = docs.filter((d) => d.section === sec.id);\n if (!inSection.length) {\n return null;\n }\n const cards = inSection.map((d, i) => card(d, sec.numbered ? i + 1 : null));\n // The API's rendered HTML: the README's own blurb, hygiene-passed by the Worker.\n const blurb = sec.blurb\n ? html`<div class=\"doc-section-blurb\">${unsafeHtml(sec.blurb)}</div>`\n : null;\n return html`<section class=\"doc-section\"><h2 class=\"doc-section-t\">${sec.title}</h2>${blurb}${sec.numbered ? html`<ol class=\"doc-cards\">${cards}</ol>` : html`<ul class=\"doc-cards\">${cards}</ul>`}</section>`;\n });\n const main = html`<div class=\"docs-index\"><p class=\"eyebrow\">Documentation</p><h1>${landing.title}</h1><div class=\"docs-index-intro\">${unsafeHtml(landing.intro)}</div>${sections}</div>`;\n // Three uniform out-links under a \"Kestrel\" label \u2014 the same shape as the API rail's\n // label + tiers, so mobile can give both the same chip row. getkestrel.dev is the\n // project's home, the same for every instance, so it's a constant; the source and\n // license links are the deploy's own repo (package.json \u2192 build stamp), so they're\n // absent when no repo is known.\n // Each link carries a short form for the mobile chip row (styles.css swaps which span\n // shows); the full label stays the accessible name at every width.\n const repoUrl = appState.appConfig?.deployment.build.repoUrl || \"\";\n const out = (href: string, label: string, short = label): Html =>\n html`<a class=\"rail-link\" href=\"${href}\" target=\"_blank\" rel=\"noopener\" aria-label=\"${label}\"><span class=\"rail-link-full\">${label}</span><span class=\"rail-link-short\" aria-hidden=\"true\">${short}</span> <span aria-hidden=\"true\">\u2197</span></a>`;\n const rail = html`<div class=\"toc-label\">Kestrel</div>${out(\"https://getkestrel.dev\", \"Project site\", \"Project\")}${\n repoUrl\n ? [out(repoUrl, \"Source on GitHub\", \"Source\"), out(`${repoUrl}/blob/HEAD/LICENSE`, \"License\")]\n : null\n }`;\n setHtml(root, roomShell(\"docs\", rail, main));\n window.scrollTo(0, 0);\n}\n\n// One doc, deep-linked by slug. The rail is this doc's \"On this page\" (scroll-spy-tracked)\n// \u2014 never a list of the other docs. Sequential Prev/Next sits on the title line (compact)\n// and at the foot of the article (with titles), not in the rail.\n//\n// The rail folds on mobile: \"On this page\" is a <details> that is open on desktop (where\n// the summary is inert \u2014 it just looks like the label) and closed on mobile, where it is\n// one tappable row above the article and closes again once a section is picked. The state\n// follows the layout, not the width at render time: crossing 720px (a rotation, a resized\n// window) re-opens it on desktop \u2014 where nothing else could, the summary being inert \u2014\n// and takes the summary out of the tab order there, so a keyboard user can't collapse a\n// list no pointer can reopen. One listener for the app's lifetime, registered by the first\n// doc page rendered (the view's own, so not boot's); it finds the fold that is in the DOM,\n// if any.\nconst mobileMq = matchMedia(\"(max-width: 720px)\");\nfunction syncFold(fold: HTMLDetailsElement | null): void {\n if (!fold) {\n return;\n }\n const mobile = mobileMq.matches;\n fold.open = !mobile;\n $(\"summary\", fold).tabIndex = mobile ? 0 : -1;\n}\n// The fold is only in the DOM on a doc page, so a nullable lookup is the right one here.\nconst findFold = () => document.querySelector<HTMLDetailsElement>(\"details#tocOnPage\");\nlet foldFollowsLayout = false;\nfunction followLayout(): void {\n if (foldFollowsLayout) {\n return;\n }\n foldFollowsLayout = true;\n mobileMq.addEventListener(\"change\", () => syncFold(findFold()));\n}\n\ninterface DocSection {\n id: string;\n /** The heading's anchor, the last segment of its route; empty for the title. */\n anchor: string;\n title: string;\n}\n\n/** A heading's anchor as GitHub derives it from the heading's text: lowercased, with\n * punctuation dropped and each space a hyphen. The guide is read on GitHub too, so a link\n * written against GitHub's anchors (`05-verify.md#7-read-the-logs`) works here unchanged. */\nexport function headingAnchor(text: string): string {\n return text\n .trim()\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}\\s_-]/gu, \"\")\n .replace(/\\s/g, \"-\");\n}\n\n// Element ids carry a prefix, so a heading can never take an id the shell already uses.\nconst headingId = (anchor: string): string => `doc-h-${anchor}`;\n\nfunction renderDocPage(\n root: HTMLElement,\n landing: DocsLanding,\n docs: DocFragment[],\n slug: string,\n anchor?: string,\n): void {\n const at = docs.findIndex((d) => d.slug === slug);\n const cur = docs[at];\n if (!cur) {\n // A stale or renamed deep link shouldn't masquerade as a doc \u2014 heal to the index.\n toast(`No doc named \u201C${slug}\u201D, so here\u2019s the index.`);\n history.replaceState(history.state, \"\", \"#/docs\");\n renderDocsIndex(root, landing, docs);\n return;\n }\n // Prev/Next stay within the doc's section: the main path ends at its last step rather than\n // running on into the guides after it, which are picked from, not read in order.\n const siblings = docs.filter((d) => d.section === cur.section);\n const pos = siblings.indexOf(cur);\n const prev = siblings[pos - 1];\n const next = siblings[pos + 1];\n // Compact Prev/Next on the title line, right of the H1 \u2014 no titles (the foot pager\n // carries those; each link's aria-label names its target). On mobile the words drop\n // and the arrows alone remain, so the row still fits beside a wrapping title.\n const topLink = (doc: DocFragment, dir: \"Previous\" | \"Next\"): Html =>\n html`<a href=\"#/docs/${doc.slug}\" aria-label=\"${dir}: ${doc.title}\">${\n dir === \"Previous\"\n ? html`<span aria-hidden=\"true\">\u2190</span><span class=\"doc-topnav-word\">Previous</span>`\n : html`<span class=\"doc-topnav-word\">Next</span><span aria-hidden=\"true\">\u2192</span>`\n }</a>`;\n const topNav =\n prev || next\n ? html`<nav class=\"doc-topnav\" aria-label=\"Adjacent docs\">${prev ? topLink(prev, \"Previous\") : null}${next ? topLink(next, \"Next\") : null}</nav>`\n : null;\n // The rail gets a slot, filled once the sections are known (below) \u2014 the slot, not the\n // whole rail, so the build stamp roomShell set under it stays.\n setHtml(\n root,\n roomShell(\n \"docs\",\n html`<div id=\"docToc\"></div>`,\n html`<article class=\"doc\" id=\"docsMain\"></article>`,\n ),\n );\n const navEl = $(\"#docToc\");\n const mainEl = $(\"#docsMain\");\n // The API's rendered HTML: the repo's own Markdown, hygiene-passed by the Worker.\n setHtml(\n mainEl,\n html`<section class=\"doc-part\" id=\"doc-${cur.slug}\">${unsafeHtml(cur.html)}</section>`,\n );\n\n // The fragment carries no ids \u2014 give every heading GitHub's anchor for it (a repeat gets\n // GitHub's -1, -2), and collect the H1 and H2s for \"On this page\". The H1 leads so\n // there's a way back to the top.\n const sec = $(\"section.doc-part\", mainEl);\n const seen = new Map<string, number>();\n for (const h of $$<HTMLHeadingElement>(\"h1, h2, h3, h4\", sec)) {\n const base = headingAnchor(h.textContent || \"\");\n const n = seen.get(base) ?? 0;\n seen.set(base, n + 1);\n h.dataset.anchor = n ? `${base}-${n}` : base;\n h.id = headingId(h.dataset.anchor);\n }\n const h1 = sec.querySelector(\"h1\");\n const sections: DocSection[] = [];\n if (h1) {\n sections.push({ id: h1.id, anchor: \"\", title: h1.textContent || cur.title });\n }\n // Put the H1 and the compact pager on one line: wrap the fragment's own H1 in a title\n // row and set the pager beside it (the H1 stays the doc's H1 \u2014 same node, same id).\n if (topNav) {\n const head = document.createElement(\"div\");\n head.className = \"doc-head\";\n setHtml(head, topNav);\n if (h1) {\n h1.before(head);\n head.prepend(h1);\n } else {\n sec.prepend(head);\n }\n }\n for (const h2 of $$<HTMLHeadingElement>(\"h2\", sec)) {\n sections.push({ id: h2.id, anchor: h2.dataset.anchor || \"\", title: h2.textContent || \"\" });\n }\n\n // Previous / Next at the foot \u2014 with titles, the sequential path through the guide.\n const pager = document.createElement(\"nav\");\n pager.className = \"doc-pager\";\n setHtml(\n pager,\n html`${\n prev\n ? html`<a class=\"doc-pager-btn prev\" href=\"#/docs/${prev.slug}\"><span class=\"doc-pager-dir\">\u2190 Previous</span><span class=\"doc-pager-title\">${prev.title}</span></a>`\n : html`<span></span>`\n }${\n next\n ? html`<a class=\"doc-pager-btn next\" href=\"#/docs/${next.slug}\"><span class=\"doc-pager-dir\">Next \u2192</span><span class=\"doc-pager-title\">${next.title}</span></a>`\n : html`<span></span>`\n }`,\n );\n mainEl.appendChild(pager);\n\n // Rail: this doc's \"On this page\" only (scroll-spy-tracked). No back-link (the \"Docs\" tab\n // returns to the index) and no rail pager (Prev/Next is the title line + the article\n // foot). A <details>, open unless this is mobile (the header comment says why).\n const onPage = sections.map(\n (s) =>\n html`<a class=\"toc-sub\" href=\"#/docs/${cur.slug}${s.anchor ? `/${s.anchor}` : \"\"}\" data-target=\"${s.id}\">${s.title}</a>`,\n );\n setHtml(\n navEl,\n sections.length\n ? html`<details class=\"toc-onpage\" id=\"tocOnPage\"><summary class=\"toc-label\">On this page</summary>${onPage}</details>`\n : html``,\n );\n\n // \"On this page\" links jump within the current doc and highlight at once. On mobile the\n // fold closes first, so the page height above the target is settled before the scroll\n // is measured.\n const onPageEl = findFold();\n syncFold(onPageEl); // open + inert on desktop, closed + tappable on mobile (before paint)\n followLayout();\n const markActive = (id: string) => {\n if (!onPageEl) {\n return;\n }\n for (const a of $$(\".toc-sub\", onPageEl)) {\n a.classList.toggle(\"on\", a.dataset.target === id);\n }\n };\n navEl.addEventListener(\"click\", (ev) => {\n const a =\n ev.target instanceof Element ? ev.target.closest<HTMLElement>(\"a[data-target]\") : null;\n const target = a?.dataset.target;\n if (!target) {\n return;\n }\n ev.preventDefault();\n markActive(target);\n if (onPageEl && mobileMq.matches) {\n onPageEl.open = false;\n }\n // The address names the section, so it can be shared, without a hashchange that would\n // render the page again.\n history.replaceState(history.state, \"\", a?.getAttribute(\"href\") ?? location.hash);\n document.getElementById(target)?.scrollIntoView({ block: \"start\" });\n });\n\n // A link out of the guide (a provider's sign-up page, say) opens beside the app rather\n // than replacing it, so the reader keeps their place in the step they're on.\n for (const a of $$<HTMLAnchorElement>(\"a[href^='http']\", mainEl)) {\n a.target = \"_blank\";\n a.rel = \"noopener\";\n }\n\n // Copy buttons on the guide's shell / DNS code blocks.\n for (const pre of $$<HTMLPreElement>(\"pre\", mainEl)) {\n pre.classList.add(\"has-copy\");\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.className = \"code-copy\";\n btn.textContent = \"Copy\";\n btn.addEventListener(\"click\", async () => {\n const code = pre.querySelector(\"code\");\n try {\n await navigator.clipboard.writeText((code || pre).innerText);\n btn.textContent = \"Copied\";\n setTimeout(() => {\n btn.textContent = \"Copy\";\n }, 1500);\n } catch {\n toast(\"Couldn\u2019t copy to clipboard\");\n }\n });\n pre.appendChild(btn);\n }\n\n // Scroll-spy: the active section is the last heading scrolled above a line just under the\n // sticky room bar; at the bottom the last heading wins so a short final section still\n // highlights. One document.onscroll slot, self-cleared once this doc leaves the DOM.\n const ids = sections.map((s) => s.id);\n const first = ids[0];\n if (onPageEl && first) {\n const last = ids[ids.length - 1] ?? first;\n // Just under the sticky bar. Measured, not assumed: the bar is --bar-h at every width\n // (see roomShell), but large-text zoom can push it taller, and the spy should follow.\n const barH = document.querySelector(\".room-bar\")?.getBoundingClientRect().height || 54;\n const line = barH + 26;\n const spy = () => {\n if (!document.getElementById(first)) {\n document.onscroll = null;\n return;\n }\n let active = first;\n if (Math.ceil(window.innerHeight + window.scrollY) >= document.documentElement.scrollHeight) {\n active = last;\n } else {\n for (const id of ids) {\n if (\n (document.getElementById(id)?.getBoundingClientRect().top ??\n Number.POSITIVE_INFINITY) <= line\n ) {\n active = id;\n } else {\n break;\n }\n }\n }\n markActive(active);\n };\n document.onscroll = spy;\n spy();\n }\n\n // Each doc is its own page \u2014 start at the top, or at the heading the route names.\n const heading = anchor ? document.getElementById(headingId(anchor)) : null;\n if (heading) {\n heading.scrollIntoView({ block: \"start\" });\n } else {\n window.scrollTo(0, 0);\n }\n}\n", "// The curl command the API reference offers for a route: the method, the URL on this\n// instance, the credential headers its tier needs, and the example body when it has one.\n\nimport type { ReferenceEntry } from \"../../shared/reference\";\n\n/** How this instance authenticates the admin tier, as the boot probe reported it. */\nexport type AuthMode = \"access\" | \"dev\";\n\n/**\n * The credential headers an admin call carries: the Access service token's pair once\n * deployed (the names the setup guide uses), or the dev token locally (as in the README).\n */\nfunction authLines(mode: AuthMode): string[] {\n return mode === \"access\"\n ? [\n '-H \"CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID\"',\n '-H \"CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET\"',\n ]\n : ['-H \"Authorization: Bearer $TOKEN\"'];\n}\n\n/**\n * The route's path as a URL path to type: the pattern syntax a URL never carries (a\n * parameter's regex, an optional segment) is dropped, and `:id`-style parameters stay as\n * the placeholders to replace. The reference shows its rows this way too.\n */\nexport function concretePath(path: string): string {\n return path.replace(/\\([^)]*\\)/g, \"\").replace(/\\{[^}]*\\}\\??/g, \"\") || \"/\";\n}\n\n/**\n * A copy-paste curl command for a route on this instance, or null for a webhook, which\n * only the email provider can call (it must carry the provider's signature).\n */\nexport function curlCommand(r: ReferenceEntry, origin: string, mode: AuthMode): string | null {\n if (r.access === \"webhook\") {\n return null;\n }\n // A query parameter the route can't do without rides the URL, as a placeholder like a path's.\n const required = (r.query ?? []).filter((q) => q.required);\n const search = required.length ? `?${required.map((q) => `${q.name}=:${q.name}`).join(\"&\")}` : \"\";\n const url = `\"${origin}${concretePath(r.path)}${search}\"`;\n const lines = [r.method === \"GET\" ? `curl ${url}` : `curl -X ${r.method} ${url}`];\n if (r.access === \"admin\") {\n lines.push(...authLines(mode));\n }\n if (r.example?.request !== undefined) {\n // Single-quoted for the shell, so a quote inside the JSON closes, escapes, and reopens.\n const body = JSON.stringify(r.example.request).replaceAll(\"'\", `'\\\\''`);\n lines.push('-H \"Content-Type: application/json\"', `-d '${body}'`);\n }\n return lines.join(\" \\\\\\n \");\n}\n", "// The API reference room, rendered from /api/reference.\n\nimport type {\n QueryParam,\n ReferenceEntry,\n ReferenceGroup,\n ReferenceResource,\n ReferenceResponse,\n} from \"../../shared/reference\";\nimport { api, describeError } from \"../api\";\nimport { mount } from \"../lifecycle\";\nimport { appState } from \"../state\";\nimport { codeBlock, wireCopy } from \"../ui/copy\";\nimport { $, $$ } from \"../ui/dom\";\nimport { highlightCurl, highlightJson } from \"../ui/highlight\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { renderError } from \"../ui/widgets\";\nimport { concretePath, curlCommand } from \"./curl\";\nimport { roomShell } from \"./shell\";\n\n// Every route the app and Claude can call, generated from the route manifest\n// (src/app.ts) and served as JSON by the authed /api/reference route. The SPA renders\n// it natively: a rail of tiers and their resources beside the route list, where each\n// route is one scannable row, a native <details>, that opens in place to its full\n// documentation.\nfunction apiExample(label: string, value: unknown): Html | null {\n return value === undefined\n ? null\n : codeBlock(label, highlightJson(value), JSON.stringify(value, null, 2));\n}\n/** The route as a curl command on this instance, with the credential its tier needs. */\nfunction apiCurl(r: ReferenceEntry): Html | null {\n const cmd = curlCommand(r, location.origin, appState.session?.auth.mode ?? \"dev\");\n return cmd === null ? null : codeBlock(\"curl\", highlightCurl(cmd), cmd);\n}\n// Query params for a list route, rendered as a name\u2192description table so the\n// generated reference documents filter/sort/pagination from the registration.\nfunction apiQueryHtml(query: QueryParam[] | undefined): Html | null {\n if (!query?.length) {\n return null;\n }\n const rows = query.map(\n (q) =>\n html`<tr><td><code>${q.name}</code>${q.required ? html` <span class=\"api-req\">required</span>` : null}</td><td class=\"muted\">${q.description}</td></tr>`,\n );\n return html`<div class=\"block\"><div class=\"block-head\"><span class=\"block-label\">Query</span></div><table class=\"api-query\"><tbody>${rows}</tbody></table></div>`;\n}\n// The body types a write accepts, from the registration the router enforces them by. A\n// route that takes no body lists none.\nfunction apiAcceptsHtml(accepts: readonly string[] | undefined): Html | null {\n if (!accepts?.length) {\n return null;\n }\n const rows = accepts.map((t) => html`<tr><td><code>${t}</code></td></tr>`);\n return html`<div class=\"block\"><div class=\"block-head\"><span class=\"block-label\">Content-Type</span></div><table class=\"api-query api-accepts\"><tbody>${rows}</tbody></table></div>`;\n}\n/**\n * The path as a URL path to type (the curl command's own, without the router's pattern\n * syntax), with its parameters marked so the variable parts read at a glance.\n */\nfunction apiPathHtml(path: string): Html {\n return html`${concretePath(path)\n .split(/(:[A-Za-z_]+)/)\n .map((part, i) => (i % 2 ? html`<span class=\"api-param\">${part}</span>` : part))}`;\n}\nfunction apiRouteHtml(r: ReferenceEntry): Html {\n // What the filter matches: the method, the path, and the one-line summary.\n const hay = `${r.method} ${concretePath(r.path)} ${r.summary}`.toLowerCase();\n return html`<li data-hay=\"${hay}\"><details class=\"api-route\">\n <summary class=\"api-route-sum\">\n <span class=\"api-method m-${r.method}\">${r.method}</span>\n <code class=\"api-path\">${apiPathHtml(r.path)}</code>\n <span class=\"api-route-line\">${r.summary}</span>\n <span class=\"api-chev\" aria-hidden=\"true\"></span>\n </summary>\n <div class=\"api-route-detail\">\n <p class=\"api-summary\">${r.summary}</p>\n ${r.description ? html`<p class=\"api-desc muted\">${r.description}</p>` : null}\n ${apiQueryHtml(r.query)}\n ${apiAcceptsHtml(r.accepts)}\n ${apiExample(\"Request\", r.example?.request)}\n ${apiExample(\"Response\", r.example?.response)}\n ${apiCurl(r)}\n </div>\n </details></li>`;\n}\n/** A resource's routes within a tier, in the order the tier lists them. */\nconst routesIn = (g: ReferenceGroup, res: ReferenceResource) =>\n g.routes.filter((r) => r.resource === res.key);\nfunction apiResourceHtml(g: ReferenceGroup, res: ReferenceResource): Html {\n const sec = `${g.access}-${res.key}`;\n const routes = routesIn(g, res);\n return html`<div class=\"api-res\" id=\"api-${sec}\" data-sec=\"${sec}\">\n <div class=\"api-res-head\"><h3>${res.title}</h3></div>\n <ul class=\"api-rows\">${routes.map(apiRouteHtml)}</ul>\n </div>`;\n}\nfunction apiSectionHtml(g: ReferenceGroup): Html {\n return html`<section class=\"api-section\" id=\"api-${g.access}\" data-tier=\"${g.access}\">\n <h2>${g.title}</h2>\n <p class=\"api-blurb muted\">${g.blurb}</p>\n ${g.resources.map((res) => apiResourceHtml(g, res))}\n </section>`;\n}\n// The rail: each tier, then its resources beneath it. A tier with one resource lists only\n// the tier, since the resource would repeat it. No counts: the rail is for moving.\nfunction apiNavHtml(groups: ReferenceGroup[]): Html {\n return html`${groups.map(\n (g) =>\n html`<a href=\"#/reference\" class=\"api-nav-tier\" data-sec=\"${g.access}\" data-tier=\"${g.access}\">${g.title}</a>${\n g.resources.length > 1\n ? g.resources.map(\n (res) =>\n html`<a href=\"#/reference\" class=\"api-nav-res\" data-sec=\"${g.access}-${res.key}\">${res.title}</a>`,\n )\n : null\n }`,\n )}`;\n}\n\n/**\n * Show only the routes whose method, path, or summary contains every word of the query,\n * with the resources, tiers, and rail links that still hold one. An empty query shows\n * everything. The last tier still shown is marked, so it keeps the room to scroll\n * up under the bar that the last tier has.\n */\nfunction applyFilter(query: string, navEl: HTMLElement, contentEl: HTMLElement): void {\n const terms = query.trim().toLowerCase().split(/\\s+/).filter(Boolean);\n let shown = 0;\n for (const sec of $$<HTMLElement>(\".api-section\", contentEl)) {\n let inTier = 0;\n for (const res of $$<HTMLElement>(\".api-res\", sec)) {\n let inRes = 0;\n for (const li of $$<HTMLElement>(\"li[data-hay]\", res)) {\n const hit = terms.every((w) => (li.dataset.hay ?? \"\").includes(w));\n li.hidden = !hit;\n inRes += hit ? 1 : 0;\n }\n res.hidden = inRes === 0;\n const link = navEl.querySelector<HTMLElement>(`a[data-sec=\"${res.dataset.sec}\"]`);\n if (link) {\n link.hidden = inRes === 0;\n }\n inTier += inRes;\n }\n sec.hidden = inTier === 0;\n const tierLink = navEl.querySelector<HTMLElement>(`a[data-tier=\"${sec.dataset.tier}\"]`);\n if (tierLink) {\n tierLink.hidden = inTier === 0;\n }\n shown += inTier;\n }\n const sections = $$<HTMLElement>(\".api-section\", contentEl);\n const lastShown = sections.filter((sec) => !sec.hidden).at(-1);\n for (const sec of sections) {\n sec.classList.toggle(\"api-last\", sec === lastShown);\n }\n $<HTMLElement>(\"#apiEmpty\", contentEl).hidden = shown > 0;\n}\n\n/**\n * Highlight the rail link for what the reader is in: the last tier or resource heading\n * scrolled above a line just under the sticky room bar (so an instant jump lands right,\n * not only a scroll that passes each heading), or the last one once the page is at its\n * bottom. A resource the rail doesn't list lights its tier; a filtered-out one is skipped.\n */\nfunction spyRail(navEl: HTMLElement, contentEl: HTMLElement): void {\n // Measured, not assumed, as the docs' spy does: large-text zoom can push the bar taller.\n const line = ($(\".room-bar\").getBoundingClientRect().height || 54) + 26;\n const marks = $$(\".api-section, .api-res\", contentEl).filter((el) => !el.closest(\"[hidden]\"));\n // At the bottom, the last heading wins; a page that hasn't scrolled is at its top.\n const atBottom =\n window.scrollY > 0 &&\n Math.ceil(window.innerHeight + window.scrollY) >= document.documentElement.scrollHeight;\n let current = marks[0];\n for (const el of marks) {\n if (atBottom || el.getBoundingClientRect().top <= line) {\n current = el;\n } else {\n break;\n }\n }\n const sec = current?.dataset.sec ?? current?.dataset.tier;\n const tier = current?.closest<HTMLElement>(\".api-section\")?.dataset.tier;\n // A resource link the rail isn't showing (a phone's rail is the tier chips alone) hands\n // the highlight to its tier.\n const links = $$(\"a[data-sec]\", navEl).filter((a) => getComputedStyle(a).display !== \"none\");\n const target =\n links.find((a) => a.dataset.sec === sec) ??\n (tier === undefined ? undefined : links.find((a) => a.dataset.tier === tier));\n for (const a of $$(\"a[data-sec]\", navEl)) {\n a.classList.toggle(\"active\", a === target);\n }\n}\n\nexport async function renderReference(root: HTMLElement, signal: AbortSignal): Promise<void> {\n setHtml(\n root,\n roomShell(\n \"reference\",\n html`<div class=\"toc-label\">API</div><nav class=\"api-nav\" id=\"apiNav\" aria-label=\"API sections\"></nav>`,\n html`<div class=\"api-content\" id=\"apiContent\"><p class=\"muted\">Loading\u2026</p></div>`,\n ),\n );\n const navEl = $(\"#apiNav\");\n const contentEl = $(\"#apiContent\");\n // Delegate clicks synchronously with one listener on the stable nav, so it survives\n // the async fill below: a rail click jumps to that tier or resource.\n navEl.addEventListener(\"click\", (ev) => {\n const a = ev.target instanceof Element ? ev.target.closest<HTMLElement>(\"a[data-sec]\") : null;\n const sec = a?.dataset.sec;\n if (!sec) {\n return;\n }\n ev.preventDefault();\n document.getElementById(`api-${sec}`)?.scrollIntoView({ block: \"start\" });\n });\n let groups: ReferenceGroup[];\n try {\n ({ groups } = await api<ReferenceResponse>(\"/api/reference\", { signal }));\n } catch (e) {\n renderError(contentEl, describeError(e), () => mount(renderReference));\n return;\n }\n\n setHtml(navEl, apiNavHtml(groups));\n setHtml(\n contentEl,\n html`<header class=\"api-head\"><h1>API reference</h1><p class=\"muted\">Generated from the route registration, so every endpoint the app and Claude can call is listed here. Base URL <code>${location.origin}</code>.</p><div class=\"api-filter\">${icon(\"search\")}<input type=\"search\" id=\"apiFilter\" placeholder=\"Filter routes by method, path, or summary\" autocomplete=\"off\" aria-label=\"Filter routes\" aria-keyshortcuts=\"/\"><kbd aria-hidden=\"true\">/</kbd><button type=\"button\" class=\"icon api-filter-clear\" id=\"apiFilterClear\" aria-label=\"Clear filter\" hidden>${icon(\"x\")}</button></div></header>${groups.map(apiSectionHtml)}<p class=\"api-empty muted\" id=\"apiEmpty\" hidden>No routes match. Try a path segment like <code>sends</code> or a method like <code>DELETE</code>.</p>`,\n );\n const filterEl = $<HTMLInputElement>(\"#apiFilter\");\n const clearEl = $(\"#apiFilterClear\");\n const spy = () => spyRail(navEl, contentEl);\n const filterTo = (query: string) => {\n filterEl.value = query;\n clearEl.hidden = query === \"\";\n applyFilter(query, navEl, contentEl);\n spy();\n };\n filterEl.addEventListener(\"input\", () => filterTo(filterEl.value));\n clearEl.addEventListener(\"click\", () => {\n filterTo(\"\");\n filterEl.focus();\n });\n // A code block's Copy puts its plain text (the example, or the curl command) on the\n // clipboard and says so where it sits (ui/copy.ts).\n wireCopy(contentEl);\n // \"/\" jumps to the filter from anywhere in the room (unless typing elsewhere); Escape\n // in the filter clears it. Bound to the view, so it ends when the room does.\n document.addEventListener(\n \"keydown\",\n (ev) => {\n const typing =\n ev.target instanceof HTMLElement &&\n (ev.target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(ev.target.tagName));\n if (ev.key === \"/\" && !typing && !ev.metaKey && !ev.ctrlKey && !ev.altKey) {\n ev.preventDefault();\n filterEl.focus();\n } else if (ev.key === \"Escape\" && ev.target === filterEl && filterEl.value) {\n filterTo(\"\");\n }\n },\n { signal },\n );\n\n // Mark the last tier (the filter keeps it current from here).\n applyFilter(\"\", navEl, contentEl);\n // The rail follows the reading position, and the filter, which moves every heading.\n window.addEventListener(\"scroll\", spy, { passive: true, signal });\n // Its own page \u2014 start at the top, not wherever the last doc was scrolled to.\n window.scrollTo(0, 0);\n spy();\n}\n", "// The sent list: scheduled sends with their countdowns, the sends in progress and any that\n// need the operator, and the record of what has gone out, kept current by the send-state\n// layer (docs/DESIGN.md \u00A74).\n\nimport { earlierCursor } from \"../../shared/cursor\";\nimport type { SendListResponse, SendView } from \"../../shared/sends\";\nimport { api, describeError } from \"../api\";\nimport {\n connectProviderLink,\n heldSendsHtml,\n noEmailProvider,\n notConnectedWarning,\n providerNotConnected,\n} from \"../deployment\";\nimport { mount } from \"../lifecycle\";\nimport {\n followSends,\n readSendsNow,\n type SendStage,\n type SendsUpdate,\n stageOf,\n} from \"../send_state\";\nimport { $, $$ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { html, setHtml } from \"../ui/html\";\nimport {\n type ListState,\n listQuery,\n listToolbar,\n renderPager,\n th,\n wireSort,\n wireToolbar,\n} from \"../ui/list_controls\";\nimport { busy, renderError, toast } from \"../ui/widgets\";\nimport { openRescheduleModal, openResolveModal } from \"./dialogs\";\nimport {\n activeRowHtml,\n can,\n conditionOf,\n countdownHtml,\n countdowns,\n deliveredCell,\n needsOperator,\n sendsNow,\n} from \"./progress\";\n\nconst message = describeError;\n\n/** A scheduled card's Reschedule and Cancel: offered while the server would take them, and\n * hidden at the fire time, when the review window closes (SPEC \u00A76), by the countdown's\n * tick rather than the next read. */\nfunction windowControls(s: SendView) {\n const buttons = [\n can(s, \"reschedule\")\n ? html`<button class=\"ghost\" data-reschedule=\"${s.id}\">Reschedule</button>`\n : null,\n can(s, \"cancel\") ? html`<button class=\"ghost\" data-cancel=\"${s.id}\">Cancel</button>` : null,\n ];\n return buttons.some(Boolean)\n ? html`<div class=\"row\" data-closes=\"${s.fire_at}\">${buttons}</div>`\n : null;\n}\n\nexport async function renderSent(root: HTMLElement, signal: AbortSignal): Promise<void> {\n // The dispatch side: the still-cancelable Scheduled queue on top, then the frozen Sent\n // records. The table is sent-only, so it carries no status column or filter; default\n // sort = fire desc so the \"When\" header shows its arrow from the start.\n const state: ListState = {\n status: \"sent\",\n search: \"\",\n failures: \"\",\n sort: \"fire\",\n dir: \"desc\",\n offset: 0,\n limit: 50,\n };\n setHtml(\n root,\n html`<h1>Sent</h1>\n ${noEmailProvider() ? html`<p class=\"muted\">A local stand-in sends these: they are recorded here, but nothing is delivered.</p>` : null}\n ${notConnectedWarning(\"sent-not-connected\", html`No email provider connected, so nothing can be sent yet. ${connectProviderLink()} to start sending.`)}\n <div id=\"stuck\"></div>\n <h2>Scheduled</h2><div id=\"scheduled\" class=\"muted\">Loading\u2026</div>\n <div id=\"active\"></div>\n <h2>Sent posts</h2>\n ${listToolbar({ searchPlaceholder: \"Search subject\u2026\", failures: true })}\n <div id=\"sendsList\" class=\"muted\">Loading\u2026</div>\n <div id=\"sendsPager\"></div>`,\n );\n const stuckEl = $(\"#stuck\", root);\n const schedEl = $(\"#scheduled\", root);\n const activeEl = $(\"#active\", root);\n const listEl = $(\"#sendsList\", root);\n const pagerEl = $(\"#sendsPager\", root);\n const tickCountdowns = countdowns(root, signal);\n\n // The sends in flight as the page last read them, and every send the layer has reported\n // since, as it now stands: together, what the attention and in-progress sections show.\n let sendingRows: SendView[] = [];\n // Until a read of the sends in flight succeeds, the In progress slot keeps its error and\n // Retry: the layer reports only sends that change, so one refused for an hour would\n // otherwise never show.\n let liveRead = false;\n // Each section paints only its latest read, so a slow earlier one (a queue read for a\n // send turning due, landing after the one for its start) never paints over a newer one.\n let scheduledReads = 0;\n let listReads = 0;\n const reported = new Map<string, SendView>();\n\n // Resolving a wedged send or canceling a scheduled one touches several sections at once:\n // re-read the two this page reads itself, and have the layer read now, so the attention\n // and in-progress sections catch up with the act rather than at the next read.\n function afterAct() {\n loadScheduled();\n loadList();\n readSendsNow();\n }\n\n // The sends in progress: a card each, with a live bar, for every send still sending that\n // doesn't need the operator (that one's home is the attention block above).\n function renderActive(live: SendView[]) {\n const active = live.filter((s) => s.status === \"sending\" && !needsOperator(s));\n setHtml(\n activeEl,\n active.length ? html`<h2>In progress</h2>${active.map(activeRowHtml)}` : html``,\n );\n for (const card of $$<HTMLElement>(\"[data-watch]\", activeEl)) {\n card.onclick = (e) => {\n if (!(e.target instanceof Element && e.target.tagName === \"A\")) {\n location.hash = `#/sent/${card.dataset.watch}`;\n }\n };\n }\n }\n // The sends that need the operator (SPEC \u00A712), each an attention card that stays until\n // its condition clears: repainted from every report of the layer.\n function renderStuck(live: SendView[]) {\n // A refused send carries no control: the fix is in the provider's account or the\n // deployment's secrets, and the send resumes on its own once it lands. Each card is the\n // server's words for its condition. A send held for want of a provider (SPEC \u00A79) is the\n // deployment's state showing on a send, so while the deployment is not connected the\n // held sends and the state are one card in place of the state's amber line, with the\n // fix once, as a link (DESIGN \u00A74).\n const allRefused = live.flatMap((s) => {\n const c = conditionOf(s, \"refused\");\n return c ? [{ s, c }] : [];\n });\n const held = providerNotConnected()\n ? allRefused.filter((r) => r.c.cause === \"not_connected\")\n : [];\n const refused = allRefused.filter((r) => !held.includes(r));\n const notConnectedLine = root.querySelector<HTMLElement>(\".sent-not-connected\");\n if (notConnectedLine) {\n notConnectedLine.hidden = held.length > 0;\n }\n const heldNames = held.map(\n ({ s }, i) =>\n html`${i === 0 ? \"\" : i === held.length - 1 ? \" and \" : \", \"}<a href=\"#/sent/${s.id}\">${s.subject}</a>`,\n );\n const heldCard = held.length\n ? html`<div class=\"card stuck-card sent-held\"><div class=\"stuck-head\"><span class=\"stuck-dot\">\u26A0\uFE0F</span><div>${heldSendsHtml(html`${heldNames}`, held.length)}</div></div></div>`\n : null;\n const wedged = live.flatMap((s) => {\n const c = conditionOf(s, \"wedged\");\n return c ? [{ s, c }] : [];\n });\n setHtml(\n stuckEl,\n html`${heldCard}${refused.map(\n ({ s, c }) =>\n html`<div class=\"card stuck-card\"><div class=\"stuck-head\"><span class=\"stuck-dot\">\u26A0\uFE0F</span><div><strong><a href=\"#/sent/${s.id}\">${s.subject}</a></strong><div class=\"muted\">${c.message}</div></div></div></div>`,\n )}${wedged.map(({ s, c }) => {\n // Resolve is offered while the server would take it.\n return html`<div class=\"card stuck-card\"><div class=\"stuck-head\"><span class=\"stuck-dot\">\u26A0\uFE0F</span><div><strong><a href=\"#/sent/${s.id}\">${s.subject}</a></strong><div class=\"muted\">${c.message}</div></div></div>${can(s, \"resolve\") ? html`<button class=\"primary\" data-resolve=\"${s.id}\">Resolve\u2026</button>` : null}</div>`;\n })}`,\n );\n for (const b of $$<HTMLButtonElement>(\"[data-resolve]\", stuckEl)) {\n const w = wedged.find((x) => x.s.id === b.dataset.resolve);\n if (w) {\n b.onclick = () =>\n openResolveModal({ id: w.s.id, c_in_flight: w.c.count }, afterAct, signal);\n }\n }\n }\n // A listed send's Delivered cell follows the layer's counts, so the table moves with its\n // receipts (a late one included) without being read again.\n function patchDelivered() {\n for (const s of reported.values()) {\n const cell = $$<HTMLTableRowElement>(\"tr[data-id]\", listEl)\n .find((tr) => tr.dataset.id === s.id)\n ?.querySelector(\"td.delivered\");\n if (cell) {\n setHtml(cell, deliveredCell(s.counts));\n }\n }\n }\n function paintLive() {\n const live = sendsNow(sendingRows, reported);\n renderStuck(live);\n if (liveRead) {\n renderActive(live);\n }\n patchDelivered();\n }\n // Each report moves sends between this page's sections, whoever changed them. Any change\n // to a send in (or leaving) the queue re-reads it: a cancel, a move, a re-make, or a new\n // schedule by the other client, the fire time passing, a miss, a start. A send reaching\n // the sent side re-reads the records; one that went due to sent between two reads is one\n // change, and still lands there. Everything else is a report's own numbers.\n const inQueue = (st: SendStage | null) => st === \"scheduled\" || st === \"due\";\n const sentSide = (st: SendStage | null) => st === \"sent\" || st === \"complete\";\n function onUpdate(u: SendsUpdate) {\n for (const s of u.sends) {\n reported.set(s.id, s);\n }\n for (const id of u.removed) {\n reported.delete(id);\n }\n paintLive();\n if (u.sends.some((s) => inQueue(stageOf(s))) || u.changes.some((c) => inQueue(c.from))) {\n loadScheduled();\n }\n // A removed send was canceled with its post deleted, so it may be in either section.\n if (u.removed.length) {\n loadScheduled();\n loadList();\n } else if (u.changes.some((c) => sentSide(c.to) && !sentSide(c.from))) {\n loadList();\n }\n }\n\n // The layer follows every send from where the page's own reads stood, the earliest of\n // them, so nothing that changed after any of those reads is missed. A section whose first\n // read failed starts it from its Retry instead.\n let following = false;\n function follow(reads: (SendListResponse | null)[]) {\n const ok = reads.filter((r): r is SendListResponse => r !== null);\n if (following || !ok.length) {\n return;\n }\n following = true;\n followSends(\n {\n cursor: ok.map((r) => r.cursor).reduce(earlierCursor),\n sends: ok.flatMap((r) => r.sends),\n },\n {\n update: onUpdate,\n // The database is behind the page's reads (a reset or a restore): read it all again,\n // unless the reader has moved on.\n stale: () => {\n if (!signal.aborted) {\n mount(renderSent);\n }\n },\n },\n signal,\n );\n }\n\n // The sends in flight, for the attention and in-progress sections; from then on the\n // layer's reports keep them current.\n async function loadLive(): Promise<SendListResponse | null> {\n try {\n const res = await api<SendListResponse>(\"/api/sends?status=sending&limit=200\", { signal });\n sendingRows = res.sends;\n liveRead = true;\n paintLive();\n return res;\n } catch (e) {\n renderError(activeEl, message(e), async () => follow([await loadLive()]));\n return null;\n }\n }\n\n // The upcoming queue, soonest-first: the next send to fire (and the one you'd reach\n // for the cancel window on) sits at the top. Fetched on its own so it shows every\n // scheduled send regardless of the table's paging/filter below.\n async function loadScheduled(): Promise<SendListResponse | null> {\n const mine = ++scheduledReads;\n try {\n const res = await api<SendListResponse>(\n \"/api/sends?status=scheduled&sort=fire&dir=asc&limit=200\",\n { signal },\n );\n if (mine !== scheduledReads) {\n return res; // a later read paints\n }\n const { sends } = res;\n const schedCard = (s: SendView) =>\n html`<div class=\"card spread clickable sched-card\" data-post=\"${s.post_id}\"><div><a class=\"card-link sched-subj\" href=\"#/edit/${s.post_id}\">${s.subject}</a><div class=\"muted\">${countdownHtml(s)} \u00B7 ${fmt(s.fire_at)} \u00B7 ${s.audience.count} recipients</div></div>${windowControls(s)}</div>`;\n const [first, ...rest] = sends;\n if (!first) {\n setHtml(schedEl, html`<p class=\"muted\">Nothing scheduled.</p>`);\n } else {\n // Show only the soonest to send, so the sent records stay near the top of the\n // page; any others collapse behind a \"Show all\" toggle (usually there are none:\n // a post has at most one active send).\n setHtml(\n schedEl,\n html`${schedCard(first)}${\n rest.length\n ? html`<div id=\"schedMore\" hidden>${rest.map(schedCard)}</div><button type=\"button\" class=\"ghost sched-toggle\" id=\"schedToggle\" aria-expanded=\"false\">Show all ${sends.length} scheduled</button>`\n : null\n }`,\n );\n const toggle = schedEl.querySelector<HTMLButtonElement>(\"#schedToggle\");\n if (toggle) {\n toggle.onclick = () => {\n const more = $(\"#schedMore\", schedEl);\n const show = more.hidden;\n more.hidden = !show;\n toggle.setAttribute(\"aria-expanded\", String(show));\n toggle.textContent = show ? \"Show fewer\" : `Show all ${sends.length} scheduled`;\n };\n }\n }\n // The whole card opens the post; the subject link handles keyboard/middle-click,\n // and the schedule-management buttons (Reschedule, Cancel) opt out of navigation\n // (like the posts table's row-click guard).\n for (const card of $$<HTMLElement>(\".card.clickable\", schedEl)) {\n card.onclick = (e) => {\n const t = e.target;\n if (t instanceof Element && t.tagName !== \"A\" && !t.closest(\"button\")) {\n location.hash = `#/edit/${card.dataset.post}`;\n }\n };\n }\n for (const b of $$<HTMLButtonElement>(\"[data-reschedule]\", schedEl)) {\n const s = sends.find((x) => x.id === b.dataset.reschedule);\n if (s) {\n b.onclick = () => openRescheduleModal(s.id, s.fire_at, afterAct, signal);\n }\n }\n for (const b of $$<HTMLButtonElement>(\"[data-cancel]\", schedEl)) {\n b.onclick = () =>\n busy(b, \"Canceling\u2026\", async () => {\n try {\n await api(`/api/sends/${b.dataset.cancel}/cancel`, { method: \"POST\" });\n toast(\"Canceled\");\n // A cancel drops it from the queue, and a due send from the layer's reads.\n afterAct();\n } catch (e) {\n toast(message(e));\n }\n });\n }\n tickCountdowns();\n return res;\n } catch (e) {\n if (mine === scheduledReads) {\n renderError(schedEl, message(e), async () => follow([await loadScheduled()]));\n }\n return null;\n }\n }\n\n // The frozen Sent records: every completed post, each opening its read-only record\n // view. Sent-only, so no status column; the \"When\" is the send's completion.\n async function loadList(): Promise<SendListResponse | null> {\n const mine = ++listReads;\n try {\n const data = await api<SendListResponse>(`/api/sends?${listQuery(state)}`, { signal });\n if (mine !== listReads) {\n return data; // a later read paints\n }\n const sends = data.sends;\n if (!sends.length) {\n setHtml(\n listEl,\n html`<p class=\"muted\">${\n state.search\n ? \"No sent posts match.\"\n : state.failures\n ? \"Every sent post delivered cleanly.\"\n : \"No sent posts yet.\"\n }</p>`,\n );\n setHtml(pagerEl, html``);\n return data;\n }\n // Cells are named (subject / recipients / delivered) and the count is wrapped in\n // `.n` so the \u2264720px layout can stack a row and label its numbers from CSS alone.\n setHtml(\n listEl,\n html`<div class=\"table-wrap\"><table class=\"list-table sent-table stacks\"><colgroup><col><col class=\"c-date\"><col class=\"c-num\"><col class=\"c-delivered\"></colgroup><thead><tr>${th(\"Subject\", \"subject\", state)}${th(\"When\", \"fire\", state)}${th(\"Recipients\", \"recipients\", state, \"num\")}<th class=\"num\">Delivered</th></tr></thead><tbody>${sends.map(\n (s) =>\n html`<tr class=\"clickable\" data-id=\"${s.id}\"><td class=\"subject\"><a href=\"#/sent/${s.id}\">${s.subject}</a></td><td class=\"muted\">${fmt(s.completed_at ?? s.fire_at)}</td><td class=\"num recipients\"><span class=\"n\">${s.audience.count.toLocaleString()}</span></td><td class=\"num delivered\">${deliveredCell(s.counts)}</td></tr>`,\n )}</tbody></table></div>`,\n );\n wireSort(listEl, state, loadList);\n // The whole row opens the record; the subject link handles keyboard/middle-click.\n for (const tr of $$<HTMLTableRowElement>(\"tr[data-id]\", listEl)) {\n tr.onclick = (e) => {\n if (!(e.target instanceof Element && e.target.tagName === \"A\")) {\n location.hash = `#/sent/${tr.dataset.id}`;\n }\n };\n }\n renderPager(pagerEl, state, data.page, loadList);\n patchDelivered();\n return data;\n } catch (e) {\n if (mine === listReads) {\n renderError(listEl, message(e), async () => follow([await loadList()]));\n }\n return null;\n }\n }\n\n wireToolbar(root, state, loadList);\n // The queue, the sends in flight, and the records are this page's own reads; from them\n // the layer follows every send for the life of the mount, so a send moves through the\n // queue, in progress, and the records on its own.\n follow(await Promise.all([loadScheduled(), loadLive(), loadList()]));\n}\n", "// The sent record view and the live in-flight watch.\n\nimport type {\n DeliveryListResponse,\n DeliveryOutcomes,\n DeliveryRecord,\n DeliveryView,\n SendCounts,\n SendPhase,\n SendResponse,\n SendView,\n} from \"../../shared/sends\";\nimport { ApiError, api, apiFile, describeError } from \"../api\";\nimport { heldAdviceHtml, noEmailProvider, providerNotConnected } from \"../deployment\";\nimport { every, mount } from \"../lifecycle\";\nimport { followSend } from \"../send_state\";\nimport { $, $$ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { type ListState, renderPager, th, wireSort } from \"../ui/list_controls\";\nimport { busy, renderError, toast } from \"../ui/widgets\";\nimport { openResolveModal } from \"./dialogs\";\nimport { can, clampPct, conditionOf, fmtDuration, has } from \"./progress\";\n\nconst message = describeError;\n\n/**\n * A sent post is a frozen record (I3), not an editable object, so it opens this instead\n * of a locked editor: how the send went over the audience at fire, with a link to the\n * archived post. A send still IN FLIGHT opens the live watch (two bars, dispatch and the\n * lagging delivery, a derived phase, a counts grid, throughput, and a provider-health\n * strip), following the send through the send-state layer until dispatch completes, after\n * which the record keeps absorbing delivery receipts as they settle (SPEC \u00A76/\u00A78/\u00A712).\n */\nexport async function renderSentRecord(\n id: string,\n root: HTMLElement,\n signal: AbortSignal,\n): Promise<void> {\n // The watch re-enters here when dispatch ends, and the record when it resumes; each is\n // a fresh mount (its own root and signal), so the poll that noticed ends with its own.\n const remount = () => {\n if (!signal.aborted) {\n mount((r, s) => renderSentRecord(id, r, s)); // never over wherever the reader went since\n }\n };\n setHtml(root, html`<p class=\"muted\">Loading\u2026</p>`);\n let data: SendResponse;\n try {\n data = await api<SendResponse>(`/api/sends/${id}`, { signal });\n } catch (e) {\n renderError(root, message(e), remount);\n return;\n }\n if (signal.aborted) {\n return; // navigated away while loading: the redirect below must not hijack that\n }\n const { send } = data;\n\n // A scheduled send is still cancel-to-edit: its home is the editor, not this record. The\n // editor takes this page's history entry, so Back goes where the reader came from, never\n // to this URL that would only hand them to the editor again (DESIGN \u00A74).\n if (send.status === \"scheduled\") {\n location.replace(`#/edit/${send.post_id}`);\n return;\n }\n // A send still in flight opens the live watch, which follows it from this read.\n if (send.status === \"sending\") {\n return startWatch(data, root, signal, remount);\n }\n return renderFrozenRecord(id, data, root, signal, remount);\n}\n\n// Phase \u2192 { label, tone } for the derived-phase pill. Tones reuse the status/semantic\n// palettes: sending = blue (the in-flight treatment, DESIGN \u00A78), ok = green, warn/danger\n// as usual, scheduled = violet.\nconst PHASE_META: Record<SendPhase, { label: string; tone: string }> = {\n scheduled: { label: \"Scheduled\", tone: \"scheduled\" },\n due: { label: \"Due\", tone: \"scheduled\" },\n progressing: { label: \"Sending\", tone: \"sending\" },\n retrying: { label: \"Retrying\", tone: \"warn\" },\n \"backing-off\": { label: \"Backing off\", tone: \"warn\" },\n \"needs-attention\": { label: \"Needs attention\", tone: \"danger\" },\n settling: { label: \"Settling\", tone: \"sending\" },\n complete: { label: \"Complete\", tone: \"ok\" },\n canceled: { label: \"Canceled\", tone: \"muted\" },\n};\nconst PHASE_BLURB: Partial<Record<SendPhase, string>> = {\n progressing: \"Handing recipients to the provider.\",\n retrying: \"Some recipients hit a transient error and will be retried.\",\n \"backing-off\": \"Some recipients hit a transient error. They\u2019re retried on the next sweep.\",\n \"needs-attention\": \"Some deliveries are stuck and need a decision.\",\n settling: \"Dispatch complete, waiting on delivery receipts.\",\n};\n/** When a halted send's next retry is due, relative to now (SPEC \u00A712). */\nfunction nextRetry(retryAt: number | null): string {\n if (retryAt === null) {\n return \"it is retried on its own\";\n }\n const wait = retryAt - Date.now();\n return wait > 0 ? `next retry in ${fmtDuration(wait)}` : \"next retry due now\";\n}\n/** The phase's one-line gloss, said for the cause when the phase has more than one. */\nfunction phaseBlurb(prog: SendView): string {\n const halt = prog.provider.halt;\n if (has(prog, \"refused\") && halt?.cause === \"not_connected\") {\n return `No email provider is connected, so nothing goes out until one is; ${nextRetry(halt.retry_at)}.`;\n }\n if (has(prog, \"refused\") && halt) {\n return `The provider is refusing this account, so nothing more goes out until it\u2019s fixed; ${nextRetry(halt.retry_at)}.`;\n }\n if (prog.phase === \"backing-off\" && halt?.reason === \"unavailable\") {\n return `The provider is unavailable; ${nextRetry(halt.retry_at)}.`;\n }\n // A send too large for one tick waits for the next with nothing in flight. The server\n // reads that as progressing, since nothing failed; the line says why the bar is still.\n if (prog.phase === \"progressing\" && prog.counts.in_flight === 0 && prog.counts.pending > 0) {\n return \"Paused between ticks. It resumes on the next sweep.\";\n }\n return PHASE_BLURB[prog.phase] || \"\";\n}\nfunction phasePill(phase: SendPhase): Html {\n const m = PHASE_META[phase] ?? { label: phase, tone: \"muted\" };\n return html`<span class=\"phase-pill tone-${m.tone}\">${m.label}</span>`;\n}\n\n// A labeled progress bar: `tone` picks the fill color, `sub` is a muted sub-line.\nfunction progressBar(\n tone: string,\n label: string,\n value: number,\n total: number,\n sub: string | null,\n): Html {\n const p = total > 0 ? clampPct((100 * value) / total) : 0;\n return html`<div class=\"wbar-row\">\n <div class=\"wbar-head\"><span class=\"wbar-label\">${label}</span><span class=\"wbar-count\">${value.toLocaleString()} / ${total.toLocaleString()}</span></div>\n <div class=\"wbar\" role=\"progressbar\" aria-valuenow=\"${p}\" aria-valuemin=\"0\" aria-valuemax=\"100\"><div class=\"wbar-fill tone-${tone}\" style=\"width:${p}%\"></div></div>\n ${sub ? html`<div class=\"wbar-sub muted\">${sub}</div>` : null}\n </div>`;\n}\n// The delivery bar shares the dispatch bar's scale (denominator = the whole audience), so\n// the two pair up: a grey `accepted` segment tracks the exact frontier the dispatch bar\n// reaches, and green fills in behind it as receipts confirm. Green can never pass grey, so\n// delivery visibly LAGS dispatch instead of racing ahead of it.\nfunction deliveryBar(\n label: string,\n confirmed: number,\n accepted: number,\n total: number,\n sub: string,\n): Html {\n const acceptedPct = total > 0 ? clampPct((100 * accepted) / total) : 0;\n const confirmedPct = total > 0 ? clampPct((100 * confirmed) / total) : 0;\n return html`<div class=\"wbar-row\">\n <div class=\"wbar-head\"><span class=\"wbar-label\">${label}</span><span class=\"wbar-count\">${confirmed.toLocaleString()} of ${accepted.toLocaleString()} accepted</span></div>\n <div class=\"wbar dual\" role=\"progressbar\" aria-valuenow=\"${confirmedPct}\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n <div class=\"wbar-fill tone-accepted\" style=\"width:${acceptedPct}%\"></div>\n <div class=\"wbar-fill tone-ok\" style=\"width:${confirmedPct}%\"></div>\n </div>\n <div class=\"wbar-sub muted\">${sub}</div>\n </div>`;\n}\n\n// The counts grid mirrors the eight denormalized buckets; each swatch reuses the record\n// tile palette so a bucket reads the same here and on the frozen record.\nconst WATCH_COUNTS: { key: keyof SendCounts; label: string; sw: string }[] = [\n { key: \"pending\", label: \"Pending\", sw: \"neutral\" },\n { key: \"in_flight\", label: \"In flight\", sw: \"sending\" },\n { key: \"accepted\", label: \"Accepted\", sw: \"sending\" },\n { key: \"delivered\", label: \"Delivered\", sw: \"ok\" },\n { key: \"bounced\", label: \"Bounced\", sw: \"warn\" },\n { key: \"complained\", label: \"Complained\", sw: \"danger\" },\n { key: \"unsent\", label: \"Unsent\", sw: \"neutral\" },\n { key: \"skipped\", label: \"Skipped\", sw: \"neutral\" },\n];\nfunction watchCountsHtml(counts: SendCounts): Html {\n return html`<div class=\"wcounts\">${WATCH_COUNTS.map(\n (c) =>\n html`<div class=\"wcount\"><div class=\"wcount-n\">${(counts[c.key] || 0).toLocaleString()}</div><div class=\"wcount-l\"><span class=\"rec-sw sw-${c.sw}\"></span>${c.label}</div></div>`,\n )}</div>`;\n}\n\n// The dynamic half of the watch (bars + counts + health strip), repainted on each poll.\nfunction watchBodyHtml(prog: SendView): Html {\n const c = prog.counts;\n const acceptedTotal = c.accepted + c.delivered + c.bounced + c.complained;\n const rate = prog.dispatch.rate_per_min;\n // The server gives a time to finish only while the send is handing off, never while it\n // is paused (backing off, refused, or wedged). The rate goes with it: it is what a tick\n // has carried so far, so beside a pause it would promise progress nothing is making.\n const eta = prog.dispatch.eta_ms;\n const dispatchSub =\n prog.status === \"sending\"\n ? `${phaseBlurb(prog)}${\n eta ? `${rate ? ` \u00B7 ~${rate.toLocaleString()}/min` : \"\"} \u00B7 ETA ${fmtDuration(eta)}` : \"\"\n }`\n : \"Dispatch complete.\";\n const failureCount = (c.unsent || 0) + (c.bounced || 0) + (c.complained || 0);\n const providerText = noEmailProvider()\n ? \"Local stand-in: nothing is delivered\"\n : providerNotConnected()\n ? \"No email provider connected\"\n : `Provider: ${prog.provider?.name || \"\u2014\"}`;\n // The provider refusing the account is the one loud condition here without a control:\n // the fix is outside the app, and the send resumes on its own once it lands (SPEC \u00A712).\n const refused = conditionOf(prog, \"refused\");\n // No provider connected is held the same way but is no provider's refusal (SPEC \u00A79): its\n // own headline, and the fix said once, with no provider's words to quote: the step as the\n // link every not-connected line carries (or, in the local preview, the restart).\n const unconnected = refused?.cause === \"not_connected\";\n // In flight too long (SPEC \u00A712): the amber the dashboard's health line and the Sent\n // page's card give the same condition, in the server's words.\n const stuck = conditionOf(prog, \"stuck\");\n return html`\n ${stuck ? html`<div class=\"health amber\" role=\"status\"><span class=\"health-dot\">\u26A0\uFE0F</span><div>${stuck.message}</div></div>` : null}\n ${\n refused\n ? html`<div class=\"health red\" role=\"alert\"><span class=\"health-dot\">\u26A0\uFE0F</span><div><strong>${unconnected ? \"No email provider is connected\" : \"The provider is refusing this account\"}</strong>${unconnected ? null : html`<div>${refused.error}</div>`}<div class=\"refused-advice\">${unconnected ? heldAdviceHtml() : refused.advice}</div><div>${refused.since === null ? null : `Since ${fmt(refused.since)}. `}No one has been marked unsent, and once ${unconnected ? \"one is connected\" : \"this is fixed\"} the send resumes at its next retry${refused.retry_at === null ? \"\" : `, ${fmt(refused.retry_at)}`}.</div></div></div>`\n : null\n }\n <div class=\"wbars\">\n ${progressBar(\"sending\", \"Dispatch: provider-accepted\", acceptedTotal, prog.audience.count, dispatchSub)}\n ${deliveryBar(\n \"Delivery: webhook-confirmed\",\n prog.delivery.confirmed,\n acceptedTotal,\n prog.audience.count,\n \"Delivery lags acceptance: grey is accepted but unconfirmed, and green fills in as receipts arrive.\",\n )}\n </div>\n ${watchCountsHtml(c)}\n <div class=\"whealth${failureCount ? \" has-failures\" : \"\"}\"><span class=\"whealth-dot\"></span><span>${providerText} \u00B7 ${\n failureCount\n ? `${failureCount.toLocaleString()} bounced / unsent / complained`\n : \"no delivery failures\"\n }</span></div>`;\n}\nfunction watchMetaHtml(send: SendView): Html {\n const started = send.started_at ? `Started ${fmt(send.started_at)}` : \"Sending now\";\n return html`${started} \u00B7 ${send.audience.count.toLocaleString()} recipients`;\n}\nfunction watchHtml(send: SendView): Html {\n const prog = send;\n return html`\n <div class=\"editor-head\">\n <a href=\"#/sent\" class=\"back\">\u2190 Sent</a>\n ${can(prog, \"resolve\") ? html`<button type=\"button\" class=\"primary\" id=\"resolveBtn\">Resolve\u2026</button>` : null}\n </div>\n <div class=\"card rec-card watch-card\">\n <div class=\"rec-head\">\n <div class=\"watch-title\"><h1>${send.subject || html`<em>untitled</em>`}</h1><span id=\"watchPill\">${phasePill(prog.phase)}</span></div>\n <div class=\"rec-meta\" id=\"watchMeta\">${watchMetaHtml(send)}</div>\n </div>\n <div id=\"watchBody\">${watchBodyHtml(prog)}</div>\n <p class=\"rec-note muted\">This view updates live while the send is in flight. Delivery receipts keep arriving after dispatch finishes, and the record stays accurate as they settle.</p>\n </div>`;\n}\n\n// Wire the header's Resolve control (present only while the server offers it, \u00A712). It\n// reuses the same modal the Sent page uses, with the wedged count the send reports.\nfunction wireWatchHeader(\n root: HTMLElement,\n send: SendView,\n remount: () => void,\n signal: AbortSignal,\n): void {\n const rb = root.querySelector<HTMLButtonElement>(\"#resolveBtn\");\n if (rb) {\n const count = conditionOf(send, \"wedged\")?.count ?? send.counts.in_flight;\n rb.onclick = () => openResolveModal({ id: send.id, c_in_flight: count }, remount, signal);\n }\n}\nfunction paintWatch(root: HTMLElement, prog: SendView): void {\n const pill = root.querySelector(\"#watchPill\");\n if (pill) {\n setHtml(pill, phasePill(prog.phase));\n }\n const meta = root.querySelector(\"#watchMeta\");\n if (meta) {\n setHtml(meta, watchMetaHtml(prog));\n }\n const body = root.querySelector(\"#watchBody\");\n if (body) {\n setHtml(body, watchBodyHtml(prog));\n }\n}\n\nfunction startWatch(\n data: SendResponse,\n root: HTMLElement,\n signal: AbortSignal,\n remount: () => void,\n): void {\n setHtml(root, watchHtml(data.send));\n wireWatchHeader(root, data.send, remount, signal);\n // The latest report, repainted on the clock between reports: a halt's \"next retry in \u2026\"\n // counts down while nothing about the send is written.\n let latest = data.send;\n every(1000, () => paintWatch(root, latest), signal);\n // From the page's own read, the send-state layer reports each change to the send, at the\n // server's pace; the watch keeps no poll of its own. Once dispatch ends it re-enters as the\n // frozen record (which settles).\n followSend(\n data,\n {\n update: (fresh) => {\n if (fresh.status !== \"sending\") {\n remount();\n return;\n }\n latest = fresh;\n // If Resolve just became possible, or stopped being, the header changes: re-render.\n if (can(fresh, \"resolve\") !== Boolean(root.querySelector(\"#resolveBtn\"))) {\n setHtml(root, watchHtml(fresh));\n wireWatchHeader(root, fresh, remount, signal);\n } else {\n paintWatch(root, fresh);\n }\n },\n removed: remount,\n stale: remount,\n },\n signal,\n );\n}\n\n// The delivery-outcome tiles + reconciliation line: the frozen record's body, factored\n// so the settling poll can repaint them in place as late receipts arrive.\nfunction outcomeTilesHtml(outcomes: DeliveryOutcomes): Html {\n const tiles = [\n { n: outcomes.recipients, label: \"Recipients\", cls: \"\", sw: \"neutral\" },\n { n: outcomes.delivered, label: \"Delivered\", cls: \"ok\", sw: \"ok\" },\n { n: outcomes.bounced, label: \"Bounced\", cls: \"warn\", sw: \"warn\" },\n { n: outcomes.complained, label: \"Complained\", cls: \"danger\", sw: \"danger\" },\n { n: outcomes.unsent, label: \"Unsent\", cls: \"\", sw: \"neutral\" },\n ];\n return html`${tiles.map(\n (t) =>\n html`<div class=\"rec-tile\"><div class=\"rec-n ${t.cls}\">${t.n.toLocaleString()}</div><div class=\"rec-l\"><span class=\"rec-sw sw-${t.sw}\"></span>${t.label}</div></div>`,\n )}`;\n}\nfunction outcomeReconHtml(outcomes: DeliveryOutcomes): string {\n const total = outcomes.recipients;\n const parts = [`${outcomes.delivered.toLocaleString()} delivered`];\n if (outcomes.bounced) {\n parts.push(`${outcomes.bounced.toLocaleString()} bounced`);\n }\n if (outcomes.complained) {\n parts.push(`${outcomes.complained.toLocaleString()} complained`);\n }\n if (outcomes.unsent) {\n parts.push(`${outcomes.unsent.toLocaleString()} unsent`);\n }\n // Each leftover recipient is named for what it is: only an accepted one can still get a\n // receipt. A skipped one was left out at hand-off (unsubscribed or suppressed by then)\n // and never mailed, so it is final (SPEC \u00A76, \u00A78), and one in flight is still being handed.\n if (outcomes.accepted) {\n parts.push(`${outcomes.accepted.toLocaleString()} accepted, awaiting a delivery receipt`);\n }\n if (outcomes.skipped) {\n parts.push(\n `${outcomes.skipped.toLocaleString()} skipped (unsubscribed or suppressed before hand-off, never mailed)`,\n );\n }\n if (outcomes.in_flight) {\n parts.push(`${outcomes.in_flight.toLocaleString()} in flight`);\n }\n const reconciled = `All ${total.toLocaleString()} accounted for: ${parts.join(\", \")}.`;\n // The suppression note is scoped to what actually suppresses: hard bounces and complaints,\n // never soft bounces (counted, left on the list, SPEC \u00A78). The aggregate counters don't\n // split soft from hard, so gate on any bounce or complaint and state the rule rather than\n // claim a count; a clean send says nothing about suppression at all.\n const suppression =\n outcomes.bounced || outcomes.complained\n ? \" Hard bounces and spam complaints are suppressed automatically, so those addresses won\u2019t be mailed again.\"\n : \"\";\n return `${reconciled}${suppression}`;\n}\n// Whether two readings of the record agree on every bucket, so a settling tick that\n// brought nothing new leaves the page as it is.\nfunction sameOutcomes(a: DeliveryOutcomes, b: DeliveryOutcomes): boolean {\n return (Object.keys(a) as (keyof DeliveryOutcomes)[]).every((k) => a[k] === b[k]);\n}\n\ninterface Outcome {\n label: string;\n sw: string;\n hint?: string;\n}\n\n// The per-recipient record: the outcome tiles summarize, this shows the actual rows. A\n// row's OUTCOME is derived from the same two facts the tiles bucket (the webhook `event`\n// winning over the send-loop `status`), so a row reads the same bucket (and reuses the\n// same swatch palette) as its tile. A bounce splits soft vs hard on `bounce_kind`, the\n// provider's permanent/transient signal frozen onto the row when the event landed (SPEC\n// \u00A78), a fact of THIS send, not a read of the current suppression list. Null = unknown.\nfunction deliveryOutcome(r: DeliveryRecord): Outcome {\n if (r.event === \"delivered\") {\n return { label: \"Delivered\", sw: \"ok\" };\n }\n if (r.event === \"bounced\") {\n if (r.bounce_kind === \"hard\") {\n return { label: \"Hard bounce\", sw: \"warn\", hint: \"permanent, so the address is suppressed\" };\n }\n if (r.bounce_kind === \"soft\") {\n return { label: \"Soft bounce\", sw: \"warn\", hint: \"transient, so counted but not suppressed\" };\n }\n return { label: \"Bounce\", sw: \"warn\" };\n }\n if (r.event === \"complained\") {\n return { label: \"Complained\", sw: \"danger\", hint: \"suppressed\" };\n }\n switch (r.status) {\n case \"unsent\":\n return { label: \"Unsent\", sw: \"neutral\" };\n case \"skipped\":\n return { label: \"Skipped\", sw: \"neutral\" };\n case \"accepted\":\n return { label: \"Accepted\", sw: \"sending\", hint: \"awaiting a delivery receipt\" };\n default:\n return { label: \"In flight\", sw: \"sending\" };\n }\n}\n\n// The three view tabs the record opens on, failures first (the rows that went wrong).\nconst DELIVERY_TABS: { v: DeliveryView; label: string }[] = [\n { v: \"failures\", label: \"Failures\" },\n { v: \"delivered\", label: \"Delivered\" },\n { v: \"all\", label: \"All\" },\n];\n\n/** The per-recipient list's state: the list controls' plus which bucket it shows. */\ninterface DeliveryListState extends ListState {\n view: DeliveryView;\n}\n\n// A positive/neutral empty state per view: an empty \"failures\" list is good news, not a gap.\nfunction deliveryEmpty(dstate: DeliveryListState): string {\n if (dstate.search) {\n return \"No recipients match that address.\";\n }\n if (dstate.view === \"failures\") {\n return \"No delivery failures. Every recipient was accepted or delivered.\";\n }\n if (dstate.view === \"delivered\") {\n return \"No delivery receipts confirmed yet.\";\n }\n return \"No recipients on this send.\";\n}\n\nfunction recordDeliveryQuery(d: DeliveryListState): string {\n const p = new URLSearchParams();\n p.set(\"view\", d.view);\n const term = (d.search || \"\").trim();\n if (term) {\n p.set(\"search\", term);\n }\n if (d.sort) {\n p.set(\"sort\", d.sort);\n p.set(\"dir\", d.dir ?? \"asc\");\n }\n p.set(\"limit\", String(d.limit));\n p.set(\"offset\", String(d.offset));\n return p.toString();\n}\n\nfunction deliveryRowsHtml(rows: DeliveryRecord[], dstate: DeliveryListState): Html {\n const body = rows.map((r) => {\n const o = deliveryOutcome(r);\n // The label names the bucket; its consequence (suppressed or not, awaiting a receipt)\n // is taught once in prose by the reconciliation line above and kept on the row only as\n // a hover tooltip, so a row stays a clean one-liner instead of restating the taxonomy\n // on every line.\n const outcome = o.hint\n ? html`<span class=\"rec-out\" title=\"${o.hint}\"><span class=\"rec-sw sw-${o.sw}\"></span>${o.label}</span>`\n : html`<span class=\"rec-out\"><span class=\"rec-sw sw-${o.sw}\"></span>${o.label}</span>`;\n const detail = r.error || r.event_detail || \"\";\n const when = r.event_at ? fmt(r.event_at) : \"\";\n return html`<tr><td class=\"rec-email\">${r.email}</td><td>${outcome}</td><td class=\"muted\">${detail || \"\u2014\"}</td><td class=\"muted\">${when || \"\u2014\"}</td></tr>`;\n });\n return html`<div class=\"table-wrap\"><table class=\"list-table rec-people-table\"><colgroup><col><col class=\"c-out\"><col><col class=\"c-date\"></colgroup><thead><tr>${th(\n \"Recipient\",\n \"email\",\n dstate,\n )}<th class=\"c-out\">Outcome</th><th>Detail</th><th class=\"c-date\">When</th></tr></thead><tbody>${body}</tbody></table></div>`;\n}\n\nfunction renderFrozenRecord(\n id: string,\n data: SendResponse,\n root: HTMLElement,\n signal: AbortSignal,\n remount: () => void,\n): void {\n const { send, outcomes } = data;\n if (send.status === \"canceled\") {\n renderCanceled(send, root);\n return;\n }\n // The published post, which the send's view links once it has started.\n const archive = send.links.archive;\n const total = outcomes.recipients;\n const sentAt = send.completed_at ?? send.fire_at;\n\n setHtml(\n root,\n html`\n <div class=\"editor-head\">\n <a href=\"#/sent\" class=\"back\">\u2190 Sent</a>\n ${archive ? html`<button type=\"button\" class=\"primary\" id=\"viewPublished\">View published post \u2197</button>` : null}\n </div>\n <div class=\"card rec-card\">\n <div class=\"rec-head\">\n <div class=\"watch-title\"><h1>${send.subject || html`<em>untitled</em>`}</h1><span id=\"recPill\">${phasePill(send.phase)}</span></div>\n <div class=\"rec-meta\">Sent ${fmt(sentAt)} \u00B7 ${total.toLocaleString()} recipients</div>\n </div>\n <p class=\"rec-tiles-cap muted\">Delivery outcomes keep updating as receipts arrive; the audience at fire and the published post are fixed.</p>\n <div class=\"rec-tiles\">${outcomeTilesHtml(outcomes)}</div>\n <p class=\"rec-recon muted\">${outcomeReconHtml(outcomes)}</p>\n <div class=\"rec-actions\">\n <button type=\"button\" class=\"ghost\" id=\"csvBtn\">Export recipients (CSV)</button>\n </div>\n <div class=\"rec-people\">\n <div class=\"rec-people-head\">\n <h2 class=\"rec-people-title\">Recipients</h2>\n <div class=\"rec-views\" role=\"group\" aria-label=\"Which recipients to show\">\n ${DELIVERY_TABS.map(\n (t, i) =>\n html`<button type=\"button\" class=\"rec-view-btn\" data-view=\"${t.v}\" aria-pressed=\"${i === 0 ? \"true\" : \"false\"}\">${t.label}</button>`,\n )}\n </div>\n </div>\n <input class=\"rec-people-search\" type=\"search\" placeholder=\"Search email\u2026\" aria-label=\"Search recipients by email\" autocomplete=\"off\">\n <div id=\"recRows\" class=\"muted\">Loading\u2026</div>\n <div id=\"recPager\"></div>\n </div>\n <p class=\"rec-note muted\">This is the record of what went out: the published post is the exact frozen copy readers received, and nothing here is editable.</p>\n </div>`,\n );\n\n const viewBtn = document.getElementById(\"viewPublished\");\n if (viewBtn && archive) {\n viewBtn.onclick = () => window.open(archive, \"_blank\", \"noopener\");\n }\n\n // The per-recipient record, its own paged/filtered state. Default view is \"failures\" so\n // the rows that went wrong lead; default sort mirrors the CSV (email asc). It reloads on\n // interaction (a view/search/sort/page change, or re-clicking the view), and on each\n // settling refresh that moved the tiles, so the list follows the counts above it. The\n // two are separate reads, so between refreshes they can differ by what one tick brought.\n const dstate: DeliveryListState = {\n view: \"failures\",\n search: \"\",\n sort: \"email\",\n dir: \"asc\",\n offset: 0,\n limit: 50,\n };\n const rowsEl = $(\"#recRows\", root);\n const recPagerEl = $(\"#recPager\", root);\n // Loads can overlap (a refresh's reload racing the reader's click), so only the latest\n // paints. A load the reader started that a refresh then overtook still owes them an\n // answer, so the refresh's failure is theirs to see.\n let latest = 0;\n let readerWaiting = false;\n const load = async (background: boolean): Promise<void> => {\n const mine = ++latest;\n if (!background) {\n readerWaiting = true;\n }\n try {\n const d = await api<DeliveryListResponse>(\n `/api/sends/${id}/deliveries?${recordDeliveryQuery(dstate)}`,\n { signal },\n );\n if (mine !== latest) {\n return;\n }\n // The reader's page, past the end now that the total shrank: the last page instead.\n const last =\n d.page.total > 0 ? Math.floor((d.page.total - 1) / d.page.limit) * d.page.limit : 0;\n if (!d.deliveries.length && dstate.offset > last) {\n dstate.offset = last;\n return load(background);\n }\n readerWaiting = false;\n keepFocus(() => {\n if (!d.deliveries.length) {\n setHtml(rowsEl, html`<p class=\"rec-people-empty muted\">${deliveryEmpty(dstate)}</p>`);\n setHtml(recPagerEl, html``);\n return;\n }\n setHtml(rowsEl, deliveryRowsHtml(d.deliveries, dstate));\n wireSort(rowsEl, dstate, loadDeliveries);\n renderPager(recPagerEl, dstate, d.page, loadDeliveries);\n });\n } catch (e) {\n // A refresh that fails leaves the rows the reader is reading; the next refresh that\n // moves the counts tries again. Only a load the reader is waiting on says so.\n if (mine === latest && (!background || readerWaiting)) {\n readerWaiting = false;\n renderError(rowsEl, message(e), loadDeliveries);\n }\n }\n };\n const loadDeliveries = () => load(false);\n // A repaint replaces the table and the pager, so a keyboard reader on a sort header or a\n // pager button would lose their place: put focus back on the same control, if it's there.\n const keepFocus = (paint: () => void) => {\n const had = document.activeElement;\n const within = had instanceof HTMLElement && (rowsEl.contains(had) || recPagerEl.contains(had));\n const sortKey = within ? had.dataset.sort : undefined;\n const pagerCls = within\n ? [\"pager-prev\", \"pager-next\"].find((c) => had.classList.contains(c))\n : undefined;\n paint();\n const again = sortKey\n ? rowsEl.querySelector<HTMLButtonElement>(`.th-sort[data-sort=\"${sortKey}\"]`)\n : pagerCls\n ? recPagerEl.querySelector<HTMLButtonElement>(`.${pagerCls}`)\n : null;\n if (again && !again.disabled) {\n again.focus();\n }\n };\n const viewButtons = $$<HTMLButtonElement>(\".rec-view-btn\", root);\n for (const b of viewButtons) {\n b.onclick = () => {\n const v = b.dataset.view;\n if (v === \"failures\" || v === \"delivered\" || v === \"all\") {\n dstate.view = v;\n }\n dstate.offset = 0;\n for (const x of viewButtons) {\n x.setAttribute(\"aria-pressed\", String(x === b));\n }\n loadDeliveries();\n };\n }\n const recSearch = root.querySelector<HTMLInputElement>(\".rec-people-search\");\n if (recSearch) {\n let t: number | undefined;\n recSearch.oninput = () => {\n clearTimeout(t);\n t = setTimeout(() => {\n dstate.search = recSearch.value;\n dstate.offset = 0;\n loadDeliveries();\n }, 250);\n };\n }\n loadDeliveries();\n\n const csvBtn = $<HTMLButtonElement>(\"#csvBtn\");\n csvBtn.onclick = () =>\n busy(csvBtn, \"Exporting\u2026\", async () => {\n try {\n const csv = await apiFile(send.links.deliveries_csv);\n const url = URL.createObjectURL(new Blob([csv.text], { type: \"text/csv\" }));\n const a = document.createElement(\"a\");\n a.href = url;\n // The server's name for it (the archive slug), so the file is named the same\n // however it's fetched.\n a.download = csv.filename ?? \"deliveries.csv\";\n a.click();\n setTimeout(() => URL.revokeObjectURL(url), 10000);\n } catch (e) {\n // The API names only what it didn't find (\"send\"), which says nothing on its own.\n toast(\n e instanceof ApiError && e.status === 404\n ? \"This send no longer exists, so there are no deliveries to export.\"\n : message(e),\n );\n }\n });\n\n // The record keeps absorbing delivery receipts after dispatch (\u00A76), and follows them\n // through the send-state layer at the server's pace (DESIGN \u00A74): closely while they\n // arrive, easing off as they slow, then about once a minute, for as long as the page is\n // open, since a complaint can land long after the last delivery. Each report carries the\n // send's counters, which give every tile, so a read that brings nothing costs nothing\n // here. One that moved a count repaints the tiles and reloads the list on the reader's\n // page, in their view, search, and sort; the pill turns from Settling to Complete in place.\n let shown = outcomes;\n followSend(\n data,\n {\n update(fresh) {\n if (fresh.status === \"sending\") {\n remount(); // resumed (a Resolve that put recipients back, say) \u2192 back to the watch\n return;\n }\n setHtml($(\"#recPill\", root), phasePill(fresh.phase));\n const now = outcomesOf(fresh);\n if (sameOutcomes(shown, now)) {\n return;\n }\n shown = now;\n setHtml($(\".rec-tiles\", root), outcomeTilesHtml(now));\n $(\".rec-recon\", root).textContent = outcomeReconHtml(now);\n void load(true);\n },\n removed: remount,\n stale: remount,\n },\n signal,\n );\n}\n\n/** A sent send's outcomes from its counters: the same buckets as the record's own read,\n * one per recipient, over the audience fixed at fire. As there, a recipient not yet handed\n * off (pending) counts as in flight. */\nfunction outcomesOf(send: SendView): DeliveryOutcomes {\n const c = send.counts;\n return {\n recipients: send.audience.count,\n delivered: c.delivered,\n bounced: c.bounced,\n complained: c.complained,\n unsent: c.unsent,\n skipped: c.skipped,\n accepted: c.accepted,\n in_flight: c.pending + c.in_flight,\n };\n}\n\n/**\n * A canceled send's page: it never fired, so there is no audience, outcome, or published\n * post to show, only that it was canceled, when, and when it would have sent.\n */\nfunction renderCanceled(send: SendView, root: HTMLElement): void {\n setHtml(\n root,\n html`\n <div class=\"editor-head\">\n <a href=\"#/sent\" class=\"back\">\u2190 Sent</a>\n </div>\n <div class=\"card rec-card\">\n <div class=\"rec-head\">\n <div class=\"watch-title\"><h1>${send.subject || html`<em>untitled</em>`}</h1><span id=\"recPill\">${phasePill(send.phase)}</span></div>\n <div class=\"rec-meta\">Canceled ${fmt(send.completed_at)} \u00B7 was scheduled for ${fmt(send.fire_at)}</div>\n </div>\n <p class=\"rec-note muted\">This send was canceled before it fired, so no one was mailed.</p>\n </div>`,\n );\n}\n", "// The shared unsaved-changes bar.\n\nimport { html, setHtml } from \"./html\";\nimport { busy } from \"./widgets\";\n\n/** A control the page always carries (public/dashboard/index.html); missing means the wrong page. */\nfunction control<T extends HTMLElement>(id: string): T {\n const el = document.getElementById(id);\n if (!el) {\n throw new Error(`the admin page has no #${id}`);\n }\n return el as T;\n}\n\nexport interface SavebarOptions {\n /** Runs inside busy() on the shared Save button; call setDirty(false) on success or showError() on a rejection. */\n onSave: () => unknown;\n onDiscard: () => void;\n saveLabel?: string;\n discardLabel?: string;\n}\n\n/** What a mounted page drives the bar with; inert once another page has attached. */\nexport interface SavebarHandle {\n setDirty(dirty: boolean): void;\n showError(text: string): void;\n}\n\n// One full-width banner fixed to the bottom of the viewport (markup in index.html),\n// shared by every surface with an explicit save + a revertible baseline: Settings\n// and the Template page. A page attaches once on mount, then drives it \u2014 setDirty()\n// slides it up while there are unsaved changes, showError() keeps it up after a\n// rejected save and says why (right beside Save). The bar detaches when the mount's\n// signal aborts, so a page owns it exactly as long as it is mounted. The post editor\n// keeps its own autosave + conflict model (see renderEditor) and deliberately does not\n// use this \u2014 a \"Save / Discard against a baseline\" bar doesn't fit continuous autosave.\nexport const savebar = (() => {\n const el = control<HTMLElement>(\"savebar\");\n const msgEl = el.querySelector(\".savebar-msg\");\n const saveBtn = control<HTMLButtonElement>(\"savebarSave\");\n const discardBtn = control<HTMLButtonElement>(\"savebarDiscard\");\n // Bumped on every attach/detach so a stale async handler (a save that resolves\n // after the user navigated away) can never drive a bar a new page now owns.\n let token = 0;\n\n const setMsg = (text: string) => {\n if (msgEl) {\n setHtml(msgEl, html`<span class=\"savebar-dot\"></span><span>${text}</span>`);\n }\n };\n const reveal = () => {\n el.hidden = false;\n // Arm the slide-up: un-hide, then force a reflow so the off-screen base transform\n // is the established start state before .show flips it (a hidden \u2192 .show toggle in\n // one tick wouldn't transition). A synchronous reflow works even when the tab is\n // backgrounded and requestAnimationFrame is paused.\n void el.offsetHeight;\n el.classList.add(\"show\");\n };\n\n function detach(): void {\n token++;\n el.classList.remove(\"show\", \"is-error\");\n el.hidden = true;\n saveBtn.onclick = null;\n discardBtn.onclick = null;\n // Drop the bottom padding the page reserved for the (fixed) bar.\n document.body.classList.remove(\"has-savebar\");\n }\n\n /** Attach the bar to the mounting page for the life of its signal; returns the handle the page drives. */\n function attach(\n { onSave, onDiscard, saveLabel = \"Save changes\", discardLabel = \"Discard\" }: SavebarOptions,\n signal: AbortSignal,\n ): SavebarHandle {\n if (signal.aborted) {\n return { setDirty() {}, showError() {} }; // the page is already gone: nothing to drive\n }\n const mine = ++token;\n signal.addEventListener(\n \"abort\",\n () => {\n if (token === mine) {\n detach();\n }\n },\n { once: true },\n );\n saveBtn.textContent = saveLabel;\n discardBtn.textContent = discardLabel;\n el.classList.remove(\"show\", \"is-error\");\n setMsg(\"You have unsaved changes.\");\n el.hidden = true; // revealed on the first setDirty(true) / showError()\n // Reserve bottom room on the content for the whole time this page is mounted, so\n // the last section never slides under the fixed bar (no shift as it toggles).\n document.body.classList.add(\"has-savebar\");\n saveBtn.onclick = () => busy(saveBtn, \"Saving\u2026\", () => Promise.resolve(onSave()));\n discardBtn.onclick = () => onDiscard();\n const alive = () => token === mine;\n return {\n setDirty(dirty) {\n if (!alive()) {\n return;\n }\n el.classList.remove(\"is-error\");\n if (dirty) {\n setMsg(\"You have unsaved changes.\");\n reveal();\n } else {\n el.classList.remove(\"show\");\n }\n },\n showError(text) {\n if (!alive()) {\n return;\n }\n el.classList.add(\"is-error\");\n setMsg(text);\n reveal();\n },\n };\n }\n\n return { attach };\n})();\n", "// What the Settings page's sections share: the page's live state against its saved\n// baseline, the hooks a section answers so one save bar can commit them all, and the head\n// every section wears.\n\nimport type { ConfirmationEmailCopy, SettingsResponse, SettingsView } from \"../../shared/settings\";\nimport { describeError } from \"../api\";\nimport { type Html, html } from \"../ui/html\";\nimport { type IconName, icon } from \"../ui/icons\";\n\n/** The persisted fields the save bar tracks against their saved baseline. */\nexport interface SettingsBaseline {\n name: string;\n tagline: string;\n address: string;\n recipients: string[];\n confirmation: ConfirmationEmailCopy;\n /** Where notifications about sends go (SPEC \u00A78); \"\" for none. */\n notifyTo: string;\n}\n\n/** The page's live, in-memory state: the baseline fields plus the logo, which saves on\n * its own the moment it changes. */\nexport interface SettingsFormState extends SettingsBaseline {\n logoUrl: string;\n}\n\n/** What a section is handed: the page's read, its live state, and the way back to the\n * page when that state changes. */\nexport interface SettingsPage {\n data: SettingsResponse;\n state: SettingsFormState;\n /** The state as last saved: what Discard returns to, and what a test is sent from. */\n saved(): SettingsBaseline;\n /** The From display name as the email resolves it, \"\" for a bare address. What a reader\n * sees falls back to this, so it never shows a name the publisher didn't choose. */\n fromDisplay: string;\n /** The same with a visible placeholder, for what only the publisher sees. */\n fromName: string;\n /** The state changed: every section repaints what it derives from it, and the save\n * bar rises or falls. */\n changed(): void;\n /** A save landed outside the save bar (the logo's): the page adopts the saved settings. */\n applySaved(settings: SettingsView): void;\n}\n\n/** What a section answers once mounted. `repaint` redraws what it derives from the state\n * (a preview, a snippet) and runs on every change; `reset` puts its fields back to the\n * state after a save adopts the server's result or a discard restores the baseline. */\nexport interface SectionHandle {\n repaint?(): void;\n reset?(): void;\n}\n\nexport const message = describeError;\n\n// A bare attribute is markup, not text: spelled once as markup so it can be interpolated.\nexport const HIDDEN = html` hidden`;\n\n/** A section's intent chip: editable, read-only, copy-out, or a preview. */\nexport function chip(kind: IconName, label: string): Html {\n return html`<span class=\"set-chip ${kind}\">${icon(kind)}${label}</span>`;\n}\n\n/** A section's head: its title, its chips, and the rule that runs to the edge. */\nexport function secHead(title: string, chipHtml: Html, extra: Html | null = null): Html {\n return html`<div class=\"set-sec-head\"><h2 class=\"set-sec-title\">${title}</h2>${chipHtml}${extra}<span class=\"set-rule\"></span></div>`;\n}\n\n/** The letter an inbox row shows for a sender with no avatar. */\nexport function monogram(name: string, fallback: string): string {\n return (String(name || fallback).trim()[0] || \"K\").toUpperCase();\n}\n", "// Settings' Confirmation email section (SPEC \u00A77): the double opt-in email, preview-first\n// with a Preview | Edit toggle. Words only: Kestrel owns the layout and inserts the confirm\n// link, so the fields can never break double opt-in. The rendered card is byte-honest to\n// the transactional layout in src/emails/system.ts, in both themes (the card's dark palette\n// lives in styles.css, tracking the dashboard theme).\n\nimport { $ } from \"../ui/dom\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { chip, monogram, type SectionHandle, type SettingsPage, secHead } from \"./page\";\n\nexport function confirmationSection(page: SettingsPage): Html {\n const { state, fromName } = page;\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Confirmation email\", chip(\"editable\", \"Editable\"))}\n <p class=\"set-lede\">The email that asks a new subscriber to confirm. Edit the wording as you see fit. Kestrel adds the confirmation link.</p>\n <div class=\"set-preview\">\n <div class=\"set-preview-bar set-ce-bar\">\n <div class=\"set-ce-modetog\" role=\"group\" aria-label=\"Confirmation email view\">\n <button type=\"button\" class=\"set-ce-modebtn\" id=\"ceTabPreview\" aria-pressed=\"true\">${icon(\"preview\")}Preview</button>\n <button type=\"button\" class=\"set-ce-modebtn\" id=\"ceTabEdit\" aria-pressed=\"false\">${icon(\"editable\")}Edit</button>\n </div>\n </div>\n <div id=\"cePreviewBody\">\n <div class=\"set-inbox\">\n <div class=\"set-inbox-avatar\" id=\"cePvAvatar\">${monogram(state.name, fromName)}</div>\n <div class=\"set-inbox-body\">\n <div class=\"set-inbox-top\"><span class=\"set-inbox-from\" id=\"cePvFrom\">${state.name || fromName}</span><span class=\"set-inbox-time\">now</span></div>\n <div class=\"set-inbox-subj\" id=\"cePvSubject\"></div>\n </div>\n </div>\n <div class=\"set-ce-stage\">\n <div class=\"set-ce-card\">\n <div class=\"set-ce-mast\" id=\"cePvMast\" hidden>\n <div class=\"set-ce-mast-logo\" id=\"cePvMastLogo\"></div>\n <div>\n <div class=\"set-ce-mast-name\" id=\"cePvMastName\"></div>\n <div class=\"set-ce-mast-tag\" id=\"cePvMastTag\"></div>\n </div>\n </div>\n <p class=\"set-ce-msg\" id=\"cePvBody\"></p>\n <p class=\"set-ce-btnrow\"><a class=\"set-ce-btn\" id=\"cePvButton\" href=\"#\" onclick=\"return false\"></a></p>\n <p class=\"set-ce-foot\" id=\"cePvFoot\"></p>\n </div>\n </div>\n </div>\n <div id=\"ceEditBody\" hidden>\n <div class=\"set-ce-edit\">\n <div class=\"set-field\">\n <label for=\"ceSubject\">Subject line</label>\n <input id=\"ceSubject\" value=\"${state.confirmation.subject}\" maxlength=\"200\" autocomplete=\"off\">\n </div>\n <div class=\"set-field\">\n <label for=\"ceMessage\">Message</label>\n <textarea id=\"ceMessage\" rows=\"3\" maxlength=\"1000\">${state.confirmation.body}</textarea>\n <p class=\"field-hint\">The line above the confirm button.</p>\n </div>\n <div class=\"set-field\">\n <label for=\"ceButton\">Button label</label>\n <input id=\"ceButton\" value=\"${state.confirmation.buttonLabel}\" maxlength=\"80\" autocomplete=\"off\">\n </div>\n <div class=\"set-field\">\n <label for=\"ceFooter\">Reassurance line</label>\n <input id=\"ceFooter\" value=\"${state.confirmation.reassurance}\" maxlength=\"400\" autocomplete=\"off\">\n <p class=\"field-hint\">The quiet footer for anyone who didn\u2019t sign up. Leave blank to omit it.</p>\n </div>\n </div>\n <div class=\"set-ce-editfoot\">\n <button type=\"button\" class=\"ghost\" id=\"ceReset\">Reset to Kestrel default</button>\n <span class=\"grow\"></span>\n <button type=\"button\" class=\"ghost\" id=\"ceToPreview\">See preview \u2192</button>\n </div>\n </div>\n </div>\n </section>`;\n}\n\n/** The fields carry only words; the preview resolves a blank required field to the\n * built-in default (from the API) exactly as the send does, so it's never wordless. */\nexport function mountConfirmation(root: HTMLElement, page: SettingsPage): SectionHandle {\n const { state } = page;\n const ceDefault = page.data.settings.confirmationEmailDefault;\n const fields = {\n subject: $<HTMLInputElement>(\"#ceSubject\", root),\n message: $<HTMLTextAreaElement>(\"#ceMessage\", root),\n button: $<HTMLInputElement>(\"#ceButton\", root),\n footer: $<HTMLInputElement>(\"#ceFooter\", root),\n };\n const pv = {\n avatar: $(\"#cePvAvatar\", root),\n from: $(\"#cePvFrom\", root),\n subject: $(\"#cePvSubject\", root),\n body: $(\"#cePvBody\", root),\n button: $(\"#cePvButton\", root),\n foot: $(\"#cePvFoot\", root),\n mast: $(\"#cePvMast\", root),\n mastLogo: $(\"#cePvMastLogo\", root),\n mastName: $(\"#cePvMastName\", root),\n mastTag: $(\"#cePvMastTag\", root),\n };\n const repaint = () => {\n const c = state.confirmation;\n pv.avatar.textContent = monogram(state.name, page.fromName);\n pv.from.textContent = state.name || page.fromName;\n pv.subject.textContent = c.subject.trim() || ceDefault.subject || \"\";\n pv.body.textContent = c.body.trim() || ceDefault.body || \"\";\n pv.button.textContent = c.buttonLabel.trim() || ceDefault.buttonLabel || \"Confirm\";\n pv.foot.textContent = c.reassurance;\n pv.foot.hidden = !c.reassurance.trim();\n // The publication masthead (logo + name + tagline) up top. Mirrors masthead() in\n // src/emails/system.ts exactly: it uses the identity as the email resolves it (name\n // = publication name or the From display name, \"\" if neither), omits the logo cell\n // when there's no logo (rather than showing a monogram the email won't have), and\n // degrades to nothing when there's neither a name nor a logo.\n const mastName = state.name || page.fromDisplay;\n const hasLogo = Boolean(state.logoUrl);\n const showMast = hasLogo || Boolean(mastName);\n pv.mast.hidden = !showMast;\n if (showMast) {\n pv.mastLogo.hidden = !hasLogo;\n if (hasLogo) {\n setHtml(pv.mastLogo, html`<img src=\"${state.logoUrl}\" alt=\"\">`);\n }\n pv.mastName.textContent = mastName;\n pv.mastName.hidden = !mastName;\n pv.mastTag.textContent = state.tagline;\n pv.mastTag.hidden = !state.tagline;\n }\n };\n const onInput = () => {\n state.confirmation.subject = fields.subject.value;\n state.confirmation.body = fields.message.value;\n state.confirmation.buttonLabel = fields.button.value;\n state.confirmation.reassurance = fields.footer.value;\n page.changed();\n };\n for (const el of Object.values(fields)) {\n el.addEventListener(\"input\", onInput);\n }\n\n const editBody = $(\"#ceEditBody\", root);\n const previewBody = $(\"#cePreviewBody\", root);\n const tabEdit = $<HTMLButtonElement>(\"#ceTabEdit\", root);\n const tabPreview = $<HTMLButtonElement>(\"#ceTabPreview\", root);\n const setMode = (edit: boolean) => {\n editBody.hidden = !edit;\n previewBody.hidden = edit;\n tabEdit.setAttribute(\"aria-pressed\", String(edit));\n tabPreview.setAttribute(\"aria-pressed\", String(!edit));\n if (edit) {\n fields.subject.focus();\n }\n };\n tabPreview.onclick = () => setMode(false);\n tabEdit.onclick = () => setMode(true);\n $(\"#ceToPreview\", root).onclick = () => setMode(false);\n $(\"#ceReset\", root).onclick = () => {\n fields.subject.value = ceDefault.subject || \"\";\n fields.message.value = ceDefault.body || \"\";\n fields.button.value = ceDefault.buttonLabel || \"\";\n fields.footer.value = ceDefault.reassurance || \"\";\n onInput();\n };\n\n repaint();\n return {\n repaint,\n reset() {\n fields.subject.value = state.confirmation.subject;\n fields.message.value = state.confirmation.body;\n fields.button.value = state.confirmation.buttonLabel;\n fields.footer.value = state.confirmation.reassurance;\n repaint();\n },\n };\n}\n", "// Settings' \"Set at deploy\" group: the email sender and the instance's infrastructure, a\n// read-only reflection of the deploy-time config that only a redeploy changes (SPEC \u00A79).\n// Secrets never come down this wire (see routes/settings.ts).\n\nimport { DEFAULT_MIN_LEAD_MS, formatLead } from \"../../shared/sends\";\nimport type { DeploymentView, SimulationProfile } from \"../../shared/settings\";\nimport { connectProviderLink } from \"../deployment\";\nimport { type Html, html } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { chip, monogram, type SettingsPage, secHead } from \"./page\";\n\nconst PROVIDER_LABELS: Record<string, string> = {\n none: \"No email provider connected\",\n fake: \"Fake (dev, dead-end)\",\n ses: \"Amazon SES\",\n};\n\n/** The provider a deployment sends through, as Settings names it. */\nexport function providerLabel(provider: string): string {\n return PROVIDER_LABELS[provider] || provider;\n}\n\n/**\n * The Email provider row's value: the provider's name, the simulation a local server runs\n * in its place, or, while none is connected (SPEC \u00A79), the state and the step that ends it.\n */\nfunction providerValue(d: DeploymentView): Html {\n if (d.provider === \"none\") {\n return html`${providerLabel(d.provider)}. ${connectProviderLink(\"set-link\")} to start sending.`;\n }\n return html`${providerLabel(d.provider)}${d.simulation ? html`<span class=\"set-pill\">Simulating ${SIMULATION_LABELS[d.simulation.profile]}${d.simulation.faults === \"none\" ? \", no failures\" : \"\"}</span>` : null}`;\n}\n\n/** The provider a local send simulation models, as the Email provider row names it. */\nconst SIMULATION_LABELS: Record<SimulationProfile, string> = {\n generic: \"a generic provider\",\n ses: \"Amazon SES\",\n};\n\nfunction bareAddress(from: string): string {\n const m = String(from || \"\").match(/<([^>]+)>/);\n return m?.[1] ? m[1] : String(from || \"\");\n}\n\nexport function senderSection(page: SettingsPage): Html {\n const d = page.data.deployment;\n const { fromName } = page;\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Email sender\", chip(\"readonly\", \"Read-only \u00B7 set at deploy\"))}\n <div class=\"set-card sunken\">\n <div class=\"set-ro-note\">${icon(\"readonly\")}<span>The sender is set at deploy in <code>wrangler.jsonc</code>, with the AWS keys stored as secrets, so it can\u2019t be edited here. Change it in the <a class=\"set-link\" href=\"#/docs\">setup guide</a>, then redeploy. Credentials are never shown.</span></div>\n <div class=\"set-preview-bar\" style=\"background:transparent\">\n <span class=\"set-preview-lbl\">Inbox preview</span>\n <span class=\"set-preview-dot\">How readers see the sender</span>\n </div>\n <div class=\"set-inbox\">\n <div class=\"set-inbox-avatar\">${monogram(fromName, fromName)}</div>\n <div class=\"set-inbox-body\">\n <div class=\"set-inbox-top\"><span class=\"set-inbox-from\">${fromName}</span><span class=\"set-inbox-time\">9:02 AM</span></div>\n <div class=\"set-inbox-subj\">Your latest post: a sample subject line</div>\n <div class=\"set-inbox-snip\">The opening lines of your post show here as the inbox preview\u2026</div>\n <div class=\"set-inbox-addr\">${bareAddress(d.fromAddress)}</div>\n </div>\n </div>\n <div class=\"set-kv\" style=\"border-top:1px solid var(--line)\">\n <div class=\"set-kv-k\">From address</div><div class=\"set-kv-v\"><span class=\"mono\">${d.fromAddress}</span></div>\n <div class=\"set-kv-k\">Sending domain</div><div class=\"set-kv-v\"><span class=\"mono\">${d.sendingDomain}</span></div>\n <div class=\"set-kv-k\">Email provider</div><div class=\"set-kv-v\" id=\"providerValue\">${providerValue(d)}</div>\n </div>\n </div>\n </section>`;\n}\n\nexport function instanceSection(page: SettingsPage): Html {\n const d = page.data.deployment;\n const archiveBase = `${d.archiveOrigin || \"\"}${d.archiveBasePath || \"\"}`;\n const archiveIsDefault = d.archiveOrigin === d.appOrigin;\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Instance\", chip(\"readonly\", \"Read-only \u00B7 set at deploy\"))}\n <div class=\"set-card sunken\">\n <div class=\"set-ro-note\">${icon(\"info\")}<span>Deploy-time infrastructure, shown for reference. These live in your Worker config and never pass through the API. See the <a class=\"set-link\" href=\"#/docs/configuration\">setup guide</a> to change them.</span></div>\n <div class=\"set-kv\">\n <div class=\"set-kv-k\">App origin</div><div class=\"set-kv-v\"><span class=\"mono\">${d.appOrigin}</span></div>\n <div class=\"set-kv-k\">Archive URL base</div><div class=\"set-kv-v\"><span class=\"mono\">${archiveBase}</span>${archiveIsDefault ? html`<span class=\"set-pill\">default: app origin</span>` : null}</div>\n <div class=\"set-kv-k\">Image URL base</div><div class=\"set-kv-v\"><span class=\"mono\">${d.mediaPublicBase}</span></div>\n <div class=\"set-kv-k\">Auth mode</div><div class=\"set-kv-v\">${d.authMode === \"access\" ? \"Cloudflare Access\" : \"Local dev token\"}</div>\n <div class=\"set-kv-k\">Cloudflare Access</div><div class=\"set-kv-v\">${d.accessConfigured ? html`<span class=\"set-pill ok\">${icon(\"check\")}Configured</span>` : html`<span class=\"set-pill\">Not configured</span>`}</div>\n <div class=\"set-kv-k\">Minimum lead</div><div class=\"set-kv-v\">${formatLead(d.minLeadMs)}${d.minLeadMs === DEFAULT_MIN_LEAD_MS ? html`<span class=\"set-pill\">default</span>` : null}</div>\n </div>\n </div>\n </section>`;\n}\n", "// The re-make confirmation (SPEC \u00A76, \u00A79; DESIGN \u00A74): the in-use chip and the\n// confirm-then-save flow the Template and Settings pages share.\n\nimport type { InUseView, RemakeRequiredError, ScheduledSendRef } from \"../../shared/settings\";\nimport { ApiError } from \"../api\";\nimport { fmt } from \"../ui/format\";\nimport { type Html, html } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { modal, toast } from \"../ui/widgets\";\n\n/** The words a re-make confirmation uses for the commit: { action: \"Save\", gerund: \"Saving\", lead(n) }. */\nexport interface RemakeVerb {\n action: string;\n gerund: string;\n /** What is in use (\"This template is used by 2 posts \u2026\"). */\n lead: (n: number) => string;\n}\n\nfunction remakeRequired(e: unknown): RemakeRequiredError | null {\n if (e instanceof ApiError && e.status === 409) {\n const d = e.data as { error?: unknown } | null;\n if (d?.error === \"remake_required\") {\n return e.data as RemakeRequiredError;\n }\n }\n return null;\n}\n\n// What a save the page left before confirming says, wherever the publisher went.\nconst NOT_SAVED = \"Not saved: the change needed your confirmation\";\n\n/**\n * The other re-make refusal (SPEC \u00A79): a scheduled send is about to fire, so the save\n * waits until it has sent. A surface shows this one where it blocks (the save bar, the\n * logo's field error), never as a passing toast.\n */\nexport function isRemakeTooClose(e: unknown): boolean {\n if (!(e instanceof ApiError) || e.status !== 409) {\n return false;\n }\n const d = e.data as { error?: unknown } | null;\n return d?.error === \"remake_too_close\";\n}\n\n/**\n * A template or identity change reaches every scheduled email, and the server refuses\n * such a save until the client has acknowledged those sends by id (409 remake_required,\n * listing them). The flow is server-driven so the dashboard never decides which fields\n * count: `attempt(ack)` runs the write with no acknowledgement; a refusal opens the\n * confirmation with the list the server handed back; confirming retries with those ids;\n * a second refusal (a send scheduled meanwhile) asks again with the fresh list. Resolves\n * to the write's response, or to null when the publisher canceled (the caller then leaves\n * everything as it was). Any other refusal, including 409 remake_too_close (a send about\n * to fire), is rethrown for the surface's own blocking-error home. The confirmation\n * belongs to the page, so `signal` is the page's mount signal: leaving the page closes it,\n * which counts as a cancel, and nothing is saved that the publisher never confirmed; a\n * toast says so, since they are no longer on the page that would show it.\n */\nexport async function withRemakeConfirm<T>(\n attempt: (ack: string[] | null) => Promise<T>,\n verb: RemakeVerb,\n signal: AbortSignal,\n): Promise<T | null> {\n let ack: string[] | null = null;\n for (;;) {\n try {\n return await attempt(ack);\n } catch (err) {\n const required = remakeRequired(err);\n if (!required) {\n throw err;\n }\n const sends = required.sends || [];\n const ok = await confirmRemake(sends, verb, signal);\n if (!ok) {\n // A publisher who pressed Save and left before (or while) the confirmation showed\n // would otherwise never learn the save didn't happen. The toast region is the\n // app's, not the page's, so it shows on whatever page they went to.\n if (signal.aborted) {\n toast(NOT_SAVED);\n }\n return null;\n }\n ack = sends.map((s) => s.id);\n }\n }\n}\n\n// The confirmation itself (DESIGN \u00A74): one sentence on what the save reaches, the\n// scheduled posts by name, three short facts the publisher scans (what changes, what\n// stays, what's next), and the alternative (a second template, which Kestrel does not\n// offer yet).\nfunction confirmRemake(\n sends: ScheduledSendRef[],\n verb: RemakeVerb,\n signal: AbortSignal,\n): Promise<boolean> {\n const n = sends.length;\n const one = n === 1;\n const noun = `${n} scheduled email${one ? \"\" : \"s\"}`;\n return new Promise((resolve) => {\n const m = modal(\n html`<h3>Apply this change to ${noun}?</h3>\n <p class=\"hint\">${verb.lead(n)} ${verb.gerund} applies the change to ${one ? \"its email\" : \"their emails\"} too.</p>\n <ul class=\"remake-list\">${sends.map(\n (s) =>\n html`<li><span>${s.subject || html`<em>untitled</em>`}</span><span>sends ${fmt(s.fire_at)}</span></li>`,\n )}</ul>\n <dl class=\"remake-facts\">\n <div><dt>Changes</dt><dd>the look of ${one ? \"its email\" : \"their emails\"}</dd></div>\n <div><dt>Stays</dt><dd>${one ? \"its content and fire time\" : \"their content and fire times\"}</dd></div>\n <div><dt>Next</dt><dd>send yourself a fresh test${one ? \"\" : \" of each\"}</dd></div>\n </dl>\n <p class=\"hint\">To leave scheduled emails as they are while future posts change, you\u2019d need a second template, which Kestrel doesn\u2019t offer yet.</p>\n <div class=\"actions\"><button type=\"button\" id=\"rmCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"rmGo\">${verb.action} and apply</button></div>`,\n signal,\n );\n // The page is already gone, so the dialog never showed: nothing was confirmed.\n if (!m.el.isConnected) {\n resolve(false);\n return;\n }\n let settled = false;\n const done = (v: boolean) => {\n if (!settled) {\n settled = true;\n resolve(v);\n }\n };\n const cancel = m.el.querySelector<HTMLButtonElement>(\"#rmCancel\");\n const go = m.el.querySelector<HTMLButtonElement>(\"#rmGo\");\n if (cancel) {\n cancel.onclick = () => {\n m.close();\n done(false);\n };\n }\n if (go) {\n go.onclick = () => {\n m.close();\n done(true);\n };\n }\n // Backdrop click, Escape, and leaving the page close the modal without going through\n // a button.\n const obs = new MutationObserver(() => {\n if (!m.el.isConnected) {\n obs.disconnect();\n done(false);\n }\n });\n obs.observe(document.body, { childList: true });\n go?.focus();\n });\n}\n\n/** The template surface's lead: what is in use by the scheduled posts. */\nexport const REMAKE_TEMPLATE: RemakeVerb = {\n action: \"Save\",\n gerund: \"Saving\",\n lead: (n) =>\n `This template is used by ${n} post${n === 1 ? \" that\u2019s\" : \"s that are\"} already scheduled.`,\n};\n\n/** The identity surface's lead, for the action the page commits with. */\nexport const remakeIdentity = (action: string, gerund: string): RemakeVerb => ({\n action,\n gerund,\n lead: (n) =>\n `Your name, tagline, address, and logo ride inside every email, including ${n} post${n === 1 ? \" that\u2019s\" : \"s that are\"} already scheduled.`,\n});\n\n/**\n * The standing in-use chip (DESIGN \u00A78): the state a template or identity change would\n * reach, read from GET /api/settings `inUse`.\n */\nexport function inUseChip(inUse: InUseView | null | undefined): Html {\n const sends = inUse?.sends || [];\n if (!sends.length) {\n return html`<span class=\"set-chip\">${icon(\"info\")}No posts scheduled</span>`;\n }\n const n = sends.length;\n const title = inUse?.retry_after\n ? `One sends at ${fmt(inUse.retry_after)}; saving waits until it has sent.`\n : null;\n return title\n ? html`<span class=\"set-chip inuse\" title=\"${title}\"><span class=\"set-chip-dot\"></span>In use by ${n} scheduled post${n === 1 ? \"\" : \"s\"}</span>`\n : html`<span class=\"set-chip inuse\"><span class=\"set-chip-dot\"></span>In use by ${n} scheduled post${n === 1 ? \"\" : \"s\"}</span>`;\n}\n\n/**\n * After a save that applied to scheduled emails, the toast says how many and that a\n * test is needed again (DESIGN \u00A74); with none, the plain confirmation.\n */\nexport function savedToast(what: string, remade: readonly unknown[] | null | undefined): string {\n const n = (remade || []).length;\n if (!n) {\n return what;\n }\n return `${what} and applied to ${n} scheduled email${n === 1 ? \"\" : \"s\"}. Send yourself a test of each.`;\n}\n", "// The email's logo image, spelled once for the render path and the Template page's\n// sample preview, so the preview shows exactly the sign-off the email will carry.\n\nconst escapeAttr = (s: string): string =>\n s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n\n/**\n * What `{{ .Publication.Logo }}` fills: the logo as an `<img>`, or nothing at all when no\n * logo is set. An `<img src=\"\">` is a broken image in every mail client, and a sent\n * email is frozen into the permanent archive, so a missing logo renders no element\n * rather than an empty one. `alt` is the publication name.\n */\nexport function emailLogoHtml(logoUrl: string, name: string): string {\n if (!logoUrl) {\n return \"\";\n }\n return `<img class=\"logo\" src=\"${escapeAttr(logoUrl)}\" alt=\"${escapeAttr(name)}\" width=\"44\" height=\"44\" />`;\n}\n", "// The email template page: the sample preview, the example menu, and the template\n// editor.\n\nimport { emailLogoHtml } from \"../../shared/email_logo\";\nimport { publicationName } from \"../../shared/publication\";\nimport type {\n SettingsPatchBody,\n SettingsResponse,\n SettingsSavedResponse,\n TemplateTestResponse,\n} from \"../../shared/settings\";\nimport { api, describeError } from \"../api\";\nimport { notConnectedTestLine, providerNotConnected, withNoProviderNote } from \"../deployment\";\nimport { mount } from \"../lifecycle\";\nimport { appState } from \"../state\";\nimport { copyFace, copyWith } from \"../ui/copy\";\nimport { $, $$ } from \"../ui/dom\";\nimport { EMAIL_FRAME_SANDBOX, openLinksInNewTab } from \"../ui/email_frame\";\nimport { invalidAddressesMessage, notSentMessage, parseAddresses } from \"../ui/format\";\nimport { highlightTemplate } from \"../ui/highlight\";\nimport { escapeHtml, type Html, html, setHtml, unsafeHtml } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { savebar } from \"../ui/savebar\";\nimport { busy, modal, openMenu, renderError, toast } from \"../ui/widgets\";\nimport { inUseChip, REMAKE_TEMPLATE, savedToast, withRemakeConfirm } from \"./remake\";\n\nconst message = describeError;\n\n// The one layout each post ships inside, authored as an HTML template with\n// logic-less {{ }} placeholders over a fixed variable context. Logic-less means a\n// token is only ever swapped for its value \u2014 nothing executes \u2014 which is why this\n// is a template editor, not a WYSIWYG. Edits commit through the shared save bar. A\n// send renders the saved template through the single render path (SPEC \u00A75, I5) and\n// freezes the result at schedule (I3); the sample here only fills the placeholders\n// with sample values in the browser, so it shows the layout, and a test email is the\n// real render.\n\ninterface TemplateVar {\n token: string;\n desc: string;\n}\ninterface TemplateVarGroup {\n group: string;\n vars: TemplateVar[];\n}\n// The variables a template can drop in, grouped for the reference panel.\nconst EMAIL_TEMPLATE_VARS: TemplateVarGroup[] = [\n {\n group: \"Post\",\n vars: [\n {\n token: \"{{ .Post.Body }}\",\n desc: \"Your post\u2019s Markdown, rendered to HTML.\",\n },\n { token: \"{{ .Post.Subject }}\", desc: \"The post\u2019s subject line.\" },\n ],\n },\n {\n group: \"Publication\",\n vars: [\n { token: \"{{ .Publication.Name }}\", desc: \"Publication name (from Identity, above).\" },\n { token: \"{{ .Publication.Tagline }}\", desc: \"Your tagline.\" },\n {\n token: \"{{ .Publication.Logo }}\",\n desc: \"Your logo as an image, or nothing when no logo is set.\",\n },\n { token: \"{{ .Publication.LogoURL }}\", desc: \"Absolute URL of your logo, or empty.\" },\n {\n token: \"{{ .Publication.Address }}\",\n desc: \"Your mailing address, if you\u2019ve added one in Settings; empty otherwise. Keep it in the footer if you send promotional email.\",\n },\n ],\n },\n {\n group: \"Email\",\n vars: [\n { token: \"{{ .Email.SentTo }}\", desc: \"The recipient\u2019s address (filled per send).\" },\n { token: \"{{ .Email.UnsubscribeURL }}\", desc: \"Their one-click unsubscribe link.\" },\n { token: \"{{ .Email.ViewInBrowserURL }}\", desc: \"The web version\u2019s permanent URL.\" },\n ],\n },\n {\n group: \"Email only\",\n vars: [\n {\n token: \"{{ if .IsEmail }} \u2026 {{ end }}\",\n desc: \"Reports whether this is the email or the web version. Put your unsubscribe link and address inside; the web version leaves them out.\",\n },\n ],\n },\n];\n\n// Two starting points; the publisher edits the HTML freely from there. Identity sits\n// at the FOOT (a sign-off), so the email stays faithful to today's masthead-free top.\n// The example templates are authored with a <style> block + classes \u2014 clean to read\n// and edit. A real send can't rely on a <style> block (Gmail/Outlook strip or ignore\n// it), so the real engine (a later step) would INLINE these rules at render time:\n// author with a stylesheet, inline on the way out \u2014 the standard email pattern, and\n// what src/render/template.ts already does by hand (inline base + a <style> block\n// only for what inline can't express, like dark mode). The preview renders the\n// template as-is in an isolated iframe, so a <style> block behaves exactly as a mail\n// client \u2014 or the view-in-browser page \u2014 would show it.\ninterface TemplateExample {\n label: string;\n /** The line under its name in the \"Start from example\" menu. */\n description: string;\n html: string;\n}\ntype ExampleKey = \"signed\" | \"plain\";\nexport const EMAIL_TEMPLATE_EXAMPLES: Record<ExampleKey, TemplateExample> = {\n signed: {\n label: \"Signed\",\n description:\n \"Your post, then your logo, name, and tagline as a sign-off. Your mailing address sits in the footer once you add one.\",\n html: `<style>\n .email {\n font: 16px/1.6 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\n color: #18181b;\n }\n .email h1,\n .email h2,\n .email h3 {\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.2;\n }\n .email a {\n color: #3355cc;\n }\n .email .rule {\n border: 0;\n border-top: 1px solid #e4e4e7;\n margin: 28px 0;\n }\n .signoff td {\n vertical-align: middle;\n }\n .signoff .logo {\n display: block;\n border-radius: 9px;\n margin-right: 14px;\n }\n .signoff .name {\n font: 600 17px/1.2 Georgia, 'Times New Roman', serif;\n }\n .signoff .tagline {\n font-size: 13px;\n color: #52525b;\n margin-top: 2px;\n }\n .footer {\n margin-top: 22px;\n font-size: 12px;\n line-height: 1.7;\n color: #8a8a93;\n }\n .footer a {\n color: #8a8a93;\n text-decoration: underline;\n }\n .footer .address {\n margin-top: 6px;\n }\n @media (prefers-color-scheme: dark) {\n .email {\n color: #ededed !important;\n }\n .email a {\n color: #93c5fd !important;\n }\n .email .rule {\n border-color: #2e2e33 !important;\n }\n .signoff .name {\n color: #ededed !important;\n }\n .signoff .tagline {\n color: #a1a1aa !important;\n }\n .footer {\n color: #a1a1aa !important;\n }\n .footer a {\n color: #a1a1aa !important;\n }\n }\n</style>\n\n<div class=\"email\">\n {{ .Post.Body }}\n\n <hr class=\"rule\" />\n\n <table class=\"signoff\" role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\">\n <tr>\n <td class=\"logo-cell\">{{ .Publication.Logo }}</td>\n <td>\n <div class=\"name\">{{ .Publication.Name }}</div>\n <div class=\"tagline\">{{ .Publication.Tagline }}</div>\n </td>\n </tr>\n </table>\n\n <div class=\"footer\">\n Powered by Kestrel{{ if .IsEmail }} \u00B7\n <a href=\"{{ .Email.UnsubscribeURL }}\">Unsubscribe</a> \u00B7\n <a href=\"{{ .Email.ViewInBrowserURL }}\">View in browser</a>\n <div class=\"address\">{{ .Publication.Address }}</div>{{ end }}\n </div>\n</div>`,\n },\n plain: {\n label: \"Plain\",\n description:\n \"Just your post and the footer links. Your mailing address sits in the footer once you add one.\",\n html: `<style>\n .email {\n font: 16px/1.6 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\n color: #18181b;\n }\n .email h1,\n .email h2,\n .email h3 {\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.2;\n }\n .email a {\n color: #3355cc;\n }\n .email .rule {\n border: 0;\n border-top: 1px solid #e4e4e7;\n margin: 28px 0;\n }\n .footer {\n font-size: 12px;\n line-height: 1.7;\n color: #8a8a93;\n }\n .footer a {\n color: #8a8a93;\n text-decoration: underline;\n }\n .footer .address {\n margin-top: 6px;\n }\n @media (prefers-color-scheme: dark) {\n .email {\n color: #ededed !important;\n }\n .email a {\n color: #93c5fd !important;\n }\n .email .rule {\n border-color: #2e2e33 !important;\n }\n .footer {\n color: #a1a1aa !important;\n }\n .footer a {\n color: #a1a1aa !important;\n }\n }\n</style>\n\n<div class=\"email\">\n {{ .Post.Body }}\n\n <hr class=\"rule\" />\n\n <div class=\"footer\">\n Powered by Kestrel{{ if .IsEmail }} \u00B7\n <a href=\"{{ .Email.UnsubscribeURL }}\">Unsubscribe</a> \u00B7\n <a href=\"{{ .Email.ViewInBrowserURL }}\">View in browser</a>\n <div class=\"address\">{{ .Publication.Address }}</div>{{ end }}\n </div>\n</div>`,\n },\n};\nconst isExampleKey = (k: string): k is ExampleKey => Object.hasOwn(EMAIL_TEMPLATE_EXAMPLES, k);\n\n// Sample post body for the preview \u2014 representative prose inside a called-out slot,\n// so it's unmistakable where a real post's rendered Markdown lands. Its typography\n// comes from the template's own .email rules (the callout frame + label are a\n// preview device, not part of the email). In a real send {{ .Post.Body }} is the\n// rendered Markdown.\nconst EMAIL_TEMPLATE_SAMPLE_BODY =\n '<div style=\"position:relative;border:1px dashed #93a7e6;border-radius:8px;padding:20px 14px 8px;margin:0 0 6px\">' +\n \"<span style=\\\"position:absolute;top:-8px;left:10px;font:650 10px/1.4 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;letter-spacing:.04em;text-transform:uppercase;color:#3355cc;background:#fff;padding:0 6px\\\">Your post\u2019s Markdown renders here</span>\" +\n '<h2 style=\"margin:0 0 10px\">Lorem ipsum,</h2>' +\n '<p style=\"margin:0\">Dolor sit amet, consectetur adipiscing elit. Proin sed ex ipsum. Suspendisse vulputate nisi et odio dapibus, quis pellentesque felis sollicitudin. Proin vel cursus enim. Phasellus sollicitudin malesuada elementum. Suspendisse euismod eros turpis, ut mollis est imperdiet ut. Sed luctus accumsan erat, at eleifend purus eleifend quis.</p>' +\n \"</div>\";\n\n// Fill logic-less {{ token }} placeholders from a flat context. {{ .Post.Body }} (the\n// rendered Markdown) and {{ .Publication.Logo }} (built with its own escaping) are raw\n// HTML; every other value is escaped, so a stray < or \"\n// in a name can't break the surrounding markup. An unknown token renders empty, and the\n// region tags ({{ if .IsEmail }}, {{ end }}) render nothing: the sample is the email, which\n// carries the region's content. A string\n// builder by nature: the template is the publisher's own HTML, filled in and vouched for\n// at the preview's boundary (mountSampleEmailPreview).\nconst RAW_TOKENS = new Set([\".Post.Body\", \".Publication.Logo\"]);\nfunction fillEmailTemplate(template: string, ctx: Record<string, string>): string {\n const unmarked = String(template).replace(/\\{\\{\\s*(?:if\\b[^}]*|end)\\s*\\}\\}/g, \"\");\n return unmarked.replace(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g, (_m, key: string) =>\n RAW_TOKENS.has(key) ? ctx[key] || \"\" : escapeHtml(ctx[key] ?? \"\"),\n );\n}\n\n/** The identity a sample preview binds: the live or loaded publication fields. */\nexport interface TemplateIdentity {\n name: string;\n tagline: string;\n logoUrl: string;\n address: string;\n}\n\n// Sample values the template preview binds \u2014 mirrors the render path's context, with\n// .Email.* standing in for per-recipient values. The logo is the real one or none, never\n// a stand-in: the preview shows the sign-off the email will carry.\nfunction templateSampleCtx(id: TemplateIdentity): Record<string, string> {\n const name = id.name || \"Your publication\";\n return {\n \".Post.Body\": EMAIL_TEMPLATE_SAMPLE_BODY,\n \".Post.Subject\": \"The starlings are back\",\n \".Publication.Name\": name,\n \".Publication.Tagline\": id.tagline || \"Your tagline\",\n \".Publication.LogoURL\": id.logoUrl,\n \".Publication.Logo\": emailLogoHtml(id.logoUrl, name),\n \".Publication.Address\": id.address,\n \".Email.SentTo\": \"you@example.com\",\n \".Email.UnsubscribeURL\": \"#unsubscribe\",\n \".Email.ViewInBrowserURL\": \"#view-in-browser\",\n };\n}\n\n/** A template filled with the preview's sample values: what the sample email shows. */\nexport function sampleEmailHtml(template: string, id: TemplateIdentity): string {\n return fillEmailTemplate(template, templateSampleCtx(id));\n}\n\n// The isolated preview document: a white (dark in dark mode) email canvas whose\n// reading column is capped at the email measure (~640px, matching view-in-browser),\n// so a template's own <style> applies as a mail client would and never leaks out.\nconst TEMPLATE_FRAME_DOC =\n '<!doctype html><html><head><meta charset=\"utf-8\">' +\n '<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">' +\n \"<style>html,body{margin:0}body{background:#fff}\" +\n \"@media (prefers-color-scheme:dark){body{background:#18181b}}\" +\n \".kestrel-email{max-width:640px;margin:0 auto;padding:26px 20px;box-sizing:border-box}\" +\n \".kestrel-email img{max-width:100%}</style></head>\" +\n '<body><div class=\"kestrel-email\"></div></body></html>';\n\n/** A mounted sample-email preview; call `repaint` after the template or identity changes. */\nexport interface SampleEmailPreview {\n repaint(): void;\n}\n\n/**\n * Mount a sample-email preview into an <iframe>, kept sized to its content. getTemplate()\n * returns the current template HTML; getIdentity() the live identity. Shared by the\n * Template page (live editor preview) and Settings (a read-only one). The frame takes the\n * Preview tab's sandbox: the template is HTML Claude may write too (`PUT /api/settings`),\n * and markup set into a same-origin frame runs an inline handler (`onerror`) though it\n * leaves a `<script>` inert, so without the sandbox a template could act as the\n * publisher (SPEC \u00A75).\n */\nexport function mountSampleEmailPreview(\n iframe: HTMLIFrameElement,\n getTemplate: () => string,\n getIdentity: () => TemplateIdentity,\n): SampleEmailPreview {\n // Before the srcdoc below: a sandbox applies from the frame's next navigation.\n iframe.setAttribute(\"sandbox\", EMAIL_FRAME_SANDBOX);\n let ready = false;\n const size = () => {\n try {\n // Measure the BODY, which is content-sized. documentElement.scrollHeight is floored\n // at the iframe's own height, so it can grow but never shrink \u2014 which is what left\n // the frame too tall after 375 \u2192 640 and clipped the footer at 640 \u2192 375.\n const h = iframe.contentDocument?.body?.scrollHeight;\n if (h) {\n iframe.style.height = `${Math.max(200, h)}px`;\n }\n } catch {}\n };\n const repaint = () => {\n const doc = iframe.contentDocument;\n const slot = ready && doc ? doc.querySelector(\".kestrel-email\") : null;\n if (!slot) {\n return;\n }\n // Set as markup (not srcdoc per keystroke): flicker-free. A <script> stays inert and\n // the frame's sandbox stops an inline handler. The publisher's own template, filled\n // with sample values, in its own frame.\n setHtml(slot, unsafeHtml(sampleEmailHtml(getTemplate(), getIdentity())));\n openLinksInNewTab(slot.ownerDocument);\n size();\n };\n iframe.addEventListener(\"load\", () => {\n ready = true;\n repaint();\n // Keep the frame fitted to its content through every change: an edit, the width\n // toggle's animated reflow, or a logo/font finishing loading. The body is\n // content-sized and size() only touches the outer iframe, so this can't loop.\n try {\n const doc = iframe.contentDocument;\n if (doc?.body && typeof ResizeObserver !== \"undefined\") {\n new ResizeObserver(size).observe(doc.body);\n }\n } catch {}\n });\n iframe.srcdoc = TEMPLATE_FRAME_DOC;\n return { repaint };\n}\n\n// The grouped variable reference (a <details> body), shared by both surfaces.\nfunction templateVarsHtml(): Html {\n return html`${EMAIL_TEMPLATE_VARS.map(\n (g) =>\n html`<div class=\"set-tpl-vargroup\"><h4>${g.group}</h4>${g.vars.map(\n (v) =>\n html`<div class=\"set-tpl-var\"><code>${v.token}</code><span class=\"set-tpl-var-desc\">${v.desc}</span></div>`,\n )}</div>`,\n )}`;\n}\n\n/** What a save of the template reports: the advisory warnings and the sends it re-made. */\ninterface TemplateSaved {\n warnings: string[];\n remade: SettingsSavedResponse[\"remade\"];\n}\n\n/**\n * The Email template page (top-level \"Template\" nav item). The one layout each post\n * is sent inside: a live sample-email preview over an HTML editor (with starter\n * examples, a variable reference, and its own validated Save). Editing lives here,\n * not in Settings, so each surface has a single, unambiguous save.\n */\nexport async function renderTemplate(root: HTMLElement, signal: AbortSignal): Promise<void> {\n setHtml(\n root,\n html`<div class=\"tpl-page\"><div class=\"page-head\"><div class=\"page-head-row\"><h1>Email template</h1><span id=\"tplInUse\"></span></div><p class=\"set-lede set-page-lede\">The template controls the look and feel of the emails you send. You write it as HTML with a <code><style></code> block and <code>{{ variables }}</code> Kestrel fills in; your post\u2019s Markdown is rendered into <code>{{ .Post.Body }}</code>.</p></div><div id=\"tplBody\" class=\"muted\">Loading\u2026</div></div>`,\n );\n const bodyEl = $(\"#tplBody\", root);\n let data: SettingsResponse;\n try {\n data = await api<SettingsResponse>(\"/api/settings\", { signal });\n } catch (e) {\n renderError(bodyEl, message(e), () => mount(renderTemplate));\n return;\n }\n const s = data.settings;\n const d = data.deployment;\n const p = s.publication;\n const identity: TemplateIdentity = {\n name: publicationName(p.name || \"\", d.fromAddress),\n tagline: p.tagline || \"\",\n logoUrl: p.logoUrl || \"\",\n address: p.address || \"\",\n };\n let templateBaseline = s.emailTemplate || \"\";\n const defaultRecipients = Array.isArray(s.testRecipients) ? s.testRecipients : [];\n // Standing: whether scheduled posts are using this template (SPEC \u00A79, DESIGN \u00A78).\n setHtml($(\"#tplInUse\"), inUseChip(data.inUse));\n\n setHtml(\n bodyEl,\n html`\n <div class=\"set-preview set-tpl-sample\">\n <div class=\"set-preview-bar\">\n <span class=\"set-preview-titles\">\n <span class=\"set-preview-lbl\">Sample email</span>\n <span class=\"set-preview-dot\">A preview with sample content, showing the layout used for a sent email</span>\n </span>\n <span class=\"set-preview-actions\">\n <span class=\"set-wtog\" role=\"group\" aria-label=\"Preview width\">\n <button type=\"button\" class=\"wtog-btn\" data-w=\"640\" aria-pressed=\"true\">640</button>\n <button type=\"button\" class=\"wtog-btn\" data-w=\"375\" aria-pressed=\"false\">375</button>\n </span>\n <button type=\"button\" class=\"secondary\" id=\"tplTest\">${icon(\"send\")}<span id=\"tplTestLbl\">Send test email</span></button>\n </span>\n </div>\n <div class=\"set-email-stage\" id=\"tplStage\">\n <iframe class=\"set-email-frame\" id=\"tplPreview\" title=\"Sample email preview\" scrolling=\"no\"></iframe>\n </div>\n <div class=\"set-preview-cap\">Your post\u2019s Markdown fills the body, and the <code>{{ .Email.* }}</code> values are set for each recipient when the post sends. Email clients render differently, so send yourself a test to see it in a real inbox.</div>\n </div>\n\n <div class=\"set-card\">\n <div class=\"set-card-pad\">\n <div class=\"set-tpl-block\">\n <div class=\"set-tpl-editor-head\">\n <div class=\"set-tpl-tools\">\n <button type=\"button\" class=\"ghost set-menu-btn\" id=\"tplExamplesBtn\" aria-haspopup=\"menu\" aria-expanded=\"false\"><span>Start from example</span><span class=\"set-menu-caret\"></span></button>\n <button type=\"button\" class=\"set-icon-btn\" id=\"tplLineNums\" aria-pressed=\"false\" title=\"Show line numbers\" aria-label=\"Show line numbers\">${icon(\"lines\")}</button>\n <button type=\"button\" class=\"ghost\" id=\"tplCopyAll\" title=\"Copy the whole template to the clipboard\" aria-label=\"Copy the whole template\">${copyFace()}</button>\n </div>\n <div class=\"set-tpl-required\" aria-label=\"Required variables\">\n <span class=\"set-req-lbl\">Required</span>\n <span class=\"set-req-pill\" id=\"reqBody\"><span class=\"dot\"></span>{{ .Post.Body }}</span>\n <span class=\"set-req-pill\" id=\"reqUnsub\"><span class=\"dot\"></span>{{ .Email.UnsubscribeURL }}</span>\n </div>\n </div>\n <div class=\"set-tpl-editor-wrap\" id=\"tplEditorWrap\">\n <div class=\"set-tpl-gutter\" id=\"tplGutter\" aria-hidden=\"true\">1</div>\n <pre class=\"set-tpl-hl\" id=\"tplHl\" aria-hidden=\"true\"><code></code></pre>\n <textarea id=\"tplEditor\" class=\"set-tpl-editor\" spellcheck=\"false\" wrap=\"off\" aria-label=\"Email template HTML\"></textarea>\n </div>\n <div class=\"set-tpl-msgs\" id=\"tplMsgs\" hidden></div>\n </div>\n </div>\n <div class=\"set-note\">${icon(\"info\")}<span>Kestrel uses this one template for every post, starting from a sensible default. Saving a change while posts are scheduled applies it to their emails too, after you confirm, so nothing scheduled goes out on an older look; their content stays as it was. Sent emails are archived exactly as they went out and never change. Save and Discard are in the bar at the bottom of the page.</span></div>\n </div>\n\n <div class=\"set-card set-tpl-varcard\">\n <div class=\"set-card-pad\">\n <div class=\"set-tpl-varhead\">\n <h3 class=\"set-tpl-vartitle\">Variables</h3>\n <p class=\"field-hint\">Kestrel replaces these variables with real values when you send an email. Type a variable exactly as shown, or it renders as empty.</p>\n </div>\n <div class=\"set-tpl-vars-body\">${templateVarsHtml()}</div>\n </div>\n </div>`,\n );\n\n const tplEditor = $<HTMLTextAreaElement>(\"#tplEditor\");\n const preview = mountSampleEmailPreview(\n $<HTMLIFrameElement>(\"#tplPreview\"),\n () => tplEditor.value,\n () => identity,\n );\n const tplMsgsEl = $(\"#tplMsgs\");\n const tplTestEl = $<HTMLButtonElement>(\"#tplTest\");\n const tplTestLbl = $(\"#tplTestLbl\");\n const isDirty = () => tplEditor.value !== templateBaseline;\n\n // Syntax-highlight overlay: a transparent <textarea> over a highlighted\n // <pre>, plus a line-number gutter, kept in scroll sync. Vanilla, no dependency.\n const tplHl = $(\"#tplHl\");\n const tplHlCode = $(\"code\", tplHl);\n const tplGutter = $(\"#tplGutter\");\n const paintEditor = () => {\n const src = tplEditor.value;\n // Trailing newline so the last line renders and the overlay height matches the textarea.\n setHtml(tplHlCode, html`${highlightTemplate(src)}\\n`);\n let g = \"\";\n const lines = src.split(\"\\n\").length;\n for (let i = 1; i <= lines; i++) {\n g += `${i}\\n`;\n }\n tplGutter.textContent = g;\n };\n const syncScroll = () => {\n tplHl.scrollTop = tplEditor.scrollTop;\n tplHl.scrollLeft = tplEditor.scrollLeft;\n tplGutter.scrollTop = tplEditor.scrollTop;\n };\n tplEditor.addEventListener(\"scroll\", syncScroll);\n\n // The two blocking-required tokens, predicted live in the toolbar pills: green when\n // present, red when missing \u2014 so a rejected save is visible before you press Save.\n const reqBodyEl = $(\"#reqBody\");\n const reqUnsubEl = $(\"#reqUnsub\");\n const paintReq = () => {\n reqBodyEl.className = `set-req-pill ${/\\{\\{\\s*\\.Post\\.Body\\s*\\}\\}/.test(tplEditor.value) ? \"ok\" : \"bad\"}`;\n reqUnsubEl.className = `set-req-pill ${/\\{\\{\\s*\\.Email\\.UnsubscribeURL\\s*\\}\\}/.test(tplEditor.value) ? \"ok\" : \"bad\"}`;\n };\n\n // Save + Discard live in the shared bottom save bar (onSave/onDiscard below); the\n // page never renders its own Save button. A rejected save (e.g. a template missing\n // {{ .Email.UnsubscribeURL }}, a 400) is a blocking error, so it shows IN the bar\n // (which stays up, right beside Save). Warnings are advisory and describe the\n // template that was just saved, so they stay inline under the editor.\n const bar = savebar.attach({ onSave: onSaveTemplate, onDiscard: revertTemplate }, signal);\n\n function refreshDirty() {\n paintEditor();\n paintReq();\n syncScroll();\n bar.setDirty(isDirty());\n // A test always sends the SAVED template (what will ship, I5). When there are\n // unsaved edits the button says so plainly: it saves first, then sends.\n tplTestLbl.textContent = isDirty() ? \"Save & send test\" : \"Send test email\";\n tplTestEl.title = isDirty()\n ? \"Saves your changes first, then sends: a test always reflects the saved template that will ship.\"\n : \"Sends a sample post through the saved template so you can see it in a real inbox.\";\n }\n // Advisory warnings for the template that was just saved; blocking errors go to the\n // save bar instead (bar.showError), so the bar owns the blocking state and this owns\n // the post-save advisory state.\n function showWarnings(msgs: string[]) {\n if (!msgs.length) {\n tplMsgsEl.hidden = true;\n setHtml(tplMsgsEl, html``);\n return;\n }\n tplMsgsEl.hidden = false;\n tplMsgsEl.className = \"set-tpl-msgs warn\";\n setHtml(tplMsgsEl, html`${msgs.map((m) => html`<div>${m}</div>`)}`);\n }\n function revertTemplate() {\n tplEditor.value = templateBaseline;\n showWarnings([]);\n preview.repaint();\n refreshDirty();\n }\n // Persist the current editor content, confirming first when the save would apply to\n // scheduled emails (withRemakeConfirm). Returns the warnings and what was re-made, or\n // null when the publisher declined the confirmation (nothing was saved; the page stays\n // dirty). THROWS on a rejected template (no unsubscribe link \u2192 400) or a send about to\n // fire (409 remake_too_close), so callers decide what to do. Shared by the save bar's\n // Save and Save-&-send-test.\n async function saveTemplate(): Promise<TemplateSaved | null> {\n const value = tplEditor.value;\n const r = await withRemakeConfirm(\n (ack) => {\n const body: SettingsPatchBody = ack\n ? { emailTemplate: value, remake: ack }\n : { emailTemplate: value };\n return api<SettingsSavedResponse>(\"/api/settings\", { method: \"PUT\", json: body });\n },\n REMAKE_TEMPLATE,\n signal,\n );\n if (!r) {\n return null;\n }\n // The server may resolve \"\" to the default \u2014 reflect what was actually stored.\n templateBaseline = r.settings.emailTemplate;\n tplEditor.value = templateBaseline;\n // The cached config the sidebar reads: this page's own response, with the saved half.\n appState.appConfig = { ...data, settings: r.settings };\n preview.repaint();\n refreshDirty(); // clean now \u2014 slides the bar away\n return { warnings: Array.isArray(r.warnings) ? r.warnings : [], remade: r.remade || [] };\n }\n // The shared save bar's Save button (run inside busy() by the controller). Persists,\n // surfaces warnings inline; a rejected save keeps the bar up and shows why in it.\n async function onSaveTemplate() {\n try {\n const saved = await saveTemplate();\n if (!saved) {\n return; // declined: the edits stay, the bar stays up\n }\n showWarnings(saved.warnings);\n toast(\n savedToast(\n saved.warnings.length ? \"Template saved with warnings\" : \"Template saved\",\n saved.remade,\n ),\n );\n } catch (err) {\n // The bar owns the blocking error (it stays up and says why). No toast \u2014 a\n // bottom-center toast would sit on top of the bar and hide the very message.\n bar.showError(message(err));\n }\n }\n\n const loadExample = (key: string) => {\n const ex = EMAIL_TEMPLATE_EXAMPLES[isExampleKey(key) ? key : \"signed\"];\n tplEditor.value = ex.html;\n showWarnings([]);\n preview.repaint();\n refreshDirty();\n };\n tplEditor.addEventListener(\"input\", () => {\n preview.repaint();\n refreshDirty();\n });\n // Examples: a \"Start from example\" dropdown menu (a compact, secondary action \u2014\n // loading one is destructive, so it isn't a permanent fixture on the page). The shared\n // menu gives it the keyboard and focus a menu owes (DESIGN \u00A711) and closes with the page.\n const exBtn = $<HTMLButtonElement>(\"#tplExamplesBtn\");\n exBtn.onclick = () =>\n openMenu(\n exBtn,\n Object.entries(EMAIL_TEMPLATE_EXAMPLES).map(([key, ex]) => ({\n label: ex.label,\n description: ex.description,\n onClick: () => loadExample(key),\n })),\n signal,\n { align: \"start\" },\n );\n\n // Preview width toggle (640 / 375) \u2014 proof both inbox measures; 640 is the default.\n const frameEl = $<HTMLIFrameElement>(\"#tplPreview\");\n for (const wb of $$<HTMLButtonElement>(\".wtog-btn\", bodyEl)) {\n wb.onclick = () => {\n for (const o of $$<HTMLButtonElement>(\".wtog-btn\", bodyEl)) {\n o.setAttribute(\"aria-pressed\", String(o === wb));\n }\n frameEl.style.maxWidth = `${wb.dataset.w}px`;\n preview.repaint();\n };\n }\n\n // Line numbers: hidden by default; the toolbar toggle shows them and the choice is\n // remembered per browser (a lightweight convenience \u2014 safe to lose).\n const editorWrap = $(\"#tplEditorWrap\");\n const lineNumsBtn = $<HTMLButtonElement>(\"#tplLineNums\");\n const setLineNums = (on: boolean) => {\n editorWrap.classList.toggle(\"show-lines\", on);\n lineNumsBtn.setAttribute(\"aria-pressed\", String(on));\n syncScroll();\n };\n let lineNumsOn = false;\n try {\n lineNumsOn = localStorage.getItem(\"kestrel.tpl.lineNums\") === \"1\";\n } catch {}\n setLineNums(lineNumsOn);\n lineNumsBtn.onclick = () => {\n lineNumsOn = !lineNumsOn;\n setLineNums(lineNumsOn);\n try {\n localStorage.setItem(\"kestrel.tpl.lineNums\", lineNumsOn ? \"1\" : \"0\");\n } catch {}\n };\n\n // Copy the whole template to the clipboard, said on the button (ui/copy.ts).\n const copyAllBtn = $<HTMLButtonElement>(\"#tplCopyAll\");\n copyAllBtn.onclick = () => copyWith(copyAllBtn, tplEditor.value);\n\n // --- send a test of the saved template (edit \u2192 test \u2192 iterate) ---\n // A test renders a sample post through the SAVED template \u2014 what will actually\n // ship (I5). So if the editor is dirty we save first (a \"Save & send test\" flow);\n // a rejected save (e.g. missing unsubscribe) stops the send, honestly. We never\n // render unsaved editor content, which would test something that won't ship.\n tplTestEl.onclick = () => {\n // While no provider is connected the server would refuse a test, so the dialog leaves\n // out the recipients and keeps only the standing line (DESIGN \u00A74), and says nothing of\n // saving first, since nothing will be sent.\n const notConnected = providerNotConnected();\n const dirty = !notConnected && isDirty();\n const m = modal(\n html`<h3>Send a test email</h3><p class=\"hint\">Delivers a sample post rendered through your <strong>saved</strong> template, so you can see it in a real inbox. One address per line.</p>${notConnectedTestLine()}${\n dirty\n ? html`<p class=\"hint\" style=\"color:var(--warn-fg)\"><strong>Unsaved changes:</strong> sending will save your template first, so the test reflects what will actually ship.</p>`\n : null\n }${notConnected ? null : html`<label for=\"tplTestTo\">Recipients</label><textarea id=\"tplTestTo\" rows=\"3\" placeholder=\"you@example.com\"></textarea><p class=\"hint\" id=\"tplTestHint\" hidden></p>`}<div class=\"actions\"><button type=\"button\" id=\"ttCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"ttGo\">${\n dirty ? \"Save & send test\" : \"Send test\"\n }</button></div>`,\n signal,\n );\n const go = $<HTMLButtonElement>(\"#ttGo\", m.el);\n $(\"#ttCancel\", m.el).onclick = m.close;\n go.disabled = notConnected;\n if (notConnected) {\n $(\"#ttCancel\", m.el).focus();\n return;\n }\n const to = $<HTMLTextAreaElement>(\"#tplTestTo\", m.el);\n to.focus();\n // Pre-fill from the saved default test recipients (don't clobber typed input).\n if (defaultRecipients.length && !to.value.trim()) {\n to.value = defaultRecipients.join(\"\\n\");\n const hint = $(\"#tplTestHint\", m.el);\n hint.textContent = \"Pre-filled from your default test recipients (Settings).\";\n hint.hidden = false;\n }\n go.onclick = () =>\n busy(go, isDirty() ? \"Saving\u2026\" : \"Sending\u2026\", async () => {\n m.clearError();\n const { valid: addrs, invalid } = parseAddresses(to.value);\n const notAddresses = invalidAddressesMessage(invalid);\n if (notAddresses) {\n m.error(notAddresses, to);\n return;\n }\n if (!addrs.length) {\n m.error(\"Enter at least one email address\", to);\n return;\n }\n // Honest unsaved-changes handling: persist first so the test renders what ships.\n // A rejected save shows in the bar and stops the send.\n if (isDirty()) {\n try {\n const saved = await saveTemplate();\n if (!saved) {\n m.close(); // declined the re-make: nothing saved, so nothing to test yet\n return;\n }\n showWarnings(saved.warnings);\n } catch (err) {\n // The save failed, so the test can't send what would ship. The bar shows\n // why (and stays up); closing the dialog returns you to it. No toast \u2014 it\n // would overlay the bar and hide the reason.\n bar.showError(message(err));\n m.close();\n return;\n }\n }\n try {\n const r = await api<TemplateTestResponse>(\"/api/settings/template/test\", {\n method: \"POST\",\n json: { to: addrs },\n });\n if (r.failed.length) {\n // The dialog stays open with only the addresses that didn't get it, so Send again\n // tries those and no one gets a second copy.\n to.value = r.failed.join(\"\\n\");\n m.error(notSentMessage(r.sent, r.total, r.failed));\n return;\n }\n m.close();\n toast(withNoProviderNote(`Test sent to ${r.sent} address${r.sent === 1 ? \"\" : \"es\"}`));\n } catch (e) {\n m.error(message(e));\n }\n });\n };\n tplEditor.value = templateBaseline;\n preview.repaint();\n refreshDirty();\n}\n", "// Settings' \"Your emails\" group: the email template, shown as a sample email with the way\n// to the Template page where it is edited, and the default test recipients the publisher\n// proofs a post against.\n\nimport { isValidEmail, normalizeEmail } from \"../../shared/email\";\nimport { $ } from \"../ui/dom\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { fieldError } from \"../ui/widgets\";\nimport { chip, type SectionHandle, type SettingsPage, secHead } from \"./page\";\nimport { inUseChip } from \"./remake\";\nimport { mountSampleEmailPreview } from \"./template\";\n\n/** The template as what a publication's email looks like: a sample email in the saved\n * template, read-only, with the way to the Template page, where editing lives (so each\n * surface has one save). */\nexport function templateSection(page: SettingsPage): Html {\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Email template\", chip(\"editable\", \"Editable\"), inUseChip(page.data.inUse))}\n <p class=\"set-lede\">The template controls the look and feel of the emails you send. Edit it on the Template page.</p>\n <div class=\"set-preview set-tpl-sample\">\n <div class=\"set-preview-bar\">\n <span class=\"set-preview-lbl\">Sample email</span>\n <span class=\"set-preview-dot\">Current template</span>\n </div>\n <iframe class=\"set-email-frame\" id=\"tplPreview\" title=\"Sample email preview\" scrolling=\"no\"></iframe>\n </div>\n <div class=\"row\" style=\"margin-top:12px\"><button type=\"button\" id=\"tplEditLink\">Edit template \u2192</button></div>\n </section>`;\n}\n\n/** The sample follows the identity fields as they are typed. */\nexport function mountTemplate(root: HTMLElement, page: SettingsPage): SectionHandle {\n const template = page.data.settings.emailTemplate || \"\";\n const preview = mountSampleEmailPreview(\n $<HTMLIFrameElement>(\"#tplPreview\", root),\n () => template,\n () => ({\n name: page.state.name || page.fromDisplay,\n tagline: page.state.tagline,\n logoUrl: page.state.logoUrl,\n address: page.state.address,\n }),\n );\n $(\"#tplEditLink\", root).onclick = () => {\n location.hash = \"#/template\";\n };\n return { repaint: () => preview.repaint() };\n}\n\nexport function recipientsSection(): Html {\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Default test recipients\", chip(\"editable\", \"Editable\"))}\n <div class=\"set-card\">\n <div class=\"set-recip\">\n <p class=\"field-hint\" style=\"margin:0\">Pre-filled into <strong>Send test email</strong> so you can proof a post against your own inboxes before scheduling. These are your addresses, and they don\u2019t go through the subscribe/consent flow.</p>\n <div class=\"set-chips\" id=\"recipChips\"></div>\n <div class=\"set-recip-add\">\n <input type=\"email\" id=\"recipInput\" placeholder=\"you@example.com\" autocomplete=\"off\">\n <button type=\"button\" id=\"recipAdd\">Add inbox</button>\n </div>\n </div>\n </div>\n </section>`;\n}\n\n/** Removable chips and an add row; the list commits through the save bar. */\nexport function mountRecipients(root: HTMLElement, page: SettingsPage): SectionHandle {\n const { state } = page;\n const chips = $(\"#recipChips\", root);\n const input = $<HTMLInputElement>(\"#recipInput\", root);\n const render = () => {\n if (!state.recipients.length) {\n setHtml(\n chips,\n html`<span class=\"set-recip-empty\">No default recipients yet. Add one below.</span>`,\n );\n return;\n }\n setHtml(\n chips,\n html`${state.recipients.map(\n (addr, i) =>\n html`<span class=\"set-recip-chip\">${addr}<button type=\"button\" data-rm=\"${i}\" aria-label=\"Remove ${addr}\">${icon(\"x\")}</button></span>`,\n )}`,\n );\n };\n chips.addEventListener(\"click\", (ev) => {\n const b = ev.target instanceof Element ? ev.target.closest<HTMLElement>(\"[data-rm]\") : null;\n if (!b) {\n return;\n }\n state.recipients.splice(Number(b.dataset.rm), 1);\n render();\n page.changed();\n });\n const add = () => {\n const v = normalizeEmail(input.value);\n if (!v) {\n return;\n }\n // A refused address says why on the field, and keeps what was typed so it can be fixed\n // in place; the reason goes as soon as the field is edited (DESIGN \u00A75).\n if (!isValidEmail(v)) {\n fieldError(input, \"That doesn\u2019t look like an email address.\");\n input.focus();\n return;\n }\n if (state.recipients.includes(v)) {\n fieldError(input, \"That inbox is already in the list.\");\n input.focus();\n return;\n }\n state.recipients.push(v);\n input.value = \"\";\n render();\n page.changed();\n };\n $(\"#recipAdd\", root).onclick = add;\n input.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n add();\n }\n });\n render();\n return { reset: render };\n}\n", "// An image file made square: the largest centered square, scaled to a fixed size and\n// encoded as PNG in the browser, so a logo of any shape uploads as the square it is shown as.\n\n/** The largest centered square of a `width` \u00D7 `height` image, as a source rectangle. */\nexport function centerSquare(\n width: number,\n height: number,\n): { x: number; y: number; side: number } {\n const side = Math.min(width, height);\n return { x: Math.floor((width - side) / 2), y: Math.floor((height - side) / 2), side };\n}\n\n/** The file isn't an image this browser can decode (not an image at all, or a format it\n * doesn't read), as distinct from a browser that can't make the PNG. */\nexport class UnreadableImage extends Error {\n constructor() {\n super(\"Couldn\u2019t read that image. Pick a PNG, JPEG, WebP, or GIF.\");\n }\n}\n\n/** An image squared to `size` \u00D7 `size`: the PNG, and the source's own dimensions, so a\n * caller can say when the source was cropped. */\nexport interface SquaredImage {\n png: Blob;\n width: number;\n height: number;\n}\n\n/**\n * Decode `file`, crop its centered square, scale it to `size` \u00D7 `size`, and encode a PNG.\n * PNG keeps transparency and shows in every mail client, which WebP doesn't. An animated\n * GIF keeps its first frame. Rejects with `UnreadableImage` when the file isn't an image the\n * browser can decode, and with a plain Error when the browser can't make the PNG.\n */\nexport async function squareImage(file: Blob, size: number): Promise<SquaredImage> {\n // Upright as the photo was taken: a phone JPEG's EXIF rotation applied, said outright\n // rather than left to a default browsers have not always agreed on.\n const bitmap = await createImageBitmap(file, { imageOrientation: \"from-image\" }).catch(() => {\n throw new UnreadableImage();\n });\n try {\n const { x, y, side } = centerSquare(bitmap.width, bitmap.height);\n const canvas = document.createElement(\"canvas\");\n canvas.width = size;\n canvas.height = size;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"This browser can\u2019t prepare the image.\");\n }\n ctx.imageSmoothingQuality = \"high\";\n ctx.drawImage(bitmap, x, y, side, side, 0, 0, size, size);\n const png = await new Promise<Blob>((resolve, reject) =>\n canvas.toBlob(\n (b) => (b ? resolve(b) : reject(new Error(\"This browser can\u2019t prepare the image.\"))),\n \"image/png\",\n ),\n );\n return { png, width: bitmap.width, height: bitmap.height };\n } finally {\n bitmap.close();\n }\n}\n", "// Settings' Publication identity section (SPEC \u00A79): the name, tagline, and mailing address,\n// which commit through the save bar, and the logo, which saves the moment it changes.\n\nimport type { InUseView, LogoResponse } from \"../../shared/settings\";\nimport { api } from \"../api\";\nimport { $ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { type Html, html } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { squareImage } from \"../ui/square_image\";\nimport { busy, toast } from \"../ui/widgets\";\nimport { chip, HIDDEN, message, type SectionHandle, type SettingsPage, secHead } from \"./page\";\nimport { remakeIdentity, savedToast, withRemakeConfirm } from \"./remake\";\n\n/** The note under the card: where the identity shows up (the sample email shows which\n * fields) and, only while posts are scheduled, that a save reaches them too (SPEC \u00A79).\n * The confirmation dialog carries the rest, at the moment it matters, so the standing\n * note stays one or two short lines. */\nfunction identityNote(inUse: InUseView): string {\n if (!(inUse.identityFields || []).length) {\n return \"Your email template doesn\u2019t show any of these.\";\n }\n const shown = \"Shown in every email.\";\n const n = inUse.sends.length;\n if (!n) {\n return shown;\n }\n const reach = `Saving a change updates ${n} scheduled email${n === 1 ? \"\" : \"s\"} too; you\u2019ll confirm first.`;\n const wait = inUse.retry_after\n ? ` One sends at ${fmt(inUse.retry_after)}, so saving waits until it has sent.`\n : \"\";\n return `${shown} ${reach}${wait}`;\n}\n\n/** The side of the square PNG a logo uploads as: sharp at 3\u00D7 the largest place it shows\n * (the reader pages' 64 px), and small for any inbox. */\nconst LOGO_SIZE = 256;\n\n// The logo tile's background image is set from the DOM (applyLogoUi), not written into\n// the markup: a URL has no place inside a style attribute the tag would escape as text.\nexport function identitySection(page: SettingsPage): Html {\n const { state, fromDisplay, fromName } = page;\n // The mailing address is optional (SPEC \u00A79): with none saved it is an \"Add\" row, not an\n // empty field that reads as a missing required value.\n const addressOpen = Boolean(state.address);\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Publication identity\", chip(\"editable\", \"Editable\"))}\n <div class=\"set-card\">\n <div class=\"set-id-grid\">\n <div class=\"set-logo-slot\">\n <div class=\"set-logo-tile${state.logoUrl ? \" has-img\" : \"\"}\" id=\"logoTile\" role=\"button\" tabindex=\"0\" aria-label=\"Upload logo\">\n <span class=\"set-logo-ph\" id=\"logoPh\"${state.logoUrl ? HIDDEN : null}>${icon(\"upload\")}Upload</span>\n </div>\n <input type=\"file\" id=\"logoInput\" accept=\"image/png,image/jpeg,image/webp,image/gif\" hidden>\n <div class=\"set-logo-actions\">\n <button type=\"button\" id=\"logoReplace\">${state.logoUrl ? \"Replace\" : \"Upload\"}</button>\n <button type=\"button\" class=\"danger-subtle\" id=\"logoRemove\"${state.logoUrl ? null : HIDDEN}>Remove</button>\n </div>\n <div class=\"set-logo-notes\">\n <p class=\"field-hint\">Shown as a square. Pick an image at least ${String(LOGO_SIZE)} \u00D7 ${String(LOGO_SIZE)} px; a wider or taller one is cropped to its center square.</p>\n <p class=\"set-field-warn\" id=\"logoShape\" hidden>Your logo isn\u2019t square, so emails show it stretched. Replace it to have it cropped square.</p>\n <div class=\"field-error\" id=\"logoError\" role=\"alert\" hidden><span class=\"field-error-ico\" aria-hidden=\"true\">!</span><span></span></div>\n </div>\n </div>\n <div class=\"set-id-fields\">\n <div class=\"set-field\">\n <label for=\"setName\">Name</label>\n <input id=\"setName\" value=\"${state.name}\" placeholder=\"${fromName}\" maxlength=\"120\" autocomplete=\"off\">\n <p class=\"field-hint\">${\n fromDisplay\n ? html`Blank falls back to the email \u201CFrom\u201D name (\u201C${fromDisplay}\u201D).`\n : \"Blank leaves the publication unnamed, since the email \u201CFrom\u201D has no name.\"\n }</p>\n </div>\n <div class=\"set-field\">\n <label for=\"setTagline\">Tagline</label>\n <input id=\"setTagline\" value=\"${state.tagline}\" placeholder=\"A one-line description\" maxlength=\"200\" autocomplete=\"off\">\n <p class=\"field-hint\">A short line under the name on your public pages.</p>\n </div>\n <div class=\"set-field set-addr-add\" id=\"addrAdd\"${addressOpen ? HIDDEN : null}>\n <button type=\"button\" class=\"ghost\" id=\"addrAddBtn\" aria-describedby=\"addrAddHint\">${icon(\"plus\")}Add a mailing address</button>\n <p class=\"field-hint\" id=\"addrAddHint\">For promotional email, US law (CAN-SPAM) requires a postal address in the footer. A P.O. box works.</p>\n </div>\n <div class=\"set-field\" id=\"addrField\"${addressOpen ? null : HIDDEN}>\n <div class=\"set-field-head\">\n <label for=\"setAddress\">Mailing address</label>\n <button type=\"button\" class=\"danger-subtle set-field-remove\" id=\"addrRemove\">Remove</button>\n </div>\n <input id=\"setAddress\" value=\"${state.address}\" placeholder=\"PO Box 123, City, ST 00000\" maxlength=\"300\" autocomplete=\"off\">\n <p class=\"set-field-warn\" id=\"addrWarn\" hidden>Your email template currently doesn\u2019t include this field. If you send promotional email, add it to the template.</p>\n </div>\n </div>\n </div>\n <div class=\"set-note\">${icon(\"info\")}<span>${identityNote(page.data.inUse)}</span></div>\n </div>\n </section>`;\n}\n\nexport function mountIdentity(\n root: HTMLElement,\n page: SettingsPage,\n signal: AbortSignal,\n): SectionHandle {\n const { state } = page;\n const nameEl = $<HTMLInputElement>(\"#setName\", root);\n const taglineEl = $<HTMLInputElement>(\"#setTagline\", root);\n const addressEl = $<HTMLInputElement>(\"#setAddress\", root);\n\n // --- live fields: the state follows every keystroke, and the page repaints what shows it.\n const onInput = () => {\n state.name = nameEl.value.trim();\n state.tagline = taglineEl.value.trim();\n state.address = addressEl.value.trim();\n page.changed();\n };\n nameEl.addEventListener(\"input\", onInput);\n taglineEl.addEventListener(\"input\", onInput);\n addressEl.addEventListener(\"input\", onInput);\n\n // --- the optional address: the Add row or the open field, and the field's warning,\n // shown while an address is set and the template leaves it out.\n let addressOpen = Boolean(state.address);\n const templatePrintsAddress = (page.data.inUse.identityFields || []).includes(\"address\");\n const addrAdd = $(\"#addrAdd\", root);\n const addrField = $(\"#addrField\", root);\n const addrWarn = $(\"#addrWarn\", root);\n const applyAddressUi = () => {\n addrAdd.hidden = addressOpen;\n addrField.hidden = !addressOpen;\n addrWarn.hidden = !(addressOpen && state.address && !templatePrintsAddress);\n // Described by the warning only while it shows: a hidden element named by\n // aria-describedby is still read out.\n if (addrWarn.hidden) {\n addressEl.removeAttribute(\"aria-describedby\");\n } else {\n addressEl.setAttribute(\"aria-describedby\", \"addrWarn\");\n }\n };\n addressEl.addEventListener(\"input\", applyAddressUi);\n $(\"#addrAddBtn\", root).onclick = () => {\n addressOpen = true;\n applyAddressUi();\n addressEl.focus();\n };\n $(\"#addrRemove\", root).onclick = () => {\n addressEl.value = \"\";\n addressOpen = false;\n onInput();\n applyAddressUi();\n $(\"#addrAddBtn\", root).focus();\n };\n\n // --- logo: immediate upload / remove (their own endpoints), updated in place.\n const logoInput = $<HTMLInputElement>(\"#logoInput\", root);\n const logoTile = $(\"#logoTile\", root);\n const logoPh = $(\"#logoPh\", root);\n const logoReplace = $<HTMLButtonElement>(\"#logoReplace\", root);\n const logoRemove = $<HTMLButtonElement>(\"#logoRemove\", root);\n // A logo uploaded before the crop step may not be square, and emails show it stretched\n // to one. Said under the tile until it is replaced; the image is read as any <img> is.\n const logoShape = $(\"#logoShape\", root);\n const checkLogoShape = (url: string) => {\n logoShape.hidden = true;\n if (!url) {\n return;\n }\n const img = new Image();\n img.onload = () => {\n logoShape.hidden = state.logoUrl !== url || img.naturalWidth === img.naturalHeight;\n };\n img.src = url;\n };\n const applyLogoUi = () => {\n const has = !!state.logoUrl;\n checkLogoShape(state.logoUrl);\n logoTile.classList.toggle(\"has-img\", has);\n logoTile.style.backgroundImage = has ? `url('${state.logoUrl}')` : \"\";\n logoPh.hidden = has;\n logoRemove.hidden = !has;\n logoReplace.textContent = has ? \"Replace\" : \"Upload\";\n };\n logoReplace.onclick = () => logoInput.click();\n logoTile.addEventListener(\"click\", () => logoInput.click());\n logoTile.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n logoInput.click();\n }\n });\n // A refused logo change (a scheduled send about to fire) is said beneath the tile,\n // field validation's home (DESIGN \u00A75): the logo has no save bar state of its own.\n const logoError = $(\"#logoError\", root);\n const logoErrorText = $(\"span:last-of-type\", logoError);\n const showLogoError = (msg: string) => {\n logoErrorText.textContent = msg;\n logoError.hidden = !msg;\n };\n // A logo change lands at once: the page adopts it, and every preview that shows the\n // logo repaints rather than waiting for the next identity edit.\n const logoSaved = (r: LogoResponse, done: string) => {\n state.logoUrl = r.settings.publication.logoUrl || \"\";\n page.applySaved(r.settings);\n applyLogoUi();\n page.changed();\n toast(savedToast(done, r.remade));\n };\n // The logo has no save bar, so whatever kept it from saving is said under its tile.\n const logoFailed = (err: unknown) => showLogoError(message(err));\n logoInput.onchange = async () => {\n const file = logoInput.files?.[0];\n logoInput.value = \"\";\n if (!file) {\n return;\n }\n showLogoError(\"\");\n try {\n // Squared before it uploads: every surface shows the logo square, and an email\n // stretches one that isn't. The tile shows the result; Replace is the way back.\n const squared = await squareImage(file, LOGO_SIZE);\n const cropped = squared.width !== squared.height;\n // The logo is part of the identity, so an upload that reaches scheduled emails\n // asks first (withRemakeConfirm); the file is held and re-sent with the ids.\n const r = await withRemakeConfirm(\n (ack) => {\n const fd = new FormData();\n fd.append(\"file\", new File([squared.png], \"logo.png\", { type: \"image/png\" }));\n const q = ack ? `?remake=${encodeURIComponent(ack.join(\",\"))}` : \"\";\n return api<LogoResponse>(`/api/settings/logo${q}`, { method: \"POST\", body: fd });\n },\n remakeIdentity(\"Upload\", \"Uploading\"),\n signal,\n );\n if (r) {\n logoSaved(r, cropped ? \"Logo updated, cropped to its center square\" : \"Logo updated\");\n }\n } catch (err) {\n logoFailed(err);\n }\n };\n logoRemove.onclick = () =>\n busy(logoRemove, \"Removing\u2026\", async () => {\n showLogoError(\"\");\n try {\n const r = await withRemakeConfirm(\n (ack) => {\n const q = ack ? `?remake=${encodeURIComponent(ack.join(\",\"))}` : \"\";\n return api<LogoResponse>(`/api/settings/logo${q}`, { method: \"DELETE\" });\n },\n remakeIdentity(\"Remove\", \"Removing\"),\n signal,\n );\n if (r) {\n logoSaved(r, \"Logo removed\");\n }\n } catch (err) {\n logoFailed(err);\n }\n });\n\n applyLogoUi();\n applyAddressUi();\n return {\n reset() {\n nameEl.value = state.name;\n taglineEl.value = state.tagline;\n addressEl.value = state.address;\n addressOpen = Boolean(state.address);\n applyAddressUi();\n },\n };\n}\n", "// Settings' Notifications section (SPEC \u00A78): where Kestrel tells the publisher about their\n// sends is a preference, committed through the save bar; how it gets there, and from whom,\n// is deploy config, shown read-only beside it with how the last one went.\n\nimport type {\n NotificationStatusKind,\n NotificationTestResponse,\n SettingsResponse,\n} from \"../../shared/settings\";\nimport { api } from \"../api\";\nimport { $ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { busy, toast } from \"../ui/widgets\";\nimport { providerLabel } from \"./deploy\";\nimport { chip, message, type SectionHandle, type SettingsPage, secHead } from \"./page\";\n\n/** How the deploy-time notification channel reads to the publisher. */\nfunction notifyChannelLabel(\n channel: SettingsResponse[\"deployment\"][\"notifyChannel\"],\n provider: string,\n): string {\n switch (channel) {\n case \"cloudflare\":\n return \"Cloudflare Email, separate from your newsletter\u2019s provider\";\n case \"provider\":\n return provider === \"none\"\n ? \"Your email provider, once one is connected\"\n : `Your email provider (${providerLabel(provider)})`;\n default:\n return \"Local stand-in: nothing is delivered\";\n }\n}\n\nconst NOTIFICATION_KINDS: Record<NotificationStatusKind, string> = {\n finished: \"went out\",\n refused: \"provider refusing\",\n stuck: \"in flight too long\",\n wedged: \"waiting to be resolved\",\n missed: \"missed fire time\",\n test: \"test\",\n};\n\n/** What a status line is about: the send and what happened, or just \"test notification\". */\nfunction aboutNotification(n: { kind: NotificationStatusKind; subject: string }): string {\n return n.kind === \"test\" ? \"test notification\" : `${n.subject} (${NOTIFICATION_KINDS[n.kind]})`;\n}\n\n/** The last delivered notification, and a newer failure when the channel has stopped working. */\nfunction notificationStatusHtml(status: SettingsResponse[\"notificationStatus\"]): Html {\n const { lastSent, lastFailure } = status;\n if (lastFailure) {\n return html`<span class=\"set-pill danger\">Not delivered</span> <span class=\"muted\">${fmt(lastFailure.at)}, ${aboutNotification(lastFailure)}: ${lastFailure.error}</span>`;\n }\n if (lastSent) {\n return html`<span class=\"set-pill ok\">${icon(\"check\")}Delivered</span> <span class=\"muted\">${fmt(lastSent.at)}, ${aboutNotification(lastSent)}</span>`;\n }\n return html`<span class=\"muted\">None yet</span>`;\n}\n\nexport function notificationsSection(page: SettingsPage): Html {\n const d = page.data.deployment;\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Notifications\", chip(\"editable\", \"Editable\"))}\n <p class=\"set-lede\">Kestrel emails you when a send goes out, and right away if a send runs into a problem.</p>\n <div class=\"set-card\">\n <div class=\"set-recip\">\n <div class=\"set-field\" style=\"margin:0\">\n <label for=\"notifyTo\">Send notifications to</label>\n <input type=\"email\" id=\"notifyTo\" value=\"${page.state.notifyTo}\" placeholder=\"you@example.com\" autocomplete=\"off\">\n <p class=\"field-hint\">One address. Leave blank for no notifications.</p>\n </div>\n <div class=\"row\"><button type=\"button\" class=\"ghost\" id=\"notifyTest\">Send a test notification</button><span class=\"field-hint\" id=\"notifyTestHint\"></span></div>\n </div>\n <div class=\"set-kv\" style=\"border-top:1px solid var(--line)\">\n <div class=\"set-kv-k\">Sent through</div><div class=\"set-kv-v\">${notifyChannelLabel(d.notifyChannel, d.provider)}</div>\n <div class=\"set-kv-k\">From address</div><div class=\"set-kv-v\"><span class=\"mono\">${d.notifyFrom}</span></div>\n <div class=\"set-kv-k\">Last notification</div><div class=\"set-kv-v\" id=\"notifyStatus\">${notificationStatusHtml(page.data.notificationStatus)}</div>\n </div>\n <div class=\"set-note\">${icon(\"readonly\")}<span>The channel and its sender are set at deploy. ${d.notifyChannel === \"provider\" ? \"Notifications go through your newsletter\u2019s own provider, so one about the provider refusing your account cannot reach you; Cloudflare\u2019s email avoids that. \" : \"\"}See the <a class=\"set-link\" href=\"#/docs/notifications\">setup guide</a>.</span></div>\n </div>\n </section>`;\n}\n\n/** The address rides the save bar; the test goes to the SAVED address only (the server\n * never takes one from the request), so it waits for a save. Its outcome is the channel's\n * latest word, so the status line takes it in place. */\nexport function mountNotifications(root: HTMLElement, page: SettingsPage): SectionHandle {\n const toEl = $<HTMLInputElement>(\"#notifyTo\", root);\n const test = $<HTMLButtonElement>(\"#notifyTest\", root);\n const testHint = $(\"#notifyTestHint\", root);\n const status = $(\"#notifyStatus\", root);\n toEl.addEventListener(\"input\", () => {\n page.state.notifyTo = toEl.value.trim().toLowerCase();\n page.changed();\n });\n const refreshTest = () => {\n const saved = page.saved().notifyTo;\n const reason = !saved\n ? \"Save an address to send a test.\"\n : page.state.notifyTo !== saved\n ? \"Save first: the test goes to the saved address.\"\n : \"\";\n test.disabled = Boolean(reason);\n testHint.textContent = reason;\n };\n test.onclick = () =>\n busy(test, \"Sending\u2026\", async () => {\n try {\n const r = await api<NotificationTestResponse>(\"/api/settings/notifications/test\", {\n method: \"POST\",\n });\n toast(\n r.channel === \"fake\"\n ? `Test notification recorded for ${r.to} (local stand-in: nothing is delivered)`\n : `Test notification sent to ${r.to}`,\n );\n setHtml(\n status,\n notificationStatusHtml({\n lastSent: { kind: \"test\", subject: \"\", at: Date.now() },\n lastFailure: null,\n }),\n );\n } catch (err) {\n // The outcome's home is the channel's status row, as it is for every notification;\n // a toast would say it a second time.\n setHtml(\n status,\n notificationStatusHtml({\n lastSent: null,\n lastFailure: { kind: \"test\", subject: \"\", at: Date.now(), error: message(err) },\n }),\n );\n }\n });\n refreshTest();\n return {\n repaint: refreshTest,\n reset() {\n toEl.value = page.state.notifyTo;\n refreshTest();\n },\n };\n}\n", "// The pages a reader can land on, grouped by the journey that reaches them (SPEC \u00A75, \u00A77),\n// as Settings' Reader pages section lists them and `GET /api/reader-pages/:journey/:state`\n// renders them. One list for both ends, so the editor can't offer a page the Worker\n// doesn't render.\n\n/** Where a reader meets a page: under the app's origin, or under the archive's (origin\n * and base path), and the rest of the address. Shown to the publisher, never fetched. */\nexport interface ReaderPageAddress {\n base: \"app\" | \"archive\";\n path: string;\n}\n\n/** One page or state a reader can land on. `offPath` marks one a reader reaches only when\n * something goes wrong or there is nothing yet (an expired link, a publication that has\n * sent no post). */\nexport interface ReaderPageState {\n id: string;\n label: string;\n offPath?: true;\n at: ReaderPageAddress;\n}\n\nconst SUBSCRIBE: ReaderPageAddress = { base: \"app\", path: \"/subscribe\" };\nconst CONFIRM: ReaderPageAddress = { base: \"app\", path: \"/confirm?token=\u2026\" };\nconst UNSUBSCRIBE: ReaderPageAddress = { base: \"app\", path: \"/unsubscribe?token=\u2026\" };\n\n/** Every journey a reader takes, in the order they take them, with each page and state\n * it can land on. A state's `id` is its path segment; `label` is how Settings names it.\n * A journey's `flow` says how its main-path pages relate: `steps` a reader takes in\n * order, or `peers` a reader moves between. */\nexport const READER_JOURNEYS = [\n {\n id: \"subscribe\",\n label: \"Subscribe\",\n flow: \"steps\",\n states: [\n { id: \"form\", label: \"Form\", at: SUBSCRIBE },\n { id: \"check-inbox\", label: \"Check your inbox\", at: SUBSCRIBE },\n { id: \"not-an-email\", label: \"Not an email\", offPath: true, at: SUBSCRIBE },\n ],\n },\n {\n id: \"confirm\",\n label: \"Confirm\",\n flow: \"steps\",\n states: [\n { id: \"ready\", label: \"Ready\", at: CONFIRM },\n { id: \"confirmed\", label: \"Confirmed\", at: CONFIRM },\n { id: \"expired\", label: \"Expired\", offPath: true, at: CONFIRM },\n { id: \"invalid\", label: \"Invalid link\", offPath: true, at: CONFIRM },\n ],\n },\n {\n id: \"unsubscribe\",\n label: \"Unsubscribe\",\n flow: \"steps\",\n states: [\n { id: \"ask\", label: \"Ask\", at: UNSUBSCRIBE },\n { id: \"done\", label: \"Unsubscribed\", at: UNSUBSCRIBE },\n { id: \"invalid\", label: \"Invalid link\", offPath: true, at: UNSUBSCRIBE },\n ],\n },\n {\n id: \"reading\",\n label: \"Reading\",\n flow: \"peers\",\n states: [\n { id: \"landing\", label: \"Landing page\", at: { base: \"app\", path: \"/\" } },\n { id: \"archive\", label: \"Archive\", at: { base: \"archive\", path: \"\" } },\n // The sample post's slug, as the Worker's preview renders it.\n { id: \"post\", label: \"Post page\", at: { base: \"archive\", path: \"/sample-post\" } },\n // A publication with nothing sent yet: what its first visitors get.\n {\n id: \"landing-empty\",\n label: \"Landing, no posts yet\",\n offPath: true,\n at: { base: \"app\", path: \"/\" },\n },\n {\n id: \"archive-empty\",\n label: \"Archive, no posts yet\",\n offPath: true,\n at: { base: \"archive\", path: \"\" },\n },\n {\n id: \"not-found\",\n label: \"Nothing here yet\",\n offPath: true,\n at: { base: \"archive\", path: \"/\u2026\" },\n },\n ],\n },\n] as const satisfies readonly {\n id: string;\n label: string;\n flow: \"steps\" | \"peers\";\n states: readonly ReaderPageState[];\n}[];\n\nexport type ReaderJourney = (typeof READER_JOURNEYS)[number];\nexport type ReaderJourneyId = ReaderJourney[\"id\"];\n\n/** The path of one page's preview. */\nexport function readerPagePath(journey: string, state: string): string {\n return `/api/reader-pages/${encodeURIComponent(journey)}/${encodeURIComponent(state)}`;\n}\n\n/** The journey and state a pair of path segments names, or null for one the list lacks. */\nexport function findReaderPage(\n journey: string,\n state: string,\n): { journey: ReaderJourney; state: ReaderPageState } | null {\n const j = READER_JOURNEYS.find((x) => x.id === journey);\n const s = j?.states.find((x) => x.id === state);\n return j && s ? { journey: j, state: s } : null;\n}\n", "// Settings' Reader pages section: every page a reader can land on (SPEC \u00A75), as an outline\n// beside one framed page. The journeys are the outline's headings and never controls, and\n// the pages are its rows, so the two levels can't be mistaken for each other and every\n// page is in view, the hard-to-reach ones included. A narrow card folds the outline into\n// one grouped select. The journeys and their pages are shared/'s list; each page is\n// `GET /api/reader-pages/:journey/:state`, exactly as a reader gets it, framed so nothing\n// on it acts.\n\nimport {\n READER_JOURNEYS,\n type ReaderJourney,\n type ReaderJourneyId,\n type ReaderPageAddress,\n type ReaderPageState,\n readerPagePath,\n} from \"../../shared/reader_pages\";\nimport type { DeploymentView } from \"../../shared/settings\";\nimport { apiText, describeError } from \"../api\";\nimport { onAbort } from \"../lifecycle\";\nimport { $, $$ } from \"../ui/dom\";\nimport { framedCopy } from \"../ui/email_frame\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { type IconName, icon } from \"../ui/icons\";\n\n/** What on a journey's pages follows the publisher, what they word, and what is fixed. */\nconst JOURNEY_NOTES: Record<ReaderJourneyId, Html> = {\n subscribe: html`Your name, tagline, and logo head each page; the words are Kestrel\u2019s. The embeddable form in Ways to subscribe posts to these pages too.`,\n confirm: html`Your identity heads each page. You word the email that links here, under Confirmation email; these pages\u2019 words are fixed, so what a reader agrees to can\u2019t be edited away.`,\n unsubscribe: html`Your identity heads each page; the words are fixed, so leaving always reads the same. A mail client\u2019s own Unsubscribe button leaves without a page.`,\n reading: html`Your identity heads each page, and a post page is your <a class=\"set-link\" href=\"#/template\">email template</a> around the post, as its web version shows it. Where a page lists posts, they are samples.`,\n};\n\nconst JOURNEY_ICONS: Record<ReaderJourneyId, IconName> = {\n subscribe: \"mail\",\n confirm: \"circle-check\",\n unsubscribe: \"circle-minus\",\n reading: \"book\",\n};\n\n/** The width a preview is framed at: the page as a reader's window shows it, or a phone. */\ntype PreviewWidth = \"desktop\" | \"phone\";\n\n/** A peer's mark: a dot drawn in CSS rather than a \"\u00B7\" glyph, which sits wherever the font\n * puts it, so the circle's own centering places it exactly. */\nconst PEER_DOT = html`<span class=\"set-rp-dot\"></span>`;\n\n/** One row of the outline: a step's number, a peer's dot, or an off-path page's branch. */\nfunction outlineRow(j: ReaderJourney, s: ReaderPageState, mark: Html | string): Html {\n return html`<li><button type=\"button\" class=\"set-rp-item${s.offPath ? \" off\" : \"\"}\" data-journey=\"${j.id}\" data-state=\"${s.id}\" aria-current=\"false\"><span class=\"set-rp-mark\" aria-hidden=\"true\">${mark}</span>${s.label}</button></li>`;\n}\n\n/** The outline: each journey a heading over its main-path pages, then the ones off it.\n * A thread joins a journey's steps; peers carry a dot, not a number. The off-path pages\n * are a list of their own, so the thread ends at the last step and never runs behind\n * a page a reader reaches only when something goes wrong. */\nfunction outline(): Html {\n return html`${READER_JOURNEYS.map((j) => {\n const states: readonly ReaderPageState[] = j.states;\n const steps = j.flow === \"steps\";\n return html`<div class=\"set-rp-journey\" data-journey=\"${j.id}\">\n <h3 class=\"set-rp-jhead\" id=\"rpJourney-${j.id}\">${icon(JOURNEY_ICONS[j.id])}${j.label}</h3>\n <ul class=\"set-rp-list${steps ? \" steps\" : \"\"}\" aria-labelledby=\"rpJourney-${j.id}\">\n ${states.filter((s) => !s.offPath).map((s, i) => outlineRow(j, s, steps ? String(i + 1) : PEER_DOT))}\n </ul>\n <ul class=\"set-rp-list\" aria-label=\"${j.label}, off the main path\">\n ${states.filter((s) => s.offPath).map((s) => outlineRow(j, s, icon(\"branch\")))}\n </ul>\n </div>`;\n })}`;\n}\n\n/** The outline as one select, for a card too narrow for it: a group per journey, its\n * steps numbered as the outline numbers them. An off-path page carries no mark: a branch\n * glyph alone in an option's text reads as a stray character, not as \"off the path\",\n * and these pages' names already say what went wrong. */\nfunction picker(): Html {\n return html`<select id=\"rpSelect\" aria-label=\"Reader page\">${READER_JOURNEYS.map((j) => {\n const states: readonly ReaderPageState[] = j.states;\n let step = 0;\n return html`<optgroup label=\"${j.label}\">${states.map((s) => {\n const n = j.flow === \"steps\" && !s.offPath ? `${++step}. ` : \"\";\n return html`<option value=\"${j.id}/${s.id}\">${n}${s.label}</option>`;\n })}</optgroup>`;\n })}</select>`;\n}\n\n/** The section's markup, under the head the page builds for every section. */\nexport function readerPagesSection(head: Html): Html {\n return html`\n <section class=\"set-sec set-rp-sec\" id=\"readerPages\">\n ${head}\n <p class=\"set-lede\">Every page a reader can land on, exactly as they see it, with your publication\u2019s identity and sample values. Nothing here changes a page; it shows the ones that are hard to reach by hand.</p>\n <div class=\"set-card set-rp\">\n <nav class=\"set-rp-nav\" aria-label=\"Reader pages\">${outline()}</nav>\n <div class=\"set-rp-pick\">${picker()}</div>\n <div class=\"set-rp-main\">\n <div class=\"set-rp-top\">\n <div>\n <p class=\"set-rp-crumb\"><span class=\"set-rp-crumb-j\" id=\"rpJourney\"></span><span class=\"set-rp-crumb-sep\">/</span><span id=\"rpState\"></span></p>\n <p class=\"set-rp-note\" id=\"rpNote\"></p>\n </div>\n <span class=\"set-wtog\" id=\"rpWidth\" role=\"group\" aria-label=\"Preview width\">\n <button type=\"button\" class=\"wtog-btn\" data-width=\"desktop\" aria-pressed=\"true\">${icon(\"desktop\")}Desktop</button>\n <button type=\"button\" class=\"wtog-btn\" data-width=\"phone\" aria-pressed=\"false\">${icon(\"phone\")}Phone</button>\n </span>\n </div>\n <div class=\"set-rp-at\"><span class=\"mono\" id=\"rpAt\"></span><span>Sample \u00B7 links and forms do nothing</span></div>\n <p class=\"set-rp-error\" id=\"rpError\" role=\"alert\" hidden></p>\n <div class=\"set-rp-stage\" id=\"rpStage\">\n <iframe class=\"set-rp-frame\" id=\"rpFrame\" sandbox=\"allow-same-origin\" title=\"Reader page preview\" scrolling=\"no\"></iframe>\n </div>\n </div>\n </div>\n </section>`;\n}\n\n/** Where a reader meets a page on this deployment, as the address line shows it: without\n * its scheme, which says nothing to a publisher. */\nexport function readerPageAddress(at: ReaderPageAddress, d: DeploymentView): string {\n const base = at.base === \"app\" ? d.appOrigin : `${d.archiveOrigin}${d.archiveBasePath}`;\n return `${base}${at.path}`.replace(/^https?:\\/\\//, \"\");\n}\n\n/** The section's controls, once its markup is in the document. `refresh` re-reads the\n * page shown, for after any save, since one may have changed the identity it wears. */\nexport function mountReaderPages(\n root: HTMLElement,\n deployment: DeploymentView,\n signal: AbortSignal,\n): { refresh(): void } {\n const nav = $(\".set-rp-nav\", root);\n const select = $<HTMLSelectElement>(\"#rpSelect\", root);\n const widthEl = $(\"#rpWidth\", root);\n const stage = $(\"#rpStage\", root);\n const frame = $<HTMLIFrameElement>(\"#rpFrame\", root);\n const errorEl = $(\"#rpError\", root);\n\n let journey: ReaderJourney = READER_JOURNEYS[0];\n let state: ReaderPageState = journey.states[0];\n let width: PreviewWidth = \"desktop\";\n // Only the latest read paints: a slow one answered after a later click is dropped.\n let seq = 0;\n let watch: ResizeObserver | null = null;\n onAbort(signal, () => watch?.disconnect());\n\n const paintChoice = () => {\n for (const b of $$<HTMLButtonElement>(\".set-rp-item\", nav)) {\n const on = b.dataset.journey === journey.id && b.dataset.state === state.id;\n b.setAttribute(\"aria-current\", on ? \"true\" : \"false\");\n }\n for (const g of $$(\".set-rp-journey\", nav)) {\n g.classList.toggle(\"current\", g.dataset.journey === journey.id);\n }\n select.value = `${journey.id}/${state.id}`;\n $(\"#rpJourney\", root).textContent = journey.label;\n $(\"#rpState\", root).textContent = state.label;\n setHtml($(\"#rpNote\", root), JOURNEY_NOTES[journey.id]);\n $(\"#rpAt\", root).textContent = readerPageAddress(state.at, deployment);\n };\n\n const fitHeight = () => {\n const h = frame.contentDocument?.body?.scrollHeight;\n if (h) {\n frame.style.height = `${h}px`;\n }\n };\n\n const load = async () => {\n const mine = ++seq;\n try {\n const doc = await apiText(readerPagePath(journey.id, state.id), { signal });\n if (mine !== seq || signal.aborted) {\n return;\n }\n errorEl.hidden = true;\n stage.hidden = false;\n // Under the policy a page that submits no form carries, so beside the sandbox no\n // form on it submits (ui/email_frame.ts).\n frame.srcdoc = framedCopy(doc);\n } catch (e) {\n if (mine !== seq || signal.aborted) {\n return;\n }\n // The page named above is not the one the frame last held, so the frame empties\n // rather than show it under the wrong name, and the error takes its place.\n frame.srcdoc = \"\";\n stage.hidden = true;\n errorEl.textContent = `Couldn\u2019t load this page: ${describeError(e)}`;\n errorEl.hidden = false;\n }\n };\n\n const choose = (journeyId: string | undefined, stateId: string | undefined) => {\n const j = READER_JOURNEYS.find((x) => x.id === journeyId);\n const states: readonly ReaderPageState[] = j?.states ?? [];\n const s = states.find((x) => x.id === stateId);\n if (!j || !s || (j === journey && s === state)) {\n return;\n }\n journey = j;\n state = s;\n paintChoice();\n void load();\n };\n\n frame.onload = () => {\n const page = frame.contentDocument;\n if (!page || signal.aborted) {\n return;\n }\n // A preview shows a page; following a link would leave it for the real thing.\n page.addEventListener(\"click\", (e) => {\n const target = e.target as Element | null;\n if (target?.closest?.(\"a[href], area[href]\")) {\n e.preventDefault();\n }\n });\n // As tall as the page, so Settings scrolls it, and kept fitted as fonts, images,\n // and the width change it.\n watch?.disconnect();\n fitHeight();\n if (page.body && typeof ResizeObserver !== \"undefined\") {\n watch = new ResizeObserver(fitHeight);\n watch.observe(page.body);\n }\n };\n\n nav.addEventListener(\"click\", (e) => {\n const b = (e.target as Element).closest<HTMLButtonElement>(\".set-rp-item\");\n choose(b?.dataset.journey, b?.dataset.state);\n });\n select.addEventListener(\"change\", () => {\n const [j, s] = select.value.split(\"/\");\n choose(j, s);\n });\n widthEl.addEventListener(\"click\", (e) => {\n const b = (e.target as Element).closest<HTMLButtonElement>(\"button[data-width]\");\n const w = b?.dataset.width;\n if ((w !== \"desktop\" && w !== \"phone\") || w === width) {\n return;\n }\n width = w;\n for (const o of $$<HTMLButtonElement>(\"button\", widthEl)) {\n o.setAttribute(\"aria-pressed\", String(o.dataset.width === w));\n }\n stage.classList.toggle(\"phone\", w === \"phone\");\n });\n\n paintChoice();\n void load();\n return { refresh: () => void load() };\n}\n", "// Settings' Ways to subscribe section: the public subscribe page's address and a form to\n// paste into the publisher's own site. Both post to the public /subscribe and start the\n// double opt-in, never an auto-confirm (I1).\n\nimport { connectProviderLink } from \"../deployment\";\nimport { copyButton, copyFace, wireCopy } from \"../ui/copy\";\nimport { $, $$ } from \"../ui/dom\";\nimport { escapeHtml, type Html, html, setHtml } from \"../ui/html\";\nimport { type IconName, icon } from \"../ui/icons\";\nimport { chip, type SectionHandle, type SettingsPage, secHead } from \"./page\";\n\ntype EmbedMode = \"plain\" | \"styled\";\n\n/** The embed's heading: \"Subscribe to <name>\", or \"Subscribe\" with no name (SPEC \u00A79). */\nfunction embedHeading(name: string): string {\n return name ? `Subscribe to ${name}` : \"Subscribe\";\n}\n\n/** The snippet a publisher pastes into their own page. It is text here (shown in a code\n * block and copied), escaped for the page it will land in. */\nexport function buildEmbed(mode: EmbedMode, name: string, appOrigin: string): string {\n const heading = escapeHtml(embedHeading(name));\n const action = `${escapeHtml(appOrigin)}/subscribe`;\n if (mode === \"styled\") {\n // The hosted subscribe page's look, carried inline: a serif headline (Georgia, the\n // reader pages' own fallback, so it needs no web font) over a sans body, in the reader\n // pages' ink and muted colors. The headline inherits its color from the host page, and\n // the field's border is a step darker than the reader pages' line so it still shows on\n // a host page of any background. The apostrophe is plain ASCII, so the snippet reads\n // right whatever encoding the host page declares.\n return (\n `<form action=\"${action}\" method=\"post\" style=\"max-width:460px;font:16px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif\">\\n` +\n ` <div style=\"font:600 22px/1.2 Georgia,'Times New Roman',serif;letter-spacing:-0.01em;margin-bottom:12px\">${heading}</div>\\n` +\n ` <div style=\"display:flex;gap:10px;flex-wrap:wrap\">\\n` +\n ` <input type=\"email\" name=\"email\" required placeholder=\"you@example.com\" aria-label=\"Email address\" style=\"flex:1 1 220px;padding:11px 13px;border:1px solid #d6d3cc;border-radius:9px;background:#fff;color:#1b1a17;font:inherit\">\\n` +\n ` <button type=\"submit\" style=\"padding:11px 20px;border:1px solid #1b1a17;border-radius:9px;background:#1b1a17;color:#fbfbfa;font:inherit;font-size:15px;font-weight:600;cursor:pointer;white-space:nowrap\">Subscribe</button>\\n` +\n ` </div>\\n` +\n ` <p style=\"margin:12px 0 0;font-size:12.5px;color:#6b6a63\">We\u2019ll email you a link to confirm. Unsubscribe anytime.</p>\\n` +\n `</form>`\n );\n }\n return (\n `<form action=\"${action}\" method=\"post\">\\n` +\n ` <label>\\n` +\n ` ${heading}\\n` +\n ` <input type=\"email\" name=\"email\" placeholder=\"you@example.com\" required>\\n` +\n ` </label>\\n` +\n ` <button type=\"submit\">Subscribe</button>\\n` +\n `</form>`\n );\n}\n\n/** The snippet as it will look on the publisher's site: a mockup, inert. The styled form\n * wears the hosted page's look; the plain one is laid out as its markup renders, with\n * the browser's own controls, since bare markup gets the site's styles or none. */\nfunction embedPreview(mode: EmbedMode, name: string): Html {\n const heading = embedHeading(name);\n if (mode === \"styled\") {\n return html`<form class=\"set-pf-styled\"><span class=\"l\">${heading}</span><div class=\"rowf\"><input type=\"email\" placeholder=\"you@example.com\" disabled><button type=\"button\" class=\"sub-btn\" tabindex=\"-1\">Subscribe</button></div><p class=\"set-pf-fine\">We\u2019ll email you a link to confirm. Unsubscribe anytime.</p></form>`;\n }\n return html`<form class=\"set-pf-plain\"><label>${heading} <input type=\"email\" placeholder=\"you@example.com\" disabled></label> <button type=\"button\" tabindex=\"-1\">Subscribe</button></form>`;\n}\n\nfunction appOriginOf(page: SettingsPage): string {\n return page.data.deployment.appOrigin || location.origin;\n}\n\n/** The page's line names the publication only when it has a name, since the page then\n * reads \"Subscribe\" (SPEC \u00A79). */\nfunction pageLine(name: string): string {\n return name\n ? \"A public page with your publication\u2019s name and a sign-up form ready to go.\"\n : \"A public page with a sign-up form ready to go.\";\n}\n\n/** One way to subscribe, a band of the section's card: its glyph and title, its line,\n * and any control at the head's right end, then what it offers. The two ways share\n * this head so they read as parallel options. */\nfunction way(\n glyph: IconName,\n title: string,\n line: Html,\n body: Html,\n control: Html | null = null,\n): Html {\n return html`<div class=\"set-way\"><div class=\"set-way-head\"><div class=\"set-way-name\"><h3 class=\"set-way-title\">${icon(glyph)}${title}</h3><p class=\"set-way-line\">${line}</p></div>${control}</div>${body}</div>`;\n}\n\nexport function subscribeSection(page: SettingsPage): Html {\n const subscribeUrl = `${appOriginOf(page)}/subscribe`;\n return html`\n <section class=\"set-sec set-sec-subscribe\">\n ${secHead(\"Ways to subscribe\", chip(\"copyout\", \"Copy-out\"))}\n <p class=\"set-lede\">Two ways for readers to join your list. Either way, a reader is added only once they confirm by email.</p>\n ${\n // Signups are closed while no provider is connected (SPEC \u00A79): the page says so and\n // a form takes no address. The address and the code stay, ready for when they open.\n page.data.deployment.provider === \"none\"\n ? html`<div class=\"health amber set-sub-not-connected\" role=\"status\"><span class=\"health-dot\">\u26A0\uFE0F</span><div><span>No email provider connected, so signups aren\u2019t open yet: the page below says so, and the form takes no address. ${connectProviderLink()} to open signups.</span></div></div>`\n : null\n }\n <div class=\"set-card\">\n ${way(\n \"link\",\n \"Share the page\",\n html`<span id=\"subscribePageLine\"></span>`,\n html`<div class=\"set-way-body set-way-addr\"><code class=\"set-addr\">${subscribeUrl}</code>${copyButton(subscribeUrl, \"Copy the subscribe page\u2019s address\")}<a class=\"ghost-link\" href=\"${subscribeUrl}\" target=\"_blank\" rel=\"noopener\">Open \u2197</a></div>`,\n )}\n ${way(\n \"code\",\n \"Embed a form\",\n html`Put a sign-up form on another site you own.`,\n html`<div class=\"set-embed\">\n <div class=\"set-embed-preview\" id=\"embedPreview\"></div>\n <pre class=\"set-embed-code\"><code id=\"embedCode\"></code></pre>\n </div>\n <div class=\"set-embed-foot\" id=\"embedCopy\"></div>`,\n html`<span class=\"set-wtog\" role=\"group\" aria-label=\"Snippet style\">\n <button type=\"button\" class=\"wtog-btn\" data-embed=\"styled\" aria-pressed=\"true\">Styled</button>\n <button type=\"button\" class=\"wtog-btn\" data-embed=\"plain\" aria-pressed=\"false\">Plain</button>\n </span>`,\n )}\n </div>\n </section>`;\n}\n\n/** The embed's one act, copying the snippet, as a Secondary button rather than the quiet\n * Copy a code block wears, since here copying is what the band is for (DESIGN \u00A77). */\nfunction embedCopy(mode: EmbedMode, code: string): Html {\n const variant = mode === \"styled\" ? \"Styled\" : \"Plain\";\n return html`<button type=\"button\" class=\"secondary set-embed-copy\" data-copy=\"${code}\" data-copy-word=\"Copy HTML\" aria-label=\"Copy HTML, ${variant} form\">${copyFace(false, \"Copy HTML\")}</button>`;\n}\n\n/** The Plain/Styled toggle drives the code, the rendered preview, and the Copy payload;\n * the name follows the live identity field. */\nexport function mountSubscribe(root: HTMLElement, page: SettingsPage): SectionHandle {\n const appOrigin = appOriginOf(page);\n const codeEl = $(\"#embedCode\", root);\n const copyEl = $(\"#embedCopy\", root);\n const previewEl = $(\"#embedPreview\", root);\n const lineEl = $(\"#subscribePageLine\", root);\n // Styled first: it pastes in looking like the hosted page anywhere, so the first form a\n // publisher sees is their publication's, and Plain is the choice for one who'll style it.\n let mode: EmbedMode = \"styled\";\n const name = () => page.state.name || page.fromDisplay;\n const rebuild = () => {\n const code = buildEmbed(mode, name(), appOrigin);\n codeEl.textContent = code;\n setHtml(copyEl, embedCopy(mode, code));\n setHtml(previewEl, embedPreview(mode, name()));\n lineEl.textContent = pageLine(name());\n };\n const modeButtons = $$(\"[data-embed]\", root);\n for (const b of modeButtons) {\n b.onclick = () => {\n mode = b.dataset.embed === \"styled\" ? \"styled\" : \"plain\";\n for (const o of modeButtons) {\n o.setAttribute(\"aria-pressed\", String(o.dataset.embed === mode));\n }\n rebuild();\n };\n }\n wireCopy($(\".set-sec-subscribe\", root));\n rebuild();\n // The page repaints every section on any change, but the section shows only the name:\n // rebuilt for anything else, its Copy would lose a check it had just shown.\n let shownName = name();\n return {\n repaint() {\n if (name() !== shownName) {\n shownName = name();\n rebuild();\n }\n },\n };\n}\n", "// The Settings page: its sections in groups by whom they serve (the publication, its\n// readers, its emails, the publisher), editable first and set-at-deploy last, under one\n// save bar. Each section is its own module; this one loads\n// the settings, holds the live state against the saved baseline, and commits it.\n\nimport { fromDisplayName } from \"../../shared/publication\";\nimport type {\n ConfirmationEmailCopy,\n SettingsPatchBody,\n SettingsResponse,\n SettingsSavedResponse,\n SettingsView,\n} from \"../../shared/settings\";\nimport { ApiError, api } from \"../api\";\nimport { renderSidebarBrand } from \"../brand\";\nimport { mount } from \"../lifecycle\";\nimport { appState } from \"../state\";\nimport { $ } from \"../ui/dom\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { savebar } from \"../ui/savebar\";\nimport { fieldError, renderError, toast } from \"../ui/widgets\";\nimport { confirmationSection, mountConfirmation } from \"./confirmation\";\nimport { instanceSection, senderSection } from \"./deploy\";\nimport { mountRecipients, mountTemplate, recipientsSection, templateSection } from \"./emails\";\nimport { identitySection, mountIdentity } from \"./identity\";\nimport { mountNotifications, notificationsSection } from \"./notifications\";\nimport {\n chip,\n message,\n type SectionHandle,\n type SettingsBaseline,\n type SettingsFormState,\n type SettingsPage,\n secHead,\n} from \"./page\";\nimport { mountReaderPages, readerPagesSection } from \"./reader_pages\";\nimport { remakeIdentity, savedToast, withRemakeConfirm } from \"./remake\";\nimport { mountSubscribe, subscribeSection } from \"./subscribe\";\n\n/** One group of sections, kept together by whom they serve: no heading of its own, since\n * the sections' titles already say what each is, only a wider gap from the next group. */\nfunction group(id: string, sections: Html[]): Html {\n return html`<div class=\"set-group\" data-group=\"${id}\">${sections}</div>`;\n}\n\nfunction baselineOf(state: SettingsBaseline): SettingsBaseline {\n return {\n name: state.name,\n tagline: state.tagline,\n address: state.address,\n recipients: [...state.recipients],\n confirmation: { ...state.confirmation },\n notifyTo: state.notifyTo,\n };\n}\n\nconst sameList = (a: string[], b: string[]) =>\n a.length === b.length && a.every((x, i) => x === b[i]);\nconst sameCopy = (a: ConfirmationEmailCopy, b: ConfirmationEmailCopy) =>\n a.subject === b.subject &&\n a.body === b.body &&\n a.buttonLabel === b.buttonLabel &&\n a.reassurance === b.reassurance;\n\nexport async function renderSettings(root: HTMLElement, signal: AbortSignal): Promise<void> {\n setHtml(\n root,\n html`<div class=\"settings\"><div class=\"page-head\"><h1>Settings</h1><p class=\"set-lede set-page-lede\">Your publication\u2019s identity, the ways readers subscribe and the pages they land on, the email each post is sent inside, and where Kestrel tells you about your sends. Facts set when Kestrel was deployed come last, read-only.</p></div><div id=\"settingsBody\" class=\"muted\">Loading\u2026</div></div>`,\n );\n const body = $(\"#settingsBody\", root);\n let data: SettingsResponse;\n try {\n data = await api<SettingsResponse>(\"/api/settings\", { signal });\n } catch (e) {\n renderError(body, message(e), () => mount(renderSettings));\n return;\n }\n const s = data.settings;\n const p = s.publication;\n const ce = s.confirmationEmail;\n // Live, in-memory state. The save bar tracks the persisted fields against the saved\n // baseline; the logo saves the moment it changes (its own endpoints), and the email\n // template has its own page and its own Save, so neither feeds the bar.\n const state: SettingsFormState = {\n name: p.name || \"\",\n tagline: p.tagline || \"\",\n address: p.address || \"\",\n logoUrl: p.logoUrl || \"\",\n recipients: [...(s.testRecipients || [])],\n // The confirmation email's words (SPEC \u00A77); the layout, masthead, and confirm link\n // are Kestrel's.\n confirmation: {\n subject: ce.subject || \"\",\n body: ce.body || \"\",\n buttonLabel: ce.buttonLabel || \"\",\n reassurance: ce.reassurance || \"\",\n },\n notifyTo: s.notifications.to || \"\",\n };\n let baseline = baselineOf(state);\n const fromDisplay = fromDisplayName(data.deployment.fromAddress);\n\n const isDirty = () =>\n state.name !== baseline.name ||\n state.tagline !== baseline.tagline ||\n state.address !== baseline.address ||\n !sameList(state.recipients, baseline.recipients) ||\n !sameCopy(state.confirmation, baseline.confirmation) ||\n state.notifyTo !== baseline.notifyTo;\n\n const handles: SectionHandle[] = [];\n // The shared bottom save bar (Save + Discard), its callbacks the hoisted saveSettings /\n // discardSettings below.\n const bar = savebar.attach({ onSave: saveSettings, onDiscard: discardSettings }, signal);\n let refreshReaderPages = () => {};\n const page: SettingsPage = {\n data,\n state,\n saved: () => baseline,\n fromDisplay,\n fromName: fromDisplay || \"Your publication\",\n changed() {\n for (const h of handles) {\n h.repaint?.();\n }\n bar.setDirty(isDirty());\n },\n // Keep the cached config and the sidebar brand, which reads the same identity, in\n // step with a save: this page's own response, with the saved half.\n applySaved(settings: SettingsView) {\n appState.appConfig = { ...data, settings };\n renderSidebarBrand();\n // The reader pages wear the saved identity, so the one shown re-reads it.\n refreshReaderPages();\n },\n };\n\n setHtml(\n body,\n html`\n ${group(\"publication\", [identitySection(page)])}\n ${group(\"readers\", [subscribeSection(page), confirmationSection(page), readerPagesSection(secHead(\"Reader pages\", chip(\"preview\", \"Preview\")))])}\n ${group(\"emails\", [templateSection(page), recipientsSection()])}\n ${group(\"you\", [notificationsSection(page)])}\n ${group(\"deploy\", [senderSection(page), instanceSection(page)])}\n `,\n );\n handles.push(\n mountIdentity(body, page, signal),\n mountSubscribe(body, page),\n mountConfirmation(body, page),\n mountTemplate(body, page),\n mountRecipients(body, page),\n mountNotifications(body, page),\n );\n const readerPages = mountReaderPages(body, data.deployment, signal);\n refreshReaderPages = readerPages.refresh;\n\n // --- save / discard. Wired into the shared save bar above; the controller runs\n // saveSettings inside busy() on its Save button, so these stay plain callbacks.\n // The field a refused save marked, cleared when the next save starts or the edits are\n // discarded, neither of which edits the field itself.\n let clearFieldMark: (() => void) | null = null;\n const unmarkField = () => {\n clearFieldMark?.();\n clearFieldMark = null;\n };\n async function saveSettings() {\n unmarkField();\n try {\n // An identity change the template renders reaches every scheduled email, so the\n // server may ask for the acknowledgement first (withRemakeConfirm); a save that\n // touches only recipients or the confirmation wording never does.\n const payload: SettingsPatchBody = {\n publication: { name: state.name, tagline: state.tagline, address: state.address },\n testRecipients: state.recipients,\n confirmationEmail: state.confirmation,\n notifications: { to: state.notifyTo },\n };\n const r = await withRemakeConfirm(\n (ack) =>\n api<SettingsSavedResponse>(\"/api/settings\", {\n method: \"PUT\",\n json: ack ? { ...payload, remake: ack } : payload,\n }),\n remakeIdentity(\"Save\", \"Saving\"),\n signal,\n );\n if (!r) {\n return; // declined: the edits stay, the bar stays up\n }\n // Adopt the server's normalized result (trim, lowercase, dedupe) as the baseline.\n const ns = r.settings;\n state.name = ns.publication.name;\n state.tagline = ns.publication.tagline;\n state.address = ns.publication.address;\n state.recipients = [...ns.testRecipients];\n state.confirmation = { ...ns.confirmationEmail };\n state.notifyTo = ns.notifications.to;\n baseline = baselineOf(state);\n for (const h of handles) {\n h.reset?.();\n }\n page.applySaved(ns);\n page.changed(); // clean now: slides the bar away\n toast(savedToast(\"Settings saved\", r.remade));\n } catch (err) {\n // A failed save leaves the edits in place (still dirty, so the bar stays up), and the\n // bar says why, as on the Template page: a toast would sit over the bar and vanish\n // while the edits it is about are still unsaved (DESIGN \u00A75). A refusal naming a field\n // on the page says why under that field, focuses it, and the bar points there.\n const input = err instanceof ApiError ? fieldInput(root, err.field) : null;\n if (input && err instanceof ApiError) {\n clearFieldMark = fieldError(input, message(err));\n input.focus();\n bar.showError(\"Not saved: fix the field marked above.\");\n } else {\n bar.showError(message(err));\n }\n }\n }\n function discardSettings() {\n unmarkField();\n Object.assign(state, baselineOf(baseline));\n for (const h of handles) {\n h.reset?.();\n }\n page.changed();\n }\n\n page.changed();\n}\n\n// The input each settings field the API can name is edited in, by its wire name.\nconst FIELD_INPUTS: Record<string, string> = {\n \"publication.name\": \"#setName\",\n \"publication.tagline\": \"#setTagline\",\n \"publication.address\": \"#setAddress\",\n testRecipients: \"#recipInput\",\n \"notifications.to\": \"#notifyTo\",\n \"confirmationEmail.subject\": \"#ceSubject\",\n \"confirmationEmail.body\": \"#ceMessage\",\n \"confirmationEmail.buttonLabel\": \"#ceButton\",\n \"confirmationEmail.reassurance\": \"#ceFooter\",\n};\n\n/** The input a refusal's `field` names, when the page shows it; null otherwise. */\nfunction fieldInput(\n root: HTMLElement,\n field: string | null,\n): HTMLInputElement | HTMLTextAreaElement | null {\n const sel = field ? FIELD_INPUTS[field] : undefined;\n const el = sel ? root.querySelector(sel) : null;\n return (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) &&\n !el.closest(\"[hidden]\")\n ? el\n : null;\n}\n", "// The subscribers list: the by-status composition and the filterable roster.\n\nimport type { SubscriberListItem, SubscriberListResponse } from \"../../shared/subscribers\";\nimport { api, describeError } from \"../api\";\nimport { $, $$ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport {\n type ListState,\n listQuery,\n listToolbar,\n renderPager,\n th,\n wireSort,\n wireToolbar,\n} from \"../ui/list_controls\";\nimport { badge, infoTip, openMenu, renderError } from \"../ui/widgets\";\nimport { addSubscriberModal, confirmUnsubscribe } from \"./dialogs\";\n\n// The story of the list as a whole: its composition (by-status counts) and the roster,\n// filterable, sortable, and searchable. Consent status and suppression are separate\n// axes: the status filter narrows the roster; the suppression facet is an overlay. The\n// dashboard tiles deep-link via #/subscribers/<filter> \u2014 `initialFilter` seeds the\n// status filter, or the suppression facet for \"suppressed\".\nconst SUB_STATUSES = [\n { value: \"confirmed\", label: \"Confirmed\" },\n { value: \"pending\", label: \"Pending\" },\n { value: \"unsubscribed\", label: \"Unsubscribed\" },\n];\n\nexport async function renderSubscribers(\n initialFilter: string | undefined,\n root: HTMLElement,\n signal: AbortSignal,\n): Promise<void> {\n const state: ListState = {\n status: \"\",\n search: \"\",\n suppressed: \"\",\n sort: \"joined\",\n dir: \"desc\",\n offset: 0,\n limit: 50,\n };\n if (initialFilter === \"suppressed\") {\n state.suppressed = \"only\";\n } else if (\n initialFilter === \"confirmed\" ||\n initialFilter === \"pending\" ||\n initialFilter === \"unsubscribed\"\n ) {\n state.status = initialFilter;\n }\n\n setHtml(\n root,\n html`\n <div class=\"spread page-head\"><h1>Subscribers</h1><button class=\"primary\" id=\"addSub\">Add subscriber</button></div>\n <div id=\"subCounts\" class=\"muted\">Loading\u2026</div>\n ${listToolbar({ statuses: SUB_STATUSES, suppressible: true, searchPlaceholder: \"Search email\u2026\" })}\n <div id=\"subList\" class=\"muted\">Loading\u2026</div>\n <div id=\"subPager\"></div>`,\n );\n\n const listEl = $(\"#subList\");\n const pagerEl = $(\"#subPager\");\n\n async function load() {\n try {\n const data = await api<SubscriberListResponse>(`/api/subscribers?${listQuery(state)}`, {\n signal,\n });\n const c = data.counts;\n // The counts card stays a global by-status summary (independent of the active\n // filter); the page total below reflects the filtered roster.\n setHtml(\n $(\"#subCounts\"),\n html`<div class=\"card row\" style=\"gap:24px\"><span><strong>${c.confirmed}</strong> confirmed</span><span>${c.pending} pending</span><span>${c.unsubscribed} unsubscribed</span><span>${c.suppressed} suppressed</span>${infoTip(\n \"Pending: subscribed but hasn\u2019t clicked the confirmation email. Confirmed: consented, and receives sends. Unsubscribed: opted out. Suppressed: bounced or complained, and never mailed, whatever the consent state.\",\n { below: true },\n )}</div>`,\n );\n renderSubTable(listEl, data.subscribers, state, load, signal);\n renderPager(pagerEl, state, data.page, load);\n } catch (e) {\n renderError(listEl, describeError(e), load);\n }\n }\n\n $(\"#addSub\").onclick = () => addSubscriberModal(load, signal);\n wireToolbar(root, state, load);\n load();\n}\n\n// The inline suppression flag rides next to the email (suppression is a deliverability\n// overlay, not a consent status) and names WHY: bounced / complaint / blocked, with the\n// provider detail in the tooltip. Reasons come from the webhook (bounce, complaint) or a\n// manual block; anything unrecognized falls back to a generic label.\nconst SUPPRESSION_LABELS: Record<string, string> = {\n bounce: \"bounced\",\n complaint: \"complaint\",\n manual: \"blocked\",\n};\nconst SUPPRESSION_TIPS: Record<string, string> = {\n bounce: \"Hard bounce: mail to this address failed, so it won\u2019t be mailed again.\",\n complaint: \"Marked as spam, so it won\u2019t be mailed again, whatever the consent state.\",\n manual: \"Manually blocked, so it won\u2019t be mailed.\",\n};\nfunction suppressionFlag(s: SubscriberListItem): Html | null {\n if (!s.suppressed) {\n return null;\n }\n const reason = s.suppression_reason ?? \"\";\n const label = SUPPRESSION_LABELS[reason] || \"suppressed\";\n const base =\n SUPPRESSION_TIPS[reason] || \"Suppressed, so it won\u2019t be mailed, whatever the consent state.\";\n const tip = s.suppression_detail ? `${base} (${s.suppression_detail})` : base;\n return html` <span class=\"badge suppressed row-flag\" title=\"${tip}\">${label}</span>`;\n}\n\nfunction renderSubTable(\n listEl: Element,\n rows: SubscriberListItem[],\n state: ListState,\n reload: () => void,\n signal: AbortSignal,\n): void {\n if (!rows.length) {\n setHtml(listEl, html`<p class=\"muted\">No subscribers match.</p>`);\n return;\n }\n // The joined cell is named so the \u2264720px layout can stack a row and label it.\n setHtml(\n listEl,\n html`<div class=\"table-wrap\"><table class=\"list-table subs-table stacks has-actions\"><colgroup><col><col class=\"c-status\"><col class=\"c-date\"><col class=\"c-act\"></colgroup><thead><tr>${th(\"Email\", \"email\", state)}${th(\"Status\", null, state)}${th(\"Joined\", \"joined\", state)}<th></th></tr></thead><tbody>${rows.map(\n (s) =>\n html`<tr data-id=\"${s.id}\"><td>${s.email}${suppressionFlag(s)}</td><td>${badge(s.status)}</td><td class=\"muted joined\">${fmt(s.created_at)}</td><td class=\"act\">${\n s.status === \"confirmed\"\n ? html`<button class=\"icon\" data-menu=\"${s.id}\" aria-label=\"Subscriber actions\" aria-haspopup=\"menu\">\u22EF</button>`\n : null\n }</td></tr>`,\n )}</tbody></table></div>`,\n );\n wireSort(listEl, state, reload);\n for (const b of $$<HTMLButtonElement>(\"[data-menu]\", listEl)) {\n b.onclick = (e) => {\n e.stopPropagation();\n const row = rows.find((r) => r.id === b.dataset.menu);\n if (!row) {\n return;\n }\n openMenu(\n b,\n [\n {\n label: \"Unsubscribe\",\n danger: true,\n onClick: () => confirmUnsubscribe(row, reload, signal),\n },\n ],\n signal,\n );\n };\n }\n}\n", "// The hash router: route() mounts the view the hash names, and the mount tears the\n// previous one down; the leave guards live here, over the mounted view's handle, wired by\n// installRouter() when boot calls it.\n\nimport { dashboardRouteFromSearch, isDashboardRoute } from \"../shared/dashboard_link\";\nimport { renderDashboard } from \"./dashboard/dashboard\";\nimport { mount, mounted } from \"./lifecycle\";\nimport { renderDrafts } from \"./posts/drafts\";\nimport { renderEditor } from \"./posts/editor\";\nimport { renderWebVersion, webPostId } from \"./posts/web\";\nimport { renderDocs } from \"./room/docs\";\nimport { renderReference } from \"./room/reference\";\nimport { renderSent } from \"./sends/list\";\nimport { renderSentRecord } from \"./sends/record\";\nimport { renderSettings } from \"./settings/settings\";\nimport { renderTemplate } from \"./settings/template\";\nimport { setNavOpen } from \"./shell\";\nimport { renderSubscribers } from \"./subscribers/list\";\n\nconst LEAVE_MSG = \"You have unsaved changes. Leave without saving?\";\n\n// The hash the mounted view was routed at: a hashchange back to it is nothing to do (the\n// echo of a declined leave prompt putting it back), and a declined prompt knows where to go.\nlet mountedHash = \"\";\n\n/** Mount the view the hash names; the mount tears the previous one down. */\nexport function route(): Promise<void> {\n const hash = location.hash || \"#/dashboard\";\n mountedHash = hash;\n // Views put a route's segments into API paths as they are, so a hash a link from an email\n // could not name (a `..` segment, say) opens the dashboard home, as a mangled link does.\n const [, view, arg, sub] = (isDashboardRoute(hash.slice(1)) ? hash : \"#/dashboard\").split(\"/\");\n // The editor wants the full width, and carries its own \"\u2190 Posts\" affordance, so\n // it hides the sidebar rather than living beside it (SPEC \u00A711: admin-only chrome).\n document.body.classList.toggle(\"editor-mode\", view === \"edit\");\n // A post's web version is a page of its own, in a tab of its own: the whole window, with\n // no app chrome around it. Only for a route that names a post, so a mangled address\n // falls through to the dashboard with its navigation.\n const webPost = view === \"web\" ? webPostId(arg, sub) : null;\n document.body.classList.toggle(\"web-mode\", webPost !== null);\n // The reference room (Docs, API) is about Kestrel itself, not the publication, so it\n // drops the publication sidebar for a slim tool bar.\n const toolMode = view === \"docs\" || view === \"reference\";\n document.body.classList.toggle(\"tool-mode\", toolMode);\n // Mark the active nav item across both sidebar navs (primary + tools) so the\n // reader can see where they are (aria-current also styles it).\n for (const a of document.querySelectorAll<HTMLAnchorElement>(\".sidebar a[data-view]\")) {\n if (a.dataset.view === view) {\n a.setAttribute(\"aria-current\", \"page\");\n } else {\n a.removeAttribute(\"aria-current\");\n }\n }\n // The reference room's surface switch is marked at render time (roomShell). Close\n // the mobile nav drawer on any navigation.\n setNavOpen(false);\n if (view === \"edit\" && arg) {\n return mount((root, signal) => renderEditor(arg, root, signal));\n }\n if (webPost !== null) {\n return mount((root, signal) => renderWebVersion(webPost, root, signal));\n }\n if (view === \"drafts\") {\n return mount(renderDrafts);\n }\n if (view === \"subscribers\") {\n return mount((root, signal) => renderSubscribers(arg, root, signal));\n }\n if (view === \"sent\") {\n return arg ? mount((root, signal) => renderSentRecord(arg, root, signal)) : mount(renderSent);\n }\n if (view === \"template\") {\n return mount(renderTemplate);\n }\n if (view === \"settings\") {\n return mount(renderSettings);\n }\n if (view === \"reference\") {\n return mount(renderReference);\n }\n if (view === \"docs\") {\n return mount((root, signal) => renderDocs(arg, root, signal, sub));\n }\n return mount(renderDashboard);\n}\n\n/** Adopt the route a link from an email names in its query (`/dashboard/?to=/sent/<id>`,\n * shared/dashboard_link.ts), which survives an Access login where a hash would not: it\n * becomes the hash, the query goes, and no history entry is added. Boot calls this once,\n * signed in, before the first route(). */\nexport function adoptLinkedRoute(): void {\n const linked = dashboardRouteFromSearch(location.search);\n if (linked) {\n // Edit this page's own URL, so the result is same-origin whatever its path; the link's\n // route replaces any hash, since it is the page the link was sent to open.\n const url = new URL(location.href);\n url.search = \"\";\n url.hash = linked;\n history.replaceState(null, \"\", url);\n }\n}\n\n/** Wire navigation and the mounted view's guards: hashchange, the unload prompt, and \u2318S. Boot calls this once. */\nexport function installRouter(): void {\n // hashchange fires after the hash has already moved, so the mounted view is asked\n // before route() tears it down: a dirty editor saves in the background and lets the\n // navigation through, unless its last save failed (a silent flush could lose work),\n // when it asks for a confirm() (synchronous, unlike the modal helper) and, on cancel,\n // the hash goes back, which is a hashchange to the mounted hash and so nothing to do.\n window.addEventListener(\"hashchange\", () => {\n if (location.hash === mountedHash) {\n return;\n }\n if (mounted()?.beforeLeave?.() === \"confirm\" && !confirm(LEAVE_MSG)) {\n location.hash = mountedHash;\n return;\n }\n route();\n });\n // Tab close / reload / external navigation: can't reliably finish an async save,\n // so fall back to the browser's own generic unsaved-changes prompt.\n window.addEventListener(\"beforeunload\", (e) => {\n if (mounted()?.dirty?.()) {\n e.preventDefault();\n e.returnValue = \"\";\n }\n });\n // \u2318S / Ctrl-S saves the mounted view, when it saves (registered once; no-op elsewhere).\n window.addEventListener(\"keydown\", (e) => {\n const save = mounted()?.manualSave;\n if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === \"s\" && save) {\n e.preventDefault();\n save();\n }\n });\n}\n", "// Kestrel editor \u2014 a small vanilla SPA over the same HTTP API Claude uses.\n// Auth is edge-centric: in production Cloudflare Access gates this surface, so the\n// browser's Access session cookie authenticates every same-origin call and there is\n// nothing to paste. In local dev there is no edge, so the editor mints a dev token\n// on boot (/api/dev/token) and sends it as a bearer. Either way the boot probe\n// (/api/whoami) tells us who we are and which mode we're in; the identity chip and\n// the failure handling follow from that.\n\nimport type { SettingsResponse } from \"../shared/settings\";\nimport { api } from \"./api\";\nimport { probeSession, renderIdentity } from \"./auth\";\nimport { renderSidebarBrand } from \"./brand\";\nimport { startDevReload } from \"./dev_reload\";\nimport { installRoomBar } from \"./room/shell\";\nimport { adoptLinkedRoute, installRouter, route } from \"./router\";\nimport { installShell } from \"./shell\";\nimport { appState } from \"./state\";\nimport { installTooltips } from \"./ui/widgets\";\n\nasync function boot(): Promise<unknown> {\n // Wire the app before anything asynchronous, in one visible sequence: no module does\n // anything at load but define things, so what the app does at boot is read from here.\n installShell();\n installTooltips();\n installRoomBar();\n installRouter();\n const session = await probeSession();\n if (session) {\n appState.session = session;\n document.body.classList.remove(\"signed-out\");\n renderIdentity();\n // Load the publication identity for the sidebar brand. Non-fatal: on failure the\n // brand keeps its \"Kestrel\" placeholder and routing still proceeds.\n try {\n appState.appConfig = await api<SettingsResponse>(\"/api/settings\");\n } catch {\n /* keep the placeholder brand */\n }\n renderSidebarBrand();\n // Dev build only: pick up a rebuilt bundle or an edited stylesheet without a hand\n // refresh. The production build compiles this out (see scripts/build-client.mjs).\n if (__DEV__) {\n startDevReload();\n }\n // A link from a notification email names its page in the query, which survives an\n // Access login where a hash would not.\n adoptLinkedRoute();\n return route();\n }\n // probeSession has already put up the wall it landed on.\n return undefined;\n}\nboot();\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["root", "id", "el", "app", "identity", "toasts", "navToggle", "navScrim", "setNavOpen", "open", "navCollapse", "navOverride", "computeNavMode", "applyNavMode", "mode", "expanded", "label", "navResizeRaf", "navResizeSettle", "installShell", "controller", "handle", "mount", "view", "c", "root", "app", "outcome", "e", "h", "unmount", "mounted", "every", "ms", "fn", "signal", "id", "at", "time", "cancel", "poll", "tick", "arm", "again", "isExpectedFailure", "onAbort", "$", "selector", "root", "el", "$$", "ESCAPES", "escapeHtml", "value", "c", "Html", "markup", "toMarkup", "UNQUOTED_ATTRIBUTE", "html", "strings", "values", "out", "chunk", "i", "s", "unsafeHtml", "setHtml", "el", "ACCESS_GUIDE", "accessNotConfigured", "data", "d", "showAccessLocked", "app", "unmount", "setHtml", "html", "$", "configRefusal", "data", "d", "CONFIGURATION_REFERENCE", "REFERENCE_ANCHORS", "REFUSALS_ANCHOR", "referenceUrl", "variable", "showConfigProblem", "refusal", "app", "unmount", "heading", "reference", "html", "setHtml", "$", "TOKEN_KEY", "appState", "setToken", "t", "appState", "TOKEN_KEY", "authHeaders", "renderIdentity", "session", "p", "who", "setHtml", "identity", "html", "showReauth", "app", "unmount", "$", "mintDevToken", "r", "whoami", "probeSession", "res", "data", "refusal", "configRefusal", "showConfigProblem", "accessNotConfigured", "showAccessLocked", "ApiError", "message", "status", "data", "d", "REAUTH_MESSAGE", "isBareWord", "m", "NETWORK_FAILURE", "describeError", "written", "send", "path", "opts", "headers", "authHeaders", "body", "res", "accessNotConfigured", "showAccessLocked", "showReauth", "messageOf", "failure", "text", "refusal", "configRefusal", "showConfigProblem", "fallback", "api", "apiText", "apiFile", "disposition", "filename", "fromDisplayName", "fromAddress", "header", "lt", "display", "publicationName", "name", "derivePublication", "data", "p", "publicationName", "renderSidebarBrand", "pub", "appState", "nameEl", "tagEl", "logoEl", "setHtml", "html", "archivePostUrl", "origin", "basePath", "slug", "NOT_CONNECTED_ADVICE", "STUCK_THRESHOLD_MS", "DEFAULT_MIN_LEAD_MS", "MIN_LEAD_FLOOR_MS", "MIN_LEAD_CEILING_MS", "formatLead", "ms", "seconds", "minutes", "BOUNCE_SPIKE_RECENT_MS", "buildRefParts", "b", "appState", "version", "html", "sha", "minLeadMs", "DEFAULT_MIN_LEAD_MS", "minLeadText", "formatLead", "tooSoonText", "offersSendNow", "why", "isTooSoon", "fireAt", "now", "earliestFireAt", "noEmailProvider", "providerNotConnected", "notConnectedPreview", "CONNECT_PROVIDER_HREF", "connectProviderLink", "cls", "classAttr", "NOT_CONNECTED_ADVICE", "heldAdviceHtml", "heldSendsHtml", "which", "count", "one", "notConnectedWarning", "words", "heldWhenFiresHtml", "notConnectedLine", "after", "notConnectedTestLine", "notConnectedAddLine", "notConnectedScheduleLine", "notConnectedRescheduleLine", "withNoProviderNote", "msg", "postArchiveUrl", "slug", "d", "archivePostUrl", "article_default", "bold_default", "book_default", "branch_default", "check_default", "circle_check_default", "circle_minus_default", "code_default", "copyout_default", "desktop_default", "editable_default", "heading_default", "indent_default", "info_default", "info_filled_default", "italic_default", "kestrel_default", "lines_default", "link_default", "mail_default", "ol_default", "paperclip_default", "phone_default", "plus_default", "preview_default", "quote_default", "readonly_default", "search_default", "send_default", "ul_default", "upload_default", "x_default", "ICONS", "unsafeHtml", "article_default", "bold_default", "check_default", "code_default", "copyout_default", "editable_default", "heading_default", "indent_default", "info_default", "info_filled_default", "italic_default", "kestrel_default", "lines_default", "link_default", "ol_default", "paperclip_default", "plus_default", "preview_default", "quote_default", "readonly_default", "search_default", "send_default", "ul_default", "upload_default", "book_default", "branch_default", "circle_check_default", "circle_minus_default", "desktop_default", "mail_default", "phone_default", "x_default", "icon", "name", "ICONS", "roomShell", "active", "rail", "main", "tab", "view", "label", "html", "parts", "buildRefParts", "bt", "appState", "built", "stamp", "railFoot", "foot", "body", "icon", "installRoomBar", "app", "ev", "a", "ROUTE", "DOT_SEGMENT", "isDashboardRoute", "route", "dashboardRouteFromSearch", "search", "PATTERN", "encodeSendCursor", "cursor", "decodeSendCursor", "raw", "match", "seq", "at", "earlierCursor", "a", "b", "x", "y", "stageOf", "s", "RETRY_MS", "RETRY_MAX_MS", "READ_TIMEOUT_MS", "followers", "cursor", "floor", "timer", "reading", "readAgain", "readQueued", "failures", "safely", "fn", "e", "lower", "a", "b", "earlierCursor", "arm", "ms", "read", "since", "round", "c", "cutOff", "query", "api", "res", "f", "err", "ApiError", "dropped", "leave", "queueRead", "onVisibility", "join", "signal", "followSends", "from", "follower", "seen", "changes", "send", "to", "was", "removed", "r", "id", "followSend", "change", "readSendsNow", "normalizeEmail", "email", "isValidEmail", "fmt", "ms", "parseAddresses", "text", "valid", "invalid", "part", "a", "normalizeEmail", "isValidEmail", "invalidAddressesMessage", "notSentMessage", "sent", "total", "failed", "reasons", "why", "toLocalInput", "d", "p", "untilStr", "fireAt", "due", "min", "hr", "rm", "days", "lateStr", "listQuery", "state", "p", "term", "listToolbar", "cfg", "statusSel", "html", "s", "suppressed", "failures", "wireToolbar", "root", "reload", "search", "status", "t", "th", "label", "key", "cls", "active", "arrow", "klass", "wireSort", "container", "b", "renderPager", "el", "page", "limit", "setHtml", "from", "to", "hasPrev", "hasNext", "DISABLED", "prev", "next", "TOAST_MS", "TOAST_LEAVE_MS", "MAX_TOASTS", "toastTimers", "toast", "msg", "live", "toasts", "same", "t", "lingerToast", "old", "leaveToast", "badge", "status", "html", "fieldErrorSeq", "fieldErrors", "errorLine", "message", "line", "ico", "text", "fieldError", "field", "describedBy", "cleared", "clear", "modal", "content", "signal", "back", "setHtml", "dialog", "opener", "onKey", "e", "close", "trapTab", "clearCurrent", "clearError", "error", "actions", "active", "hadFocus", "focusables", "FOCUSABLE", "root", "el", "els", "first", "last", "at", "TIP_GUTTER", "positionInfoTip", "tip", "tipW", "iconLeft", "vw", "shift", "installTooltips", "type", "infoTip", "below", "icon", "menuEls", "menuSignal", "menuAnchor", "closeMenu", "openMenu", "anchor", "items", "align", "overlay", "m", "it", "b", "name", "desc", "menuKey", "r", "i", "go", "n", "busy", "btn", "label", "fn", "orig", "renderError", "container", "retryFn", "DRAFT_STATUSES", "DRAFTS_SCOPE", "createNewPost", "btn", "busy", "post", "api", "err", "toast", "describeError", "renderDrafts", "root", "signal", "state", "setHtml", "html", "listToolbar", "newPost", "$", "listEl", "pagerEl", "shownFire", "shownSends", "latest", "reload", "load", "background", "mine", "data", "listQuery", "posts", "p", "filtered", "th", "sending", "href", "badge", "fmt", "wireSort", "tr", "$$", "e", "t", "b", "st", "target", "items", "id", "confirmDelete", "openMenu", "renderPager", "renderError", "following", "firstLoad", "follow", "first", "followSends", "sends", "changes", "removed", "c", "s", "mount", "wireToolbar", "pid", "m", "modal", "go", "clampPct", "n", "fmtDuration", "ms", "s", "m", "countdownHtml", "html", "has", "countdowns", "root", "signal", "armed", "tick", "now", "next", "watch", "moment", "el", "$$", "closes", "fire", "missed", "lateStr", "untilStr", "at", "every", "kind", "c", "conditionOf", "can", "name", "a", "sendsNow", "rows", "reported", "listed", "r", "needsOperator", "deliveredCell", "delivered", "kinds", "note", "text", "i", "activeRowHtml", "accepted", "confirmed", "total", "pct", "eta", "stuck", "addSubscriberModal", "onDone", "signal", "notConnected", "providerNotConnected", "m", "modal", "html", "notConnectedAddLine", "go", "$", "input", "busy", "addr", "normalizeEmail", "isValidEmail", "r", "api", "toast", "addedMessage", "e", "describeError", "ApiError", "action", "confirmUnsubscribe", "sub", "COPIED_MS", "copyFace", "copied", "word", "html", "icon", "copyButton", "text", "label", "codeBlock", "code", "raw", "pending", "copyWith", "button", "toast", "prior", "setHtml", "timer", "wireCopy", "root", "ev", "b", "computeHealth", "live", "alerts", "missed", "s", "has", "wedged", "c", "conditionOf", "firstWedged", "n", "sum", "w", "noun", "html", "notConnected", "providerNotConnected", "allRefused", "held", "r", "refused", "which", "sends", "heldSendsHtml", "firstRefused", "stuck", "needsOperator", "firstStuck", "spiky", "firstSpike", "connectProviderLink", "healthHtml", "level", "i", "activeOf", "renderDashboard", "view", "signal", "remount", "mount", "setHtml", "root", "$", "posts", "first", "counts", "p", "subs", "api", "e", "renderError", "describeError", "reported", "pub", "derivePublication", "appState", "deployment", "totalSubs", "setupChecklistHtml", "apiConnectCard", "wireDashActions", "tiles", "tilesHtml", "t", "drafts", "draftsHtml", "fmt", "homeUrl", "pubCardHtml", "copyButton", "claudeConnected", "apiCardHtml", "quickHtml", "sendsNow", "dashActiveHtml", "dashScheduledHtml", "scheduledOf", "dashSentHtml", "tr", "$$", "wireDashSentRows", "wireDashActiveCards", "wireDashScheduledCards", "tickCountdowns", "countdowns", "reading", "readSends", "mine", "fresh", "paintLive", "now", "patchDelivered", "inQueue", "st", "followSends", "u", "id", "stageOf", "a", "b", "recent", "url", "badge", "deliveredCell", "cell", "scheduled", "heldWhenFiresHtml", "countdownHtml", "card", "active", "activeRowHtml", "reload", "createNewPost", "addSubscriberModal", "wireCopy", "connected", "icon", "subscribeUrl", "CONNECT_PROVIDER_HREF", "copyFace", "message", "describeError", "openResolveModal", "send", "reload", "signal", "n", "m", "modal", "html", "$", "doResolve", "btn", "resolution", "verb", "busy", "res", "api", "toast", "e", "unsent", "accepted", "openRescheduleModal", "sendId", "currentFireAt", "onDone", "minStr", "toLocalInput", "earliestFireAt", "cur", "minLeadText", "notConnectedRescheduleLine", "box", "go", "when", "v", "t", "isTooSoon", "tooSoonText", "ApiError", "readSendsNow", "pagePolicy", "formAction", "opts", "d", "stayPut", "EMAIL_FRAME_SANDBOX", "openLinksInNewTab", "doc", "link", "intoHead", "markup", "head", "tag", "framedCopy", "pagePolicy", "hlTokens", "s", "m", "hlHtml", "raw", "escapeHtml", "_m", "br", "name", "sl", "hlCss", "ind", "sel", "sp", "prop", "colon", "highlightTemplate", "src", "unsafeHtml", "seg", "hlMdInline", "code", "bang", "ltext", "lurl", "bd", "btext", "aem", "uem", "d", "text", "renderMdLine", "line", "h", "q", "li", "highlightMarkdown", "inFence", "html", "band", "fence", "lang", "JSON_TOKEN", "highlightJson", "value", "out", "last", "whole", "str", "num", "lit", "highlightCurl", "AUTOSAVE_TIMERS", "createAutosave", "save", "timers", "idle", "cap", "cancel", "fire", "DirtyTracker", "read", "#saved", "sent", "pending", "trackSaves", "chain", "savesSettled", "conflictFromError", "ApiError", "d", "RevisionTracker", "base", "revision", "conflict", "answer", "at", "rev", "webVersionHash", "postId", "webPostId", "kind", "id", "shortUrl", "url", "webBar", "state", "tag", "html", "stage", "at", "normalUrl", "previewOpensWebVersion", "doc", "archiveUrl", "webUrl", "own", "link", "href", "leaveFor", "readPost", "signal", "api", "renderWebVersion", "root", "appTitle", "onAbort", "savesSettled", "data", "apiText", "postArchiveUrl", "bar", "setHtml", "EMAIL_FRAME_SANDBOX", "frame", "page", "openLinksInNewTab", "fitFrame", "h", "watch", "framedCopy", "e", "box", "msg", "ApiError", "renderError", "mount", "r", "s", "message", "describeError", "TOOLBAR", "READONLY", "html", "ARIA_DISABLED", "renderEditor", "id", "root", "signal", "remount", "mount", "r", "s", "gone", "removed", "msg", "toast", "handOff", "setHtml", "data", "sendRead", "api", "home", "sendHome", "e", "isNotFound", "renderError", "movedOn", "post", "markdown", "scheduled", "locked", "revisions", "RevisionTracker", "toolbarHtml", "group", "i", "kind", "label", "icon", "ro", "webVersionUrl", "webVersionHash", "heldWhenFiresHtml", "infoTip", "EMAIL_FRAME_SANDBOX", "notConnectedWarning", "connectProviderLink", "subjectEl", "$", "slugEl", "ta", "toolbarEl", "previewFrame", "mdHl", "mdHlCode", "syncMdScroll", "paintMarkdown", "highlightMarkdown", "autoEl", "collect", "held", "applyHeld", "refreshDirty", "clearSubjectErr", "clearSubjectError", "validateSubject", "fieldError", "reflect", "markEdited", "saveDraft", "dirty", "savedSlug", "mine", "createAutosave", "onAbort", "tabs", "$$", "showTab", "name", "t", "on", "showPreview", "apiText", "doc", "openLinksInNewTab", "previewOpensWebVersion", "postArchiveUrl", "foundGone", "lockFoot", "nudgeTimer", "nudge", "wrapSel", "before", "after", "placeholder", "sel", "prefixLines", "prefix", "start", "out", "l", "applyFormat", "b", "k", "editorDirty", "saveFailed", "conflicted", "saveError", "retryDelay", "retryArmed", "saveBtn", "saveStatus", "DirtyTracker", "saving", "renderSaveStatus", "scheduleAutosave", "saveChain", "silent", "next", "doSaveDraft", "trackSaves", "fields", "sent", "slugShown", "body", "u", "handSet", "conflict", "conflictFromError", "showConflict", "named", "ApiError", "at", "freshnessEl", "busy", "paintSaveBtn", "leaveFlush", "handle", "el", "friendlyAuthor", "a", "clearConflict", "who", "keep", "pollFreshness", "baseAtRequest", "fresh", "decision", "every", "readWhenShown", "current", "whenEl", "controls", "painted", "armed", "seenOpen", "reschedBtn", "cancelBtn", "offers", "can", "paintBanner", "now", "due", "markup", "scheduledBannerHtml", "key", "remadeEl", "paintRemade", "remade", "conditionOf", "follow", "from", "followSend", "send", "change", "poll", "read", "openRescheduleModal", "readSendsNow", "uploadAndInsert", "file", "fd", "image", "snippet", "composerBody", "wouldEdit", "f", "imgInput", "foot", "it", "warningsEl", "showWarnings", "ws", "lead", "notConnected", "providerNotConnected", "m", "modal", "notConnectedTestLine", "go", "to", "defaults", "hint", "addrs", "invalid", "parseAddresses", "notAddresses", "invalidAddressesMessage", "failed", "reasons", "lastWarnings", "addr", "notSentMessage", "withNoProviderNote", "scheduleBtn", "minStr", "toLocalInput", "earliestFireAt", "def", "scheduleView", "minLeadText", "notConnectedScheduleLine", "box", "refused", "doSchedule", "isTooSoon", "tooSoonText", "fmt", "doSendNow", "wireSchedule", "showSendNow", "n", "whoEl", "has", "lateStr", "untilStr", "hash", "docsCache", "docDescription", "doc", "tmp", "setHtml", "unsafeHtml", "raw", "leadsIn", "text", "MAX", "renderDocs", "slug", "root", "signal", "anchor", "roomShell", "html", "api", "e", "renderError", "$", "describeError", "mount", "r", "s", "landing", "docs", "renderDocPage", "decodeAnchor", "renderDocsIndex", "card", "d", "n", "desc", "sections", "sec", "inSection", "cards", "i", "blurb", "main", "repoUrl", "appState", "out", "href", "label", "short", "rail", "mobileMq", "syncFold", "fold", "mobile", "findFold", "foldFollowsLayout", "followLayout", "headingAnchor", "headingId", "at", "cur", "toast", "siblings", "pos", "prev", "next", "topLink", "dir", "topNav", "navEl", "mainEl", "seen", "h", "$$", "base", "h1", "head", "h2", "pager", "onPage", "onPageEl", "markActive", "id", "a", "ev", "target", "pre", "btn", "code", "ids", "first", "last", "line", "spy", "active", "heading", "authLines", "mode", "concretePath", "path", "curlCommand", "r", "origin", "required", "q", "search", "url", "lines", "body", "apiExample", "label", "value", "codeBlock", "highlightJson", "apiCurl", "r", "cmd", "curlCommand", "appState", "highlightCurl", "apiQueryHtml", "query", "rows", "q", "html", "apiAcceptsHtml", "accepts", "t", "apiPathHtml", "path", "concretePath", "part", "i", "apiRouteHtml", "hay", "routesIn", "g", "res", "apiResourceHtml", "sec", "routes", "apiSectionHtml", "apiNavHtml", "groups", "applyFilter", "navEl", "contentEl", "terms", "shown", "$$", "inTier", "inRes", "li", "hit", "w", "link", "tierLink", "sections", "lastShown", "$", "spyRail", "line", "marks", "el", "atBottom", "current", "tier", "links", "a", "target", "renderReference", "root", "signal", "setHtml", "roomShell", "ev", "api", "e", "renderError", "describeError", "mount", "icon", "filterEl", "clearEl", "spy", "filterTo", "wireCopy", "typing", "message", "describeError", "windowControls", "s", "buttons", "can", "html", "renderSent", "root", "signal", "state", "setHtml", "noEmailProvider", "notConnectedWarning", "connectProviderLink", "listToolbar", "stuckEl", "$", "schedEl", "activeEl", "listEl", "pagerEl", "tickCountdowns", "countdowns", "sendingRows", "liveRead", "scheduledReads", "listReads", "reported", "afterAct", "loadScheduled", "loadList", "readSendsNow", "renderActive", "live", "active", "needsOperator", "activeRowHtml", "card", "$$", "e", "renderStuck", "allRefused", "c", "conditionOf", "held", "providerNotConnected", "r", "refused", "notConnectedLine", "heldNames", "i", "heldCard", "heldSendsHtml", "wedged", "b", "w", "x", "openResolveModal", "patchDelivered", "cell", "tr", "deliveredCell", "paintLive", "sendsNow", "inQueue", "st", "sentSide", "onUpdate", "u", "id", "stageOf", "following", "follow", "reads", "ok", "followSends", "earlierCursor", "mount", "loadLive", "res", "api", "renderError", "mine", "sends", "schedCard", "countdownHtml", "fmt", "first", "rest", "toggle", "more", "show", "t", "openRescheduleModal", "busy", "toast", "data", "listQuery", "th", "wireSort", "renderPager", "wireToolbar", "message", "describeError", "renderSentRecord", "id", "root", "signal", "remount", "mount", "r", "s", "setHtml", "html", "data", "api", "e", "renderError", "send", "startWatch", "renderFrozenRecord", "PHASE_META", "PHASE_BLURB", "nextRetry", "retryAt", "wait", "fmtDuration", "phaseBlurb", "prog", "halt", "has", "phasePill", "phase", "m", "progressBar", "tone", "label", "value", "total", "sub", "p", "clampPct", "deliveryBar", "confirmed", "accepted", "acceptedPct", "confirmedPct", "WATCH_COUNTS", "watchCountsHtml", "counts", "c", "watchBodyHtml", "acceptedTotal", "rate", "eta", "dispatchSub", "failureCount", "providerText", "noEmailProvider", "providerNotConnected", "refused", "conditionOf", "unconnected", "stuck", "heldAdviceHtml", "fmt", "watchMetaHtml", "started", "watchHtml", "can", "wireWatchHeader", "rb", "count", "openResolveModal", "paintWatch", "pill", "meta", "body", "latest", "every", "followSend", "fresh", "outcomeTilesHtml", "outcomes", "tiles", "t", "outcomeReconHtml", "parts", "reconciled", "suppression", "sameOutcomes", "a", "b", "k", "deliveryOutcome", "DELIVERY_TABS", "deliveryEmpty", "dstate", "recordDeliveryQuery", "d", "term", "deliveryRowsHtml", "rows", "o", "outcome", "detail", "when", "th", "renderCanceled", "archive", "sentAt", "i", "viewBtn", "rowsEl", "$", "recPagerEl", "readerWaiting", "load", "background", "mine", "last", "keepFocus", "wireSort", "loadDeliveries", "renderPager", "paint", "had", "within", "sortKey", "pagerCls", "again", "viewButtons", "$$", "v", "x", "recSearch", "csvBtn", "busy", "csv", "apiFile", "url", "toast", "ApiError", "shown", "now", "outcomesOf", "control", "id", "el", "savebar", "msgEl", "saveBtn", "discardBtn", "token", "setMsg", "text", "setHtml", "html", "reveal", "detach", "attach", "onSave", "onDiscard", "saveLabel", "discardLabel", "signal", "mine", "busy", "alive", "dirty", "message", "describeError", "HIDDEN", "html", "chip", "kind", "label", "icon", "secHead", "title", "chipHtml", "extra", "monogram", "name", "fallback", "confirmationSection", "page", "state", "fromName", "html", "secHead", "chip", "icon", "monogram", "mountConfirmation", "root", "ceDefault", "fields", "$", "pv", "repaint", "c", "mastName", "hasLogo", "showMast", "setHtml", "onInput", "el", "editBody", "previewBody", "tabEdit", "tabPreview", "setMode", "edit", "PROVIDER_LABELS", "providerLabel", "provider", "providerValue", "d", "html", "connectProviderLink", "SIMULATION_LABELS", "bareAddress", "from", "m", "senderSection", "page", "fromName", "secHead", "chip", "icon", "monogram", "instanceSection", "archiveBase", "archiveIsDefault", "formatLead", "DEFAULT_MIN_LEAD_MS", "remakeRequired", "ApiError", "NOT_SAVED", "withRemakeConfirm", "attempt", "verb", "signal", "ack", "err", "required", "remakeRequired", "sends", "confirmRemake", "toast", "NOT_SAVED", "s", "n", "one", "noun", "resolve", "m", "modal", "html", "fmt", "settled", "done", "v", "cancel", "go", "obs", "REMAKE_TEMPLATE", "remakeIdentity", "action", "gerund", "inUseChip", "inUse", "icon", "title", "savedToast", "what", "remade", "escapeAttr", "s", "emailLogoHtml", "logoUrl", "name", "message", "describeError", "EMAIL_TEMPLATE_VARS", "EMAIL_TEMPLATE_EXAMPLES", "isExampleKey", "k", "EMAIL_TEMPLATE_SAMPLE_BODY", "RAW_TOKENS", "fillEmailTemplate", "template", "ctx", "_m", "key", "escapeHtml", "templateSampleCtx", "id", "name", "emailLogoHtml", "sampleEmailHtml", "TEMPLATE_FRAME_DOC", "mountSampleEmailPreview", "iframe", "getTemplate", "getIdentity", "EMAIL_FRAME_SANDBOX", "ready", "size", "h", "repaint", "doc", "slot", "setHtml", "unsafeHtml", "openLinksInNewTab", "templateVarsHtml", "html", "g", "v", "renderTemplate", "root", "signal", "bodyEl", "$", "data", "api", "e", "renderError", "mount", "s", "d", "p", "identity", "publicationName", "templateBaseline", "defaultRecipients", "inUseChip", "icon", "copyFace", "tplEditor", "preview", "tplMsgsEl", "tplTestEl", "tplTestLbl", "isDirty", "tplHl", "tplHlCode", "tplGutter", "paintEditor", "src", "highlightTemplate", "lines", "i", "syncScroll", "reqBodyEl", "reqUnsubEl", "paintReq", "bar", "savebar", "onSaveTemplate", "revertTemplate", "refreshDirty", "showWarnings", "msgs", "m", "saveTemplate", "value", "r", "withRemakeConfirm", "ack", "REMAKE_TEMPLATE", "appState", "saved", "toast", "savedToast", "err", "loadExample", "ex", "exBtn", "openMenu", "frameEl", "wb", "$$", "o", "editorWrap", "lineNumsBtn", "setLineNums", "on", "lineNumsOn", "copyAllBtn", "copyWith", "notConnected", "providerNotConnected", "dirty", "modal", "notConnectedTestLine", "go", "to", "hint", "busy", "addrs", "invalid", "parseAddresses", "notAddresses", "invalidAddressesMessage", "notSentMessage", "withNoProviderNote", "templateSection", "page", "html", "secHead", "chip", "inUseChip", "mountTemplate", "root", "template", "preview", "mountSampleEmailPreview", "$", "recipientsSection", "mountRecipients", "state", "chips", "input", "render", "setHtml", "addr", "i", "icon", "ev", "b", "add", "v", "normalizeEmail", "isValidEmail", "fieldError", "e", "centerSquare", "width", "height", "side", "UnreadableImage", "squareImage", "file", "size", "bitmap", "x", "y", "canvas", "ctx", "resolve", "reject", "b", "identityNote", "inUse", "shown", "reach", "wait", "fmt", "LOGO_SIZE", "identitySection", "page", "state", "fromDisplay", "fromName", "addressOpen", "html", "secHead", "chip", "HIDDEN", "icon", "mountIdentity", "root", "signal", "nameEl", "$", "taglineEl", "addressEl", "onInput", "templatePrintsAddress", "addrAdd", "addrField", "addrWarn", "applyAddressUi", "logoInput", "logoTile", "logoPh", "logoReplace", "logoRemove", "logoShape", "checkLogoShape", "url", "img", "applyLogoUi", "has", "e", "logoError", "logoErrorText", "showLogoError", "msg", "logoSaved", "r", "done", "toast", "savedToast", "logoFailed", "err", "message", "file", "squared", "squareImage", "cropped", "withRemakeConfirm", "ack", "fd", "q", "api", "remakeIdentity", "busy", "notifyChannelLabel", "channel", "provider", "providerLabel", "NOTIFICATION_KINDS", "aboutNotification", "n", "notificationStatusHtml", "status", "lastSent", "lastFailure", "html", "fmt", "icon", "notificationsSection", "page", "d", "secHead", "chip", "mountNotifications", "root", "toEl", "$", "test", "testHint", "refreshTest", "saved", "reason", "busy", "r", "api", "toast", "setHtml", "err", "message", "SUBSCRIBE", "CONFIRM", "UNSUBSCRIBE", "READER_JOURNEYS", "readerPagePath", "journey", "state", "JOURNEY_NOTES", "html", "JOURNEY_ICONS", "PEER_DOT", "outlineRow", "j", "s", "mark", "outline", "READER_JOURNEYS", "states", "steps", "icon", "picker", "step", "n", "readerPagesSection", "head", "readerPageAddress", "at", "d", "mountReaderPages", "root", "deployment", "signal", "nav", "$", "select", "widthEl", "stage", "frame", "errorEl", "journey", "state", "width", "seq", "watch", "onAbort", "paintChoice", "b", "$$", "on", "g", "setHtml", "fitHeight", "load", "mine", "doc", "apiText", "readerPagePath", "framedCopy", "e", "describeError", "choose", "journeyId", "stateId", "x", "page", "w", "o", "embedHeading", "name", "buildEmbed", "mode", "appOrigin", "heading", "escapeHtml", "action", "embedPreview", "html", "appOriginOf", "page", "pageLine", "way", "glyph", "title", "line", "body", "control", "icon", "subscribeSection", "subscribeUrl", "secHead", "chip", "connectProviderLink", "copyButton", "embedCopy", "code", "copyFace", "mountSubscribe", "root", "codeEl", "$", "copyEl", "previewEl", "lineEl", "rebuild", "setHtml", "modeButtons", "$$", "b", "o", "wireCopy", "shownName", "group", "id", "sections", "html", "baselineOf", "state", "sameList", "a", "b", "x", "i", "sameCopy", "renderSettings", "root", "signal", "setHtml", "body", "$", "data", "api", "e", "renderError", "message", "mount", "s", "p", "ce", "baseline", "fromDisplay", "fromDisplayName", "isDirty", "handles", "bar", "savebar", "saveSettings", "discardSettings", "refreshReaderPages", "page", "h", "settings", "appState", "renderSidebarBrand", "identitySection", "subscribeSection", "confirmationSection", "readerPagesSection", "secHead", "chip", "templateSection", "recipientsSection", "notificationsSection", "senderSection", "instanceSection", "mountIdentity", "mountSubscribe", "mountConfirmation", "mountTemplate", "mountRecipients", "mountNotifications", "mountReaderPages", "clearFieldMark", "unmarkField", "payload", "r", "withRemakeConfirm", "ack", "remakeIdentity", "ns", "toast", "savedToast", "err", "input", "ApiError", "fieldInput", "fieldError", "FIELD_INPUTS", "field", "sel", "el", "SUB_STATUSES", "renderSubscribers", "initialFilter", "root", "signal", "state", "setHtml", "html", "listToolbar", "listEl", "$", "pagerEl", "load", "data", "api", "listQuery", "c", "infoTip", "renderSubTable", "renderPager", "e", "renderError", "describeError", "addSubscriberModal", "wireToolbar", "SUPPRESSION_LABELS", "SUPPRESSION_TIPS", "suppressionFlag", "s", "reason", "label", "base", "tip", "rows", "reload", "th", "badge", "fmt", "wireSort", "b", "$$", "row", "r", "openMenu", "confirmUnsubscribe", "LEAVE_MSG", "mountedHash", "route", "hash", "view", "arg", "sub", "isDashboardRoute", "webPost", "webPostId", "toolMode", "a", "setNavOpen", "mount", "root", "signal", "renderEditor", "renderWebVersion", "renderDrafts", "renderSubscribers", "renderSentRecord", "renderSent", "renderTemplate", "renderSettings", "renderReference", "renderDocs", "renderDashboard", "adoptLinkedRoute", "linked", "dashboardRouteFromSearch", "url", "installRouter", "mounted", "save", "boot", "installShell", "installTooltips", "installRoomBar", "installRouter", "session", "probeSession", "appState", "renderIdentity", "api", "renderSidebarBrand", "adoptLinkedRoute", "route"]
|
|
4
|
+
"sourcesContent": ["// The app shell: the DOM roots every view renders into, and the sidebar's drawer and\n// collapse modes, which installShell() wires when boot calls it.\n\n/** A root the page always carries (public/dashboard/index.html); missing means the wrong page. */\nfunction root(id: string): HTMLElement {\n const el = document.getElementById(id);\n if (!el) {\n throw new Error(`the admin page has no #${id}`);\n }\n return el;\n}\n\nexport const app = root(\"app\");\nexport const identity = root(\"identity\");\nexport const toasts = root(\"toasts\");\n\n// Mobile nav drawer: the hamburger slides the sidebar in; the scrim or any nav\n// click closes it. On desktop the sidebar is always in view and these are inert.\nconst navToggle = document.getElementById(\"navToggle\");\nconst navScrim = document.getElementById(\"navScrim\");\nexport function setNavOpen(open: boolean): void {\n document.body.classList.toggle(\"nav-open\", open);\n navToggle?.setAttribute(\"aria-expanded\", open ? \"true\" : \"false\");\n if (navScrim) {\n navScrim.hidden = !open;\n }\n}\n// Collapsible sidebar. The effective width mode lives in data-nav on <html> (\"full\"\n// vs \"rail\"); CSS keys off it. By default the mode follows the viewport width (the\n// <head> guard in index.html sets it before first paint). The footer chevron toggles a\n// manual override, but that override is deliberately session-only \u2014 held in memory, not\n// stored \u2014 so a plain reload always drops back to the width-based default. That gives a\n// one-keystroke way back to \"auto\" and avoids a saved preference getting stuck fighting\n// the width across sizes. The chevron is hidden below 540, where the sidebar is an\n// off-canvas overlay driven by the hamburger instead.\nconst navCollapse = document.getElementById(\"navCollapse\");\n// null = follow the width; \"collapsed\" / \"expanded\" = manual override for this load.\nlet navOverride: \"collapsed\" | \"expanded\" | null = null;\nfunction computeNavMode(): \"full\" | \"rail\" {\n if (window.innerWidth < 540) {\n return \"full\";\n }\n if (navOverride === \"collapsed\") {\n return \"rail\";\n }\n if (navOverride === \"expanded\") {\n return \"full\";\n }\n return window.innerWidth <= 1024 ? \"rail\" : \"full\";\n}\nfunction applyNavMode(): void {\n const mode = computeNavMode();\n document.documentElement.dataset.nav = mode;\n if (!navCollapse) {\n return;\n }\n const expanded = mode !== \"rail\";\n const label = expanded ? \"Collapse sidebar\" : \"Expand sidebar\";\n navCollapse.setAttribute(\"aria-expanded\", expanded ? \"true\" : \"false\");\n navCollapse.setAttribute(\"aria-label\", label);\n navCollapse.setAttribute(\"title\", label);\n}\nlet navResizeRaf = 0;\nlet navResizeSettle = 0;\n\n/** Wire the drawer, the collapse chevron, and the resize tracking; boot calls this once. */\nexport function installShell(): void {\n navToggle?.addEventListener(\"click\", () =>\n setNavOpen(!document.body.classList.contains(\"nav-open\")),\n );\n navScrim?.addEventListener(\"click\", () => setNavOpen(false));\n document.querySelector(\".sidebar\")?.addEventListener(\"click\", (e) => {\n if (e.target instanceof Element && e.target.closest(\"a\")) {\n setNavOpen(false);\n }\n });\n navCollapse?.addEventListener(\"click\", () => {\n navOverride = document.documentElement.dataset.nav !== \"rail\" ? \"collapsed\" : \"expanded\";\n applyNavMode();\n });\n // Track the responsive default as the window resizes (rAF-coalesced). A session\n // override still wins at 540 and up; below it, the mobile overlay takes over. The\n // nav-resizing class suppresses the sidebar's own transitions for the duration, so a\n // breakpoint cross (rail \u2192 mobile drawer) snaps instead of animating a stray slide.\n window.addEventListener(\"resize\", () => {\n document.body.classList.add(\"nav-resizing\");\n clearTimeout(navResizeSettle);\n navResizeSettle = setTimeout(() => {\n document.body.classList.remove(\"nav-resizing\");\n }, 200);\n if (navResizeRaf) {\n return;\n }\n navResizeRaf = requestAnimationFrame(() => {\n navResizeRaf = 0;\n applyNavMode();\n });\n });\n applyNavMode();\n}\n", "// The view lifecycle: one view is mounted at a time, into its own root under #app, with\n// an AbortSignal that ends with it. Everything a view starts that could outlive it, a\n// timer, a poll, a read that paints, a bar it borrowed, is bound to that signal, so\n// navigating away tears the view down by construction, and the view never has to know\n// whether it is still the one on screen.\n\nimport { app } from \"./shell\";\n\n/**\n * What a mounted view offers the app around it. Every member is optional: a list view\n * offers nothing; the editor offers all three.\n */\nexport interface ViewHandle {\n /** Unsaved work the browser should warn about before the tab unloads. */\n dirty?(): boolean;\n /**\n * Asked once before a navigation away. The view may save in the background here and\n * answer \"leave\"; \"confirm\" has the router ask the reader first, and a declined prompt\n * keeps the view mounted.\n */\n beforeLeave?(): \"leave\" | \"confirm\";\n /** \u2318S / Ctrl-S. */\n manualSave?(): void;\n}\n\n// What a view hands back: a handle, or nothing (the async ones resolve to nothing, so\n// this is `void`, not `undefined`, and the union is the honest type of that).\n// biome-ignore lint/suspicious/noConfusingVoidType: see above\ntype Offered = ViewHandle | void;\n\n/** A view: paints into `root`, binds what it starts to `signal`, and may return a handle. */\nexport type View = (root: HTMLElement, signal: AbortSignal) => Offered | Promise<Offered>;\n\nlet controller: AbortController | null = null;\nlet handle: ViewHandle | null = null;\n\n/**\n * Tear the mounted view down and mount `view` in its place. The previous view's signal\n * aborts first, then a fresh root replaces its markup, so a stale continuation (a read\n * that resolved after the abort) paints into a detached element and nobody sees it. A\n * view re-enters itself the same way (a reload, a retry): the same abort, a fresh root.\n */\nexport function mount(view: View): Promise<void> {\n controller?.abort();\n handle = null;\n const c = new AbortController();\n controller = c;\n const root = document.createElement(\"div\");\n root.className = \"view\";\n app.replaceChildren(root);\n // The view starts now, not in a microtask: a second mount in the same tick must find\n // it started (its signal listeners in place), or the abort would reach a view that\n // never ran and a promise that never settles.\n let outcome: Promise<Offered>;\n try {\n outcome = Promise.resolve(view(root, c.signal));\n } catch (e) {\n outcome = Promise.reject(e);\n }\n return outcome.then(\n (h) => {\n if (!c.signal.aborted && h) {\n handle = h;\n }\n },\n (e) => {\n // A view cut off mid-load is not an error; anything else is the view's own bug.\n if (!c.signal.aborted) {\n throw e;\n }\n },\n );\n}\n\n/** Tear the mounted view down and leave #app empty (the re-auth wall paints its own). */\nexport function unmount(): void {\n controller?.abort();\n controller = null;\n handle = null;\n app.replaceChildren();\n}\n\n/** The mounted view's handle, for the router's guards; null while nothing offers one. */\nexport function mounted(): ViewHandle | null {\n return handle;\n}\n\n/** Run `fn` every `ms` until the signal aborts. */\nexport function every(ms: number, fn: () => void, signal: AbortSignal): void {\n if (signal.aborted) {\n return;\n }\n const id = setInterval(fn, ms);\n signal.addEventListener(\"abort\", () => clearInterval(id), { once: true });\n}\n\n/**\n * Run `fn` once at the wall-clock moment `time`, unless the signal aborts first. For a\n * change that belongs to an exact moment (a review window closing at the fire time), which a\n * once-a-second tick would reach up to a second late. Only for moments close at hand: a\n * timer's delay tops out near 25 days, past which it would fire at once.\n */\nexport function at(time: number, fn: () => void, signal: AbortSignal): void {\n if (signal.aborted) {\n return;\n }\n const cancel = () => clearTimeout(id);\n const id = setTimeout(\n () => {\n signal.removeEventListener(\"abort\", cancel);\n fn();\n },\n Math.max(0, time - Date.now()),\n );\n signal.addEventListener(\"abort\", cancel, { once: true });\n}\n\n// A tick's answer: `false` ends the poll; anything else (nothing, for a tick that always\n// continues) keeps it going.\n// biome-ignore lint/suspicious/noConfusingVoidType: see above\ntype Again = boolean | void;\n\n/**\n * A repeating read that never overlaps itself: `tick` runs `ms` after the previous one\n * finished, until the signal aborts or a tick answers `false`. A tick's own reads take\n * the signal, so one in flight is cut off by navigation; a tick that throws yields to the\n * next. A cut-off read and a refused one (the API answered non-2xx) are the expected\n * failures and stay quiet; anything else is a bug in the tick and is reported, so a\n * broken paint cannot hide inside a poll that keeps retrying.\n */\nexport function poll(ms: number, tick: () => Promise<Again>, signal: AbortSignal): void {\n const arm = () => {\n if (signal.aborted) {\n return;\n }\n const cancel = () => clearTimeout(id);\n const id = setTimeout(async () => {\n signal.removeEventListener(\"abort\", cancel);\n let again: boolean | undefined = true;\n try {\n again = (await tick()) ?? true;\n } catch (e) {\n if (!isExpectedFailure(e)) {\n reportError(e);\n }\n }\n if (again !== false) {\n arm();\n }\n }, ms);\n signal.addEventListener(\"abort\", cancel, { once: true });\n };\n arm();\n}\n\n/** Run `fn` when the signal aborts (at once if it already has). */\nexport function onAbort(signal: AbortSignal, fn: () => void): void {\n if (signal.aborted) {\n fn();\n return;\n }\n signal.addEventListener(\"abort\", fn, { once: true });\n}\n\n// A read cut off by the signal (the DOMException fetch rejects with), or one the API\n// refused (client/api.ts's ApiError, matched by name: importing it here would put\n// lifecycle on a cycle through api and auth).\nfunction isExpectedFailure(e: unknown): boolean {\n return e instanceof Error && (e.name === \"AbortError\" || e.name === \"ApiError\");\n}\n", "// Querying markup a module itself rendered: the ids and classes are the module's own, so\n// an element that is missing is a bug in its template, not a state to handle. These fail\n// loud, naming the selector, instead of returning null for every call site to re-check.\n\n/** The one element a selector matches under `root`, or a loud failure naming it. */\nexport function $<T extends Element = HTMLElement>(\n selector: string,\n root: ParentNode = document,\n): T {\n const el = root.querySelector<T>(selector);\n if (!el) {\n throw new Error(`no element matches ${selector}`);\n }\n return el;\n}\n\n/** Every element a selector matches under `root`, as an array. */\nexport function $$<T extends Element = HTMLElement>(\n selector: string,\n root: ParentNode = document,\n): T[] {\n return [...root.querySelectorAll<T>(selector)];\n}\n", "// Markup as a value. html`\u2026` builds it and escapes every interpolation that is not already\n// markup, so the safe path is the default: a subject, an email, a template a publisher typed\n// can never break out of the element it is shown in. The unsafe path has a name to grep for.\n\nconst ESCAPES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\n/** Escape text for an HTML text node or a quoted attribute value. */\nexport function escapeHtml(value: string | number): string {\n return String(value).replace(/[&<>\"']/g, (c) => ESCAPES[c] ?? c);\n}\n\n/**\n * Markup that is safe to put in the document. Only `html`, `unsafeHtml`, and what they\n * build produce one: the declared private field makes the type nominal, so a look-alike\n * object cannot pass as markup at compile time, and `instanceof` is what the runtime\n * checks, which a look-alike cannot pass either. An object rather than a branded string so\n * nesting can tell markup from text and never escape it twice. Frozen, so what was built is\n * what is set.\n */\nexport class Html {\n private declare readonly brand: undefined;\n readonly markup: string;\n constructor(markup: string) {\n this.markup = markup;\n Object.freeze(this);\n }\n toString(): string {\n return this.markup;\n }\n}\n\n/**\n * What an interpolation may be. Arrays flatten; `null`, `undefined`, and `false` render\n * nothing, so `${cond && html\\`\u2026\\`}` reads plainly. `true` is not admitted: a bare boolean\n * in markup is a bug, not a value.\n */\nexport type Interpolation = Html | string | number | false | null | undefined | Interpolation[];\n\nfunction toMarkup(value: Interpolation): string {\n if (value instanceof Html) {\n return value.markup;\n }\n if (value == null || value === false) {\n return \"\";\n }\n if (Array.isArray(value)) {\n return value.map(toMarkup).join(\"\");\n }\n return escapeHtml(value);\n}\n\n// A literal chunk that ends in `=` (optionally followed by whitespace) puts the next\n// interpolation in an unquoted attribute value, where nothing escaping can do makes it safe.\nconst UNQUOTED_ATTRIBUTE = /=\\s*$/;\n\n/**\n * Build markup. Escaping makes exactly two contexts safe: text, and a quoted attribute\n * value whose meaning is text. It does nothing for a value that is code or a URL: not\n * `href`, `src`, `action`, `formaction` (a `javascript:` URL has no character to escape;\n * check the scheme first), not `srcdoc`, `style`, or an `on*` handler, and never inside\n * `<script>` or `<style>`, an attribute name, or a tag name. An interpolation in an\n * unquoted attribute value throws. A literal with an invalid escape sequence (`\\u`, `\\x`,\n * `\\1`) throws rather than vanish: that is what the cooked string would do.\n */\nexport function html(strings: TemplateStringsArray, ...values: Interpolation[]): Html {\n let out = chunk(strings, 0);\n for (let i = 0; i < values.length; i++) {\n const value = values[i];\n if (!(value instanceof Html) && UNQUOTED_ATTRIBUTE.test(out)) {\n throw new Error(\n \"html: an interpolation in an unquoted attribute value cannot be made safe; quote it\",\n );\n }\n out += toMarkup(value) + chunk(strings, i + 1);\n }\n return new Html(out);\n}\n\nfunction chunk(strings: TemplateStringsArray, i: number): string {\n const s = strings[i];\n if (s === undefined) {\n throw new Error(\n `html: invalid escape sequence in template text near ${JSON.stringify(strings.raw[i])}`,\n );\n }\n return s;\n}\n\n/**\n * Markup the tag did not build: server-rendered HTML the API returns, or markup a module\n * still assembles by hand. The caller vouches for it. Grep for this name when auditing.\n */\nexport function unsafeHtml(markup: string): Html {\n return new Html(markup);\n}\n\n/**\n * Put markup in an element: the one place strings reach innerHTML. The runtime check\n * holds the line where the type checker cannot see (a spec's cast, a value that crossed\n * an `unknown`): a string here is a bug, and it fails loud rather than render the word\n * \"undefined\".\n */\nexport function setHtml(el: Element, markup: Html): void {\n if (!(markup instanceof Html)) {\n throw new Error(\n \"setHtml: expected Html; build it with html`\u2026` or vouch for it with unsafeHtml()\",\n );\n }\n el.innerHTML = markup.markup;\n}\n", "// The wall a deployment with no Access lands on (SPEC \u00A711): until Access is set up, no\n// request can authenticate, and the API's 401 says so, so the editor shows that state, full\n// page, with the guide that sets Access up, instead of a session that never existed.\n\nimport { unmount } from \"./lifecycle\";\nimport { app } from \"./shell\";\nimport { $ } from \"./ui/dom\";\nimport { html, setHtml } from \"./ui/html\";\n\n/**\n * The published Access guide. The editor's own Docs tab is read from the API, which is\n * refusing every call until Access is set up, so the wall links to the project's site.\n */\nexport const ACCESS_GUIDE = \"https://getkestrel.dev/docs/access/\";\n\n/** Whether a 401's body says no Access is configured, rather than this request failing it. */\nexport function accessNotConfigured(data: unknown): boolean {\n if (!data || typeof data !== \"object\") {\n return false;\n }\n const d = data as { error?: unknown; reason?: unknown };\n return d.error === \"unauthorized\" && d.reason === \"access_not_configured\";\n}\n\n/**\n * Replace the app with the explanation: the editor is locked until Access is set up, why,\n * and the guide that does it. The mounted view is torn down first, so nothing keeps calling\n * an API that can only refuse, and the publication chrome hides, as on the sign-in wall.\n * Every call in flight fails the same way, so a wall already up is left as it is.\n */\nexport function showAccessLocked(): void {\n if (app.querySelector(\".access-wall\")) {\n return;\n }\n unmount();\n document.body.classList.add(\"signed-out\");\n setHtml(\n app,\n html`<div class=\"card auth-wall access-wall\"><h2 tabindex=\"-1\">The editor is locked until you set up Access</h2><p>Until Cloudflare Access is in front of the editor, the app can\u2019t tell who is asking, so it lets no one in. On a new deployment, that\u2019s expected. Your public pages work as usual.</p><p class=\"hint\"><a href=\"${ACCESS_GUIDE}\" target=\"_blank\" rel=\"noopener\">Lock the dashboard with Access <span aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\"> (opens in a new tab)</span></a></p><button id=\"access-reload\" class=\"primary\">Reload</button></div>`,\n );\n $(\"#access-reload\", app).onclick = () => location.reload();\n $<HTMLElement>(\".access-wall h2\", app).focus();\n}\n", "// The wall a refused deployment lands on (SPEC \u00A79): while the Worker refuses its\n// configuration, every API call answers `invalid_config` naming the variable, and while its\n// database is behind the release, `schema_behind` naming the migrations and the command that\n// applies them, so the editor shows that once, full page, with the fix, instead of failing\n// call by call.\n\nimport { unmount } from \"./lifecycle\";\nimport { app } from \"./shell\";\nimport { $ } from \"./ui/dom\";\nimport { type Html, html, setHtml } from \"./ui/html\";\n\n/** What the API says of a refused deployment: its words about it, and the variable to fix\n * when it is a setting. A database behind the release names no variable: its words name the\n * migrations and the command. */\nexport interface ConfigRefusal {\n variable?: string;\n message: string;\n}\n\n/** The refusal an error body carries, or null for any other body. */\nexport function configRefusal(data: unknown): ConfigRefusal | null {\n if (!data || typeof data !== \"object\") {\n return null;\n }\n const d = data as { error?: unknown; variable?: unknown; message?: unknown };\n if (d.error === \"schema_behind\" && typeof d.message === \"string\") {\n return { message: d.message };\n }\n if (d.error !== \"invalid_config\" || typeof d.variable !== \"string\") {\n return null;\n }\n return {\n variable: d.variable,\n message: typeof d.message === \"string\" ? d.message : `${d.variable} is refused`,\n };\n}\n\n/**\n * The published Configuration reference. The editor's own Docs tab is read from the API,\n * which is refusing every call until the configuration is fixed, so the wall links to the\n * project's site instead.\n */\nexport const CONFIGURATION_REFERENCE = \"https://getkestrel.dev/docs/configuration/\";\n\n/** Each variable's heading in the Configuration reference, by its GitHub-style anchor; a\n * secret's is the Secrets table. */\nexport const REFERENCE_ANCHORS: Readonly<Record<string, string>> = {\n PROVIDER: \"provider\",\n ALLOW_FAKE_PROVIDER: \"provider\",\n APP_ORIGIN: \"vars\",\n AWS_REGION: \"vars\",\n FROM_ADDRESS: \"from_address-and-sending_domain\",\n SENDING_DOMAIN: \"from_address-and-sending_domain\",\n ARCHIVE_BASE_PATH: \"archive_base_path\",\n SES_MAX_SEND_RATE: \"ses_max_send_rate\",\n SUBREQUEST_BUDGET: \"subrequest_budget\",\n MIN_LEAD_SECONDS: \"min_lead_seconds\",\n ARCHIVE_ORIGIN: \"archive_origin-and-media_public_base\",\n MEDIA_PUBLIC_BASE: \"archive_origin-and-media_public_base\",\n NOTIFY_FROM: \"notify_from\",\n ACCESS_TEAM_DOMAIN: \"secrets\",\n ACCESS_AUD: \"secrets\",\n ACCESS_ALLOWED_EMAILS: \"secrets\",\n AWS_ACCESS_KEY_ID: \"secrets\",\n AWS_SECRET_ACCESS_KEY: \"secrets\",\n SNS_TOPIC_ARN: \"secrets\",\n SES_CONFIGURATION_SET: \"secrets\",\n SIMULATE_SENDS: \"what-the-app-refuses\",\n};\n\n/** The section of the reference listing everything the app refuses, for a variable with no\n * heading of its own. */\nconst REFUSALS_ANCHOR = \"what-the-app-refuses\";\n\n/** The reference's address for one variable, at its own heading when it has one. */\nexport function referenceUrl(variable: string): string {\n return `${CONFIGURATION_REFERENCE}#${REFERENCE_ANCHORS[variable] ?? REFUSALS_ANCHOR}`;\n}\n\n/**\n * Replace the app with the explanation: the server's words about what's wrong, which name\n * the variable, and the reference, which says how to set it wherever the editor runs.\n * The how-to lives in the reference, not here, so it can't drift from it. The mounted view is torn down first, so nothing keeps calling\n * an API that can only refuse, and the publication chrome hides, as on the sign-in wall.\n * Every call in flight fails the same way, so a wall already up is left as it is.\n */\nexport function showConfigProblem(refusal: ConfigRefusal): void {\n if (app.querySelector(\".config-wall\")) {\n return;\n }\n unmount();\n document.body.classList.add(\"signed-out\");\n const { variable } = refusal;\n // A setting links to its reference; a database behind the release has its command in the\n // server's words already, and the reference has nothing to add.\n const heading = variable ? \"A setting needs fixing\" : \"The database needs updating\";\n const reference: Html | string = variable\n ? html`<p class=\"hint\"><a href=\"${referenceUrl(variable)}\" target=\"_blank\" rel=\"noopener\">${variable} in the Configuration reference <span aria-hidden=\"true\">\u2197</span><span class=\"visually-hidden\"> (opens in a new tab)</span></a></p>`\n : \"\";\n setHtml(\n app,\n html`<div class=\"card auth-wall config-wall\" role=\"alert\"><h2 tabindex=\"-1\">${heading}</h2><p>Kestrel won\u2019t run until it\u2019s fixed, and nothing is sent in the meantime.</p><p class=\"config-wall-problem\">${refusal.message}</p>${reference}<button id=\"config-reload\" class=\"primary\">Reload</button></div>`,\n );\n $(\"#config-reload\", app).onclick = () => location.reload();\n $<HTMLElement>(\".config-wall h2\", app).focus();\n}\n", "// The app-wide state: the three values every view reads and boot writes, in one object\n// (`appState`) so each cross-module write is visible here. What a view owns lives in the\n// view, for as long as its mount (client/lifecycle.ts).\n\nimport type { SettingsResponse } from \"../shared/settings\";\n\n/** The boot probe's answer (GET /api/whoami): who we are and which auth mode gates this surface. */\nexport interface Session {\n principal: { kind: \"human\" | \"service\"; email?: string };\n auth: { mode: \"access\" | \"dev\" };\n}\n\nexport interface AppState {\n /** The dev token (local dev only; deployed envs authenticate at the edge). */\n token: string;\n /** Set once booted. */\n session: Session | null;\n /**\n * The last GET /api/settings payload, fetched at boot so the sidebar brand and the\n * dashboard's publication and setup cards can read the origins and the From-address\n * fallback without re-fetching on every render.\n */\n appConfig: SettingsResponse | null;\n}\n\n/** localStorage key of the dev token. */\nexport const TOKEN_KEY = \"kestrel_token\";\n\nexport const appState: AppState = {\n token: localStorage.getItem(TOKEN_KEY) || \"\",\n session: null,\n appConfig: null,\n};\n", "// Auth on the client: the boot probe, the dev token, the bearer header, the identity chip,\n// and the re-auth wall a dead session lands on.\n\nimport { accessNotConfigured, showAccessLocked } from \"./access_locked\";\nimport { configRefusal, showConfigProblem } from \"./config_problem\";\nimport { unmount } from \"./lifecycle\";\nimport { app, identity } from \"./shell\";\nimport { appState, type Session, TOKEN_KEY } from \"./state\";\nimport { $ } from \"./ui/dom\";\nimport { html, setHtml } from \"./ui/html\";\n\n/** Remember the dev token; an empty one forgets it, so the next boot mints instead of probing with a dead value. */\nexport function setToken(t: string | null | undefined): void {\n appState.token = (t || \"\").trim();\n try {\n // Clearing (empty token) removes the key rather than storing \"\", so the next\n // boot takes the \"no token \u2192 mint\" path instead of probing with a dead value.\n if (appState.token) {\n localStorage.setItem(TOKEN_KEY, appState.token);\n } else {\n localStorage.removeItem(TOKEN_KEY);\n }\n } catch {\n /* private mode */\n }\n}\n\n/**\n * The dev token goes in Authorization; in Access mode there is no token and the session\n * cookie authenticates instead, so no header is sent.\n */\nexport function authHeaders(): Record<string, string> {\n return appState.token ? { Authorization: `Bearer ${appState.token}` } : {};\n}\n\n/**\n * Render the sidebar identity chip from the session: who is signed in under Access, with\n * the sign-out link, or the local-dev marker when there is no edge.\n */\nexport function renderIdentity(): void {\n const session = appState.session;\n if (session?.auth.mode === \"access\") {\n const p = session.principal;\n const who = p.email || (p.kind === \"service\" ? \"Service token\" : \"Signed in\");\n setHtml(\n identity,\n html`<span class=\"who\" title=\"${who}\">${who}</span><a class=\"ghost\" href=\"/cdn-cgi/access/logout\" title=\"Sign out\"><svg class=\"signout-icon\" aria-hidden=\"true\"><use href=\"#i-signout\"/></svg><span class=\"signout-label\">Sign out</span></a>`,\n );\n } else {\n setHtml(\n identity,\n html`<span class=\"who dev\" title=\"Local dev: sign-in is skipped on localhost\">Local dev</span>`,\n );\n }\n}\n\n/**\n * Access sessions expire at the edge (the request never reaches the app), so the only\n * recovery is a fresh document load that re-triggers the Access login. In dev this\n * shouldn't happen, but a reload re-mints, so the same affordance is safe.\n */\nexport function showReauth(): void {\n // Every call in flight fails the same way, so a wall already up is left as it is: drawn\n // again, it would take focus back from wherever the reader had moved it.\n if (app.querySelector(\".reauth-wall\")) {\n return;\n }\n // A dead token means every background poll (and a pending editor autosave) now 401s \u2014\n // which is what routed us here. Tear the view down so a walled tab goes quiet instead of\n // re-hitting the API on its timers until reload.\n unmount();\n // No identity yet \u2014 hide the publication chrome so the wall stands alone.\n document.body.classList.add(\"signed-out\");\n setHtml(\n app,\n html`<div class=\"card auth-wall reauth-wall\"><h2 tabindex=\"-1\">Session expired</h2><p class=\"hint\">Your access session ended. Sign in again to continue.</p><button id=\"reauth\" class=\"primary\">Sign in</button></div>`,\n );\n $(\"#reauth\", app).onclick = () => location.reload();\n // Focus starts on the heading, as on the other walls, so a screen reader begins there.\n $<HTMLElement>(\".reauth-wall h2\", app).focus();\n}\n\n// Mint a dev token into localStorage. Returns false when deployed, where the endpoint 404s\n// (or is unreachable) and the Access cookie authenticates instead.\nasync function mintDevToken(): Promise<boolean> {\n try {\n const r = await fetch(\"/api/dev/token?kind=human\");\n if (r.ok) {\n setToken(((await r.json()) as { token: string }).token);\n return true;\n }\n } catch {\n /* deployed: the endpoint is absent; the Access cookie authenticates instead */\n }\n return false;\n}\n\n// Probe identity. A network error or an opaque Access redirect can't be recovered here, so\n// it surfaces as a null result (the sign-in wall).\nasync function whoami(): Promise<Response | null> {\n try {\n return await fetch(\"/api/whoami\", { headers: authHeaders(), redirect: \"manual\" });\n } catch {\n return null;\n }\n}\n\n/**\n * The boot probe: who is signed in, minting a dev token first in local development. Answers\n * the session, or null once it has put up the wall the probe landed on: the refused\n * configuration's when the server names a setting to fix (SPEC \u00A79), the locked editor's when\n * the deployment has no Access yet (SPEC \u00A711), since signing in again cannot help with\n * either, and the sign-in wall otherwise.\n */\nexport async function probeSession(): Promise<Session | null> {\n if (!appState.token) {\n await mintDevToken();\n }\n let res = await whoami();\n // Stale stored token in dev: clear it, mint a fresh one, and probe again so a leftover\n // credential self-heals. In Access mode the re-mint fails and `res` stays as it was.\n if (!res?.ok && appState.token) {\n setToken(\"\");\n if (await mintDevToken()) {\n res = await whoami();\n }\n }\n if (res?.ok) {\n return (await res.json()) as Session;\n }\n const data: unknown = res ? await res.json().catch(() => null) : null;\n const refusal = configRefusal(data);\n if (refusal) {\n showConfigProblem(refusal);\n } else if (accessNotConfigured(data)) {\n showAccessLocked();\n } else {\n showReauth();\n }\n return null;\n}\n", "// The one network seam: every call to the HTTP API goes through api(), apiText(), or\n// apiFile(), which attach the credential, route a 401 to re-auth (or to the locked editor,\n// before Access is set up) and a refused deploy configuration to its wall, and turn any\n// other failure into one ApiError carrying the server's message. They call the global\n// fetch, so a spec stands in for the API by scripting fetch and the real functions run\n// over it.\n\nimport type { ApiErrorBody } from \"../shared/errors\";\nimport { accessNotConfigured, showAccessLocked } from \"./access_locked\";\nimport { authHeaders, showReauth } from \"./auth\";\nimport { configRefusal, showConfigProblem } from \"./config_problem\";\n\n/** What a request may carry. `json` sets the body and its content type; `body` is sent as is. */\nexport interface ApiOptions {\n method?: string;\n headers?: Record<string, string>;\n body?: BodyInit | null;\n json?: unknown;\n /**\n * The mounted view's signal, for a read that paints: navigating away cuts the request\n * off, and the view is spared a stale answer. Never for a write (a save the reader\n * started must land whether or not they stayed to watch).\n */\n signal?: AbortSignal;\n}\n\n/**\n * A non-2xx answer. Callers branch on `status` (the editor's conflict handling on 409)\n * and read the parsed body, when there was one, from `data`.\n */\nexport class ApiError extends Error {\n constructor(\n message: string,\n readonly status: number,\n readonly data: unknown = null,\n ) {\n super(message);\n this.name = \"ApiError\";\n }\n\n /** The API's error code (`error` on the body), such as `cursor_ahead`, or null when the\n * body carried none. */\n get code(): string | null {\n const d = this.data as Partial<ApiErrorBody> | null;\n return typeof d?.error === \"string\" ? d.error : null;\n }\n\n /** The request field a refusal is about (`field` on the body), dotted for a nested one,\n * or null when the refusal names none. */\n get field(): string | null {\n const d = this.data as Partial<ApiErrorBody> | null;\n return typeof d?.field === \"string\" ? d.field : null;\n }\n}\n\nconst REAUTH_MESSAGE = \"Not authorized. Sign in again.\";\n\n// A server message written as a bare code (\"internal_error\") or a bare noun (\"post\"), with\n// no space in it, is a word for a program, not a person.\nconst isBareWord = (m: string) => /^[a-z_]+$/i.test(m.trim());\n\n// What fetch rejects with when no answer came: Chrome, Firefox, and Safari's words.\nconst NETWORK_FAILURE =\n /^(Failed to fetch|NetworkError when attempting to fetch resource\\.?|Load failed|The network connection was lost\\.?|The Internet connection appears to be offline\\.?)$/;\n\n/**\n * Whether a failure is a request that never got an answer: offline, a dropped connection,\n * the server unreachable. fetch rejects with a TypeError then, whose text differs by browser,\n * so only those exact texts count; a TypeError from a bug in the page is not one.\n */\nexport function isNetworkFailure(e: unknown): boolean {\n return e instanceof TypeError && NETWORK_FAILURE.test(e.message);\n}\n\n/**\n * What to tell the publisher about a failure, in their words: the server's own message when\n * it wrote one for people, otherwise a plain sentence for what happened. A dropped\n * connection, a server error with no message, and a 404 naming only what it couldn't find\n * would otherwise reach the page as \"Failed to fetch\", \"internal_error\", or \"post\".\n */\nexport function describeError(e: unknown): string {\n if (e instanceof ApiError) {\n // Only the server's own message counts as written for people: an ApiError built from\n // a body that isn't the API's (a proxy's page, an empty body) carries the HTTP status\n // text, \"Bad Gateway\", which is no better than a code.\n const written = (e.data as { message?: unknown } | null)?.message;\n if (typeof written === \"string\" && !isBareWord(written)) {\n return written;\n }\n if (e.status === 401) {\n return e.message; // the sign-in wall is already up; this is its one line\n }\n if (e.status === 404) {\n return \"It isn\u2019t there any more. It may have been deleted elsewhere.\";\n }\n if (e.status >= 500) {\n return \"Something went wrong on the server. Try again in a moment.\";\n }\n if (e.status === 429) {\n return \"Too many requests at once. Try again in a moment.\";\n }\n return \"The server couldn\u2019t do that. Reload the page and try again.\";\n }\n // A request that never got an answer; any other error (a bug) keeps its own words.\n if (isNetworkFailure(e)) {\n return \"Couldn\u2019t reach Kestrel. Check your connection and try again.\";\n }\n if (e instanceof Error) {\n return e.message;\n }\n return String(e);\n}\n\nasync function send(path: string, opts: ApiOptions): Promise<Response> {\n const headers: Record<string, string> = { ...authHeaders(), ...(opts.headers ?? {}) };\n let body = opts.body;\n if (opts.json !== undefined) {\n headers[\"content-type\"] = \"application/json\";\n body = JSON.stringify(opts.json);\n }\n const res = await fetch(path, {\n method: opts.method ?? \"GET\",\n headers,\n body,\n signal: opts.signal,\n });\n if (res.status === 401) {\n // A deployment with no Access can authenticate no one, which signing in again can't\n // fix, so the 401 says so and the editor shows that instead (SPEC \u00A711).\n const data: unknown = await res.json().catch(() => null);\n if (accessNotConfigured(data)) {\n showAccessLocked();\n } else {\n showReauth();\n }\n throw new ApiError(REAUTH_MESSAGE, 401, data);\n }\n return res;\n}\n\n/** The message an error body carries, when it carries one. */\nfunction messageOf(data: unknown): string | undefined {\n if (data && typeof data === \"object\") {\n const d = data as { message?: unknown; error?: unknown };\n if (typeof d.message === \"string\") {\n return d.message;\n }\n if (typeof d.error === \"string\") {\n return d.error;\n }\n }\n return undefined;\n}\n\n/**\n * The ApiError a non-2xx response stands for, the same from every reader: the server's\n * words from its JSON error body, or, for a body that is not one (a proxy's error page, an\n * empty body), the status text, and the status itself when even that is empty, as it\n * always is over HTTP/2.\n */\nasync function failure(res: Response): Promise<ApiError> {\n const text = await res.text();\n let data: unknown = null;\n try {\n data = text ? JSON.parse(text) : null;\n } catch {\n // Not JSON, so not the API's error body: fall back to the status below.\n }\n // A refused deploy configuration fails every call alike, so it is shown once, full page,\n // rather than as one more failed read.\n const refusal = configRefusal(data);\n if (refusal) {\n showConfigProblem(refusal);\n }\n const fallback = res.statusText || `Request failed (HTTP ${res.status})`;\n return new ApiError(messageOf(data) ?? fallback, res.status, data);\n}\n\n/** Call the API and parse its JSON answer (null for an empty body). Throws ApiError on a non-2xx. */\nexport async function api<T = unknown>(path: string, opts: ApiOptions = {}): Promise<T> {\n const res = await send(path, opts);\n if (!res.ok) {\n throw await failure(res);\n }\n const text = await res.text();\n return (text ? JSON.parse(text) : null) as T;\n}\n\n/**\n * Like api(), but the raw response text, for the endpoints that answer with HTML (the\n * rendered preview). Same 401 guard, which a bare fetch(authHeaders()) would skip.\n */\nexport async function apiText(path: string, opts: ApiOptions = {}): Promise<string> {\n const res = await send(path, opts);\n if (!res.ok) {\n throw await failure(res);\n }\n return res.text();\n}\n\n/** A file the API serves for download: its text, and the name the server gives it in\n * `Content-Disposition`, if any, so a saved file is named as the server names it. */\nexport async function apiFile(\n path: string,\n opts: ApiOptions = {},\n): Promise<{ text: string; filename: string | null }> {\n const res = await send(path, opts);\n if (!res.ok) {\n throw await failure(res);\n }\n const disposition = res.headers.get(\"content-disposition\") ?? \"\";\n const filename = /filename=\"([^\"]+)\"/.exec(disposition)?.[1] ?? null;\n return { text: await res.text(), filename };\n}\n", "// The publication's display name, as every surface shows it: the email, the reader pages,\n// the confirmation email, and the editor's previews of them (SPEC \u00A79). One function, so\n// no two surfaces can name the same publication differently.\n\n/** The display name of a `From:` header: `Birds Weekly <hello@birds.example>` and\n * `\"Birds Weekly\" <hello@birds.example>` give `Birds Weekly`; a bare address, or an\n * address in angle brackets alone, gives \"\". A quoted name's escapes are undone. */\nexport function fromDisplayName(fromAddress: string): string {\n const header = fromAddress.trim();\n const lt = header.lastIndexOf(\"<\");\n if (lt < 0 || !header.endsWith(\">\")) {\n return \"\";\n }\n const display = header.slice(0, lt).trim();\n if (display.length >= 2 && display.startsWith('\"') && display.endsWith('\"')) {\n return display.slice(1, -1).replace(/\\\\(.)/g, \"$1\").trim();\n }\n return display;\n}\n\n/** The name a publication goes by: the one the publisher set, else the sender's display\n * name, else \"\" (never a made-up name, so a surface with none words itself without one). */\nexport function publicationName(name: string, fromAddress: string): string {\n return name || fromDisplayName(fromAddress);\n}\n", "// Publication identity for the sidebar brand: name, tagline, logo, and the From-name\n// fallback.\n\nimport { publicationName } from \"../shared/publication\";\nimport type { SettingsResponse } from \"../shared/settings\";\nimport { appState } from \"./state\";\nimport { html, setHtml } from \"./ui/html\";\n\nexport interface Publication {\n name: string;\n tagline: string;\n logoUrl: string | null;\n}\n\n/**\n * The publication's name / tagline / logo come from the settings surface\n * (settings.publication). Each field falls back sensibly when unset: the name as the\n * email resolves it, and with none a placeholder only the publisher sees, and a neutral\n * initial tile for the logo.\n */\nexport function derivePublication(data: SettingsResponse | null): Publication {\n const p = data?.settings.publication;\n return {\n name: publicationName(p?.name || \"\", data?.deployment.fromAddress ?? \"\") || \"Your publication\",\n tagline: p?.tagline || \"\",\n logoUrl: p?.logoUrl || null,\n };\n}\n\nexport function renderSidebarBrand(): void {\n const pub = derivePublication(appState.appConfig);\n const nameEl = document.getElementById(\"brandName\");\n const tagEl = document.getElementById(\"brandTagline\");\n const logoEl = document.getElementById(\"brandLogo\");\n if (nameEl) {\n nameEl.textContent = pub.name;\n }\n if (tagEl) {\n tagEl.textContent = pub.tagline;\n tagEl.hidden = !pub.tagline;\n }\n if (logoEl) {\n if (pub.logoUrl) {\n setHtml(logoEl, html`<img src=\"${pub.logoUrl}\" alt=\"\">`);\n logoEl.classList.remove(\"brand-logo-placeholder\");\n } else {\n // Neutral placeholder tile: the publication's initial on the accent.\n logoEl.textContent = (pub.name.trim()[0] || \"K\").toUpperCase();\n logoEl.classList.add(\"brand-logo-placeholder\");\n }\n }\n}\n", "// The one spelling of a post's public archive URL, shared by the Worker (emails, the\n// reader surface, the API) and the editor, so the two can never disagree (SPEC \u00A75).\n\n/** The canonical archive URL of a post: `<origin><basePath>/<slug>`. */\nexport function archivePostUrl(origin: string, basePath: string, slug: string): string {\n return `${origin}${basePath}/${slug}`;\n}\n", "// Sends as the API carries them (SPEC \u00A76, \u00A78, \u00A712): one `SendView` of a send, the same on\n// every route (the list, the send, the feed, and every action's answer), the feed the pages\n// follow, and the per-recipient record; beside them, the stored send row the Worker builds\n// the view from, which never goes on the wire as it is.\n// The Worker's routes produce these shapes and the editor consumes them; one definition,\n// so neither can drift.\n\nimport type { PageMeta } from \"./list\";\n\nexport type SendStatus = \"scheduled\" | \"sending\" | \"sent\" | \"canceled\";\n\n/**\n * Why a provider refused a whole batch (SPEC \u00A712): it is `unavailable` (an outage or a\n * rate limit, which clears on its own), or it refuses the `account` (a bad key, an\n * unverified domain, a paused account), which needs the operator.\n */\nexport type HaltReason = \"unavailable\" | \"account\";\n\n/**\n * What a refusal is about, so the advice can name the fix: the access key or its permissions,\n * a deployment setting naming something the provider can't find (an SES configuration set),\n * the sender (an unverified domain or from-address), a spent sending quota, the account\n * paused or suspended by the provider, a rate limit, the provider itself failing, or no\n * provider connected to the deployment at all (SPEC \u00A79).\n */\nexport type HaltCause =\n | \"credentials\"\n | \"configuration\"\n | \"sender\"\n | \"quota\"\n | \"suspended\"\n | \"rate_limit\"\n | \"outage\"\n | \"not_connected\";\n\n/**\n * The advice for a send held for want of a provider (SPEC \u00A79), in its parts: the setup step,\n * which a page makes the link every not-connected line carries, and what follows it. The\n * guide's mention stands in for that link where there is none (an email, the API's words).\n * \"Deploy\" is the step's own last one.\n */\nexport const NOT_CONNECTED_ADVICE = {\n step: \"Connect Amazon SES\",\n guide: \" (the setup guide walks through it)\",\n after: \", then deploy.\",\n} as const;\n\n/**\n * What to do about the provider refusing the account, by what the refusal is about\n * (SPEC \u00A712). The fix is always outside the app: nothing in it can change a credential or\n * the provider's view of the account (SPEC \u00A79). One wording for the watch and the notification\n * that tells the publisher, so the two never advise differently. Plain text: a page that\n * links the not-connected step builds it from `NOT_CONNECTED_ADVICE`.\n */\nexport function refusalAdvice(cause: HaltCause | null): string {\n switch (cause) {\n case \"credentials\":\n return \"Replace the SES access key in the deployment\u2019s secrets, or give it ses:SendEmail and ses:SendBulkEmail in its IAM policy.\";\n case \"configuration\":\n return \"Check that the SES_CONFIGURATION_SET secret names a configuration set in your SES region, or create the set in the SES console.\";\n case \"sender\":\n return \"Verify the sending domain or from-address with the provider.\";\n case \"quota\":\n return \"Wait for SES\u2019s 24-hour quota to free up, or ask AWS to raise it in the Service Quotas console.\";\n case \"suspended\":\n return \"Settle the account\u2019s standing with the provider (for SES, in the SES console).\";\n case \"not_connected\":\n return `${NOT_CONNECTED_ADVICE.step}${NOT_CONNECTED_ADVICE.guide}${NOT_CONNECTED_ADVICE.after}`;\n default:\n return \"Fix the account with the provider.\";\n }\n}\n\n/** The provider's standing refusal of a send, while one lasts. */\nexport interface SendHalt {\n reason: HaltReason;\n cause: HaltCause | null;\n error: string;\n /** How many runs in a row this refusal has halted. */\n retries: number;\n /** When refusals for this reason began; null only if the record lacks it. */\n since: number | null;\n /** When the next retry is due. The sweep leaves the send alone until then; null only if the record lacks it. */\n retry_at: number | null;\n}\n\n/** The eight denormalized progress counters on a send. */\nexport interface SendCounts {\n pending: number;\n in_flight: number;\n accepted: number;\n delivered: number;\n bounced: number;\n complained: number;\n skipped: number;\n unsent: number;\n}\n\n/**\n * A send as stored: its frozen render, its facts, and the counters (the `c_*` columns). The\n * Worker's own row type; the API carries `SendView`, built from it, never this.\n */\nexport interface Send {\n id: string;\n post_id: string;\n status: SendStatus;\n fire_at: number;\n rendered_html: string;\n rendered_text: string;\n subject: string;\n recipient_count: number;\n locked_until: number | null;\n scheduled_at: number;\n started_at: number | null;\n completed_at: number | null;\n /** When the send's first run fixed its audience (SPEC \u00A76); null until it fires. From then on `recipient_count` is the audience at fire. */\n audience_resolved_at: number | null;\n /** When a template or identity change last re-made the frozen render while the send was scheduled; null if never. */\n remade_at: number | null;\n /** When a test of the frozen copy was last sent while the send was scheduled; null if never. */\n tested_at: number | null;\n /** Why the provider refused this send's last batch as a whole (SPEC \u00A712), or null once a batch is answered: `unavailable` retries on its own, `account` needs the operator. */\n halt_reason: HaltReason | null;\n /** What that refusal is about, for the advice shown with it. */\n halt_cause: HaltCause | null;\n /** The provider's own words for that refusal. */\n halt_error: string | null;\n /** When refusals for this reason began. */\n halted_at: number | null;\n /** How many runs in a row this refusal has halted; 0 while the send is not halted. */\n halt_retries: number;\n /** When the sweep next retries the halted send (SPEC \u00A712); null while it is not halted. */\n halt_retry_at: number | null;\n c_pending: number;\n c_in_flight: number;\n c_accepted: number;\n c_delivered: number;\n c_bounced: number;\n c_complained: number;\n c_skipped: number;\n c_unsent: number;\n /**\n * Where the send's last visible change sits in one app-wide sequence (SPEC \u00A78): any\n * change a reader could see (a cancel, a move, a re-make, a counter move, a receipt)\n * gives the send a higher `rev` than every change before it, across all sends. A lease\n * renewal alone does not move it.\n */\n rev: number;\n}\n\n/** A stored send without the large frozen bodies: what a view is built from. */\nexport type SendSummary = Omit<Send, \"rendered_html\" | \"rendered_text\">;\n\n/**\n * How long a send may stay `sending` before it is flagged as in flight too long (SPEC\n * \u00A712): long enough that an ordinary outage's retries never alarm, short enough to be\n * told the same afternoon. The server decides the flag; the editor only words it.\n */\nexport const STUCK_THRESHOLD_MS = 30 * 60 * 1000;\n\n/**\n * The minimum lead a deployment runs with when it sets none (SPEC \u00A76): every send spends\n * at least this long visible and cancelable before it fires (I6). The value in force is the\n * deployment's (`DeploymentView.minLeadMs`); this is only its default.\n */\nexport const DEFAULT_MIN_LEAD_MS = 5 * 60 * 1000;\n\n/**\n * The least minimum lead any deployment may set: one sweep tick. The sweep runs once a\n * minute, so a shorter lead would still be kept, but the tick, not the lead, would decide\n * when a send fires, a precision the system doesn't have (SPEC \u00A76).\n */\nexport const MIN_LEAD_FLOOR_MS = 60 * 1000;\n\n/**\n * The most minimum lead any deployment may set: one day. The lead bounds only the least\n * wait before a send, never how far out one may be scheduled, so a longer one holds back\n * every Send now and is almost certainly milliseconds typed as seconds (SPEC \u00A76).\n */\nexport const MIN_LEAD_CEILING_MS = 24 * 60 * 60 * 1000;\n\n/** A minimum lead in words, \"5 minutes\" or \"90 seconds\", so the server's refusal and the\n * editor's copy name it the same way. */\nexport function formatLead(ms: number): string {\n const seconds = Math.round(ms / 1000);\n if (seconds % 60 === 0) {\n const minutes = seconds / 60;\n return `${minutes} minute${minutes === 1 ? \"\" : \"s\"}`;\n }\n return `${seconds} seconds`;\n}\n\n/**\n * Why a requested fire time is refused for being inside the minimum lead (SPEC \u00A76), the one\n * wording for the server's `fire_at_too_soon` and the editor's own check before it asks, on\n * scheduling and rescheduling alike: the lead is what keeps time to cancel.\n */\nexport function tooSoonMessage(minLeadMs: number): string {\n return `Pick a time at least ${formatLead(minLeadMs)} from now, so there\u2019s time to cancel.`;\n}\n\n/** GET /api/sends */\nexport interface SendListResponse {\n sends: SendView[];\n page: PageMeta;\n /**\n * Where this read stands among the changes to sends, to hand back as `since`: `<seq>.<at>`,\n * the change sequence at the read and the server's time of it, both decimal. Two compare\n * by those numbers (`shared/cursor.ts`).\n */\n cursor: string;\n}\n\n/**\n * The live reporting phase, derived from the counters, never stored: waiting in the\n * review window; due (the fire time has passed and the next sweep starts it); handing\n * recipients to the provider (with or without retries); paused between sweep ticks;\n * wedged and awaiting Resolve; dispatched with receipts still arriving; every accepted\n * recipient confirmed; or canceled.\n */\nexport type SendPhase =\n | \"scheduled\"\n | \"due\"\n | \"progressing\"\n | \"retrying\"\n | \"backing-off\"\n | \"needs-attention\"\n | \"settling\"\n | \"complete\"\n | \"canceled\";\n\n/**\n * One send, as every route carries it (SPEC \u00A78): `GET /api/sends` rows, `GET /api/sends/:id`, the\n * feed, and every action's answer, so a client keeps one shape current from any of them and\n * tells two apart by `rev`. The stored facts, less the frozen bodies (at their own route,\n * `links.email_html` and `links.email_text`), with what the server derives from them at\n * `as_of`: the phase, the conditions, the actions, and the next change.\n */\nexport interface SendView {\n id: string;\n post_id: string;\n subject: string;\n /** The stored lifecycle state (SPEC \u00A72); `phase` is the finer reading of it. */\n status: SendStatus;\n /** Where the send's last visible change sits in the change sequence: of two views of one send, the higher `rev` is the newer. */\n rev: number;\n /** The server's clock when the view was read: what the clock derives (phase, conditions, actions, `next_change_at`) is as of then. */\n as_of: number;\n fire_at: number;\n scheduled_at: number;\n started_at: number | null;\n completed_at: number | null;\n /** When a template or identity change last re-made the scheduled email; null if never. */\n remade_at: number | null;\n /** When the scheduled email was last tested; null if never. */\n tested_at: number | null;\n /**\n * Who the send goes to: `count` is an estimate while the send is scheduled (the audience\n * is resolved when it fires, SPEC \u00A76), and the audience at fire once `fixed`, from\n * `fixed_at` on, never growing.\n */\n audience: { count: number; fixed: boolean; fixed_at: number | null };\n /** Every recipient in exactly one bucket, summing to the audience once it is fixed. */\n counts: SendCounts;\n /** Provider hand-off: how far the send loop has gotten. */\n dispatch: {\n /** Recipients the loop has finished with (accepted, unsent, or skipped). */\n done: number;\n percent: number;\n /** Recipients handed off a minute: what a sweep tick carries, averaged over the ticks run so far (null when not sending, or before the first tick has finished). */\n rate_per_min: number | null;\n /** Rough time to finish dispatch, counted in sweep ticks: to the end of the last tick the rest needs, the wait between ticks included. Only while it is handing off (null otherwise, never while paused, nor before the first tick has finished). */\n eta_ms: number | null;\n };\n /** Delivery confirmation, which lags acceptance. */\n delivery: { confirmed: number; percent_of_accepted: number };\n /** The provider, and its standing refusal of this send while one lasts. */\n provider: { name: string; halt: SendHalt | null };\n phase: SendPhase;\n /** What is wrong with the send, or worth knowing, now (SPEC \u00A78, \u00A712), each with its own words and the action that settles it, if any. */\n conditions: SendCondition[];\n /** What the server would accept on the send right now. */\n actions: SendAction[];\n /**\n * The earliest moment the send can change with no one acting on it, by the server's\n * clock: at or before `as_of` while it can move at any moment (due, or sending with work\n * in hand), a later time while it waits on the clock or the sweep (its fire time, a halt's\n * next retry, the in-flight-too-long threshold), or null when only an action or a receipt\n * can change it (wedged, missed, finished).\n */\n next_change_at: number | null;\n /** Where the send's other resources are. `archive` is the published post, absolute, once the send has started; null before. */\n links: {\n self: string;\n email_html: string;\n email_text: string;\n deliveries: string;\n deliveries_csv: string;\n post: string;\n archive: string | null;\n };\n}\n\n/** Something a person can do to a send through the API, exactly as the server would take it now. */\nexport interface SendAction {\n name: \"cancel\" | \"reschedule\" | \"resolve\";\n method: \"POST\";\n /** The route, with the send's id filled in. */\n path: string;\n}\n\n/** What a condition asks of a person: `action` needs one (red), `warn` is worth a look (amber), `info` is worth knowing. */\nexport type ConditionSeverity = \"action\" | \"warn\" | \"info\";\n\n/** What every condition carries. */\ninterface ConditionBase {\n severity: ConditionSeverity;\n /** When the condition began, or null when the record does not say. */\n since: number | null;\n /** The server's words for it, whole sentences, the same wherever it is shown. */\n message: string;\n /** The action that settles it, if a person can take one through the API. */\n action: SendAction | null;\n}\n\n/**\n * One open condition on a send (SPEC \u00A78, \u00A712), derived by the server from the record, the\n * same on every route:\n *\n * - `missed`: still scheduled past the missed tolerance; the sweep that fires it has not run.\n * - `stuck`: still sending past the stuck threshold after it started.\n * - `wedged`: recipients whose delivery is unknown; `count` of them, settled by Resolve.\n * - `refused`: the provider refuses the account; its `cause`, `error`, the `advice` for the\n * fix (outside the app), and `retry_at`. No action: the send resumes on its own.\n * - `provider_unavailable`: the provider is down or rate-limiting; retried at `retry_at`.\n * - `bounce_spike`: a recently sent send's confirmed bounces reached the danger zone.\n * - `remade`: a template or identity change re-made the scheduled email after its last test.\n * - `daily_writes`: on Workers Free's limits, a scheduled send to more recipients than D1's\n * daily writes comfortably cover; `recipients` is its audience as scheduled.\n */\nexport type SendCondition =\n | (ConditionBase & { kind: \"missed\" })\n | (ConditionBase & { kind: \"stuck\" })\n | (ConditionBase & { kind: \"wedged\"; count: number })\n | (ConditionBase & {\n kind: \"refused\";\n cause: HaltCause | null;\n error: string;\n advice: string;\n retry_at: number | null;\n })\n | (ConditionBase & { kind: \"provider_unavailable\"; error: string; retry_at: number | null })\n | (ConditionBase & { kind: \"bounce_spike\"; bounced: number; rate: number })\n | (ConditionBase & { kind: \"remade\"; tested_at: number | null })\n | (ConditionBase & { kind: \"daily_writes\"; recipients: number });\n\n/** The kinds of condition. */\nexport type ConditionKind = SendCondition[\"kind\"];\n\n/** A condition in the feed's roll-up: which send it is on, and the condition. */\nexport type FeedCondition = SendCondition & { send_id: string; subject: string };\n\n/**\n * The bounce spike (SPEC \u00A78): a send's confirmed bounces over its audience at fire reaching\n * the provider's danger zone. SES puts a sender under review at a 5% bounce rate; the small\n * absolute floor keeps a tiny audience's noisy rate (one bad address among a handful) from\n * tripping it; and only a send finished within the recent window is read, since an old\n * send's bounces are history, not a risk to act on. Well above the dev simulation's ~2%.\n */\nexport const BOUNCE_SPIKE_RATE = 0.05;\nexport const BOUNCE_SPIKE_MIN = 3;\nexport const BOUNCE_SPIKE_RECENT_MS = 7 * 24 * 60 * 60 * 1000;\n\n/**\n * On the Workers Free plan's limits, a scheduled send to more than this many recipients may\n * spend D1's daily allowance of written rows (about 14 a recipient, before anything else\n * writes that day), which stops the instance until midnight UTC. Past it, Workers Paid is\n * recommended. The warning never refuses: the account may be on Paid while the Worker is\n * configured for Free.\n */\nexport const DAILY_WRITES_SEND_RECIPIENTS = 5_000;\n\n/** The `daily_writes` condition's words, for a send to `recipients`. */\nexport function dailyWritesSendWarning(recipients: number): string {\n return `On the Workers Free plan\u2019s limits, a send to ${recipients.toLocaleString(\"en-US\")} recipients may use up the database\u2019s daily allowance of writes. That stops this newsletter until midnight UTC, the send included. Workers Paid is recommended for a list of more than ${DAILY_WRITES_SEND_RECIPIENTS.toLocaleString(\"en-US\")}.`;\n}\n\n/** GET /api/sends/feed: what a client follows to keep up with sends without polling each one. */\nexport interface SendFeedResponse {\n /** The server's clock at the read, so a client times its next read by the server's clock, not its own. */\n now: number;\n /**\n * With `since`, every send that changed after that cursor, whatever its state, those the\n * clock changed with no write included (turned due, missed, in flight too long, or wedged\n * as a lease ran out); without it, every send that can change on its own. Soonest fire first.\n */\n sends: SendView[];\n /** With `since`, every send removed after that cursor (deleted with its post), in sequence order; empty without it. */\n removed: RemovedSend[];\n /** Where this read stands (`<seq>.<at>`, as on `GET /api/sends`), to hand back as `since` on the next. */\n cursor: string;\n /** Whether the read stopped at `limit` changes with more after `cursor`: read again at once from it. */\n more: boolean;\n /** Every open condition across the sends that can have one (scheduled, sending, and recently sent), whether or not they changed: every open problem in one read, most severe first. */\n conditions: FeedCondition[];\n /**\n * When to read again, by the server's clock: soon while a send can move, about once a\n * minute while none can, and never later than just past the next change the clock or the\n * sweep will make. Advisory: reading sooner (right after acting) is always fine.\n */\n read_again_at: number;\n}\n\n/** A send removed since a cursor: which, and where the removal sits in the change sequence. */\nexport interface RemovedSend {\n id: string;\n rev: number;\n}\n\n/** The sent record's per-recipient delivery breakdown (SPEC \u00A78). */\nexport interface DeliveryOutcomes {\n recipients: number;\n delivered: number;\n bounced: number;\n complained: number;\n /** Never accepted by the provider (transport-level; does not itself suppress). */\n unsent: number;\n /** Excluded at send time (unsubscribed or suppressed after the audience froze). */\n skipped: number;\n /** Accepted by the provider, with no delivery event yet (a provider may emit none). */\n accepted: number;\n in_flight: number;\n}\n\n/** GET /api/sends/:id: the send, and its record's outcome breakdown. */\nexport interface SendResponse {\n send: SendView;\n outcomes: DeliveryOutcomes;\n /** Where this read stands among the changes to sends (`<seq>.<at>`), to follow the send from with `GET /api/sends/feed`. */\n cursor: string;\n}\n\n/** The buckets GET /api/sends/:id/deliveries filters by, exactly as the outcomes count them, plus \"all\" and \"failures\" (bounced, complained, or unsent). */\nexport type DeliveryView =\n | \"all\"\n | \"failures\"\n | \"delivered\"\n | \"bounced\"\n | \"complained\"\n | \"unsent\"\n | \"skipped\"\n | \"accepted\"\n | \"in_flight\";\n\n/**\n * One recipient's row for the in-app record: the send-loop status and the later webhook\n * event, the provider detail or error, and for a bounce the frozen hard/soft kind (null\n * when the kind is unknown; rendered as a plain \"Bounce\").\n */\nexport interface DeliveryRecord {\n email: string;\n status: string;\n event: string | null;\n event_detail: string | null;\n event_at: number | null;\n error: string | null;\n attempts: number;\n bounce_kind: string | null;\n}\n\n/** GET /api/sends/:id/deliveries */\nexport interface DeliveryListResponse {\n deliveries: DeliveryRecord[];\n view: DeliveryView;\n page: PageMeta;\n}\n\n/** The publisher's answer for a wedged send's ambiguous recipients (SPEC \u00A712). */\nexport type StuckResolution = \"unsent\" | \"accepted\";\n\n/** POST /api/sends/:id/resolve */\nexport interface ResolveResponse {\n send: SendView;\n /** Where the answer stands among the changes to sends, to follow the send from. */\n cursor: string;\n /** How many ambiguous recipients this action adjudicated. */\n resolved: number;\n /** Whether the send reached its completion gate and finished. */\n completed: boolean;\n}\n\n/** POST /api/sends/:id/cancel and /reschedule */\nexport interface SendActionResponse {\n send: SendView;\n /** Where the answer stands among the changes to sends, to follow the send from. */\n cursor: string;\n /** False when the send already stood as asked (canceled already, or already at that time): nothing was written. */\n changed: boolean;\n}\n\n/**\n * POST /api/posts/:id/schedule and /send: the send that was frozen (SPEC \u00A76). A second\n * send-now for a post already in its window answers with that send and `idempotent`.\n */\nexport interface ScheduleResponse {\n send: SendView;\n /** Where the answer stands among the changes to sends, to follow the send from. */\n cursor: string;\n idempotent?: boolean;\n}\n", "// Reads of the deployment reflection (appState.appConfig.deployment; SPEC \u00A79), fetched once\n// at boot: the build reference, the archive URL, the minimum lead, and the no-provider notes.\n\nimport { archivePostUrl } from \"../shared/archive_url\";\nimport {\n DEFAULT_MIN_LEAD_MS,\n formatLead,\n NOT_CONNECTED_ADVICE,\n tooSoonMessage,\n} from \"../shared/sends\";\nimport { appState } from \"./state\";\nimport { type Html, html } from \"./ui/html\";\n\n/** The build reference as two link fragments: the version (to its release) and the sha (to its commit). */\nexport interface BuildRefParts {\n version: Html;\n sha: Html;\n}\n\n/**\n * { version, sha } from the read-only deployment reflection (appConfig.deployment.build,\n * resolved at build in src/build.ts): version \u2192 its release (only when this build IS that\n * tagged release), sha \u2192 its commit, both on the repo and never getkestrel.dev. Each\n * degrades to plain text (no dead link) when there's no repo, the build isn't a release,\n * or the sha is \"dev\" (a local build). null until a build is known. Build metadata is\n * quiet, secondary info: roomShell pins it to the rail's bottom-left corner (and, on\n * mobile, sets it as the room's foot); it's also at GET /api/version, where a bug report\n * is filed. Never in the publisher-facing dashboard.\n */\nexport function buildRefParts(): BuildRefParts | null {\n const b = appState.appConfig?.deployment.build;\n if (!b?.version) {\n return null;\n }\n const version = b.tagUrl\n ? html`<a class=\"build-link\" href=\"${b.tagUrl}\" target=\"_blank\" rel=\"noopener\">v${b.version}</a>`\n : html`v${b.version}`;\n const sha = b.commitUrl\n ? html`<a class=\"build-link\" href=\"${b.commitUrl}\" target=\"_blank\" rel=\"noopener\">${b.sha}</a>`\n : html`${b.sha}`;\n return { version, sha };\n}\n\n/**\n * The deployment's minimum lead (SPEC \u00A76), the least time between a request to send and the\n * send firing, which the server enforces on every schedule, send now, and reschedule. The\n * default until the reflection has loaded: the editor never states a lead of its own.\n */\nexport function minLeadMs(): number {\n return appState.appConfig?.deployment.minLeadMs ?? DEFAULT_MIN_LEAD_MS;\n}\n\n/** The minimum lead in words, \"5 minutes\", for the editor's hints and toasts. */\nexport function minLeadText(): string {\n return formatLead(minLeadMs());\n}\n\n/**\n * Why a picked time is refused for being too soon, checked before asking the server: the\n * server's own `fire_at_too_soon` words for this deployment's lead (SPEC \u00A76), so the editor's\n * check and the server's refusal read the same.\n */\nexport function tooSoonText(): string {\n return tooSoonMessage(minLeadMs());\n}\n\n/**\n * When a Send now goes out, in words: one lead from the request, then up to the next whole\n * minute, since every fire time is on the minute (SPEC \u00A76). \"1 to 2 minutes\" at the floor.\n */\nexport function sendNowWhenText(): string {\n const lead = minLeadMs();\n if (lead % 60_000 === 0) {\n const minutes = lead / 60_000;\n return `${minutes} to ${minutes + 1} minutes`;\n }\n return `${formatLead(lead)} to ${formatLead(lead + 60_000)}`;\n}\n\n/** Whether a picked time is inside the minimum lead, checked before asking the server. */\nexport function isTooSoon(fireAt: number, now = Date.now()): boolean {\n return fireAt < now + minLeadMs();\n}\n\n/**\n * The earliest time a send-time picker offers: one lead out, plus the minute the picker\n * can't show (it reads to the minute) and the moment the publisher spends choosing, so the\n * time it proposes is still outside the lead when it reaches the server.\n */\nexport function earliestFireAt(now = Date.now()): Date {\n return new Date(now + minLeadMs() + 60_000);\n}\n\n/**\n * True when the deployment sends through the `fake` stand-in, which records a send but\n * never delivers: local development, the tests, or a hosted demo, each of which names it on\n * purpose (SPEC \u00A79). This is the developer's \"nothing is delivered\" voice. A deployment\n * partway through setup is the other state, not connected (`providerNotConnected`). Read\n * from the read-only deployment reflection fetched once at boot; the copy avoids the\n * internal \"fake transport\" term (docs/DESIGN.md \u00A74).\n */\nexport function noEmailProvider(): boolean {\n return appState.appConfig?.deployment.provider === \"fake\";\n}\n\n/**\n * True when no email provider is connected (SPEC \u00A79): a deployment whose `PROVIDER` is not\n * set yet, which refuses every send. The operator's voice: name the state and point to the\n * step that ends it, labelled \"Connect Amazon SES\" (`connectProviderLink`).\n */\nexport function providerNotConnected(): boolean {\n return appState.appConfig?.deployment.provider === \"none\";\n}\n\n/**\n * True on the Workers Free plan's limits, as the reflection says, where a large send may spend\n * D1's daily writes (SPEC \u00A78).\n */\nexport function freePlanLimits(): boolean {\n return appState.appConfig?.deployment.freePlanLimits === true;\n}\n\n/**\n * True when this is local development's preview of the not-connected state\n * (`SIMULATE_SENDS=not-connected`, SPEC \u00A710) rather than a deployment partway through setup,\n * as the reflection says. Its way out is a restart, not a deploy.\n */\nexport function notConnectedPreview(): boolean {\n return providerNotConnected() && appState.appConfig?.deployment.notConnectedPreview === true;\n}\n\n/** The setup step that connects a provider: Get started's Connect Amazon SES. */\nexport const CONNECT_PROVIDER_HREF = \"#/docs/ses\";\n\n/** The one link every not-connected line carries, worded for any provider. */\nexport function connectProviderLink(cls?: string): Html {\n const classAttr = cls ? html` class=\"${cls}\"` : null;\n return html`<a${classAttr} href=\"${CONNECT_PROVIDER_HREF}\">${NOT_CONNECTED_ADVICE.step}</a>`;\n}\n\n/**\n * What to do about a send held for want of a provider (SPEC \u00A79), as a page says it: the\n * shared advice (`refusalAdvice`) with its step as the link every not-connected line\n * carries, in place of the guide's mention. In the local preview there is nothing to\n * deploy, so it names the restart that ends the state instead.\n */\nexport function heldAdviceHtml(): Html {\n return notConnectedPreview()\n ? html`Restart <code>npm run dev</code> without <code>SIMULATE_SENDS=not-connected</code> to send.`\n : html`${connectProviderLink()}${NOT_CONNECTED_ADVICE.after}`;\n}\n\n/**\n * The one line for sends held while no provider is connected (DESIGN \u00A74): the state, the\n * held sends (`which`, a link to one or to the list of several), the reassurance every\n * refused send carries (SPEC \u00A712), and the fix, once. The dashboard's health block and the\n * Sent page's attention card both say it, in place of the state's line and the send's own.\n */\nexport function heldSendsHtml(which: Html, count: number): Html {\n const one = count === 1;\n return html`No email provider connected, so nothing can be sent and ${which} ${one ? \"is\" : \"are\"} held, with no one marked unsent; ${one ? \"it goes out at its\" : \"they go out at their\"} first retry once one is connected. ${heldAdviceHtml()}`;\n}\n\n/**\n * A page's standing not-connected warning (DESIGN \u00A74): the health block's amber treatment,\n * not dismissible, since the state holds every time the page is read.\n * `cls` names the surface's line; null when a provider is connected.\n */\nexport function notConnectedWarning(cls: string, words: Html): Html | null {\n return providerNotConnected()\n ? html`<div class=\"health amber ${cls}\" role=\"status\"><span class=\"health-dot\">\u26A0\uFE0F</span><div><span>${words}</span></div></div>`\n : null;\n}\n\n/**\n * A scheduled send's line while no provider is connected: its window is real and still\n * cancelable, so the countdown stays, but at the fire time it is held, not sent. Read from\n * the deployment reflection, not from the send, which carries no condition until it fires.\n */\nexport function heldWhenFiresHtml(): Html | null {\n return providerNotConnected()\n ? html`<div class=\"sched-held\">Will be held: no email provider is connected.</div>`\n : null;\n}\n\n/**\n * The standing line a dialog carries while no provider is connected (DESIGN \u00A74): not\n * dismissible, since the state holds every time the dialog opens, and gone\n * once a provider is set. `why` says what the state stops; `after` ends the sentence the\n * link starts. Null otherwise.\n */\nfunction notConnectedLine(why: string, after: string): Html | null {\n return providerNotConnected()\n ? html`<p class=\"hint not-connected\">No email provider connected, so ${why}. ${connectProviderLink()}${after}.</p>`\n : null;\n}\n\n/** The Send-a-test dialogs' standing line; their Send is disabled beside it. */\nexport function notConnectedTestLine(): Html | null {\n return notConnectedLine(\"a test can\u2019t be sent yet\", \" to send one\");\n}\n\n/** The Add subscriber dialog's standing line: adding sends a confirmation, so its Send\n * confirmation is disabled beside it and the address field left out. */\nexport function notConnectedAddLine(): Html | null {\n return notConnectedLine(\"a confirmation can\u2019t be sent yet\", \" to add subscribers\");\n}\n\n/**\n * The Schedule dialog's place for the warning about D1's daily writes, on Workers Free's\n * limits with a provider connected: hidden until the audience is read and found too large\n * for a day's writes, then filled with the scheduled send's own condition words (DESIGN \u00A74).\n */\nexport function dailyWritesLine(): Html | null {\n return freePlanLimits() && !providerNotConnected()\n ? html`<p class=\"hint daily-writes\" id=\"schDailyWrites\" role=\"status\" hidden></p>`\n : null;\n}\n\n/** The Schedule dialog's standing line; its Schedule and Send now are disabled beside it. */\nexport function notConnectedScheduleLine(): Html | null {\n return notConnectedLine(\"this can\u2019t be scheduled yet\", \" to schedule it\");\n}\n\n/**\n * The Reschedule dialog's line. Moving a send already scheduled is still accepted (it sends\n * nothing), so its control stays live; the line says the send is held when it fires.\n */\nexport function notConnectedRescheduleLine(): Html | null {\n return notConnectedLine(\"this send is held when it fires, until one is\", \" to send it\");\n}\n\n/**\n * Suffix a send/test/schedule confirmation so it never reads as a real send on the `fake`\n * stand-in. Tense-neutral (\"nothing is delivered\") so it fits a test that already ran, a\n * send now queued, and a schedule that will fire later alike. A not-connected deployment\n * refuses those acts, so it never gets this far.\n */\nexport function withNoProviderNote(msg: string): string {\n return noEmailProvider() ? `${msg} (local stand-in: nothing is delivered)` : msg;\n}\n\n/** A post's public archive address under this deployment, by the formula the Worker\n * uses (shared/archive_url.ts), or null before the reflection has loaded. */\nexport function postArchiveUrl(slug: string): string | null {\n const d = appState.appConfig?.deployment;\n return d ? archivePostUrl(d.archiveOrigin, d.archiveBasePath, slug) : null;\n}\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M280-280h280v-80H280v80Zm0-160h400v-80H280v80Zm0-160h400v-80H280v80Zm-80 480q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm0-560v560-560Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M272-200v-560h221q65 0 120 40t55 111q0 51-23 78.5T602-491q25 11 55.5 41t30.5 90q0 89-65 124.5T501-200H272Zm121-112h104q48 0 58.5-24.5T566-372q0-11-10.5-35.5T494-432H393v120Zm0-228h93q33 0 48-17t15-38q0-24-17-39t-44-15h-95v109Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2z\"/><path d=\"M4 21V5\"/><path d=\"M9 8h6\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 3v8a4 4 0 0 0 4 4h8\"/><path d=\"m14 11 4 4-4 4\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 6 9 17l-5-5\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"m8 12 3 3 5-6\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M8 12h8\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M320-240 80-480l240-240 57 57-184 184 183 183-56 56Zm320 0-57-57 184-184-183-183 56-56 240 240-240 240Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\"/><path d=\"M5 15V5a2 2 0 0 1 2-2h10\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"12\" rx=\"2\"/><path d=\"M8 20h8\"/><path d=\"M12 16v4\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M360-280v-400h80v160h160v-160h80v400h-80v-160H440v160h-80Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M120-120v-80h720v80H120Zm320-160v-80h400v80H440Zm0-160v-80h400v80H440Zm0-160v-80h400v80H440ZM120-760v-80h720v80H120Zm0 440v-320l160 160-160 160Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 16v-4M12 8h.01\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M440-280h80v-240h-80v240Zm68.5-331.5Q520-623 520-640t-11.5-28.5Q497-680 480-680t-28.5 11.5Q440-657 440-640t11.5 28.5Q463-600 480-600t28.5-11.5ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M200-200v-100h160l120-360H320v-100h400v100H580L460-300h140v100H200Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 360 360\" aria-hidden=\"true\"><path d=\"M290.028 216.064C285.698 218.264 280.838 219.394 275.978 219.344C281.268 225.044 284.128 232.924 283.708 240.694C283.278 248.844 279.388 256.514 274.268 262.874C269.148 269.244 262.808 274.494 256.608 279.814C237.148 296.474 218.248 314.404 195.328 325.854C178.568 334.224 160.148 338.864 143.458 347.374C140.148 349.064 136.898 350.904 133.418 352.224C129.938 353.544 126.178 354.324 122.498 353.794C121.718 353.684 120.938 353.504 120.228 353.184C119.508 352.864 118.848 352.374 118.408 351.734C117.918 351.024 117.708 350.154 117.718 349.304C117.728 348.454 117.948 347.614 118.268 346.824C118.567 346.054 118.967 345.339 119.371 344.618L119.418 344.534C128.788 327.894 140.118 312.454 150.358 296.344C160.598 280.224 169.848 263.174 174.428 244.634C176.258 237.264 177.328 229.644 176.738 222.074C176.138 214.504 173.808 206.964 169.308 200.854C164.588 194.434 157.728 189.874 150.538 186.434C139.628 181.204 127.768 178.284 115.858 176.154C93.9183 172.224 71.6083 170.884 49.4083 168.944C45.8483 168.634 42.2483 168.294 38.8783 167.124C34.7683 165.704 31.0683 163.004 28.7383 159.334C26.4183 155.664 25.5583 151.024 26.7483 146.844C27.6583 143.624 29.7283 140.784 32.3583 138.724C34.9883 136.654 38.1683 135.324 41.4383 134.634C44.7183 133.944 48.0983 133.874 51.4383 134.154C53.2683 134.304 55.0883 134.554 56.8983 134.824C72.2383 137.094 87.3783 140.484 102.548 143.644C115.468 146.334 128.498 148.864 141.688 149.224C154.878 149.574 168.338 147.654 180.178 141.824C196.548 133.764 208.758 118.514 215.298 101.474C224.258 78.094 223.028 52.174 220.398 27.284C219.788 21.484 219.108 15.614 220.058 9.854C220.178 9.124 220.328 8.38398 220.658 7.71398C220.988 7.05398 221.528 6.45399 222.228 6.19399C222.858 5.95399 223.568 6.01399 224.218 6.22399C224.858 6.43399 225.448 6.77399 226.028 7.12399C262.588 29.154 290.508 63.614 310.198 101.484C312.188 105.314 314.108 109.204 315.408 113.314C316.708 117.434 317.358 121.814 316.768 126.084C316.348 129.114 315.268 132.104 313.318 134.454C310.668 137.634 306.688 139.354 302.778 140.744C298.878 142.124 294.808 143.324 291.488 145.804C286.998 149.174 284.328 154.834 284.598 160.444C292.098 161.904 299.448 164.114 306.508 167.014C312.788 169.594 318.998 172.854 323.418 178.014C327.088 182.294 329.288 187.614 331.438 192.844C332.158 194.584 332.878 196.344 333.328 198.184C333.778 200.014 333.958 201.944 333.578 203.794C333.138 205.934 331.948 207.914 330.248 209.294C329.898 208.024 329.258 206.834 328.408 205.834C326.958 204.154 324.928 203.054 322.798 202.454C318.258 201.174 313.328 202.114 309.018 204.044C304.708 205.974 300.898 208.834 297.068 211.584C294.808 213.204 292.508 214.804 290.028 216.064Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 6h11M9 12h11M9 18h11M4 6h1M4 12h1M4 18h1\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm200 160v-80h160q50 0 85-35t35-85q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H520Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"m3 7 9 6 9-6\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M120-80v-60h100v-30h-60v-60h60v-30H120v-60h120q17 0 28.5 11.5T280-280v40q0 17-11.5 28.5T240-200q17 0 28.5 11.5T280-160v40q0 17-11.5 28.5T240-80H120Zm0-280v-110q0-17 11.5-28.5T160-510h60v-30H120v-60h120q17 0 28.5 11.5T280-560v70q0 17-11.5 28.5T240-450h-60v30h100v60H120Zm60-280v-180h-60v-60h120v240h-60Zm180 440v-80h480v80H360Zm0-240v-80h480v80H360Zm0-240v-80h480v80H360Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M720-330q0 104-73 177T470-80q-104 0-177-73t-73-177v-370q0-75 52.5-127.5T400-880q75 0 127.5 52.5T580-700v350q0 46-32 78t-78 32q-46 0-78-32t-32-78v-370h80v370q0 13 8.5 21.5T470-320q13 0 21.5-8.5T500-350v-350q-1-42-29.5-71T400-800q-42 0-71 29t-29 71v370q-1 71 49 120.5T470-160q70 0 119-49.5T640-330v-390h80v390Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"7\" y=\"2\" width=\"10\" height=\"20\" rx=\"2\"/><path d=\"M11 18h2\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"m228-240 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T458-480L320-240h-92Zm360 0 92-160q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 23-5.5 42.5T818-480L680-240h-92ZM362.5-517.5Q380-535 380-560t-17.5-42.5Q345-620 320-620t-42.5 17.5Q260-585 260-560t17.5 42.5Q295-500 320-500t42.5-17.5Zm360 0Q740-535 740-560t-17.5-42.5Q705-620 680-620t-42.5 17.5Q620-585 620-560t17.5 42.5Q655-500 680-500t42.5-17.5ZM680-560Zm-360 0Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"4\" y=\"11\" width=\"16\" height=\"10\" rx=\"2\"/><path d=\"M8 11V7a4 4 0 0 1 8 0v4\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"6.5\"/><path d=\"m20 20-4.2-4.2\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 3 11 14M22 3l-7 18-4-7-7-4 18-7z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" aria-hidden=\"true\"><path d=\"M360-200v-80h480v80H360Zm0-240v-80h480v80H360Zm0-240v-80h480v80H360ZM200-160q-33 0-56.5-23.5T120-240q0-33 23.5-56.5T200-320q33 0 56.5 23.5T280-240q0 33-23.5 56.5T200-160Zm0-240q-33 0-56.5-23.5T120-480q0-33 23.5-56.5T200-560q33 0 56.5 23.5T280-480q0 33-23.5 56.5T200-400Zm-56.5-263.5Q120-687 120-720t23.5-56.5Q167-800 200-800t56.5 23.5Q280-753 280-720t-23.5 56.5Q233-640 200-640t-56.5-23.5Z\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 16V4M8 8l4-4 4 4\"/><path d=\"M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2\"/></svg>\n", "<svg xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\"><path d=\"M6 6l12 12M18 6 6 18\"/></svg>\n", "// Icons as files: client/ui/icons/*.svg, each a complete, viewable SVG that the build imports\n// as text and the editor serves inline, so an icon can be opened, previewed, and diffed as\n// the drawing it is. One registry, one renderer; the sidebar's static sprite in index.html\n// is separate because it paints before any script runs.\n\nimport { type Html, unsafeHtml } from \"./html\";\nimport article from \"./icons/article.svg\";\nimport bold from \"./icons/bold.svg\";\nimport book from \"./icons/book.svg\";\nimport branch from \"./icons/branch.svg\";\nimport check from \"./icons/check.svg\";\nimport circleCheck from \"./icons/circle-check.svg\";\nimport circleMinus from \"./icons/circle-minus.svg\";\nimport code from \"./icons/code.svg\";\nimport copyout from \"./icons/copyout.svg\";\nimport desktop from \"./icons/desktop.svg\";\nimport editable from \"./icons/editable.svg\";\nimport heading from \"./icons/heading.svg\";\nimport indent from \"./icons/indent.svg\";\nimport info from \"./icons/info.svg\";\nimport infoFilled from \"./icons/info-filled.svg\";\nimport italic from \"./icons/italic.svg\";\nimport kestrel from \"./icons/kestrel.svg\";\nimport lines from \"./icons/lines.svg\";\nimport link from \"./icons/link.svg\";\nimport mail from \"./icons/mail.svg\";\nimport ol from \"./icons/ol.svg\";\nimport paperclip from \"./icons/paperclip.svg\";\nimport phone from \"./icons/phone.svg\";\nimport plus from \"./icons/plus.svg\";\nimport preview from \"./icons/preview.svg\";\nimport quote from \"./icons/quote.svg\";\nimport readonly from \"./icons/readonly.svg\";\nimport search from \"./icons/search.svg\";\nimport send from \"./icons/send.svg\";\nimport ul from \"./icons/ul.svg\";\nimport upload from \"./icons/upload.svg\";\nimport x from \"./icons/x.svg\";\n\n// The files are the app's own drawings, so vouching for them here is the one place icons\n// become markup; every other use goes through icon().\nconst ICONS = {\n article: unsafeHtml(article),\n bold: unsafeHtml(bold),\n check: unsafeHtml(check),\n code: unsafeHtml(code),\n copyout: unsafeHtml(copyout),\n editable: unsafeHtml(editable),\n heading: unsafeHtml(heading),\n indent: unsafeHtml(indent),\n info: unsafeHtml(info),\n \"info-filled\": unsafeHtml(infoFilled),\n italic: unsafeHtml(italic),\n kestrel: unsafeHtml(kestrel),\n lines: unsafeHtml(lines),\n link: unsafeHtml(link),\n ol: unsafeHtml(ol),\n paperclip: unsafeHtml(paperclip),\n plus: unsafeHtml(plus),\n preview: unsafeHtml(preview),\n quote: unsafeHtml(quote),\n readonly: unsafeHtml(readonly),\n search: unsafeHtml(search),\n send: unsafeHtml(send),\n ul: unsafeHtml(ul),\n upload: unsafeHtml(upload),\n book: unsafeHtml(book),\n branch: unsafeHtml(branch),\n \"circle-check\": unsafeHtml(circleCheck),\n \"circle-minus\": unsafeHtml(circleMinus),\n desktop: unsafeHtml(desktop),\n mail: unsafeHtml(mail),\n phone: unsafeHtml(phone),\n x: unsafeHtml(x),\n} as const;\n\nexport type IconName = keyof typeof ICONS;\n\n/** Whether a string names an icon; for call sites that pick one by a runtime key. */\nexport function isIconName(name: string): name is IconName {\n return Object.hasOwn(ICONS, name);\n}\n\n/** The inline markup of an icon, decorative (aria-hidden) and sized by its container. */\nexport function icon(name: IconName): Html {\n return ICONS[name];\n}\n", "// The reference room shell shared by Docs and the API reference: the top bar, the\n// contents rail, and the surface switch; installRoomBar() wires the bar's one delegate.\n\nimport { buildRefParts } from \"../deployment\";\nimport { app } from \"../shell\";\nimport { appState } from \"../state\";\nimport { type Html, html } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\n\n/** The two surfaces the room switches between. */\nexport type RoomSurface = \"docs\" | \"reference\";\n\n/**\n * The reference room shell shared by Docs / API: a top bar (a rail-width \"\u2190 Dashboard\",\n * the Kestrel mark, the surface switch) over a two-column grid whose left column, the\n * contents rail, lines up exactly under \"\u2190 Dashboard\". Pass rail = null for a surface\n * with no contents rail.\n *\n * On mobile (\u2264720px, styles.css) the same markup collapses to one --bar-h row (a square\n * back arrow, the mark, the tabs) so the bar never grows past the height the in-page\n * anchors and the sticky rail are calibrated for; the \u2715 drops there (\u2190 is the one way\n * back). The body stacks, and each rail decides its own mobile shape (a folded \"On this\n * page\", or a chip row; see the surfaces).\n */\nexport function roomShell(active: RoomSurface, rail: Html | null, main: Html): Html {\n const tab = (view: RoomSurface, label: string) =>\n active === view\n ? html`<a href=\"#/${view}\" data-room=\"${view}\" data-text=\"${label}\" aria-current=\"page\">${label}</a>`\n : html`<a href=\"#/${view}\" data-room=\"${view}\" data-text=\"${label}\">${label}</a>`;\n // The running build (SPEC \u00A79) as quiet metadata (version \u2192 release, sha \u2192 commit),\n // pinned to the rail's bottom-left corner on every surface, so the bar stays identity +\n // nav. On mobile the rail is no longer a column, so the same stamp is the room's foot\n // instead (styles.css shows one or the other). Nothing until a build is known; the build\n // time rides the tooltip. Build info lives in the app's own room (and at GET\n // /api/version), never in the publisher-facing dashboard.\n const parts = buildRefParts();\n const bt = appState.appConfig?.deployment.build.buildTime;\n const built = bt\n ? `Built ${new Date(bt).toLocaleString(undefined, { dateStyle: \"medium\", timeStyle: \"short\" })}`\n : null;\n const stamp = parts\n ? html`${parts.version} <span aria-hidden=\"true\">\u00B7</span> ${parts.sha}`\n : null;\n const railFoot = stamp\n ? built\n ? html`<div class=\"rail-foot\" title=\"${built}\">${stamp}</div>`\n : html`<div class=\"rail-foot\">${stamp}</div>`\n : null;\n const foot = stamp\n ? built\n ? html`<footer class=\"room-foot\" title=\"${built}\">${stamp}</footer>`\n : html`<footer class=\"room-foot\">${stamp}</footer>`\n : null;\n const body =\n rail == null\n ? html`<div class=\"room-body norail\"><div class=\"room-main\">${main}</div></div>`\n : html`<div class=\"room-body\"><nav class=\"room-rail\" aria-label=\"Contents\"><div class=\"rail-inner\">${rail}${railFoot}</div></nav><div class=\"room-main\">${main}</div></div>`;\n return html`<div class=\"room\">\n <header class=\"room-bar\">\n <a class=\"room-back\" href=\"#/dashboard\" aria-label=\"Back to dashboard\"><span aria-hidden=\"true\">\u2190</span><span class=\"room-back-label\">Dashboard</span></a>\n <div class=\"room-nav\">\n <a class=\"room-brand\" href=\"#/docs\">${icon(\"kestrel\")}<span>Kestrel</span></a>\n <nav class=\"room-switch\" aria-label=\"Reference\">${tab(\"docs\", \"Docs\")}${tab(\"reference\", \"API\")}</nav>\n <a class=\"room-close\" href=\"#/dashboard\" title=\"Back to publication\" aria-label=\"Back to publication\"><span aria-hidden=\"true\">\u2715</span></a>\n </div>\n </header>\n ${body}\n ${foot}\n </div>`;\n}\n\n/**\n * A room-bar link to the page you're already on (the active tab, or the wordmark on the\n * docs index) sets the hash to what it already is, so no hashchange fires and nothing\n * re-renders or scrolls. Make it the \"back to the top\" it reads as, so the bar behaves\n * the same whether or not the tap happens to change the hash. Instant, like every other\n * in-page move here (the \"On this page\" links) and like a cross-page click landing at\n * the top of its page. Delegated once on #app, so it survives every re-render of the room;\n * boot calls this once.\n */\nexport function installRoomBar(): void {\n app.addEventListener(\"click\", (ev) => {\n const a = ev.target instanceof Element ? ev.target.closest(\".room-bar a[href^='#/']\") : null;\n if (!a || a.getAttribute(\"href\") !== location.hash) {\n return;\n }\n ev.preventDefault();\n window.scrollTo(0, 0);\n });\n}\n", "// The one spelling of a link into the editor from outside it (a notification email to the\n// publisher), shared by the Worker that writes the link and the editor that opens it, so\n// the two can't disagree.\n//\n// The editor's pages are hash routes (`#/sent/<id>`), but a browser never sends the hash\n// to the server, so a login in between (Cloudflare Access, when the publisher isn't\n// signed in) returns to `/dashboard/` without it and lands on the editor's home. A link\n// from outside carries its route in the query instead, which the login keeps, and the\n// editor turns it into the hash at boot.\n\n/** The query parameter that carries the route. */\nconst PARAM = \"to\";\n\n/** A link from outside the editor to one of its pages: `route` is the hash route without\n * its `#` (`/sent/<id>`), each segment already encoded. The route's slashes stay literal,\n * so the link reads as the page it opens (`\u2026/dashboard/?to=/sent/<id>`). */\nexport function dashboardLink(appOrigin: string, route: string): string {\n return `${appOrigin}/dashboard/?${PARAM}=${encodeURIComponent(route).replaceAll(\"%2F\", \"/\")}`;\n}\n\n/** A route: `/` and then the characters an encoded segment uses, repeated. */\nconst ROUTE = /^(\\/[A-Za-z0-9._~%-]+)+$/;\n\n/** A segment a URL parser reads as `.` or `..`, spelled out or percent-encoded. The editor\n * puts a route's segments into API paths as they are, so one of these would climb out of\n * the path the page meant to read. */\nconst DOT_SEGMENT = /\\/(?:\\.|%2e){1,2}(?=\\/|$)/i;\n\n/** Whether `route` (a hash route without its `#`) is one the editor will open: path-shaped,\n * with no dot segment. The editor checks its hash with this as well as a link's query, so a\n * route typed into the address bar is held to the same rule as one from an email. */\nexport function isDashboardRoute(route: string): boolean {\n return ROUTE.test(route) && !DOT_SEGMENT.test(route);\n}\n\n/** The hash a link's query names (`#/sent/<id>`), or null when it names none. Only a route\n * the editor will open is taken, so a mangled or hand-typed query opens the editor's home\n * rather than a nonsense route. */\nexport function dashboardRouteFromSearch(search: string): string | null {\n const route = new URLSearchParams(search).get(PARAM);\n return route && isDashboardRoute(route) ? `#${route}` : null;\n}\n", "/**\n * The send cursor (SPEC \u00A78): where a read of sends stood, handed to a client so it can\n * later ask for what changed since.\n *\n * It carries two positions. `seq` is the change sequence at the read: a send whose visible\n * state changed afterward has a higher `rev`. `at` is the server's time of the read, for\n * what changes with the clock rather than with a write (a fire time passing, a send\n * becoming stuck, a lease running out), which no `rev` can record.\n *\n * The format is part of the API, not a private detail: `<seq>.<at>`, both decimal\n * integers. Any client may read it and compare two, so a client following sends from\n * several reads (a list, and one send's page) can follow them all from the earlier cursor\n * in one feed read, as the editor does; nothing here is the editor's alone. A cursor that\n * does not parse is refused, never guessed at.\n */\n\n/** A read's position among the changes to sends. */\nexport interface SendCursor {\n seq: number;\n at: number;\n}\n\nconst PATTERN = /^(\\d+)\\.(\\d+)$/;\n\n/** The cursor as the API carries it. */\nexport function encodeSendCursor(cursor: SendCursor): string {\n return `${cursor.seq}.${cursor.at}`;\n}\n\n/** A cursor a client handed back, or null when it is not one this server issued. */\nexport function decodeSendCursor(raw: string): SendCursor | null {\n const match = PATTERN.exec(raw);\n if (!match?.[1] || !match[2]) {\n return null;\n }\n const seq = Number(match[1]);\n const at = Number(match[2]);\n return Number.isSafeInteger(seq) && Number.isSafeInteger(at) ? { seq, at } : null;\n}\n\n/**\n * A cursor at or before both (the smaller `seq` and the smaller `at`): asking what changed\n * since it answers for each of them, and a send reported twice reads the same both times.\n * The rule any client may apply to the documented format; one that does not parse gives way\n * to the other.\n */\nexport function earlierCursor(a: string, b: string): string {\n const x = decodeSendCursor(a);\n const y = decodeSendCursor(b);\n if (!x || !y) {\n return x ? a : b;\n }\n return encodeSendCursor({ seq: Math.min(x.seq, y.seq), at: Math.min(x.at, y.at) });\n}\n", "// The send-state layer: one shared read of the send feed (GET /api/sends/feed), which pages\n// follow instead of polling, so every page tells the same story about a send and keeps up\n// with it, whichever client changed it (docs/DESIGN.md \u00A74). The server says when to read\n// again; the layer keeps to it.\n\nimport { earlierCursor } from \"../shared/cursor\";\nimport type { SendFeedResponse, SendPhase, SendStatus, SendView } from \"../shared/sends\";\nimport { ApiError, api } from \"./api\";\n\n/**\n * Where a send is in its life, as a page reacts to it: in the review window, due (its\n * fire time has passed and the next sweep starts it), sending, sent with receipts still\n * arriving, complete, or canceled. Coarser than the phase: a send that retries, backs off,\n * or needs attention is still sending.\n */\nexport type SendStage = \"scheduled\" | \"due\" | \"sending\" | \"sent\" | \"complete\" | \"canceled\";\n\n/** A send's stage, from its status and derived phase. */\nexport function stageOf(s: { status: SendStatus; phase: SendPhase }): SendStage {\n switch (s.status) {\n case \"scheduled\":\n return s.phase === \"due\" ? \"due\" : \"scheduled\";\n case \"sending\":\n return \"sending\";\n case \"sent\":\n return s.phase === \"complete\" ? \"complete\" : \"sent\";\n default:\n return \"canceled\";\n }\n}\n\n/** A send that moved since a follower last looked; `from` is null for one it had not seen. */\nexport interface StageChange {\n send: SendView;\n from: SendStage | null;\n to: SendStage;\n}\n\n/** What a follower of every send gets from a read that found something changed. */\nexport interface SendsUpdate {\n /** Every send that changed since the follower's last update, soonest fire first. */\n sends: SendView[];\n /** Those whose stage moved, a send the follower had not seen included. */\n changes: StageChange[];\n /** The ids of sends removed since (deleted with their post). */\n removed: string[];\n}\n\n/**\n * What a page does with what the layer reads. `stale` is the server saying the page's\n * cursor is ahead of its database (reset or restored since): nothing after it will ever be\n * reported, so the layer stops following for every page, and each re-reads its own list or\n * send and follows again from that read.\n */\nexport interface SendsFollower {\n update(update: SendsUpdate): void;\n stale(): void;\n}\n\n/** What a page following one send does with what the layer reads: `update` for each read\n * that changed it, `removed` once it is deleted, `stale` as for `SendsFollower`. */\nexport interface SendFollower {\n update(send: SendView, change: StageChange | null): void;\n removed(): void;\n stale(): void;\n}\n\n/** Where a page's own read of sends (`GET /api/sends`) stood: its cursor, and the rows it painted. */\nexport interface ListRead {\n cursor: string;\n sends: readonly { id: string; status: SendStatus; phase: SendPhase }[];\n}\n\n/** Where a page's own read of one send (`GET /api/sends/:id`, or an action's answer) stood. */\nexport interface SendRead {\n cursor: string;\n send: Pick<SendView, \"id\" | \"status\" | \"phase\">;\n}\n\n// A failed read is tried again after this, doubling up to the cap while it keeps failing.\nconst RETRY_MS = 3000;\nconst RETRY_MAX_MS = 60_000;\n// A read that has not answered by this is cut off and counts as failed. Without it, one\n// that never settles (a dropped connection, a dev server restarted mid-request) would hold\n// `reading` for good, and the layer would never read again until the page was reloaded.\nconst READ_TIMEOUT_MS = 30_000;\n\n/** One page's interest in the feed. */\ninterface Follower {\n /** Take a read it was part of. */\n take(res: SendFeedResponse): void;\n /** Its cursor is ahead of the database: it has been dropped, and reads again. */\n stale(): void;\n}\n\nconst followers = new Set<Follower>();\n// Where the layer's last read stood, and the earliest cursor a page joined with since: the\n// next read asks from the earlier of the two, so it answers for every follower (a send\n// reported twice reads the same both times).\nlet cursor: string | null = null;\nlet floor: string | null = null;\nlet timer: ReturnType<typeof setTimeout> | undefined;\nlet reading: AbortController | null = null;\nlet readAgain = false;\nlet readQueued = false;\nlet failures = 0;\n\n// Whatever a follower's handler throws is its own bug: reported, and never allowed to stop\n// the others or the cadence.\nfunction safely(fn: () => void): void {\n try {\n fn();\n } catch (e) {\n reportError(e);\n }\n}\n\nfunction lower(a: string | null, b: string | null): string | null {\n return a && b ? earlierCursor(a, b) : (a ?? b);\n}\n\nfunction arm(ms: number): void {\n clearTimeout(timer);\n timer = undefined;\n if (!followers.size || document.hidden) {\n return;\n }\n timer = setTimeout(read, ms);\n}\n\nfunction read(): void {\n clearTimeout(timer);\n timer = undefined;\n if (!followers.size) {\n return;\n }\n if (reading) {\n readAgain = true; // someone new, or a page that just acted: once more when this lands\n return;\n }\n const since = lower(cursor, floor);\n floor = null;\n const round = [...followers];\n const c = new AbortController();\n reading = c;\n const cutOff = setTimeout(() => c.abort(), READ_TIMEOUT_MS);\n const query = since ? `?since=${encodeURIComponent(since)}` : \"\";\n api<SendFeedResponse>(`/api/sends/feed${query}`, { signal: c.signal }).then(\n (res) => {\n clearTimeout(cutOff);\n if (reading !== c) {\n return; // stopped meanwhile\n }\n reading = null;\n failures = 0;\n cursor = res.cursor;\n for (const f of round) {\n if (followers.has(f)) {\n f.take(res);\n }\n }\n if (readAgain) {\n readAgain = false;\n read();\n return;\n }\n // By the server's clock: its `now` and `read_again_at` are one clock, whatever ours says.\n arm(Math.max(0, res.read_again_at - res.now));\n },\n (err: unknown) => {\n clearTimeout(cutOff);\n if (reading !== c) {\n return; // stopped meanwhile (a cut-off read is still the one out, so it goes on)\n }\n reading = null;\n readAgain = false;\n if (err instanceof ApiError && err.code === \"cursor_ahead\") {\n // The database is behind every cursor the layer holds (a reset or a restore): no\n // read from them would ever report anything. Every page reads its own sends again\n // and follows from that read.\n const dropped = [...followers];\n for (const f of dropped) {\n leave(f);\n }\n for (const f of dropped) {\n safely(() => f.stale());\n }\n return;\n }\n if (err instanceof ApiError && err.status === 400) {\n // A cursor this server no longer reads (one from before a deploy changed its\n // format): start again from what can change on its own.\n cursor = null;\n } else {\n floor = lower(floor, since);\n }\n // A background read that fails stays quiet and is tried again, spaced out: the page\n // loaded from its own read, and the next good read reports everything since.\n failures += 1;\n arm(Math.min(RETRY_MS * 2 ** (failures - 1), RETRY_MAX_MS));\n },\n );\n}\n\n// Followers that join in the same tick (a page wiring several sections) share one read.\nfunction queueRead(): void {\n if (readQueued) {\n return;\n }\n readQueued = true;\n queueMicrotask(() => {\n readQueued = false;\n read();\n });\n}\n\n// Hidden, the layer reads nothing (its timer is dropped, and `arm` sets none); shown again,\n// it reads at once, since anything may have happened meanwhile.\nfunction onVisibility(): void {\n if (document.hidden) {\n clearTimeout(timer);\n timer = undefined;\n } else {\n read();\n }\n}\n\nfunction join(f: Follower, since: string, signal: AbortSignal): void {\n if (signal.aborted) {\n return;\n }\n followers.add(f);\n floor = lower(floor, since);\n if (followers.size === 1) {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n signal.addEventListener(\"abort\", () => leave(f), { once: true });\n queueRead();\n}\n\nfunction leave(f: Follower): void {\n followers.delete(f);\n if (followers.size) {\n return;\n }\n clearTimeout(timer);\n timer = undefined;\n reading?.abort();\n reading = null;\n readAgain = false;\n failures = 0;\n cursor = null;\n floor = null;\n document.removeEventListener(\"visibilitychange\", onVisibility);\n}\n\n/**\n * Follow every send from the page's own list read, for the life of `signal`. The first\n * feed read asks from that read's cursor, so nothing that changed between the two is\n * missed. `follower.update` gets each later read that found a change, with the stage\n * changes since the page's read (a send it did not list reads as first seen) and the sends\n * removed.\n */\nexport function followSends(from: ListRead, follower: SendsFollower, signal: AbortSignal): void {\n const seen = new Map<string, SendStage>(from.sends.map((s) => [s.id, stageOf(s)]));\n join(\n {\n take(res) {\n if (!res.sends.length && !res.removed.length) {\n return;\n }\n const changes: StageChange[] = [];\n for (const send of res.sends) {\n const to = stageOf(send);\n const was = seen.get(send.id) ?? null;\n if (was !== to) {\n changes.push({ send, from: was, to });\n }\n seen.set(send.id, to);\n }\n const removed = res.removed.map((r) => r.id);\n for (const id of removed) {\n seen.delete(id);\n }\n safely(() => follower.update({ sends: res.sends, changes, removed }));\n },\n stale: () => follower.stale(),\n },\n from.cursor,\n signal,\n );\n}\n\n/**\n * Follow one send from the page's own read of it, for the life of `signal`, whatever its\n * state. `follower.update` gets the send from each later read that found it changed, with\n * its stage change when it moved; `follower.removed` is told once it is deleted.\n */\nexport function followSend(from: SendRead, follower: SendFollower, signal: AbortSignal): void {\n const id = from.send.id;\n let seen: SendStage = stageOf(from.send);\n join(\n {\n take(res) {\n if (res.removed.some((r) => r.id === id)) {\n safely(() => follower.removed());\n return;\n }\n const send = res.sends.find((s) => s.id === id);\n if (!send) {\n return;\n }\n const to = stageOf(send);\n const change = to !== seen ? { send, from: seen, to } : null;\n seen = to;\n safely(() => follower.update(send, change));\n },\n stale: () => follower.stale(),\n },\n from.cursor,\n signal,\n );\n}\n\n/**\n * Read now, for a page that has just acted on a send (a cancel, a move, a Resolve), so\n * everything following sends catches up at once instead of at the next read. Nothing when\n * nothing follows.\n */\nexport function readSendsNow(): void {\n if (followers.size) {\n read();\n }\n}\n", "// What a valid email address is, spelled once for the Worker and the editor, so a form\n// never accepts an address the API will refuse, or refuses one it would take.\n\n/** The form an address is stored and compared in: trimmed and lowercased. */\nexport function normalizeEmail(email: string): string {\n return email.trim().toLowerCase();\n}\n\n/**\n * A plausible address: one `@`, something before it, and a dotted domain after it, with\n * no whitespace. Deliberately loose: the confirmation email is the real check (SPEC \u00A77).\n * Pass it a normalized address.\n */\nexport function isValidEmail(email: string): boolean {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);\n}\n", "// Pure formatting and parsing, with no DOM: dates, countdowns, the datetime-local value,\n// and the recipient list.\n\nimport { isValidEmail, normalizeEmail } from \"../../shared/email\";\n\n/** A short local date-time, or an em dash for none. */\nexport const fmt = (ms: number | null | undefined): string =>\n ms\n ? new Date(ms).toLocaleString(undefined, {\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n })\n : \"\u2014\";\n\n/**\n * Split a free-text recipient list (newlines or commas) into unique, normalized\n * addresses, and the entries that aren't addresses, as typed, so the caller can name\n * them instead of quietly sending to fewer people than were listed. Server-side\n * validation is still authoritative; this tidies the Send-test input.\n */\nexport function parseAddresses(text: string | null | undefined): {\n valid: string[];\n invalid: string[];\n} {\n const valid = new Set<string>();\n const invalid: string[] = [];\n for (const part of String(text || \"\").split(/[\\n,]+/)) {\n const a = normalizeEmail(part);\n if (isValidEmail(a)) {\n valid.add(a);\n } else if (a) {\n invalid.push(part.trim());\n }\n }\n return { valid: [...valid], invalid };\n}\n\n/** The toast for Send-test entries that aren't addresses, or null when there are none. */\nexport function invalidAddressesMessage(invalid: string[]): string | null {\n if (!invalid.length) {\n return null;\n }\n return `Not ${invalid.length === 1 ? \"an email address\" : \"email addresses\"}: ${invalid.join(\", \")}`;\n}\n\n/**\n * The dialog error for a test that didn't reach every address: who got it, who didn't,\n * each distinct reason once, and that Send again tries only those. Both test dialogs leave\n * just the failed addresses in their field, so a second press mails no one twice.\n */\nexport function notSentMessage(\n sent: number,\n total: number,\n failed: string[],\n reasons: string[] = [],\n): string {\n const why = reasons.length ? ` ${reasons.join(\" \")}` : \"\";\n return sent\n ? `Sent to ${sent} of ${total}. Not sent to ${failed.join(\", \")}.${why} Send again to try those.`\n : `Not sent.${why} Send again to try.`;\n}\n\n/** A Date as the value of a `datetime-local` input, in local time. */\nexport function toLocalInput(d: Date): string {\n const p = (n: number) => String(n).padStart(2, \"0\");\n return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;\n}\n\n/**\n * A countdown that gets coarser the further out the fire time is: it ticks seconds\n * only inside the last few minutes (the imminent / send-now cancel window), then\n * counts down by the minute, by the hour within a day, and by whole days beyond, so\n * a send scheduled days away reads \"Sends in 2 days\", not a ticking \"47h 47m\".\n */\nexport function untilStr(fireAt: number, due = false): string {\n const d = fireAt - Date.now();\n if (due || d <= 0) {\n // From the fire time the send waits for the sweep's tick on that minute (a fire time is\n // on the minute, SPEC \u00A76), a few seconds; nothing is sending yet, and the card leaves the\n // queue once something is (docs/DESIGN.md \u00A74).\n return \"Preparing to send\u2026\";\n }\n const s = Math.floor(d / 1000);\n const min = Math.floor(s / 60);\n const hr = Math.floor(min / 60);\n if (s < 300) {\n return min > 0 ? `Sends in ${min}m ${String(s % 60).padStart(2, \"0\")}s` : `Sends in ${s}s`;\n }\n if (min < 60) {\n return `Sends in ${min}m`;\n }\n if (hr < 24) {\n const rm = min % 60;\n return rm > 0 ? `Sends in ${hr}h ${rm}m` : `Sends in ${hr}h`;\n }\n const days = Math.round(hr / 24);\n return `Sends in ${days} day${days === 1 ? \"\" : \"s\"}`;\n}\n\n/** A scheduled send past the server's missed tolerance, by how late it now is. */\nexport function lateStr(fireAt: number): string {\n const min = Math.max(0, Math.floor((Date.now() - fireAt) / 60_000));\n return `Missed its fire time \u00B7 ${min} min late`;\n}\n", "// Shared list controls: the query model, toolbar, sortable headers, and pager.\n\nimport type { PageMeta, SortDir } from \"../../shared/list\";\nimport { type Html, html, setHtml } from \"./html\";\n\n// One convention for the admin list views (Posts, Subscribers, Sends): a search-left\n// / filters-right toolbar, clickable sortable column headers, and offset pagination \u2014\n// all driven by a small per-view `state` object whose reload() rebuilds the query and\n// re-fetches. The hash only seeds the initial filter (dashboard deep-links); sort,\n// page, and filter operate in place, matching the `page` envelope the endpoints return\n// (shared/list.ts).\n\n/** The per-view list state every control reads and writes. */\nexport interface ListState {\n status?: string;\n search?: string;\n sort?: string;\n dir?: SortDir;\n /** \"only\" narrows to suppressed subscribers; a deliverability axis, independent of status. */\n suppressed?: \"\" | \"only\";\n /** \"only\" narrows to sends with delivery failures. */\n failures?: \"\" | \"only\";\n offset: number;\n limit: number;\n}\n\n/** What a list toolbar offers; see listToolbar for what each field means. */\nexport interface ToolbarConfig {\n statuses?: { value: string; label: string }[];\n allValue?: string;\n searchPlaceholder?: string;\n suppressible?: boolean;\n failures?: boolean;\n}\n\n/**\n * Build the query string for a list request from the view state. `sort`/`dir` are sent\n * only once a column is chosen (state.sort set), so a view keeps its endpoint's bespoke\n * default order (e.g. posts' scheduled-first) until the reader sorts.\n */\nexport function listQuery(state: ListState): string {\n const p = new URLSearchParams();\n if (state.status) {\n p.set(\"status\", state.status);\n }\n if (state.suppressed) {\n p.set(\"suppressed\", state.suppressed);\n }\n if (state.failures) {\n p.set(\"failures\", state.failures);\n }\n const term = (state.search || \"\").trim();\n if (term) {\n p.set(\"search\", term);\n }\n if (state.sort) {\n p.set(\"sort\", state.sort);\n p.set(\"dir\", state.dir ?? \"desc\");\n }\n p.set(\"limit\", String(state.limit));\n p.set(\"offset\", String(state.offset));\n return p.toString();\n}\n\n/**\n * A filter/search toolbar: search on the left, the status filter pinned right.\n * `cfg.statuses` = [{value,label}]. `cfg.suppressible` (subscribers only) adds a\n * separate \"Suppressed only\" toggle \u2014 suppression is a deliverability flag, not a\n * consent status, so it's its own control (an independent axis you can combine with a\n * status), never an option inside the status dropdown.\n * `cfg.allValue` sets what the \"All statuses\" option means \u2014 normally \"\" (no status\n * filter), but the Drafts view passes \"draft,scheduled\" so \"All\" stays scoped to the\n * two draft-side statuses rather than reaching sent posts. Omit `cfg.statuses` for a\n * search-only toolbar (the Sent list is single-status, so it carries no status filter).\n * `cfg.failures` adds the Sent list's \"With delivery failures\" flag \u2014 a filter, not a sort: it keeps\n * the newest-first order the operator scans by and needs no severity weighting (a summed\n * sort would rank 25 retried unsent recipients above one spam complaint).\n */\nexport function listToolbar(cfg: ToolbarConfig): Html {\n const statusSel = cfg.statuses?.length\n ? html`<select class=\"lt-status\" aria-label=\"Filter by status\"><option value=\"${cfg.allValue || \"\"}\">All statuses</option>${cfg.statuses.map(\n (s) => html`<option value=\"${s.value}\">${s.label}</option>`,\n )}</select>`\n : null;\n const suppressed = cfg.suppressible\n ? html`<label class=\"lt-toggle\"><input type=\"checkbox\" class=\"lt-suppressed\"><span>Suppressed</span></label>`\n : null;\n const failures = cfg.failures\n ? html`<label class=\"lt-toggle\"><input type=\"checkbox\" class=\"lt-failures\"><span>With delivery failures</span></label>`\n : null;\n return html`<div class=\"list-toolbar\">\n <input class=\"lt-search\" type=\"search\" placeholder=\"${cfg.searchPlaceholder || \"Search\u2026\"}\" aria-label=\"Search\" autocomplete=\"off\">\n <div class=\"lt-filters\">${statusSel}${suppressed}${failures}</div>\n </div>`;\n}\n\n/**\n * Wire the toolbar controls (within `root`) to the view's reload, seeding their values\n * from state so a deep-linked filter shows selected. Search is debounced; any change\n * resets to the first page. Status and the suppression toggle are independent axes.\n */\nexport function wireToolbar(root: ParentNode, state: ListState, reload: () => void): void {\n const search = root.querySelector<HTMLInputElement>(\".lt-search\");\n const status = root.querySelector<HTMLSelectElement>(\".lt-status\");\n const suppressed = root.querySelector<HTMLInputElement>(\".lt-suppressed\");\n const failures = root.querySelector<HTMLInputElement>(\".lt-failures\");\n if (search) {\n search.value = state.search || \"\";\n let t: number | undefined;\n search.oninput = () => {\n clearTimeout(t);\n t = setTimeout(() => {\n state.search = search.value;\n state.offset = 0;\n reload();\n }, 250);\n };\n }\n if (status) {\n status.value = state.status || \"\";\n status.onchange = () => {\n state.status = status.value;\n state.offset = 0;\n reload();\n };\n }\n if (suppressed) {\n suppressed.checked = state.suppressed === \"only\";\n suppressed.onchange = () => {\n state.suppressed = suppressed.checked ? \"only\" : \"\";\n state.offset = 0;\n reload();\n };\n }\n if (failures) {\n failures.checked = state.failures === \"only\";\n failures.onchange = () => {\n state.failures = failures.checked ? \"only\" : \"\";\n state.offset = 0;\n reload();\n };\n }\n}\n\n/**\n * A table header cell. A sortable column (given a `key`) renders a button that toggles\n * asc/desc and shows the active direction; other columns are plain labels. `cls` adds a\n * column class (e.g. \"num\" for right-aligned numeric columns).\n */\nexport function th(label: string, key: string | null, state: ListState, cls?: string): Html {\n if (!key) {\n return cls ? html`<th class=\"${cls}\">${label}</th>` : html`<th>${label}</th>`;\n }\n const active = state.sort === key;\n // A fixed-width slot always reserved (empty when unsorted) so the label doesn't\n // shift when the arrow appears; light \u2191/\u2193 to match the app's other arrows.\n const arrow = active ? (state.dir === \"asc\" ? \"\u2191\" : \"\u2193\") : \"\";\n const klass = `${cls ? `${cls} ` : \"\"}sortable${active ? \" sorted\" : \"\"}`;\n return html`<th class=\"${klass}\"><button type=\"button\" class=\"th-sort\" data-sort=\"${key}\">${label}<span class=\"th-arrow\" aria-hidden=\"true\">${arrow}</span></button></th>`;\n}\n\n/**\n * Wire the sortable headers inside a freshly-rendered table. Clicking a column sorts by\n * it (default desc), or flips direction if it is already the sort key; resets to page 1.\n */\nexport function wireSort(container: ParentNode, state: ListState, reload: () => void): void {\n for (const b of container.querySelectorAll<HTMLButtonElement>(\".th-sort\")) {\n b.onclick = () => {\n const key = b.dataset.sort;\n if (state.sort === key) {\n state.dir = state.dir === \"asc\" ? \"desc\" : \"asc\";\n } else {\n state.sort = key;\n state.dir = \"desc\";\n }\n state.offset = 0;\n reload();\n };\n }\n}\n\n/**\n * Offset pager: \"a\u2013b of N\" with Prev/Next. Renders nothing when one page covers all.\n */\nexport function renderPager(\n el: Element,\n state: ListState,\n page: PageMeta | null | undefined,\n reload: () => void,\n): void {\n // Page by the limit the server actually clamped to (page.limit), not the requested one.\n const limit = page?.limit ?? state.limit;\n if (!page || page.total <= limit) {\n setHtml(el, html``);\n return;\n }\n const from = page.offset + 1;\n const to = Math.min(page.offset + limit, page.total);\n const hasPrev = page.offset > 0;\n const hasNext = page.offset + limit < page.total;\n setHtml(\n el,\n html`<div class=\"pager\"><button type=\"button\" class=\"pager-prev\"${hasPrev ? null : DISABLED}>\u2190 Prev</button><span class=\"pager-range muted\">${from}\u2013${to} of ${page.total}</span><button type=\"button\" class=\"pager-next\"${hasNext ? null : DISABLED}>Next \u2192</button></div>`,\n );\n if (hasPrev) {\n const prev = el.querySelector<HTMLButtonElement>(\".pager-prev\");\n if (prev) {\n prev.onclick = () => {\n state.offset = Math.max(0, page.offset - limit);\n reload();\n };\n }\n }\n if (hasNext) {\n const next = el.querySelector<HTMLButtonElement>(\".pager-next\");\n if (next) {\n next.onclick = () => {\n state.offset = page.offset + limit;\n reload();\n };\n }\n }\n}\n\n// A bare attribute is markup, not text: spelled once as markup so it can be interpolated.\nconst DISABLED = html` disabled`;\n", "// Small shared widgets, each independent of the others: the toast, the status badge, the\n// modal, the info tooltip, the row-action menu, the busy state on a button, and the error\n// view with its retry. The tooltip's document\n// listeners are wired by installTooltips() when boot calls it.\n\nimport { toasts } from \"../shell\";\nimport { type Html, html, setHtml } from \"./html\";\nimport { icon } from \"./icons\";\n\n// Long enough to read a sentence-length confirmation (\"Test sent to 2 addresses\").\nconst TOAST_MS = 3600;\n// Matches the .toast.leaving transition in styles.css, so removal trails the collapse.\nconst TOAST_LEAVE_MS = 300;\n// More than this at once is a wall, not a confirmation (DESIGN \u00A75).\nconst MAX_TOASTS = 3;\nconst toastTimers = new WeakMap<HTMLElement, number>();\n\n/**\n * A transient confirmation in the app-wide toast region (DESIGN \u00A75). The same message\n * already showing has its time restarted rather than a second copy stacked beneath it,\n * and past MAX_TOASTS the oldest leaves early, so a burst never builds a tower.\n */\nexport function toast(msg: string): void {\n const live = [...toasts.querySelectorAll<HTMLElement>(\".toast:not(.leaving)\")];\n const same = live.find((t) => t.textContent === msg);\n if (same) {\n lingerToast(same);\n return;\n }\n const t = document.createElement(\"div\");\n t.className = \"toast\";\n t.textContent = msg;\n toasts.appendChild(t);\n requestAnimationFrame(() => t.classList.add(\"show\"));\n lingerToast(t);\n for (const old of live.slice(0, live.length + 1 - MAX_TOASTS)) {\n leaveToast(old);\n }\n}\n\nfunction lingerToast(t: HTMLElement): void {\n clearTimeout(toastTimers.get(t));\n toastTimers.set(\n t,\n window.setTimeout(() => leaveToast(t), TOAST_MS),\n );\n}\n\n// Fade and collapse the toast's height together, so the ones beside it slide into its\n// place rather than jump. The height is pinned and committed first because a transition\n// can't start from auto.\nfunction leaveToast(t: HTMLElement): void {\n clearTimeout(toastTimers.get(t));\n t.style.height = `${t.offsetHeight}px`;\n void t.offsetHeight;\n t.classList.add(\"leaving\");\n t.style.height = \"0\";\n setTimeout(() => t.remove(), TOAST_LEAVE_MS);\n}\n\n/** A status pill; the status doubles as its class. */\nexport const badge = (status: string): Html => html`<span class=\"badge ${status}\">${status}</span>`;\n\nexport interface Modal {\n el: HTMLElement;\n close: () => void;\n /**\n * Say why the dialog's act didn't go through, inside the dialog: beneath `field` when one\n * field is at fault (marked invalid, focused, and cleared as soon as it is edited),\n * otherwise just above the actions, where the reader is looking; a toast at the foot of\n * the screen would be far from the field and gone while the dialog stays open. A second\n * call replaces the first.\n */\n error: (message: string, field?: HTMLInputElement | HTMLTextAreaElement) => void;\n /** Take the error away, as a new try begins. */\n clearError: () => void;\n}\n\nlet fieldErrorSeq = 0;\n// The clear function of the error each field shows now, so a second error replaces the first.\nconst fieldErrors = new WeakMap<HTMLElement, () => void>();\n\n/** The one look of a field or dialog error: an icon and the reason, announced as an alert. */\nfunction errorLine(message: string): HTMLElement {\n const line = document.createElement(\"div\");\n line.className = \"field-error\";\n line.id = `field-error-${++fieldErrorSeq}`;\n line.setAttribute(\"role\", \"alert\");\n const ico = document.createElement(\"span\");\n ico.className = \"field-error-ico\";\n ico.setAttribute(\"aria-hidden\", \"true\");\n ico.textContent = \"!\";\n const text = document.createElement(\"span\");\n text.textContent = message;\n line.append(ico, text);\n return line;\n}\n\n/**\n * Say why a field's value is refused, beneath the field (DESIGN \u00A75): the field is marked\n * invalid and described by the reason, and both go as soon as the field is edited.\n * Returns the function that takes the error away sooner; a second call on the same\n * field replaces the first.\n */\nexport function fieldError(\n field: HTMLInputElement | HTMLTextAreaElement,\n message: string,\n): () => void {\n fieldErrors.get(field)?.();\n const line = errorLine(message);\n const describedBy = field.getAttribute(\"aria-describedby\");\n field.after(line);\n field.setAttribute(\"aria-describedby\", describedBy ? `${describedBy} ${line.id}` : line.id);\n field.classList.add(\"is-invalid\");\n let cleared = false;\n const clear = () => {\n if (cleared) {\n return;\n }\n cleared = true;\n line.remove();\n field.classList.remove(\"is-invalid\");\n if (describedBy === null) {\n field.removeAttribute(\"aria-describedby\");\n } else {\n field.setAttribute(\"aria-describedby\", describedBy);\n }\n field.removeEventListener(\"input\", clear);\n fieldErrors.delete(field);\n };\n field.addEventListener(\"input\", clear);\n fieldErrors.set(field, clear);\n return clear;\n}\n\n/**\n * A modal over a backdrop; a click outside or Escape closes it. The content is markup.\n * The dialog belongs to the view that opened it: `signal` is that view's mount signal, and\n * when it aborts (a navigation, a remount) the dialog closes with it, so it never sits over\n * a page it does not belong to. Only the dialog goes: a write already started from it does\n * not take the signal (no write does) and still lands, its answer arriving after the\n * dialog is gone.\n */\nexport function modal(content: Html, signal: AbortSignal): Modal {\n const back = document.createElement(\"div\");\n back.className = \"modal-backdrop\";\n setHtml(back, html`<div class=\"modal\" role=\"dialog\" aria-modal=\"true\">${content}</div>`);\n const dialog = back.firstElementChild as HTMLElement;\n // Focus goes back where it was when the dialog closes, so a keyboard reader carries on\n // from the control that opened it rather than from the top of the page.\n const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n // Escape and Tab listen on the document and the end of the view on its signal, so\n // close() takes both listeners with it however the dialog closes (a button, the\n // backdrop, Escape, the view ending), and each lives exactly as long as the dialog.\n const onKey = (e: KeyboardEvent) => {\n // With dialogs stacked (a confirmation over the dialog that asked for it), only the top\n // one answers the keyboard: Escape closes it alone, and Tab stays inside it.\n if (back !== [...document.querySelectorAll(\".modal-backdrop\")].pop()) {\n return;\n }\n if (e.key === \"Escape\") {\n close();\n } else if (e.key === \"Tab\") {\n trapTab(dialog, e);\n }\n };\n // One error at a time: a field's (from fieldError) or the dialog's own line.\n let clearCurrent: (() => void) | null = null;\n const clearError = () => {\n clearCurrent?.();\n clearCurrent = null;\n };\n const error = (message: string, field?: HTMLInputElement | HTMLTextAreaElement) => {\n clearError();\n if (field) {\n clearCurrent = fieldError(field, message);\n field.focus();\n return;\n }\n const line = errorLine(message);\n line.classList.add(\"dialog-error\");\n const actions = dialog.querySelector(\".actions\");\n if (actions) {\n actions.before(line);\n } else {\n dialog.append(line);\n }\n clearCurrent = () => line.remove();\n };\n const close = () => {\n // A click on the backdrop has already dropped focus to the page body, so that counts as\n // focus lost with the dialog too.\n const active = document.activeElement;\n const hadFocus = back.contains(active) || !active || active === document.body;\n back.remove();\n document.removeEventListener(\"keydown\", onKey);\n signal.removeEventListener(\"abort\", close);\n if (hadFocus && opener?.isConnected) {\n opener.focus();\n }\n };\n if (signal.aborted) {\n // The view is already gone, so the dialog is never shown.\n return { el: back, close, error, clearError };\n }\n document.body.appendChild(back);\n back.addEventListener(\"click\", (e) => {\n if (e.target === back) {\n close();\n }\n });\n document.addEventListener(\"keydown\", onKey);\n signal.addEventListener(\"abort\", close, { once: true });\n // A caller that focuses a field of its own does so before this runs; otherwise focus\n // lands on the dialog's first control, so the keyboard starts inside it.\n queueMicrotask(() => {\n if (back.isConnected && !dialog.contains(document.activeElement)) {\n focusables(dialog)[0]?.focus();\n }\n });\n return { el: back, close, error, clearError };\n}\n\nconst FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\nfunction focusables(root: HTMLElement): HTMLElement[] {\n return [...root.querySelectorAll<HTMLElement>(FOCUSABLE)].filter((el) => !el.closest(\"[hidden]\"));\n}\n// Tab and Shift+Tab cycle within the dialog: aria-modal says the page behind is inert, so\n// focus must not walk out to it.\nfunction trapTab(dialog: HTMLElement, e: KeyboardEvent): void {\n const els = focusables(dialog);\n const first = els[0];\n const last = els[els.length - 1];\n if (!first || !last) {\n e.preventDefault();\n return;\n }\n const at = document.activeElement;\n if (!dialog.contains(at)) {\n e.preventDefault();\n first.focus();\n } else if (e.shiftKey && at === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && at === last) {\n e.preventDefault();\n first.focus();\n }\n}\n\n// Keep an info tooltip within the viewport. The tip is a CSS pseudo-element\n// anchored to the icon's left edge; pure CSS can't see the viewport, so before it\n// shows we measure the icon and, if the (width-capped) tip would run off the right\n// edge on a narrow screen, slide it left via --tip-x. Delegated so it survives view\n// re-renders; with JS off the tip falls back to left:0. Vertical placement stays in\n// CSS (the .tip-below variant) \u2014 which icons sit near the top is static, not dynamic.\nconst TIP_GUTTER = 8;\nfunction positionInfoTip(el: HTMLElement): void {\n // Measure the rendered tip (laid out even while hidden) so this stays in step\n // with the CSS max-width/padding rather than duplicating them here.\n const tip = getComputedStyle(el, \"::after\");\n const tipW = parseFloat(tip.width) + parseFloat(tip.paddingLeft) + parseFloat(tip.paddingRight);\n if (!Number.isFinite(tipW)) {\n return;\n }\n const iconLeft = el.getBoundingClientRect().left;\n const vw = document.documentElement.clientWidth;\n // Slide left enough to clear the right gutter, but never so far that the left\n // edge crosses the gutter (very narrow screens) and never rightward (shift \u2264 0).\n const shift = Math.min(0, Math.max(vw - TIP_GUTTER - tipW - iconLeft, TIP_GUTTER - iconLeft));\n el.style.setProperty(\"--tip-x\", `${Math.round(shift)}px`);\n}\n/** Wire the tooltip's positioning and dismissal, delegated on the document; boot calls this once. */\nexport function installTooltips(): void {\n // Position before the tip shows on either trigger: pointer hover, or focus \u2014 the\n // latter is how keyboard (Tab) and touch (tap focuses the span) reach it.\n for (const type of [\"pointerover\", \"focusin\"]) {\n document.addEventListener(type, (e) => {\n const el = e.target instanceof Element ? e.target.closest<HTMLElement>(\".info\") : null;\n if (el) {\n positionInfoTip(el);\n }\n });\n }\n // Escape dismisses a focus-shown tip without tabbing away (the pointer tip just\n // needs the mouse to leave).\n document.addEventListener(\"keydown\", (e) => {\n const active = document.activeElement;\n if (e.key === \"Escape\" && active instanceof HTMLElement && active.classList.contains(\"info\")) {\n active.blur();\n }\n });\n}\n\n/**\n * The \u24D8 affordance whose explanation shows as a tooltip. Focusable and role/aria-labelled\n * so it's reachable by keyboard and touch (the tip shows on :focus, not only :hover) and\n * read by screen readers \u2014 the aria-label mirrors the visible tip. `below` drops the tip\n * under the icon (for icons near the page top).\n */\nexport function infoTip(tip: string, { below = false }: { below?: boolean } = {}): Html {\n return html`<span class=\"info${below ? \" tip-below\" : \"\"}\" role=\"img\" tabindex=\"0\" aria-label=\"${tip}\" data-tip=\"${tip}\">${icon(\"info-filled\")}</span>`;\n}\n\nexport interface MenuItem {\n label: string;\n /** A line under the label saying what the pick does, for a menu whose items need it. */\n description?: string;\n danger?: boolean;\n onClick: () => void;\n}\n\n/** Where a menu sits under its trigger: `end` lines up right edges (a row's \u22EF), `start` left edges (a labeled button). */\nexport interface MenuOptions {\n align?: \"start\" | \"end\";\n}\n\n// popover menu, for row actions (\u22EF) and labeled menu buttons. A transparent full-screen overlay (behind\n// the menu) closes it on an outside click \u2014 no document-listener race.\nlet menuEls: HTMLElement[] = [];\nlet menuSignal: AbortSignal | null = null;\nlet menuAnchor: HTMLElement | null = null;\nfunction closeMenu(): void {\n const hadFocus = menuEls.some((e) => e.contains(document.activeElement));\n for (const e of menuEls) {\n e.remove();\n }\n menuEls = [];\n menuSignal?.removeEventListener(\"abort\", closeMenu);\n menuSignal = null;\n menuAnchor?.setAttribute(\"aria-expanded\", \"false\");\n // Back to the \u22EF that opened it, so the keyboard keeps its place in the list. A pick that\n // opens a dialog runs after this, and the dialog returns focus here when it closes.\n if (hadFocus && menuAnchor?.isConnected) {\n menuAnchor.focus();\n }\n menuAnchor = null;\n}\n/**\n * Open a menu under an anchor (a row's \u22EF or a labeled button); any outside click or a pick closes it. Like a\n * modal, it belongs to the view that opened it and closes when `signal`, that view's\n * mount signal, aborts.\n */\nexport function openMenu(\n anchor: Element,\n items: MenuItem[],\n signal: AbortSignal,\n { align = \"end\" }: MenuOptions = {},\n): void {\n closeMenu();\n if (signal.aborted) {\n return;\n }\n const overlay = document.createElement(\"div\");\n overlay.className = \"menu-overlay\";\n overlay.onclick = closeMenu;\n const m = document.createElement(\"div\");\n m.className = \"menu\";\n m.setAttribute(\"role\", \"menu\");\n for (const it of items) {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.setAttribute(\"role\", \"menuitem\");\n b.tabIndex = -1;\n b.className = `menu-item${it.danger ? \" danger-item\" : \"\"}`;\n if (it.description) {\n const name = document.createElement(\"span\");\n name.className = \"menu-item-name\";\n name.textContent = it.label;\n const desc = document.createElement(\"span\");\n desc.className = \"menu-item-desc\";\n desc.textContent = it.description;\n b.append(name, desc);\n m.classList.add(\"menu-described\");\n } else {\n b.textContent = it.label;\n }\n b.onclick = () => {\n closeMenu();\n it.onClick();\n };\n m.appendChild(b);\n }\n m.addEventListener(\"keydown\", (e) => menuKey(m, e));\n document.body.appendChild(overlay);\n document.body.appendChild(m);\n menuEls = [overlay, m];\n menuSignal = signal;\n menuAnchor = anchor instanceof HTMLElement ? anchor : null;\n menuAnchor?.setAttribute(\"aria-expanded\", \"true\");\n signal.addEventListener(\"abort\", closeMenu, { once: true });\n const r = anchor.getBoundingClientRect();\n m.style.top = `${r.bottom + window.scrollY + 4}px`;\n m.style.left = `${(align === \"start\" ? r.left : r.right - m.offsetWidth) + window.scrollX}px`;\n m.querySelector<HTMLElement>(\".menu-item\")?.focus();\n}\n// The menu keys: arrows, Home, and End move between items (wrapping), Escape closes it,\n// and Tab closes it and lets focus move on from the \u22EF, as a menu button does.\nfunction menuKey(m: HTMLElement, e: KeyboardEvent): void {\n const items = [...m.querySelectorAll<HTMLElement>(\".menu-item\")];\n const i = items.indexOf(document.activeElement as HTMLElement);\n const go = (n: number) => {\n e.preventDefault();\n items[(n + items.length) % items.length]?.focus();\n };\n if (e.key === \"ArrowDown\") {\n go(i + 1);\n } else if (e.key === \"ArrowUp\") {\n go(i - 1);\n } else if (e.key === \"Home\") {\n go(0);\n } else if (e.key === \"End\") {\n go(items.length - 1);\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n e.stopPropagation();\n closeMenu();\n } else if (e.key === \"Tab\") {\n closeMenu();\n }\n}\n\n/** Run an action with the button disabled and relabeled, restoring it after, if it is still in the page. */\nexport async function busy<T>(\n btn: HTMLButtonElement,\n label: string | null,\n fn: () => Promise<T>,\n): Promise<T> {\n const orig = btn.textContent;\n btn.disabled = true;\n if (label) {\n btn.textContent = label;\n }\n try {\n return await fn();\n } finally {\n if (btn.isConnected) {\n btn.disabled = false;\n btn.textContent = orig;\n }\n }\n}\n\n/** The error view with a retry, in place of what failed to load. */\nexport function renderError(container: Element, msg: string, retryFn: () => void): void {\n setHtml(\n container,\n html`<div class=\"error\"><span>${msg}</span><button class=\"ghost\" data-retry>Retry</button></div>`,\n );\n const b = container.querySelector<HTMLButtonElement>(\"[data-retry]\");\n if (b) {\n b.onclick = retryFn;\n }\n}\n", "// The drafts list (draft + scheduled), kept current with its posts' sends.\n\nimport type { PostListResponse, PostSavedResponse } from \"../../shared/posts\";\nimport { api, describeError } from \"../api\";\nimport { mount } from \"../lifecycle\";\nimport { followSends } from \"../send_state\";\nimport { $, $$ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { html, setHtml } from \"../ui/html\";\nimport {\n type ListState,\n listQuery,\n listToolbar,\n renderPager,\n th,\n wireSort,\n wireToolbar,\n} from \"../ui/list_controls\";\nimport { badge, busy, type MenuItem, modal, openMenu, renderError, toast } from \"../ui/widgets\";\n\n// The writing side: draft + scheduled only \u2014 a sent post is a frozen record and lives in\n// Sent. \"All statuses\" is scoped to those two, so it never reaches sent.\nconst DRAFT_STATUSES = [\n { value: \"draft\", label: \"Draft\" },\n { value: \"scheduled\", label: \"Scheduled\" },\n];\nconst DRAFTS_SCOPE = \"draft,scheduled\";\n\n/**\n * Create a draft and jump into the editor \u2014 shared by the Posts list, the Dashboard,\n * and the setup checklist so the \"New post\" affordance behaves identically everywhere.\n */\nexport function createNewPost(btn: HTMLButtonElement): Promise<void> {\n return busy(btn, \"Creating\u2026\", async () => {\n try {\n // An empty subject: the editor shows \"Untitled\" as the field's placeholder, so the\n // first thing typed is the whole subject, not an addition to a stand-in.\n const { post } = await api<PostSavedResponse>(\"/api/posts\", { method: \"POST\", json: {} });\n location.hash = `#/edit/${post.id}`;\n } catch (err) {\n toast(describeError(err));\n }\n });\n}\n\nexport async function renderDrafts(root: HTMLElement, signal: AbortSignal): Promise<void> {\n // Default sort left empty so the server keeps its scheduled-first order until the\n // reader clicks a column header.\n const state: ListState = {\n status: DRAFTS_SCOPE,\n search: \"\",\n sort: \"\",\n dir: \"desc\",\n offset: 0,\n limit: 50,\n };\n setHtml(\n root,\n html`<div class=\"spread page-head\"><h1>Drafts</h1><button class=\"primary\" id=\"newPost\">New post</button></div>\n ${listToolbar({ statuses: DRAFT_STATUSES, allValue: DRAFTS_SCOPE, searchPlaceholder: \"Search subject\u2026\" })}\n <div id=\"list\" class=\"muted\">Loading\u2026</div>\n <div id=\"postsPager\"></div>`,\n );\n const newPost = $<HTMLButtonElement>(\"#newPost\", root);\n newPost.onclick = () => createNewPost(newPost);\n const listEl = $(\"#list\", root);\n const pagerEl = $(\"#postsPager\", root);\n\n // The posts on screen and their sends' fire times, so a report of a send knows whether it\n // changes this page; and the active sends shown, for a report of one removed.\n let shownFire = new Map<string, number | null>();\n let shownSends = new Set<string>();\n // Loads can overlap (a send's change reloading while the reader sorts or pages), so only\n // the latest paints. A background reload that fails leaves the list the reader is\n // reading; only a load the reader started shows the error.\n let latest = 0;\n const reload = () => load(false);\n async function load(background: boolean): Promise<PostListResponse | null> {\n const mine = ++latest;\n try {\n const data = await api<PostListResponse>(`/api/posts?${listQuery(state)}`, { signal });\n if (mine !== latest) {\n return null;\n }\n const posts = data.posts;\n shownFire = new Map(posts.map((p) => [p.id, p.fire_at]));\n shownSends = new Set(posts.flatMap((p) => (p.active_send_id ? [p.active_send_id] : [])));\n if (!posts.length) {\n // \"Filtered\" = a real narrowing beyond the default drafts scope (a search, or a\n // single-status pick) \u2014 so a fresh, empty list still reads as an invitation.\n const filtered = state.search || (state.status && state.status !== DRAFTS_SCOPE);\n setHtml(\n listEl,\n html`<p class=\"muted\">${filtered ? \"No drafts match.\" : \"No drafts yet. Create your first draft.\"}</p>`,\n );\n setHtml(pagerEl, html``);\n return data;\n }\n // Cells are named (when / updated) and an empty Scheduled cell is marked, so the\n // \u2264720px layout can stack a row and label its dates from CSS alone.\n setHtml(\n listEl,\n html`<div class=\"table-wrap\"><table class=\"list-table posts-table stacks has-actions\"><colgroup><col><col class=\"c-status\"><col class=\"c-date\"><col class=\"c-date\"><col class=\"c-act\"></colgroup><thead><tr>${th(\"Title\", \"title\", state)}${th(\"Status\", null, state)}${th(\"Scheduled\", \"scheduled\", state)}${th(\"Updated\", \"updated\", state)}<th></th></tr></thead><tbody>${posts.map(\n (p) => {\n // A post whose send is in flight is no longer an editable/cancelable draft \u2014\n // show it as `sending` and route it to the live watch, not the editor.\n const sending = p.active_send_status === \"sending\";\n const href = sending ? `#/sent/${p.active_send_id}` : `#/edit/${p.id}`;\n return html`<tr class=\"clickable\" data-id=\"${p.id}\" data-target=\"${href}\"><td><a href=\"${href}\">${p.subject || html`<em>untitled</em>`}</a></td><td>${sending ? badge(\"sending\") : badge(p.status)}</td><td class=\"muted when${p.fire_at ? \"\" : \" empty\"}\">${p.fire_at ? fmt(p.fire_at) : \"\u2014\"}</td><td class=\"muted updated\">${fmt(p.updated_at)}</td><td class=\"act\"><button class=\"icon\" data-menu=\"${p.id}\" data-status=\"${sending ? \"sending\" : p.status}\" data-target=\"${href}\" aria-label=\"Post actions\" aria-haspopup=\"menu\">\u22EF</button></td></tr>`;\n },\n )}</tbody></table></div>`,\n );\n wireSort(listEl, state, reload);\n for (const tr of $$<HTMLTableRowElement>(\"tr[data-id]\", listEl)) {\n tr.onclick = (e) => {\n const t = e.target;\n if (\n t instanceof Element &&\n t.tagName !== \"A\" &&\n !t.closest(\"[data-menu]\") &&\n tr.dataset.target\n ) {\n location.hash = tr.dataset.target;\n }\n };\n }\n for (const b of $$<HTMLButtonElement>(\"[data-menu]\", listEl)) {\n b.onclick = (e) => {\n e.stopPropagation();\n const st = b.dataset.status;\n const target = b.dataset.target ?? \"\";\n const items: MenuItem[] = [\n {\n label: st === \"sending\" ? \"Watch send\" : \"Open\",\n onClick: () => {\n location.hash = target;\n },\n },\n ];\n if (st === \"draft\" && b.dataset.menu) {\n const id = b.dataset.menu;\n items.push({\n label: \"Delete draft\",\n danger: true,\n onClick: () => confirmDelete(id, reload, signal),\n });\n }\n openMenu(b, items, signal);\n };\n }\n renderPager(pagerEl, state, data.page, reload);\n return data;\n } catch (e) {\n if (!background && mine === latest) {\n // Until the list has first answered, Retry is that first read, which starts following.\n renderError(listEl, describeError(e), () => (following ? reload() : void firstLoad()));\n }\n return null;\n }\n }\n // Follow the listed posts' sends from the first read of the list that answers (DESIGN \u00A74):\n // a send that starts or is canceled, or moves, re-reads the list, whichever client made the\n // change, so a post that has gone out stops reading scheduled. A send's progress alone\n // (counts moving while it sends) changes nothing Drafts shows, so it reads nothing.\n let following = false;\n const follow = (first: PostListResponse) => {\n following = true;\n followSends(\n {\n cursor: first.cursor,\n sends: first.posts.flatMap((p) =>\n p.active_send_id && p.active_send_status\n ? [\n {\n id: p.active_send_id,\n status: p.active_send_status,\n phase: p.active_send_status === \"sending\" ? \"progressing\" : \"scheduled\",\n },\n ]\n : [],\n ),\n },\n {\n update({ sends, changes, removed }) {\n const touched =\n changes.some((c) => shownFire.has(c.send.post_id)) ||\n sends.some(\n (s) =>\n s.status === \"scheduled\" &&\n shownFire.has(s.post_id) &&\n shownFire.get(s.post_id) !== s.fire_at,\n ) ||\n removed.some((id) => shownSends.has(id));\n if (touched) {\n void load(true);\n }\n },\n stale: () => mount(renderDrafts),\n },\n signal,\n );\n };\n const firstLoad = async () => {\n const data = await load(false);\n if (data && !following) {\n follow(data);\n }\n };\n wireToolbar(root, state, reload);\n await firstLoad();\n}\n\nfunction confirmDelete(pid: string, reload: () => unknown, signal: AbortSignal): void {\n const m = modal(\n html`<h3>Delete draft?</h3><p class=\"hint\">This permanently deletes the draft and its revisions. This can\u2019t be undone.</p><div class=\"actions\"><button type=\"button\" id=\"dCancel\">Cancel</button><button type=\"button\" class=\"danger\" id=\"dGo\">Delete</button></div>`,\n signal,\n );\n const go = $<HTMLButtonElement>(\"#dGo\", m.el);\n $(\"#dCancel\", m.el).onclick = m.close;\n go.onclick = () =>\n busy(go, \"Deleting\u2026\", async () => {\n m.clearError();\n try {\n await api(`/api/posts/${pid}`, { method: \"DELETE\" });\n m.close();\n toast(\"Draft deleted\");\n reload();\n } catch (e) {\n m.error(describeError(e));\n }\n });\n}\n", "// What a send's view says, shared by the list, the record, and the dashboard: a page's sends\n// as they stand, its conditions and actions, who needs the operator, the Delivered cell,\n// the in-progress card, the scheduled cards' countdowns, and the small number formats.\n\nimport type {\n ConditionKind,\n SendAction,\n SendCondition,\n SendCounts,\n SendView,\n} from \"../../shared/sends\";\nimport { at, every } from \"../lifecycle\";\nimport { $$ } from \"../ui/dom\";\nimport { lateStr, untilStr } from \"../ui/format\";\nimport { type Html, html } from \"../ui/html\";\n\n/** A percentage clamped to 0\u2013100 and rounded. */\nexport const clampPct = (n: number | null | undefined): number =>\n Math.max(0, Math.min(100, Math.round(n || 0)));\n\n/** A rough duration, coarsening with size: \"45s\", \"12 min\", \"3 hr\"; an em dash for none. */\nexport function fmtDuration(ms: number | null | undefined): string {\n if (ms == null || !Number.isFinite(ms) || ms <= 0) {\n return \"\u2014\";\n }\n const s = Math.round(ms / 1000);\n if (s < 90) {\n return `${s}s`;\n }\n const m = Math.round(s / 60);\n if (m < 90) {\n return `${m} min`;\n }\n return `${Math.round(m / 60)} hr`;\n}\n\n/**\n * A scheduled card's countdown cell, from its `GET /api/sends` row: the fire time, whether the\n * server reads the send due, and whether it is past the missed tolerance. Empty until\n * `countdowns` words it.\n */\nexport function countdownHtml(s: SendView): Html {\n return html`<span class=\"countdown\" data-fire=\"${s.fire_at}\"${s.phase === \"due\" && html` data-due`}${has(s, \"missed\") && html` data-missed`}></span>`;\n}\n\n/**\n * The live countdown cells under `root`, ticking once a second for the life of the mount:\n * \"Sends in \u2026\" before the fire time; \"Preparing to send\u2026\" from the fire time (by the clock,\n * or sooner when the server already reads the send due) until the send starts and its card\n * leaves the queue; and, past the server's missed tolerance, how late it is, in the danger\n * tone. A card's window controls (`data-closes`, Cancel and Reschedule) hide at the fire\n * time, when the review window closes (SPEC \u00A76) and the server would refuse them, rather\n * than wait for the next read. A fire time inside the coming second gets a tick of its own\n * at that moment, so the switch lands when the countdown runs out, not up to a second\n * after. Controls hidden this way stay hidden: a card is re-rendered only from a read, whose\n * `actions` decide what it offers. Returns the tick, for a section that re-renders its\n * cards: a fresh cell is empty until the next tick paints it.\n */\nexport function countdowns(root: ParentNode, signal: AbortSignal): () => void {\n let armed = 0; // the moment an extra tick is set for, so a repeated tick sets it once\n const tick = () => {\n const now = Date.now();\n let next = Number.POSITIVE_INFINITY;\n const watch = (moment: number) => {\n if (moment > now && moment < next) {\n next = moment;\n }\n };\n for (const el of $$<HTMLElement>(\"[data-closes]\", root)) {\n const closes = Number(el.dataset.closes);\n el.hidden = now >= closes;\n watch(closes);\n }\n for (const el of $$<HTMLElement>(\"[data-fire]\", root)) {\n const fire = Number(el.dataset.fire);\n const missed = el.dataset.missed !== undefined;\n el.textContent = missed ? lateStr(fire) : untilStr(fire, el.dataset.due !== undefined);\n el.classList.toggle(\"countdown-missed\", missed);\n watch(fire);\n }\n if (next - now <= 1000 && next !== armed) {\n armed = next;\n at(next, tick, signal);\n }\n };\n every(1000, tick, signal);\n return tick;\n}\n\n/** Whether the server reports an open condition of `kind` on the send. */\nexport const has = (s: { conditions: SendCondition[] }, kind: ConditionKind): boolean =>\n s.conditions.some((c) => c.kind === kind);\n\n/** The send's open condition of `kind`, as the server words it, if any. */\nexport function conditionOf<K extends ConditionKind>(\n s: { conditions: SendCondition[] },\n kind: K,\n): Extract<SendCondition, { kind: K }> | undefined {\n return s.conditions.find((c) => c.kind === kind) as\n | Extract<SendCondition, { kind: K }>\n | undefined;\n}\n\n/** Whether the server would take the action on the send now: a control is offered only then. */\nexport const can = (s: { actions: SendAction[] }, name: SendAction[\"name\"]): boolean =>\n s.actions.some((a) => a.name === name);\n\n/**\n * A page's sends as they stand: each row it read, replaced by the layer's latest report of\n * that send once there is one, then every reported send it did not read. The layer reports\n * every change after the page's read, so a report is never older than the row it replaces;\n * both are the one `SendView`, so nothing is converted.\n */\nexport function sendsNow(\n rows: readonly SendView[],\n reported: ReadonlyMap<string, SendView>,\n): SendView[] {\n const listed = new Set(rows.map((r) => r.id));\n return [\n ...rows.map((r) => reported.get(r.id) ?? r),\n ...[...reported.values()].filter((s) => !listed.has(s.id)),\n ];\n}\n\n/**\n * A send that needs the operator rather than patience (SPEC \u00A712): wedged on ambiguous\n * deliveries, which only Resolve can settle without risking a double-mail (I4), or refused\n * by the provider, which only a fix to the account lifts. Its home is the attention block,\n * not the in-progress cards. The server decides both conditions, so every page agrees.\n */\nexport function needsOperator(s: SendView): boolean {\n return has(s, \"wedged\") || has(s, \"refused\");\n}\n\n/** The counts a Delivered cell reads. */\nexport type DeliveredCounts = Pick<SendCounts, \"delivered\" | \"bounced\" | \"complained\" | \"unsent\">;\n\n/**\n * A send's \"Delivered\" cell in the send lists, from its counts: a list row's, or a live send's as its receipts settle. It reports TRUE delivered\n * (webhook-confirmed, not provider-accepted), so the Sent list and the dashboard's Sent\n * table agree with the record view's \"Delivered\" for the same send, and a bounced or\n * complained recipient is never miscounted as delivered. Any bounce, complaint, or unsent\n * shows as a muted delivery-failure note beneath the count, worst first, so a bad send\n * reads as one at a glance. A clean send prints nothing.\n */\nexport function deliveredCell(c: DeliveredCounts): Html {\n const delivered = c.delivered || 0;\n const kinds: string[] = [];\n if (c.complained) {\n kinds.push(`${c.complained.toLocaleString()} complained`);\n }\n if (c.bounced) {\n kinds.push(`${c.bounced.toLocaleString()} bounced`);\n }\n if (c.unsent) {\n kinds.push(`${c.unsent.toLocaleString()} unsent`);\n }\n // Each kind is one unbreakable unit, so a wrap lands between kinds, never inside one.\n const note = kinds.length\n ? html`<span class=\"muted delivered-note\">${kinds.map((text, i) => html`${i ? \", \" : \"\"}<span class=\"delivered-kind\">${text}</span>`)}</span>`\n : null;\n return html`<span class=\"n\">${delivered.toLocaleString()}</span>${note}`;\n}\n\n/**\n * One in-progress send as a card with a live mini dispatch bar, its counts, and a Watch\n * link, from the layer's read. The bar is accepted over the audience, as the watch's\n * dispatch bar is, and the time to finish is the server's, shown only while the send is\n * handing off, never while it is paused. A send in flight too long says so on its card.\n * The whole card opens the watch; the \"Watch\" link is the keyboard/middle-click target.\n */\nexport function activeRowHtml(s: SendView): Html {\n const c = s.counts;\n const accepted = c.accepted + c.delivered + c.bounced + c.complained;\n const confirmed = c.delivered + c.bounced + c.complained;\n const total = s.audience.count;\n const pct = total > 0 ? (100 * accepted) / total : 0;\n // The server gives a time to finish only while the send is handing off.\n const eta = s.dispatch.eta_ms ? ` \u00B7 ~${fmtDuration(s.dispatch.eta_ms)} left` : \"\";\n const stuck = conditionOf(s, \"stuck\");\n return html`<div class=\"card spread clickable active-card\" data-watch=\"${s.id}\">\n <div class=\"active-main\">\n <a class=\"card-link active-subj\" href=\"#/sent/${s.id}\">${s.subject || html`<em>untitled</em>`}</a>\n <div class=\"active-bar\"><div class=\"active-fill\" style=\"width:${clampPct(pct)}%\"></div></div>\n <div class=\"muted active-stat\">Sending: ${accepted.toLocaleString()} of ${total.toLocaleString()} accepted${\n confirmed ? ` \u00B7 ${confirmed.toLocaleString()} confirmed` : \"\"\n }${eta}</div>\n ${stuck ? html`<div class=\"active-stuck\">${stuck.message}</div>` : null}\n </div>\n <a class=\"ghost-link\" href=\"#/sent/${s.id}\">Watch \u2192</a>\n </div>`;\n}\n", "// The subscriber actions taken from more than one surface: adding one (the list, the\n// dashboard's quick actions) and unsubscribing one (the list's row menu, the dashboard).\n\nimport { isValidEmail, normalizeEmail } from \"../../shared/email\";\nimport type { SubscribeAction, SubscribeResponse, Subscriber } from \"../../shared/subscribers\";\nimport { ApiError, api, describeError } from \"../api\";\nimport { notConnectedAddLine, providerNotConnected } from \"../deployment\";\nimport { $ } from \"../ui/dom\";\nimport { html } from \"../ui/html\";\nimport { busy, modal, toast } from \"../ui/widgets\";\n\n/**\n * Add subscriber \u2192 the normal double opt-in (never an auto-confirm). While no provider is\n * connected the confirmation can't go and the server would refuse, so the dialog says so\n * before the publisher types anything: the standing line, no address field, its act\n * disabled (DESIGN \u00A74).\n */\nexport function addSubscriberModal(onDone: () => void, signal: AbortSignal): void {\n const notConnected = providerNotConnected();\n const m = modal(\n html`<h3>Add subscriber</h3><p class=\"hint\">Starts the normal double opt-in: they get a confirmation email and won\u2019t receive posts until they confirm.</p>${notConnectedAddLine()}${notConnected ? null : html`<label for=\"addEmail\">Email address</label><input type=\"email\" id=\"addEmail\" placeholder=\"person@example.com\">`}<div class=\"actions\"><button type=\"button\" id=\"aCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"aGo\">Send confirmation</button></div>`,\n signal,\n );\n const go = $<HTMLButtonElement>(\"#aGo\", m.el);\n $(\"#aCancel\", m.el).onclick = m.close;\n go.disabled = notConnected;\n if (notConnected) {\n $(\"#aCancel\", m.el).focus();\n return;\n }\n const input = $<HTMLInputElement>(\"#addEmail\", m.el);\n input.focus();\n go.onclick = () =>\n busy(go, \"Adding\u2026\", async () => {\n m.clearError();\n const addr = normalizeEmail(input.value);\n if (!isValidEmail(addr)) {\n m.error(\"Enter a valid email address\", input);\n return;\n }\n try {\n const r = await api<SubscribeResponse>(\"/api/subscribers\", {\n method: \"POST\",\n json: { email: addr },\n });\n m.close();\n toast(addedMessage(addr, r.action));\n onDone();\n } catch (e) {\n // A refusal about the address goes under it; anything else, above the buttons.\n m.error(describeError(e), e instanceof ApiError && e.field === \"email\" ? input : undefined);\n }\n });\n}\n\n/** What the Add told the admin: a confirmation went, or why none was due. A refused\n * confirmation is an API error, shown by the caller as it is. */\nfunction addedMessage(addr: string, action: SubscribeAction): string {\n switch (action) {\n case \"already_confirmed\":\n return `${addr} is already confirmed`;\n case \"suppressed\":\n return `${addr} is suppressed, so no confirmation was sent`;\n case \"recently_sent\":\n return `A confirmation went to ${addr} a few minutes ago; try again later`;\n default:\n return `Confirmation sent to ${addr}`;\n }\n}\n\nexport function confirmUnsubscribe(sub: Subscriber, onDone: () => void, signal: AbortSignal): void {\n const m = modal(\n html`<h3>Unsubscribe this subscriber?</h3><p class=\"hint\">Removes <strong>${sub.email}</strong> from the send audience immediately. They can re-subscribe later through the double opt-in.</p><div class=\"actions\"><button type=\"button\" id=\"uCancel\">Cancel</button><button type=\"button\" class=\"danger\" id=\"uGo\">Unsubscribe</button></div>`,\n signal,\n );\n const go = $<HTMLButtonElement>(\"#uGo\", m.el);\n $(\"#uCancel\", m.el).onclick = m.close;\n go.onclick = () =>\n busy(go, \"Unsubscribing\u2026\", async () => {\n m.clearError();\n try {\n await api(`/api/subscribers/${sub.id}/unsubscribe`, { method: \"POST\" });\n m.close();\n toast(`Unsubscribed ${sub.email}`);\n onDone();\n } catch (e) {\n m.error(describeError(e));\n }\n });\n}\n", "// Copying text out of the app, one way everywhere (DESIGN \u00A77): a quiet Copy and its glyph,\n// which says it worked where it sits, the glyph turning to a check for a moment while the\n// word stays, never with a toast. A code block wears it on its label line (`codeBlock`);\n// a single value (an address) wears it beside the value; either copies the plain text.\n\nimport { type Html, html, setHtml } from \"./html\";\nimport { icon } from \"./icons\";\nimport { toast } from \"./widgets\";\n\nconst COPIED_MS = 1500;\n\n/** The copy control's face: its word, then its glyph, a check once a copy has landed. A\n * button whose word says what it copies (\"Copy HTML\") carries it as `data-copy-word`, so\n * the face it returns to after the check says it again. */\nexport function copyFace(copied = false, word = \"Copy\"): Html {\n return html`<span>${word}</span>${icon(copied ? \"check\" : \"copyout\")}`;\n}\n\n/** A Copy that puts `text` on the clipboard when clicked. `label` names what it copies,\n * for a screen reader (\"Copy curl\"). */\nexport function copyButton(text: string, label: string): Html {\n return html`<button type=\"button\" class=\"copy-btn\" data-copy=\"${text}\" aria-label=\"${label}\">${copyFace()}</button>`;\n}\n\n/** A labelled code block with a Copy of its plain text on the label line. `code` is the\n * block's markup (highlighted, say); `raw` is what the clipboard gets. */\nexport function codeBlock(label: string, code: Html, raw: string): Html {\n return html`<div class=\"block\"><div class=\"block-head\"><span class=\"block-label\">${label}</span>${copyButton(raw, `Copy ${label}`)}</div><pre><code>${code}</code></pre></div>`;\n}\n\nconst pending = new WeakMap<HTMLElement, { label: string; timer: number }>();\n\n/** Copy `text` and say so on `button`: the check, and \"Copied\" as its accessible name,\n * for a moment; a second copy restarts the moment. A clipboard the browser refuses is\n * the one thing said elsewhere, since the button can't show it. */\nexport async function copyWith(button: HTMLElement, text: string): Promise<void> {\n try {\n await navigator.clipboard.writeText(text);\n } catch {\n toast(\"Couldn\u2019t copy to clipboard\");\n return;\n }\n const prior = pending.get(button);\n const label = prior?.label ?? button.getAttribute(\"aria-label\") ?? \"Copy\";\n const word = button.dataset.copyWord;\n window.clearTimeout(prior?.timer);\n setHtml(button, copyFace(true, word));\n button.setAttribute(\"aria-label\", \"Copied\");\n button.classList.add(\"copied\");\n const timer = window.setTimeout(() => {\n setHtml(button, copyFace(false, word));\n button.setAttribute(\"aria-label\", label);\n button.classList.remove(\"copied\");\n pending.delete(button);\n }, COPIED_MS);\n pending.set(button, { label, timer });\n}\n\n/** Every Copy under `root` (a `[data-copy]` button) copies its text when clicked: one\n * listener on the root, so a block painted later is covered too. */\nexport function wireCopy(root: HTMLElement): void {\n root.addEventListener(\"click\", (ev) => {\n const b = ev.target instanceof Element ? ev.target.closest<HTMLElement>(\"[data-copy]\") : null;\n if (b && root.contains(b)) {\n void copyWith(b, b.dataset.copy ?? \"\");\n }\n });\n}\n", "// The dashboard (home): counts, the active send widget, drafts and sent tables, the\n// setup checklist, and quick actions.\n\nimport type { PostListItem, PostListResponse } from \"../../shared/posts\";\nimport type { SendListResponse, SendView } from \"../../shared/sends\";\nimport type { DeploymentView } from \"../../shared/settings\";\nimport type { SubscriberCounts, SubscriberListResponse } from \"../../shared/subscribers\";\nimport { api, describeError } from \"../api\";\nimport { derivePublication, type Publication } from \"../brand\";\nimport {\n CONNECT_PROVIDER_HREF,\n connectProviderLink,\n heldSendsHtml,\n heldWhenFiresHtml,\n providerNotConnected,\n} from \"../deployment\";\nimport { mount } from \"../lifecycle\";\nimport { createNewPost } from \"../posts/drafts\";\nimport { followSends, type SendStage, type SendsUpdate, stageOf } from \"../send_state\";\nimport {\n activeRowHtml,\n conditionOf,\n countdownHtml,\n countdowns,\n deliveredCell,\n has,\n needsOperator,\n sendsNow,\n} from \"../sends/progress\";\nimport { appState } from \"../state\";\nimport { addSubscriberModal } from \"../subscribers/dialogs\";\nimport { copyButton, copyFace, wireCopy } from \"../ui/copy\";\nimport { $, $$ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { badge, renderError } from \"../ui/widgets\";\n\n// The post-login landing and the brand's target (the default route). Built entirely\n// from existing authed endpoints \u2014 GET /api/posts, /sends, /subscribers, and the cached\n// /api/settings \u2014 so it adds no surface and can't touch an invariant. It answers\n// SPEC \u00A78's questions at a glance: is anything wrong, who's on the list, what's\n// scheduled, what went out, and what's still in progress.\n\n/** One line of the health block: red needs a decision, amber a look. */\ninterface HealthAlert {\n level: \"red\" | \"amber\";\n text: string | Html;\n}\n\n// Health (SPEC \u00A78 \"is anything wrong\", \u00A712 loud failure): calm in the common case, loud\n// only when something needs attention. Every line is a condition the server reports on a\n// send as it stands (the page's rows, overlaid by the layer's reports), so a line appears\n// within a read of its condition and stays until the condition clears, and the dashboard\n// keeps no rule of its own: the bounce spike included, which the server reads off each\n// recently sent send. Only the loud kinds are lines here; a re-made send's note and a\n// provider outage short of the stuck threshold are shown where the send is. The one line\n// not about a send is the deployment's: no email provider connected (SPEC \u00A79), an amber\n// standing state with no time, read from the deployment reflection, gone once one is set.\n// When that state is holding a send, the two are said as one red line instead of two.\nfunction computeHealth(live: SendView[]): HealthAlert[] {\n const alerts: HealthAlert[] = [];\n // Missed only past the server's tolerance: an ordinary slow tick is never a miss (\u00A712).\n const missed = live.filter((s) => has(s, \"missed\"));\n if (missed.length) {\n alerts.push({\n level: \"red\",\n text: `${missed.length} scheduled send${missed.length === 1 ? \"\" : \"s\"} passed the fire time without going out.`,\n });\n }\n // A send wedged on ambiguous in-flight rows needs a decision, not just patience: red,\n // linking to where Resolve is (its page; the Sent page for several), and kept out of the\n // in-progress lines below so it isn't reported twice (SPEC \u00A712).\n const wedged = live.flatMap((s) => {\n const c = conditionOf(s, \"wedged\");\n return c ? [{ s, count: c.count }] : [];\n });\n const [firstWedged] = wedged;\n if (firstWedged) {\n const n = wedged.reduce((sum, w) => sum + w.count, 0);\n const noun = n === 1 ? \"delivery\" : \"deliveries\";\n alerts.push({\n level: \"red\",\n text:\n wedged.length === 1\n ? html`<a href=\"#/sent/${firstWedged.s.id}\">${firstWedged.s.subject}</a> has ${n} ambiguous ${noun} awaiting a decision; resolve ${n === 1 ? \"it\" : \"them\"} on its page.`\n : html`${n} ambiguous ${noun} across <a href=\"#/sent\">${wedged.length} sends</a> await a decision; resolve them on the Sent page.`,\n });\n }\n // The provider refusing the account stops every send it touches until the operator\n // fixes the account (SPEC \u00A712): one red line, in the server's words for it.\n // A send held for want of a provider (SPEC \u00A79) is the deployment's state showing on a\n // send, so while the deployment is not connected the two are one line: the state, the\n // held sends, and the fix once, as a link. Red, since a send is waiting on the operator.\n const notConnected = providerNotConnected();\n const allRefused = live.flatMap((s) => {\n const c = conditionOf(s, \"refused\");\n return c ? [{ s, c }] : [];\n });\n const held = notConnected ? allRefused.filter((r) => r.c.cause === \"not_connected\") : [];\n const refused = allRefused.filter((r) => !held.includes(r));\n // One send links to its watch; several, to the Sent page that lists them all.\n const which = (sends: { s: SendView }[]) =>\n sends.length === 1 && sends[0]\n ? html`<a href=\"#/sent/${sends[0].s.id}\">${sends[0].s.subject}</a>`\n : html`<a href=\"#/sent\">${sends.length} sends</a>`;\n if (held.length) {\n alerts.push({\n level: \"red\",\n text: html`<span class=\"dash-not-connected\">${heldSendsHtml(which(held), held.length)}</span>`,\n });\n }\n const [firstRefused] = refused;\n if (firstRefused) {\n alerts.push({ level: \"red\", text: html`${which(refused)}: ${firstRefused.c.message}` });\n }\n // A healthy in-progress send is NOT surfaced here: the active-send widget below is its\n // home (a bar and a Watch link). The health line is loud-only, so it keeps just the\n // *stuck* case: a send the server reports in flight too long.\n const stuck = live.filter((s) => has(s, \"stuck\") && !needsOperator(s));\n const [firstStuck] = stuck;\n if (firstStuck) {\n alerts.push({\n level: \"amber\",\n text:\n stuck.length === 1\n ? html`<a href=\"#/sent/${firstStuck.id}\">${firstStuck.subject}</a>: ${conditionOf(firstStuck, \"stuck\")?.message}`\n : html`<a href=\"#/sent\">${stuck.length} sends</a> have been sending longer than they should; they may be retrying.`,\n });\n }\n // Bounce spike (SPEC \u00A78): the server's reading of a recent send's confirmed bounces over\n // its audience at fire. Read-only reporting: it never throttles or halts a send.\n const spiky = live.flatMap((s) => {\n const c = conditionOf(s, \"bounce_spike\");\n return c ? [{ s, c }] : [];\n });\n const [firstSpike] = spiky;\n if (firstSpike) {\n alerts.push({\n level: \"amber\",\n text: html`<a href=\"#/sent/${firstSpike.s.id}\">${firstSpike.s.subject}</a>: ${firstSpike.c.message}`,\n });\n }\n if (notConnected && !held.length) {\n alerts.push({\n level: \"amber\",\n text: html`<span class=\"dash-not-connected\">No email provider connected, so nothing can be sent yet. ${connectProviderLink()} to start sending.</span>`,\n });\n }\n return alerts;\n}\n\n/** The health block: nothing at all when nothing needs attention. */\nfunction healthHtml(alerts: HealthAlert[]): Html {\n if (!alerts.length) {\n return html``;\n }\n const level = alerts.some((i) => i.level === \"red\") ? \"red\" : \"amber\";\n return html`<div class=\"health ${level}\"><span class=\"health-dot\">\u26A0\uFE0F</span><div>${alerts.map(\n (i) => html`<div>${i.text}</div>`,\n )}</div></div>`;\n}\n\n/** The sends the active-send widget shows: in flight, and not needing the operator (that\n * one's home is its red line). */\nconst activeOf = (live: SendView[]) =>\n live.filter((s) => s.status === \"sending\" && !needsOperator(s));\n\n/** A subscriber-count tile; each deep-links into the roster on its own filter. */\ninterface Tile {\n label: string;\n emph?: boolean;\n v: number;\n filter: string;\n sub?: string;\n}\n\nexport async function renderDashboard(view: HTMLElement, signal: AbortSignal): Promise<void> {\n const remount = () => {\n if (!signal.aborted) {\n mount(renderDashboard); // never over wherever the reader went since\n }\n };\n setHtml(view, html`<div class=\"dash\" id=\"dash\"><p class=\"muted\">Loading\u2026</p></div>`);\n const root = $(\"#dash\", view);\n let posts: PostListItem[];\n let first: SendListResponse;\n let counts: SubscriberCounts;\n try {\n // The health line scans every send and the archive-link slug map needs every post,\n // so ask for a full window rather than the list default (50). Subscribers is only\n // read for its (filter-independent) counts, so its row limit doesn't matter.\n const [p, s, subs] = await Promise.all([\n api<PostListResponse>(\"/api/posts?limit=200\", { signal }),\n api<SendListResponse>(\"/api/sends?limit=200\", { signal }),\n api<SubscriberListResponse>(\"/api/subscribers\", { signal }),\n ]);\n posts = p.posts;\n first = s;\n counts = subs.counts;\n } catch (e) {\n renderError(root, describeError(e), remount);\n return;\n }\n let sends = first.sends;\n // Every send the layer has reported since the page's read, as it now stands.\n const reported = new Map<string, SendView>();\n const pub = derivePublication(appState.appConfig);\n const deployment = appState.appConfig?.deployment ?? null;\n const totalSubs = counts.confirmed + counts.pending + counts.unsubscribed + counts.suppressed;\n\n // First run \u2014 nothing written and no one on the list: replace the body with the\n // onboarding checklist (the shared Getting-started component) rather than a wall\n // of empty tiles.\n if (!posts.length && totalSubs === 0) {\n setHtml(\n root,\n html`<div class=\"dash-head\"><div><h1>${pub.name}</h1>${\n pub.tagline ? html`<p class=\"muted dash-tagline\">${pub.tagline}</p>` : null\n }<p class=\"muted\">Let\u2019s get your first post out the door.</p></div></div>${setupChecklistHtml(pub, deployment)}<section class=\"dash-section\"><h2>API access</h2>${apiConnectCard(false)}</section>`,\n );\n wireDashActions(root, remount, signal);\n return;\n }\n\n // Each tile deep-links into the roster pre-filtered on its criterion\n // (#/subscribers/<filter>), so a count is a way in, not just a number.\n const tiles: Tile[] = [\n {\n label: \"Confirmed\",\n emph: true,\n v: counts.confirmed,\n filter: \"confirmed\",\n },\n { label: \"Pending\", v: counts.pending, filter: \"pending\" },\n { label: \"Unsubscribed\", v: counts.unsubscribed, filter: \"unsubscribed\" },\n { label: \"Suppressed\", v: counts.suppressed, filter: \"suppressed\" },\n ];\n const tilesHtml = html`<div class=\"tiles\">${tiles.map(\n (t) =>\n html`<a class=\"tile${t.emph ? \" tile-emph\" : \"\"}\" href=\"#/subscribers/${t.filter}\"><span class=\"tile-n\">${t.v}</span><span class=\"tile-label\">${t.label}${\n t.sub ? html`<span class=\"tile-sub\">${t.sub}</span>` : null\n }</span></a>`,\n )}</div>`;\n\n const drafts = posts.filter((p) => p.status === \"draft\").slice(0, 5);\n const draftsHtml = drafts.length\n ? html`<div class=\"table-wrap\"><table><tbody>${drafts.map(\n (p) =>\n html`<tr class=\"clickable\" data-id=\"${p.id}\"><td><a href=\"#/edit/${p.id}\">${p.subject || html`<em>untitled</em>`}</a></td><td class=\"muted\">edited ${fmt(p.updated_at)}</td></tr>`,\n )}</tbody></table></div>`\n : html`<p class=\"muted\">No drafts in progress.</p>`;\n\n // The publication's home is the landing page at the app's own origin (SPEC \u00A711); the\n // archive and every post are a click from it, so the card names the one address to share.\n const homeUrl = deployment?.appOrigin || location.origin;\n const pubCardHtml = html`<div class=\"card pub-card\">\n <div class=\"pub-row\"><code class=\"pub-val\">${homeUrl}</code>${copyButton(homeUrl, \"Copy the publication\u2019s address\")}</div>\n <div class=\"pub-foot\"><a href=\"/\" target=\"_blank\" rel=\"noopener\">View publication \u2197</a></div>\n </div>`;\n\n // Has Claude (the `service` principal) edited here? Authorship on any current revision\n // flips the API-access card to \"connected\" (SPEC \u00A74). Uses the author already on each\n // list row, so no extra fetch; a post a human later re-edited no longer counts.\n const claudeConnected = posts.some((p) => p.author === \"service\");\n const apiCardHtml = apiConnectCard(claudeConnected);\n\n const quickHtml = html`<div class=\"row quick-actions\"><button data-act=\"new-post\">New post</button><button data-act=\"add-sub\">Add subscriber</button><button data-nav=\"#/settings\">Edit publication</button></div>`;\n\n // No news is good news: the health block appears only when something needs attention\n // (SPEC \u00A78 / \u00A712, the only thing that ever surfaces loudly). The active-send widget sits\n // with it, a glanceable entry into the watch while a send is in flight.\n setHtml(\n root,\n html`\n <div class=\"dash-head\">\n <div><h1>${pub.name}</h1>${pub.tagline ? html`<p class=\"muted dash-tagline\">${pub.tagline}</p>` : null}</div>\n <button class=\"primary\" data-act=\"new-post\">New post</button>\n </div>\n <div id=\"dashHealth\">${healthHtml(computeHealth(sendsNow(sends, reported)))}</div>\n <div id=\"dashActive\">${dashActiveHtml(activeOf(sendsNow(sends, reported)))}</div>\n <section class=\"dash-section\"><h2>Subscribers</h2>${tilesHtml}</section>\n <div class=\"dash-cols\">\n <section class=\"dash-section\"><h2>Scheduled</h2><div id=\"dashScheduled\">${dashScheduledHtml(scheduledOf(sends))}</div></section>\n <section class=\"dash-section\"><h2>Drafts</h2>${draftsHtml}</section>\n </div>\n <section class=\"dash-section\"><h2>Sent</h2><div id=\"dashSent\">${dashSentHtml(sends)}</div></section>\n <section class=\"dash-section\"><h2>Quick actions</h2>${quickHtml}</section>\n <div class=\"dash-cols\">\n <section class=\"dash-section\"><h2>Publication</h2>${pubCardHtml}</section>\n <section class=\"dash-section\"><h2>API access</h2>${apiCardHtml}</section>\n </div>`,\n );\n\n wireDashActions(root, remount, signal);\n // Row / card clicks open the post (subject links + Cancel opt out \u2014 the same guard\n // the Posts table and the Sends cards use).\n for (const tr of $$<HTMLTableRowElement>(\"tr[data-id]\", root)) {\n tr.onclick = (e) => {\n const t = e.target;\n if (t instanceof Element && t.tagName !== \"A\" && !t.closest(\"button\")) {\n location.hash = `#/edit/${tr.dataset.id}`;\n }\n };\n }\n wireDashSentRows(root);\n wireDashActiveCards(root);\n wireDashScheduledCards(root);\n const tickCountdowns = countdowns(root, signal);\n tickCountdowns();\n\n // From here the send-state layer keeps the send sections current (docs/DESIGN.md \u00A74),\n // following every send from where the page's own read stood. Each report repaints what it\n // carries directly (the health block, the active-send widget, a listed send's Delivered).\n // A stage change, or any change to a scheduled or due send (a cancel, a move, a re-make,\n // a new schedule, the fire time passing, a miss), reads the sends again for the queue,\n // and the Sent table, whoever made it: a send that went due to sent between\n // two reads lands straight in the Sent table, and the other client's move shows within\n // one idle read.\n let reading = 0;\n const readSends = async () => {\n const mine = ++reading;\n try {\n const fresh = await api<SendListResponse>(\"/api/sends?limit=200\", { signal });\n if (mine !== reading) {\n return; // a later change's read is the one to paint\n }\n sends = fresh.sends;\n setHtml($(\"#dashScheduled\", root), dashScheduledHtml(scheduledOf(sends)));\n wireDashScheduledCards(root);\n tickCountdowns(); // the fresh cards are empty until the next tick\n setHtml($(\"#dashSent\", root), dashSentHtml(sends));\n wireDashSentRows(root);\n paintLive();\n } catch {\n /* a background read: the sections keep their last render until the next change */\n }\n };\n const paintLive = () => {\n const now = sendsNow(sends, reported);\n setHtml($(\"#dashHealth\", root), healthHtml(computeHealth(now)));\n setHtml($(\"#dashActive\", root), dashActiveHtml(activeOf(now)));\n wireDashActiveCards(root);\n patchDelivered(root, [...reported.values()]);\n };\n const inQueue = (st: SendStage | null) => st === \"scheduled\" || st === \"due\";\n followSends(\n first,\n {\n update: (u: SendsUpdate) => {\n for (const s of u.sends) {\n reported.set(s.id, s);\n }\n for (const id of u.removed) {\n reported.delete(id);\n }\n paintLive();\n if (u.changes.length || u.removed.length || u.sends.some((s) => inQueue(stageOf(s)))) {\n readSends();\n }\n },\n // The database is behind the page's read (a reset or a restore): read it all again.\n stale: remount,\n },\n signal,\n );\n}\n\n/** The scheduled sends, soonest first. */\nconst scheduledOf = (sends: SendView[]) =>\n sends.filter((s) => s.status === \"scheduled\").sort((a, b) => a.fire_at - b.fire_at);\n\n/** The dashboard's Sent table: the latest five sends that have started, each opening its\n * page (the watch while it sends, the record once sent). */\nfunction dashSentHtml(sends: SendView[]): Html {\n const recent = sends.filter((s) => s.status === \"sent\" || s.status === \"sending\").slice(0, 5);\n if (!recent.length) {\n return html`<p class=\"muted\">No sends yet.</p>`;\n }\n return html`<div class=\"table-wrap\"><table><thead><tr><th>Subject</th><th>Status</th><th class=\"num\">Recipients</th><th class=\"num\">Delivered</th><th></th></tr></thead><tbody>${recent.map(\n (s) => {\n // The published post, which the send's view links once it has started.\n const url = s.links.archive;\n return html`<tr class=\"clickable\" data-send=\"${s.id}\"><td><a href=\"#/sent/${s.id}\">${s.subject}</a></td><td>${badge(s.status)}</td><td class=\"num\">${s.audience.count.toLocaleString()}</td><td class=\"num delivered\">${deliveredCell(s.counts)}</td><td class=\"act\">${\n url\n ? html`<a class=\"ghost-link\" href=\"${url}\" target=\"_blank\" rel=\"noopener\">Archive \u2197</a>`\n : null\n }</td></tr>`;\n },\n )}</tbody></table></div>`;\n}\n// Sent-table rows carry a SEND id (not a post id) and open the send's page.\nfunction wireDashSentRows(root: HTMLElement): void {\n for (const tr of $$<HTMLTableRowElement>(\"tr[data-send]\", root)) {\n tr.onclick = (e) => {\n const t = e.target;\n if (t instanceof Element && t.tagName !== \"A\") {\n location.hash = `#/sent/${tr.dataset.send}`;\n }\n };\n }\n}\n/** A listed send still sending or settling: its Delivered cell follows the layer's counts,\n * so the table moves with its receipts without being read again. */\nfunction patchDelivered(root: HTMLElement, sends: SendView[]): void {\n for (const s of sends) {\n const cell = $$<HTMLTableRowElement>(\"#dashSent tr[data-send]\", root)\n .find((tr) => tr.dataset.send === s.id)\n ?.querySelector(\"td.delivered\");\n if (cell) {\n setHtml(cell, deliveredCell(s.counts));\n }\n }\n}\n\n/** The dashboard's scheduled cards are read-only summaries: the whole card links into the\n * editor, where the schedule is actually managed. A send the server reports as re-made\n * since its last test says so, and the editor, where the test is, says what to do. The\n * Sent page keeps the one-call cancel the review window needs (SPEC \u00A78). */\nfunction dashScheduledHtml(scheduled: SendView[]): Html {\n if (!scheduled.length) {\n return html`<p class=\"muted\">Nothing scheduled.</p>`;\n }\n const held = heldWhenFiresHtml();\n return html`${scheduled.map(\n (s) =>\n html`<div class=\"card spread clickable nextup sched-card\" data-post=\"${s.post_id}\"><div><a class=\"card-link sched-subj\" href=\"#/edit/${s.post_id}\">${s.subject}</a><div class=\"muted\">${countdownHtml(s)} \u00B7 ${fmt(s.fire_at)} \u00B7 ${s.audience.count} recipients</div>${held}${has(s, \"remade\") ? html`<div class=\"sched-remade-mark\">Look changed since your last test</div>` : null}</div></div>`,\n )}`;\n}\nfunction wireDashScheduledCards(root: HTMLElement): void {\n for (const card of $$(\"#dashScheduled .nextup\", root)) {\n card.onclick = (e) => {\n const t = e.target;\n if (t instanceof Element && t.tagName !== \"A\") {\n location.hash = `#/edit/${card.dataset.post}`;\n }\n };\n }\n}\n\n/** The dashboard active-send section (nothing when nothing is in flight). */\nfunction dashActiveHtml(active: SendView[]): Html {\n if (!active.length) {\n return html``;\n }\n return html`<section class=\"dash-section\"><h2>Active send${active.length === 1 ? \"\" : \"s\"}</h2>${active.map(\n activeRowHtml,\n )}</section>`;\n}\nfunction wireDashActiveCards(root: HTMLElement): void {\n for (const card of $$(\"#dashActive .active-card[data-watch]\", root)) {\n card.onclick = (e) => {\n const t = e.target;\n if (t instanceof Element && t.tagName !== \"A\") {\n location.hash = `#/sent/${card.dataset.watch}`;\n }\n };\n }\n}\n\n// Controls shared by the Dashboard and the Getting-started view: hash navigation,\n// \"New post\", \"Add subscriber\", and copy buttons.\nfunction wireDashActions(root: HTMLElement, reload: () => void, signal: AbortSignal): void {\n for (const b of $$(\"[data-nav]\", root)) {\n b.onclick = () => {\n location.hash = b.dataset.nav ?? \"\";\n };\n }\n for (const b of $$<HTMLButtonElement>(\"[data-act='new-post']\", root)) {\n b.onclick = () => createNewPost(b);\n }\n for (const b of $$(\"[data-act='add-sub']\", root)) {\n b.onclick = () => addSubscriberModal(reload, signal);\n }\n wireCopy(root);\n}\n\n// The onboarding checklist, shared by the first-run dashboard and Getting-started.\n// The \"API access\" card. Two states: an invitation to connect Claude, or \u2014 once Claude\n// (the `service` principal, SPEC \u00A74) has edited here \u2014 a plain \"Claude is connected\" note.\n// Shared by the populated dashboard and the first-run state (a card below the setup\n// checklist), so the two can't drift; connecting an agent is optional, so this is never a\n// required setup step. Both states have one shape \u2014 a line, then the card's links stacked\n// at one weight, each led by a glyph for what it is (the guide, the reference) \u2014 because\n// neither link is an action: connecting happens outside the app (an Access service token),\n// so nothing here may look like a button that would do it. Typography-led with no base-URL\n// field: the operator already knows their own origin (it's the Publication card's URL right\n// beside this one), and the connect guide is where that URL is actually used.\nfunction apiConnectCard(connected: boolean): Html {\n if (connected) {\n return html`<div class=\"card pub-card\">\n <p class=\"conn-status\"><span class=\"conn-dot\" aria-hidden=\"true\"></span>Claude is connected.</p>\n <div class=\"pub-foot pub-links\"><a href=\"#/reference\">${icon(\"code\")}API reference \u2192</a><a href=\"#/docs/connect-claude\">${icon(\"article\")}Connection guide \u2192</a></div>\n </div>`;\n }\n return html`<div class=\"card pub-card\">\n <p class=\"pub-note\">Let Claude draft, proofread, and schedule your posts.</p>\n <div class=\"pub-foot pub-links\"><a href=\"#/docs/connect-claude\">${icon(\"article\")}Connect Claude \u2192</a><a href=\"#/reference\">${icon(\"code\")}API reference \u2192</a></div>\n </div>`;\n}\n\n/**\n * The first-run checklist, in the order the steps depend on each other. Writing a post\n * comes late because its New post creates a draft, which ends the first-run page; sharing\n * comes last so a visitor finds a post in the archive. While no provider is connected,\n * connecting one replaces the sending-domain step (connecting SES verifies the domain) and\n * the subscribe step says signups wait for it (the page is closed).\n */\nfunction setupChecklistHtml(pub: Publication, deployment: DeploymentView | null): Html {\n const subscribeUrl = `${deployment?.appOrigin || location.origin}/subscribe`;\n const notConnected = providerNotConnected();\n return html`<div class=\"card setup\">\n <h2 class=\"setup-title\">Set up your publication</h2>\n <ol class=\"setup-steps\">\n <li><div class=\"setup-step-main\"><strong>Name your publication</strong><span class=\"muted\">Currently \u201C${pub.name}\u201D. Set the name, tagline, and brand in Settings.</span></div><button data-nav=\"#/settings\">Settings</button></li>\n ${\n notConnected\n ? html`<li class=\"setup-connect\"><div class=\"setup-step-main\"><strong>Connect Amazon SES</strong><span class=\"muted\">Nothing can be sent, and signups stay closed, until SES is connected.</span></div><button data-nav=\"${CONNECT_PROVIDER_HREF}\">Connect Amazon SES</button></li>`\n : html`<li><div class=\"setup-step-main\"><strong>Confirm your sending domain</strong><span class=\"muted\">SPF, DKIM, and DMARC on your From address. The setup guide walks through it.</span></div><button data-nav=\"#/docs/sending-domain-dns\">Docs</button></li>`\n }\n <li><div class=\"setup-step-main\"><strong>Write your first post</strong><span class=\"muted\">Draft a post in Markdown and preview it exactly as the email.</span></div><button class=\"primary\" data-act=\"new-post\">New post</button></li>\n ${\n notConnected\n ? html`<li class=\"setup-subscribe\"><div class=\"setup-step-main\"><strong>Share your subscribe link</strong><span class=\"muted\">Signups open once an email provider is connected; until then the page says they aren\u2019t open yet.</span></div></li>`\n : html`<li class=\"setup-subscribe\"><div class=\"setup-step-main\"><strong>Share your subscribe link</strong><code class=\"setup-url\">${subscribeUrl}</code></div><button type=\"button\" data-copy=\"${subscribeUrl}\" aria-label=\"Copy the subscribe link\">${copyFace()}</button></li>`\n }\n </ol>\n </div>`;\n}\n", "// The send actions a publisher takes from more than one surface: resolving a wedged send\n// (the list, the watch) and rescheduling (the list's scheduled card, the editor's banner).\n\nimport type { ResolveResponse, SendSummary, StuckResolution } from \"../../shared/sends\";\nimport { ApiError, api, describeError } from \"../api\";\nimport { earliestFireAt, isTooSoon, notConnectedRescheduleLine, tooSoonText } from \"../deployment\";\nimport { readSendsNow } from \"../send_state\";\nimport { $ } from \"../ui/dom\";\nimport { toLocalInput } from \"../ui/format\";\nimport { html } from \"../ui/html\";\nimport { busy, modal, toast } from \"../ui/widgets\";\n\nconst message = describeError;\n\n/**\n * The one manual step for a wedged send: decide whether the ambiguous batch went out or\n * not. Both outcomes are safe for I4 (neither re-mails this post), so the modal explains\n * the trade-off (record accuracy) rather than warning of a double-send.\n */\nexport function openResolveModal(\n send: Pick<SendSummary, \"id\" | \"c_in_flight\">,\n reload: () => void,\n signal: AbortSignal,\n): void {\n const n = send.c_in_flight || 0;\n const noun = n === 1 ? \"delivery\" : \"deliveries\";\n const m = modal(\n html`<h3>Resolve ${n} ambiguous ${noun}</h3>\n <p class=\"hint\">A transport error left ${n} recipient${n === 1 ? \"\" : \"s\"} in flight: the request went out but the provider never confirmed, so there\u2019s no way to know whether it was accepted. To avoid mailing anyone twice, the send won\u2019t retry ${n === 1 ? \"it\" : \"them\"} on its own, so it can\u2019t finish until you decide. Neither choice re-sends this post.</p>\n <p class=\"hint\">Kestrel can\u2019t tell whether ${n === 1 ? \"this email\" : \"these emails\"} went out, so unless you know, <strong>Assume not sent</strong> is the safe default.</p>\n <p class=\"hint\"><strong>Assume not sent</strong>: recorded as unsent. ${n === 1 ? \"That reader\" : \"Those readers\"} may miss this post but ${n === 1 ? \"gets\" : \"get\"} your next one, and no one is mailed twice.</p>\n <p class=\"hint\"><strong>Assume sent</strong>: recorded as accepted by your provider, with delivery still waiting on its receipt. Choose this when you know otherwise, such as a reader who says they got it.</p>\n <div class=\"actions\"><button type=\"button\" id=\"rCancel\">Cancel</button><button type=\"button\" id=\"rUnsent\">Assume not sent</button><button type=\"button\" class=\"primary\" id=\"rAccepted\">Assume sent</button></div>`,\n signal,\n );\n $(\"#rCancel\", m.el).onclick = m.close;\n const doResolve = (btn: HTMLButtonElement, resolution: StuckResolution, verb: string) =>\n busy(btn, \"Resolving\u2026\", async () => {\n m.clearError();\n try {\n const res = await api<ResolveResponse>(`/api/sends/${send.id}/resolve`, {\n method: \"POST\",\n json: { resolution },\n });\n m.close();\n toast(res.completed ? \"Send completed\" : `Marked ${verb}`);\n reload();\n } catch (e) {\n m.error(message(e));\n }\n });\n const unsent = $<HTMLButtonElement>(\"#rUnsent\", m.el);\n const accepted = $<HTMLButtonElement>(\"#rAccepted\", m.el);\n unsent.onclick = () => doResolve(unsent, \"unsent\", \"not sent\");\n accepted.onclick = () => doResolve(accepted, \"accepted\", \"sent\");\n}\n\n/**\n * Move a scheduled send's fire time without canceling or re-editing: the content stays\n * frozen (I3) and the cancelable review window is preserved (I6); only fire_at moves,\n * via POST /api/sends/:id/reschedule (SPEC \u00A76). The same datetime picker as the Schedule\n * modal, prefilled with the current fire time and floored at the minimum lead. Shared\n * by the editor's scheduled banner and the Sent page's scheduled card (SPEC \u00A78), so\n * `onDone` re-renders whichever surface opened it.\n */\nexport function openRescheduleModal(\n sendId: string,\n currentFireAt: number,\n onDone: () => void,\n signal: AbortSignal,\n): void {\n const minStr = toLocalInput(earliestFireAt());\n const cur = toLocalInput(new Date(currentFireAt));\n const m = modal(\n html`<h3 id=\"rsHead\">Reschedule this post</h3>\n <p class=\"hint\">Move when it sends. The content stays frozen and the cancelable window is kept. Only the time changes.</p>\n ${notConnectedRescheduleLine()}\n <label for=\"rsWhen\">Send at</label><input type=\"datetime-local\" id=\"rsWhen\" min=\"${minStr}\" value=\"${cur}\">\n <div class=\"actions\"><button type=\"button\" id=\"rsCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"rsGo\">Reschedule</button></div>`,\n signal,\n );\n const box = $(\".modal\", m.el);\n box.setAttribute(\"aria-labelledby\", \"rsHead\");\n const go = $<HTMLButtonElement>(\"#rsGo\", box);\n const when = $<HTMLInputElement>(\"#rsWhen\", box);\n $(\"#rsCancel\", box).onclick = m.close;\n go.onclick = () =>\n busy(go, \"Rescheduling\u2026\", async () => {\n m.clearError();\n const v = when.value;\n const t = v ? new Date(v).getTime() : Number.NaN;\n if (Number.isNaN(t)) {\n m.error(\"Pick a valid date & time\", when);\n return;\n }\n if (isTooSoon(t)) {\n m.error(tooSoonText(), when);\n return;\n }\n try {\n await api(`/api/sends/${sendId}/reschedule`, {\n method: \"POST\",\n json: { fire_at: new Date(t).toISOString() },\n });\n m.close();\n toast(\"Rescheduled\");\n onDone();\n } catch (e) {\n if (e instanceof ApiError && e.status === 404) {\n // The send went with its post. Say so in words (the API names only \"send\"), and\n // read the feed now so the page showing it follows: the editor goes to Drafts with\n // the same message, which shows as one toast.\n m.close();\n toast(\"This post was deleted elsewhere.\");\n readSendsNow();\n return;\n }\n if (e instanceof ApiError && e.field === \"fire_at\") {\n m.error(message(e), when);\n return;\n }\n m.error(message(e));\n }\n });\n when.focus();\n}\n", "// The one content security policy for a page carrying a post's HTML (SPEC \u00A75), shared by\n// the Worker, which sends it as a header on every page it serves, and the editor, which\n// restates it inside a framed copy of such a page, so the two can't drift.\n//\n// What the pages actually load: inline styles (the email's own, the page chrome's\n// <style>), the display font from Google Fonts, and images from anywhere a post may\n// point (the media origin, which may be a separate public bucket domain, or any image the\n// author linked). Nothing else, and no script, frame, plugin, or base rewrite.\n\n/**\n * The policy, as a header value. `formAction` is the one difference between the kinds of\n * page (a post's page submits no form at all; a page the app wrote posts back to itself).\n * `framed` is the copy the editor frames, carried in a <meta> tag: it leaves out\n * `frame-ancestors`, which a <meta> can't carry.\n */\nexport function pagePolicy(formAction: string, opts: { framed?: boolean } = {}): string {\n return [\n \"default-src 'none'\",\n \"script-src 'none'\",\n \"object-src 'none'\",\n \"frame-src 'none'\",\n \"base-uri 'none'\",\n `form-action ${formAction}`,\n opts.framed ? null : \"frame-ancestors 'none'\",\n \"img-src * data:\",\n \"style-src 'unsafe-inline' https://fonts.googleapis.com\",\n \"font-src https://fonts.gstatic.com\",\n ]\n .filter((d) => d !== null)\n .join(\"; \");\n}\n", "// A framed email or page in the editor: the post's Preview tab, its web version, the sample\n// email on the Template page and in Settings, and Settings' reader page previews. Its\n// sandbox, the policy a framed copy carries, and what its links do.\n\nimport { pagePolicy } from \"../../shared/page_policy\";\n\n// A framed preview is a srcdoc document, whose links resolve against the editor's own URL,\n// so a fragment-only link (the sample email's `#unsubscribe` placeholder) names the\n// dashboard rather than a spot in the email: followed, it would open the dashboard or load\n// it into the frame. The click is refused instead. The listener belongs to the editor, not\n// the frame, so it runs even where the frame's sandbox allows no scripts; one shared\n// function, so running the helper again on the same link adds nothing.\nconst stayPut = (e: Event) => e.preventDefault();\n\n/** The sandbox for a frame holding a post's email or page. Same-origin, so the editor can\n * size the frame and set its links to open a new tab (`openLinksInNewTab`); popups, so\n * that new tab can open at all; and popups escaping the sandbox, so the page it opens\n * works as it would from an inbox rather than inheriting this sandbox. Never scripts or\n * forms: the post's markup does nothing inside the editor but show. */\nexport const EMAIL_FRAME_SANDBOX = \"allow-same-origin allow-popups allow-popups-to-escape-sandbox\";\n\n/** Every link in a framed email made to open a new tab, with no opener and no referrer.\n * Followed in the frame, a link would load its target there, and most targets (the app's\n * own pages among them) refuse to be framed, so the frame would go blank and the preview\n * be lost. A fragment-only link does nothing instead (`stayPut`). Run it whenever the\n * frame's content changes. A sandboxed frame also needs `allow-popups` for the new tab to\n * open at all. */\nexport function openLinksInNewTab(doc: Document): void {\n for (const link of doc.querySelectorAll<HTMLAnchorElement | HTMLAreaElement>(\n \"a[href], area[href]\",\n )) {\n if (link.getAttribute(\"href\")?.startsWith(\"#\")) {\n link.addEventListener(\"click\", stayPut);\n link.addEventListener(\"auxclick\", stayPut);\n continue;\n }\n link.target = \"_blank\";\n link.rel = \"noopener noreferrer\";\n }\n}\n\n/** `markup` placed first in the document's <head> (never a <header>), or first in the\n * document when it has none. */\nfunction intoHead(doc: string, markup: string): string {\n const head = /<head(\\s[^>]*)?>/i;\n return head.test(doc) ? doc.replace(head, (tag) => tag + markup) : markup + doc;\n}\n\n/** The framed copy of a page (a post's page, a reader page's preview) under the policy a\n * page that submits no form carries when served (shared/page_policy.ts). The frame's sandbox already keeps script and forms out; the\n * policy is the second lock. Only the framed copy changes, never the page served. */\nexport function framedCopy(doc: string): string {\n return intoHead(\n doc,\n `<meta http-equiv=\"Content-Security-Policy\" content=\"${pagePolicy(\"'none'\", { framed: true })}\">`,\n );\n}\n", "// Syntax highlighting for the template editor (HTML/CSS with template tokens), the post\n// composer (Markdown), and the API reference's examples (JSON and a curl command). String\n// assembly by nature: the source is escaped first, then the escaped text is decorated with\n// spans, so the result is vouched for as markup at the boundary rather than built with the\n// html tag.\n\nimport { escapeHtml as esc, type Html, unsafeHtml } from \"./html\";\n\n// --- template syntax highlighting ---\n// A small tokenizer for the overlay editor: colors the {{ tokens }}, HTML tags and\n// attribute strings, and \u2014 inside the <style> block \u2014 CSS selectors, properties,\n// colors and at-rules. Logic-less templates need nothing heavier, so this stays\n// inside the dashboard's framework-free, no-build ethos (no CodeMirror, no bundler).\nfunction hlTokens(s: string): string {\n return s.replace(/\\{\\{\\s*(?:if\\s+)?[\\w.]+\\s*\\}\\}/g, (m) => `<span class=\"cx-var\">${m}</span>`);\n}\nfunction hlHtml(raw: string): string {\n let s = esc(raw);\n s = s.replace(/"[^&]*?"/g, (m) => `<span class=\"cx-str\">${m}</span>`);\n s = s.replace(\n /(<\\/?)([a-zA-Z][\\w-]*)/g,\n (_m, br, name) => `<span class=\"cx-punct\">${br}</span><span class=\"cx-tag\">${name}</span>`,\n );\n s = s.replace(/(\\/?)>/g, (_m, sl) => `<span class=\"cx-punct\">${sl}></span>`);\n return hlTokens(s);\n}\nfunction hlCss(raw: string): string {\n let s = esc(raw);\n s = s.replace(/"[^&]*?"/g, (m) => `<span class=\"cx-str\">${m}</span>`);\n s = s.replace(/#[0-9a-fA-F]{3,8}\\b/g, (m) => `<span class=\"cx-num\">${m}</span>`);\n s = s.replace(/@[\\w-]+/g, (m) => `<span class=\"cx-at\">${m}</span>`);\n s = s.replace(\n /^(\\s*)(?![@\\s])([^\\n{}<]+?)(\\s*)\\{$/gm,\n (_m, ind, sel, sp) => `${ind}<span class=\"cx-sel\">${sel}</span>${sp}{`,\n );\n s = s.replace(\n /^(\\s*)([a-z-]+)(\\s*:)/gm,\n (_m, ind, prop, colon) => `${ind}<span class=\"cx-prop\">${prop}</span>${colon}`,\n );\n return hlTokens(s);\n}\n/** The template source as highlighted markup for the overlay editor. */\nexport function highlightTemplate(src: string): Html {\n return unsafeHtml(\n String(src)\n .split(/(<style>[\\s\\S]*?<\\/style>)/)\n .map((seg) => {\n const m = seg.match(/^<style>([\\s\\S]*?)<\\/style>$/);\n if (m) {\n return `<span class=\"cx-punct\"><</span><span class=\"cx-tag\">style</span><span class=\"cx-punct\">></span>${hlCss(m[1] ?? \"\")}<span class=\"cx-punct\"></</span><span class=\"cx-tag\">style</span><span class=\"cx-punct\">></span>`;\n }\n return hlHtml(seg);\n })\n .join(\"\"),\n );\n}\n\n// --- post (Markdown) syntax highlighting ---\n// highlightTemplate()'s counterpart for the post editor: the same transparent-textarea-\n// over-highlighted-<pre> overlay, tokenizing Markdown instead of HTML/CSS, in the same\n// framework-free, no-build spirit. The scheme is deliberately minimal (Sublime-style): it\n// colors the *marks* an author scans for \u2014 the #, >, list bullets, the emphasis *asterisks*,\n// a link's URL \u2014 and leaves the CONTENT in the font color, carrying only weight/slant. So a\n// bold word reads as weight, not as a color that could be confused with a heading. It is a\n// light, line-oriented pass, not a full CommonMark parser, over the shared --syntax-* palette.\n\n// Inline constructs within one line, in a single left-to-right pass. One alternation consumes\n// each construct whole so we never re-tokenize inside a span we just emitted (the classic bug\n// from chaining .replace() calls \u2014 an italic pass eating a bold marker). Marks are colored, the\n// content plain: emphasis asterisks are pink and the word carries only weight/slant; a link's\n// text stays plain and only its URL is treated; inline `code` sits on a chip. Order at a given\n// position: `code`, then [links]/images, then **strong** before *em*. Single-underscore _em_ is\n// guarded by \\b so intra-word underscores (snake_case, price_1) stay literal, not emphasized.\nfunction hlMdInline(raw: string): string {\n return esc(raw).replace(\n /(`[^`\\n]+`)|(!?)(\\[[^\\]\\n]*\\])\\(([^)\\n]*)\\)|(\\*\\*|__)([^\\n]+?)\\5|\\*([^*\\n]+?)\\*|\\b_([^_\\n]+?)_\\b/g,\n (_m, code, bang, ltext, lurl, bd, btext, aem, uem) => {\n if (code) {\n return `<span class=\"cx-md-code\">${code}</span>`;\n }\n if (ltext !== undefined) {\n // Link/image: text + brackets stay plain; only the URL is treated (blue, underlined).\n return `${bang || \"\"}${ltext}(<span class=\"cx-md-url\">${lurl}</span>)`;\n }\n if (bd) {\n return `<span class=\"cx-md-mark\">${bd}</span><span class=\"cx-md-strong\">${btext}</span><span class=\"cx-md-mark\">${bd}</span>`;\n }\n // Emphasis: asterisk and underscore share the same treatment; the delimiter is a literal.\n const [d, text] = aem !== undefined ? [\"*\", aem] : [\"_\", uem];\n return `<span class=\"cx-md-mark\">${d}</span><span class=\"cx-md-em\">${text}</span><span class=\"cx-md-mark\">${d}</span>`;\n },\n );\n}\n// One line's block-level construct. Marks are colored and the text stays plain (still inline-\n// tokenized). Falls through to a plain inline pass for prose.\nfunction renderMdLine(line: string): string {\n // ATX heading: crimson # marks, heading text in the font color (bold for hierarchy).\n const h = line.match(/^(\\s{0,3}#{1,6})(\\s.*)?$/);\n if (h) {\n return `<span class=\"cx-md-hmark\">${esc(h[1] ?? \"\")}</span><span class=\"cx-md-htext\">${esc(h[2] || \"\")}</span>`;\n }\n // Thematic break: --- *** ___ (three or more of one mark), alone on the line.\n if (/^\\s{0,3}([-*_])(?:\\s*\\1){2,}\\s*$/.test(line)) {\n return `<span class=\"cx-md-bmark\">${esc(line)}</span>`;\n }\n // Blockquote: > markers colored, the quoted text plain (still inline-tokenized).\n const q = line.match(/^(\\s{0,3})((?:>\\s?)+)(.*)$/);\n if (q) {\n return `${esc(q[1] ?? \"\")}<span class=\"cx-md-bmark\">${esc(q[2] ?? \"\")}</span>${hlMdInline(q[3] ?? \"\")}`;\n }\n // List item: -, *, + (unordered) or 1. / 1) (ordered); colored marker, plain text.\n const li = line.match(/^(\\s*)([-*+]|\\d{1,9}[.)])(\\s+)(.*)$/);\n if (li) {\n return `${li[1]}<span class=\"cx-md-bmark\">${esc(li[2] ?? \"\")}</span>${li[3]}${hlMdInline(li[4] ?? \"\")}`;\n }\n return hlMdInline(line);\n}\n/** The post's Markdown as highlighted markup for the composer overlay, one block row per line. */\nexport function highlightMarkdown(src: string): Html {\n let inFence = false;\n return unsafeHtml(\n String(src)\n .split(\"\\n\")\n .map((line) => {\n let html: string;\n let band = false;\n // Fenced code block: a ``` or ~~~ line toggles the fence. The backticks stay plain and\n // only the language after them is colored. The fence lines themselves stay off the band \u2014\n // only the code between the fences carries the background.\n const fence = line.match(/^(\\s*)(```+|~~~+)(.*)$/);\n if (fence) {\n inFence = !inFence;\n const lang = fence[3] ? `<span class=\"cx-md-bmark\">${esc(fence[3])}</span>` : \"\";\n html = `${fence[1]}${esc(fence[2] ?? \"\")}${lang}`;\n } else if (inFence) {\n band = true;\n html = esc(line);\n } else {\n html = renderMdLine(line);\n }\n // Each source line is its own block row, so the caret aligns line-for-line with the\n // textarea and consecutive fenced rows' backgrounds abut into one continuous band. An\n // empty row is held open to one line by .cx-md-ln's min-height (CSS), not a filler glyph.\n return `<span class=\"cx-md-ln${band ? \" cx-md-band\" : \"\"}\">${html}</span>`;\n })\n .join(\"\"),\n );\n}\n\n// --- API reference examples ---\n// One pass over the raw JSON text: each token is escaped and wrapped, and the whitespace\n// between tokens is escaped as it stands, so nothing is decorated twice.\nconst JSON_TOKEN =\n /(\"(?:\\\\.|[^\"\\\\])*\")(\\s*:)?|(-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)|\\b(true|false|null)\\b|([{}[\\],])/g;\n/** A value as pretty-printed JSON (two-space indent), highlighted on the shared syntax palette. */\nexport function highlightJson(value: unknown): Html {\n const src = JSON.stringify(value, null, 2) ?? \"\";\n let out = \"\";\n let last = 0;\n for (const m of src.matchAll(JSON_TOKEN)) {\n out += esc(src.slice(last, m.index));\n const [whole, str, colon, num, lit] = m;\n if (str !== undefined) {\n out += colon\n ? `<span class=\"cx-prop\">${esc(str)}</span><span class=\"cx-punct\">${esc(colon)}</span>`\n : `<span class=\"cx-str\">${esc(str)}</span>`;\n } else if (num !== undefined) {\n out += `<span class=\"cx-num\">${num}</span>`;\n } else if (lit !== undefined) {\n out += `<span class=\"cx-tag\">${lit}</span>`;\n } else {\n out += `<span class=\"cx-punct\">${esc(whole)}</span>`;\n }\n last = m.index + whole.length;\n }\n return unsafeHtml(out + esc(src.slice(last)));\n}\n/** A curl command with the command, its flags, and the shell variables to fill in marked. */\nexport function highlightCurl(src: string): Html {\n return unsafeHtml(\n esc(src)\n .replace(/^curl\\b/, '<span class=\"cx-tag\">curl</span>')\n .replace(/(^|\\s)(-[A-Za-z])\\b/g, '$1<span class=\"cx-num\">$2</span>')\n .replace(/\\$[A-Z_]+/g, '<span class=\"cx-var\">$&</span>'),\n );\n}\n", "// Autosave timing, apart from the editor that uses it: save after a quiet pause, but never\n// let an edit sit unsaved longer than a hard cap even while the typing never pauses. The\n// idle timer restarts on every edit; the cap timer starts on the first edit after a save\n// and is left alone until a save fires or cancels it. Manual Save stays the primary path;\n// this is the safety net.\n\nexport interface AutosaveTimers {\n /** Quiet pause before a background save. */\n idleMs: number;\n /** Hard cap: no edit stays unsaved longer than this. */\n maxMs: number;\n}\n\nexport const AUTOSAVE_TIMERS: AutosaveTimers = { idleMs: 5000, maxMs: 30000 };\n\nexport interface Autosave {\n /** An edit happened. */\n touch(): void;\n /** Forget the pending save: a save is starting, a conflict paused autosave, or the editor is leaving. */\n cancel(): void;\n /** Whether a save is scheduled. */\n readonly pending: boolean;\n}\n\n/** The two-timer machine; `save` runs once when either timer fires, with both cleared first. */\nexport function createAutosave(\n save: () => void,\n timers: AutosaveTimers = AUTOSAVE_TIMERS,\n): Autosave {\n let idle: number | null = null;\n let cap: number | null = null;\n const cancel = () => {\n if (idle !== null) {\n clearTimeout(idle);\n }\n if (cap !== null) {\n clearTimeout(cap);\n }\n idle = null;\n cap = null;\n };\n const fire = () => {\n cancel();\n save();\n };\n return {\n touch() {\n if (idle !== null) {\n clearTimeout(idle);\n }\n idle = setTimeout(fire, timers.idleMs);\n if (cap === null) {\n cap = setTimeout(fire, timers.maxMs);\n }\n },\n cancel,\n get pending() {\n return idle !== null || cap !== null;\n },\n };\n}\n", "// Whether the editor's fields differ from what was last saved, as one comparison of a\n// snapshot: the fields serialized. \"Dirty\" drives the save-status cue, the navigation\n// guards, and whether an autosave has anything to do.\n\nexport class DirtyTracker<T> {\n #saved: string;\n\n constructor(private readonly read: () => T) {\n this.#saved = JSON.stringify(read());\n }\n\n /** The current values, in the form they are compared and sent. */\n snapshot(): string {\n return JSON.stringify(this.read());\n }\n\n get dirty(): boolean {\n return this.snapshot() !== this.#saved;\n }\n\n /**\n * What is saved now. Pass the snapshot that was actually sent: a save takes time, and an\n * edit typed while it was in flight is not saved by it. Marking the fields as they are\n * when the save lands would show that edit as saved and let a leave flush skip it.\n */\n markSaved(sent: string = this.snapshot()): void {\n this.#saved = sent;\n }\n}\n", "// The editor's saves still in flight, for a view the editor hands over to in the same\n// document. The web version link opens in place in a browser pane with no tabs, and its\n// read of the post must not overtake the save carrying the publisher's last edits.\n\nlet pending: Promise<unknown> = Promise.resolve();\n\n/** Record the editor's save chain as it stands; it settles once every save queued so far has. */\nexport function trackSaves(chain: Promise<unknown>): void {\n pending = chain;\n}\n\n/** Settles, never rejects, once every save the editor has queued has landed or failed. A\n * failed save shows in the editor; the reader here only waits its turn. */\nexport function savesSettled(): Promise<void> {\n return pending.then(\n () => {},\n () => {},\n );\n}\n", "// Optimistic concurrency on the client (SPEC \u00A74): the revision this editor is based on,\n// and what a failed save or a freshness poll means against it. The server rejects a save\n// whose base is not the newest revision (409) rather than clobber another writer's; the\n// editor's job is to tell the publisher, not to decide for them.\n\nimport { ApiError } from \"../api\";\n\n/** Who wrote a revision: an email, \"service\" for Claude, or unknown. */\nexport type Author = string | null;\n\n/** What the editor must surface: another writer's newer revision, or a post no longer editable here. */\nexport type Conflict = { kind: \"stale\"; revision: string; author: Author } | { kind: \"locked\" };\n\n/** Classify a failed save; null when it is a plain failure, not a conflict. */\nexport function conflictFromError(e: unknown): Conflict | null {\n if (!(e instanceof ApiError) || e.status !== 409) {\n return null;\n }\n const d = e.data as { error?: unknown; current_revision?: unknown; author?: unknown } | null;\n if (d?.error === \"stale_revision\" && typeof d.current_revision === \"string\") {\n return {\n kind: \"stale\",\n revision: d.current_revision,\n author: typeof d.author === \"string\" ? d.author : null,\n };\n }\n return { kind: \"locked\" };\n}\n\n/** A freshness poll's answer: the post's status and newest revision, and who wrote it. */\nexport interface PollAnswer {\n status: string;\n revision: string | null;\n author: Author;\n}\n\n/** What was true when the poll was sent, to tell our own save's effect from another writer's. */\nexport interface PollContext {\n saving: boolean;\n baseAtRequest: string | null;\n}\n\nexport class RevisionTracker {\n /** The revision this editor is based on; carried on every save so the server can refuse a stale one. */\n base: string | null;\n /** The newest revision already surfaced, so the poll re-warns only on a genuinely newer one. */\n warnedRevision: string | null = null;\n\n constructor(base: string | null) {\n this.base = base;\n }\n\n /** Our save landed: it is the newest revision, so the poll compares against it. */\n saved(revision: string | null): void {\n this.base = revision;\n }\n\n /** A conflict was shown; a stale one is remembered so the poll does not repeat it. */\n noteWarned(conflict: Conflict): void {\n if (conflict.kind === \"stale\") {\n this.warnedRevision = conflict.revision;\n }\n }\n\n /** The publisher chose to keep editing: adopt the newer revision as the base, so the next save wins. */\n adoptWarned(): void {\n if (this.warnedRevision !== null) {\n this.base = this.warnedRevision;\n }\n this.warnedRevision = null;\n }\n\n clearWarning(): void {\n this.warnedRevision = null;\n }\n\n /**\n * What a poll's answer means. \"ignore\" when our own save moved the base while the poll\n * was in flight (the answer may predate it) or a save is running now; \"locked\" when the\n * post is no longer a draft; a stale conflict when there is a newer revision not yet\n * surfaced; \"fresh\" otherwise.\n */\n decide(answer: PollAnswer, at: PollContext): Conflict | \"ignore\" | \"fresh\" {\n if (at.saving || this.base !== at.baseAtRequest) {\n return \"ignore\";\n }\n if (answer.status !== \"draft\") {\n return { kind: \"locked\" };\n }\n const rev = answer.revision;\n if (rev && rev !== this.base && rev !== this.warnedRevision) {\n return { kind: \"stale\", revision: rev, author: answer.author };\n }\n return \"fresh\";\n }\n}\n", "// A post's web version: the post as its public post page will show it (SPEC \u00A75), for the\n// publisher to proof before it is permanent. It opens in a tab of its own from the editor's\n// Web version link, so it carries no app chrome: one strip in the brand accent above the\n// page says what it is, where the post stands, and where its public page will be, so the\n// copy is never mistaken for the live page, and the frame below holds the page exactly as\n// it will be. The strip is the editor's, outside the frame, so it stays while the post\n// scrolls and the framed page is the public page's bytes.\n// Once the post's send starts, the public page itself is the web version.\n\nimport type { PostResponse } from \"../../shared/posts\";\nimport { ApiError, api, apiText, describeError } from \"../api\";\nimport { postArchiveUrl } from \"../deployment\";\nimport { mount, onAbort } from \"../lifecycle\";\nimport { EMAIL_FRAME_SANDBOX, framedCopy, openLinksInNewTab } from \"../ui/email_frame\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { renderError } from \"../ui/widgets\";\nimport { savesSettled } from \"./pending_save\";\n\n/** The editor route (the hash) of a post's web version. */\nexport function webVersionHash(postId: string): string {\n return `#/web/post/${encodeURIComponent(postId)}`;\n}\n\n/** The post id a web-version route names (`#/web/post/<id>`, split into its parts), or null\n * for one that names none, so the router falls through. */\nexport function webPostId(kind: string | undefined, id: string | undefined): string | null {\n if (kind !== \"post\" || !id) {\n return null;\n }\n try {\n return decodeURIComponent(id);\n } catch {\n // A hand-typed or mangled address: no page, never a throw.\n return null;\n }\n}\n\n/** Where a post stands, for the bar: a draft or scheduled, and its public address (null\n * while unknown, so the bar names none). */\nexport interface WebBarState {\n scheduled: boolean;\n url: string | null;\n}\n\n/** An address as the bar shows it: without its scheme, which says nothing to a reader. */\nfunction shortUrl(url: string): string {\n return url.replace(/^https?:\\/\\//, \"\");\n}\n\n/** The bar's words: what the page is, as its tag, then where the post stands and where it\n * will be published (as text, since the address doesn't answer yet), with no date. Null\n * state: the post couldn't be read, so the bar says only what the page is. */\nexport function webBar(state: WebBarState | null): Html {\n const tag = html`<span class=\"web-bar-tag\">Web version</span>`;\n if (state === null) {\n return tag;\n }\n const stage = state.scheduled ? \"Scheduled\" : \"Draft\";\n const at = state.url ? html` \u00B7 will be published at ${shortUrl(state.url)}` : null;\n return html`${tag} <span class=\"web-bar-text\">${stage}${at}</span>`;\n}\n\n/** The same address, spelled one way, or null for one that is not a whole URL. */\nfunction normalUrl(url: string): string | null {\n try {\n return new URL(url).href;\n } catch {\n return null;\n }\n}\n\n/** In the Preview tab of an unpublished post, every link to the post's own public address\n * (its \"View in browser\" link) made to open its web version instead (SPEC \u00A75). That address\n * answers \"Nothing here yet\" until the send starts, so a publisher who follows it to see\n * the post's page would find none; the web version is that page. Only the framed copy\n * changes: the email's bytes, and every test and send of it, keep the public address.\n * Links to anywhere else, another post's page among them, are left as they are. Only\n * the address changes: `openLinksInNewTab` still opens the link in a new tab. */\nexport function previewOpensWebVersion(\n doc: Document,\n archiveUrl: string | null,\n webUrl: string,\n): void {\n const own = archiveUrl ? normalUrl(archiveUrl) : null;\n if (!own) {\n return;\n }\n for (const link of doc.querySelectorAll<HTMLAnchorElement | HTMLAreaElement>(\n \"a[href], area[href]\",\n )) {\n const href = link.getAttribute(\"href\");\n if (href && normalUrl(href) === own) {\n link.setAttribute(\"href\", webUrl);\n }\n }\n}\n\n/** Leave for another page, replacing this one in the history (so Back skips it). */\nexport function leaveFor(url: string): void {\n location.replace(url);\n}\n\n/** The post's read, or null when it fails (the page still shows, under a bar that names no\n * stage or address). */\nasync function readPost(id: string, signal: AbortSignal): Promise<PostResponse | null> {\n try {\n return await api<PostResponse>(`/api/posts/${encodeURIComponent(id)}`, { signal });\n } catch {\n return null;\n }\n}\n\nexport async function renderWebVersion(\n id: string,\n root: HTMLElement,\n signal: AbortSignal,\n): Promise<void> {\n const appTitle = document.title;\n document.title = \"Web version\";\n onAbort(signal, () => {\n document.title = appTitle;\n });\n try {\n // Opened in place from the editor, the click's save may still be going: read after it.\n await savesSettled();\n const [doc, data] = await Promise.all([\n apiText(`/api/posts/${encodeURIComponent(id)}/web`, { signal }),\n readPost(id, signal),\n ]);\n if (signal.aborted) {\n return;\n }\n const url = data ? postArchiveUrl(data.post.slug) : null;\n // Once the send starts the post is public, and its web version is that page: go there.\n if (data && (data.sending || data.post.status === \"sent\") && url) {\n leaveFor(url);\n return;\n }\n const bar = webBar(data ? { scheduled: data.scheduled != null, url } : null);\n setHtml(\n root,\n html`<div class=\"web-page\">\n <div class=\"web-bar\" role=\"note\"><div class=\"web-bar-in\">${bar}</div></div>\n <iframe class=\"web-frame\" sandbox=\"${EMAIL_FRAME_SANDBOX}\" title=\"Web version\" scrolling=\"no\"></iframe>\n </div>`,\n );\n const frame = root.querySelector<HTMLIFrameElement>(\".web-frame\");\n if (!frame) {\n return;\n }\n // A link opens a new tab, as in the Preview tab, and the page's own <title> is the\n // post's subject.\n frame.onload = () => {\n const page = frame.contentDocument;\n if (!page || signal.aborted) {\n return;\n }\n openLinksInNewTab(page);\n if (page.title) {\n document.title = `Web version \u00B7 ${page.title}`;\n }\n // The frame is as tall as the post, so the window scrolls the page and the strip\n // above goes up with it rather than staying pinned. Measured on the body, which is\n // content-sized, and kept fitted as fonts, images, and the window's width change it.\n const fitFrame = () => {\n const h = page.body?.scrollHeight;\n if (h) {\n frame.style.height = `${h}px`;\n }\n };\n fitFrame();\n if (page.body && typeof ResizeObserver !== \"undefined\") {\n const watch = new ResizeObserver(fitFrame);\n watch.observe(page.body);\n onAbort(signal, () => watch.disconnect());\n }\n };\n frame.srcdoc = framedCopy(doc);\n } catch (e) {\n if (signal.aborted) {\n return;\n }\n setHtml(root, html`<div class=\"web-error\"></div>`);\n const box = root.querySelector(\".web-error\");\n if (box) {\n // The API names only what it didn't find (\"post\"), which says nothing on its own.\n const notFound = e instanceof ApiError && e.status === 404;\n const msg = notFound ? \"There\u2019s no post here. It may have been deleted.\" : describeError(e);\n renderError(box, msg, () => mount((r, s) => renderWebVersion(id, r, s)));\n }\n }\n}\n", "// The post editor: subject/slug, the Markdown composer, autosave with its idle and\n// hard-cap timers, the freshness poll, images, the schedule / send-now dialogs, and a\n// scheduled post's banner, which follows its send until the send page takes over.\n\nimport type { ImageUploadResponse } from \"../../shared/images\";\nimport type {\n PostEditBody,\n PostResponse,\n PostSavedResponse,\n TestSendResponse,\n} from \"../../shared/posts\";\nimport {\n DAILY_WRITES_SEND_RECIPIENTS,\n dailyWritesSendWarning,\n type ScheduleResponse,\n type SendAction,\n type SendResponse,\n type SendView,\n} from \"../../shared/sends\";\nimport type { SettingsResponse } from \"../../shared/settings\";\nimport type { SubscriberListResponse } from \"../../shared/subscribers\";\nimport { ApiError, api, apiText, describeError, isNetworkFailure } from \"../api\";\nimport {\n connectProviderLink,\n dailyWritesLine,\n earliestFireAt,\n heldWhenFiresHtml,\n isTooSoon,\n notConnectedScheduleLine,\n notConnectedTestLine,\n notConnectedWarning,\n postArchiveUrl,\n providerNotConnected,\n sendNowWhenText,\n tooSoonText,\n withNoProviderNote,\n} from \"../deployment\";\nimport { at, every, mount, onAbort, poll, type ViewHandle } from \"../lifecycle\";\nimport { followSend, readSendsNow, type StageChange } from \"../send_state\";\nimport { openRescheduleModal } from \"../sends/dialogs\";\nimport { can, conditionOf, has } from \"../sends/progress\";\nimport { $, $$ } from \"../ui/dom\";\nimport { EMAIL_FRAME_SANDBOX, openLinksInNewTab } from \"../ui/email_frame\";\nimport {\n fmt,\n invalidAddressesMessage,\n lateStr,\n notSentMessage,\n parseAddresses,\n toLocalInput,\n untilStr,\n} from \"../ui/format\";\nimport { highlightMarkdown } from \"../ui/highlight\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { type IconName, icon } from \"../ui/icons\";\nimport { busy, fieldError, infoTip, modal, renderError, toast } from \"../ui/widgets\";\nimport { createAutosave } from \"./autosave\";\nimport { DirtyTracker } from \"./dirty\";\nimport { trackSaves } from \"./pending_save\";\nimport { type Author, type Conflict, conflictFromError, RevisionTracker } from \"./revisions\";\nimport { previewOpensWebVersion, webVersionHash } from \"./web\";\n\nconst message = describeError;\n\nconst TOOLBAR: [IconName, string][][] = [\n [\n [\"heading\", \"Heading\"],\n [\"bold\", \"Bold (\u2318B)\"],\n [\"italic\", \"Italic (\u2318I)\"],\n ],\n [\n [\"quote\", \"Quote\"],\n [\"code\", \"Code\"],\n [\"link\", \"Link (\u2318K)\"],\n ],\n [\n [\"ul\", \"Bulleted list\"],\n [\"ol\", \"Numbered list\"],\n [\"indent\", \"Indent\"],\n ],\n];\n\n// A bare attribute is markup, not text: spelled once as markup so it can be interpolated.\nconst READONLY = html` readonly`;\nconst ARIA_DISABLED = html` aria-disabled=\"true\"`;\n\ntype ComposerTab = \"edit\" | \"preview\";\n\n/**\n * How a save ended. `saved` and `unchanged` both mean the server holds what the editor\n * shows. `refused` means it does not: another writer's revision is newer, so the save was\n * rejected (or is paused behind the out-of-date banner), and an action that would freeze\n * the content \u2014 schedule, send now (SPEC \u00A76, I3) \u2014 must not proceed on the publisher's\n * behalf. A plain failure throws instead of answering.\n */\ntype SaveResult = \"saved\" | \"unchanged\" | \"refused\";\n\nexport async function renderEditor(\n id: string,\n root: HTMLElement,\n signal: AbortSignal,\n): Promise<ViewHandle | undefined> {\n // Reload, cancel-schedule, and error-retry re-enter through mount(), which tears this\n // mount down (its autosave, its freshness poll) before the next one starts.\n // (A retry or reload click reaches this from a live root; a re-entry after a write the\n // reader did not stay to watch, such as a cancel that landed after they left, must not\n // mount the editor over wherever they went.)\n const remount = () => {\n if (!signal.aborted) {\n mount((r, s) => renderEditor(id, r, s));\n }\n };\n // The post was deleted elsewhere (another tab, or Claude through the API), found by\n // whichever read or write answers 404 first: say so once and go to Drafts. Deleting a\n // post is deliberate, and autosave keeps what is unsaved to the last few seconds of\n // typing, so the editor doesn't stay open to hold it.\n let gone = false;\n const removed = (msg = \"This post was deleted elsewhere.\") => {\n if (gone) {\n return;\n }\n gone = true;\n toast(msg);\n if (!signal.aborted) {\n handOff(\"#/drafts\");\n }\n };\n setHtml(root, html`<p class=\"muted\">Loading\u2026</p>`);\n let data: PostResponse;\n // A scheduled post's send, as the send routes report it: the banner's countdown and\n // conditions, and the cursor the editor follows the send from.\n let sendRead: SendResponse | null = null;\n try {\n data = await api<PostResponse>(`/api/posts/${id}`, { signal });\n if (signal.aborted) {\n return; // navigated away while loading: the redirects below must not hijack that\n }\n // A sent post is a frozen record, not editable: it opens the sent record view, never\n // the editor. Redirect a stale #/edit link (or a post sent in another tab / by Claude)\n // there instead of a locked editor. A post whose send is in flight is no longer an\n // editable, cancelable scheduled draft but an active send, so its link opens the live\n // watch, not a soft-locked editor with a dead Cancel.\n const home = sendHome(data);\n if (home) {\n handOff(home);\n return;\n }\n } catch (e) {\n // A Retry can only fail again for a post that isn't there (gone since the last read,\n // or a link to one that never was), so it goes to Drafts instead.\n if (isNotFound(e)) {\n removed(\"There\u2019s no post here. It may have been deleted.\");\n return;\n }\n renderError(root, message(e), remount);\n return;\n }\n if (data.scheduled) {\n try {\n sendRead = await api<SendResponse>(`/api/sends/${data.scheduled.id}`, { signal });\n } catch (e) {\n if (signal.aborted) {\n return;\n }\n // A send is deleted only with its post, so the post went between the two reads.\n if (isNotFound(e)) {\n removed();\n return;\n }\n // Any other failure leaves the post readable: the editor opens with its banner from\n // the post's own read, and follows the send once a later read of it succeeds.\n }\n if (signal.aborted || (sendRead && movedOn(sendRead.send, remount))) {\n return;\n }\n }\n const { post, markdown, scheduled } = data;\n\n // The editor now only ever mounts a draft or a scheduled (frozen) post, so `locked`\n // means scheduled \u2014 signaled by the scheduled banner, not a status pill.\n const locked = post.status !== \"draft\";\n // The revision this editor is based on, for optimistic concurrency (SPEC \u00A74).\n // Advanced on each successful save; carried on every save so the server rejects\n // (409) rather than clobbers a newer save from another tab or from Claude.\n const revisions = new RevisionTracker(post.current_revision);\n // A scheduled post keeps its formatting toolbar in view, greyed. The buttons take\n // aria-disabled rather than disabled so a click still reaches applyFormat, whose locked\n // branch nudges the foot line, the way out (DESIGN \u00A73).\n const toolbarHtml = TOOLBAR.map(\n (group, i) =>\n html`${i ? html`<span class=\"sep\"></span>` : null}${group.map(\n ([kind, label]) =>\n html`<button type=\"button\" class=\"tb\" data-fmt=\"${kind}\" title=\"${label}\" aria-label=\"${label}\"${locked ? ARIA_DISABLED : null}>${icon(kind)}</button>`,\n )}`,\n );\n // readonly, not disabled: a scheduled post's text can still be read, selected, and\n // copied; only a change is refused (DESIGN \u00A73).\n const ro = locked ? READONLY : null;\n // The web version's address (SPEC \u00A75), whole, so it opens as its own page. It differs\n // from this one only in the hash, which is what keeps a save safe when the link opens in\n // place (below).\n const webVersionUrl = `${location.origin}${location.pathname}${webVersionHash(id)}`;\n\n setHtml(\n root,\n html`\n <div class=\"editor-head\">\n <a href=\"#/drafts\" class=\"back\">\u2190 Drafts</a>\n <div class=\"editor-head-right\">\n <a class=\"ghost btn\" id=\"webLink\" href=\"${webVersionUrl}\" target=\"_blank\" rel=\"noopener\" aria-label=\"Web version (opens in a new tab)\">Web version \u2197</a>\n </div>\n </div>\n ${locked && scheduled ? html`<div class=\"banner banner-scheduled\"><div class=\"sched-main\"><span id=\"schedWhen\"></span>${heldWhenFiresHtml()}<div class=\"sched-remade\" id=\"schedRemade\" hidden><span id=\"schedRemadeMsg\"></span></div><div class=\"sched-daily-writes\" id=\"schedDailyWrites\" hidden><span class=\"sched-note-mark\" aria-hidden=\"true\">\u26A0\uFE0F</span><span id=\"schedDailyWritesMsg\"></span></div></div><span class=\"row\" id=\"schedControls\"><button type=\"button\" class=\"ghost\" id=\"rescheduleSchedule\">Reschedule</button><button type=\"button\" class=\"ghost\" id=\"cancelSchedule\">Cancel</button></span></div>` : null}\n <div id=\"freshnessBanner\" class=\"banner banner-conflict\" role=\"alert\" hidden></div>\n <div class=\"card\">\n <div class=\"grid2\">\n <div><label for=\"f-subject\">Subject</label><input id=\"f-subject\" value=\"${post.subject}\" placeholder=\"Untitled\"${ro}></div>\n <div>\n <div class=\"label-row\">\n <label for=\"f-slug\">Slug</label>\n ${infoTip(\"The web address of this post\u2019s archive page.\")}\n </div>\n <input id=\"f-slug\" value=\"${post.slug}\"${ro}>\n ${locked ? null : html`<label class=\"slug-auto-toggle\"><input type=\"checkbox\" id=\"f-slug-auto\">Auto-generate from subject</label>`}\n </div>\n </div>\n\n <label for=\"f-markdown\">Body</label>\n <div class=\"composer\">\n <div class=\"composer-head\">\n <div class=\"ctabs\" role=\"tablist\">\n <button type=\"button\" class=\"ctab active\" data-tab=\"edit\" data-text=\"Edit\" role=\"tab\" aria-selected=\"true\"><span class=\"ctab-label\">${icon(\"editable\")}Edit</span></button>\n <button type=\"button\" class=\"ctab\" data-tab=\"preview\" data-text=\"Preview\" role=\"tab\" aria-selected=\"false\"><span class=\"ctab-label\">${icon(\"preview\")}Preview</span></button>\n </div>\n <div class=\"toolbar\" role=\"toolbar\" aria-label=\"Formatting\">${toolbarHtml}</div>\n </div>\n <div class=\"composer-body${locked ? \" locked\" : \"\"}\" id=\"composerBody\">\n <pre class=\"md-hl\" id=\"mdHl\" aria-hidden=\"true\"><code></code></pre>\n <textarea id=\"f-markdown\" class=\"editor\"${locked ? null : html` placeholder=\"Type your post in Markdown\u2026\"`}${ro}>${markdown}</textarea>\n <iframe id=\"previewFrame\" class=\"preview\" sandbox=\"${EMAIL_FRAME_SANDBOX}\" title=\"Email preview\" hidden></iframe>\n </div>\n ${\n locked\n ? html`<div class=\"composer-foot composer-foot-lock\" id=\"lockFoot\" role=\"status\">${icon(\"readonly\")}<span>Cancel the schedule to edit</span></div>`\n : html`<div class=\"composer-foot\" id=\"dropFoot\">${icon(\"paperclip\")}<span>Paste, drop, or click to add images</span></div>`\n }\n <input type=\"file\" id=\"imgInput\" accept=\"image/png,image/jpeg,image/webp,image/gif\" multiple hidden>\n </div>\n <div id=\"warnings\"></div>\n ${locked ? null : notConnectedWarning(\"editor-not-connected\", html`No email provider connected: you can write and preview, not send. ${connectProviderLink()}.`)}\n\n <div class=\"actions-bar\">\n ${\n locked\n ? html`<div class=\"row\"><button type=\"button\" class=\"secondary\" id=\"testBtn\">${icon(\"send\")}<span>Send test email</span></button></div>`\n : html`<div class=\"row\">\n <button type=\"button\" class=\"ghost\" id=\"saveBtn\">Save draft</button>\n <span class=\"save-status\" id=\"saveStatus\" aria-live=\"polite\"></span>\n </div>\n <div class=\"row\">\n <button type=\"button\" class=\"secondary\" id=\"testBtn\">${icon(\"send\")}<span>Send test email</span></button>\n <button type=\"button\" class=\"primary\" id=\"scheduleBtn\">Schedule</button>\n </div>`\n }\n </div>\n </div>`,\n );\n\n const subjectEl = $<HTMLInputElement>(\"#f-subject\");\n const slugEl = $<HTMLInputElement>(\"#f-slug\");\n const ta = $<HTMLTextAreaElement>(\"#f-markdown\");\n const toolbarEl = $(\".toolbar\", root);\n const previewFrame = $<HTMLIFrameElement>(\"#previewFrame\");\n\n // Syntax-highlight overlay: a transparent <textarea> over a highlighted\n // <pre>, kept in scroll sync \u2014 the template editor's scaffolding, tokenizing Markdown.\n // Prose soft-wraps, so both layers share pre-wrap and identical metrics (the caret lands\n // on the colored text); that wrapping is also why there's no line-number gutter \u2014 a\n // number can't track a wrapped line and prose doesn't want one. The highlight is the point.\n const mdHl = $(\"#mdHl\");\n const mdHlCode = $(\"code\", mdHl);\n const syncMdScroll = () => {\n mdHl.scrollTop = ta.scrollTop;\n mdHl.scrollLeft = ta.scrollLeft;\n };\n const paintMarkdown = () => {\n // highlightMarkdown emits one block row per source line (split on \"\\n\"), so the row count \u2014\n // and thus the overlay height \u2014 already tracks the textarea, including a trailing blank line.\n setHtml(mdHlCode, highlightMarkdown(ta.value));\n syncMdScroll();\n };\n ta.addEventListener(\"scroll\", syncMdScroll);\n paintMarkdown(); // paint the initial content (a scheduled post is read-only but still highlighted)\n\n // Whether the slug follows the subject is the post's own (`slug_auto`), never guessed from\n // its shape: `spring-update-2027` set by hand looks just like a numbered `spring-update`.\n // While it follows, a save sends no slug of its own and the server derives and numbers it;\n // once set by hand, the field is what is sent. Either way the field shows only slugs the server\n // answered with, never the editor's own guess, so it can lag a subject edit until the save\n // but never shows an address the post won't get.\n const autoEl = locked ? null : $<HTMLInputElement>(\"#f-slug-auto\");\n const collect = () => ({\n subject: subjectEl.value,\n ...(autoEl?.checked ? { slug_auto: true } : { slug: slugEl.value, slug_auto: false }),\n markdown: ta.value,\n });\n // A save's answer for a slug being typed by hand waits until the field is left (or Enter),\n // so the cursor is never moved under the publisher; it lands only if the field still\n // holds what that save sent.\n let held: { shown: string; slug: string } | null = null;\n const applyHeld = () => {\n if (held && slugEl.value === held.shown) {\n slugEl.value = held.slug;\n refreshDirty(); // the field now matches what was saved\n }\n held = null;\n };\n\n // --- subject validation (SPEC \u00A76: freeze() rejects a subjectless send) ---\n // Surfaced on the field, not by disabling the button. The old guard greyed out\n // Schedule and hung the reason in a `title` on the row \u2014 invisible to keyboard,\n // touch, and screen readers. Instead Schedule stays live and we validate on click:\n // an empty subject puts the input into an error state with the reason directly\n // beneath it (DESIGN \u00A75), tied by aria-describedby and announced (role=alert).\n let clearSubjectErr: (() => void) | null = null;\n function clearSubjectError() {\n clearSubjectErr?.();\n clearSubjectErr = null;\n }\n // Gates both send paths (the scheduled send and the in-modal Send now both open\n // from Schedule): true when a subject is present; otherwise shows the error, moves\n // focus to the field, and returns false so the modal never opens.\n function validateSubject() {\n if (subjectEl.value.trim() === \"\") {\n clearSubjectErr = fieldError(subjectEl, \"Add a subject before you schedule.\");\n subjectEl.focus();\n return false;\n }\n clearSubjectError();\n return true;\n }\n\n // The \"Auto-generate from subject\" box, checked while the slug follows the subject. The\n // slug stays editable throughout. Typing your own slug takes manual control (unchecks);\n // emptying the field, or ticking the box, hands it back to the subject. The slug the\n // subject gives arrives with the save that leaving the subject makes (or Enter, or the\n // autosave).\n if (autoEl) {\n autoEl.checked = post.slug_auto;\n\n // Muted while it tracks the subject; normal color once it's set by hand.\n const reflect = () => slugEl.classList.toggle(\"slug-auto\", autoEl.checked);\n reflect();\n\n // Typing a slug takes manual control; clearing it follows the subject again.\n slugEl.addEventListener(\"input\", () => {\n autoEl.checked = slugEl.value.trim() === \"\";\n reflect();\n });\n // Ticking the box saves at once, so the field shows the subject's slug as soon as the\n // server answers; unticking hands over the field ready to edit.\n autoEl.addEventListener(\"change\", () => {\n reflect();\n markEdited(); // the checkbox changes what a save sends without an input event\n if (autoEl.checked) {\n saveDraft(true).catch(() => {}) /* the save cue says why */;\n } else {\n slugEl.focus();\n slugEl.select();\n }\n });\n // Never leave an empty slug: a field left empty follows the subject again. The save\n // that leaving it makes fills it in; with nothing to save, the post already has the\n // subject's slug, so the field shows that.\n slugEl.addEventListener(\"blur\", () => {\n applyHeld();\n if (slugEl.value.trim() === \"\") {\n autoEl.checked = true;\n reflect();\n markEdited();\n if (!dirty.dirty) {\n slugEl.value = savedSlug;\n }\n }\n });\n\n // Clear the empty-subject error as soon as a real subject is typed; it re-fires\n // on the next Schedule click if the field is still empty (see validateSubject).\n subjectEl.addEventListener(\"input\", () => {\n if (subjectEl.value.trim() !== \"\") {\n clearSubjectError();\n }\n });\n }\n\n // Autosave (./autosave.ts): save after a quiet pause, but never let an edit sit\n // unsaved longer than the hard cap. Manual Save + \u2318S stays the primary path; this is\n // the safety net. Failures surface as a toast, never silently. Cancelled with the mount,\n // so a navigation or the re-auth wall ends it. Declared before any path that can save\n // (the Preview tab saves first), since saveDraft cancels it.\n const mine = createAutosave(() => {\n saveDraft(true).catch(() => {}); // the save cue says why\n });\n onAbort(signal, () => mine.cancel());\n\n // --- tabs ---\n const tabs = $$<HTMLButtonElement>(\".ctab\", root);\n function showTab(name: ComposerTab) {\n for (const t of tabs) {\n const on = t.dataset.tab === name;\n t.classList.toggle(\"active\", on);\n t.setAttribute(\"aria-selected\", on ? \"true\" : \"false\");\n }\n ta.hidden = name !== \"edit\";\n mdHl.hidden = name !== \"edit\"; // the highlight layer travels with the textarea\n previewFrame.hidden = name !== \"preview\";\n toolbarEl.classList.toggle(\"off\", name !== \"edit\");\n // The image foot dims with the toolbar: a click on Preview would insert into a hidden\n // textarea behind a preview that doesn't change. A drop still lands (it switches to Edit).\n root.querySelector(\"#dropFoot\")?.classList.toggle(\"off\", name !== \"edit\");\n if (name === \"edit\") {\n syncMdScroll();\n }\n }\n // The slug the server last saved, which the preview renders with (a dedupe may differ\n // from what the field shows while it is being typed in).\n let savedSlug = post.slug;\n async function showPreview() {\n showTab(\"preview\");\n try {\n if (!locked) {\n await saveDraft(true);\n }\n previewFrame.srcdoc = await apiText(`/api/posts/${id}/preview`);\n previewFrame.onload = () => {\n try {\n const doc = previewFrame.contentDocument;\n if (doc) {\n openLinksInNewTab(doc);\n // Read on each load, so a slug edited since the last preview is the one\n // matched. The editor holds only an unpublished post: once its send starts, the\n // editor hands off to the send page (and a web version opened after that goes\n // on to the public page itself).\n previewOpensWebVersion(doc, postArchiveUrl(savedSlug), webVersionUrl);\n previewFrame.style.height = `${doc.body.scrollHeight + 24}px`;\n }\n } catch {}\n };\n } catch (e) {\n if (signal.aborted || foundGone(e)) {\n return;\n }\n toast(message(e));\n }\n }\n for (const t of tabs) {\n t.onclick = () => (t.dataset.tab === \"preview\" ? showPreview() : showTab(\"edit\"));\n }\n // A scheduled post opens on Preview: the preview is the copy that will send, and the\n // Edit tab is read-only, so it is somewhere the publisher goes deliberately and finds\n // the way out written on it (DESIGN \u00A73).\n if (locked) {\n showPreview();\n }\n\n // --- the lock's nudge ---\n // A scheduled post refuses edits at the browser (readonly) and at the API (SPEC \u00A76). When\n // one is attempted anyway, the foot's \"Cancel the schedule to edit\" pulses once, so the\n // way out is seen where the keystroke landed rather than announced elsewhere (DESIGN \u00A73).\n const lockFoot = locked ? $(\"#lockFoot\") : null;\n let nudgeTimer: number | undefined;\n function nudge() {\n if (!lockFoot) {\n return;\n }\n lockFoot.classList.remove(\"nudge\");\n requestAnimationFrame(() => lockFoot.classList.add(\"nudge\")); // a frame apart, so a second nudge restarts the pulse\n clearTimeout(nudgeTimer);\n nudgeTimer = setTimeout(() => lockFoot.classList.remove(\"nudge\"), 900);\n }\n\n // --- formatting toolbar ---\n function wrapSel(before: string, after: string, placeholder: string) {\n const s = ta.selectionStart;\n const e = ta.selectionEnd;\n const sel = ta.value.slice(s, e) || placeholder;\n ta.value = ta.value.slice(0, s) + before + sel + after + ta.value.slice(e);\n ta.focus();\n ta.selectionStart = s + before.length;\n ta.selectionEnd = s + before.length + sel.length;\n }\n function prefixLines(prefix: string) {\n const s = ta.selectionStart;\n const e = ta.selectionEnd;\n const start = ta.value.lastIndexOf(\"\\n\", s - 1) + 1;\n const out = (ta.value.slice(start, e) || \"\")\n .split(\"\\n\")\n .map((l) => prefix + l)\n .join(\"\\n\");\n ta.value = ta.value.slice(0, start) + out + ta.value.slice(e);\n ta.focus();\n ta.selectionStart = start;\n ta.selectionEnd = start + out.length;\n }\n function applyFormat(kind: string) {\n if (locked) {\n nudge(); // reached by a toolbar click or a Cmd+B / I / K shortcut\n return;\n }\n showTab(\"edit\");\n if (kind === \"bold\") {\n wrapSel(\"**\", \"**\", \"bold text\");\n } else if (kind === \"italic\") {\n wrapSel(\"*\", \"*\", \"italic text\");\n } else if (kind === \"heading\") {\n prefixLines(\"## \");\n } else if (kind === \"quote\") {\n prefixLines(\"> \");\n } else if (kind === \"ul\") {\n prefixLines(\"- \");\n } else if (kind === \"ol\") {\n prefixLines(\"1. \");\n } else if (kind === \"indent\") {\n prefixLines(\" \");\n } else if (kind === \"link\") {\n wrapSel(\"[\", \"](https://)\", \"link text\");\n } else if (kind === \"code\") {\n const s = ta.selectionStart;\n const e = ta.selectionEnd;\n if (s === e || ta.value.slice(s, e).includes(\"\\n\")) {\n wrapSel(\"```\\n\", \"\\n```\", \"code\");\n } else {\n wrapSel(\"`\", \"`\", \"code\");\n }\n }\n markEdited();\n }\n for (const b of $$<HTMLButtonElement>(\".tb[data-fmt]\", root)) {\n b.onclick = () => applyFormat(b.dataset.fmt ?? \"\");\n }\n ta.addEventListener(\"keydown\", (e) => {\n if (!(e.metaKey || e.ctrlKey)) {\n return;\n }\n const k = e.key.toLowerCase();\n if (k === \"b\") {\n e.preventDefault();\n applyFormat(\"bold\");\n } else if (k === \"i\") {\n e.preventDefault();\n applyFormat(\"italic\");\n } else if (k === \"k\") {\n e.preventDefault();\n applyFormat(\"link\");\n }\n });\n\n // --- unsaved-changes tracking + save ---\n // A snapshot of the last-saved field values; the editor is \"dirty\" whenever the\n // current values differ. We reflect that in a save-status indicator beside the\n // button, guard navigation (at the router, through the handle), and autosave.\n let editorDirty = false;\n // The last save errored, or the draft changed elsewhere (the out-of-date banner is up):\n // either way the leave guard prompts instead of silently flushing.\n let saveFailed = false;\n let conflicted = false;\n // Why the last save failed, in the publisher's words, kept on the save cue until a save\n // succeeds: a toast would be gone in seconds and leave the cue reading like ordinary\n // typing. A failed save tries again on its own, spaced out, so a dropped connection\n // recovers without another edit.\n let saveError: string | null = null;\n let retryDelay = 0;\n let retryArmed = false; // one pending retry at a time, however many saves fail meanwhile\n // Bumped to call off an armed retry: a later failure the server would repeat (a refused\n // field, say) means trying again unchanged can only send the refused value once more.\n let retryRound = 0;\n // The save row is a draft's; a scheduled post renders none.\n const saveBtn = locked ? null : $<HTMLButtonElement>(\"#saveBtn\");\n const saveStatus = locked ? null : $(\"#saveStatus\");\n const dirty = new DirtyTracker(collect);\n let saving = false;\n // The autosave editor's counterpart to the shared save bar's dot: same amber\n // \"unsaved\" cue (dot + text), extended to the autosave lifecycle it actually has \u2014\n // Saving\u2026 while a save is in flight, Saved once it lands. Empty while locked\n // (a scheduled post can't be edited here).\n function renderSaveStatus() {\n if (!saveStatus) {\n return;\n }\n if (locked) {\n saveStatus.className = \"save-status\";\n saveStatus.textContent = \"\";\n } else if (saving) {\n saveStatus.className = \"save-status is-saving\";\n saveStatus.textContent = \"Saving\u2026\";\n } else if (saveError && editorDirty) {\n saveStatus.className = \"save-status is-error\";\n saveStatus.textContent = `Not saved: ${saveError}`;\n } else if (editorDirty) {\n saveStatus.className = \"save-status is-dirty\";\n saveStatus.textContent = \"Unsaved changes\";\n } else {\n saveStatus.className = \"save-status is-saved\";\n saveStatus.textContent = \"Saved\";\n }\n }\n function refreshDirty() {\n editorDirty = dirty.dirty;\n if (!editorDirty) {\n // Back to what was last saved (an undo, say): nothing is unsaved, so no failure stands.\n saveError = null;\n saveFailed = false;\n retryDelay = 0;\n }\n renderSaveStatus();\n }\n renderSaveStatus(); // paint the initial state (Saved on a fresh draft; empty if locked)\n function scheduleAutosave() {\n mine.touch();\n }\n // Called after any edit \u2014 typed, formatted, or an inserted image.\n function markEdited() {\n if (locked) {\n return;\n }\n paintMarkdown(); // repaint the highlight overlay from the new textarea value\n refreshDirty();\n if (!conflicted) {\n scheduleAutosave();\n }\n }\n\n // Saves are chained so an autosave and an explicit save can never overlap; a\n // silent save with nothing pending is skipped.\n let saveChain: Promise<unknown> = Promise.resolve();\n function saveDraft(silent: boolean): Promise<SaveResult> {\n const next = saveChain.catch(() => {}).then(() => doSaveDraft(silent));\n saveChain = next;\n trackSaves(saveChain);\n return next;\n }\n async function doSaveDraft(silent: boolean): Promise<SaveResult> {\n if (silent && !dirty.dirty) {\n return \"unchanged\"; // nothing changed since the last save\n }\n if (conflicted) {\n return \"refused\"; // paused until the out-of-date banner is resolved\n }\n mine.cancel(); // a save is starting \u2014 cancel any pending autosave trigger\n saving = true;\n renderSaveStatus(); // reflect Saving\u2026 right away; the finally re-renders when done\n // What this save sends is what it saves: an edit typed while it is in flight stays\n // dirty and goes with the next one.\n const fields = collect();\n let sent = JSON.stringify(fields);\n const slugShown = slugEl.value;\n try {\n const body: PostEditBody = { ...fields, base_revision: revisions.base };\n const { post: u } = await api<PostSavedResponse>(`/api/posts/${id}`, {\n method: \"PUT\",\n json: body,\n });\n // Show the slug the server gave (derived, numbered if taken), unless the field changed\n // while the save was out: that edit is unsaved, and the next save brings its own answer.\n // Focus alone is no reason to drop it, since tabbing through the field changes nothing;\n // a slug being typed by hand only has it wait until the field is left.\n if (u.slug && slugEl.value === slugShown) {\n // A slug set by hand is sent as typed; a following one is the server's to derive.\n const handSet = \"slug\" in fields;\n if (handSet && document.activeElement === slugEl) {\n held = { shown: slugShown, slug: u.slug }; // being typed by hand: see applyHeld\n } else {\n slugEl.value = u.slug;\n }\n if (handSet) {\n sent = JSON.stringify({ ...fields, slug: u.slug }); // what was saved, not what was typed\n }\n }\n if (u.slug) {\n savedSlug = u.slug;\n }\n revisions.saved(u.current_revision); // our save is now the newest; poll against it\n dirty.markSaved(sent);\n saveFailed = false;\n saveError = null;\n retryDelay = 0;\n if (!silent) {\n toast(\"Saved\");\n }\n return \"saved\";\n } catch (e) {\n // A stale-revision 409 isn't a plain failure: another writer got there first.\n // Surface the out-of-date banner (notify, don't clobber) instead of an error toast.\n const conflict = conflictFromError(e);\n if (conflict) {\n showConflict(conflict);\n return \"refused\";\n }\n if (isNotFound(e)) {\n removed();\n return \"refused\";\n }\n saveFailed = true; // the leave guard now prompts rather than silently flushing\n // A refusal naming the subject or slug is said under that field, and the cue points\n // there; the server will refuse the same value again, so it is not retried.\n const named =\n e instanceof ApiError ? { subject: subjectEl, slug: slugEl }[e.field ?? \"\"] : undefined;\n if (named) {\n fieldError(named, describeError(e));\n saveError = \"fix the field marked above.\";\n } else {\n saveError = describeError(e);\n }\n // Only a failure that can pass on its own is tried again: no answer, the server's\n // own trouble, or too many requests at once. Anything else (a refusal, a bug in the\n // page) would fail the same way, so it calls off a retry an earlier failure armed.\n const transient =\n isNetworkFailure(e) || (e instanceof ApiError && (e.status >= 500 || e.status === 429));\n if (!transient) {\n retryRound += 1;\n retryArmed = false;\n retryDelay = 0; // a later dropped connection starts its spacing afresh\n } else if (!retryArmed) {\n retryArmed = true;\n retryDelay = retryDelay ? Math.min(retryDelay * 2, 60_000) : 5_000;\n const round = retryRound;\n at(\n Date.now() + retryDelay,\n () => {\n if (round !== retryRound) {\n return; // called off by a failure that would only repeat\n }\n retryArmed = false;\n if (saveError && !conflicted) {\n saveDraft(true).catch(() => {}); // a failure says so on the cue again\n }\n },\n signal,\n );\n }\n throw e;\n } finally {\n saving = false;\n refreshDirty();\n }\n }\n if (saveBtn) {\n saveBtn.onclick = () => {\n // While the out-of-date banner is up a save is paused (notify, never clobber: SPEC\n // \u00A74), so the button reads as unavailable and a click (or \u2318S) does not pretend to\n // save: it points at the banner, whose Reload or Keep editing is the next step.\n // aria-disabled, not disabled, so the click still lands to do that.\n if (conflicted) {\n freshnessEl.scrollIntoView({ block: \"nearest\" });\n return;\n }\n busy(saveBtn, \"Saving\u2026\", () => saveDraft(false).catch(() => {})).finally(refreshDirty);\n };\n }\n // Save draft follows the banner: unavailable while it asks for a decision, and says why.\n function paintSaveBtn() {\n if (!saveBtn) {\n return;\n }\n if (conflicted) {\n saveBtn.setAttribute(\"aria-disabled\", \"true\");\n saveBtn.title = \"Saving is paused: choose Reload or Keep editing above\";\n } else {\n saveBtn.removeAttribute(\"aria-disabled\");\n saveBtn.removeAttribute(\"title\");\n }\n }\n\n // Leaving the editor saves in the background instead of prompting. Capture the\n // fields NOW (the mount tears down the DOM right after) and send them through\n // the chain so they can't overlap an in-flight save. Deliberately not bound to the\n // signal: a save the reader started must land whether or not they stayed to watch.\n // A save already in flight (a blur's, the Web version link's) may carry these very\n // edits, so the flush asks again once it lands, and reads the base then: a base taken\n // now would be older than that save, and the server would refuse our own edits as\n // another writer's.\n const leaveFlush = () => {\n mine.cancel();\n if (locked || !dirty.dirty) {\n return;\n }\n const fields = collect();\n const sent = JSON.stringify(fields);\n editorDirty = false;\n saveChain = saveChain\n .catch(() => {})\n .then(async () => {\n if (!dirty.dirty) {\n return;\n }\n const body: PostEditBody = { ...fields, base_revision: revisions.base };\n const { post: u } = await api<PostSavedResponse>(`/api/posts/${id}`, {\n method: \"PUT\",\n json: body,\n });\n revisions.saved(u.current_revision);\n dirty.markSaved(sent);\n })\n .catch((e) => {\n if (isNotFound(e)) {\n // The reader has left, and their edits with the page: say what happened to them.\n toast(\"This post was deleted elsewhere, so your last edits weren\u2019t saved.\");\n return;\n }\n toast(\n e instanceof ApiError && e.status === 409\n ? \"Changed elsewhere, so your edits weren\u2019t saved\"\n : `Couldn\u2019t save your changes: ${message(e)}`,\n );\n });\n trackSaves(saveChain);\n };\n const handle: ViewHandle = {\n dirty: () => editorDirty,\n beforeLeave() {\n if (gone) {\n return \"leave\"; // on its way to Drafts: there is nothing left to save to\n }\n if (editorDirty && (saveFailed || conflicted)) {\n return \"confirm\"; // a silent flush would fail (or clobber): the reader decides\n }\n leaveFlush();\n return \"leave\";\n },\n manualSave() {\n if (saveBtn && !saveBtn.disabled) {\n saveBtn.click();\n }\n },\n };\n\n // Typed edits mark dirty; leaving subject/slug, or Enter in one, flushes promptly. The body is\n // left to the idle/cap timers so a toolbar click (which blurs it) doesn't save\n // on every interaction.\n if (!locked) {\n for (const el of [subjectEl, slugEl, ta]) {\n el.addEventListener(\"input\", markEdited);\n }\n for (const el of [subjectEl, slugEl]) {\n el.addEventListener(\n \"blur\",\n () => saveDraft(true).catch(() => {}) /* the save cue says why */,\n );\n // Enter commits a one-line field the way leaving it does, but not the Enter that ends\n // an IME composition, which Safari reports with isComposing false and keyCode 229.\n el.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" && !e.isComposing && e.keyCode !== 229) {\n e.preventDefault();\n saveDraft(true)\n .then(applyHeld) // Enter commits the slug, so show what it saved as\n .catch(() => {}); // the save cue says why\n }\n });\n }\n }\n\n // --- concurrent-edit detection (SPEC \u00A74) ---\n // Another tab, or Claude through the API, can save this draft while it's open\n // here. A save carries base_revision so the server rejects a stale write (409);\n // a light poll warns before the writer invests more effort. We notify, never\n // adopt: Reload takes the other version, Keep editing keeps yours (your next\n // save overwrites it). Re-arm only on a genuinely newer revision.\n const freshnessEl = $(\"#freshnessBanner\");\n const friendlyAuthor = (a: Author) => (a === \"service\" ? \"Claude\" : a || null);\n\n function clearConflict() {\n conflicted = false;\n paintSaveBtn();\n revisions.clearWarning();\n freshnessEl.hidden = true;\n setHtml(freshnessEl, html``);\n }\n function showConflict(conflict: Conflict) {\n if (locked) {\n return;\n }\n conflicted = true; // pauses autosave; makes the leave guard prompt\n saveError = null; // the banner is the story now; an older failure's reason would contradict it\n paintSaveBtn();\n revisions.noteWarned(conflict);\n if (conflict.kind === \"locked\") {\n setHtml(\n freshnessEl,\n html`<span><span aria-hidden=\"true\">\u26A0\uFE0F</span> This draft was scheduled elsewhere and can no longer be edited here.</span><span class=\"row\"><button type=\"button\" class=\"ghost\" id=\"freshReload\">Reload</button></span>`,\n );\n } else {\n const who = friendlyAuthor(conflict.author);\n setHtml(\n freshnessEl,\n html`<span><span aria-hidden=\"true\">\u26A0\uFE0F</span> This draft was changed elsewhere${who ? html`, last edited by <strong>${who}</strong>` : null}. Saving is paused until you choose: Reload to load that version (discards your unsaved edits), or Keep editing to keep yours, and your next save overwrites that version.</span><span class=\"row\"><button type=\"button\" class=\"ghost\" id=\"freshReload\">Reload</button><button type=\"button\" class=\"ghost\" id=\"freshKeep\">Keep editing</button></span>`,\n );\n }\n freshnessEl.hidden = false;\n $<HTMLButtonElement>(\"#freshReload\", freshnessEl).onclick = () => {\n clearConflict();\n remount();\n };\n const keep = freshnessEl.querySelector<HTMLButtonElement>(\"#freshKeep\");\n if (keep) {\n keep.onclick = () => {\n revisions.adoptWarned(); // the newer revision becomes our base \u2014 our next save wins\n clearConflict();\n refreshDirty();\n if (editorDirty) {\n scheduleAutosave();\n }\n };\n }\n }\n\n // A read or write that found the post gone (a 404) answers it here, whichever control\n // made it: the API names only what it didn't find (\"post\"), which says nothing alone.\n function foundGone(e: unknown): boolean {\n if (!isNotFound(e)) {\n return false;\n }\n removed();\n return true;\n }\n\n if (!locked) {\n // Skipped while hidden, saving, or already warned \u2014 poll GET is cheap and only\n // re-warns on a revision we haven't surfaced yet. Read at once when the tab is shown.\n const pollFreshness = async () => {\n if (saving || conflicted || gone || document.hidden) {\n return;\n }\n const baseAtRequest = revisions.base; // to tell our own save landing mid-poll from another writer's\n try {\n const fresh = await api<PostResponse>(`/api/posts/${id}`, { signal });\n const decision = revisions.decide(\n {\n status: fresh.post.status,\n revision: fresh.post.current_revision,\n author: fresh.author,\n },\n { saving, baseAtRequest },\n );\n if (decision !== \"ignore\" && decision !== \"fresh\") {\n showConflict(decision);\n }\n } catch (e) {\n if (isNotFound(e)) {\n removed();\n }\n // Anything else is transient: try again next tick.\n }\n };\n every(10000, pollFreshness, signal);\n readWhenShown(pollFreshness, signal);\n }\n\n // --- web version: the post as its public page will show it (SPEC \u00A75) ---\n // A plain link, so no browser blocks it as a pop-up. The click saves pending edits and\n // lets the link go: a new tab loads the editor and signs in before it reads the page, so\n // the save is ahead of it; opened in place, the page waits for it (pending_save.ts). A\n // refused or failed save shows here as any save does.\n // Where the link opens in place (a browser pane with no tabs), it is a same-document hash\n // change, so nothing unloads to cut the save off, and the leave flush that follows sees\n // it in flight and doesn't send it twice.\n $<HTMLAnchorElement>(\"#webLink\").addEventListener(\"click\", () => {\n if (!locked) {\n saveDraft(true).catch(() => {}) /* the save cue says why */;\n }\n });\n\n if (locked && scheduled) {\n // The send as last reported: this page's read of it (or, until that succeeds, what the\n // post's read says of it), then each read of the send-state layer that found it\n // changed, whichever client changed it (DESIGN \u00A74).\n let current: BannerSend = sendRead?.send ?? {\n ...scheduled,\n phase: \"scheduled\",\n conditions: [],\n };\n const whenEl = $(\"#schedWhen\");\n const controls = $(\"#schedControls\");\n // --- the banner: the cards' countdown, and the review window closing at the fire time ---\n // Before the fire time it counts down on the clock. From then the server refuses Cancel\n // and Reschedule (SPEC \u00A76), so the same line reads \"Preparing to send\u2026\" in place of the\n // countdown and the buttons stay where they are, disabled: the page hands off to the\n // live send within seconds, so nothing needs to move. Past the server's missed\n // tolerance it says how late the send is, from the server's condition.\n // The switch happens on this page's clock, at the fire time itself (a tick of its own\n // when the fire time is inside the coming second), with no read needed. Before it the\n // buttons are the server's to offer (`actions`); from it the clock only disables what\n // was offered, so a read that answered before the fire time cannot bring them back.\n let painted = \"\";\n let armed = 0; // the fire time an exact tick is set for, so it is set once\n let seenOpen = false; // whether this page showed the window open, and so its buttons\n const reschedBtn = $<HTMLButtonElement>(\"#rescheduleSchedule\");\n const cancelBtn = $<HTMLButtonElement>(\"#cancelSchedule\");\n const offers = (name: SendAction[\"name\"]) =>\n current.actions === undefined || can({ actions: current.actions }, name);\n const paintBanner = () => {\n const now = Date.now();\n const due = current.phase === \"due\" || now >= current.fire_at;\n if (!due) {\n seenOpen = true;\n reschedBtn.hidden = !offers(\"reschedule\");\n cancelBtn.hidden = !offers(\"cancel\");\n } else if (!seenOpen) {\n // Opened after the window closed: there is nothing to show disabled.\n reschedBtn.hidden = true;\n cancelBtn.hidden = true;\n }\n reschedBtn.disabled = due;\n cancelBtn.disabled = due;\n controls.hidden = reschedBtn.hidden && cancelBtn.hidden;\n const markup = scheduledBannerHtml(current, due);\n const key = String(markup);\n if (key !== painted) {\n painted = key;\n setHtml(whenEl, markup);\n }\n if (!due && current.fire_at - now <= 1000 && current.fire_at !== armed) {\n armed = current.fire_at;\n at(current.fire_at, paintBanner, signal);\n }\n };\n paintBanner();\n every(1000, paintBanner, signal);\n\n // --- re-made since its last test (SPEC \u00A78): the server's condition, in the banner ---\n // A template or identity change re-made the frozen email, so an earlier test no longer\n // shows what will go out. The server says so until a test of the re-made copy lands,\n // the same fact Claude reads. The banner only says so: Send test email is in the actions\n // bar below, and a second one here would crowd the banner's own controls.\n // On Workers Free's limits, a send too large for D1's daily writes says so here too, in\n // the server's words, for as long as it is scheduled (DESIGN \u00A74). It never blocks.\n const remadeEl = $(\"#schedRemade\");\n const dailyWritesEl = $(\"#schedDailyWrites\");\n const paintConditions = () => {\n const remade = conditionOf(current, \"remade\");\n remadeEl.hidden = !remade;\n $(\"#schedRemadeMsg\").textContent = remade?.message ?? \"\";\n const dailyWrites = conditionOf(current, \"daily_writes\");\n dailyWritesEl.hidden = !dailyWrites;\n $(\"#schedDailyWritesMsg\").textContent = dailyWrites?.message ?? \"\";\n };\n paintConditions();\n\n // --- following the send (DESIGN \u00A74) ---\n // A move, a re-make, or a cancel made elsewhere (by Claude or another tab) shows in\n // place, and when the send starts this page hands off to its send page, which is the\n // send's home from then on, saying why the page changed.\n const follow = (from: SendResponse) =>\n followSend(\n from,\n {\n update(send: SendView, change: StageChange | null) {\n current = send;\n if (change?.to === \"sending\" || change?.to === \"sent\" || change?.to === \"complete\") {\n toast(\"Your send started, so this page switched to its live watch.\");\n handOff(`#/sent/${send.id}`);\n return;\n }\n if (change?.to === \"canceled\") {\n toast(\"The schedule was canceled elsewhere, so this post is a draft again.\");\n remount();\n return;\n }\n paintBanner();\n paintConditions();\n },\n removed,\n // The layer's cursor ran ahead of the database: read the post and its send again.\n stale: remount,\n },\n signal,\n );\n if (sendRead) {\n follow(sendRead);\n } else {\n // The send's read failed: try it again, spaced out, and follow from the first that\n // answers. A failure throws, which the poll takes as \"try again\".\n poll(\n 5000,\n async () => {\n const read = await api<SendResponse>(`/api/sends/${scheduled.id}`, { signal });\n if (!movedOn(read.send, remount)) {\n current = read.send;\n paintBanner();\n paintConditions();\n follow(read);\n }\n return false;\n },\n signal,\n );\n }\n\n // --- reschedule (from the scheduled banner): move the fire time, content stays frozen ---\n reschedBtn.onclick = () => openRescheduleModal(current.id, current.fire_at, remount, signal);\n\n // --- cancel schedule (from the scheduled banner) ---\n cancelBtn.onclick = () =>\n busy(cancelBtn, \"Canceling\u2026\", async () => {\n try {\n await api(`/api/sends/${scheduled.id}/cancel`, { method: \"POST\" });\n toast(\"Schedule canceled\");\n remount();\n } catch (e) {\n if (isNotFound(e)) {\n removed(); // the send went with its post before the follow noticed\n } else if (e instanceof ApiError && e.code === \"window_closed\") {\n // The fire time passed while the cancel was out, so the email is going out. The\n // server's clock has closed the window even if this page's has not, so the banner\n // reads as due, and the follow, read at once, hands off to the live watch when\n // the send starts.\n current = { ...current, phase: \"due\" };\n toast(\"Too late to cancel: the send time has passed, so this email is going out.\");\n readSendsNow();\n } else {\n toast(message(e));\n }\n }\n // `busy` hands the button back when it ends; a cancel still answering at the fire\n // time must not re-enable it, so the clock's verdict is painted again at once.\n }).finally(paintBanner);\n }\n\n // --- image upload: drag/drop, paste, click ---\n async function uploadAndInsert(file: File) {\n if (!file.type.startsWith(\"image/\")) {\n return;\n }\n try {\n const fd = new FormData();\n fd.append(\"file\", file);\n const { image } = await api<ImageUploadResponse>(`/api/posts/${id}/images`, {\n method: \"POST\",\n body: fd,\n });\n const s = ta.selectionStart;\n const snippet = `\\n\\n`;\n ta.value = ta.value.slice(0, s) + snippet + ta.value.slice(s);\n ta.selectionStart = ta.selectionEnd = s + snippet.length;\n markEdited();\n toast(\"Image added\");\n } catch (e) {\n if (!foundGone(e)) {\n toast(message(e));\n }\n }\n }\n const composerBody = $(\"#composerBody\");\n if (!locked) {\n const imgInput = $<HTMLInputElement>(\"#imgInput\");\n const foot = $(\"#dropFoot\");\n composerBody.addEventListener(\"dragover\", (e) => {\n e.preventDefault();\n composerBody.classList.add(\"dragover\");\n });\n composerBody.addEventListener(\"dragleave\", (e) => {\n if (e.target === composerBody) {\n composerBody.classList.remove(\"dragover\");\n }\n });\n composerBody.addEventListener(\"drop\", (e) => {\n e.preventDefault();\n composerBody.classList.remove(\"dragover\");\n showTab(\"edit\");\n for (const f of e.dataTransfer?.files ?? []) {\n uploadAndInsert(f);\n }\n });\n ta.addEventListener(\"paste\", (e) => {\n for (const it of e.clipboardData?.items || []) {\n if (it.type.startsWith(\"image/\")) {\n const f = it.getAsFile();\n if (f) {\n e.preventDefault();\n uploadAndInsert(f);\n }\n }\n }\n });\n foot.onclick = () => imgInput.click();\n imgInput.onchange = () => {\n for (const f of imgInput.files ?? []) {\n uploadAndInsert(f);\n }\n imgInput.value = \"\";\n };\n } else {\n // Locked: a key that would change the text, a paste, or a drop is refused (readonly\n // does the refusing; a drop is stopped from opening the file) and nudges the foot.\n // Navigation, selection, and copy keys pass, so reading stays free.\n const wouldEdit = (e: KeyboardEvent) => {\n if (e.metaKey || e.ctrlKey) {\n return [\"x\", \"z\", \"y\"].includes(e.key.toLowerCase()); // cut, undo, redo (paste fires its own event)\n }\n return e.key.length === 1 || e.key === \"Enter\" || e.key === \"Backspace\" || e.key === \"Delete\";\n };\n const fields: HTMLElement[] = [ta, subjectEl, slugEl];\n for (const f of fields) {\n f.addEventListener(\"keydown\", (e) => {\n if (wouldEdit(e)) {\n nudge();\n }\n });\n f.addEventListener(\"paste\", nudge);\n }\n composerBody.addEventListener(\"dragover\", (e) => e.preventDefault());\n composerBody.addEventListener(\"drop\", (e) => {\n e.preventDefault();\n nudge();\n });\n }\n\n const warningsEl = $(\"#warnings\");\n function showWarnings(ws: string[] | null | undefined) {\n setHtml(\n warningsEl,\n ws?.length\n ? html`<div class=\"warnings\"><strong>Warnings:</strong> ${ws.join(\"; \")}</div>`\n : html``,\n );\n }\n\n // --- send test (modal) ---\n // Pre-fills from the default test recipients (Settings) and accepts\n // several \u2014 one per line. Each address is a separate test send through the same\n // per-recipient path as a real send (I5).\n $<HTMLButtonElement>(\"#testBtn\").onclick = () => {\n // A scheduled post's test is its frozen copy, exactly as it will fire (SPEC \u00A75);\n // the dialog is where that is said (DESIGN \u00A73).\n const lead = locked\n ? \"Delivers the frozen copy that will send, exactly as it will fire, so you can check it in a client. One address per line.\"\n : \"Delivers the rendered email to real inboxes so you can check it in a client. One address per line.\";\n // While no provider is connected the server would refuse a test, so the dialog leaves\n // out the recipients and keeps only the standing line (DESIGN \u00A74).\n const notConnected = providerNotConnected();\n const m = modal(\n html`<h3>Send a test</h3><p class=\"hint\">${lead}</p>${notConnectedTestLine()}${notConnected ? null : html`<label for=\"testTo\">Recipients</label><textarea id=\"testTo\" rows=\"3\" placeholder=\"you@example.com\"></textarea><p class=\"hint\" id=\"testDefaultsHint\" hidden></p>`}<div class=\"actions\"><button type=\"button\" id=\"tCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"tGo\">Send test</button></div>`,\n signal,\n );\n const go = $<HTMLButtonElement>(\"#tGo\", m.el);\n $(\"#tCancel\", m.el).onclick = m.close;\n go.disabled = notConnected;\n if (notConnected) {\n $(\"#tCancel\", m.el).focus();\n return;\n }\n const to = $<HTMLTextAreaElement>(\"#testTo\", m.el);\n to.focus();\n // Pre-fill with saved defaults (don't clobber anything already typed).\n api<SettingsResponse>(\"/api/settings\")\n .then((s) => {\n const defaults = s.settings.testRecipients || [];\n if (defaults.length && !to.value.trim()) {\n to.value = defaults.join(\"\\n\");\n m.clearError(); // an empty-field error raised before the defaults arrived no longer holds\n const hint = $(\"#testDefaultsHint\", m.el);\n hint.textContent = \"Pre-filled from your default test recipients (Settings).\";\n hint.hidden = false;\n }\n })\n .catch(() => {});\n go.onclick = () =>\n busy(go, \"Sending\u2026\", async () => {\n m.clearError();\n const { valid: addrs, invalid } = parseAddresses(to.value);\n const notAddresses = invalidAddressesMessage(invalid);\n if (notAddresses) {\n m.error(notAddresses, to);\n return;\n }\n if (!addrs.length) {\n m.error(\"Enter at least one email address\", to);\n return;\n }\n try {\n if (!locked) {\n await saveDraft(true);\n }\n // One request per address, each on its own, so one that fails doesn't stop the\n // rest, and the dialog can name exactly the ones to try again.\n let sent = 0;\n const failed: string[] = [];\n const reasons = new Set<string>(); // each distinct reason once, in the order met\n let lastWarnings: string[] | null = null;\n for (const addr of addrs) {\n try {\n const r = await api<TestSendResponse>(`/api/posts/${id}/test`, {\n method: \"POST\",\n json: { to: addr },\n });\n if (r.sent) {\n sent++;\n } else {\n failed.push(addr);\n }\n lastWarnings = r.warnings;\n } catch (e) {\n // A post that's gone, or a session that ended, ends the loop: every address\n // after it would fail the same way.\n if (isNotFound(e) || (e instanceof ApiError && e.status === 401)) {\n throw e;\n }\n failed.push(addr);\n reasons.add(describeError(e));\n }\n }\n showWarnings(lastWarnings);\n // A test of a scheduled post settles its re-made condition on the server; read the\n // feed now so the banner's line goes with it rather than at the next idle read.\n if (locked && sent > 0) {\n readSendsNow();\n }\n if (failed.length) {\n // The dialog stays open with only the addresses that didn't get it, so Send again\n // tries those and no one gets a second copy.\n to.value = failed.join(\"\\n\");\n m.error(notSentMessage(sent, addrs.length, failed, [...reasons]));\n return;\n }\n m.close();\n toast(withNoProviderNote(`Test sent to ${sent} address${sent === 1 ? \"\" : \"es\"}`));\n } catch (e) {\n if (isNotFound(e)) {\n m.close();\n foundGone(e);\n return;\n }\n m.error(message(e));\n }\n });\n };\n\n // --- schedule (the one send entry point; Send now lives inside as a demoted link) ---\n // Scheduling behind a cancelable review window is the literal default (SPEC \u00A76);\n // sending immediately is the deliberate sub-choice. Both server flows are unchanged\n // (/schedule, /send) \u2014 this is one modal with two views, so the safer path is what\n // the Primary opens and the louder one is a step down.\n const scheduleBtn = locked ? null : $<HTMLButtonElement>(\"#scheduleBtn\");\n if (scheduleBtn) {\n scheduleBtn.onclick = () => {\n if (!validateSubject()) {\n return; // empty subject: the field-level error is now showing; don't open the modal\n }\n const minStr = toLocalInput(earliestFireAt());\n const def = toLocalInput(new Date(Date.now() + 24 * 3600 * 1000));\n // While no provider is connected the server would refuse both views, so the dialog\n // drops the picker and the Send now row and leaves only the standing line (DESIGN \u00A74).\n const notConnected = providerNotConnected();\n const scheduleView = html`<h3 id=\"schHead\">Schedule this post</h3><p class=\"hint\">It sends at the time you pick, with a cancelable window until then.</p>${notConnectedScheduleLine()}${dailyWritesLine()}${notConnected ? null : html`<label for=\"schWhen\">Send at</label><input type=\"datetime-local\" id=\"schWhen\" min=\"${minStr}\" value=\"${def}\">`}<div class=\"actions\"><button type=\"button\" id=\"schCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"schGo\">Schedule</button></div>${notConnected ? null : html`<div class=\"altrow\"><span class=\"altrow-note\">Or send as soon as possible.</span><button type=\"button\" class=\"linkbtn\" id=\"toSendNow\">Send now \u2192</button></div>`}`;\n const m = modal(scheduleView, signal);\n const box = $(\".modal\", m.el);\n // Who a send would reach now: the confirmed addresses that aren't suppressed (SPEC \u00A76),\n // read at most once, when a view first needs it. Neither view waits on it: their copy\n // reads sensibly until then.\n let audience: Promise<number | null> | null = null;\n const readAudience = () => {\n audience ??= api<SubscriberListResponse>(\n \"/api/subscribers?status=confirmed&suppressed=hide&limit=1\",\n { signal },\n )\n .then((s) => s.page.total)\n .catch(() => null);\n return audience;\n };\n // On Workers Free's limits, a send this large may spend D1's daily writes. The line\n // is the scheduled send's own `daily_writes` warning, in the same words, shown before\n // it is scheduled; it never disables a control (DESIGN \u00A74).\n const paintDailyWrites = async () => {\n if (!box.querySelector(\"#schDailyWrites\")) {\n return; // not on Free's limits, or no provider to send through\n }\n const n = await readAudience();\n const el = box.querySelector<HTMLElement>(\"#schDailyWrites\"); // gone if closed or flipped\n if (el && n !== null && n > DAILY_WRITES_SEND_RECIPIENTS) {\n el.textContent = dailyWritesSendWarning(n);\n el.hidden = false;\n }\n };\n // A refused save closes the dialog, and the out-of-date banner it was covering is\n // the whole answer \u2014 so make sure the publisher is actually looking at it: they\n // clicked Schedule at the foot of the post and the banner sits at its head. Only\n // here, never in showConflict: the freshness poll raises the same banner on its own\n // every ten seconds, and scrolling the page out from under someone mid-sentence to\n // announce it would be its own bug. `nearest` leaves an already-visible banner alone.\n const refused = () => {\n m.close();\n freshnessEl.scrollIntoView({ block: \"nearest\" });\n };\n\n const doSchedule = () =>\n busy($<HTMLButtonElement>(\"#schGo\", box), \"Scheduling\u2026\", async () => {\n m.clearError();\n const whenEl = $<HTMLInputElement>(\"#schWhen\", box);\n const t = whenEl.value ? new Date(whenEl.value).getTime() : Number.NaN;\n if (Number.isNaN(t)) {\n m.error(\"Pick a valid date & time\", whenEl);\n return;\n }\n if (isTooSoon(t)) {\n m.error(tooSoonText(), whenEl);\n return;\n }\n try {\n // A refused save means the server holds another writer's newer revision, so\n // scheduling now would freeze content the publisher never saw (SPEC \u00A76).\n // Close the dialog and let the out-of-date banner behind it be the next step.\n if ((await saveDraft(true)) === \"refused\") {\n refused();\n return;\n }\n await api<ScheduleResponse>(`/api/posts/${id}/schedule`, {\n method: \"POST\",\n json: { fire_at: new Date(t).toISOString() },\n });\n m.close();\n // Stay on the post, re-rendered in its scheduled state: the window is the\n // review and this page is the review surface (SPEC \u00A76, DESIGN \u00A73).\n toast(withNoProviderNote(`Scheduled for ${fmt(t)}. Send yourself a test.`));\n remount();\n } catch (e) {\n if (isNotFound(e)) {\n m.close();\n foundGone(e);\n return;\n }\n // The server's clock is the authority on the lead; its refusal, in the same\n // words as the editor's own check, goes under the picker.\n if (e instanceof ApiError && e.field === \"fire_at\") {\n m.error(message(e), $<HTMLInputElement>(\"#schWhen\", box));\n return;\n }\n m.error(message(e));\n }\n });\n\n const doSendNow = () =>\n busy($<HTMLButtonElement>(\"#snGo\", box), \"Queuing\u2026\", async () => {\n m.clearError();\n try {\n // As in doSchedule: a refused save must not become a frozen send.\n if ((await saveDraft(true)) === \"refused\") {\n refused();\n return;\n }\n // The toast names the time the server answered, not the lead: the fire time is\n // rounded up to the minute (SPEC \u00A76), so it can be up to a minute past the lead.\n const { send } = await api<ScheduleResponse>(`/api/posts/${id}/send`, {\n method: \"POST\",\n });\n m.close();\n toast(withNoProviderNote(`Sends at ${fmt(send.fire_at)}, cancelable until then.`));\n remount();\n } catch (e) {\n if (isNotFound(e)) {\n m.close();\n foundGone(e);\n return;\n }\n m.error(message(e));\n }\n });\n\n function wireSchedule() {\n box.setAttribute(\"aria-labelledby\", \"schHead\");\n $(\"#schCancel\", box).onclick = m.close;\n $(\"#schGo\", box).onclick = doSchedule;\n // The server would refuse a schedule; the standing line above says why and where to\n // fix it, and the picker and Send now row are left out of the view.\n $<HTMLButtonElement>(\"#schGo\", box).disabled = notConnected;\n if (notConnected) {\n $(\"#schCancel\", box).focus();\n return;\n }\n $(\"#toSendNow\", box).onclick = showSendNow;\n $(\"#schWhen\", box).focus();\n void paintDailyWrites();\n }\n\n async function showSendNow() {\n setHtml(\n box,\n html`<h3 id=\"snHead\">Send now?</h3><p class=\"hint\">Sends this draft, as it is now, to <strong id=\"snWho\">your subscribers</strong> in ${sendNowWhenText()}. The post locks until then, and you can cancel any time before it starts sending.</p>${dailyWritesLine()}<div class=\"altrow altrow-top\"><button type=\"button\" class=\"linkbtn\" id=\"toSchedule\">\u2190 Back to schedule</button></div><div class=\"actions\"><button type=\"button\" id=\"snCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"snGo\">Send now</button></div>`,\n );\n box.setAttribute(\"aria-labelledby\", \"snHead\");\n $(\"#snCancel\", box).onclick = m.close;\n $(\"#snGo\", box).onclick = doSendNow;\n $<HTMLButtonElement>(\"#snGo\", box).disabled = providerNotConnected();\n $(\"#toSchedule\", box).onclick = () => {\n setHtml(box, scheduleView);\n wireSchedule();\n };\n $(\"#snGo\", box).focus();\n void paintDailyWrites();\n // Fill the count once known. The confirmed count would still include the suppressed.\n const n = await readAudience();\n const whoEl = box.querySelector(\"#snWho\"); // gone if the view flipped back meanwhile\n if (whoEl && n !== null) {\n whoEl.textContent = `${n} recipient${n === 1 ? \"\" : \"s\"}`;\n }\n }\n\n wireSchedule();\n };\n }\n return handle;\n}\n\n/** What the scheduled banner reads of its send. `actions` is absent only until the send's\n * own read answers (the post's read does not carry them). */\ntype BannerSend = Pick<SendView, \"id\" | \"fire_at\" | \"phase\" | \"conditions\" | \"remade_at\"> &\n Partial<Pick<SendView, \"actions\">>;\n\n/**\n * Whether the send has moved on from scheduled, found on the editor's own read of it: once\n * it has started its send page is its home, and once canceled its post is a draft again,\n * which a fresh read of the post shows.\n */\nfunction movedOn(send: SendView, remount: () => void): boolean {\n if (send.status === \"sending\" || send.status === \"sent\") {\n handOff(`#/sent/${send.id}`);\n return true;\n }\n if (send.status === \"canceled\") {\n remount();\n return true;\n }\n return false;\n}\n\n/**\n * The scheduled banner's words for where its send stands (DESIGN \u00A74), in the cards' own\n * formats: the fire time and a countdown while the window is open; from the fire time, the\n * same line with \"Preparing to send\u2026\" in place of the countdown; past the server's missed\n * tolerance, how late it is, in the danger tone.\n */\nfunction scheduledBannerHtml(send: BannerSend, due: boolean): Html {\n if (has(send, \"missed\")) {\n return html`Scheduled for <strong>${fmt(send.fire_at)}</strong> \u00B7 <span class=\"countdown countdown-missed\">${lateStr(send.fire_at)}</span>`;\n }\n if (due) {\n return html`Scheduled for <strong>${fmt(send.fire_at)}</strong> \u00B7 <span class=\"countdown\">${untilStr(send.fire_at, true)}</span>`;\n }\n return html`Scheduled for <strong>${fmt(send.fire_at)}</strong> \u00B7 <span class=\"countdown\">${untilStr(send.fire_at)}</span>`;\n}\n\n/** Whether a read or write found its post (or its send, which goes with it) gone. */\nfunction isNotFound(e: unknown): boolean {\n return e instanceof ApiError && e.status === 404;\n}\n\n/** Where a post's send now lives, when that is no longer the editor: the watch while it\n * sends, the record once it has sent. */\nfunction sendHome(data: PostResponse): string | null {\n if (data.sending) {\n return `#/sent/${data.sending.id}`;\n }\n if (data.post.status === \"sent\") {\n return data.sent ? `#/sent/${data.sent.id}` : \"#/sent\";\n }\n return null;\n}\n\n/**\n * Move to a send's new home in place of this page's history entry (DESIGN \u00A74): the\n * editor URL the reader opened has become the send page, so Back goes where they came\n * from rather than to an editor that would only hand them forward again.\n */\nfunction handOff(hash: string): void {\n location.replace(hash);\n}\n\n// A tab hidden while a poll ticks skips those ticks; shown again, it reads at once rather\n// than at the next tick, since anything may have happened meanwhile.\nfunction readWhenShown(read: () => unknown, signal: AbortSignal): void {\n document.addEventListener(\n \"visibilitychange\",\n () => {\n if (!document.hidden) {\n read();\n }\n },\n { signal },\n );\n}\n", "// The in-app docs room: the setup guide fetched from the authed /api/docs routes.\n\nimport type { DocFragment, DocsLanding, DocsResponse } from \"../../shared/docs\";\nimport { api, describeError } from \"../api\";\nimport { mount } from \"../lifecycle\";\nimport { appState } from \"../state\";\nimport { $, $$ } from \"../ui/dom\";\nimport { type Html, html, setHtml, unsafeHtml } from \"../ui/html\";\nimport { renderError, toast } from \"../ui/widgets\";\nimport { roomShell } from \"./shell\";\n\n// The setup guide, authored in docs/README.md and its section folders, and served read-only by the authed\n// GET /api/docs route as sanitized HTML fragments. `#/docs` is the index \u2014 an intro over the\n// guide's sections, the main path numbered as steps; `#/docs/:slug` is one doc, and\n// `#/docs/:slug/:anchor` one heading on it; its rail a back-link to the index\n// plus that doc's \"On this page\" (never a tree of all docs). No iframe: the content is\n// trusted (repo markdown, hygiene-passed), so injecting the fragments is safe. Fetched once\n// and cached (the bundle never changes at runtime), so paging is instant.\nlet docsCache: DocsResponse | null = null;\n\n// A doc card's blurb on the index: the doc's own first paragraph, condensed. Derived here\n// (docs carry no front-matter description) so it stays in sync with the doc itself.\nfunction docDescription(doc: DocFragment): string {\n const tmp = document.createElement(\"div\");\n // The API's rendered HTML, parsed only to read its first paragraph; never attached.\n setHtml(tmp, unsafeHtml(doc.html || \"\"));\n const raw = (tmp.querySelector(\"p\")?.textContent || \"\").trim().replace(/\\s+/g, \" \");\n // A paragraph that leads into a list ends in \":\" \u2014 on a card that reads as a cut-off\n // sentence, so it ends in an ellipsis like a truncated blurb does.\n const leadsIn = /[:;,]$/.test(raw);\n const text = raw.replace(/[\\s:;,]+$/, \"\");\n const MAX = 150;\n if (text.length > MAX) {\n return `${text.slice(0, MAX).replace(/[\\s.,;:]+\\S*$/, \"\")}\u2026`;\n }\n return leadsIn ? `${text}\u2026` : text;\n}\n\nexport async function renderDocs(\n slug: string | undefined,\n root: HTMLElement,\n signal: AbortSignal,\n anchor?: string,\n): Promise<void> {\n setHtml(\n root,\n roomShell(\"docs\", null, html`<div class=\"docs-index\"><p class=\"muted\">Loading\u2026</p></div>`),\n );\n if (!docsCache) {\n // The first visit fetches (with the mount's signal, so a tap on API or a different\n // doc while it is in flight cuts it off rather than let it paint a stale room).\n try {\n docsCache = await api<DocsResponse>(\"/api/docs\", { signal });\n } catch (e) {\n if (!signal.aborted) {\n renderError($(\".room-main\", root), describeError(e), () =>\n mount((r, s) => renderDocs(slug, r, s, anchor)),\n );\n }\n return;\n }\n }\n const { landing, docs } = docsCache;\n if (!docs.length) {\n setHtml($(\".room-main\"), html`<p class=\"muted\">No documentation.</p>`);\n return;\n }\n if (slug) {\n renderDocPage(root, landing, docs, slug, anchor ? decodeAnchor(anchor) : undefined);\n } else {\n renderDocsIndex(root, landing, docs);\n }\n}\n\n/** The route's anchor segment as the heading's anchor. A malformed escape in a hand-edited\n * link opens the page at its top rather than failing the room. */\nfunction decodeAnchor(anchor: string): string | undefined {\n try {\n return decodeURIComponent(anchor);\n } catch {\n return undefined;\n }\n}\n\n// The index: the landing page `docs/README.md` lays out (its title, intro, and sections),\n// with a card per doc. A section the README lists as numbered shows numbers, since its pages\n// are steps taken in order; the others are picked from. The cards' titles and blurbs come\n// from the docs themselves. It keeps the\n// room's two-column shape (a doc page's rail is that doc's \"On this page\"); here the rail\n// holds the project's external links \u2014 the reference room is about Kestrel itself, so this\n// is where getkestrel.dev and the source live.\nfunction renderDocsIndex(root: HTMLElement, landing: DocsLanding, docs: DocFragment[]): void {\n const card = (d: DocFragment, n: number | null): Html => {\n const desc = docDescription(d);\n return html`<li><a class=\"doc-card\" href=\"#/docs/${d.slug}\">${n === null ? null : html`<span class=\"doc-card-n\">${String(n).padStart(2, \"0\")}</span>`}<span class=\"doc-card-main\"><span class=\"doc-card-t\">${d.title} <span class=\"doc-card-go\" aria-hidden=\"true\">\u2192</span></span>${desc ? html`<span class=\"doc-card-d\">${desc}</span>` : null}</span></a></li>`;\n };\n const sections = landing.sections.map((sec) => {\n const inSection = docs.filter((d) => d.section === sec.id);\n if (!inSection.length) {\n return null;\n }\n const cards = inSection.map((d, i) => card(d, sec.numbered ? i + 1 : null));\n // The API's rendered HTML: the README's own blurb, hygiene-passed by the Worker.\n const blurb = sec.blurb\n ? html`<div class=\"doc-section-blurb\">${unsafeHtml(sec.blurb)}</div>`\n : null;\n return html`<section class=\"doc-section\"><h2 class=\"doc-section-t\">${sec.title}</h2>${blurb}${sec.numbered ? html`<ol class=\"doc-cards\">${cards}</ol>` : html`<ul class=\"doc-cards\">${cards}</ul>`}</section>`;\n });\n const main = html`<div class=\"docs-index\"><p class=\"eyebrow\">Documentation</p><h1>${landing.title}</h1><div class=\"docs-index-intro\">${unsafeHtml(landing.intro)}</div>${sections}</div>`;\n // Three uniform out-links under a \"Kestrel\" label \u2014 the same shape as the API rail's\n // label + tiers, so mobile can give both the same chip row. getkestrel.dev is the\n // project's home, the same for every instance, so it's a constant; the source and\n // license links are the deploy's own repo (package.json \u2192 build stamp), so they're\n // absent when no repo is known.\n // Each link carries a short form for the mobile chip row (styles.css swaps which span\n // shows); the full label stays the accessible name at every width.\n const repoUrl = appState.appConfig?.deployment.build.repoUrl || \"\";\n const out = (href: string, label: string, short = label): Html =>\n html`<a class=\"rail-link\" href=\"${href}\" target=\"_blank\" rel=\"noopener\" aria-label=\"${label}\"><span class=\"rail-link-full\">${label}</span><span class=\"rail-link-short\" aria-hidden=\"true\">${short}</span> <span aria-hidden=\"true\">\u2197</span></a>`;\n const rail = html`<div class=\"toc-label\">Kestrel</div>${out(\"https://getkestrel.dev\", \"Project site\", \"Project\")}${\n repoUrl\n ? [out(repoUrl, \"Source on GitHub\", \"Source\"), out(`${repoUrl}/blob/HEAD/LICENSE`, \"License\")]\n : null\n }`;\n setHtml(root, roomShell(\"docs\", rail, main));\n window.scrollTo(0, 0);\n}\n\n// One doc, deep-linked by slug. The rail is this doc's \"On this page\" (scroll-spy-tracked)\n// \u2014 never a list of the other docs. Sequential Prev/Next sits on the title line (compact)\n// and at the foot of the article (with titles), not in the rail.\n//\n// The rail folds on mobile: \"On this page\" is a <details> that is open on desktop (where\n// the summary is inert \u2014 it just looks like the label) and closed on mobile, where it is\n// one tappable row above the article and closes again once a section is picked. The state\n// follows the layout, not the width at render time: crossing 720px (a rotation, a resized\n// window) re-opens it on desktop \u2014 where nothing else could, the summary being inert \u2014\n// and takes the summary out of the tab order there, so a keyboard user can't collapse a\n// list no pointer can reopen. One listener for the app's lifetime, registered by the first\n// doc page rendered (the view's own, so not boot's); it finds the fold that is in the DOM,\n// if any.\nconst mobileMq = matchMedia(\"(max-width: 720px)\");\nfunction syncFold(fold: HTMLDetailsElement | null): void {\n if (!fold) {\n return;\n }\n const mobile = mobileMq.matches;\n fold.open = !mobile;\n $(\"summary\", fold).tabIndex = mobile ? 0 : -1;\n}\n// The fold is only in the DOM on a doc page, so a nullable lookup is the right one here.\nconst findFold = () => document.querySelector<HTMLDetailsElement>(\"details#tocOnPage\");\nlet foldFollowsLayout = false;\nfunction followLayout(): void {\n if (foldFollowsLayout) {\n return;\n }\n foldFollowsLayout = true;\n mobileMq.addEventListener(\"change\", () => syncFold(findFold()));\n}\n\ninterface DocSection {\n id: string;\n /** The heading's anchor, the last segment of its route; empty for the title. */\n anchor: string;\n title: string;\n}\n\n/** A heading's anchor as GitHub derives it from the heading's text: lowercased, with\n * punctuation dropped and each space a hyphen. The guide is read on GitHub too, so a link\n * written against GitHub's anchors (`05-verify.md#7-read-the-logs`) works here unchanged. */\nexport function headingAnchor(text: string): string {\n return text\n .trim()\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}\\s_-]/gu, \"\")\n .replace(/\\s/g, \"-\");\n}\n\n// Element ids carry a prefix, so a heading can never take an id the shell already uses.\nconst headingId = (anchor: string): string => `doc-h-${anchor}`;\n\nfunction renderDocPage(\n root: HTMLElement,\n landing: DocsLanding,\n docs: DocFragment[],\n slug: string,\n anchor?: string,\n): void {\n const at = docs.findIndex((d) => d.slug === slug);\n const cur = docs[at];\n if (!cur) {\n // A stale or renamed deep link shouldn't masquerade as a doc \u2014 heal to the index.\n toast(`No doc named \u201C${slug}\u201D, so here\u2019s the index.`);\n history.replaceState(history.state, \"\", \"#/docs\");\n renderDocsIndex(root, landing, docs);\n return;\n }\n // Prev/Next stay within the doc's section: the main path ends at its last step rather than\n // running on into the guides after it, which are picked from, not read in order.\n const siblings = docs.filter((d) => d.section === cur.section);\n const pos = siblings.indexOf(cur);\n const prev = siblings[pos - 1];\n const next = siblings[pos + 1];\n // Compact Prev/Next on the title line, right of the H1 \u2014 no titles (the foot pager\n // carries those; each link's aria-label names its target). On mobile the words drop\n // and the arrows alone remain, so the row still fits beside a wrapping title.\n const topLink = (doc: DocFragment, dir: \"Previous\" | \"Next\"): Html =>\n html`<a href=\"#/docs/${doc.slug}\" aria-label=\"${dir}: ${doc.title}\">${\n dir === \"Previous\"\n ? html`<span aria-hidden=\"true\">\u2190</span><span class=\"doc-topnav-word\">Previous</span>`\n : html`<span class=\"doc-topnav-word\">Next</span><span aria-hidden=\"true\">\u2192</span>`\n }</a>`;\n const topNav =\n prev || next\n ? html`<nav class=\"doc-topnav\" aria-label=\"Adjacent docs\">${prev ? topLink(prev, \"Previous\") : null}${next ? topLink(next, \"Next\") : null}</nav>`\n : null;\n // The rail gets a slot, filled once the sections are known (below) \u2014 the slot, not the\n // whole rail, so the build stamp roomShell set under it stays.\n setHtml(\n root,\n roomShell(\n \"docs\",\n html`<div id=\"docToc\"></div>`,\n html`<article class=\"doc\" id=\"docsMain\"></article>`,\n ),\n );\n const navEl = $(\"#docToc\");\n const mainEl = $(\"#docsMain\");\n // The API's rendered HTML: the repo's own Markdown, hygiene-passed by the Worker.\n setHtml(\n mainEl,\n html`<section class=\"doc-part\" id=\"doc-${cur.slug}\">${unsafeHtml(cur.html)}</section>`,\n );\n\n // The fragment carries no ids \u2014 give every heading GitHub's anchor for it (a repeat gets\n // GitHub's -1, -2), and collect the H1 and H2s for \"On this page\". The H1 leads so\n // there's a way back to the top.\n const sec = $(\"section.doc-part\", mainEl);\n const seen = new Map<string, number>();\n for (const h of $$<HTMLHeadingElement>(\"h1, h2, h3, h4\", sec)) {\n const base = headingAnchor(h.textContent || \"\");\n const n = seen.get(base) ?? 0;\n seen.set(base, n + 1);\n h.dataset.anchor = n ? `${base}-${n}` : base;\n h.id = headingId(h.dataset.anchor);\n }\n const h1 = sec.querySelector(\"h1\");\n const sections: DocSection[] = [];\n if (h1) {\n sections.push({ id: h1.id, anchor: \"\", title: h1.textContent || cur.title });\n }\n // Put the H1 and the compact pager on one line: wrap the fragment's own H1 in a title\n // row and set the pager beside it (the H1 stays the doc's H1 \u2014 same node, same id).\n if (topNav) {\n const head = document.createElement(\"div\");\n head.className = \"doc-head\";\n setHtml(head, topNav);\n if (h1) {\n h1.before(head);\n head.prepend(h1);\n } else {\n sec.prepend(head);\n }\n }\n for (const h2 of $$<HTMLHeadingElement>(\"h2\", sec)) {\n sections.push({ id: h2.id, anchor: h2.dataset.anchor || \"\", title: h2.textContent || \"\" });\n }\n\n // Previous / Next at the foot \u2014 with titles, the sequential path through the guide.\n const pager = document.createElement(\"nav\");\n pager.className = \"doc-pager\";\n setHtml(\n pager,\n html`${\n prev\n ? html`<a class=\"doc-pager-btn prev\" href=\"#/docs/${prev.slug}\"><span class=\"doc-pager-dir\">\u2190 Previous</span><span class=\"doc-pager-title\">${prev.title}</span></a>`\n : html`<span></span>`\n }${\n next\n ? html`<a class=\"doc-pager-btn next\" href=\"#/docs/${next.slug}\"><span class=\"doc-pager-dir\">Next \u2192</span><span class=\"doc-pager-title\">${next.title}</span></a>`\n : html`<span></span>`\n }`,\n );\n mainEl.appendChild(pager);\n\n // Rail: this doc's \"On this page\" only (scroll-spy-tracked). No back-link (the \"Docs\" tab\n // returns to the index) and no rail pager (Prev/Next is the title line + the article\n // foot). A <details>, open unless this is mobile (the header comment says why).\n const onPage = sections.map(\n (s) =>\n html`<a class=\"toc-sub\" href=\"#/docs/${cur.slug}${s.anchor ? `/${s.anchor}` : \"\"}\" data-target=\"${s.id}\">${s.title}</a>`,\n );\n setHtml(\n navEl,\n sections.length\n ? html`<details class=\"toc-onpage\" id=\"tocOnPage\"><summary class=\"toc-label\">On this page</summary>${onPage}</details>`\n : html``,\n );\n\n // \"On this page\" links jump within the current doc and highlight at once. On mobile the\n // fold closes first, so the page height above the target is settled before the scroll\n // is measured.\n const onPageEl = findFold();\n syncFold(onPageEl); // open + inert on desktop, closed + tappable on mobile (before paint)\n followLayout();\n const markActive = (id: string) => {\n if (!onPageEl) {\n return;\n }\n for (const a of $$(\".toc-sub\", onPageEl)) {\n a.classList.toggle(\"on\", a.dataset.target === id);\n }\n };\n navEl.addEventListener(\"click\", (ev) => {\n const a =\n ev.target instanceof Element ? ev.target.closest<HTMLElement>(\"a[data-target]\") : null;\n const target = a?.dataset.target;\n if (!target) {\n return;\n }\n ev.preventDefault();\n markActive(target);\n if (onPageEl && mobileMq.matches) {\n onPageEl.open = false;\n }\n // The address names the section, so it can be shared, without a hashchange that would\n // render the page again.\n history.replaceState(history.state, \"\", a?.getAttribute(\"href\") ?? location.hash);\n document.getElementById(target)?.scrollIntoView({ block: \"start\" });\n });\n\n // A link out of the guide (a provider's sign-up page, say) opens beside the app rather\n // than replacing it, so the reader keeps their place in the step they're on.\n for (const a of $$<HTMLAnchorElement>(\"a[href^='http']\", mainEl)) {\n a.target = \"_blank\";\n a.rel = \"noopener\";\n }\n\n // Copy buttons on the guide's shell / DNS code blocks.\n for (const pre of $$<HTMLPreElement>(\"pre\", mainEl)) {\n pre.classList.add(\"has-copy\");\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.className = \"code-copy\";\n btn.textContent = \"Copy\";\n btn.addEventListener(\"click\", async () => {\n const code = pre.querySelector(\"code\");\n try {\n await navigator.clipboard.writeText((code || pre).innerText);\n btn.textContent = \"Copied\";\n setTimeout(() => {\n btn.textContent = \"Copy\";\n }, 1500);\n } catch {\n toast(\"Couldn\u2019t copy to clipboard\");\n }\n });\n pre.appendChild(btn);\n }\n\n // Scroll-spy: the active section is the last heading scrolled above a line just under the\n // sticky room bar; at the bottom the last heading wins so a short final section still\n // highlights. One document.onscroll slot, self-cleared once this doc leaves the DOM.\n const ids = sections.map((s) => s.id);\n const first = ids[0];\n if (onPageEl && first) {\n const last = ids[ids.length - 1] ?? first;\n // Just under the sticky bar. Measured, not assumed: the bar is --bar-h at every width\n // (see roomShell), but large-text zoom can push it taller, and the spy should follow.\n const barH = document.querySelector(\".room-bar\")?.getBoundingClientRect().height || 54;\n const line = barH + 26;\n const spy = () => {\n if (!document.getElementById(first)) {\n document.onscroll = null;\n return;\n }\n let active = first;\n if (Math.ceil(window.innerHeight + window.scrollY) >= document.documentElement.scrollHeight) {\n active = last;\n } else {\n for (const id of ids) {\n if (\n (document.getElementById(id)?.getBoundingClientRect().top ??\n Number.POSITIVE_INFINITY) <= line\n ) {\n active = id;\n } else {\n break;\n }\n }\n }\n markActive(active);\n };\n document.onscroll = spy;\n spy();\n }\n\n // Each doc is its own page \u2014 start at the top, or at the heading the route names.\n const heading = anchor ? document.getElementById(headingId(anchor)) : null;\n if (heading) {\n heading.scrollIntoView({ block: \"start\" });\n } else {\n window.scrollTo(0, 0);\n }\n}\n", "// The curl command the API reference offers for a route: the method, the URL on this\n// instance, the credential headers its tier needs, and the example body when it has one.\n\nimport type { ReferenceEntry } from \"../../shared/reference\";\n\n/** How this instance authenticates the admin tier, as the boot probe reported it. */\nexport type AuthMode = \"access\" | \"dev\";\n\n/**\n * The credential headers an admin call carries: the Access service token's pair once\n * deployed (the names the setup guide uses), or the dev token locally (as in the README).\n */\nfunction authLines(mode: AuthMode): string[] {\n return mode === \"access\"\n ? [\n '-H \"CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID\"',\n '-H \"CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET\"',\n ]\n : ['-H \"Authorization: Bearer $TOKEN\"'];\n}\n\n/**\n * The route's path as a URL path to type: the pattern syntax a URL never carries (a\n * parameter's regex, an optional segment) is dropped, and `:id`-style parameters stay as\n * the placeholders to replace. The reference shows its rows this way too.\n */\nexport function concretePath(path: string): string {\n return path.replace(/\\([^)]*\\)/g, \"\").replace(/\\{[^}]*\\}\\??/g, \"\") || \"/\";\n}\n\n/**\n * A copy-paste curl command for a route on this instance, or null for a webhook, which\n * only the email provider can call (it must carry the provider's signature).\n */\nexport function curlCommand(r: ReferenceEntry, origin: string, mode: AuthMode): string | null {\n if (r.access === \"webhook\") {\n return null;\n }\n // A query parameter the route can't do without rides the URL, as a placeholder like a path's.\n const required = (r.query ?? []).filter((q) => q.required);\n const search = required.length ? `?${required.map((q) => `${q.name}=:${q.name}`).join(\"&\")}` : \"\";\n const url = `\"${origin}${concretePath(r.path)}${search}\"`;\n const lines = [r.method === \"GET\" ? `curl ${url}` : `curl -X ${r.method} ${url}`];\n if (r.access === \"admin\") {\n lines.push(...authLines(mode));\n }\n if (r.example?.request !== undefined) {\n // Single-quoted for the shell, so a quote inside the JSON closes, escapes, and reopens.\n const body = JSON.stringify(r.example.request).replaceAll(\"'\", `'\\\\''`);\n lines.push('-H \"Content-Type: application/json\"', `-d '${body}'`);\n }\n return lines.join(\" \\\\\\n \");\n}\n", "// The API reference room, rendered from /api/reference.\n\nimport type {\n QueryParam,\n ReferenceEntry,\n ReferenceGroup,\n ReferenceResource,\n ReferenceResponse,\n} from \"../../shared/reference\";\nimport { api, describeError } from \"../api\";\nimport { mount } from \"../lifecycle\";\nimport { appState } from \"../state\";\nimport { codeBlock, wireCopy } from \"../ui/copy\";\nimport { $, $$ } from \"../ui/dom\";\nimport { highlightCurl, highlightJson } from \"../ui/highlight\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { renderError } from \"../ui/widgets\";\nimport { concretePath, curlCommand } from \"./curl\";\nimport { roomShell } from \"./shell\";\n\n// Every route the app and Claude can call, generated from the route manifest\n// (src/app.ts) and served as JSON by the authed /api/reference route. The SPA renders\n// it natively: a rail of tiers and their resources beside the route list, where each\n// route is one scannable row, a native <details>, that opens in place to its full\n// documentation.\nfunction apiExample(label: string, value: unknown): Html | null {\n return value === undefined\n ? null\n : codeBlock(label, highlightJson(value), JSON.stringify(value, null, 2));\n}\n/** The route as a curl command on this instance, with the credential its tier needs. */\nfunction apiCurl(r: ReferenceEntry): Html | null {\n const cmd = curlCommand(r, location.origin, appState.session?.auth.mode ?? \"dev\");\n return cmd === null ? null : codeBlock(\"curl\", highlightCurl(cmd), cmd);\n}\n// Query params for a list route, rendered as a name\u2192description table so the\n// generated reference documents filter/sort/pagination from the registration.\nfunction apiQueryHtml(query: QueryParam[] | undefined): Html | null {\n if (!query?.length) {\n return null;\n }\n const rows = query.map(\n (q) =>\n html`<tr><td><code>${q.name}</code>${q.required ? html` <span class=\"api-req\">required</span>` : null}</td><td class=\"muted\">${q.description}</td></tr>`,\n );\n return html`<div class=\"block\"><div class=\"block-head\"><span class=\"block-label\">Query</span></div><table class=\"api-query\"><tbody>${rows}</tbody></table></div>`;\n}\n// The body types a write accepts, from the registration the router enforces them by. A\n// route that takes no body lists none.\nfunction apiAcceptsHtml(accepts: readonly string[] | undefined): Html | null {\n if (!accepts?.length) {\n return null;\n }\n const rows = accepts.map((t) => html`<tr><td><code>${t}</code></td></tr>`);\n return html`<div class=\"block\"><div class=\"block-head\"><span class=\"block-label\">Content-Type</span></div><table class=\"api-query api-accepts\"><tbody>${rows}</tbody></table></div>`;\n}\n/**\n * The path as a URL path to type (the curl command's own, without the router's pattern\n * syntax), with its parameters marked so the variable parts read at a glance.\n */\nfunction apiPathHtml(path: string): Html {\n return html`${concretePath(path)\n .split(/(:[A-Za-z_]+)/)\n .map((part, i) => (i % 2 ? html`<span class=\"api-param\">${part}</span>` : part))}`;\n}\nfunction apiRouteHtml(r: ReferenceEntry): Html {\n // What the filter matches: the method, the path, and the one-line summary.\n const hay = `${r.method} ${concretePath(r.path)} ${r.summary}`.toLowerCase();\n return html`<li data-hay=\"${hay}\"><details class=\"api-route\">\n <summary class=\"api-route-sum\">\n <span class=\"api-method m-${r.method}\">${r.method}</span>\n <code class=\"api-path\">${apiPathHtml(r.path)}</code>\n <span class=\"api-route-line\">${r.summary}</span>\n <span class=\"api-chev\" aria-hidden=\"true\"></span>\n </summary>\n <div class=\"api-route-detail\">\n <p class=\"api-summary\">${r.summary}</p>\n ${r.description ? html`<p class=\"api-desc muted\">${r.description}</p>` : null}\n ${apiQueryHtml(r.query)}\n ${apiAcceptsHtml(r.accepts)}\n ${apiExample(\"Request\", r.example?.request)}\n ${apiExample(\"Response\", r.example?.response)}\n ${apiCurl(r)}\n </div>\n </details></li>`;\n}\n/** A resource's routes within a tier, in the order the tier lists them. */\nconst routesIn = (g: ReferenceGroup, res: ReferenceResource) =>\n g.routes.filter((r) => r.resource === res.key);\nfunction apiResourceHtml(g: ReferenceGroup, res: ReferenceResource): Html {\n const sec = `${g.access}-${res.key}`;\n const routes = routesIn(g, res);\n return html`<div class=\"api-res\" id=\"api-${sec}\" data-sec=\"${sec}\">\n <div class=\"api-res-head\"><h3>${res.title}</h3></div>\n <ul class=\"api-rows\">${routes.map(apiRouteHtml)}</ul>\n </div>`;\n}\nfunction apiSectionHtml(g: ReferenceGroup): Html {\n return html`<section class=\"api-section\" id=\"api-${g.access}\" data-tier=\"${g.access}\">\n <h2>${g.title}</h2>\n <p class=\"api-blurb muted\">${g.blurb}</p>\n ${g.resources.map((res) => apiResourceHtml(g, res))}\n </section>`;\n}\n// The rail: each tier, then its resources beneath it. A tier with one resource lists only\n// the tier, since the resource would repeat it. No counts: the rail is for moving.\nfunction apiNavHtml(groups: ReferenceGroup[]): Html {\n return html`${groups.map(\n (g) =>\n html`<a href=\"#/reference\" class=\"api-nav-tier\" data-sec=\"${g.access}\" data-tier=\"${g.access}\">${g.title}</a>${\n g.resources.length > 1\n ? g.resources.map(\n (res) =>\n html`<a href=\"#/reference\" class=\"api-nav-res\" data-sec=\"${g.access}-${res.key}\">${res.title}</a>`,\n )\n : null\n }`,\n )}`;\n}\n\n/**\n * Show only the routes whose method, path, or summary contains every word of the query,\n * with the resources, tiers, and rail links that still hold one. An empty query shows\n * everything. The last tier still shown is marked, so it keeps the room to scroll\n * up under the bar that the last tier has.\n */\nfunction applyFilter(query: string, navEl: HTMLElement, contentEl: HTMLElement): void {\n const terms = query.trim().toLowerCase().split(/\\s+/).filter(Boolean);\n let shown = 0;\n for (const sec of $$<HTMLElement>(\".api-section\", contentEl)) {\n let inTier = 0;\n for (const res of $$<HTMLElement>(\".api-res\", sec)) {\n let inRes = 0;\n for (const li of $$<HTMLElement>(\"li[data-hay]\", res)) {\n const hit = terms.every((w) => (li.dataset.hay ?? \"\").includes(w));\n li.hidden = !hit;\n inRes += hit ? 1 : 0;\n }\n res.hidden = inRes === 0;\n const link = navEl.querySelector<HTMLElement>(`a[data-sec=\"${res.dataset.sec}\"]`);\n if (link) {\n link.hidden = inRes === 0;\n }\n inTier += inRes;\n }\n sec.hidden = inTier === 0;\n const tierLink = navEl.querySelector<HTMLElement>(`a[data-tier=\"${sec.dataset.tier}\"]`);\n if (tierLink) {\n tierLink.hidden = inTier === 0;\n }\n shown += inTier;\n }\n const sections = $$<HTMLElement>(\".api-section\", contentEl);\n const lastShown = sections.filter((sec) => !sec.hidden).at(-1);\n for (const sec of sections) {\n sec.classList.toggle(\"api-last\", sec === lastShown);\n }\n $<HTMLElement>(\"#apiEmpty\", contentEl).hidden = shown > 0;\n}\n\n/**\n * Highlight the rail link for what the reader is in: the last tier or resource heading\n * scrolled above a line just under the sticky room bar (so an instant jump lands right,\n * not only a scroll that passes each heading), or the last one once the page is at its\n * bottom. A resource the rail doesn't list lights its tier; a filtered-out one is skipped.\n */\nfunction spyRail(navEl: HTMLElement, contentEl: HTMLElement): void {\n // Measured, not assumed, as the docs' spy does: large-text zoom can push the bar taller.\n const line = ($(\".room-bar\").getBoundingClientRect().height || 54) + 26;\n const marks = $$(\".api-section, .api-res\", contentEl).filter((el) => !el.closest(\"[hidden]\"));\n // At the bottom, the last heading wins; a page that hasn't scrolled is at its top.\n const atBottom =\n window.scrollY > 0 &&\n Math.ceil(window.innerHeight + window.scrollY) >= document.documentElement.scrollHeight;\n let current = marks[0];\n for (const el of marks) {\n if (atBottom || el.getBoundingClientRect().top <= line) {\n current = el;\n } else {\n break;\n }\n }\n const sec = current?.dataset.sec ?? current?.dataset.tier;\n const tier = current?.closest<HTMLElement>(\".api-section\")?.dataset.tier;\n // A resource link the rail isn't showing (a phone's rail is the tier chips alone) hands\n // the highlight to its tier.\n const links = $$(\"a[data-sec]\", navEl).filter((a) => getComputedStyle(a).display !== \"none\");\n const target =\n links.find((a) => a.dataset.sec === sec) ??\n (tier === undefined ? undefined : links.find((a) => a.dataset.tier === tier));\n for (const a of $$(\"a[data-sec]\", navEl)) {\n a.classList.toggle(\"active\", a === target);\n }\n}\n\nexport async function renderReference(root: HTMLElement, signal: AbortSignal): Promise<void> {\n setHtml(\n root,\n roomShell(\n \"reference\",\n html`<div class=\"toc-label\">API</div><nav class=\"api-nav\" id=\"apiNav\" aria-label=\"API sections\"></nav>`,\n html`<div class=\"api-content\" id=\"apiContent\"><p class=\"muted\">Loading\u2026</p></div>`,\n ),\n );\n const navEl = $(\"#apiNav\");\n const contentEl = $(\"#apiContent\");\n // Delegate clicks synchronously with one listener on the stable nav, so it survives\n // the async fill below: a rail click jumps to that tier or resource.\n navEl.addEventListener(\"click\", (ev) => {\n const a = ev.target instanceof Element ? ev.target.closest<HTMLElement>(\"a[data-sec]\") : null;\n const sec = a?.dataset.sec;\n if (!sec) {\n return;\n }\n ev.preventDefault();\n document.getElementById(`api-${sec}`)?.scrollIntoView({ block: \"start\" });\n });\n let groups: ReferenceGroup[];\n try {\n ({ groups } = await api<ReferenceResponse>(\"/api/reference\", { signal }));\n } catch (e) {\n renderError(contentEl, describeError(e), () => mount(renderReference));\n return;\n }\n\n setHtml(navEl, apiNavHtml(groups));\n setHtml(\n contentEl,\n html`<header class=\"api-head\"><h1>API reference</h1><p class=\"muted\">Generated from the route registration, so every endpoint the app and Claude can call is listed here. Base URL <code>${location.origin}</code>.</p><div class=\"api-filter\">${icon(\"search\")}<input type=\"search\" id=\"apiFilter\" placeholder=\"Filter routes by method, path, or summary\" autocomplete=\"off\" aria-label=\"Filter routes\" aria-keyshortcuts=\"/\"><kbd aria-hidden=\"true\">/</kbd><button type=\"button\" class=\"icon api-filter-clear\" id=\"apiFilterClear\" aria-label=\"Clear filter\" hidden>${icon(\"x\")}</button></div></header>${groups.map(apiSectionHtml)}<p class=\"api-empty muted\" id=\"apiEmpty\" hidden>No routes match. Try a path segment like <code>sends</code> or a method like <code>DELETE</code>.</p>`,\n );\n const filterEl = $<HTMLInputElement>(\"#apiFilter\");\n const clearEl = $(\"#apiFilterClear\");\n const spy = () => spyRail(navEl, contentEl);\n const filterTo = (query: string) => {\n filterEl.value = query;\n clearEl.hidden = query === \"\";\n applyFilter(query, navEl, contentEl);\n spy();\n };\n filterEl.addEventListener(\"input\", () => filterTo(filterEl.value));\n clearEl.addEventListener(\"click\", () => {\n filterTo(\"\");\n filterEl.focus();\n });\n // A code block's Copy puts its plain text (the example, or the curl command) on the\n // clipboard and says so where it sits (ui/copy.ts).\n wireCopy(contentEl);\n // \"/\" jumps to the filter from anywhere in the room (unless typing elsewhere); Escape\n // in the filter clears it. Bound to the view, so it ends when the room does.\n document.addEventListener(\n \"keydown\",\n (ev) => {\n const typing =\n ev.target instanceof HTMLElement &&\n (ev.target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(ev.target.tagName));\n if (ev.key === \"/\" && !typing && !ev.metaKey && !ev.ctrlKey && !ev.altKey) {\n ev.preventDefault();\n filterEl.focus();\n } else if (ev.key === \"Escape\" && ev.target === filterEl && filterEl.value) {\n filterTo(\"\");\n }\n },\n { signal },\n );\n\n // Mark the last tier (the filter keeps it current from here).\n applyFilter(\"\", navEl, contentEl);\n // The rail follows the reading position, and the filter, which moves every heading.\n window.addEventListener(\"scroll\", spy, { passive: true, signal });\n // Its own page \u2014 start at the top, not wherever the last doc was scrolled to.\n window.scrollTo(0, 0);\n spy();\n}\n", "// The sent list: scheduled sends with their countdowns, the sends in progress and any that\n// need the operator, and the record of what has gone out, kept current by the send-state\n// layer (docs/DESIGN.md \u00A74).\n\nimport { earlierCursor } from \"../../shared/cursor\";\nimport type { SendListResponse, SendView } from \"../../shared/sends\";\nimport { api, describeError } from \"../api\";\nimport {\n connectProviderLink,\n heldSendsHtml,\n noEmailProvider,\n notConnectedWarning,\n providerNotConnected,\n} from \"../deployment\";\nimport { mount } from \"../lifecycle\";\nimport {\n followSends,\n readSendsNow,\n type SendStage,\n type SendsUpdate,\n stageOf,\n} from \"../send_state\";\nimport { $, $$ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { html, setHtml } from \"../ui/html\";\nimport {\n type ListState,\n listQuery,\n listToolbar,\n renderPager,\n th,\n wireSort,\n wireToolbar,\n} from \"../ui/list_controls\";\nimport { busy, renderError, toast } from \"../ui/widgets\";\nimport { openRescheduleModal, openResolveModal } from \"./dialogs\";\nimport {\n activeRowHtml,\n can,\n conditionOf,\n countdownHtml,\n countdowns,\n deliveredCell,\n needsOperator,\n sendsNow,\n} from \"./progress\";\n\nconst message = describeError;\n\n/** A scheduled card's Reschedule and Cancel: offered while the server would take them, and\n * hidden at the fire time, when the review window closes (SPEC \u00A76), by the countdown's\n * tick rather than the next read. */\nfunction windowControls(s: SendView) {\n const buttons = [\n can(s, \"reschedule\")\n ? html`<button class=\"ghost\" data-reschedule=\"${s.id}\">Reschedule</button>`\n : null,\n can(s, \"cancel\") ? html`<button class=\"ghost\" data-cancel=\"${s.id}\">Cancel</button>` : null,\n ];\n return buttons.some(Boolean)\n ? html`<div class=\"row\" data-closes=\"${s.fire_at}\">${buttons}</div>`\n : null;\n}\n\nexport async function renderSent(root: HTMLElement, signal: AbortSignal): Promise<void> {\n // The dispatch side: the still-cancelable Scheduled queue on top, then the frozen Sent\n // records. The table is sent-only, so it carries no status column or filter; default\n // sort = fire desc so the \"When\" header shows its arrow from the start.\n const state: ListState = {\n status: \"sent\",\n search: \"\",\n failures: \"\",\n sort: \"fire\",\n dir: \"desc\",\n offset: 0,\n limit: 50,\n };\n setHtml(\n root,\n html`<h1>Sent</h1>\n ${noEmailProvider() ? html`<p class=\"muted\">A local stand-in sends these: they are recorded here, but nothing is delivered.</p>` : null}\n ${notConnectedWarning(\"sent-not-connected\", html`No email provider connected, so nothing can be sent yet. ${connectProviderLink()} to start sending.`)}\n <div id=\"stuck\"></div>\n <h2>Scheduled</h2><div id=\"scheduled\" class=\"muted\">Loading\u2026</div>\n <div id=\"active\"></div>\n <h2>Sent posts</h2>\n ${listToolbar({ searchPlaceholder: \"Search subject\u2026\", failures: true })}\n <div id=\"sendsList\" class=\"muted\">Loading\u2026</div>\n <div id=\"sendsPager\"></div>`,\n );\n const stuckEl = $(\"#stuck\", root);\n const schedEl = $(\"#scheduled\", root);\n const activeEl = $(\"#active\", root);\n const listEl = $(\"#sendsList\", root);\n const pagerEl = $(\"#sendsPager\", root);\n const tickCountdowns = countdowns(root, signal);\n\n // The sends in flight as the page last read them, and every send the layer has reported\n // since, as it now stands: together, what the attention and in-progress sections show.\n let sendingRows: SendView[] = [];\n // Until a read of the sends in flight succeeds, the In progress slot keeps its error and\n // Retry: the layer reports only sends that change, so one refused for an hour would\n // otherwise never show.\n let liveRead = false;\n // Each section paints only its latest read, so a slow earlier one (a queue read for a\n // send turning due, landing after the one for its start) never paints over a newer one.\n let scheduledReads = 0;\n let listReads = 0;\n const reported = new Map<string, SendView>();\n\n // Resolving a wedged send or canceling a scheduled one touches several sections at once:\n // re-read the two this page reads itself, and have the layer read now, so the attention\n // and in-progress sections catch up with the act rather than at the next read.\n function afterAct() {\n loadScheduled();\n loadList();\n readSendsNow();\n }\n\n // The sends in progress: a card each, with a live bar, for every send still sending that\n // doesn't need the operator (that one's home is the attention block above).\n function renderActive(live: SendView[]) {\n const active = live.filter((s) => s.status === \"sending\" && !needsOperator(s));\n setHtml(\n activeEl,\n active.length ? html`<h2>In progress</h2>${active.map(activeRowHtml)}` : html``,\n );\n for (const card of $$<HTMLElement>(\"[data-watch]\", activeEl)) {\n card.onclick = (e) => {\n if (!(e.target instanceof Element && e.target.tagName === \"A\")) {\n location.hash = `#/sent/${card.dataset.watch}`;\n }\n };\n }\n }\n // The sends that need the operator (SPEC \u00A712), each an attention card that stays until\n // its condition clears: repainted from every report of the layer.\n function renderStuck(live: SendView[]) {\n // A refused send carries no control: the fix is in the provider's account or the\n // deployment's secrets, and the send resumes on its own once it lands. Each card is the\n // server's words for its condition. A send held for want of a provider (SPEC \u00A79) is the\n // deployment's state showing on a send, so while the deployment is not connected the\n // held sends and the state are one card in place of the state's amber line, with the\n // fix once, as a link (DESIGN \u00A74).\n const allRefused = live.flatMap((s) => {\n const c = conditionOf(s, \"refused\");\n return c ? [{ s, c }] : [];\n });\n const held = providerNotConnected()\n ? allRefused.filter((r) => r.c.cause === \"not_connected\")\n : [];\n const refused = allRefused.filter((r) => !held.includes(r));\n const notConnectedLine = root.querySelector<HTMLElement>(\".sent-not-connected\");\n if (notConnectedLine) {\n notConnectedLine.hidden = held.length > 0;\n }\n const heldNames = held.map(\n ({ s }, i) =>\n html`${i === 0 ? \"\" : i === held.length - 1 ? \" and \" : \", \"}<a href=\"#/sent/${s.id}\">${s.subject}</a>`,\n );\n const heldCard = held.length\n ? html`<div class=\"card stuck-card sent-held\"><div class=\"stuck-head\"><span class=\"stuck-dot\">\u26A0\uFE0F</span><div>${heldSendsHtml(html`${heldNames}`, held.length)}</div></div></div>`\n : null;\n const wedged = live.flatMap((s) => {\n const c = conditionOf(s, \"wedged\");\n return c ? [{ s, c }] : [];\n });\n setHtml(\n stuckEl,\n html`${heldCard}${refused.map(\n ({ s, c }) =>\n html`<div class=\"card stuck-card\"><div class=\"stuck-head\"><span class=\"stuck-dot\">\u26A0\uFE0F</span><div><strong><a href=\"#/sent/${s.id}\">${s.subject}</a></strong><div class=\"muted\">${c.message}</div></div></div></div>`,\n )}${wedged.map(({ s, c }) => {\n // Resolve is offered while the server would take it.\n return html`<div class=\"card stuck-card\"><div class=\"stuck-head\"><span class=\"stuck-dot\">\u26A0\uFE0F</span><div><strong><a href=\"#/sent/${s.id}\">${s.subject}</a></strong><div class=\"muted\">${c.message}</div></div></div>${can(s, \"resolve\") ? html`<button class=\"primary\" data-resolve=\"${s.id}\">Resolve\u2026</button>` : null}</div>`;\n })}`,\n );\n for (const b of $$<HTMLButtonElement>(\"[data-resolve]\", stuckEl)) {\n const w = wedged.find((x) => x.s.id === b.dataset.resolve);\n if (w) {\n b.onclick = () =>\n openResolveModal({ id: w.s.id, c_in_flight: w.c.count }, afterAct, signal);\n }\n }\n }\n // A listed send's Delivered cell follows the layer's counts, so the table moves with its\n // receipts (a late one included) without being read again.\n function patchDelivered() {\n for (const s of reported.values()) {\n const cell = $$<HTMLTableRowElement>(\"tr[data-id]\", listEl)\n .find((tr) => tr.dataset.id === s.id)\n ?.querySelector(\"td.delivered\");\n if (cell) {\n setHtml(cell, deliveredCell(s.counts));\n }\n }\n }\n function paintLive() {\n const live = sendsNow(sendingRows, reported);\n renderStuck(live);\n if (liveRead) {\n renderActive(live);\n }\n patchDelivered();\n }\n // Each report moves sends between this page's sections, whoever changed them. Any change\n // to a send in (or leaving) the queue re-reads it: a cancel, a move, a re-make, or a new\n // schedule by the other client, the fire time passing, a miss, a start. A send reaching\n // the sent side re-reads the records; one that went due to sent between two reads is one\n // change, and still lands there. Everything else is a report's own numbers.\n const inQueue = (st: SendStage | null) => st === \"scheduled\" || st === \"due\";\n const sentSide = (st: SendStage | null) => st === \"sent\" || st === \"complete\";\n function onUpdate(u: SendsUpdate) {\n for (const s of u.sends) {\n reported.set(s.id, s);\n }\n for (const id of u.removed) {\n reported.delete(id);\n }\n paintLive();\n if (u.sends.some((s) => inQueue(stageOf(s))) || u.changes.some((c) => inQueue(c.from))) {\n loadScheduled();\n }\n // A removed send was canceled with its post deleted, so it may be in either section.\n if (u.removed.length) {\n loadScheduled();\n loadList();\n } else if (u.changes.some((c) => sentSide(c.to) && !sentSide(c.from))) {\n loadList();\n }\n }\n\n // The layer follows every send from where the page's own reads stood, the earliest of\n // them, so nothing that changed after any of those reads is missed. A section whose first\n // read failed starts it from its Retry instead.\n let following = false;\n function follow(reads: (SendListResponse | null)[]) {\n const ok = reads.filter((r): r is SendListResponse => r !== null);\n if (following || !ok.length) {\n return;\n }\n following = true;\n followSends(\n {\n cursor: ok.map((r) => r.cursor).reduce(earlierCursor),\n sends: ok.flatMap((r) => r.sends),\n },\n {\n update: onUpdate,\n // The database is behind the page's reads (a reset or a restore): read it all again,\n // unless the reader has moved on.\n stale: () => {\n if (!signal.aborted) {\n mount(renderSent);\n }\n },\n },\n signal,\n );\n }\n\n // The sends in flight, for the attention and in-progress sections; from then on the\n // layer's reports keep them current.\n async function loadLive(): Promise<SendListResponse | null> {\n try {\n const res = await api<SendListResponse>(\"/api/sends?status=sending&limit=200\", { signal });\n sendingRows = res.sends;\n liveRead = true;\n paintLive();\n return res;\n } catch (e) {\n renderError(activeEl, message(e), async () => follow([await loadLive()]));\n return null;\n }\n }\n\n // The upcoming queue, soonest-first: the next send to fire (and the one you'd reach\n // for the cancel window on) sits at the top. Fetched on its own so it shows every\n // scheduled send regardless of the table's paging/filter below.\n async function loadScheduled(): Promise<SendListResponse | null> {\n const mine = ++scheduledReads;\n try {\n const res = await api<SendListResponse>(\n \"/api/sends?status=scheduled&sort=fire&dir=asc&limit=200\",\n { signal },\n );\n if (mine !== scheduledReads) {\n return res; // a later read paints\n }\n const { sends } = res;\n const schedCard = (s: SendView) =>\n html`<div class=\"card spread clickable sched-card\" data-post=\"${s.post_id}\"><div><a class=\"card-link sched-subj\" href=\"#/edit/${s.post_id}\">${s.subject}</a><div class=\"muted\">${countdownHtml(s)} \u00B7 ${fmt(s.fire_at)} \u00B7 ${s.audience.count} recipients</div></div>${windowControls(s)}</div>`;\n const [first, ...rest] = sends;\n if (!first) {\n setHtml(schedEl, html`<p class=\"muted\">Nothing scheduled.</p>`);\n } else {\n // Show only the soonest to send, so the sent records stay near the top of the\n // page; any others collapse behind a \"Show all\" toggle (usually there are none:\n // a post has at most one active send).\n setHtml(\n schedEl,\n html`${schedCard(first)}${\n rest.length\n ? html`<div id=\"schedMore\" hidden>${rest.map(schedCard)}</div><button type=\"button\" class=\"ghost sched-toggle\" id=\"schedToggle\" aria-expanded=\"false\">Show all ${sends.length} scheduled</button>`\n : null\n }`,\n );\n const toggle = schedEl.querySelector<HTMLButtonElement>(\"#schedToggle\");\n if (toggle) {\n toggle.onclick = () => {\n const more = $(\"#schedMore\", schedEl);\n const show = more.hidden;\n more.hidden = !show;\n toggle.setAttribute(\"aria-expanded\", String(show));\n toggle.textContent = show ? \"Show fewer\" : `Show all ${sends.length} scheduled`;\n };\n }\n }\n // The whole card opens the post; the subject link handles keyboard/middle-click,\n // and the schedule-management buttons (Reschedule, Cancel) opt out of navigation\n // (like the posts table's row-click guard).\n for (const card of $$<HTMLElement>(\".card.clickable\", schedEl)) {\n card.onclick = (e) => {\n const t = e.target;\n if (t instanceof Element && t.tagName !== \"A\" && !t.closest(\"button\")) {\n location.hash = `#/edit/${card.dataset.post}`;\n }\n };\n }\n for (const b of $$<HTMLButtonElement>(\"[data-reschedule]\", schedEl)) {\n const s = sends.find((x) => x.id === b.dataset.reschedule);\n if (s) {\n b.onclick = () => openRescheduleModal(s.id, s.fire_at, afterAct, signal);\n }\n }\n for (const b of $$<HTMLButtonElement>(\"[data-cancel]\", schedEl)) {\n b.onclick = () =>\n busy(b, \"Canceling\u2026\", async () => {\n try {\n await api(`/api/sends/${b.dataset.cancel}/cancel`, { method: \"POST\" });\n toast(\"Canceled\");\n // A cancel drops it from the queue, and a due send from the layer's reads.\n afterAct();\n } catch (e) {\n toast(message(e));\n }\n });\n }\n tickCountdowns();\n return res;\n } catch (e) {\n if (mine === scheduledReads) {\n renderError(schedEl, message(e), async () => follow([await loadScheduled()]));\n }\n return null;\n }\n }\n\n // The frozen Sent records: every completed post, each opening its read-only record\n // view. Sent-only, so no status column; the \"When\" is the send's completion.\n async function loadList(): Promise<SendListResponse | null> {\n const mine = ++listReads;\n try {\n const data = await api<SendListResponse>(`/api/sends?${listQuery(state)}`, { signal });\n if (mine !== listReads) {\n return data; // a later read paints\n }\n const sends = data.sends;\n if (!sends.length) {\n setHtml(\n listEl,\n html`<p class=\"muted\">${\n state.search\n ? \"No sent posts match.\"\n : state.failures\n ? \"Every sent post delivered cleanly.\"\n : \"No sent posts yet.\"\n }</p>`,\n );\n setHtml(pagerEl, html``);\n return data;\n }\n // Cells are named (subject / recipients / delivered) and the count is wrapped in\n // `.n` so the \u2264720px layout can stack a row and label its numbers from CSS alone.\n setHtml(\n listEl,\n html`<div class=\"table-wrap\"><table class=\"list-table sent-table stacks\"><colgroup><col><col class=\"c-date\"><col class=\"c-num\"><col class=\"c-delivered\"></colgroup><thead><tr>${th(\"Subject\", \"subject\", state)}${th(\"When\", \"fire\", state)}${th(\"Recipients\", \"recipients\", state, \"num\")}<th class=\"num\">Delivered</th></tr></thead><tbody>${sends.map(\n (s) =>\n html`<tr class=\"clickable\" data-id=\"${s.id}\"><td class=\"subject\"><a href=\"#/sent/${s.id}\">${s.subject}</a></td><td class=\"muted\">${fmt(s.completed_at ?? s.fire_at)}</td><td class=\"num recipients\"><span class=\"n\">${s.audience.count.toLocaleString()}</span></td><td class=\"num delivered\">${deliveredCell(s.counts)}</td></tr>`,\n )}</tbody></table></div>`,\n );\n wireSort(listEl, state, loadList);\n // The whole row opens the record; the subject link handles keyboard/middle-click.\n for (const tr of $$<HTMLTableRowElement>(\"tr[data-id]\", listEl)) {\n tr.onclick = (e) => {\n if (!(e.target instanceof Element && e.target.tagName === \"A\")) {\n location.hash = `#/sent/${tr.dataset.id}`;\n }\n };\n }\n renderPager(pagerEl, state, data.page, loadList);\n patchDelivered();\n return data;\n } catch (e) {\n if (mine === listReads) {\n renderError(listEl, message(e), async () => follow([await loadList()]));\n }\n return null;\n }\n }\n\n wireToolbar(root, state, loadList);\n // The queue, the sends in flight, and the records are this page's own reads; from them\n // the layer follows every send for the life of the mount, so a send moves through the\n // queue, in progress, and the records on its own.\n follow(await Promise.all([loadScheduled(), loadLive(), loadList()]));\n}\n", "// The sent record view and the live in-flight watch.\n\nimport type {\n DeliveryListResponse,\n DeliveryOutcomes,\n DeliveryRecord,\n DeliveryView,\n SendCounts,\n SendPhase,\n SendResponse,\n SendView,\n} from \"../../shared/sends\";\nimport { ApiError, api, apiFile, describeError } from \"../api\";\nimport { heldAdviceHtml, noEmailProvider, providerNotConnected } from \"../deployment\";\nimport { every, mount } from \"../lifecycle\";\nimport { followSend } from \"../send_state\";\nimport { $, $$ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { type ListState, renderPager, th, wireSort } from \"../ui/list_controls\";\nimport { busy, renderError, toast } from \"../ui/widgets\";\nimport { openResolveModal } from \"./dialogs\";\nimport { can, clampPct, conditionOf, fmtDuration, has } from \"./progress\";\n\nconst message = describeError;\n\n/**\n * A sent post is a frozen record (I3), not an editable object, so it opens this instead\n * of a locked editor: how the send went over the audience at fire, with a link to the\n * archived post. A send still IN FLIGHT opens the live watch (two bars, dispatch and the\n * lagging delivery, a derived phase, a counts grid, throughput, and a provider-health\n * strip), following the send through the send-state layer until dispatch completes, after\n * which the record keeps absorbing delivery receipts as they settle (SPEC \u00A76/\u00A78/\u00A712).\n */\nexport async function renderSentRecord(\n id: string,\n root: HTMLElement,\n signal: AbortSignal,\n): Promise<void> {\n // The watch re-enters here when dispatch ends, and the record when it resumes; each is\n // a fresh mount (its own root and signal), so the poll that noticed ends with its own.\n const remount = () => {\n if (!signal.aborted) {\n mount((r, s) => renderSentRecord(id, r, s)); // never over wherever the reader went since\n }\n };\n setHtml(root, html`<p class=\"muted\">Loading\u2026</p>`);\n let data: SendResponse;\n try {\n data = await api<SendResponse>(`/api/sends/${id}`, { signal });\n } catch (e) {\n renderError(root, message(e), remount);\n return;\n }\n if (signal.aborted) {\n return; // navigated away while loading: the redirect below must not hijack that\n }\n const { send } = data;\n\n // A scheduled send is still cancel-to-edit: its home is the editor, not this record. The\n // editor takes this page's history entry, so Back goes where the reader came from, never\n // to this URL that would only hand them to the editor again (DESIGN \u00A74).\n if (send.status === \"scheduled\") {\n location.replace(`#/edit/${send.post_id}`);\n return;\n }\n // A send still in flight opens the live watch, which follows it from this read.\n if (send.status === \"sending\") {\n return startWatch(data, root, signal, remount);\n }\n return renderFrozenRecord(id, data, root, signal, remount);\n}\n\n// Phase \u2192 { label, tone } for the derived-phase pill. Tones reuse the status/semantic\n// palettes: sending = blue (the in-flight treatment, DESIGN \u00A78), ok = green, warn/danger\n// as usual, scheduled = violet.\nconst PHASE_META: Record<SendPhase, { label: string; tone: string }> = {\n scheduled: { label: \"Scheduled\", tone: \"scheduled\" },\n due: { label: \"Due\", tone: \"scheduled\" },\n progressing: { label: \"Sending\", tone: \"sending\" },\n retrying: { label: \"Retrying\", tone: \"warn\" },\n \"backing-off\": { label: \"Backing off\", tone: \"warn\" },\n \"needs-attention\": { label: \"Needs attention\", tone: \"danger\" },\n settling: { label: \"Settling\", tone: \"sending\" },\n complete: { label: \"Complete\", tone: \"ok\" },\n canceled: { label: \"Canceled\", tone: \"muted\" },\n};\nconst PHASE_BLURB: Partial<Record<SendPhase, string>> = {\n progressing: \"Handing recipients to the provider.\",\n retrying: \"Some recipients hit a transient error and will be retried.\",\n \"backing-off\": \"Some recipients hit a transient error. They\u2019re retried on the next sweep.\",\n \"needs-attention\": \"Some deliveries are stuck and need a decision.\",\n settling: \"Dispatch complete, waiting on delivery receipts.\",\n};\n/** When a halted send's next retry is due, relative to now (SPEC \u00A712). */\nfunction nextRetry(retryAt: number | null): string {\n if (retryAt === null) {\n return \"it is retried on its own\";\n }\n const wait = retryAt - Date.now();\n return wait > 0 ? `next retry in ${fmtDuration(wait)}` : \"next retry due now\";\n}\n/** The phase's one-line gloss, said for the cause when the phase has more than one. */\nfunction phaseBlurb(prog: SendView): string {\n const halt = prog.provider.halt;\n if (has(prog, \"refused\") && halt?.cause === \"not_connected\") {\n return `No email provider is connected, so nothing goes out until one is; ${nextRetry(halt.retry_at)}.`;\n }\n if (has(prog, \"refused\") && halt) {\n return `The provider is refusing this account, so nothing more goes out until it\u2019s fixed; ${nextRetry(halt.retry_at)}.`;\n }\n if (prog.phase === \"backing-off\" && halt?.reason === \"unavailable\") {\n return `The provider is unavailable; ${nextRetry(halt.retry_at)}.`;\n }\n // A send too large for one tick waits for the next with nothing in flight. The server\n // reads that as progressing, since nothing failed; the line says why the bar is still.\n if (prog.phase === \"progressing\" && prog.counts.in_flight === 0 && prog.counts.pending > 0) {\n return \"Paused between ticks. It resumes on the next sweep.\";\n }\n return PHASE_BLURB[prog.phase] || \"\";\n}\nfunction phasePill(phase: SendPhase): Html {\n const m = PHASE_META[phase] ?? { label: phase, tone: \"muted\" };\n return html`<span class=\"phase-pill tone-${m.tone}\">${m.label}</span>`;\n}\n\n// A labeled progress bar: `tone` picks the fill color, `sub` is a muted sub-line.\nfunction progressBar(\n tone: string,\n label: string,\n value: number,\n total: number,\n sub: string | null,\n): Html {\n const p = total > 0 ? clampPct((100 * value) / total) : 0;\n return html`<div class=\"wbar-row\">\n <div class=\"wbar-head\"><span class=\"wbar-label\">${label}</span><span class=\"wbar-count\">${value.toLocaleString()} / ${total.toLocaleString()}</span></div>\n <div class=\"wbar\" role=\"progressbar\" aria-valuenow=\"${p}\" aria-valuemin=\"0\" aria-valuemax=\"100\"><div class=\"wbar-fill tone-${tone}\" style=\"width:${p}%\"></div></div>\n ${sub ? html`<div class=\"wbar-sub muted\">${sub}</div>` : null}\n </div>`;\n}\n// The delivery bar shares the dispatch bar's scale (denominator = the whole audience), so\n// the two pair up: a grey `accepted` segment tracks the exact frontier the dispatch bar\n// reaches, and green fills in behind it as receipts confirm. Green can never pass grey, so\n// delivery visibly LAGS dispatch instead of racing ahead of it.\nfunction deliveryBar(\n label: string,\n confirmed: number,\n accepted: number,\n total: number,\n sub: string,\n): Html {\n const acceptedPct = total > 0 ? clampPct((100 * accepted) / total) : 0;\n const confirmedPct = total > 0 ? clampPct((100 * confirmed) / total) : 0;\n return html`<div class=\"wbar-row\">\n <div class=\"wbar-head\"><span class=\"wbar-label\">${label}</span><span class=\"wbar-count\">${confirmed.toLocaleString()} of ${accepted.toLocaleString()} accepted</span></div>\n <div class=\"wbar dual\" role=\"progressbar\" aria-valuenow=\"${confirmedPct}\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n <div class=\"wbar-fill tone-accepted\" style=\"width:${acceptedPct}%\"></div>\n <div class=\"wbar-fill tone-ok\" style=\"width:${confirmedPct}%\"></div>\n </div>\n <div class=\"wbar-sub muted\">${sub}</div>\n </div>`;\n}\n\n// The counts grid mirrors the eight denormalized buckets; each swatch reuses the record\n// tile palette so a bucket reads the same here and on the frozen record.\nconst WATCH_COUNTS: { key: keyof SendCounts; label: string; sw: string }[] = [\n { key: \"pending\", label: \"Pending\", sw: \"neutral\" },\n { key: \"in_flight\", label: \"In flight\", sw: \"sending\" },\n { key: \"accepted\", label: \"Accepted\", sw: \"sending\" },\n { key: \"delivered\", label: \"Delivered\", sw: \"ok\" },\n { key: \"bounced\", label: \"Bounced\", sw: \"warn\" },\n { key: \"complained\", label: \"Complained\", sw: \"danger\" },\n { key: \"unsent\", label: \"Unsent\", sw: \"neutral\" },\n { key: \"skipped\", label: \"Skipped\", sw: \"neutral\" },\n];\nfunction watchCountsHtml(counts: SendCounts): Html {\n return html`<div class=\"wcounts\">${WATCH_COUNTS.map(\n (c) =>\n html`<div class=\"wcount\"><div class=\"wcount-n\">${(counts[c.key] || 0).toLocaleString()}</div><div class=\"wcount-l\"><span class=\"rec-sw sw-${c.sw}\"></span>${c.label}</div></div>`,\n )}</div>`;\n}\n\n// The dynamic half of the watch (bars + counts + health strip), repainted on each poll.\nfunction watchBodyHtml(prog: SendView): Html {\n const c = prog.counts;\n const acceptedTotal = c.accepted + c.delivered + c.bounced + c.complained;\n const rate = prog.dispatch.rate_per_min;\n // The server gives a time to finish only while the send is handing off, never while it\n // is paused (backing off, refused, or wedged). The rate goes with it: it is what a tick\n // has carried so far, so beside a pause it would promise progress nothing is making.\n const eta = prog.dispatch.eta_ms;\n const dispatchSub =\n prog.status === \"sending\"\n ? `${phaseBlurb(prog)}${\n eta ? `${rate ? ` \u00B7 ~${rate.toLocaleString()}/min` : \"\"} \u00B7 ETA ${fmtDuration(eta)}` : \"\"\n }`\n : \"Dispatch complete.\";\n const failureCount = (c.unsent || 0) + (c.bounced || 0) + (c.complained || 0);\n const providerText = noEmailProvider()\n ? \"Local stand-in: nothing is delivered\"\n : providerNotConnected()\n ? \"No email provider connected\"\n : `Provider: ${prog.provider?.name || \"unknown\"}`;\n // The provider refusing the account is the one loud condition here without a control:\n // the fix is outside the app, and the send resumes on its own once it lands (SPEC \u00A712).\n const refused = conditionOf(prog, \"refused\");\n // No provider connected is held the same way but is no provider's refusal (SPEC \u00A79): its\n // own headline, and the fix said once, with no provider's words to quote: the step as the\n // link every not-connected line carries (or, in the local preview, the restart).\n const unconnected = refused?.cause === \"not_connected\";\n // In flight too long (SPEC \u00A712): the amber the dashboard's health line and the Sent\n // page's card give the same condition, in the server's words.\n const stuck = conditionOf(prog, \"stuck\");\n return html`\n ${stuck ? html`<div class=\"health amber\" role=\"status\"><span class=\"health-dot\">\u26A0\uFE0F</span><div>${stuck.message}</div></div>` : null}\n ${\n refused\n ? html`<div class=\"health red\" role=\"alert\"><span class=\"health-dot\">\u26A0\uFE0F</span><div><strong>${unconnected ? \"No email provider is connected\" : \"The provider is refusing this account\"}</strong>${unconnected ? null : html`<div>${refused.error}</div>`}<div class=\"refused-advice\">${unconnected ? heldAdviceHtml() : refused.advice}</div><div>${refused.since === null ? null : `Since ${fmt(refused.since)}. `}No one has been marked unsent, and once ${unconnected ? \"one is connected\" : \"this is fixed\"} the send resumes at its next retry${refused.retry_at === null ? \"\" : `, ${fmt(refused.retry_at)}`}.</div></div></div>`\n : null\n }\n <div class=\"wbars\">\n ${progressBar(\"sending\", \"Dispatch: provider-accepted\", acceptedTotal, prog.audience.count, dispatchSub)}\n ${deliveryBar(\n \"Delivery: webhook-confirmed\",\n prog.delivery.confirmed,\n acceptedTotal,\n prog.audience.count,\n \"Delivery lags acceptance: grey is accepted but unconfirmed, and green fills in as receipts arrive.\",\n )}\n </div>\n ${watchCountsHtml(c)}\n <div class=\"whealth${failureCount ? \" has-failures\" : \"\"}\"><span class=\"whealth-dot\"></span><span>${providerText} \u00B7 ${\n failureCount\n ? `${failureCount.toLocaleString()} bounced / unsent / complained`\n : \"no delivery failures\"\n }</span></div>`;\n}\nfunction watchMetaHtml(send: SendView): Html {\n const started = send.started_at ? `Started ${fmt(send.started_at)}` : \"Sending now\";\n return html`${started} \u00B7 ${send.audience.count.toLocaleString()} recipients`;\n}\nfunction watchHtml(send: SendView): Html {\n const prog = send;\n return html`\n <div class=\"editor-head\">\n <a href=\"#/sent\" class=\"back\">\u2190 Sent</a>\n ${can(prog, \"resolve\") ? html`<button type=\"button\" class=\"primary\" id=\"resolveBtn\">Resolve\u2026</button>` : null}\n </div>\n <div class=\"card rec-card watch-card\">\n <div class=\"rec-head\">\n <div class=\"watch-title\"><h1>${send.subject || html`<em>untitled</em>`}</h1><span id=\"watchPill\">${phasePill(prog.phase)}</span></div>\n <div class=\"rec-meta\" id=\"watchMeta\">${watchMetaHtml(send)}</div>\n </div>\n <div id=\"watchBody\">${watchBodyHtml(prog)}</div>\n <p class=\"rec-note muted\">This view updates live while the send is in flight. Delivery receipts keep arriving after dispatch finishes, and the record stays accurate as they settle.</p>\n </div>`;\n}\n\n// Wire the header's Resolve control (present only while the server offers it, \u00A712). It\n// reuses the same modal the Sent page uses, with the wedged count the send reports.\nfunction wireWatchHeader(\n root: HTMLElement,\n send: SendView,\n remount: () => void,\n signal: AbortSignal,\n): void {\n const rb = root.querySelector<HTMLButtonElement>(\"#resolveBtn\");\n if (rb) {\n const count = conditionOf(send, \"wedged\")?.count ?? send.counts.in_flight;\n rb.onclick = () => openResolveModal({ id: send.id, c_in_flight: count }, remount, signal);\n }\n}\nfunction paintWatch(root: HTMLElement, prog: SendView): void {\n const pill = root.querySelector(\"#watchPill\");\n if (pill) {\n setHtml(pill, phasePill(prog.phase));\n }\n const meta = root.querySelector(\"#watchMeta\");\n if (meta) {\n setHtml(meta, watchMetaHtml(prog));\n }\n const body = root.querySelector(\"#watchBody\");\n if (body) {\n setHtml(body, watchBodyHtml(prog));\n }\n}\n\nfunction startWatch(\n data: SendResponse,\n root: HTMLElement,\n signal: AbortSignal,\n remount: () => void,\n): void {\n setHtml(root, watchHtml(data.send));\n wireWatchHeader(root, data.send, remount, signal);\n // The latest report, repainted on the clock between reports: a halt's \"next retry in \u2026\"\n // counts down while nothing about the send is written.\n let latest = data.send;\n every(1000, () => paintWatch(root, latest), signal);\n // From the page's own read, the send-state layer reports each change to the send, at the\n // server's pace; the watch keeps no poll of its own. Once dispatch ends it re-enters as the\n // frozen record (which settles).\n followSend(\n data,\n {\n update: (fresh) => {\n if (fresh.status !== \"sending\") {\n remount();\n return;\n }\n latest = fresh;\n // If Resolve just became possible, or stopped being, the header changes: re-render.\n if (can(fresh, \"resolve\") !== Boolean(root.querySelector(\"#resolveBtn\"))) {\n setHtml(root, watchHtml(fresh));\n wireWatchHeader(root, fresh, remount, signal);\n } else {\n paintWatch(root, fresh);\n }\n },\n removed: remount,\n stale: remount,\n },\n signal,\n );\n}\n\n// The delivery-outcome tiles + reconciliation line: the frozen record's body, factored\n// so the settling poll can repaint them in place as late receipts arrive.\nfunction outcomeTilesHtml(outcomes: DeliveryOutcomes): Html {\n const tiles = [\n { n: outcomes.recipients, label: \"Recipients\", cls: \"\", sw: \"neutral\" },\n { n: outcomes.delivered, label: \"Delivered\", cls: \"ok\", sw: \"ok\" },\n { n: outcomes.bounced, label: \"Bounced\", cls: \"warn\", sw: \"warn\" },\n { n: outcomes.complained, label: \"Complained\", cls: \"danger\", sw: \"danger\" },\n { n: outcomes.unsent, label: \"Unsent\", cls: \"\", sw: \"neutral\" },\n ];\n return html`${tiles.map(\n (t) =>\n html`<div class=\"rec-tile\"><div class=\"rec-n ${t.cls}\">${t.n.toLocaleString()}</div><div class=\"rec-l\"><span class=\"rec-sw sw-${t.sw}\"></span>${t.label}</div></div>`,\n )}`;\n}\nfunction outcomeReconHtml(outcomes: DeliveryOutcomes): string {\n const total = outcomes.recipients;\n const parts = [`${outcomes.delivered.toLocaleString()} delivered`];\n if (outcomes.bounced) {\n parts.push(`${outcomes.bounced.toLocaleString()} bounced`);\n }\n if (outcomes.complained) {\n parts.push(`${outcomes.complained.toLocaleString()} complained`);\n }\n if (outcomes.unsent) {\n parts.push(`${outcomes.unsent.toLocaleString()} unsent`);\n }\n // Each leftover recipient is named for what it is: only an accepted one can still get a\n // receipt. A skipped one was left out at hand-off (unsubscribed or suppressed by then)\n // and never mailed, so it is final (SPEC \u00A76, \u00A78), and one in flight is still being handed.\n if (outcomes.accepted) {\n parts.push(`${outcomes.accepted.toLocaleString()} accepted, awaiting a delivery receipt`);\n }\n if (outcomes.skipped) {\n parts.push(\n `${outcomes.skipped.toLocaleString()} skipped (unsubscribed or suppressed before hand-off, never mailed)`,\n );\n }\n if (outcomes.in_flight) {\n parts.push(`${outcomes.in_flight.toLocaleString()} in flight`);\n }\n const reconciled = `All ${total.toLocaleString()} accounted for: ${parts.join(\", \")}.`;\n // The suppression note is scoped to what actually suppresses: hard bounces and complaints,\n // never soft bounces (counted, left on the list, SPEC \u00A78). The aggregate counters don't\n // split soft from hard, so gate on any bounce or complaint and state the rule rather than\n // claim a count; a clean send says nothing about suppression at all.\n const suppression =\n outcomes.bounced || outcomes.complained\n ? \" Hard bounces and spam complaints are suppressed automatically, so those addresses won\u2019t be mailed again.\"\n : \"\";\n return `${reconciled}${suppression}`;\n}\n// Whether two readings of the record agree on every bucket, so a settling tick that\n// brought nothing new leaves the page as it is.\nfunction sameOutcomes(a: DeliveryOutcomes, b: DeliveryOutcomes): boolean {\n return (Object.keys(a) as (keyof DeliveryOutcomes)[]).every((k) => a[k] === b[k]);\n}\n\ninterface Outcome {\n label: string;\n sw: string;\n hint?: string;\n}\n\n// The per-recipient record: the outcome tiles summarize, this shows the actual rows. A\n// row's OUTCOME is derived from the same two facts the tiles bucket (the webhook `event`\n// winning over the send-loop `status`), so a row reads the same bucket (and reuses the\n// same swatch palette) as its tile. A bounce splits soft vs hard on `bounce_kind`, the\n// provider's permanent/transient signal frozen onto the row when the event landed (SPEC\n// \u00A78), a fact of THIS send, not a read of the current suppression list. Null = unknown.\nfunction deliveryOutcome(r: DeliveryRecord): Outcome {\n if (r.event === \"delivered\") {\n return { label: \"Delivered\", sw: \"ok\" };\n }\n if (r.event === \"bounced\") {\n if (r.bounce_kind === \"hard\") {\n return { label: \"Hard bounce\", sw: \"warn\", hint: \"permanent, so the address is suppressed\" };\n }\n if (r.bounce_kind === \"soft\") {\n return { label: \"Soft bounce\", sw: \"warn\", hint: \"transient, so counted but not suppressed\" };\n }\n return { label: \"Bounce\", sw: \"warn\" };\n }\n if (r.event === \"complained\") {\n return { label: \"Complained\", sw: \"danger\", hint: \"suppressed\" };\n }\n switch (r.status) {\n case \"unsent\":\n return { label: \"Unsent\", sw: \"neutral\" };\n case \"skipped\":\n return { label: \"Skipped\", sw: \"neutral\" };\n case \"accepted\":\n return { label: \"Accepted\", sw: \"sending\", hint: \"awaiting a delivery receipt\" };\n default:\n return { label: \"In flight\", sw: \"sending\" };\n }\n}\n\n// The three view tabs the record opens on, failures first (the rows that went wrong).\nconst DELIVERY_TABS: { v: DeliveryView; label: string }[] = [\n { v: \"failures\", label: \"Failures\" },\n { v: \"delivered\", label: \"Delivered\" },\n { v: \"all\", label: \"All\" },\n];\n\n/** The per-recipient list's state: the list controls' plus which bucket it shows. */\ninterface DeliveryListState extends ListState {\n view: DeliveryView;\n}\n\n// A positive/neutral empty state per view: an empty \"failures\" list is good news, not a gap.\nfunction deliveryEmpty(dstate: DeliveryListState): string {\n if (dstate.search) {\n return \"No recipients match that address.\";\n }\n if (dstate.view === \"failures\") {\n return \"No delivery failures. Every recipient was accepted or delivered.\";\n }\n if (dstate.view === \"delivered\") {\n return \"No delivery receipts confirmed yet.\";\n }\n return \"No recipients on this send.\";\n}\n\nfunction recordDeliveryQuery(d: DeliveryListState): string {\n const p = new URLSearchParams();\n p.set(\"view\", d.view);\n const term = (d.search || \"\").trim();\n if (term) {\n p.set(\"search\", term);\n }\n if (d.sort) {\n p.set(\"sort\", d.sort);\n p.set(\"dir\", d.dir ?? \"asc\");\n }\n p.set(\"limit\", String(d.limit));\n p.set(\"offset\", String(d.offset));\n return p.toString();\n}\n\nfunction deliveryRowsHtml(rows: DeliveryRecord[], dstate: DeliveryListState): Html {\n const body = rows.map((r) => {\n const o = deliveryOutcome(r);\n // The label names the bucket; its consequence (suppressed or not, awaiting a receipt)\n // is taught once in prose by the reconciliation line above and kept on the row only as\n // a hover tooltip, so a row stays a clean one-liner instead of restating the taxonomy\n // on every line.\n const outcome = o.hint\n ? html`<span class=\"rec-out\" title=\"${o.hint}\"><span class=\"rec-sw sw-${o.sw}\"></span>${o.label}</span>`\n : html`<span class=\"rec-out\"><span class=\"rec-sw sw-${o.sw}\"></span>${o.label}</span>`;\n const detail = r.error || r.event_detail || \"\";\n const when = r.event_at ? fmt(r.event_at) : \"\";\n return html`<tr><td class=\"rec-email\">${r.email}</td><td>${outcome}</td><td class=\"muted\">${detail || \"\u2014\"}</td><td class=\"muted\">${when || \"\u2014\"}</td></tr>`;\n });\n return html`<div class=\"table-wrap\"><table class=\"list-table rec-people-table\"><colgroup><col><col class=\"c-out\"><col><col class=\"c-date\"></colgroup><thead><tr>${th(\n \"Recipient\",\n \"email\",\n dstate,\n )}<th class=\"c-out\">Outcome</th><th>Detail</th><th class=\"c-date\">When</th></tr></thead><tbody>${body}</tbody></table></div>`;\n}\n\nfunction renderFrozenRecord(\n id: string,\n data: SendResponse,\n root: HTMLElement,\n signal: AbortSignal,\n remount: () => void,\n): void {\n const { send, outcomes } = data;\n if (send.status === \"canceled\") {\n renderCanceled(send, root);\n return;\n }\n // The published post, which the send's view links once it has started.\n const archive = send.links.archive;\n const total = outcomes.recipients;\n const sentAt = send.completed_at ?? send.fire_at;\n\n setHtml(\n root,\n html`\n <div class=\"editor-head\">\n <a href=\"#/sent\" class=\"back\">\u2190 Sent</a>\n ${archive ? html`<button type=\"button\" class=\"primary\" id=\"viewPublished\">View published post \u2197</button>` : null}\n </div>\n <div class=\"card rec-card\">\n <div class=\"rec-head\">\n <div class=\"watch-title\"><h1>${send.subject || html`<em>untitled</em>`}</h1><span id=\"recPill\">${phasePill(send.phase)}</span></div>\n <div class=\"rec-meta\">Sent ${fmt(sentAt)} \u00B7 ${total.toLocaleString()} recipients</div>\n </div>\n <p class=\"rec-tiles-cap muted\">Delivery outcomes keep updating as receipts arrive; the audience at fire and the published post are fixed.</p>\n <div class=\"rec-tiles\">${outcomeTilesHtml(outcomes)}</div>\n <p class=\"rec-recon muted\">${outcomeReconHtml(outcomes)}</p>\n <div class=\"rec-actions\">\n <button type=\"button\" class=\"ghost\" id=\"csvBtn\">Export recipients (CSV)</button>\n </div>\n <div class=\"rec-people\">\n <div class=\"rec-people-head\">\n <h2 class=\"rec-people-title\">Recipients</h2>\n <div class=\"rec-views\" role=\"group\" aria-label=\"Which recipients to show\">\n ${DELIVERY_TABS.map(\n (t, i) =>\n html`<button type=\"button\" class=\"rec-view-btn\" data-view=\"${t.v}\" aria-pressed=\"${i === 0 ? \"true\" : \"false\"}\">${t.label}</button>`,\n )}\n </div>\n </div>\n <input class=\"rec-people-search\" type=\"search\" placeholder=\"Search email\u2026\" aria-label=\"Search recipients by email\" autocomplete=\"off\">\n <div id=\"recRows\" class=\"muted\">Loading\u2026</div>\n <div id=\"recPager\"></div>\n </div>\n <p class=\"rec-note muted\">This is the record of what went out: the published post is the exact frozen copy readers received, and nothing here is editable.</p>\n </div>`,\n );\n\n const viewBtn = document.getElementById(\"viewPublished\");\n if (viewBtn && archive) {\n viewBtn.onclick = () => window.open(archive, \"_blank\", \"noopener\");\n }\n\n // The per-recipient record, its own paged/filtered state. Default view is \"failures\" so\n // the rows that went wrong lead; default sort mirrors the CSV (email asc). It reloads on\n // interaction (a view/search/sort/page change, or re-clicking the view), and on each\n // settling refresh that moved the tiles, so the list follows the counts above it. The\n // two are separate reads, so between refreshes they can differ by what one tick brought.\n const dstate: DeliveryListState = {\n view: \"failures\",\n search: \"\",\n sort: \"email\",\n dir: \"asc\",\n offset: 0,\n limit: 50,\n };\n const rowsEl = $(\"#recRows\", root);\n const recPagerEl = $(\"#recPager\", root);\n // Loads can overlap (a refresh's reload racing the reader's click), so only the latest\n // paints. A load the reader started that a refresh then overtook still owes them an\n // answer, so the refresh's failure is theirs to see.\n let latest = 0;\n let readerWaiting = false;\n const load = async (background: boolean): Promise<void> => {\n const mine = ++latest;\n if (!background) {\n readerWaiting = true;\n }\n try {\n const d = await api<DeliveryListResponse>(\n `/api/sends/${id}/deliveries?${recordDeliveryQuery(dstate)}`,\n { signal },\n );\n if (mine !== latest) {\n return;\n }\n // The reader's page, past the end now that the total shrank: the last page instead.\n const last =\n d.page.total > 0 ? Math.floor((d.page.total - 1) / d.page.limit) * d.page.limit : 0;\n if (!d.deliveries.length && dstate.offset > last) {\n dstate.offset = last;\n return load(background);\n }\n readerWaiting = false;\n keepFocus(() => {\n if (!d.deliveries.length) {\n setHtml(rowsEl, html`<p class=\"rec-people-empty muted\">${deliveryEmpty(dstate)}</p>`);\n setHtml(recPagerEl, html``);\n return;\n }\n setHtml(rowsEl, deliveryRowsHtml(d.deliveries, dstate));\n wireSort(rowsEl, dstate, loadDeliveries);\n renderPager(recPagerEl, dstate, d.page, loadDeliveries);\n });\n } catch (e) {\n // A refresh that fails leaves the rows the reader is reading; the next refresh that\n // moves the counts tries again. Only a load the reader is waiting on says so.\n if (mine === latest && (!background || readerWaiting)) {\n readerWaiting = false;\n renderError(rowsEl, message(e), loadDeliveries);\n }\n }\n };\n const loadDeliveries = () => load(false);\n // A repaint replaces the table and the pager, so a keyboard reader on a sort header or a\n // pager button would lose their place: put focus back on the same control, if it's there.\n const keepFocus = (paint: () => void) => {\n const had = document.activeElement;\n const within = had instanceof HTMLElement && (rowsEl.contains(had) || recPagerEl.contains(had));\n const sortKey = within ? had.dataset.sort : undefined;\n const pagerCls = within\n ? [\"pager-prev\", \"pager-next\"].find((c) => had.classList.contains(c))\n : undefined;\n paint();\n const again = sortKey\n ? rowsEl.querySelector<HTMLButtonElement>(`.th-sort[data-sort=\"${sortKey}\"]`)\n : pagerCls\n ? recPagerEl.querySelector<HTMLButtonElement>(`.${pagerCls}`)\n : null;\n if (again && !again.disabled) {\n again.focus();\n }\n };\n const viewButtons = $$<HTMLButtonElement>(\".rec-view-btn\", root);\n for (const b of viewButtons) {\n b.onclick = () => {\n const v = b.dataset.view;\n if (v === \"failures\" || v === \"delivered\" || v === \"all\") {\n dstate.view = v;\n }\n dstate.offset = 0;\n for (const x of viewButtons) {\n x.setAttribute(\"aria-pressed\", String(x === b));\n }\n loadDeliveries();\n };\n }\n const recSearch = root.querySelector<HTMLInputElement>(\".rec-people-search\");\n if (recSearch) {\n let t: number | undefined;\n recSearch.oninput = () => {\n clearTimeout(t);\n t = setTimeout(() => {\n dstate.search = recSearch.value;\n dstate.offset = 0;\n loadDeliveries();\n }, 250);\n };\n }\n loadDeliveries();\n\n const csvBtn = $<HTMLButtonElement>(\"#csvBtn\");\n csvBtn.onclick = () =>\n busy(csvBtn, \"Exporting\u2026\", async () => {\n try {\n const csv = await apiFile(send.links.deliveries_csv);\n const url = URL.createObjectURL(new Blob([csv.text], { type: \"text/csv\" }));\n const a = document.createElement(\"a\");\n a.href = url;\n // The server's name for it (the archive slug), so the file is named the same\n // however it's fetched.\n a.download = csv.filename ?? \"deliveries.csv\";\n a.click();\n setTimeout(() => URL.revokeObjectURL(url), 10000);\n } catch (e) {\n // The API names only what it didn't find (\"send\"), which says nothing on its own.\n toast(\n e instanceof ApiError && e.status === 404\n ? \"This send no longer exists, so there are no deliveries to export.\"\n : message(e),\n );\n }\n });\n\n // The record keeps absorbing delivery receipts after dispatch (\u00A76), and follows them\n // through the send-state layer at the server's pace (DESIGN \u00A74): closely while they\n // arrive, easing off as they slow, then about once a minute, for as long as the page is\n // open, since a complaint can land long after the last delivery. Each report carries the\n // send's counters, which give every tile, so a read that brings nothing costs nothing\n // here. One that moved a count repaints the tiles and reloads the list on the reader's\n // page, in their view, search, and sort; the pill turns from Settling to Complete in place.\n let shown = outcomes;\n followSend(\n data,\n {\n update(fresh) {\n if (fresh.status === \"sending\") {\n remount(); // resumed (a Resolve that put recipients back, say) \u2192 back to the watch\n return;\n }\n setHtml($(\"#recPill\", root), phasePill(fresh.phase));\n const now = outcomesOf(fresh);\n if (sameOutcomes(shown, now)) {\n return;\n }\n shown = now;\n setHtml($(\".rec-tiles\", root), outcomeTilesHtml(now));\n $(\".rec-recon\", root).textContent = outcomeReconHtml(now);\n void load(true);\n },\n removed: remount,\n stale: remount,\n },\n signal,\n );\n}\n\n/** A sent send's outcomes from its counters: the same buckets as the record's own read,\n * one per recipient, over the audience fixed at fire. As there, a recipient not yet handed\n * off (pending) counts as in flight. */\nfunction outcomesOf(send: SendView): DeliveryOutcomes {\n const c = send.counts;\n return {\n recipients: send.audience.count,\n delivered: c.delivered,\n bounced: c.bounced,\n complained: c.complained,\n unsent: c.unsent,\n skipped: c.skipped,\n accepted: c.accepted,\n in_flight: c.pending + c.in_flight,\n };\n}\n\n/**\n * A canceled send's page: it never fired, so there is no audience, outcome, or published\n * post to show, only that it was canceled, when, and when it would have sent.\n */\nfunction renderCanceled(send: SendView, root: HTMLElement): void {\n setHtml(\n root,\n html`\n <div class=\"editor-head\">\n <a href=\"#/sent\" class=\"back\">\u2190 Sent</a>\n </div>\n <div class=\"card rec-card\">\n <div class=\"rec-head\">\n <div class=\"watch-title\"><h1>${send.subject || html`<em>untitled</em>`}</h1><span id=\"recPill\">${phasePill(send.phase)}</span></div>\n <div class=\"rec-meta\">Canceled ${fmt(send.completed_at)} \u00B7 was scheduled for ${fmt(send.fire_at)}</div>\n </div>\n <p class=\"rec-note muted\">This send was canceled before it fired, so no one was mailed.</p>\n </div>`,\n );\n}\n", "// The shared unsaved-changes bar.\n\nimport { html, setHtml } from \"./html\";\nimport { busy } from \"./widgets\";\n\n/** A control the page always carries (public/dashboard/index.html); missing means the wrong page. */\nfunction control<T extends HTMLElement>(id: string): T {\n const el = document.getElementById(id);\n if (!el) {\n throw new Error(`the admin page has no #${id}`);\n }\n return el as T;\n}\n\nexport interface SavebarOptions {\n /** Runs inside busy() on the shared Save button; call setDirty(false) on success or showError() on a rejection. */\n onSave: () => unknown;\n onDiscard: () => void;\n saveLabel?: string;\n discardLabel?: string;\n}\n\n/** What a mounted page drives the bar with; inert once another page has attached. */\nexport interface SavebarHandle {\n setDirty(dirty: boolean): void;\n showError(text: string): void;\n}\n\n// One full-width banner fixed to the bottom of the viewport (markup in index.html),\n// shared by every surface with an explicit save + a revertible baseline: Settings\n// and the Template page. A page attaches once on mount, then drives it \u2014 setDirty()\n// slides it up while there are unsaved changes, showError() keeps it up after a\n// rejected save and says why (right beside Save). The bar detaches when the mount's\n// signal aborts, so a page owns it exactly as long as it is mounted. The post editor\n// keeps its own autosave + conflict model (see renderEditor) and deliberately does not\n// use this \u2014 a \"Save / Discard against a baseline\" bar doesn't fit continuous autosave.\nexport const savebar = (() => {\n const el = control<HTMLElement>(\"savebar\");\n const msgEl = el.querySelector(\".savebar-msg\");\n const saveBtn = control<HTMLButtonElement>(\"savebarSave\");\n const discardBtn = control<HTMLButtonElement>(\"savebarDiscard\");\n // Bumped on every attach/detach so a stale async handler (a save that resolves\n // after the user navigated away) can never drive a bar a new page now owns.\n let token = 0;\n\n const setMsg = (text: string) => {\n if (msgEl) {\n setHtml(msgEl, html`<span class=\"savebar-dot\"></span><span>${text}</span>`);\n }\n };\n const reveal = () => {\n el.hidden = false;\n // Arm the slide-up: un-hide, then force a reflow so the off-screen base transform\n // is the established start state before .show flips it (a hidden \u2192 .show toggle in\n // one tick wouldn't transition). A synchronous reflow works even when the tab is\n // backgrounded and requestAnimationFrame is paused.\n void el.offsetHeight;\n el.classList.add(\"show\");\n };\n\n function detach(): void {\n token++;\n el.classList.remove(\"show\", \"is-error\");\n el.hidden = true;\n saveBtn.onclick = null;\n discardBtn.onclick = null;\n // Drop the bottom padding the page reserved for the (fixed) bar.\n document.body.classList.remove(\"has-savebar\");\n }\n\n /** Attach the bar to the mounting page for the life of its signal; returns the handle the page drives. */\n function attach(\n { onSave, onDiscard, saveLabel = \"Save changes\", discardLabel = \"Discard\" }: SavebarOptions,\n signal: AbortSignal,\n ): SavebarHandle {\n if (signal.aborted) {\n return { setDirty() {}, showError() {} }; // the page is already gone: nothing to drive\n }\n const mine = ++token;\n signal.addEventListener(\n \"abort\",\n () => {\n if (token === mine) {\n detach();\n }\n },\n { once: true },\n );\n saveBtn.textContent = saveLabel;\n discardBtn.textContent = discardLabel;\n el.classList.remove(\"show\", \"is-error\");\n setMsg(\"You have unsaved changes.\");\n el.hidden = true; // revealed on the first setDirty(true) / showError()\n // Reserve bottom room on the content for the whole time this page is mounted, so\n // the last section never slides under the fixed bar (no shift as it toggles).\n document.body.classList.add(\"has-savebar\");\n saveBtn.onclick = () => busy(saveBtn, \"Saving\u2026\", () => Promise.resolve(onSave()));\n discardBtn.onclick = () => onDiscard();\n const alive = () => token === mine;\n return {\n setDirty(dirty) {\n if (!alive()) {\n return;\n }\n el.classList.remove(\"is-error\");\n if (dirty) {\n setMsg(\"You have unsaved changes.\");\n reveal();\n } else {\n el.classList.remove(\"show\");\n }\n },\n showError(text) {\n if (!alive()) {\n return;\n }\n el.classList.add(\"is-error\");\n setMsg(text);\n reveal();\n },\n };\n }\n\n return { attach };\n})();\n", "// What the Settings page's sections share: the page's live state against its saved\n// baseline, the hooks a section answers so one save bar can commit them all, and the head\n// every section wears.\n\nimport type { ConfirmationEmailCopy, SettingsResponse, SettingsView } from \"../../shared/settings\";\nimport { describeError } from \"../api\";\nimport { type Html, html } from \"../ui/html\";\nimport { type IconName, icon } from \"../ui/icons\";\n\n/** The persisted fields the save bar tracks against their saved baseline. */\nexport interface SettingsBaseline {\n name: string;\n tagline: string;\n address: string;\n recipients: string[];\n confirmation: ConfirmationEmailCopy;\n /** Where notifications about sends go (SPEC \u00A78); \"\" for none. */\n notifyTo: string;\n}\n\n/** The page's live, in-memory state: the baseline fields plus the logo, which saves on\n * its own the moment it changes. */\nexport interface SettingsFormState extends SettingsBaseline {\n logoUrl: string;\n}\n\n/** What a section is handed: the page's read, its live state, and the way back to the\n * page when that state changes. */\nexport interface SettingsPage {\n data: SettingsResponse;\n state: SettingsFormState;\n /** The state as last saved: what Discard returns to, and what a test is sent from. */\n saved(): SettingsBaseline;\n /** The From display name as the email resolves it, \"\" for a bare address. What a reader\n * sees falls back to this, so it never shows a name the publisher didn't choose. */\n fromDisplay: string;\n /** The same with a visible placeholder, for what only the publisher sees. */\n fromName: string;\n /** The state changed: every section repaints what it derives from it, and the save\n * bar rises or falls. */\n changed(): void;\n /** A save landed outside the save bar (the logo's): the page adopts the saved settings. */\n applySaved(settings: SettingsView): void;\n}\n\n/** What a section answers once mounted. `repaint` redraws what it derives from the state\n * (a preview, a snippet) and runs on every change; `reset` puts its fields back to the\n * state after a save adopts the server's result or a discard restores the baseline. */\nexport interface SectionHandle {\n repaint?(): void;\n reset?(): void;\n}\n\nexport const message = describeError;\n\n// A bare attribute is markup, not text: spelled once as markup so it can be interpolated.\nexport const HIDDEN = html` hidden`;\n\n/** A section's intent chip: editable, read-only, copy-out, or a preview. */\nexport function chip(kind: IconName, label: string): Html {\n return html`<span class=\"set-chip ${kind}\">${icon(kind)}${label}</span>`;\n}\n\n/** A section's head: its title, its chips, and the rule that runs to the edge. */\nexport function secHead(title: string, chipHtml: Html, extra: Html | null = null): Html {\n return html`<div class=\"set-sec-head\"><h2 class=\"set-sec-title\">${title}</h2>${chipHtml}${extra}<span class=\"set-rule\"></span></div>`;\n}\n\n/** The letter an inbox row shows for a sender with no avatar. */\nexport function monogram(name: string, fallback: string): string {\n return (String(name || fallback).trim()[0] || \"K\").toUpperCase();\n}\n", "// Settings' Confirmation email section (SPEC \u00A77): the double opt-in email, preview-first\n// with a Preview | Edit toggle. Words only: Kestrel owns the layout and inserts the confirm\n// link, so the fields can never break double opt-in. The rendered card is byte-honest to\n// the transactional layout in src/emails/system.ts, in both themes (the card's dark palette\n// lives in styles.css, tracking the dashboard theme).\n\nimport { $ } from \"../ui/dom\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { chip, monogram, type SectionHandle, type SettingsPage, secHead } from \"./page\";\n\nexport function confirmationSection(page: SettingsPage): Html {\n const { state, fromName } = page;\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Confirmation email\", chip(\"editable\", \"Editable\"))}\n <p class=\"set-lede\">The email that asks a new subscriber to confirm. Edit the wording as you see fit. Kestrel adds the confirmation link.</p>\n <div class=\"set-preview\">\n <div class=\"set-preview-bar set-ce-bar\">\n <div class=\"set-ce-modetog\" role=\"group\" aria-label=\"Confirmation email view\">\n <button type=\"button\" class=\"set-ce-modebtn\" id=\"ceTabPreview\" aria-pressed=\"true\">${icon(\"preview\")}Preview</button>\n <button type=\"button\" class=\"set-ce-modebtn\" id=\"ceTabEdit\" aria-pressed=\"false\">${icon(\"editable\")}Edit</button>\n </div>\n </div>\n <div id=\"cePreviewBody\">\n <div class=\"set-inbox\">\n <div class=\"set-inbox-avatar\" id=\"cePvAvatar\">${monogram(state.name, fromName)}</div>\n <div class=\"set-inbox-body\">\n <div class=\"set-inbox-top\"><span class=\"set-inbox-from\" id=\"cePvFrom\">${state.name || fromName}</span><span class=\"set-inbox-time\">now</span></div>\n <div class=\"set-inbox-subj\" id=\"cePvSubject\"></div>\n </div>\n </div>\n <div class=\"set-ce-stage\">\n <div class=\"set-ce-card\">\n <div class=\"set-ce-mast\" id=\"cePvMast\" hidden>\n <div class=\"set-ce-mast-logo\" id=\"cePvMastLogo\"></div>\n <div>\n <div class=\"set-ce-mast-name\" id=\"cePvMastName\"></div>\n <div class=\"set-ce-mast-tag\" id=\"cePvMastTag\"></div>\n </div>\n </div>\n <p class=\"set-ce-msg\" id=\"cePvBody\"></p>\n <p class=\"set-ce-btnrow\"><a class=\"set-ce-btn\" id=\"cePvButton\" href=\"#\" onclick=\"return false\"></a></p>\n <p class=\"set-ce-foot\" id=\"cePvFoot\"></p>\n </div>\n </div>\n </div>\n <div id=\"ceEditBody\" hidden>\n <div class=\"set-ce-edit\">\n <div class=\"set-field\">\n <label for=\"ceSubject\">Subject line</label>\n <input id=\"ceSubject\" value=\"${state.confirmation.subject}\" maxlength=\"200\" autocomplete=\"off\">\n </div>\n <div class=\"set-field\">\n <label for=\"ceMessage\">Message</label>\n <textarea id=\"ceMessage\" rows=\"3\" maxlength=\"1000\">${state.confirmation.body}</textarea>\n <p class=\"field-hint\">The line above the confirm button.</p>\n </div>\n <div class=\"set-field\">\n <label for=\"ceButton\">Button label</label>\n <input id=\"ceButton\" value=\"${state.confirmation.buttonLabel}\" maxlength=\"80\" autocomplete=\"off\">\n </div>\n <div class=\"set-field\">\n <label for=\"ceFooter\">Reassurance line</label>\n <input id=\"ceFooter\" value=\"${state.confirmation.reassurance}\" maxlength=\"400\" autocomplete=\"off\">\n <p class=\"field-hint\">The quiet footer for anyone who didn\u2019t sign up. Leave blank to omit it.</p>\n </div>\n </div>\n <div class=\"set-ce-editfoot\">\n <button type=\"button\" class=\"ghost\" id=\"ceReset\">Reset to Kestrel default</button>\n <span class=\"grow\"></span>\n <button type=\"button\" class=\"ghost\" id=\"ceToPreview\">See preview \u2192</button>\n </div>\n </div>\n </div>\n </section>`;\n}\n\n/** The fields carry only words; the preview resolves a blank required field to the\n * built-in default (from the API) exactly as the send does, so it's never wordless. */\nexport function mountConfirmation(root: HTMLElement, page: SettingsPage): SectionHandle {\n const { state } = page;\n const ceDefault = page.data.settings.confirmationEmailDefault;\n const fields = {\n subject: $<HTMLInputElement>(\"#ceSubject\", root),\n message: $<HTMLTextAreaElement>(\"#ceMessage\", root),\n button: $<HTMLInputElement>(\"#ceButton\", root),\n footer: $<HTMLInputElement>(\"#ceFooter\", root),\n };\n const pv = {\n avatar: $(\"#cePvAvatar\", root),\n from: $(\"#cePvFrom\", root),\n subject: $(\"#cePvSubject\", root),\n body: $(\"#cePvBody\", root),\n button: $(\"#cePvButton\", root),\n foot: $(\"#cePvFoot\", root),\n mast: $(\"#cePvMast\", root),\n mastLogo: $(\"#cePvMastLogo\", root),\n mastName: $(\"#cePvMastName\", root),\n mastTag: $(\"#cePvMastTag\", root),\n };\n const repaint = () => {\n const c = state.confirmation;\n pv.avatar.textContent = monogram(state.name, page.fromName);\n pv.from.textContent = state.name || page.fromName;\n pv.subject.textContent = c.subject.trim() || ceDefault.subject || \"\";\n pv.body.textContent = c.body.trim() || ceDefault.body || \"\";\n pv.button.textContent = c.buttonLabel.trim() || ceDefault.buttonLabel || \"Confirm\";\n pv.foot.textContent = c.reassurance;\n pv.foot.hidden = !c.reassurance.trim();\n // The publication masthead (logo + name + tagline) up top. Mirrors masthead() in\n // src/emails/system.ts exactly: it uses the identity as the email resolves it (name\n // = publication name or the From display name, \"\" if neither), omits the logo cell\n // when there's no logo (rather than showing a monogram the email won't have), and\n // degrades to nothing when there's neither a name nor a logo.\n const mastName = state.name || page.fromDisplay;\n const hasLogo = Boolean(state.logoUrl);\n const showMast = hasLogo || Boolean(mastName);\n pv.mast.hidden = !showMast;\n if (showMast) {\n pv.mastLogo.hidden = !hasLogo;\n if (hasLogo) {\n setHtml(pv.mastLogo, html`<img src=\"${state.logoUrl}\" alt=\"\">`);\n }\n pv.mastName.textContent = mastName;\n pv.mastName.hidden = !mastName;\n pv.mastTag.textContent = state.tagline;\n pv.mastTag.hidden = !state.tagline;\n }\n };\n const onInput = () => {\n state.confirmation.subject = fields.subject.value;\n state.confirmation.body = fields.message.value;\n state.confirmation.buttonLabel = fields.button.value;\n state.confirmation.reassurance = fields.footer.value;\n page.changed();\n };\n for (const el of Object.values(fields)) {\n el.addEventListener(\"input\", onInput);\n }\n\n const editBody = $(\"#ceEditBody\", root);\n const previewBody = $(\"#cePreviewBody\", root);\n const tabEdit = $<HTMLButtonElement>(\"#ceTabEdit\", root);\n const tabPreview = $<HTMLButtonElement>(\"#ceTabPreview\", root);\n const setMode = (edit: boolean) => {\n editBody.hidden = !edit;\n previewBody.hidden = edit;\n tabEdit.setAttribute(\"aria-pressed\", String(edit));\n tabPreview.setAttribute(\"aria-pressed\", String(!edit));\n if (edit) {\n fields.subject.focus();\n }\n };\n tabPreview.onclick = () => setMode(false);\n tabEdit.onclick = () => setMode(true);\n $(\"#ceToPreview\", root).onclick = () => setMode(false);\n $(\"#ceReset\", root).onclick = () => {\n fields.subject.value = ceDefault.subject || \"\";\n fields.message.value = ceDefault.body || \"\";\n fields.button.value = ceDefault.buttonLabel || \"\";\n fields.footer.value = ceDefault.reassurance || \"\";\n onInput();\n };\n\n repaint();\n return {\n repaint,\n reset() {\n fields.subject.value = state.confirmation.subject;\n fields.message.value = state.confirmation.body;\n fields.button.value = state.confirmation.buttonLabel;\n fields.footer.value = state.confirmation.reassurance;\n repaint();\n },\n };\n}\n", "// Settings' \"Set at deploy\" group: the email sender and the instance's infrastructure, a\n// read-only reflection of the deploy-time config that only a redeploy changes (SPEC \u00A79).\n// Secrets never come down this wire (see routes/settings.ts).\n\nimport { DEFAULT_MIN_LEAD_MS, formatLead } from \"../../shared/sends\";\nimport type { DeploymentView, SimulationProfile } from \"../../shared/settings\";\nimport { connectProviderLink } from \"../deployment\";\nimport { type Html, html } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { chip, monogram, type SettingsPage, secHead } from \"./page\";\n\nconst PROVIDER_LABELS: Record<string, string> = {\n none: \"No email provider connected\",\n fake: \"Fake (dev, dead-end)\",\n ses: \"Amazon SES\",\n};\n\n/** The provider a deployment sends through, as Settings names it. */\nexport function providerLabel(provider: string): string {\n return PROVIDER_LABELS[provider] || provider;\n}\n\n/**\n * The Email provider row's value: the provider's name, the simulation a local server runs\n * in its place, or, while none is connected (SPEC \u00A79), the state and the step that ends it.\n */\nfunction providerValue(d: DeploymentView): Html {\n if (d.provider === \"none\") {\n return html`${providerLabel(d.provider)}. ${connectProviderLink(\"set-link\")} to start sending.`;\n }\n return html`${providerLabel(d.provider)}${d.simulation ? html`<span class=\"set-pill\">Simulating ${SIMULATION_LABELS[d.simulation.profile]}${d.simulation.faults === \"none\" ? \", no failures\" : \"\"}</span>` : null}`;\n}\n\n/** The provider a local send simulation models, as the Email provider row names it. */\nconst SIMULATION_LABELS: Record<SimulationProfile, string> = {\n generic: \"a generic provider\",\n ses: \"Amazon SES\",\n};\n\nfunction bareAddress(from: string): string {\n const m = String(from || \"\").match(/<([^>]+)>/);\n return m?.[1] ? m[1] : String(from || \"\");\n}\n\nexport function senderSection(page: SettingsPage): Html {\n const d = page.data.deployment;\n const { fromName } = page;\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Email sender\", chip(\"readonly\", \"Read-only \u00B7 set at deploy\"))}\n <div class=\"set-card sunken\">\n <div class=\"set-ro-note\">${icon(\"readonly\")}<span>The sender is set at deploy in <code>wrangler.jsonc</code>, with the AWS keys stored as secrets, so it can\u2019t be edited here. Change it in the <a class=\"set-link\" href=\"#/docs\">setup guide</a>, then redeploy. Credentials are never shown.</span></div>\n <div class=\"set-preview-bar\" style=\"background:transparent\">\n <span class=\"set-preview-lbl\">Inbox preview</span>\n <span class=\"set-preview-dot\">How readers see the sender</span>\n </div>\n <div class=\"set-inbox\">\n <div class=\"set-inbox-avatar\">${monogram(fromName, fromName)}</div>\n <div class=\"set-inbox-body\">\n <div class=\"set-inbox-top\"><span class=\"set-inbox-from\">${fromName}</span><span class=\"set-inbox-time\">9:02 AM</span></div>\n <div class=\"set-inbox-subj\">Your latest post: a sample subject line</div>\n <div class=\"set-inbox-snip\">The opening lines of your post show here as the inbox preview\u2026</div>\n <div class=\"set-inbox-addr\">${bareAddress(d.fromAddress)}</div>\n </div>\n </div>\n <div class=\"set-kv\" style=\"border-top:1px solid var(--line)\">\n <div class=\"set-kv-k\">From address</div><div class=\"set-kv-v\"><span class=\"mono\">${d.fromAddress}</span></div>\n <div class=\"set-kv-k\">Sending domain</div><div class=\"set-kv-v\"><span class=\"mono\">${d.sendingDomain}</span></div>\n <div class=\"set-kv-k\">Email provider</div><div class=\"set-kv-v\" id=\"providerValue\">${providerValue(d)}</div>\n </div>\n </div>\n </section>`;\n}\n\nexport function instanceSection(page: SettingsPage): Html {\n const d = page.data.deployment;\n const archiveBase = `${d.archiveOrigin || \"\"}${d.archiveBasePath || \"\"}`;\n const archiveIsDefault = d.archiveOrigin === d.appOrigin;\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Instance\", chip(\"readonly\", \"Read-only \u00B7 set at deploy\"))}\n <div class=\"set-card sunken\">\n <div class=\"set-ro-note\">${icon(\"info\")}<span>Deploy-time infrastructure, shown for reference. These live in your Worker config and never pass through the API. See the <a class=\"set-link\" href=\"#/docs/configuration\">setup guide</a> to change them.</span></div>\n <div class=\"set-kv\">\n <div class=\"set-kv-k\">App origin</div><div class=\"set-kv-v\"><span class=\"mono\">${d.appOrigin}</span></div>\n <div class=\"set-kv-k\">Archive URL base</div><div class=\"set-kv-v\"><span class=\"mono\">${archiveBase}</span>${archiveIsDefault ? html`<span class=\"set-pill\">default: app origin</span>` : null}</div>\n <div class=\"set-kv-k\">Image URL base</div><div class=\"set-kv-v\"><span class=\"mono\">${d.mediaPublicBase}</span></div>\n <div class=\"set-kv-k\">Auth mode</div><div class=\"set-kv-v\">${d.authMode === \"access\" ? \"Cloudflare Access\" : \"Local dev token\"}</div>\n <div class=\"set-kv-k\">Cloudflare Access</div><div class=\"set-kv-v\">${d.accessConfigured ? html`<span class=\"set-pill ok\">${icon(\"check\")}Configured</span>` : html`<span class=\"set-pill\">Not configured</span>`}</div>\n <div class=\"set-kv-k\">Minimum lead</div><div class=\"set-kv-v\">${formatLead(d.minLeadMs)}${d.minLeadMs === DEFAULT_MIN_LEAD_MS ? html`<span class=\"set-pill\">default</span>` : null}</div>\n </div>\n </div>\n </section>`;\n}\n", "// The re-make confirmation (SPEC \u00A76, \u00A79; DESIGN \u00A74): the in-use chip and the\n// confirm-then-save flow the Template and Settings pages share.\n\nimport type { InUseView, RemakeRequiredError, ScheduledSendRef } from \"../../shared/settings\";\nimport { ApiError } from \"../api\";\nimport { fmt } from \"../ui/format\";\nimport { type Html, html } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { modal, toast } from \"../ui/widgets\";\n\n/** The words a re-make confirmation uses for the commit: { action: \"Save\", gerund: \"Saving\", lead(n) }. */\nexport interface RemakeVerb {\n action: string;\n gerund: string;\n /** What is in use (\"This template is used by 2 posts \u2026\"). */\n lead: (n: number) => string;\n}\n\nfunction remakeRequired(e: unknown): RemakeRequiredError | null {\n if (e instanceof ApiError && e.status === 409) {\n const d = e.data as { error?: unknown } | null;\n if (d?.error === \"remake_required\") {\n return e.data as RemakeRequiredError;\n }\n }\n return null;\n}\n\n// What a save the page left before confirming says, wherever the publisher went.\nconst NOT_SAVED = \"Not saved: the change needed your confirmation\";\n\n/**\n * The other re-make refusal (SPEC \u00A79): a scheduled send is about to fire, so the save\n * waits until it has sent. A surface shows this one where it blocks (the save bar, the\n * logo's field error), never as a passing toast.\n */\nexport function isRemakeTooClose(e: unknown): boolean {\n if (!(e instanceof ApiError) || e.status !== 409) {\n return false;\n }\n const d = e.data as { error?: unknown } | null;\n return d?.error === \"remake_too_close\";\n}\n\n/**\n * A template or identity change reaches every scheduled email, and the server refuses\n * such a save until the client has acknowledged those sends by id (409 remake_required,\n * listing them). The flow is server-driven so the dashboard never decides which fields\n * count: `attempt(ack)` runs the write with no acknowledgement; a refusal opens the\n * confirmation with the list the server handed back; confirming retries with those ids;\n * a second refusal (a send scheduled meanwhile) asks again with the fresh list. Resolves\n * to the write's response, or to null when the publisher canceled (the caller then leaves\n * everything as it was). Any other refusal, including 409 remake_too_close (a send about\n * to fire), is rethrown for the surface's own blocking-error home. The confirmation\n * belongs to the page, so `signal` is the page's mount signal: leaving the page closes it,\n * which counts as a cancel, and nothing is saved that the publisher never confirmed; a\n * toast says so, since they are no longer on the page that would show it.\n */\nexport async function withRemakeConfirm<T>(\n attempt: (ack: string[] | null) => Promise<T>,\n verb: RemakeVerb,\n signal: AbortSignal,\n): Promise<T | null> {\n let ack: string[] | null = null;\n for (;;) {\n try {\n return await attempt(ack);\n } catch (err) {\n const required = remakeRequired(err);\n if (!required) {\n throw err;\n }\n const sends = required.sends || [];\n const ok = await confirmRemake(sends, verb, signal);\n if (!ok) {\n // A publisher who pressed Save and left before (or while) the confirmation showed\n // would otherwise never learn the save didn't happen. The toast region is the\n // app's, not the page's, so it shows on whatever page they went to.\n if (signal.aborted) {\n toast(NOT_SAVED);\n }\n return null;\n }\n ack = sends.map((s) => s.id);\n }\n }\n}\n\n// The confirmation itself (DESIGN \u00A74): one sentence on what the save reaches, the\n// scheduled posts by name, three short facts the publisher scans (what changes, what\n// stays, what's next), and the alternative (a second template, which Kestrel does not\n// offer yet).\nfunction confirmRemake(\n sends: ScheduledSendRef[],\n verb: RemakeVerb,\n signal: AbortSignal,\n): Promise<boolean> {\n const n = sends.length;\n const one = n === 1;\n const noun = `${n} scheduled email${one ? \"\" : \"s\"}`;\n return new Promise((resolve) => {\n const m = modal(\n html`<h3>Apply this change to ${noun}?</h3>\n <p class=\"hint\">${verb.lead(n)} ${verb.gerund} applies the change to ${one ? \"its email\" : \"their emails\"} too.</p>\n <ul class=\"remake-list\">${sends.map(\n (s) =>\n html`<li><span>${s.subject || html`<em>untitled</em>`}</span><span>sends ${fmt(s.fire_at)}</span></li>`,\n )}</ul>\n <dl class=\"remake-facts\">\n <div><dt>Changes</dt><dd>the look of ${one ? \"its email\" : \"their emails\"}</dd></div>\n <div><dt>Stays</dt><dd>${one ? \"its content and fire time\" : \"their content and fire times\"}</dd></div>\n <div><dt>Next</dt><dd>send yourself a fresh test${one ? \"\" : \" of each\"}</dd></div>\n </dl>\n <p class=\"hint\">To leave scheduled emails as they are while future posts change, you\u2019d need a second template, which Kestrel doesn\u2019t offer yet.</p>\n <div class=\"actions\"><button type=\"button\" id=\"rmCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"rmGo\">${verb.action} and apply</button></div>`,\n signal,\n );\n // The page is already gone, so the dialog never showed: nothing was confirmed.\n if (!m.el.isConnected) {\n resolve(false);\n return;\n }\n let settled = false;\n const done = (v: boolean) => {\n if (!settled) {\n settled = true;\n resolve(v);\n }\n };\n const cancel = m.el.querySelector<HTMLButtonElement>(\"#rmCancel\");\n const go = m.el.querySelector<HTMLButtonElement>(\"#rmGo\");\n if (cancel) {\n cancel.onclick = () => {\n m.close();\n done(false);\n };\n }\n if (go) {\n go.onclick = () => {\n m.close();\n done(true);\n };\n }\n // Backdrop click, Escape, and leaving the page close the modal without going through\n // a button.\n const obs = new MutationObserver(() => {\n if (!m.el.isConnected) {\n obs.disconnect();\n done(false);\n }\n });\n obs.observe(document.body, { childList: true });\n go?.focus();\n });\n}\n\n/** The template surface's lead: what is in use by the scheduled posts. */\nexport const REMAKE_TEMPLATE: RemakeVerb = {\n action: \"Save\",\n gerund: \"Saving\",\n lead: (n) =>\n `This template is used by ${n} post${n === 1 ? \" that\u2019s\" : \"s that are\"} already scheduled.`,\n};\n\n/** The identity surface's lead, for the action the page commits with. */\nexport const remakeIdentity = (action: string, gerund: string): RemakeVerb => ({\n action,\n gerund,\n lead: (n) =>\n `Your name, tagline, address, and logo ride inside every email, including ${n} post${n === 1 ? \" that\u2019s\" : \"s that are\"} already scheduled.`,\n});\n\n/**\n * The standing in-use chip (DESIGN \u00A78): the state a template or identity change would\n * reach, read from GET /api/settings `inUse`.\n */\nexport function inUseChip(inUse: InUseView | null | undefined): Html {\n const sends = inUse?.sends || [];\n if (!sends.length) {\n return html`<span class=\"set-chip\">${icon(\"info\")}No posts scheduled</span>`;\n }\n const n = sends.length;\n const title = inUse?.retry_after\n ? `One sends at ${fmt(inUse.retry_after)}; saving waits until it has sent.`\n : null;\n return title\n ? html`<span class=\"set-chip inuse\" title=\"${title}\"><span class=\"set-chip-dot\"></span>In use by ${n} scheduled post${n === 1 ? \"\" : \"s\"}</span>`\n : html`<span class=\"set-chip inuse\"><span class=\"set-chip-dot\"></span>In use by ${n} scheduled post${n === 1 ? \"\" : \"s\"}</span>`;\n}\n\n/**\n * After a save that applied to scheduled emails, the toast says how many and that a\n * test is needed again (DESIGN \u00A74); with none, the plain confirmation.\n */\nexport function savedToast(what: string, remade: readonly unknown[] | null | undefined): string {\n const n = (remade || []).length;\n if (!n) {\n return what;\n }\n return `${what} and applied to ${n} scheduled email${n === 1 ? \"\" : \"s\"}. Send yourself a test of each.`;\n}\n", "// The email's logo image, spelled once for the render path and the Template page's\n// sample preview, so the preview shows exactly the sign-off the email will carry.\n\nconst escapeAttr = (s: string): string =>\n s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n\n/**\n * What `{{ .Publication.Logo }}` fills: the logo as an `<img>`, or nothing at all when no\n * logo is set. An `<img src=\"\">` is a broken image in every mail client, and a sent\n * email is frozen into the permanent archive, so a missing logo renders no element\n * rather than an empty one. `alt` is the publication name.\n */\nexport function emailLogoHtml(logoUrl: string, name: string): string {\n if (!logoUrl) {\n return \"\";\n }\n return `<img class=\"logo\" src=\"${escapeAttr(logoUrl)}\" alt=\"${escapeAttr(name)}\" width=\"44\" height=\"44\" />`;\n}\n", "// The email template page: the sample preview, the example menu, and the template\n// editor.\n\nimport { emailLogoHtml } from \"../../shared/email_logo\";\nimport { publicationName } from \"../../shared/publication\";\nimport type {\n SettingsPatchBody,\n SettingsResponse,\n SettingsSavedResponse,\n TemplateTestResponse,\n} from \"../../shared/settings\";\nimport { api, describeError } from \"../api\";\nimport { notConnectedTestLine, providerNotConnected, withNoProviderNote } from \"../deployment\";\nimport { mount } from \"../lifecycle\";\nimport { appState } from \"../state\";\nimport { copyFace, copyWith } from \"../ui/copy\";\nimport { $, $$ } from \"../ui/dom\";\nimport { EMAIL_FRAME_SANDBOX, openLinksInNewTab } from \"../ui/email_frame\";\nimport { invalidAddressesMessage, notSentMessage, parseAddresses } from \"../ui/format\";\nimport { highlightTemplate } from \"../ui/highlight\";\nimport { escapeHtml, type Html, html, setHtml, unsafeHtml } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { savebar } from \"../ui/savebar\";\nimport { busy, modal, openMenu, renderError, toast } from \"../ui/widgets\";\nimport { inUseChip, REMAKE_TEMPLATE, savedToast, withRemakeConfirm } from \"./remake\";\n\nconst message = describeError;\n\n// The one layout each post ships inside, authored as an HTML template with\n// logic-less {{ }} placeholders over a fixed variable context. Logic-less means a\n// token is only ever swapped for its value \u2014 nothing executes \u2014 which is why this\n// is a template editor, not a WYSIWYG. Edits commit through the shared save bar. A\n// send renders the saved template through the single render path (SPEC \u00A75, I5) and\n// freezes the result at schedule (I3); the sample here only fills the placeholders\n// with sample values in the browser, so it shows the layout, and a test email is the\n// real render.\n\ninterface TemplateVar {\n token: string;\n desc: string;\n}\ninterface TemplateVarGroup {\n group: string;\n vars: TemplateVar[];\n}\n// The variables a template can drop in, grouped for the reference panel.\nconst EMAIL_TEMPLATE_VARS: TemplateVarGroup[] = [\n {\n group: \"Post\",\n vars: [\n {\n token: \"{{ .Post.Body }}\",\n desc: \"Your post\u2019s Markdown, rendered to HTML.\",\n },\n { token: \"{{ .Post.Subject }}\", desc: \"The post\u2019s subject line.\" },\n ],\n },\n {\n group: \"Publication\",\n vars: [\n { token: \"{{ .Publication.Name }}\", desc: \"Publication name (from Identity, above).\" },\n { token: \"{{ .Publication.Tagline }}\", desc: \"Your tagline.\" },\n {\n token: \"{{ .Publication.Logo }}\",\n desc: \"Your logo as an image, or nothing when no logo is set.\",\n },\n { token: \"{{ .Publication.LogoURL }}\", desc: \"Absolute URL of your logo, or empty.\" },\n {\n token: \"{{ .Publication.Address }}\",\n desc: \"Your mailing address, if you\u2019ve added one in Settings; empty otherwise. Keep it in the footer if you send promotional email.\",\n },\n ],\n },\n {\n group: \"Email\",\n vars: [\n { token: \"{{ .Email.SentTo }}\", desc: \"The recipient\u2019s address (filled per send).\" },\n { token: \"{{ .Email.UnsubscribeURL }}\", desc: \"Their one-click unsubscribe link.\" },\n { token: \"{{ .Email.ViewInBrowserURL }}\", desc: \"The web version\u2019s permanent URL.\" },\n ],\n },\n {\n group: \"Email only\",\n vars: [\n {\n token: \"{{ if .IsEmail }} \u2026 {{ end }}\",\n desc: \"Reports whether this is the email or the web version. Put your unsubscribe link and address inside; the web version leaves them out.\",\n },\n ],\n },\n];\n\n// Two starting points; the publisher edits the HTML freely from there. Identity sits\n// at the FOOT (a sign-off), so the email stays faithful to today's masthead-free top.\n// The example templates are authored with a <style> block + classes \u2014 clean to read\n// and edit. A real send can't rely on a <style> block (Gmail/Outlook strip or ignore\n// it), so the real engine (a later step) would INLINE these rules at render time:\n// author with a stylesheet, inline on the way out \u2014 the standard email pattern, and\n// what src/render/template.ts already does by hand (inline base + a <style> block\n// only for what inline can't express, like dark mode). The preview renders the\n// template as-is in an isolated iframe, so a <style> block behaves exactly as a mail\n// client \u2014 or the view-in-browser page \u2014 would show it.\ninterface TemplateExample {\n label: string;\n /** The line under its name in the \"Start from example\" menu. */\n description: string;\n html: string;\n}\ntype ExampleKey = \"signed\" | \"plain\";\nexport const EMAIL_TEMPLATE_EXAMPLES: Record<ExampleKey, TemplateExample> = {\n signed: {\n label: \"Signed\",\n description:\n \"Your post, then your logo, name, and tagline as a sign-off. Your mailing address sits in the footer once you add one.\",\n html: `<style>\n .email {\n font: 16px/1.6 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\n color: #18181b;\n }\n .email h1,\n .email h2,\n .email h3 {\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.2;\n }\n .email a {\n color: #3355cc;\n }\n .email .rule {\n border: 0;\n border-top: 1px solid #e4e4e7;\n margin: 28px 0;\n }\n .signoff td {\n vertical-align: middle;\n }\n .signoff .logo {\n display: block;\n border-radius: 9px;\n margin-right: 14px;\n }\n .signoff .name {\n font: 600 17px/1.2 Georgia, 'Times New Roman', serif;\n }\n .signoff .tagline {\n font-size: 13px;\n color: #52525b;\n margin-top: 2px;\n }\n .footer {\n margin-top: 22px;\n font-size: 12px;\n line-height: 1.7;\n color: #8a8a93;\n }\n .footer a {\n color: #8a8a93;\n text-decoration: underline;\n }\n .footer .address {\n margin-top: 6px;\n }\n @media (prefers-color-scheme: dark) {\n .email {\n color: #ededed !important;\n }\n .email a {\n color: #93c5fd !important;\n }\n .email .rule {\n border-color: #2e2e33 !important;\n }\n .signoff .name {\n color: #ededed !important;\n }\n .signoff .tagline {\n color: #a1a1aa !important;\n }\n .footer {\n color: #a1a1aa !important;\n }\n .footer a {\n color: #a1a1aa !important;\n }\n }\n</style>\n\n<div class=\"email\">\n {{ .Post.Body }}\n\n <hr class=\"rule\" />\n\n <table class=\"signoff\" role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\">\n <tr>\n <td class=\"logo-cell\">{{ .Publication.Logo }}</td>\n <td>\n <div class=\"name\">{{ .Publication.Name }}</div>\n <div class=\"tagline\">{{ .Publication.Tagline }}</div>\n </td>\n </tr>\n </table>\n\n <div class=\"footer\">\n Powered by Kestrel{{ if .IsEmail }} \u00B7\n <a href=\"{{ .Email.UnsubscribeURL }}\">Unsubscribe</a> \u00B7\n <a href=\"{{ .Email.ViewInBrowserURL }}\">View in browser</a>\n <div class=\"address\">{{ .Publication.Address }}</div>{{ end }}\n </div>\n</div>`,\n },\n plain: {\n label: \"Plain\",\n description:\n \"Just your post and the footer links. Your mailing address sits in the footer once you add one.\",\n html: `<style>\n .email {\n font: 16px/1.6 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\n color: #18181b;\n }\n .email h1,\n .email h2,\n .email h3 {\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.2;\n }\n .email a {\n color: #3355cc;\n }\n .email .rule {\n border: 0;\n border-top: 1px solid #e4e4e7;\n margin: 28px 0;\n }\n .footer {\n font-size: 12px;\n line-height: 1.7;\n color: #8a8a93;\n }\n .footer a {\n color: #8a8a93;\n text-decoration: underline;\n }\n .footer .address {\n margin-top: 6px;\n }\n @media (prefers-color-scheme: dark) {\n .email {\n color: #ededed !important;\n }\n .email a {\n color: #93c5fd !important;\n }\n .email .rule {\n border-color: #2e2e33 !important;\n }\n .footer {\n color: #a1a1aa !important;\n }\n .footer a {\n color: #a1a1aa !important;\n }\n }\n</style>\n\n<div class=\"email\">\n {{ .Post.Body }}\n\n <hr class=\"rule\" />\n\n <div class=\"footer\">\n Powered by Kestrel{{ if .IsEmail }} \u00B7\n <a href=\"{{ .Email.UnsubscribeURL }}\">Unsubscribe</a> \u00B7\n <a href=\"{{ .Email.ViewInBrowserURL }}\">View in browser</a>\n <div class=\"address\">{{ .Publication.Address }}</div>{{ end }}\n </div>\n</div>`,\n },\n};\nconst isExampleKey = (k: string): k is ExampleKey => Object.hasOwn(EMAIL_TEMPLATE_EXAMPLES, k);\n\n// Sample post body for the preview \u2014 representative prose inside a called-out slot,\n// so it's unmistakable where a real post's rendered Markdown lands. Its typography\n// comes from the template's own .email rules (the callout frame + label are a\n// preview device, not part of the email). In a real send {{ .Post.Body }} is the\n// rendered Markdown.\nconst EMAIL_TEMPLATE_SAMPLE_BODY =\n '<div style=\"position:relative;border:1px dashed #93a7e6;border-radius:8px;padding:20px 14px 8px;margin:0 0 6px\">' +\n \"<span style=\\\"position:absolute;top:-8px;left:10px;font:650 10px/1.4 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;letter-spacing:.04em;text-transform:uppercase;color:#3355cc;background:#fff;padding:0 6px\\\">Your post\u2019s Markdown renders here</span>\" +\n '<h2 style=\"margin:0 0 10px\">Lorem ipsum,</h2>' +\n '<p style=\"margin:0\">Dolor sit amet, consectetur adipiscing elit. Proin sed ex ipsum. Suspendisse vulputate nisi et odio dapibus, quis pellentesque felis sollicitudin. Proin vel cursus enim. Phasellus sollicitudin malesuada elementum. Suspendisse euismod eros turpis, ut mollis est imperdiet ut. Sed luctus accumsan erat, at eleifend purus eleifend quis.</p>' +\n \"</div>\";\n\n// Fill logic-less {{ token }} placeholders from a flat context. {{ .Post.Body }} (the\n// rendered Markdown) and {{ .Publication.Logo }} (built with its own escaping) are raw\n// HTML; every other value is escaped, so a stray < or \"\n// in a name can't break the surrounding markup. An unknown token renders empty, and the\n// region tags ({{ if .IsEmail }}, {{ end }}) render nothing: the sample is the email, which\n// carries the region's content. A string\n// builder by nature: the template is the publisher's own HTML, filled in and vouched for\n// at the preview's boundary (mountSampleEmailPreview).\nconst RAW_TOKENS = new Set([\".Post.Body\", \".Publication.Logo\"]);\nfunction fillEmailTemplate(template: string, ctx: Record<string, string>): string {\n const unmarked = String(template).replace(/\\{\\{\\s*(?:if\\b[^}]*|end)\\s*\\}\\}/g, \"\");\n return unmarked.replace(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g, (_m, key: string) =>\n RAW_TOKENS.has(key) ? ctx[key] || \"\" : escapeHtml(ctx[key] ?? \"\"),\n );\n}\n\n/** The identity a sample preview binds: the live or loaded publication fields. */\nexport interface TemplateIdentity {\n name: string;\n tagline: string;\n logoUrl: string;\n address: string;\n}\n\n// Sample values the template preview binds \u2014 mirrors the render path's context, with\n// .Email.* standing in for per-recipient values. The logo is the real one or none, never\n// a stand-in: the preview shows the sign-off the email will carry.\nfunction templateSampleCtx(id: TemplateIdentity): Record<string, string> {\n const name = id.name || \"Your publication\";\n return {\n \".Post.Body\": EMAIL_TEMPLATE_SAMPLE_BODY,\n \".Post.Subject\": \"The starlings are back\",\n \".Publication.Name\": name,\n \".Publication.Tagline\": id.tagline || \"Your tagline\",\n \".Publication.LogoURL\": id.logoUrl,\n \".Publication.Logo\": emailLogoHtml(id.logoUrl, name),\n \".Publication.Address\": id.address,\n \".Email.SentTo\": \"you@example.com\",\n \".Email.UnsubscribeURL\": \"#unsubscribe\",\n \".Email.ViewInBrowserURL\": \"#view-in-browser\",\n };\n}\n\n/** A template filled with the preview's sample values: what the sample email shows. */\nexport function sampleEmailHtml(template: string, id: TemplateIdentity): string {\n return fillEmailTemplate(template, templateSampleCtx(id));\n}\n\n// The isolated preview document: a white (dark in dark mode) email canvas whose\n// reading column is capped at the email measure (~640px, matching view-in-browser),\n// so a template's own <style> applies as a mail client would and never leaks out.\nconst TEMPLATE_FRAME_DOC =\n '<!doctype html><html><head><meta charset=\"utf-8\">' +\n '<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">' +\n \"<style>html,body{margin:0}body{background:#fff}\" +\n \"@media (prefers-color-scheme:dark){body{background:#18181b}}\" +\n \".kestrel-email{max-width:640px;margin:0 auto;padding:26px 20px;box-sizing:border-box}\" +\n \".kestrel-email img{max-width:100%}</style></head>\" +\n '<body><div class=\"kestrel-email\"></div></body></html>';\n\n/** A mounted sample-email preview; call `repaint` after the template or identity changes. */\nexport interface SampleEmailPreview {\n repaint(): void;\n}\n\n/**\n * Mount a sample-email preview into an <iframe>, kept sized to its content. getTemplate()\n * returns the current template HTML; getIdentity() the live identity. Shared by the\n * Template page (live editor preview) and Settings (a read-only one). The frame takes the\n * Preview tab's sandbox: the template is HTML Claude may write too (`PUT /api/settings`),\n * and markup set into a same-origin frame runs an inline handler (`onerror`) though it\n * leaves a `<script>` inert, so without the sandbox a template could act as the\n * publisher (SPEC \u00A75).\n */\nexport function mountSampleEmailPreview(\n iframe: HTMLIFrameElement,\n getTemplate: () => string,\n getIdentity: () => TemplateIdentity,\n): SampleEmailPreview {\n // Before the srcdoc below: a sandbox applies from the frame's next navigation.\n iframe.setAttribute(\"sandbox\", EMAIL_FRAME_SANDBOX);\n let ready = false;\n const size = () => {\n try {\n // Measure the BODY, which is content-sized. documentElement.scrollHeight is floored\n // at the iframe's own height, so it can grow but never shrink \u2014 which is what left\n // the frame too tall after 375 \u2192 640 and clipped the footer at 640 \u2192 375.\n const h = iframe.contentDocument?.body?.scrollHeight;\n if (h) {\n iframe.style.height = `${Math.max(200, h)}px`;\n }\n } catch {}\n };\n const repaint = () => {\n const doc = iframe.contentDocument;\n const slot = ready && doc ? doc.querySelector(\".kestrel-email\") : null;\n if (!slot) {\n return;\n }\n // Set as markup (not srcdoc per keystroke): flicker-free. A <script> stays inert and\n // the frame's sandbox stops an inline handler. The publisher's own template, filled\n // with sample values, in its own frame.\n setHtml(slot, unsafeHtml(sampleEmailHtml(getTemplate(), getIdentity())));\n openLinksInNewTab(slot.ownerDocument);\n size();\n };\n iframe.addEventListener(\"load\", () => {\n ready = true;\n repaint();\n // Keep the frame fitted to its content through every change: an edit, the width\n // toggle's animated reflow, or a logo/font finishing loading. The body is\n // content-sized and size() only touches the outer iframe, so this can't loop.\n try {\n const doc = iframe.contentDocument;\n if (doc?.body && typeof ResizeObserver !== \"undefined\") {\n new ResizeObserver(size).observe(doc.body);\n }\n } catch {}\n });\n iframe.srcdoc = TEMPLATE_FRAME_DOC;\n return { repaint };\n}\n\n// The grouped variable reference (a <details> body), shared by both surfaces.\nfunction templateVarsHtml(): Html {\n return html`${EMAIL_TEMPLATE_VARS.map(\n (g) =>\n html`<div class=\"set-tpl-vargroup\"><h4>${g.group}</h4>${g.vars.map(\n (v) =>\n html`<div class=\"set-tpl-var\"><code>${v.token}</code><span class=\"set-tpl-var-desc\">${v.desc}</span></div>`,\n )}</div>`,\n )}`;\n}\n\n/** What a save of the template reports: the advisory warnings and the sends it re-made. */\ninterface TemplateSaved {\n warnings: string[];\n remade: SettingsSavedResponse[\"remade\"];\n}\n\n/**\n * The Email template page (top-level \"Template\" nav item). The one layout each post\n * is sent inside: a live sample-email preview over an HTML editor (with starter\n * examples, a variable reference, and its own validated Save). Editing lives here,\n * not in Settings, so each surface has a single, unambiguous save.\n */\nexport async function renderTemplate(root: HTMLElement, signal: AbortSignal): Promise<void> {\n setHtml(\n root,\n html`<div class=\"tpl-page\"><div class=\"page-head\"><div class=\"page-head-row\"><h1>Email template</h1><span id=\"tplInUse\"></span></div><p class=\"set-lede set-page-lede\">The template controls the look and feel of the emails you send. You write it as HTML with a <code><style></code> block and <code>{{ variables }}</code> Kestrel fills in; your post\u2019s Markdown is rendered into <code>{{ .Post.Body }}</code>.</p></div><div id=\"tplBody\" class=\"muted\">Loading\u2026</div></div>`,\n );\n const bodyEl = $(\"#tplBody\", root);\n let data: SettingsResponse;\n try {\n data = await api<SettingsResponse>(\"/api/settings\", { signal });\n } catch (e) {\n renderError(bodyEl, message(e), () => mount(renderTemplate));\n return;\n }\n const s = data.settings;\n const d = data.deployment;\n const p = s.publication;\n const identity: TemplateIdentity = {\n name: publicationName(p.name || \"\", d.fromAddress),\n tagline: p.tagline || \"\",\n logoUrl: p.logoUrl || \"\",\n address: p.address || \"\",\n };\n let templateBaseline = s.emailTemplate || \"\";\n const defaultRecipients = Array.isArray(s.testRecipients) ? s.testRecipients : [];\n // Standing: whether scheduled posts are using this template (SPEC \u00A79, DESIGN \u00A78).\n setHtml($(\"#tplInUse\"), inUseChip(data.inUse));\n\n setHtml(\n bodyEl,\n html`\n <div class=\"set-preview set-tpl-sample\">\n <div class=\"set-preview-bar\">\n <span class=\"set-preview-titles\">\n <span class=\"set-preview-lbl\">Sample email</span>\n <span class=\"set-preview-dot\">A preview with sample content, showing the layout used for a sent email</span>\n </span>\n <span class=\"set-preview-actions\">\n <span class=\"set-wtog\" role=\"group\" aria-label=\"Preview width\">\n <button type=\"button\" class=\"wtog-btn\" data-w=\"640\" aria-pressed=\"true\">640</button>\n <button type=\"button\" class=\"wtog-btn\" data-w=\"375\" aria-pressed=\"false\">375</button>\n </span>\n <button type=\"button\" class=\"secondary\" id=\"tplTest\">${icon(\"send\")}<span id=\"tplTestLbl\">Send test email</span></button>\n </span>\n </div>\n <div class=\"set-email-stage\" id=\"tplStage\">\n <iframe class=\"set-email-frame\" id=\"tplPreview\" title=\"Sample email preview\" scrolling=\"no\"></iframe>\n </div>\n <div class=\"set-preview-cap\">Your post\u2019s Markdown fills the body, and the <code>{{ .Email.* }}</code> values are set for each recipient when the post sends. Email clients render differently, so send yourself a test to see it in a real inbox.</div>\n </div>\n\n <div class=\"set-card\">\n <div class=\"set-card-pad\">\n <div class=\"set-tpl-block\">\n <div class=\"set-tpl-editor-head\">\n <div class=\"set-tpl-tools\">\n <button type=\"button\" class=\"ghost set-menu-btn\" id=\"tplExamplesBtn\" aria-haspopup=\"menu\" aria-expanded=\"false\"><span>Start from example</span><span class=\"set-menu-caret\"></span></button>\n <button type=\"button\" class=\"set-icon-btn\" id=\"tplLineNums\" aria-pressed=\"false\" title=\"Show line numbers\" aria-label=\"Show line numbers\">${icon(\"lines\")}</button>\n <button type=\"button\" class=\"ghost\" id=\"tplCopyAll\" title=\"Copy the whole template to the clipboard\" aria-label=\"Copy the whole template\">${copyFace()}</button>\n </div>\n <div class=\"set-tpl-required\" aria-label=\"Required variables\">\n <span class=\"set-req-lbl\">Required</span>\n <span class=\"set-req-pill\" id=\"reqBody\"><span class=\"dot\"></span>{{ .Post.Body }}</span>\n <span class=\"set-req-pill\" id=\"reqUnsub\"><span class=\"dot\"></span>{{ .Email.UnsubscribeURL }}</span>\n </div>\n </div>\n <div class=\"set-tpl-editor-wrap\" id=\"tplEditorWrap\">\n <div class=\"set-tpl-gutter\" id=\"tplGutter\" aria-hidden=\"true\">1</div>\n <pre class=\"set-tpl-hl\" id=\"tplHl\" aria-hidden=\"true\"><code></code></pre>\n <textarea id=\"tplEditor\" class=\"set-tpl-editor\" spellcheck=\"false\" wrap=\"off\" aria-label=\"Email template HTML\"></textarea>\n </div>\n <div class=\"set-tpl-msgs\" id=\"tplMsgs\" hidden></div>\n </div>\n </div>\n <div class=\"set-note\">${icon(\"info\")}<span>Kestrel uses this one template for every post, starting from a sensible default. Saving a change while posts are scheduled applies it to their emails too, after you confirm, so nothing scheduled goes out on an older look; their content stays as it was. Sent emails are archived exactly as they went out and never change. Save and Discard are in the bar at the bottom of the page.</span></div>\n </div>\n\n <div class=\"set-card set-tpl-varcard\">\n <div class=\"set-card-pad\">\n <div class=\"set-tpl-varhead\">\n <h3 class=\"set-tpl-vartitle\">Variables</h3>\n <p class=\"field-hint\">Kestrel replaces these variables with real values when you send an email. Type a variable exactly as shown, or it renders as empty.</p>\n </div>\n <div class=\"set-tpl-vars-body\">${templateVarsHtml()}</div>\n </div>\n </div>`,\n );\n\n const tplEditor = $<HTMLTextAreaElement>(\"#tplEditor\");\n const preview = mountSampleEmailPreview(\n $<HTMLIFrameElement>(\"#tplPreview\"),\n () => tplEditor.value,\n () => identity,\n );\n const tplMsgsEl = $(\"#tplMsgs\");\n const tplTestEl = $<HTMLButtonElement>(\"#tplTest\");\n const tplTestLbl = $(\"#tplTestLbl\");\n const isDirty = () => tplEditor.value !== templateBaseline;\n\n // Syntax-highlight overlay: a transparent <textarea> over a highlighted\n // <pre>, plus a line-number gutter, kept in scroll sync. Vanilla, no dependency.\n const tplHl = $(\"#tplHl\");\n const tplHlCode = $(\"code\", tplHl);\n const tplGutter = $(\"#tplGutter\");\n const paintEditor = () => {\n const src = tplEditor.value;\n // Trailing newline so the last line renders and the overlay height matches the textarea.\n setHtml(tplHlCode, html`${highlightTemplate(src)}\\n`);\n let g = \"\";\n const lines = src.split(\"\\n\").length;\n for (let i = 1; i <= lines; i++) {\n g += `${i}\\n`;\n }\n tplGutter.textContent = g;\n };\n const syncScroll = () => {\n tplHl.scrollTop = tplEditor.scrollTop;\n tplHl.scrollLeft = tplEditor.scrollLeft;\n tplGutter.scrollTop = tplEditor.scrollTop;\n };\n tplEditor.addEventListener(\"scroll\", syncScroll);\n\n // The two blocking-required tokens, predicted live in the toolbar pills: green when\n // present, red when missing \u2014 so a rejected save is visible before you press Save.\n const reqBodyEl = $(\"#reqBody\");\n const reqUnsubEl = $(\"#reqUnsub\");\n const paintReq = () => {\n reqBodyEl.className = `set-req-pill ${/\\{\\{\\s*\\.Post\\.Body\\s*\\}\\}/.test(tplEditor.value) ? \"ok\" : \"bad\"}`;\n reqUnsubEl.className = `set-req-pill ${/\\{\\{\\s*\\.Email\\.UnsubscribeURL\\s*\\}\\}/.test(tplEditor.value) ? \"ok\" : \"bad\"}`;\n };\n\n // Save + Discard live in the shared bottom save bar (onSave/onDiscard below); the\n // page never renders its own Save button. A rejected save (e.g. a template missing\n // {{ .Email.UnsubscribeURL }}, a 400) is a blocking error, so it shows IN the bar\n // (which stays up, right beside Save). Warnings are advisory and describe the\n // template that was just saved, so they stay inline under the editor.\n const bar = savebar.attach({ onSave: onSaveTemplate, onDiscard: revertTemplate }, signal);\n\n function refreshDirty() {\n paintEditor();\n paintReq();\n syncScroll();\n bar.setDirty(isDirty());\n // A test always sends the SAVED template (what will ship, I5). When there are\n // unsaved edits the button says so plainly: it saves first, then sends.\n tplTestLbl.textContent = isDirty() ? \"Save & send test\" : \"Send test email\";\n tplTestEl.title = isDirty()\n ? \"Saves your changes first, then sends: a test always reflects the saved template that will ship.\"\n : \"Sends a sample post through the saved template so you can see it in a real inbox.\";\n }\n // Advisory warnings for the template that was just saved; blocking errors go to the\n // save bar instead (bar.showError), so the bar owns the blocking state and this owns\n // the post-save advisory state.\n function showWarnings(msgs: string[]) {\n if (!msgs.length) {\n tplMsgsEl.hidden = true;\n setHtml(tplMsgsEl, html``);\n return;\n }\n tplMsgsEl.hidden = false;\n tplMsgsEl.className = \"set-tpl-msgs warn\";\n setHtml(tplMsgsEl, html`${msgs.map((m) => html`<div>${m}</div>`)}`);\n }\n function revertTemplate() {\n tplEditor.value = templateBaseline;\n showWarnings([]);\n preview.repaint();\n refreshDirty();\n }\n // Persist the current editor content, confirming first when the save would apply to\n // scheduled emails (withRemakeConfirm). Returns the warnings and what was re-made, or\n // null when the publisher declined the confirmation (nothing was saved; the page stays\n // dirty). THROWS on a rejected template (no unsubscribe link \u2192 400) or a send about to\n // fire (409 remake_too_close), so callers decide what to do. Shared by the save bar's\n // Save and Save-&-send-test.\n async function saveTemplate(): Promise<TemplateSaved | null> {\n const value = tplEditor.value;\n const r = await withRemakeConfirm(\n (ack) => {\n const body: SettingsPatchBody = ack\n ? { emailTemplate: value, remake: ack }\n : { emailTemplate: value };\n return api<SettingsSavedResponse>(\"/api/settings\", { method: \"PUT\", json: body });\n },\n REMAKE_TEMPLATE,\n signal,\n );\n if (!r) {\n return null;\n }\n // The server may resolve \"\" to the default \u2014 reflect what was actually stored.\n templateBaseline = r.settings.emailTemplate;\n tplEditor.value = templateBaseline;\n // The cached config the sidebar reads: this page's own response, with the saved half.\n appState.appConfig = { ...data, settings: r.settings };\n preview.repaint();\n refreshDirty(); // clean now \u2014 slides the bar away\n return { warnings: Array.isArray(r.warnings) ? r.warnings : [], remade: r.remade || [] };\n }\n // The shared save bar's Save button (run inside busy() by the controller). Persists,\n // surfaces warnings inline; a rejected save keeps the bar up and shows why in it.\n async function onSaveTemplate() {\n try {\n const saved = await saveTemplate();\n if (!saved) {\n return; // declined: the edits stay, the bar stays up\n }\n showWarnings(saved.warnings);\n toast(\n savedToast(\n saved.warnings.length ? \"Template saved with warnings\" : \"Template saved\",\n saved.remade,\n ),\n );\n } catch (err) {\n // The bar owns the blocking error (it stays up and says why). No toast \u2014 a\n // bottom-center toast would sit on top of the bar and hide the very message.\n bar.showError(message(err));\n }\n }\n\n const loadExample = (key: string) => {\n const ex = EMAIL_TEMPLATE_EXAMPLES[isExampleKey(key) ? key : \"signed\"];\n tplEditor.value = ex.html;\n showWarnings([]);\n preview.repaint();\n refreshDirty();\n };\n tplEditor.addEventListener(\"input\", () => {\n preview.repaint();\n refreshDirty();\n });\n // Examples: a \"Start from example\" dropdown menu (a compact, secondary action \u2014\n // loading one is destructive, so it isn't a permanent fixture on the page). The shared\n // menu gives it the keyboard and focus a menu owes (DESIGN \u00A711) and closes with the page.\n const exBtn = $<HTMLButtonElement>(\"#tplExamplesBtn\");\n exBtn.onclick = () =>\n openMenu(\n exBtn,\n Object.entries(EMAIL_TEMPLATE_EXAMPLES).map(([key, ex]) => ({\n label: ex.label,\n description: ex.description,\n onClick: () => loadExample(key),\n })),\n signal,\n { align: \"start\" },\n );\n\n // Preview width toggle (640 / 375) \u2014 proof both inbox measures; 640 is the default.\n const frameEl = $<HTMLIFrameElement>(\"#tplPreview\");\n for (const wb of $$<HTMLButtonElement>(\".wtog-btn\", bodyEl)) {\n wb.onclick = () => {\n for (const o of $$<HTMLButtonElement>(\".wtog-btn\", bodyEl)) {\n o.setAttribute(\"aria-pressed\", String(o === wb));\n }\n frameEl.style.maxWidth = `${wb.dataset.w}px`;\n preview.repaint();\n };\n }\n\n // Line numbers: hidden by default; the toolbar toggle shows them and the choice is\n // remembered per browser (a lightweight convenience \u2014 safe to lose).\n const editorWrap = $(\"#tplEditorWrap\");\n const lineNumsBtn = $<HTMLButtonElement>(\"#tplLineNums\");\n const setLineNums = (on: boolean) => {\n editorWrap.classList.toggle(\"show-lines\", on);\n lineNumsBtn.setAttribute(\"aria-pressed\", String(on));\n syncScroll();\n };\n let lineNumsOn = false;\n try {\n lineNumsOn = localStorage.getItem(\"kestrel.tpl.lineNums\") === \"1\";\n } catch {}\n setLineNums(lineNumsOn);\n lineNumsBtn.onclick = () => {\n lineNumsOn = !lineNumsOn;\n setLineNums(lineNumsOn);\n try {\n localStorage.setItem(\"kestrel.tpl.lineNums\", lineNumsOn ? \"1\" : \"0\");\n } catch {}\n };\n\n // Copy the whole template to the clipboard, said on the button (ui/copy.ts).\n const copyAllBtn = $<HTMLButtonElement>(\"#tplCopyAll\");\n copyAllBtn.onclick = () => copyWith(copyAllBtn, tplEditor.value);\n\n // --- send a test of the saved template (edit \u2192 test \u2192 iterate) ---\n // A test renders a sample post through the SAVED template \u2014 what will actually\n // ship (I5). So if the editor is dirty we save first (a \"Save & send test\" flow);\n // a rejected save (e.g. missing unsubscribe) stops the send, honestly. We never\n // render unsaved editor content, which would test something that won't ship.\n tplTestEl.onclick = () => {\n // While no provider is connected the server would refuse a test, so the dialog leaves\n // out the recipients and keeps only the standing line (DESIGN \u00A74), and says nothing of\n // saving first, since nothing will be sent.\n const notConnected = providerNotConnected();\n const dirty = !notConnected && isDirty();\n const m = modal(\n html`<h3>Send a test email</h3><p class=\"hint\">Delivers a sample post rendered through your <strong>saved</strong> template, so you can see it in a real inbox. One address per line.</p>${notConnectedTestLine()}${\n dirty\n ? html`<p class=\"hint\" style=\"color:var(--warn-fg)\"><strong>Unsaved changes:</strong> sending will save your template first, so the test reflects what will actually ship.</p>`\n : null\n }${notConnected ? null : html`<label for=\"tplTestTo\">Recipients</label><textarea id=\"tplTestTo\" rows=\"3\" placeholder=\"you@example.com\"></textarea><p class=\"hint\" id=\"tplTestHint\" hidden></p>`}<div class=\"actions\"><button type=\"button\" id=\"ttCancel\">Cancel</button><button type=\"button\" class=\"primary\" id=\"ttGo\">${\n dirty ? \"Save & send test\" : \"Send test\"\n }</button></div>`,\n signal,\n );\n const go = $<HTMLButtonElement>(\"#ttGo\", m.el);\n $(\"#ttCancel\", m.el).onclick = m.close;\n go.disabled = notConnected;\n if (notConnected) {\n $(\"#ttCancel\", m.el).focus();\n return;\n }\n const to = $<HTMLTextAreaElement>(\"#tplTestTo\", m.el);\n to.focus();\n // Pre-fill from the saved default test recipients (don't clobber typed input).\n if (defaultRecipients.length && !to.value.trim()) {\n to.value = defaultRecipients.join(\"\\n\");\n const hint = $(\"#tplTestHint\", m.el);\n hint.textContent = \"Pre-filled from your default test recipients (Settings).\";\n hint.hidden = false;\n }\n go.onclick = () =>\n busy(go, isDirty() ? \"Saving\u2026\" : \"Sending\u2026\", async () => {\n m.clearError();\n const { valid: addrs, invalid } = parseAddresses(to.value);\n const notAddresses = invalidAddressesMessage(invalid);\n if (notAddresses) {\n m.error(notAddresses, to);\n return;\n }\n if (!addrs.length) {\n m.error(\"Enter at least one email address\", to);\n return;\n }\n // Honest unsaved-changes handling: persist first so the test renders what ships.\n // A rejected save shows in the bar and stops the send.\n if (isDirty()) {\n try {\n const saved = await saveTemplate();\n if (!saved) {\n m.close(); // declined the re-make: nothing saved, so nothing to test yet\n return;\n }\n showWarnings(saved.warnings);\n } catch (err) {\n // The save failed, so the test can't send what would ship. The bar shows\n // why (and stays up); closing the dialog returns you to it. No toast \u2014 it\n // would overlay the bar and hide the reason.\n bar.showError(message(err));\n m.close();\n return;\n }\n }\n try {\n const r = await api<TemplateTestResponse>(\"/api/settings/template/test\", {\n method: \"POST\",\n json: { to: addrs },\n });\n if (r.failed.length) {\n // The dialog stays open with only the addresses that didn't get it, so Send again\n // tries those and no one gets a second copy.\n to.value = r.failed.join(\"\\n\");\n m.error(notSentMessage(r.sent, r.total, r.failed));\n return;\n }\n m.close();\n toast(withNoProviderNote(`Test sent to ${r.sent} address${r.sent === 1 ? \"\" : \"es\"}`));\n } catch (e) {\n m.error(message(e));\n }\n });\n };\n tplEditor.value = templateBaseline;\n preview.repaint();\n refreshDirty();\n}\n", "// Settings' \"Your emails\" group: the email template, shown as a sample email with the way\n// to the Template page where it is edited, and the default test recipients the publisher\n// proofs a post against.\n\nimport { isValidEmail, normalizeEmail } from \"../../shared/email\";\nimport { $ } from \"../ui/dom\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { fieldError } from \"../ui/widgets\";\nimport { chip, type SectionHandle, type SettingsPage, secHead } from \"./page\";\nimport { inUseChip } from \"./remake\";\nimport { mountSampleEmailPreview } from \"./template\";\n\n/** The template as what a publication's email looks like: a sample email in the saved\n * template, read-only, with the way to the Template page, where editing lives (so each\n * surface has one save). */\nexport function templateSection(page: SettingsPage): Html {\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Email template\", chip(\"editable\", \"Editable\"), inUseChip(page.data.inUse))}\n <p class=\"set-lede\">The template controls the look and feel of the emails you send. Edit it on the Template page.</p>\n <div class=\"set-preview set-tpl-sample\">\n <div class=\"set-preview-bar\">\n <span class=\"set-preview-lbl\">Sample email</span>\n <span class=\"set-preview-dot\">Current template</span>\n </div>\n <iframe class=\"set-email-frame\" id=\"tplPreview\" title=\"Sample email preview\" scrolling=\"no\"></iframe>\n </div>\n <div class=\"row\" style=\"margin-top:12px\"><button type=\"button\" id=\"tplEditLink\">Edit template \u2192</button></div>\n </section>`;\n}\n\n/** The sample follows the identity fields as they are typed. */\nexport function mountTemplate(root: HTMLElement, page: SettingsPage): SectionHandle {\n const template = page.data.settings.emailTemplate || \"\";\n const preview = mountSampleEmailPreview(\n $<HTMLIFrameElement>(\"#tplPreview\", root),\n () => template,\n () => ({\n name: page.state.name || page.fromDisplay,\n tagline: page.state.tagline,\n logoUrl: page.state.logoUrl,\n address: page.state.address,\n }),\n );\n $(\"#tplEditLink\", root).onclick = () => {\n location.hash = \"#/template\";\n };\n return { repaint: () => preview.repaint() };\n}\n\nexport function recipientsSection(): Html {\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Default test recipients\", chip(\"editable\", \"Editable\"))}\n <div class=\"set-card\">\n <div class=\"set-recip\">\n <p class=\"field-hint\" style=\"margin:0\">Pre-filled into <strong>Send test email</strong> so you can proof a post against your own inboxes before scheduling. These are your addresses, and they don\u2019t go through the subscribe/consent flow.</p>\n <div class=\"set-chips\" id=\"recipChips\"></div>\n <div class=\"set-recip-add\">\n <input type=\"email\" id=\"recipInput\" placeholder=\"you@example.com\" autocomplete=\"off\">\n <button type=\"button\" id=\"recipAdd\">Add inbox</button>\n </div>\n </div>\n </div>\n </section>`;\n}\n\n/** Removable chips and an add row; the list commits through the save bar. */\nexport function mountRecipients(root: HTMLElement, page: SettingsPage): SectionHandle {\n const { state } = page;\n const chips = $(\"#recipChips\", root);\n const input = $<HTMLInputElement>(\"#recipInput\", root);\n const render = () => {\n if (!state.recipients.length) {\n setHtml(\n chips,\n html`<span class=\"set-recip-empty\">No default recipients yet. Add one below.</span>`,\n );\n return;\n }\n setHtml(\n chips,\n html`${state.recipients.map(\n (addr, i) =>\n html`<span class=\"set-recip-chip\">${addr}<button type=\"button\" data-rm=\"${i}\" aria-label=\"Remove ${addr}\">${icon(\"x\")}</button></span>`,\n )}`,\n );\n };\n chips.addEventListener(\"click\", (ev) => {\n const b = ev.target instanceof Element ? ev.target.closest<HTMLElement>(\"[data-rm]\") : null;\n if (!b) {\n return;\n }\n state.recipients.splice(Number(b.dataset.rm), 1);\n render();\n page.changed();\n });\n const add = () => {\n const v = normalizeEmail(input.value);\n if (!v) {\n return;\n }\n // A refused address says why on the field, and keeps what was typed so it can be fixed\n // in place; the reason goes as soon as the field is edited (DESIGN \u00A75).\n if (!isValidEmail(v)) {\n fieldError(input, \"That doesn\u2019t look like an email address.\");\n input.focus();\n return;\n }\n if (state.recipients.includes(v)) {\n fieldError(input, \"That inbox is already in the list.\");\n input.focus();\n return;\n }\n state.recipients.push(v);\n input.value = \"\";\n render();\n page.changed();\n };\n $(\"#recipAdd\", root).onclick = add;\n input.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n add();\n }\n });\n render();\n return { reset: render };\n}\n", "// An image file made square: the largest centered square, scaled to a fixed size and\n// encoded as PNG in the browser, so a logo of any shape uploads as the square it is shown as.\n\n/** The largest centered square of a `width` \u00D7 `height` image, as a source rectangle. */\nexport function centerSquare(\n width: number,\n height: number,\n): { x: number; y: number; side: number } {\n const side = Math.min(width, height);\n return { x: Math.floor((width - side) / 2), y: Math.floor((height - side) / 2), side };\n}\n\n/** The file isn't an image this browser can decode (not an image at all, or a format it\n * doesn't read), as distinct from a browser that can't make the PNG. */\nexport class UnreadableImage extends Error {\n constructor() {\n super(\"Couldn\u2019t read that image. Pick a PNG, JPEG, WebP, or GIF.\");\n }\n}\n\n/** An image squared to `size` \u00D7 `size`: the PNG, and the source's own dimensions, so a\n * caller can say when the source was cropped. */\nexport interface SquaredImage {\n png: Blob;\n width: number;\n height: number;\n}\n\n/**\n * Decode `file`, crop its centered square, scale it to `size` \u00D7 `size`, and encode a PNG.\n * PNG keeps transparency and shows in every mail client, which WebP doesn't. An animated\n * GIF keeps its first frame. Rejects with `UnreadableImage` when the file isn't an image the\n * browser can decode, and with a plain Error when the browser can't make the PNG.\n */\nexport async function squareImage(file: Blob, size: number): Promise<SquaredImage> {\n // Upright as the photo was taken: a phone JPEG's EXIF rotation applied, said outright\n // rather than left to a default browsers have not always agreed on.\n const bitmap = await createImageBitmap(file, { imageOrientation: \"from-image\" }).catch(() => {\n throw new UnreadableImage();\n });\n try {\n const { x, y, side } = centerSquare(bitmap.width, bitmap.height);\n const canvas = document.createElement(\"canvas\");\n canvas.width = size;\n canvas.height = size;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"This browser can\u2019t prepare the image.\");\n }\n ctx.imageSmoothingQuality = \"high\";\n ctx.drawImage(bitmap, x, y, side, side, 0, 0, size, size);\n const png = await new Promise<Blob>((resolve, reject) =>\n canvas.toBlob(\n (b) => (b ? resolve(b) : reject(new Error(\"This browser can\u2019t prepare the image.\"))),\n \"image/png\",\n ),\n );\n return { png, width: bitmap.width, height: bitmap.height };\n } finally {\n bitmap.close();\n }\n}\n", "// Settings' Publication identity section (SPEC \u00A79): the name, tagline, and mailing address,\n// which commit through the save bar, and the logo, which saves the moment it changes.\n\nimport type { InUseView, LogoResponse } from \"../../shared/settings\";\nimport { api } from \"../api\";\nimport { $ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { type Html, html } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { squareImage } from \"../ui/square_image\";\nimport { busy, toast } from \"../ui/widgets\";\nimport { chip, HIDDEN, message, type SectionHandle, type SettingsPage, secHead } from \"./page\";\nimport { remakeIdentity, savedToast, withRemakeConfirm } from \"./remake\";\n\n/** The note under the card: where the identity shows up (the sample email shows which\n * fields) and, only while posts are scheduled, that a save reaches them too (SPEC \u00A79).\n * The confirmation dialog carries the rest, at the moment it matters, so the standing\n * note stays one or two short lines. */\nfunction identityNote(inUse: InUseView): string {\n if (!(inUse.identityFields || []).length) {\n return \"Your email template doesn\u2019t show any of these.\";\n }\n const shown = \"Shown in every email.\";\n const n = inUse.sends.length;\n if (!n) {\n return shown;\n }\n const reach = `Saving a change updates ${n} scheduled email${n === 1 ? \"\" : \"s\"} too; you\u2019ll confirm first.`;\n const wait = inUse.retry_after\n ? ` One sends at ${fmt(inUse.retry_after)}, so saving waits until it has sent.`\n : \"\";\n return `${shown} ${reach}${wait}`;\n}\n\n/** The side of the square PNG a logo uploads as: sharp at 3\u00D7 the largest place it shows\n * (the reader pages' 64 px), and small for any inbox. */\nconst LOGO_SIZE = 256;\n\n// The logo tile's background image is set from the DOM (applyLogoUi), not written into\n// the markup: a URL has no place inside a style attribute the tag would escape as text.\nexport function identitySection(page: SettingsPage): Html {\n const { state, fromDisplay, fromName } = page;\n // The mailing address is optional (SPEC \u00A79): with none saved it is an \"Add\" row, not an\n // empty field that reads as a missing required value.\n const addressOpen = Boolean(state.address);\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Publication identity\", chip(\"editable\", \"Editable\"))}\n <div class=\"set-card\">\n <div class=\"set-id-grid\">\n <div class=\"set-logo-slot\">\n <div class=\"set-logo-tile${state.logoUrl ? \" has-img\" : \"\"}\" id=\"logoTile\" role=\"button\" tabindex=\"0\" aria-label=\"Upload logo\">\n <span class=\"set-logo-ph\" id=\"logoPh\"${state.logoUrl ? HIDDEN : null}>${icon(\"upload\")}Upload</span>\n </div>\n <input type=\"file\" id=\"logoInput\" accept=\"image/png,image/jpeg,image/webp,image/gif\" hidden>\n <div class=\"set-logo-actions\">\n <button type=\"button\" id=\"logoReplace\">${state.logoUrl ? \"Replace\" : \"Upload\"}</button>\n <button type=\"button\" class=\"danger-subtle\" id=\"logoRemove\"${state.logoUrl ? null : HIDDEN}>Remove</button>\n </div>\n <div class=\"set-logo-notes\">\n <p class=\"field-hint\">Shown as a square. Pick an image at least ${String(LOGO_SIZE)} \u00D7 ${String(LOGO_SIZE)} px; a wider or taller one is cropped to its center square.</p>\n <p class=\"set-field-warn\" id=\"logoShape\" hidden>Your logo isn\u2019t square, so emails show it stretched. Replace it to have it cropped square.</p>\n <div class=\"field-error\" id=\"logoError\" role=\"alert\" hidden><span class=\"field-error-ico\" aria-hidden=\"true\">!</span><span></span></div>\n </div>\n </div>\n <div class=\"set-id-fields\">\n <div class=\"set-field\">\n <label for=\"setName\">Name</label>\n <input id=\"setName\" value=\"${state.name}\" placeholder=\"${fromName}\" maxlength=\"120\" autocomplete=\"off\">\n <p class=\"field-hint\">${\n fromDisplay\n ? html`Blank falls back to the email \u201CFrom\u201D name (\u201C${fromDisplay}\u201D).`\n : \"Blank leaves the publication unnamed, since the email \u201CFrom\u201D has no name.\"\n }</p>\n </div>\n <div class=\"set-field\">\n <label for=\"setTagline\">Tagline</label>\n <input id=\"setTagline\" value=\"${state.tagline}\" placeholder=\"A one-line description\" maxlength=\"200\" autocomplete=\"off\">\n <p class=\"field-hint\">A short line under the name on your public pages.</p>\n </div>\n <div class=\"set-field set-addr-add\" id=\"addrAdd\"${addressOpen ? HIDDEN : null}>\n <button type=\"button\" class=\"ghost\" id=\"addrAddBtn\" aria-describedby=\"addrAddHint\">${icon(\"plus\")}Add a mailing address</button>\n <p class=\"field-hint\" id=\"addrAddHint\">For promotional email, US law (CAN-SPAM) requires a postal address in the footer. A P.O. box works.</p>\n </div>\n <div class=\"set-field\" id=\"addrField\"${addressOpen ? null : HIDDEN}>\n <div class=\"set-field-head\">\n <label for=\"setAddress\">Mailing address</label>\n <button type=\"button\" class=\"danger-subtle set-field-remove\" id=\"addrRemove\">Remove</button>\n </div>\n <input id=\"setAddress\" value=\"${state.address}\" placeholder=\"PO Box 123, City, ST 00000\" maxlength=\"300\" autocomplete=\"off\">\n <p class=\"set-field-warn\" id=\"addrWarn\" hidden>Your email template currently doesn\u2019t include this field. If you send promotional email, add it to the template.</p>\n </div>\n </div>\n </div>\n <div class=\"set-note\">${icon(\"info\")}<span>${identityNote(page.data.inUse)}</span></div>\n </div>\n </section>`;\n}\n\nexport function mountIdentity(\n root: HTMLElement,\n page: SettingsPage,\n signal: AbortSignal,\n): SectionHandle {\n const { state } = page;\n const nameEl = $<HTMLInputElement>(\"#setName\", root);\n const taglineEl = $<HTMLInputElement>(\"#setTagline\", root);\n const addressEl = $<HTMLInputElement>(\"#setAddress\", root);\n\n // --- live fields: the state follows every keystroke, and the page repaints what shows it.\n const onInput = () => {\n state.name = nameEl.value.trim();\n state.tagline = taglineEl.value.trim();\n state.address = addressEl.value.trim();\n page.changed();\n };\n nameEl.addEventListener(\"input\", onInput);\n taglineEl.addEventListener(\"input\", onInput);\n addressEl.addEventListener(\"input\", onInput);\n\n // --- the optional address: the Add row or the open field, and the field's warning,\n // shown while an address is set and the template leaves it out.\n let addressOpen = Boolean(state.address);\n const templatePrintsAddress = (page.data.inUse.identityFields || []).includes(\"address\");\n const addrAdd = $(\"#addrAdd\", root);\n const addrField = $(\"#addrField\", root);\n const addrWarn = $(\"#addrWarn\", root);\n const applyAddressUi = () => {\n addrAdd.hidden = addressOpen;\n addrField.hidden = !addressOpen;\n addrWarn.hidden = !(addressOpen && state.address && !templatePrintsAddress);\n // Described by the warning only while it shows: a hidden element named by\n // aria-describedby is still read out.\n if (addrWarn.hidden) {\n addressEl.removeAttribute(\"aria-describedby\");\n } else {\n addressEl.setAttribute(\"aria-describedby\", \"addrWarn\");\n }\n };\n addressEl.addEventListener(\"input\", applyAddressUi);\n $(\"#addrAddBtn\", root).onclick = () => {\n addressOpen = true;\n applyAddressUi();\n addressEl.focus();\n };\n $(\"#addrRemove\", root).onclick = () => {\n addressEl.value = \"\";\n addressOpen = false;\n onInput();\n applyAddressUi();\n $(\"#addrAddBtn\", root).focus();\n };\n\n // --- logo: immediate upload / remove (their own endpoints), updated in place.\n const logoInput = $<HTMLInputElement>(\"#logoInput\", root);\n const logoTile = $(\"#logoTile\", root);\n const logoPh = $(\"#logoPh\", root);\n const logoReplace = $<HTMLButtonElement>(\"#logoReplace\", root);\n const logoRemove = $<HTMLButtonElement>(\"#logoRemove\", root);\n // A logo uploaded before the crop step may not be square, and emails show it stretched\n // to one. Said under the tile until it is replaced; the image is read as any <img> is.\n const logoShape = $(\"#logoShape\", root);\n const checkLogoShape = (url: string) => {\n logoShape.hidden = true;\n if (!url) {\n return;\n }\n const img = new Image();\n img.onload = () => {\n logoShape.hidden = state.logoUrl !== url || img.naturalWidth === img.naturalHeight;\n };\n img.src = url;\n };\n const applyLogoUi = () => {\n const has = !!state.logoUrl;\n checkLogoShape(state.logoUrl);\n logoTile.classList.toggle(\"has-img\", has);\n logoTile.style.backgroundImage = has ? `url('${state.logoUrl}')` : \"\";\n logoPh.hidden = has;\n logoRemove.hidden = !has;\n logoReplace.textContent = has ? \"Replace\" : \"Upload\";\n };\n logoReplace.onclick = () => logoInput.click();\n logoTile.addEventListener(\"click\", () => logoInput.click());\n logoTile.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n logoInput.click();\n }\n });\n // A refused logo change (a scheduled send about to fire) is said beneath the tile,\n // field validation's home (DESIGN \u00A75): the logo has no save bar state of its own.\n const logoError = $(\"#logoError\", root);\n const logoErrorText = $(\"span:last-of-type\", logoError);\n const showLogoError = (msg: string) => {\n logoErrorText.textContent = msg;\n logoError.hidden = !msg;\n };\n // A logo change lands at once: the page adopts it, and every preview that shows the\n // logo repaints rather than waiting for the next identity edit.\n const logoSaved = (r: LogoResponse, done: string) => {\n state.logoUrl = r.settings.publication.logoUrl || \"\";\n page.applySaved(r.settings);\n applyLogoUi();\n page.changed();\n toast(savedToast(done, r.remade));\n };\n // The logo has no save bar, so whatever kept it from saving is said under its tile.\n const logoFailed = (err: unknown) => showLogoError(message(err));\n logoInput.onchange = async () => {\n const file = logoInput.files?.[0];\n logoInput.value = \"\";\n if (!file) {\n return;\n }\n showLogoError(\"\");\n try {\n // Squared before it uploads: every surface shows the logo square, and an email\n // stretches one that isn't. The tile shows the result; Replace is the way back.\n const squared = await squareImage(file, LOGO_SIZE);\n const cropped = squared.width !== squared.height;\n // The logo is part of the identity, so an upload that reaches scheduled emails\n // asks first (withRemakeConfirm); the file is held and re-sent with the ids.\n const r = await withRemakeConfirm(\n (ack) => {\n const fd = new FormData();\n fd.append(\"file\", new File([squared.png], \"logo.png\", { type: \"image/png\" }));\n const q = ack ? `?remake=${encodeURIComponent(ack.join(\",\"))}` : \"\";\n return api<LogoResponse>(`/api/settings/logo${q}`, { method: \"POST\", body: fd });\n },\n remakeIdentity(\"Upload\", \"Uploading\"),\n signal,\n );\n if (r) {\n logoSaved(r, cropped ? \"Logo updated, cropped to its center square\" : \"Logo updated\");\n }\n } catch (err) {\n logoFailed(err);\n }\n };\n logoRemove.onclick = () =>\n busy(logoRemove, \"Removing\u2026\", async () => {\n showLogoError(\"\");\n try {\n const r = await withRemakeConfirm(\n (ack) => {\n const q = ack ? `?remake=${encodeURIComponent(ack.join(\",\"))}` : \"\";\n return api<LogoResponse>(`/api/settings/logo${q}`, { method: \"DELETE\" });\n },\n remakeIdentity(\"Remove\", \"Removing\"),\n signal,\n );\n if (r) {\n logoSaved(r, \"Logo removed\");\n }\n } catch (err) {\n logoFailed(err);\n }\n });\n\n applyLogoUi();\n applyAddressUi();\n return {\n reset() {\n nameEl.value = state.name;\n taglineEl.value = state.tagline;\n addressEl.value = state.address;\n addressOpen = Boolean(state.address);\n applyAddressUi();\n },\n };\n}\n", "// Settings' Notifications section (SPEC \u00A78): where Kestrel tells the publisher about their\n// sends is a preference, committed through the save bar; how it gets there, and from whom,\n// is deploy config, shown read-only beside it with how the last one went.\n\nimport type {\n NotificationStatusKind,\n NotificationTestResponse,\n SettingsResponse,\n} from \"../../shared/settings\";\nimport { api } from \"../api\";\nimport { $ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { icon } from \"../ui/icons\";\nimport { busy, toast } from \"../ui/widgets\";\nimport { providerLabel } from \"./deploy\";\nimport { chip, message, type SectionHandle, type SettingsPage, secHead } from \"./page\";\n\n/** How the deploy-time notification channel reads to the publisher. */\nfunction notifyChannelLabel(\n channel: SettingsResponse[\"deployment\"][\"notifyChannel\"],\n provider: string,\n): string {\n switch (channel) {\n case \"cloudflare\":\n return \"Cloudflare Email, separate from your newsletter\u2019s provider\";\n case \"provider\":\n return provider === \"none\"\n ? \"Your email provider, once one is connected\"\n : `Your email provider (${providerLabel(provider)})`;\n default:\n return \"Local stand-in: nothing is delivered\";\n }\n}\n\nconst NOTIFICATION_KINDS: Record<NotificationStatusKind, string> = {\n finished: \"went out\",\n refused: \"provider refusing\",\n stuck: \"in flight too long\",\n wedged: \"waiting to be resolved\",\n missed: \"missed fire time\",\n import: \"list imported\",\n test: \"test\",\n};\n\n/** What a status line is about: the send and what happened, or just \"test notification\". */\nfunction aboutNotification(n: { kind: NotificationStatusKind; subject: string }): string {\n return n.kind === \"test\" ? \"test notification\" : `${n.subject} (${NOTIFICATION_KINDS[n.kind]})`;\n}\n\n/** The last delivered notification, and a newer failure when the channel has stopped working. */\nfunction notificationStatusHtml(status: SettingsResponse[\"notificationStatus\"]): Html {\n const { lastSent, lastFailure } = status;\n if (lastFailure) {\n return html`<span class=\"set-pill danger\">Not delivered</span> <span class=\"muted\">${fmt(lastFailure.at)}, ${aboutNotification(lastFailure)}: ${lastFailure.error}</span>`;\n }\n if (lastSent) {\n return html`<span class=\"set-pill ok\">${icon(\"check\")}Delivered</span> <span class=\"muted\">${fmt(lastSent.at)}, ${aboutNotification(lastSent)}</span>`;\n }\n return html`<span class=\"muted\">None yet</span>`;\n}\n\nexport function notificationsSection(page: SettingsPage): Html {\n const d = page.data.deployment;\n return html`\n <section class=\"set-sec\">\n ${secHead(\"Notifications\", chip(\"editable\", \"Editable\"))}\n <p class=\"set-lede\">Kestrel emails you when a send goes out, and right away if a send runs into a problem.</p>\n <div class=\"set-card\">\n <div class=\"set-recip\">\n <div class=\"set-field\" style=\"margin:0\">\n <label for=\"notifyTo\">Send notifications to</label>\n <input type=\"email\" id=\"notifyTo\" value=\"${page.state.notifyTo}\" placeholder=\"you@example.com\" autocomplete=\"off\">\n <p class=\"field-hint\">One address. Leave blank for no notifications.</p>\n </div>\n <div class=\"row\"><button type=\"button\" class=\"ghost\" id=\"notifyTest\">Send a test notification</button><span class=\"field-hint\" id=\"notifyTestHint\"></span></div>\n </div>\n <div class=\"set-kv\" style=\"border-top:1px solid var(--line)\">\n <div class=\"set-kv-k\">Sent through</div><div class=\"set-kv-v\">${notifyChannelLabel(d.notifyChannel, d.provider)}</div>\n <div class=\"set-kv-k\">From address</div><div class=\"set-kv-v\"><span class=\"mono\">${d.notifyFrom}</span></div>\n <div class=\"set-kv-k\">Last notification</div><div class=\"set-kv-v\" id=\"notifyStatus\">${notificationStatusHtml(page.data.notificationStatus)}</div>\n </div>\n <div class=\"set-note\">${icon(\"readonly\")}<span>The channel and its sender are set at deploy. ${d.notifyChannel === \"provider\" ? \"Notifications go through your newsletter\u2019s own provider, so one about the provider refusing your account cannot reach you; Cloudflare\u2019s email avoids that. \" : \"\"}See the <a class=\"set-link\" href=\"#/docs/notifications\">setup guide</a>.</span></div>\n </div>\n </section>`;\n}\n\n/** The address rides the save bar; the test goes to the SAVED address only (the server\n * never takes one from the request), so it waits for a save. Its outcome is the channel's\n * latest word, so the status line takes it in place. */\nexport function mountNotifications(root: HTMLElement, page: SettingsPage): SectionHandle {\n const toEl = $<HTMLInputElement>(\"#notifyTo\", root);\n const test = $<HTMLButtonElement>(\"#notifyTest\", root);\n const testHint = $(\"#notifyTestHint\", root);\n const status = $(\"#notifyStatus\", root);\n toEl.addEventListener(\"input\", () => {\n page.state.notifyTo = toEl.value.trim().toLowerCase();\n page.changed();\n });\n const refreshTest = () => {\n const saved = page.saved().notifyTo;\n const reason = !saved\n ? \"Save an address to send a test.\"\n : page.state.notifyTo !== saved\n ? \"Save first: the test goes to the saved address.\"\n : \"\";\n test.disabled = Boolean(reason);\n testHint.textContent = reason;\n };\n test.onclick = () =>\n busy(test, \"Sending\u2026\", async () => {\n try {\n const r = await api<NotificationTestResponse>(\"/api/settings/notifications/test\", {\n method: \"POST\",\n });\n toast(\n r.channel === \"fake\"\n ? `Test notification recorded for ${r.to} (local stand-in: nothing is delivered)`\n : `Test notification sent to ${r.to}`,\n );\n setHtml(\n status,\n notificationStatusHtml({\n lastSent: { kind: \"test\", subject: \"\", at: Date.now() },\n lastFailure: null,\n }),\n );\n } catch (err) {\n // The outcome's home is the channel's status row, as it is for every notification;\n // a toast would say it a second time.\n setHtml(\n status,\n notificationStatusHtml({\n lastSent: null,\n lastFailure: { kind: \"test\", subject: \"\", at: Date.now(), error: message(err) },\n }),\n );\n }\n });\n refreshTest();\n return {\n repaint: refreshTest,\n reset() {\n toEl.value = page.state.notifyTo;\n refreshTest();\n },\n };\n}\n", "// The pages a reader can land on, grouped by the journey that reaches them (SPEC \u00A75, \u00A77),\n// as Settings' Reader pages section lists them and `GET /api/reader-pages/:journey/:state`\n// renders them. One list for both ends, so the editor can't offer a page the Worker\n// doesn't render.\n\n/** Where a reader meets a page: under the app's origin, or under the archive's (origin\n * and base path), and the rest of the address. Shown to the publisher, never fetched. */\nexport interface ReaderPageAddress {\n base: \"app\" | \"archive\";\n path: string;\n}\n\n/** One page or state a reader can land on. `offPath` marks one a reader reaches only when\n * something goes wrong or there is nothing yet (an expired link, a publication that has\n * sent no post). */\nexport interface ReaderPageState {\n id: string;\n label: string;\n offPath?: true;\n at: ReaderPageAddress;\n}\n\nconst SUBSCRIBE: ReaderPageAddress = { base: \"app\", path: \"/subscribe\" };\nconst CONFIRM: ReaderPageAddress = { base: \"app\", path: \"/confirm?token=\u2026\" };\nconst UNSUBSCRIBE: ReaderPageAddress = { base: \"app\", path: \"/unsubscribe?token=\u2026\" };\n\n/** Every journey a reader takes, in the order they take them, with each page and state\n * it can land on. A state's `id` is its path segment; `label` is how Settings names it.\n * A journey's `flow` says how its main-path pages relate: `steps` a reader takes in\n * order, or `peers` a reader moves between. */\nexport const READER_JOURNEYS = [\n {\n id: \"subscribe\",\n label: \"Subscribe\",\n flow: \"steps\",\n states: [\n { id: \"form\", label: \"Form\", at: SUBSCRIBE },\n { id: \"check-inbox\", label: \"Check your inbox\", at: SUBSCRIBE },\n { id: \"not-an-email\", label: \"Not an email\", offPath: true, at: SUBSCRIBE },\n ],\n },\n {\n id: \"confirm\",\n label: \"Confirm\",\n flow: \"steps\",\n states: [\n { id: \"ready\", label: \"Ready\", at: CONFIRM },\n { id: \"confirmed\", label: \"Confirmed\", at: CONFIRM },\n { id: \"expired\", label: \"Expired\", offPath: true, at: CONFIRM },\n { id: \"invalid\", label: \"Invalid link\", offPath: true, at: CONFIRM },\n ],\n },\n {\n id: \"unsubscribe\",\n label: \"Unsubscribe\",\n flow: \"steps\",\n states: [\n { id: \"ask\", label: \"Ask\", at: UNSUBSCRIBE },\n { id: \"done\", label: \"Unsubscribed\", at: UNSUBSCRIBE },\n { id: \"invalid\", label: \"Invalid link\", offPath: true, at: UNSUBSCRIBE },\n ],\n },\n {\n id: \"reading\",\n label: \"Reading\",\n flow: \"peers\",\n states: [\n { id: \"landing\", label: \"Landing page\", at: { base: \"app\", path: \"/\" } },\n { id: \"archive\", label: \"Archive\", at: { base: \"archive\", path: \"\" } },\n // The sample post's slug, as the Worker's preview renders it.\n { id: \"post\", label: \"Post page\", at: { base: \"archive\", path: \"/sample-post\" } },\n // A publication with nothing sent yet: what its first visitors get.\n {\n id: \"landing-empty\",\n label: \"Landing, no posts yet\",\n offPath: true,\n at: { base: \"app\", path: \"/\" },\n },\n {\n id: \"archive-empty\",\n label: \"Archive, no posts yet\",\n offPath: true,\n at: { base: \"archive\", path: \"\" },\n },\n {\n id: \"not-found\",\n label: \"Nothing here yet\",\n offPath: true,\n at: { base: \"archive\", path: \"/\u2026\" },\n },\n ],\n },\n] as const satisfies readonly {\n id: string;\n label: string;\n flow: \"steps\" | \"peers\";\n states: readonly ReaderPageState[];\n}[];\n\nexport type ReaderJourney = (typeof READER_JOURNEYS)[number];\nexport type ReaderJourneyId = ReaderJourney[\"id\"];\n\n/** The path of one page's preview. */\nexport function readerPagePath(journey: string, state: string): string {\n return `/api/reader-pages/${encodeURIComponent(journey)}/${encodeURIComponent(state)}`;\n}\n\n/** The journey and state a pair of path segments names, or null for one the list lacks. */\nexport function findReaderPage(\n journey: string,\n state: string,\n): { journey: ReaderJourney; state: ReaderPageState } | null {\n const j = READER_JOURNEYS.find((x) => x.id === journey);\n const s = j?.states.find((x) => x.id === state);\n return j && s ? { journey: j, state: s } : null;\n}\n", "// Settings' Reader pages section: every page a reader can land on (SPEC \u00A75), as an outline\n// beside one framed page. The journeys are the outline's headings and never controls, and\n// the pages are its rows, so the two levels can't be mistaken for each other and every\n// page is in view, the hard-to-reach ones included. A narrow card folds the outline into\n// one grouped select. The journeys and their pages are shared/'s list; each page is\n// `GET /api/reader-pages/:journey/:state`, exactly as a reader gets it, framed so nothing\n// on it acts.\n\nimport {\n READER_JOURNEYS,\n type ReaderJourney,\n type ReaderJourneyId,\n type ReaderPageAddress,\n type ReaderPageState,\n readerPagePath,\n} from \"../../shared/reader_pages\";\nimport type { DeploymentView } from \"../../shared/settings\";\nimport { apiText, describeError } from \"../api\";\nimport { onAbort } from \"../lifecycle\";\nimport { $, $$ } from \"../ui/dom\";\nimport { framedCopy } from \"../ui/email_frame\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { type IconName, icon } from \"../ui/icons\";\n\n/** What on a journey's pages follows the publisher, what they word, and what is fixed. */\nconst JOURNEY_NOTES: Record<ReaderJourneyId, Html> = {\n subscribe: html`Your name, tagline, and logo head each page; the words are Kestrel\u2019s. The embeddable form in Ways to subscribe posts to these pages too.`,\n confirm: html`Your identity heads each page. You word the email that links here, under Confirmation email; these pages\u2019 words are fixed, so what a reader agrees to can\u2019t be edited away.`,\n unsubscribe: html`Your identity heads each page; the words are fixed, so leaving always reads the same. A mail client\u2019s own Unsubscribe button leaves without a page.`,\n reading: html`Your identity heads each page, and a post page is your <a class=\"set-link\" href=\"#/template\">email template</a> around the post, as its web version shows it. Where a page lists posts, they are samples.`,\n};\n\nconst JOURNEY_ICONS: Record<ReaderJourneyId, IconName> = {\n subscribe: \"mail\",\n confirm: \"circle-check\",\n unsubscribe: \"circle-minus\",\n reading: \"book\",\n};\n\n/** The width a preview is framed at: the page as a reader's window shows it, or a phone. */\ntype PreviewWidth = \"desktop\" | \"phone\";\n\n/** A peer's mark: a dot drawn in CSS rather than a \"\u00B7\" glyph, which sits wherever the font\n * puts it, so the circle's own centering places it exactly. */\nconst PEER_DOT = html`<span class=\"set-rp-dot\"></span>`;\n\n/** One row of the outline: a step's number, a peer's dot, or an off-path page's branch. */\nfunction outlineRow(j: ReaderJourney, s: ReaderPageState, mark: Html | string): Html {\n return html`<li><button type=\"button\" class=\"set-rp-item${s.offPath ? \" off\" : \"\"}\" data-journey=\"${j.id}\" data-state=\"${s.id}\" aria-current=\"false\"><span class=\"set-rp-mark\" aria-hidden=\"true\">${mark}</span>${s.label}</button></li>`;\n}\n\n/** The outline: each journey a heading over its main-path pages, then the ones off it.\n * A thread joins a journey's steps; peers carry a dot, not a number. The off-path pages\n * are a list of their own, so the thread ends at the last step and never runs behind\n * a page a reader reaches only when something goes wrong. */\nfunction outline(): Html {\n return html`${READER_JOURNEYS.map((j) => {\n const states: readonly ReaderPageState[] = j.states;\n const steps = j.flow === \"steps\";\n return html`<div class=\"set-rp-journey\" data-journey=\"${j.id}\">\n <h3 class=\"set-rp-jhead\" id=\"rpJourney-${j.id}\">${icon(JOURNEY_ICONS[j.id])}${j.label}</h3>\n <ul class=\"set-rp-list${steps ? \" steps\" : \"\"}\" aria-labelledby=\"rpJourney-${j.id}\">\n ${states.filter((s) => !s.offPath).map((s, i) => outlineRow(j, s, steps ? String(i + 1) : PEER_DOT))}\n </ul>\n <ul class=\"set-rp-list\" aria-label=\"${j.label}, off the main path\">\n ${states.filter((s) => s.offPath).map((s) => outlineRow(j, s, icon(\"branch\")))}\n </ul>\n </div>`;\n })}`;\n}\n\n/** The outline as one select, for a card too narrow for it: a group per journey, its\n * steps numbered as the outline numbers them. An off-path page carries no mark: a branch\n * glyph alone in an option's text reads as a stray character, not as \"off the path\",\n * and these pages' names already say what went wrong. */\nfunction picker(): Html {\n return html`<select id=\"rpSelect\" aria-label=\"Reader page\">${READER_JOURNEYS.map((j) => {\n const states: readonly ReaderPageState[] = j.states;\n let step = 0;\n return html`<optgroup label=\"${j.label}\">${states.map((s) => {\n const n = j.flow === \"steps\" && !s.offPath ? `${++step}. ` : \"\";\n return html`<option value=\"${j.id}/${s.id}\">${n}${s.label}</option>`;\n })}</optgroup>`;\n })}</select>`;\n}\n\n/** The section's markup, under the head the page builds for every section. */\nexport function readerPagesSection(head: Html): Html {\n return html`\n <section class=\"set-sec set-rp-sec\" id=\"readerPages\">\n ${head}\n <p class=\"set-lede\">Every page a reader can land on, exactly as they see it, with your publication\u2019s identity and sample values. Nothing here changes a page; it shows the ones that are hard to reach by hand.</p>\n <div class=\"set-card set-rp\">\n <nav class=\"set-rp-nav\" aria-label=\"Reader pages\">${outline()}</nav>\n <div class=\"set-rp-pick\">${picker()}</div>\n <div class=\"set-rp-main\">\n <div class=\"set-rp-top\">\n <div>\n <p class=\"set-rp-crumb\"><span class=\"set-rp-crumb-j\" id=\"rpJourney\"></span><span class=\"set-rp-crumb-sep\">/</span><span id=\"rpState\"></span></p>\n <p class=\"set-rp-note\" id=\"rpNote\"></p>\n </div>\n <span class=\"set-wtog\" id=\"rpWidth\" role=\"group\" aria-label=\"Preview width\">\n <button type=\"button\" class=\"wtog-btn\" data-width=\"desktop\" aria-pressed=\"true\">${icon(\"desktop\")}Desktop</button>\n <button type=\"button\" class=\"wtog-btn\" data-width=\"phone\" aria-pressed=\"false\">${icon(\"phone\")}Phone</button>\n </span>\n </div>\n <div class=\"set-rp-at\"><span class=\"mono\" id=\"rpAt\"></span><span>Sample \u00B7 links and forms do nothing</span></div>\n <p class=\"set-rp-error\" id=\"rpError\" role=\"alert\" hidden></p>\n <div class=\"set-rp-stage\" id=\"rpStage\">\n <iframe class=\"set-rp-frame\" id=\"rpFrame\" sandbox=\"allow-same-origin\" title=\"Reader page preview\" scrolling=\"no\"></iframe>\n </div>\n </div>\n </div>\n </section>`;\n}\n\n/** Where a reader meets a page on this deployment, as the address line shows it: without\n * its scheme, which says nothing to a publisher. */\nexport function readerPageAddress(at: ReaderPageAddress, d: DeploymentView): string {\n const base = at.base === \"app\" ? d.appOrigin : `${d.archiveOrigin}${d.archiveBasePath}`;\n return `${base}${at.path}`.replace(/^https?:\\/\\//, \"\");\n}\n\n/** The section's controls, once its markup is in the document. `refresh` re-reads the\n * page shown, for after any save, since one may have changed the identity it wears. */\nexport function mountReaderPages(\n root: HTMLElement,\n deployment: DeploymentView,\n signal: AbortSignal,\n): { refresh(): void } {\n const nav = $(\".set-rp-nav\", root);\n const select = $<HTMLSelectElement>(\"#rpSelect\", root);\n const widthEl = $(\"#rpWidth\", root);\n const stage = $(\"#rpStage\", root);\n const frame = $<HTMLIFrameElement>(\"#rpFrame\", root);\n const errorEl = $(\"#rpError\", root);\n\n let journey: ReaderJourney = READER_JOURNEYS[0];\n let state: ReaderPageState = journey.states[0];\n let width: PreviewWidth = \"desktop\";\n // Only the latest read paints: a slow one answered after a later click is dropped.\n let seq = 0;\n let watch: ResizeObserver | null = null;\n onAbort(signal, () => watch?.disconnect());\n\n const paintChoice = () => {\n for (const b of $$<HTMLButtonElement>(\".set-rp-item\", nav)) {\n const on = b.dataset.journey === journey.id && b.dataset.state === state.id;\n b.setAttribute(\"aria-current\", on ? \"true\" : \"false\");\n }\n for (const g of $$(\".set-rp-journey\", nav)) {\n g.classList.toggle(\"current\", g.dataset.journey === journey.id);\n }\n select.value = `${journey.id}/${state.id}`;\n $(\"#rpJourney\", root).textContent = journey.label;\n $(\"#rpState\", root).textContent = state.label;\n setHtml($(\"#rpNote\", root), JOURNEY_NOTES[journey.id]);\n $(\"#rpAt\", root).textContent = readerPageAddress(state.at, deployment);\n };\n\n const fitHeight = () => {\n const h = frame.contentDocument?.body?.scrollHeight;\n if (h) {\n frame.style.height = `${h}px`;\n }\n };\n\n const load = async () => {\n const mine = ++seq;\n try {\n const doc = await apiText(readerPagePath(journey.id, state.id), { signal });\n if (mine !== seq || signal.aborted) {\n return;\n }\n errorEl.hidden = true;\n stage.hidden = false;\n // Under the policy a page that submits no form carries, so beside the sandbox no\n // form on it submits (ui/email_frame.ts).\n frame.srcdoc = framedCopy(doc);\n } catch (e) {\n if (mine !== seq || signal.aborted) {\n return;\n }\n // The page named above is not the one the frame last held, so the frame empties\n // rather than show it under the wrong name, and the error takes its place.\n frame.srcdoc = \"\";\n stage.hidden = true;\n errorEl.textContent = `Couldn\u2019t load this page: ${describeError(e)}`;\n errorEl.hidden = false;\n }\n };\n\n const choose = (journeyId: string | undefined, stateId: string | undefined) => {\n const j = READER_JOURNEYS.find((x) => x.id === journeyId);\n const states: readonly ReaderPageState[] = j?.states ?? [];\n const s = states.find((x) => x.id === stateId);\n if (!j || !s || (j === journey && s === state)) {\n return;\n }\n journey = j;\n state = s;\n paintChoice();\n void load();\n };\n\n frame.onload = () => {\n const page = frame.contentDocument;\n if (!page || signal.aborted) {\n return;\n }\n // A preview shows a page; following a link would leave it for the real thing.\n page.addEventListener(\"click\", (e) => {\n const target = e.target as Element | null;\n if (target?.closest?.(\"a[href], area[href]\")) {\n e.preventDefault();\n }\n });\n // As tall as the page, so Settings scrolls it, and kept fitted as fonts, images,\n // and the width change it.\n watch?.disconnect();\n fitHeight();\n if (page.body && typeof ResizeObserver !== \"undefined\") {\n watch = new ResizeObserver(fitHeight);\n watch.observe(page.body);\n }\n };\n\n nav.addEventListener(\"click\", (e) => {\n const b = (e.target as Element).closest<HTMLButtonElement>(\".set-rp-item\");\n choose(b?.dataset.journey, b?.dataset.state);\n });\n select.addEventListener(\"change\", () => {\n const [j, s] = select.value.split(\"/\");\n choose(j, s);\n });\n widthEl.addEventListener(\"click\", (e) => {\n const b = (e.target as Element).closest<HTMLButtonElement>(\"button[data-width]\");\n const w = b?.dataset.width;\n if ((w !== \"desktop\" && w !== \"phone\") || w === width) {\n return;\n }\n width = w;\n for (const o of $$<HTMLButtonElement>(\"button\", widthEl)) {\n o.setAttribute(\"aria-pressed\", String(o.dataset.width === w));\n }\n stage.classList.toggle(\"phone\", w === \"phone\");\n });\n\n paintChoice();\n void load();\n return { refresh: () => void load() };\n}\n", "// Settings' Ways to subscribe section: the public subscribe page's address and a form to\n// paste into the publisher's own site. Both post to the public /subscribe and start the\n// double opt-in, never an auto-confirm (I1).\n\nimport { connectProviderLink } from \"../deployment\";\nimport { copyButton, copyFace, wireCopy } from \"../ui/copy\";\nimport { $, $$ } from \"../ui/dom\";\nimport { escapeHtml, type Html, html, setHtml } from \"../ui/html\";\nimport { type IconName, icon } from \"../ui/icons\";\nimport { chip, type SectionHandle, type SettingsPage, secHead } from \"./page\";\n\ntype EmbedMode = \"plain\" | \"styled\";\n\n/** The embed's heading: \"Subscribe to <name>\", or \"Subscribe\" with no name (SPEC \u00A79). */\nfunction embedHeading(name: string): string {\n return name ? `Subscribe to ${name}` : \"Subscribe\";\n}\n\n/** The snippet a publisher pastes into their own page. It is text here (shown in a code\n * block and copied), escaped for the page it will land in. */\nexport function buildEmbed(mode: EmbedMode, name: string, appOrigin: string): string {\n const heading = escapeHtml(embedHeading(name));\n const action = `${escapeHtml(appOrigin)}/subscribe`;\n if (mode === \"styled\") {\n // The hosted subscribe page's look, carried inline: a serif headline (Georgia, the\n // reader pages' own fallback, so it needs no web font) over a sans body, in the reader\n // pages' ink and muted colors. The headline inherits its color from the host page, and\n // the field's border is a step darker than the reader pages' line so it still shows on\n // a host page of any background. The apostrophe is plain ASCII, so the snippet reads\n // right whatever encoding the host page declares.\n return (\n `<form action=\"${action}\" method=\"post\" style=\"max-width:460px;font:16px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif\">\\n` +\n ` <div style=\"font:600 22px/1.2 Georgia,'Times New Roman',serif;letter-spacing:-0.01em;margin-bottom:12px\">${heading}</div>\\n` +\n ` <div style=\"display:flex;gap:10px;flex-wrap:wrap\">\\n` +\n ` <input type=\"email\" name=\"email\" required placeholder=\"you@example.com\" aria-label=\"Email address\" style=\"flex:1 1 220px;padding:11px 13px;border:1px solid #d6d3cc;border-radius:9px;background:#fff;color:#1b1a17;font:inherit\">\\n` +\n ` <button type=\"submit\" style=\"padding:11px 20px;border:1px solid #1b1a17;border-radius:9px;background:#1b1a17;color:#fbfbfa;font:inherit;font-size:15px;font-weight:600;cursor:pointer;white-space:nowrap\">Subscribe</button>\\n` +\n ` </div>\\n` +\n ` <p style=\"margin:12px 0 0;font-size:12.5px;color:#6b6a63\">We\u2019ll email you a link to confirm. Unsubscribe anytime.</p>\\n` +\n `</form>`\n );\n }\n return (\n `<form action=\"${action}\" method=\"post\">\\n` +\n ` <label>\\n` +\n ` ${heading}\\n` +\n ` <input type=\"email\" name=\"email\" placeholder=\"you@example.com\" required>\\n` +\n ` </label>\\n` +\n ` <button type=\"submit\">Subscribe</button>\\n` +\n `</form>`\n );\n}\n\n/** The snippet as it will look on the publisher's site: a mockup, inert. The styled form\n * wears the hosted page's look; the plain one is laid out as its markup renders, with\n * the browser's own controls, since bare markup gets the site's styles or none. */\nfunction embedPreview(mode: EmbedMode, name: string): Html {\n const heading = embedHeading(name);\n if (mode === \"styled\") {\n return html`<form class=\"set-pf-styled\"><span class=\"l\">${heading}</span><div class=\"rowf\"><input type=\"email\" placeholder=\"you@example.com\" disabled><button type=\"button\" class=\"sub-btn\" tabindex=\"-1\">Subscribe</button></div><p class=\"set-pf-fine\">We\u2019ll email you a link to confirm. Unsubscribe anytime.</p></form>`;\n }\n return html`<form class=\"set-pf-plain\"><label>${heading} <input type=\"email\" placeholder=\"you@example.com\" disabled></label> <button type=\"button\" tabindex=\"-1\">Subscribe</button></form>`;\n}\n\nfunction appOriginOf(page: SettingsPage): string {\n return page.data.deployment.appOrigin || location.origin;\n}\n\n/** The page's line names the publication only when it has a name, since the page then\n * reads \"Subscribe\" (SPEC \u00A79). */\nfunction pageLine(name: string): string {\n return name\n ? \"A public page with your publication\u2019s name and a sign-up form ready to go.\"\n : \"A public page with a sign-up form ready to go.\";\n}\n\n/** One way to subscribe, a band of the section's card: its glyph and title, its line,\n * and any control at the head's right end, then what it offers. The two ways share\n * this head so they read as parallel options. */\nfunction way(\n glyph: IconName,\n title: string,\n line: Html,\n body: Html,\n control: Html | null = null,\n): Html {\n return html`<div class=\"set-way\"><div class=\"set-way-head\"><div class=\"set-way-name\"><h3 class=\"set-way-title\">${icon(glyph)}${title}</h3><p class=\"set-way-line\">${line}</p></div>${control}</div>${body}</div>`;\n}\n\nexport function subscribeSection(page: SettingsPage): Html {\n const subscribeUrl = `${appOriginOf(page)}/subscribe`;\n return html`\n <section class=\"set-sec set-sec-subscribe\">\n ${secHead(\"Ways to subscribe\", chip(\"copyout\", \"Copy-out\"))}\n <p class=\"set-lede\">Two ways for readers to join your list. Either way, a reader is added only once they confirm by email.</p>\n ${\n // Signups are closed while no provider is connected (SPEC \u00A79): the page says so and\n // a form takes no address. The address and the code stay, ready for when they open.\n page.data.deployment.provider === \"none\"\n ? html`<div class=\"health amber set-sub-not-connected\" role=\"status\"><span class=\"health-dot\">\u26A0\uFE0F</span><div><span>No email provider connected, so signups aren\u2019t open yet: the page below says so, and the form takes no address. ${connectProviderLink()} to open signups.</span></div></div>`\n : null\n }\n <div class=\"set-card\">\n ${way(\n \"link\",\n \"Share the page\",\n html`<span id=\"subscribePageLine\"></span>`,\n html`<div class=\"set-way-body set-way-addr\"><code class=\"set-addr\">${subscribeUrl}</code>${copyButton(subscribeUrl, \"Copy the subscribe page\u2019s address\")}<a class=\"ghost-link\" href=\"${subscribeUrl}\" target=\"_blank\" rel=\"noopener\">Open \u2197</a></div>`,\n )}\n ${way(\n \"code\",\n \"Embed a form\",\n html`Put a sign-up form on another site you own.`,\n html`<div class=\"set-embed\">\n <div class=\"set-embed-preview\" id=\"embedPreview\"></div>\n <pre class=\"set-embed-code\"><code id=\"embedCode\"></code></pre>\n </div>\n <div class=\"set-embed-foot\" id=\"embedCopy\"></div>`,\n html`<span class=\"set-wtog\" role=\"group\" aria-label=\"Snippet style\">\n <button type=\"button\" class=\"wtog-btn\" data-embed=\"styled\" aria-pressed=\"true\">Styled</button>\n <button type=\"button\" class=\"wtog-btn\" data-embed=\"plain\" aria-pressed=\"false\">Plain</button>\n </span>`,\n )}\n </div>\n </section>`;\n}\n\n/** The embed's one act, copying the snippet, as a Secondary button rather than the quiet\n * Copy a code block wears, since here copying is what the band is for (DESIGN \u00A77). */\nfunction embedCopy(mode: EmbedMode, code: string): Html {\n const variant = mode === \"styled\" ? \"Styled\" : \"Plain\";\n return html`<button type=\"button\" class=\"secondary set-embed-copy\" data-copy=\"${code}\" data-copy-word=\"Copy HTML\" aria-label=\"Copy HTML, ${variant} form\">${copyFace(false, \"Copy HTML\")}</button>`;\n}\n\n/** The Plain/Styled toggle drives the code, the rendered preview, and the Copy payload;\n * the name follows the live identity field. */\nexport function mountSubscribe(root: HTMLElement, page: SettingsPage): SectionHandle {\n const appOrigin = appOriginOf(page);\n const codeEl = $(\"#embedCode\", root);\n const copyEl = $(\"#embedCopy\", root);\n const previewEl = $(\"#embedPreview\", root);\n const lineEl = $(\"#subscribePageLine\", root);\n // Styled first: it pastes in looking like the hosted page anywhere, so the first form a\n // publisher sees is their publication's, and Plain is the choice for one who'll style it.\n let mode: EmbedMode = \"styled\";\n const name = () => page.state.name || page.fromDisplay;\n const rebuild = () => {\n const code = buildEmbed(mode, name(), appOrigin);\n codeEl.textContent = code;\n setHtml(copyEl, embedCopy(mode, code));\n setHtml(previewEl, embedPreview(mode, name()));\n lineEl.textContent = pageLine(name());\n };\n const modeButtons = $$(\"[data-embed]\", root);\n for (const b of modeButtons) {\n b.onclick = () => {\n mode = b.dataset.embed === \"styled\" ? \"styled\" : \"plain\";\n for (const o of modeButtons) {\n o.setAttribute(\"aria-pressed\", String(o.dataset.embed === mode));\n }\n rebuild();\n };\n }\n wireCopy($(\".set-sec-subscribe\", root));\n rebuild();\n // The page repaints every section on any change, but the section shows only the name:\n // rebuilt for anything else, its Copy would lose a check it had just shown.\n let shownName = name();\n return {\n repaint() {\n if (name() !== shownName) {\n shownName = name();\n rebuild();\n }\n },\n };\n}\n", "// The Settings page: its sections in groups by whom they serve (the publication, its\n// readers, its emails, the publisher), editable first and set-at-deploy last, under one\n// save bar. Each section is its own module; this one loads\n// the settings, holds the live state against the saved baseline, and commits it.\n\nimport { fromDisplayName } from \"../../shared/publication\";\nimport type {\n ConfirmationEmailCopy,\n SettingsPatchBody,\n SettingsResponse,\n SettingsSavedResponse,\n SettingsView,\n} from \"../../shared/settings\";\nimport { ApiError, api } from \"../api\";\nimport { renderSidebarBrand } from \"../brand\";\nimport { mount } from \"../lifecycle\";\nimport { appState } from \"../state\";\nimport { $ } from \"../ui/dom\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport { savebar } from \"../ui/savebar\";\nimport { fieldError, renderError, toast } from \"../ui/widgets\";\nimport { confirmationSection, mountConfirmation } from \"./confirmation\";\nimport { instanceSection, senderSection } from \"./deploy\";\nimport { mountRecipients, mountTemplate, recipientsSection, templateSection } from \"./emails\";\nimport { identitySection, mountIdentity } from \"./identity\";\nimport { mountNotifications, notificationsSection } from \"./notifications\";\nimport {\n chip,\n message,\n type SectionHandle,\n type SettingsBaseline,\n type SettingsFormState,\n type SettingsPage,\n secHead,\n} from \"./page\";\nimport { mountReaderPages, readerPagesSection } from \"./reader_pages\";\nimport { remakeIdentity, savedToast, withRemakeConfirm } from \"./remake\";\nimport { mountSubscribe, subscribeSection } from \"./subscribe\";\n\n/** One group of sections, kept together by whom they serve: no heading of its own, since\n * the sections' titles already say what each is, only a wider gap from the next group. */\nfunction group(id: string, sections: Html[]): Html {\n return html`<div class=\"set-group\" data-group=\"${id}\">${sections}</div>`;\n}\n\nfunction baselineOf(state: SettingsBaseline): SettingsBaseline {\n return {\n name: state.name,\n tagline: state.tagline,\n address: state.address,\n recipients: [...state.recipients],\n confirmation: { ...state.confirmation },\n notifyTo: state.notifyTo,\n };\n}\n\nconst sameList = (a: string[], b: string[]) =>\n a.length === b.length && a.every((x, i) => x === b[i]);\nconst sameCopy = (a: ConfirmationEmailCopy, b: ConfirmationEmailCopy) =>\n a.subject === b.subject &&\n a.body === b.body &&\n a.buttonLabel === b.buttonLabel &&\n a.reassurance === b.reassurance;\n\nexport async function renderSettings(root: HTMLElement, signal: AbortSignal): Promise<void> {\n setHtml(\n root,\n html`<div class=\"settings\"><div class=\"page-head\"><h1>Settings</h1><p class=\"set-lede set-page-lede\">Your publication\u2019s identity, the ways readers subscribe and the pages they land on, the email each post is sent inside, and where Kestrel tells you about your sends. Facts set when Kestrel was deployed come last, read-only.</p></div><div id=\"settingsBody\" class=\"muted\">Loading\u2026</div></div>`,\n );\n const body = $(\"#settingsBody\", root);\n let data: SettingsResponse;\n try {\n data = await api<SettingsResponse>(\"/api/settings\", { signal });\n } catch (e) {\n renderError(body, message(e), () => mount(renderSettings));\n return;\n }\n const s = data.settings;\n const p = s.publication;\n const ce = s.confirmationEmail;\n // Live, in-memory state. The save bar tracks the persisted fields against the saved\n // baseline; the logo saves the moment it changes (its own endpoints), and the email\n // template has its own page and its own Save, so neither feeds the bar.\n const state: SettingsFormState = {\n name: p.name || \"\",\n tagline: p.tagline || \"\",\n address: p.address || \"\",\n logoUrl: p.logoUrl || \"\",\n recipients: [...(s.testRecipients || [])],\n // The confirmation email's words (SPEC \u00A77); the layout, masthead, and confirm link\n // are Kestrel's.\n confirmation: {\n subject: ce.subject || \"\",\n body: ce.body || \"\",\n buttonLabel: ce.buttonLabel || \"\",\n reassurance: ce.reassurance || \"\",\n },\n notifyTo: s.notifications.to || \"\",\n };\n let baseline = baselineOf(state);\n const fromDisplay = fromDisplayName(data.deployment.fromAddress);\n\n const isDirty = () =>\n state.name !== baseline.name ||\n state.tagline !== baseline.tagline ||\n state.address !== baseline.address ||\n !sameList(state.recipients, baseline.recipients) ||\n !sameCopy(state.confirmation, baseline.confirmation) ||\n state.notifyTo !== baseline.notifyTo;\n\n const handles: SectionHandle[] = [];\n // The shared bottom save bar (Save + Discard), its callbacks the hoisted saveSettings /\n // discardSettings below.\n const bar = savebar.attach({ onSave: saveSettings, onDiscard: discardSettings }, signal);\n let refreshReaderPages = () => {};\n const page: SettingsPage = {\n data,\n state,\n saved: () => baseline,\n fromDisplay,\n fromName: fromDisplay || \"Your publication\",\n changed() {\n for (const h of handles) {\n h.repaint?.();\n }\n bar.setDirty(isDirty());\n },\n // Keep the cached config and the sidebar brand, which reads the same identity, in\n // step with a save: this page's own response, with the saved half.\n applySaved(settings: SettingsView) {\n appState.appConfig = { ...data, settings };\n renderSidebarBrand();\n // The reader pages wear the saved identity, so the one shown re-reads it.\n refreshReaderPages();\n },\n };\n\n setHtml(\n body,\n html`\n ${group(\"publication\", [identitySection(page)])}\n ${group(\"readers\", [subscribeSection(page), confirmationSection(page), readerPagesSection(secHead(\"Reader pages\", chip(\"preview\", \"Preview\")))])}\n ${group(\"emails\", [templateSection(page), recipientsSection()])}\n ${group(\"you\", [notificationsSection(page)])}\n ${group(\"deploy\", [senderSection(page), instanceSection(page)])}\n `,\n );\n handles.push(\n mountIdentity(body, page, signal),\n mountSubscribe(body, page),\n mountConfirmation(body, page),\n mountTemplate(body, page),\n mountRecipients(body, page),\n mountNotifications(body, page),\n );\n const readerPages = mountReaderPages(body, data.deployment, signal);\n refreshReaderPages = readerPages.refresh;\n\n // --- save / discard. Wired into the shared save bar above; the controller runs\n // saveSettings inside busy() on its Save button, so these stay plain callbacks.\n // The field a refused save marked, cleared when the next save starts or the edits are\n // discarded, neither of which edits the field itself.\n let clearFieldMark: (() => void) | null = null;\n const unmarkField = () => {\n clearFieldMark?.();\n clearFieldMark = null;\n };\n async function saveSettings() {\n unmarkField();\n try {\n // An identity change the template renders reaches every scheduled email, so the\n // server may ask for the acknowledgement first (withRemakeConfirm); a save that\n // touches only recipients or the confirmation wording never does.\n const payload: SettingsPatchBody = {\n publication: { name: state.name, tagline: state.tagline, address: state.address },\n testRecipients: state.recipients,\n confirmationEmail: state.confirmation,\n notifications: { to: state.notifyTo },\n };\n const r = await withRemakeConfirm(\n (ack) =>\n api<SettingsSavedResponse>(\"/api/settings\", {\n method: \"PUT\",\n json: ack ? { ...payload, remake: ack } : payload,\n }),\n remakeIdentity(\"Save\", \"Saving\"),\n signal,\n );\n if (!r) {\n return; // declined: the edits stay, the bar stays up\n }\n // Adopt the server's normalized result (trim, lowercase, dedupe) as the baseline.\n const ns = r.settings;\n state.name = ns.publication.name;\n state.tagline = ns.publication.tagline;\n state.address = ns.publication.address;\n state.recipients = [...ns.testRecipients];\n state.confirmation = { ...ns.confirmationEmail };\n state.notifyTo = ns.notifications.to;\n baseline = baselineOf(state);\n for (const h of handles) {\n h.reset?.();\n }\n page.applySaved(ns);\n page.changed(); // clean now: slides the bar away\n toast(savedToast(\"Settings saved\", r.remade));\n } catch (err) {\n // A failed save leaves the edits in place (still dirty, so the bar stays up), and the\n // bar says why, as on the Template page: a toast would sit over the bar and vanish\n // while the edits it is about are still unsaved (DESIGN \u00A75). A refusal naming a field\n // on the page says why under that field, focuses it, and the bar points there.\n const input = err instanceof ApiError ? fieldInput(root, err.field) : null;\n if (input && err instanceof ApiError) {\n clearFieldMark = fieldError(input, message(err));\n input.focus();\n bar.showError(\"Not saved: fix the field marked above.\");\n } else {\n bar.showError(message(err));\n }\n }\n }\n function discardSettings() {\n unmarkField();\n Object.assign(state, baselineOf(baseline));\n for (const h of handles) {\n h.reset?.();\n }\n page.changed();\n }\n\n page.changed();\n}\n\n// The input each settings field the API can name is edited in, by its wire name.\nconst FIELD_INPUTS: Record<string, string> = {\n \"publication.name\": \"#setName\",\n \"publication.tagline\": \"#setTagline\",\n \"publication.address\": \"#setAddress\",\n testRecipients: \"#recipInput\",\n \"notifications.to\": \"#notifyTo\",\n \"confirmationEmail.subject\": \"#ceSubject\",\n \"confirmationEmail.body\": \"#ceMessage\",\n \"confirmationEmail.buttonLabel\": \"#ceButton\",\n \"confirmationEmail.reassurance\": \"#ceFooter\",\n};\n\n/** The input a refusal's `field` names, when the page shows it; null otherwise. */\nfunction fieldInput(\n root: HTMLElement,\n field: string | null,\n): HTMLInputElement | HTMLTextAreaElement | null {\n const sel = field ? FIELD_INPUTS[field] : undefined;\n const el = sel ? root.querySelector(sel) : null;\n return (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) &&\n !el.closest(\"[hidden]\")\n ? el\n : null;\n}\n", "// The subscribers list: the by-status composition and the filterable roster.\n\nimport type { SubscriberListItem, SubscriberListResponse } from \"../../shared/subscribers\";\nimport { api, describeError } from \"../api\";\nimport { $, $$ } from \"../ui/dom\";\nimport { fmt } from \"../ui/format\";\nimport { type Html, html, setHtml } from \"../ui/html\";\nimport {\n type ListState,\n listQuery,\n listToolbar,\n renderPager,\n th,\n wireSort,\n wireToolbar,\n} from \"../ui/list_controls\";\nimport { badge, infoTip, openMenu, renderError } from \"../ui/widgets\";\nimport { addSubscriberModal, confirmUnsubscribe } from \"./dialogs\";\n\n// The story of the list as a whole: its composition (by-status counts) and the roster,\n// filterable, sortable, and searchable. Consent status and suppression are separate\n// axes: the status filter narrows the roster; the suppression facet is an overlay. The\n// dashboard tiles deep-link via #/subscribers/<filter> \u2014 `initialFilter` seeds the\n// status filter, or the suppression facet for \"suppressed\".\nconst SUB_STATUSES = [\n { value: \"confirmed\", label: \"Confirmed\" },\n { value: \"pending\", label: \"Pending\" },\n { value: \"unsubscribed\", label: \"Unsubscribed\" },\n];\n\nexport async function renderSubscribers(\n initialFilter: string | undefined,\n root: HTMLElement,\n signal: AbortSignal,\n): Promise<void> {\n const state: ListState = {\n status: \"\",\n search: \"\",\n suppressed: \"\",\n sort: \"joined\",\n dir: \"desc\",\n offset: 0,\n limit: 50,\n };\n if (initialFilter === \"suppressed\") {\n state.suppressed = \"only\";\n } else if (\n initialFilter === \"confirmed\" ||\n initialFilter === \"pending\" ||\n initialFilter === \"unsubscribed\"\n ) {\n state.status = initialFilter;\n }\n\n setHtml(\n root,\n html`\n <div class=\"spread page-head\"><h1>Subscribers</h1><button class=\"primary\" id=\"addSub\">Add subscriber</button></div>\n <div id=\"subCounts\" class=\"muted\">Loading\u2026</div>\n ${listToolbar({ statuses: SUB_STATUSES, suppressible: true, searchPlaceholder: \"Search email\u2026\" })}\n <div id=\"subList\" class=\"muted\">Loading\u2026</div>\n <div id=\"subPager\"></div>`,\n );\n\n const listEl = $(\"#subList\");\n const pagerEl = $(\"#subPager\");\n\n async function load() {\n try {\n const data = await api<SubscriberListResponse>(`/api/subscribers?${listQuery(state)}`, {\n signal,\n });\n const c = data.counts;\n // The counts card stays a global by-status summary (independent of the active\n // filter); the page total below reflects the filtered roster.\n setHtml(\n $(\"#subCounts\"),\n html`<div class=\"card row\" style=\"gap:24px\"><span><strong>${c.confirmed}</strong> confirmed</span><span>${c.pending} pending</span><span>${c.unsubscribed} unsubscribed</span><span>${c.suppressed} suppressed</span>${infoTip(\n \"Pending: subscribed but hasn\u2019t clicked the confirmation email. Confirmed: consented, and receives sends. Unsubscribed: opted out. Suppressed: bounced or complained, and never mailed, whatever the consent state.\",\n { below: true },\n )}</div>`,\n );\n renderSubTable(listEl, data.subscribers, state, load, signal);\n renderPager(pagerEl, state, data.page, load);\n } catch (e) {\n renderError(listEl, describeError(e), load);\n }\n }\n\n $(\"#addSub\").onclick = () => addSubscriberModal(load, signal);\n wireToolbar(root, state, load);\n load();\n}\n\n// The inline suppression flag rides next to the email (suppression is a deliverability\n// overlay, not a consent status) and names WHY: bounced / complaint / blocked, with the\n// provider detail in the tooltip. Reasons come from the webhook (bounce, complaint) or a\n// manual block; anything unrecognized falls back to a generic label.\nconst SUPPRESSION_LABELS: Record<string, string> = {\n bounce: \"bounced\",\n complaint: \"complaint\",\n manual: \"blocked\",\n};\nconst SUPPRESSION_TIPS: Record<string, string> = {\n bounce: \"Hard bounce: mail to this address failed, so it won\u2019t be mailed again.\",\n complaint: \"Marked as spam, so it won\u2019t be mailed again, whatever the consent state.\",\n manual: \"Manually blocked, so it won\u2019t be mailed.\",\n};\nfunction suppressionFlag(s: SubscriberListItem): Html | null {\n if (!s.suppressed) {\n return null;\n }\n const reason = s.suppression_reason ?? \"\";\n const label = SUPPRESSION_LABELS[reason] || \"suppressed\";\n const base =\n SUPPRESSION_TIPS[reason] || \"Suppressed, so it won\u2019t be mailed, whatever the consent state.\";\n const tip = s.suppression_detail ? `${base} (${s.suppression_detail})` : base;\n return html` <span class=\"badge suppressed row-flag\" title=\"${tip}\">${label}</span>`;\n}\n\nfunction renderSubTable(\n listEl: Element,\n rows: SubscriberListItem[],\n state: ListState,\n reload: () => void,\n signal: AbortSignal,\n): void {\n if (!rows.length) {\n setHtml(listEl, html`<p class=\"muted\">No subscribers match.</p>`);\n return;\n }\n // The joined cell is named so the \u2264720px layout can stack a row and label it.\n setHtml(\n listEl,\n html`<div class=\"table-wrap\"><table class=\"list-table subs-table stacks has-actions\"><colgroup><col><col class=\"c-status\"><col class=\"c-date\"><col class=\"c-act\"></colgroup><thead><tr>${th(\"Email\", \"email\", state)}${th(\"Status\", null, state)}${th(\"Joined\", \"joined\", state)}<th></th></tr></thead><tbody>${rows.map(\n (s) =>\n html`<tr data-id=\"${s.id}\"><td>${s.email}${suppressionFlag(s)}</td><td>${badge(s.status)}</td><td class=\"muted joined\">${fmt(s.created_at)}</td><td class=\"act\">${\n s.status === \"confirmed\"\n ? html`<button class=\"icon\" data-menu=\"${s.id}\" aria-label=\"Subscriber actions\" aria-haspopup=\"menu\">\u22EF</button>`\n : null\n }</td></tr>`,\n )}</tbody></table></div>`,\n );\n wireSort(listEl, state, reload);\n for (const b of $$<HTMLButtonElement>(\"[data-menu]\", listEl)) {\n b.onclick = (e) => {\n e.stopPropagation();\n const row = rows.find((r) => r.id === b.dataset.menu);\n if (!row) {\n return;\n }\n openMenu(\n b,\n [\n {\n label: \"Unsubscribe\",\n danger: true,\n onClick: () => confirmUnsubscribe(row, reload, signal),\n },\n ],\n signal,\n );\n };\n }\n}\n", "// The hash router: route() mounts the view the hash names, and the mount tears the\n// previous one down; the leave guards live here, over the mounted view's handle, wired by\n// installRouter() when boot calls it.\n\nimport { dashboardRouteFromSearch, isDashboardRoute } from \"../shared/dashboard_link\";\nimport { renderDashboard } from \"./dashboard/dashboard\";\nimport { mount, mounted } from \"./lifecycle\";\nimport { renderDrafts } from \"./posts/drafts\";\nimport { renderEditor } from \"./posts/editor\";\nimport { renderWebVersion, webPostId } from \"./posts/web\";\nimport { renderDocs } from \"./room/docs\";\nimport { renderReference } from \"./room/reference\";\nimport { renderSent } from \"./sends/list\";\nimport { renderSentRecord } from \"./sends/record\";\nimport { renderSettings } from \"./settings/settings\";\nimport { renderTemplate } from \"./settings/template\";\nimport { setNavOpen } from \"./shell\";\nimport { renderSubscribers } from \"./subscribers/list\";\n\nconst LEAVE_MSG = \"You have unsaved changes. Leave without saving?\";\n\n// The hash the mounted view was routed at: a hashchange back to it is nothing to do (the\n// echo of a declined leave prompt putting it back), and a declined prompt knows where to go.\nlet mountedHash = \"\";\n\n/** Mount the view the hash names; the mount tears the previous one down. */\nexport function route(): Promise<void> {\n const hash = location.hash || \"#/dashboard\";\n mountedHash = hash;\n // Views put a route's segments into API paths as they are, so a hash a link from an email\n // could not name (a `..` segment, say) opens the dashboard home, as a mangled link does.\n const [, view, arg, sub] = (isDashboardRoute(hash.slice(1)) ? hash : \"#/dashboard\").split(\"/\");\n // The editor wants the full width, and carries its own \"\u2190 Posts\" affordance, so\n // it hides the sidebar rather than living beside it (SPEC \u00A711: admin-only chrome).\n document.body.classList.toggle(\"editor-mode\", view === \"edit\");\n // A post's web version is a page of its own, in a tab of its own: the whole window, with\n // no app chrome around it. Only for a route that names a post, so a mangled address\n // falls through to the dashboard with its navigation.\n const webPost = view === \"web\" ? webPostId(arg, sub) : null;\n document.body.classList.toggle(\"web-mode\", webPost !== null);\n // The reference room (Docs, API) is about Kestrel itself, not the publication, so it\n // drops the publication sidebar for a slim tool bar.\n const toolMode = view === \"docs\" || view === \"reference\";\n document.body.classList.toggle(\"tool-mode\", toolMode);\n // Mark the active nav item across both sidebar navs (primary + tools) so the\n // reader can see where they are (aria-current also styles it).\n for (const a of document.querySelectorAll<HTMLAnchorElement>(\".sidebar a[data-view]\")) {\n if (a.dataset.view === view) {\n a.setAttribute(\"aria-current\", \"page\");\n } else {\n a.removeAttribute(\"aria-current\");\n }\n }\n // The reference room's surface switch is marked at render time (roomShell). Close\n // the mobile nav drawer on any navigation.\n setNavOpen(false);\n if (view === \"edit\" && arg) {\n return mount((root, signal) => renderEditor(arg, root, signal));\n }\n if (webPost !== null) {\n return mount((root, signal) => renderWebVersion(webPost, root, signal));\n }\n if (view === \"drafts\") {\n return mount(renderDrafts);\n }\n if (view === \"subscribers\") {\n return mount((root, signal) => renderSubscribers(arg, root, signal));\n }\n if (view === \"sent\") {\n return arg ? mount((root, signal) => renderSentRecord(arg, root, signal)) : mount(renderSent);\n }\n if (view === \"template\") {\n return mount(renderTemplate);\n }\n if (view === \"settings\") {\n return mount(renderSettings);\n }\n if (view === \"reference\") {\n return mount(renderReference);\n }\n if (view === \"docs\") {\n return mount((root, signal) => renderDocs(arg, root, signal, sub));\n }\n return mount(renderDashboard);\n}\n\n/** Adopt the route a link from an email names in its query (`/dashboard/?to=/sent/<id>`,\n * shared/dashboard_link.ts), which survives an Access login where a hash would not: it\n * becomes the hash, the query goes, and no history entry is added. Boot calls this once,\n * signed in, before the first route(). */\nexport function adoptLinkedRoute(): void {\n const linked = dashboardRouteFromSearch(location.search);\n if (linked) {\n // Edit this page's own URL, so the result is same-origin whatever its path; the link's\n // route replaces any hash, since it is the page the link was sent to open.\n const url = new URL(location.href);\n url.search = \"\";\n url.hash = linked;\n history.replaceState(null, \"\", url);\n }\n}\n\n/** Wire navigation and the mounted view's guards: hashchange, the unload prompt, and \u2318S. Boot calls this once. */\nexport function installRouter(): void {\n // hashchange fires after the hash has already moved, so the mounted view is asked\n // before route() tears it down: a dirty editor saves in the background and lets the\n // navigation through, unless its last save failed (a silent flush could lose work),\n // when it asks for a confirm() (synchronous, unlike the modal helper) and, on cancel,\n // the hash goes back, which is a hashchange to the mounted hash and so nothing to do.\n window.addEventListener(\"hashchange\", () => {\n if (location.hash === mountedHash) {\n return;\n }\n if (mounted()?.beforeLeave?.() === \"confirm\" && !confirm(LEAVE_MSG)) {\n location.hash = mountedHash;\n return;\n }\n route();\n });\n // Tab close / reload / external navigation: can't reliably finish an async save,\n // so fall back to the browser's own generic unsaved-changes prompt.\n window.addEventListener(\"beforeunload\", (e) => {\n if (mounted()?.dirty?.()) {\n e.preventDefault();\n e.returnValue = \"\";\n }\n });\n // \u2318S / Ctrl-S saves the mounted view, when it saves (registered once; no-op elsewhere).\n window.addEventListener(\"keydown\", (e) => {\n const save = mounted()?.manualSave;\n if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === \"s\" && save) {\n e.preventDefault();\n save();\n }\n });\n}\n", "// Kestrel editor \u2014 a small vanilla SPA over the same HTTP API Claude uses.\n// Auth is edge-centric: in production Cloudflare Access gates this surface, so the\n// browser's Access session cookie authenticates every same-origin call and there is\n// nothing to paste. In local dev there is no edge, so the editor mints a dev token\n// on boot (/api/dev/token) and sends it as a bearer. Either way the boot probe\n// (/api/whoami) tells us who we are and which mode we're in; the identity chip and\n// the failure handling follow from that.\n\nimport type { SettingsResponse } from \"../shared/settings\";\nimport { api } from \"./api\";\nimport { probeSession, renderIdentity } from \"./auth\";\nimport { renderSidebarBrand } from \"./brand\";\nimport { startDevReload } from \"./dev_reload\";\nimport { installRoomBar } from \"./room/shell\";\nimport { adoptLinkedRoute, installRouter, route } from \"./router\";\nimport { installShell } from \"./shell\";\nimport { appState } from \"./state\";\nimport { installTooltips } from \"./ui/widgets\";\n\nasync function boot(): Promise<unknown> {\n // Wire the app before anything asynchronous, in one visible sequence: no module does\n // anything at load but define things, so what the app does at boot is read from here.\n installShell();\n installTooltips();\n installRoomBar();\n installRouter();\n const session = await probeSession();\n if (session) {\n appState.session = session;\n document.body.classList.remove(\"signed-out\");\n renderIdentity();\n // Load the publication identity for the sidebar brand. Non-fatal: on failure the\n // brand keeps its \"Kestrel\" placeholder and routing still proceeds.\n try {\n appState.appConfig = await api<SettingsResponse>(\"/api/settings\");\n } catch {\n /* keep the placeholder brand */\n }\n renderSidebarBrand();\n // Dev build only: pick up a rebuilt bundle or an edited stylesheet without a hand\n // refresh. The production build compiles this out (see scripts/build-client.mjs).\n if (__DEV__) {\n startDevReload();\n }\n // A link from a notification email names its page in the query, which survives an\n // Access login where a hash would not.\n adoptLinkedRoute();\n return route();\n }\n // probeSession has already put up the wall it landed on.\n return undefined;\n}\nboot();\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["root", "id", "el", "app", "identity", "toasts", "navToggle", "navScrim", "setNavOpen", "open", "navCollapse", "navOverride", "computeNavMode", "applyNavMode", "mode", "expanded", "label", "navResizeRaf", "navResizeSettle", "installShell", "controller", "handle", "mount", "view", "c", "root", "app", "outcome", "e", "h", "unmount", "mounted", "every", "ms", "fn", "signal", "id", "at", "time", "cancel", "poll", "tick", "arm", "again", "isExpectedFailure", "onAbort", "$", "selector", "root", "el", "$$", "ESCAPES", "escapeHtml", "value", "c", "Html", "markup", "toMarkup", "UNQUOTED_ATTRIBUTE", "html", "strings", "values", "out", "chunk", "i", "s", "unsafeHtml", "setHtml", "el", "ACCESS_GUIDE", "accessNotConfigured", "data", "d", "showAccessLocked", "app", "unmount", "setHtml", "html", "$", "configRefusal", "data", "d", "CONFIGURATION_REFERENCE", "REFERENCE_ANCHORS", "REFUSALS_ANCHOR", "referenceUrl", "variable", "showConfigProblem", "refusal", "app", "unmount", "heading", "reference", "html", "setHtml", "$", "TOKEN_KEY", "appState", "setToken", "t", "appState", "TOKEN_KEY", "authHeaders", "renderIdentity", "session", "p", "who", "setHtml", "identity", "html", "showReauth", "app", "unmount", "$", "mintDevToken", "r", "whoami", "probeSession", "res", "data", "refusal", "configRefusal", "showConfigProblem", "accessNotConfigured", "showAccessLocked", "ApiError", "message", "status", "data", "d", "REAUTH_MESSAGE", "isBareWord", "m", "NETWORK_FAILURE", "isNetworkFailure", "describeError", "written", "send", "path", "opts", "headers", "authHeaders", "body", "res", "accessNotConfigured", "showAccessLocked", "showReauth", "messageOf", "failure", "text", "refusal", "configRefusal", "showConfigProblem", "fallback", "api", "apiText", "apiFile", "disposition", "filename", "fromDisplayName", "fromAddress", "header", "lt", "display", "publicationName", "name", "derivePublication", "data", "p", "publicationName", "renderSidebarBrand", "pub", "appState", "nameEl", "tagEl", "logoEl", "setHtml", "html", "archivePostUrl", "origin", "basePath", "slug", "NOT_CONNECTED_ADVICE", "STUCK_THRESHOLD_MS", "DEFAULT_MIN_LEAD_MS", "MIN_LEAD_FLOOR_MS", "MIN_LEAD_CEILING_MS", "formatLead", "ms", "seconds", "minutes", "tooSoonMessage", "minLeadMs", "BOUNCE_SPIKE_RECENT_MS", "DAILY_WRITES_SEND_RECIPIENTS", "dailyWritesSendWarning", "recipients", "buildRefParts", "b", "appState", "version", "html", "sha", "minLeadMs", "DEFAULT_MIN_LEAD_MS", "tooSoonText", "tooSoonMessage", "minLeadMs", "sendNowWhenText", "lead", "minutes", "formatLead", "isTooSoon", "fireAt", "now", "earliestFireAt", "noEmailProvider", "appState", "providerNotConnected", "freePlanLimits", "notConnectedPreview", "CONNECT_PROVIDER_HREF", "connectProviderLink", "cls", "classAttr", "html", "NOT_CONNECTED_ADVICE", "heldAdviceHtml", "heldSendsHtml", "which", "count", "one", "notConnectedWarning", "words", "heldWhenFiresHtml", "notConnectedLine", "why", "after", "notConnectedTestLine", "notConnectedAddLine", "dailyWritesLine", "notConnectedScheduleLine", "notConnectedRescheduleLine", "withNoProviderNote", "msg", "postArchiveUrl", "slug", "d", "archivePostUrl", "article_default", "bold_default", "book_default", "branch_default", "check_default", "circle_check_default", "circle_minus_default", "code_default", "copyout_default", "desktop_default", "editable_default", "heading_default", "indent_default", "info_default", "info_filled_default", "italic_default", "kestrel_default", "lines_default", "link_default", "mail_default", "ol_default", "paperclip_default", "phone_default", "plus_default", "preview_default", "quote_default", "readonly_default", "search_default", "send_default", "ul_default", "upload_default", "x_default", "ICONS", "unsafeHtml", "article_default", "bold_default", "check_default", "code_default", "copyout_default", "editable_default", "heading_default", "indent_default", "info_default", "info_filled_default", "italic_default", "kestrel_default", "lines_default", "link_default", "ol_default", "paperclip_default", "plus_default", "preview_default", "quote_default", "readonly_default", "search_default", "send_default", "ul_default", "upload_default", "book_default", "branch_default", "circle_check_default", "circle_minus_default", "desktop_default", "mail_default", "phone_default", "x_default", "icon", "name", "ICONS", "roomShell", "active", "rail", "main", "tab", "view", "label", "html", "parts", "buildRefParts", "bt", "appState", "built", "stamp", "railFoot", "foot", "body", "icon", "installRoomBar", "app", "ev", "a", "ROUTE", "DOT_SEGMENT", "isDashboardRoute", "route", "dashboardRouteFromSearch", "search", "PATTERN", "encodeSendCursor", "cursor", "decodeSendCursor", "raw", "match", "seq", "at", "earlierCursor", "a", "b", "x", "y", "stageOf", "s", "RETRY_MS", "RETRY_MAX_MS", "READ_TIMEOUT_MS", "followers", "cursor", "floor", "timer", "reading", "readAgain", "readQueued", "failures", "safely", "fn", "e", "lower", "a", "b", "earlierCursor", "arm", "ms", "read", "since", "round", "c", "cutOff", "query", "api", "res", "f", "err", "ApiError", "dropped", "leave", "queueRead", "onVisibility", "join", "signal", "followSends", "from", "follower", "seen", "changes", "send", "to", "was", "removed", "r", "id", "followSend", "change", "readSendsNow", "normalizeEmail", "email", "isValidEmail", "fmt", "ms", "parseAddresses", "text", "valid", "invalid", "part", "a", "normalizeEmail", "isValidEmail", "invalidAddressesMessage", "notSentMessage", "sent", "total", "failed", "reasons", "why", "toLocalInput", "d", "p", "untilStr", "fireAt", "due", "min", "hr", "rm", "days", "lateStr", "listQuery", "state", "p", "term", "listToolbar", "cfg", "statusSel", "html", "s", "suppressed", "failures", "wireToolbar", "root", "reload", "search", "status", "t", "th", "label", "key", "cls", "active", "arrow", "klass", "wireSort", "container", "b", "renderPager", "el", "page", "limit", "setHtml", "from", "to", "hasPrev", "hasNext", "DISABLED", "prev", "next", "TOAST_MS", "TOAST_LEAVE_MS", "MAX_TOASTS", "toastTimers", "toast", "msg", "live", "toasts", "same", "t", "lingerToast", "old", "leaveToast", "badge", "status", "html", "fieldErrorSeq", "fieldErrors", "errorLine", "message", "line", "ico", "text", "fieldError", "field", "describedBy", "cleared", "clear", "modal", "content", "signal", "back", "setHtml", "dialog", "opener", "onKey", "e", "close", "trapTab", "clearCurrent", "clearError", "error", "actions", "active", "hadFocus", "focusables", "FOCUSABLE", "root", "el", "els", "first", "last", "at", "TIP_GUTTER", "positionInfoTip", "tip", "tipW", "iconLeft", "vw", "shift", "installTooltips", "type", "infoTip", "below", "icon", "menuEls", "menuSignal", "menuAnchor", "closeMenu", "openMenu", "anchor", "items", "align", "overlay", "m", "it", "b", "name", "desc", "menuKey", "r", "i", "go", "n", "busy", "btn", "label", "fn", "orig", "renderError", "container", "retryFn", "DRAFT_STATUSES", "DRAFTS_SCOPE", "createNewPost", "btn", "busy", "post", "api", "err", "toast", "describeError", "renderDrafts", "root", "signal", "state", "setHtml", "html", "listToolbar", "newPost", "$", "listEl", "pagerEl", "shownFire", "shownSends", "latest", "reload", "load", "background", "mine", "data", "listQuery", "posts", "p", "filtered", "th", "sending", "href", "badge", "fmt", "wireSort", "tr", "$$", "e", "t", "b", "st", "target", "items", "id", "confirmDelete", "openMenu", "renderPager", "renderError", "following", "firstLoad", "follow", "first", "followSends", "sends", "changes", "removed", "c", "s", "mount", "wireToolbar", "pid", "m", "modal", "go", "clampPct", "n", "fmtDuration", "ms", "s", "m", "countdownHtml", "html", "has", "countdowns", "root", "signal", "armed", "tick", "now", "next", "watch", "moment", "el", "$$", "closes", "fire", "missed", "lateStr", "untilStr", "at", "every", "kind", "c", "conditionOf", "can", "name", "a", "sendsNow", "rows", "reported", "listed", "r", "needsOperator", "deliveredCell", "delivered", "kinds", "note", "text", "i", "activeRowHtml", "accepted", "confirmed", "total", "pct", "eta", "stuck", "addSubscriberModal", "onDone", "signal", "notConnected", "providerNotConnected", "m", "modal", "html", "notConnectedAddLine", "go", "$", "input", "busy", "addr", "normalizeEmail", "isValidEmail", "r", "api", "toast", "addedMessage", "e", "describeError", "ApiError", "action", "confirmUnsubscribe", "sub", "COPIED_MS", "copyFace", "copied", "word", "html", "icon", "copyButton", "text", "label", "codeBlock", "code", "raw", "pending", "copyWith", "button", "toast", "prior", "setHtml", "timer", "wireCopy", "root", "ev", "b", "computeHealth", "live", "alerts", "missed", "s", "has", "wedged", "c", "conditionOf", "firstWedged", "n", "sum", "w", "noun", "html", "notConnected", "providerNotConnected", "allRefused", "held", "r", "refused", "which", "sends", "heldSendsHtml", "firstRefused", "stuck", "needsOperator", "firstStuck", "spiky", "firstSpike", "connectProviderLink", "healthHtml", "level", "i", "activeOf", "renderDashboard", "view", "signal", "remount", "mount", "setHtml", "root", "$", "posts", "first", "counts", "p", "subs", "api", "e", "renderError", "describeError", "reported", "pub", "derivePublication", "appState", "deployment", "totalSubs", "setupChecklistHtml", "apiConnectCard", "wireDashActions", "tiles", "tilesHtml", "t", "drafts", "draftsHtml", "fmt", "homeUrl", "pubCardHtml", "copyButton", "claudeConnected", "apiCardHtml", "quickHtml", "sendsNow", "dashActiveHtml", "dashScheduledHtml", "scheduledOf", "dashSentHtml", "tr", "$$", "wireDashSentRows", "wireDashActiveCards", "wireDashScheduledCards", "tickCountdowns", "countdowns", "reading", "readSends", "mine", "fresh", "paintLive", "now", "patchDelivered", "inQueue", "st", "followSends", "u", "id", "stageOf", "a", "b", "recent", "url", "badge", "deliveredCell", "cell", "scheduled", "heldWhenFiresHtml", "countdownHtml", "card", "active", "activeRowHtml", "reload", "createNewPost", "addSubscriberModal", "wireCopy", "connected", "icon", "subscribeUrl", "CONNECT_PROVIDER_HREF", "copyFace", "message", "describeError", "openResolveModal", "send", "reload", "signal", "n", "m", "modal", "html", "$", "doResolve", "btn", "resolution", "verb", "busy", "res", "api", "toast", "e", "unsent", "accepted", "openRescheduleModal", "sendId", "currentFireAt", "onDone", "minStr", "toLocalInput", "earliestFireAt", "cur", "notConnectedRescheduleLine", "box", "go", "when", "v", "t", "isTooSoon", "tooSoonText", "ApiError", "readSendsNow", "pagePolicy", "formAction", "opts", "d", "stayPut", "EMAIL_FRAME_SANDBOX", "openLinksInNewTab", "doc", "link", "intoHead", "markup", "head", "tag", "framedCopy", "pagePolicy", "hlTokens", "s", "m", "hlHtml", "raw", "escapeHtml", "_m", "br", "name", "sl", "hlCss", "ind", "sel", "sp", "prop", "colon", "highlightTemplate", "src", "unsafeHtml", "seg", "hlMdInline", "code", "bang", "ltext", "lurl", "bd", "btext", "aem", "uem", "d", "text", "renderMdLine", "line", "h", "q", "li", "highlightMarkdown", "inFence", "html", "band", "fence", "lang", "JSON_TOKEN", "highlightJson", "value", "out", "last", "whole", "str", "num", "lit", "highlightCurl", "AUTOSAVE_TIMERS", "createAutosave", "save", "timers", "idle", "cap", "cancel", "fire", "DirtyTracker", "read", "#saved", "sent", "pending", "trackSaves", "chain", "savesSettled", "conflictFromError", "ApiError", "d", "RevisionTracker", "base", "revision", "conflict", "answer", "at", "rev", "webVersionHash", "postId", "webPostId", "kind", "id", "shortUrl", "url", "webBar", "state", "tag", "html", "stage", "at", "normalUrl", "previewOpensWebVersion", "doc", "archiveUrl", "webUrl", "own", "link", "href", "leaveFor", "readPost", "signal", "api", "renderWebVersion", "root", "appTitle", "onAbort", "savesSettled", "data", "apiText", "postArchiveUrl", "bar", "setHtml", "EMAIL_FRAME_SANDBOX", "frame", "page", "openLinksInNewTab", "fitFrame", "h", "watch", "framedCopy", "e", "box", "msg", "ApiError", "describeError", "renderError", "mount", "r", "s", "message", "describeError", "TOOLBAR", "READONLY", "html", "ARIA_DISABLED", "renderEditor", "id", "root", "signal", "remount", "mount", "r", "s", "gone", "removed", "msg", "toast", "handOff", "setHtml", "data", "sendRead", "api", "home", "sendHome", "e", "isNotFound", "renderError", "movedOn", "post", "markdown", "scheduled", "locked", "revisions", "RevisionTracker", "toolbarHtml", "group", "i", "kind", "label", "icon", "ro", "webVersionUrl", "webVersionHash", "heldWhenFiresHtml", "infoTip", "EMAIL_FRAME_SANDBOX", "notConnectedWarning", "connectProviderLink", "subjectEl", "$", "slugEl", "ta", "toolbarEl", "previewFrame", "mdHl", "mdHlCode", "syncMdScroll", "paintMarkdown", "highlightMarkdown", "autoEl", "collect", "held", "applyHeld", "refreshDirty", "clearSubjectErr", "clearSubjectError", "validateSubject", "fieldError", "reflect", "markEdited", "saveDraft", "dirty", "savedSlug", "mine", "createAutosave", "onAbort", "tabs", "$$", "showTab", "name", "t", "on", "showPreview", "apiText", "doc", "openLinksInNewTab", "previewOpensWebVersion", "postArchiveUrl", "foundGone", "lockFoot", "nudgeTimer", "nudge", "wrapSel", "before", "after", "placeholder", "sel", "prefixLines", "prefix", "start", "out", "l", "applyFormat", "b", "k", "editorDirty", "saveFailed", "conflicted", "saveError", "retryDelay", "retryArmed", "retryRound", "saveBtn", "saveStatus", "DirtyTracker", "saving", "renderSaveStatus", "scheduleAutosave", "saveChain", "silent", "next", "doSaveDraft", "trackSaves", "fields", "sent", "slugShown", "body", "u", "handSet", "conflict", "conflictFromError", "showConflict", "named", "ApiError", "isNetworkFailure", "round", "at", "freshnessEl", "busy", "paintSaveBtn", "leaveFlush", "handle", "el", "friendlyAuthor", "a", "clearConflict", "who", "keep", "pollFreshness", "baseAtRequest", "fresh", "decision", "every", "readWhenShown", "current", "whenEl", "controls", "painted", "armed", "seenOpen", "reschedBtn", "cancelBtn", "offers", "can", "paintBanner", "now", "due", "markup", "scheduledBannerHtml", "key", "remadeEl", "dailyWritesEl", "paintConditions", "remade", "conditionOf", "dailyWrites", "follow", "from", "followSend", "send", "change", "poll", "read", "openRescheduleModal", "readSendsNow", "uploadAndInsert", "file", "fd", "image", "snippet", "composerBody", "wouldEdit", "f", "imgInput", "foot", "it", "warningsEl", "showWarnings", "ws", "lead", "notConnected", "providerNotConnected", "m", "modal", "notConnectedTestLine", "go", "to", "defaults", "hint", "addrs", "invalid", "parseAddresses", "notAddresses", "invalidAddressesMessage", "failed", "reasons", "lastWarnings", "addr", "notSentMessage", "withNoProviderNote", "scheduleBtn", "minStr", "toLocalInput", "earliestFireAt", "def", "scheduleView", "notConnectedScheduleLine", "dailyWritesLine", "box", "audience", "readAudience", "paintDailyWrites", "n", "DAILY_WRITES_SEND_RECIPIENTS", "dailyWritesSendWarning", "refused", "doSchedule", "isTooSoon", "tooSoonText", "fmt", "doSendNow", "wireSchedule", "showSendNow", "sendNowWhenText", "whoEl", "has", "lateStr", "untilStr", "hash", "docsCache", "docDescription", "doc", "tmp", "setHtml", "unsafeHtml", "raw", "leadsIn", "text", "MAX", "renderDocs", "slug", "root", "signal", "anchor", "roomShell", "html", "api", "e", "renderError", "$", "describeError", "mount", "r", "s", "landing", "docs", "renderDocPage", "decodeAnchor", "renderDocsIndex", "card", "d", "n", "desc", "sections", "sec", "inSection", "cards", "i", "blurb", "main", "repoUrl", "appState", "out", "href", "label", "short", "rail", "mobileMq", "syncFold", "fold", "mobile", "findFold", "foldFollowsLayout", "followLayout", "headingAnchor", "headingId", "at", "cur", "toast", "siblings", "pos", "prev", "next", "topLink", "dir", "topNav", "navEl", "mainEl", "seen", "h", "$$", "base", "h1", "head", "h2", "pager", "onPage", "onPageEl", "markActive", "id", "a", "ev", "target", "pre", "btn", "code", "ids", "first", "last", "line", "spy", "active", "heading", "authLines", "mode", "concretePath", "path", "curlCommand", "r", "origin", "required", "q", "search", "url", "lines", "body", "apiExample", "label", "value", "codeBlock", "highlightJson", "apiCurl", "r", "cmd", "curlCommand", "appState", "highlightCurl", "apiQueryHtml", "query", "rows", "q", "html", "apiAcceptsHtml", "accepts", "t", "apiPathHtml", "path", "concretePath", "part", "i", "apiRouteHtml", "hay", "routesIn", "g", "res", "apiResourceHtml", "sec", "routes", "apiSectionHtml", "apiNavHtml", "groups", "applyFilter", "navEl", "contentEl", "terms", "shown", "$$", "inTier", "inRes", "li", "hit", "w", "link", "tierLink", "sections", "lastShown", "$", "spyRail", "line", "marks", "el", "atBottom", "current", "tier", "links", "a", "target", "renderReference", "root", "signal", "setHtml", "roomShell", "ev", "api", "e", "renderError", "describeError", "mount", "icon", "filterEl", "clearEl", "spy", "filterTo", "wireCopy", "typing", "message", "describeError", "windowControls", "s", "buttons", "can", "html", "renderSent", "root", "signal", "state", "setHtml", "noEmailProvider", "notConnectedWarning", "connectProviderLink", "listToolbar", "stuckEl", "$", "schedEl", "activeEl", "listEl", "pagerEl", "tickCountdowns", "countdowns", "sendingRows", "liveRead", "scheduledReads", "listReads", "reported", "afterAct", "loadScheduled", "loadList", "readSendsNow", "renderActive", "live", "active", "needsOperator", "activeRowHtml", "card", "$$", "e", "renderStuck", "allRefused", "c", "conditionOf", "held", "providerNotConnected", "r", "refused", "notConnectedLine", "heldNames", "i", "heldCard", "heldSendsHtml", "wedged", "b", "w", "x", "openResolveModal", "patchDelivered", "cell", "tr", "deliveredCell", "paintLive", "sendsNow", "inQueue", "st", "sentSide", "onUpdate", "u", "id", "stageOf", "following", "follow", "reads", "ok", "followSends", "earlierCursor", "mount", "loadLive", "res", "api", "renderError", "mine", "sends", "schedCard", "countdownHtml", "fmt", "first", "rest", "toggle", "more", "show", "t", "openRescheduleModal", "busy", "toast", "data", "listQuery", "th", "wireSort", "renderPager", "wireToolbar", "message", "describeError", "renderSentRecord", "id", "root", "signal", "remount", "mount", "r", "s", "setHtml", "html", "data", "api", "e", "renderError", "send", "startWatch", "renderFrozenRecord", "PHASE_META", "PHASE_BLURB", "nextRetry", "retryAt", "wait", "fmtDuration", "phaseBlurb", "prog", "halt", "has", "phasePill", "phase", "m", "progressBar", "tone", "label", "value", "total", "sub", "p", "clampPct", "deliveryBar", "confirmed", "accepted", "acceptedPct", "confirmedPct", "WATCH_COUNTS", "watchCountsHtml", "counts", "c", "watchBodyHtml", "acceptedTotal", "rate", "eta", "dispatchSub", "failureCount", "providerText", "noEmailProvider", "providerNotConnected", "refused", "conditionOf", "unconnected", "stuck", "heldAdviceHtml", "fmt", "watchMetaHtml", "started", "watchHtml", "can", "wireWatchHeader", "rb", "count", "openResolveModal", "paintWatch", "pill", "meta", "body", "latest", "every", "followSend", "fresh", "outcomeTilesHtml", "outcomes", "tiles", "t", "outcomeReconHtml", "parts", "reconciled", "suppression", "sameOutcomes", "a", "b", "k", "deliveryOutcome", "DELIVERY_TABS", "deliveryEmpty", "dstate", "recordDeliveryQuery", "d", "term", "deliveryRowsHtml", "rows", "o", "outcome", "detail", "when", "th", "renderCanceled", "archive", "sentAt", "i", "viewBtn", "rowsEl", "$", "recPagerEl", "readerWaiting", "load", "background", "mine", "last", "keepFocus", "wireSort", "loadDeliveries", "renderPager", "paint", "had", "within", "sortKey", "pagerCls", "again", "viewButtons", "$$", "v", "x", "recSearch", "csvBtn", "busy", "csv", "apiFile", "url", "toast", "ApiError", "shown", "now", "outcomesOf", "control", "id", "el", "savebar", "msgEl", "saveBtn", "discardBtn", "token", "setMsg", "text", "setHtml", "html", "reveal", "detach", "attach", "onSave", "onDiscard", "saveLabel", "discardLabel", "signal", "mine", "busy", "alive", "dirty", "message", "describeError", "HIDDEN", "html", "chip", "kind", "label", "icon", "secHead", "title", "chipHtml", "extra", "monogram", "name", "fallback", "confirmationSection", "page", "state", "fromName", "html", "secHead", "chip", "icon", "monogram", "mountConfirmation", "root", "ceDefault", "fields", "$", "pv", "repaint", "c", "mastName", "hasLogo", "showMast", "setHtml", "onInput", "el", "editBody", "previewBody", "tabEdit", "tabPreview", "setMode", "edit", "PROVIDER_LABELS", "providerLabel", "provider", "providerValue", "d", "html", "connectProviderLink", "SIMULATION_LABELS", "bareAddress", "from", "m", "senderSection", "page", "fromName", "secHead", "chip", "icon", "monogram", "instanceSection", "archiveBase", "archiveIsDefault", "formatLead", "DEFAULT_MIN_LEAD_MS", "remakeRequired", "ApiError", "NOT_SAVED", "withRemakeConfirm", "attempt", "verb", "signal", "ack", "err", "required", "remakeRequired", "sends", "confirmRemake", "toast", "NOT_SAVED", "s", "n", "one", "noun", "resolve", "m", "modal", "html", "fmt", "settled", "done", "v", "cancel", "go", "obs", "REMAKE_TEMPLATE", "remakeIdentity", "action", "gerund", "inUseChip", "inUse", "icon", "title", "savedToast", "what", "remade", "escapeAttr", "s", "emailLogoHtml", "logoUrl", "name", "message", "describeError", "EMAIL_TEMPLATE_VARS", "EMAIL_TEMPLATE_EXAMPLES", "isExampleKey", "k", "EMAIL_TEMPLATE_SAMPLE_BODY", "RAW_TOKENS", "fillEmailTemplate", "template", "ctx", "_m", "key", "escapeHtml", "templateSampleCtx", "id", "name", "emailLogoHtml", "sampleEmailHtml", "TEMPLATE_FRAME_DOC", "mountSampleEmailPreview", "iframe", "getTemplate", "getIdentity", "EMAIL_FRAME_SANDBOX", "ready", "size", "h", "repaint", "doc", "slot", "setHtml", "unsafeHtml", "openLinksInNewTab", "templateVarsHtml", "html", "g", "v", "renderTemplate", "root", "signal", "bodyEl", "$", "data", "api", "e", "renderError", "mount", "s", "d", "p", "identity", "publicationName", "templateBaseline", "defaultRecipients", "inUseChip", "icon", "copyFace", "tplEditor", "preview", "tplMsgsEl", "tplTestEl", "tplTestLbl", "isDirty", "tplHl", "tplHlCode", "tplGutter", "paintEditor", "src", "highlightTemplate", "lines", "i", "syncScroll", "reqBodyEl", "reqUnsubEl", "paintReq", "bar", "savebar", "onSaveTemplate", "revertTemplate", "refreshDirty", "showWarnings", "msgs", "m", "saveTemplate", "value", "r", "withRemakeConfirm", "ack", "REMAKE_TEMPLATE", "appState", "saved", "toast", "savedToast", "err", "loadExample", "ex", "exBtn", "openMenu", "frameEl", "wb", "$$", "o", "editorWrap", "lineNumsBtn", "setLineNums", "on", "lineNumsOn", "copyAllBtn", "copyWith", "notConnected", "providerNotConnected", "dirty", "modal", "notConnectedTestLine", "go", "to", "hint", "busy", "addrs", "invalid", "parseAddresses", "notAddresses", "invalidAddressesMessage", "notSentMessage", "withNoProviderNote", "templateSection", "page", "html", "secHead", "chip", "inUseChip", "mountTemplate", "root", "template", "preview", "mountSampleEmailPreview", "$", "recipientsSection", "mountRecipients", "state", "chips", "input", "render", "setHtml", "addr", "i", "icon", "ev", "b", "add", "v", "normalizeEmail", "isValidEmail", "fieldError", "e", "centerSquare", "width", "height", "side", "UnreadableImage", "squareImage", "file", "size", "bitmap", "x", "y", "canvas", "ctx", "resolve", "reject", "b", "identityNote", "inUse", "shown", "reach", "wait", "fmt", "LOGO_SIZE", "identitySection", "page", "state", "fromDisplay", "fromName", "addressOpen", "html", "secHead", "chip", "HIDDEN", "icon", "mountIdentity", "root", "signal", "nameEl", "$", "taglineEl", "addressEl", "onInput", "templatePrintsAddress", "addrAdd", "addrField", "addrWarn", "applyAddressUi", "logoInput", "logoTile", "logoPh", "logoReplace", "logoRemove", "logoShape", "checkLogoShape", "url", "img", "applyLogoUi", "has", "e", "logoError", "logoErrorText", "showLogoError", "msg", "logoSaved", "r", "done", "toast", "savedToast", "logoFailed", "err", "message", "file", "squared", "squareImage", "cropped", "withRemakeConfirm", "ack", "fd", "q", "api", "remakeIdentity", "busy", "notifyChannelLabel", "channel", "provider", "providerLabel", "NOTIFICATION_KINDS", "aboutNotification", "n", "notificationStatusHtml", "status", "lastSent", "lastFailure", "html", "fmt", "icon", "notificationsSection", "page", "d", "secHead", "chip", "mountNotifications", "root", "toEl", "$", "test", "testHint", "refreshTest", "saved", "reason", "busy", "r", "api", "toast", "setHtml", "err", "message", "SUBSCRIBE", "CONFIRM", "UNSUBSCRIBE", "READER_JOURNEYS", "readerPagePath", "journey", "state", "JOURNEY_NOTES", "html", "JOURNEY_ICONS", "PEER_DOT", "outlineRow", "j", "s", "mark", "outline", "READER_JOURNEYS", "states", "steps", "icon", "picker", "step", "n", "readerPagesSection", "head", "readerPageAddress", "at", "d", "mountReaderPages", "root", "deployment", "signal", "nav", "$", "select", "widthEl", "stage", "frame", "errorEl", "journey", "state", "width", "seq", "watch", "onAbort", "paintChoice", "b", "$$", "on", "g", "setHtml", "fitHeight", "load", "mine", "doc", "apiText", "readerPagePath", "framedCopy", "e", "describeError", "choose", "journeyId", "stateId", "x", "page", "w", "o", "embedHeading", "name", "buildEmbed", "mode", "appOrigin", "heading", "escapeHtml", "action", "embedPreview", "html", "appOriginOf", "page", "pageLine", "way", "glyph", "title", "line", "body", "control", "icon", "subscribeSection", "subscribeUrl", "secHead", "chip", "connectProviderLink", "copyButton", "embedCopy", "code", "copyFace", "mountSubscribe", "root", "codeEl", "$", "copyEl", "previewEl", "lineEl", "rebuild", "setHtml", "modeButtons", "$$", "b", "o", "wireCopy", "shownName", "group", "id", "sections", "html", "baselineOf", "state", "sameList", "a", "b", "x", "i", "sameCopy", "renderSettings", "root", "signal", "setHtml", "body", "$", "data", "api", "e", "renderError", "message", "mount", "s", "p", "ce", "baseline", "fromDisplay", "fromDisplayName", "isDirty", "handles", "bar", "savebar", "saveSettings", "discardSettings", "refreshReaderPages", "page", "h", "settings", "appState", "renderSidebarBrand", "identitySection", "subscribeSection", "confirmationSection", "readerPagesSection", "secHead", "chip", "templateSection", "recipientsSection", "notificationsSection", "senderSection", "instanceSection", "mountIdentity", "mountSubscribe", "mountConfirmation", "mountTemplate", "mountRecipients", "mountNotifications", "mountReaderPages", "clearFieldMark", "unmarkField", "payload", "r", "withRemakeConfirm", "ack", "remakeIdentity", "ns", "toast", "savedToast", "err", "input", "ApiError", "fieldInput", "fieldError", "FIELD_INPUTS", "field", "sel", "el", "SUB_STATUSES", "renderSubscribers", "initialFilter", "root", "signal", "state", "setHtml", "html", "listToolbar", "listEl", "$", "pagerEl", "load", "data", "api", "listQuery", "c", "infoTip", "renderSubTable", "renderPager", "e", "renderError", "describeError", "addSubscriberModal", "wireToolbar", "SUPPRESSION_LABELS", "SUPPRESSION_TIPS", "suppressionFlag", "s", "reason", "label", "base", "tip", "rows", "reload", "th", "badge", "fmt", "wireSort", "b", "$$", "row", "r", "openMenu", "confirmUnsubscribe", "LEAVE_MSG", "mountedHash", "route", "hash", "view", "arg", "sub", "isDashboardRoute", "webPost", "webPostId", "toolMode", "a", "setNavOpen", "mount", "root", "signal", "renderEditor", "renderWebVersion", "renderDrafts", "renderSubscribers", "renderSentRecord", "renderSent", "renderTemplate", "renderSettings", "renderReference", "renderDocs", "renderDashboard", "adoptLinkedRoute", "linked", "dashboardRouteFromSearch", "url", "installRouter", "mounted", "save", "boot", "installShell", "installTooltips", "installRoomBar", "installRouter", "session", "probeSession", "appState", "renderIdentity", "api", "renderSidebarBrand", "adoptLinkedRoute", "route"]
|
|
7
7
|
}
|