@kurtbruns/kestrel 1.19.0 → 1.21.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +69 -1
  2. package/bin/kestrel.mjs +1 -1
  3. package/cli/seed.mjs +2 -8
  4. package/dist/public/dashboard/app.js +307 -306
  5. package/dist/public/dashboard/app.js.map +4 -4
  6. package/dist/public/dashboard/index.html +2 -2
  7. package/dist/public/dashboard/styles.css +90 -51
  8. package/dist/worker/index.js +890 -559
  9. package/docs/get-started/02-deploy.md +3 -3
  10. package/docs/get-started/03-access.md +8 -4
  11. package/docs/get-started/04-aws-account.md +2 -0
  12. package/docs/get-started/05-ses.md +4 -2
  13. package/docs/get-started/07-verify.md +8 -6
  14. package/docs/get-started/08-go-live.md +12 -18
  15. package/docs/guides/01-allow-claude.md +7 -2
  16. package/docs/guides/03-notifications.md +17 -13
  17. package/docs/guides/07-upgrade.md +16 -2
  18. package/docs/publishing/03-name-your-publication.md +8 -0
  19. package/docs/publishing/04-write-a-post.md +2 -2
  20. package/docs/publishing/06-troubleshooting.md +52 -26
  21. package/docs/publishing/11-email-template.md +1 -1
  22. package/docs/publishing/README.md +1 -1
  23. package/docs/reference/02-sending-domain-dns.md +3 -3
  24. package/migrations/0021_notifications_outage_receipts.sql +46 -0
  25. package/package.json +10 -10
  26. package/packages/kestrel/lib/api.mjs +19 -12
  27. package/packages/kestrel/lib/login.mjs +2 -2
  28. package/packages/kestrel/lib/media-types.mjs +34 -0
  29. package/packages/kestrel/lib/oauth.mjs +19 -2
  30. package/packages/kestrel/lib/openapi.mjs +1 -1
  31. package/template/README.md +1 -1
  32. package/template/claude/CLAUDE.md +1 -1
  33. package/template/claude/skills/upgrade/SKILL.md +1 -0
@@ -1,272 +1,273 @@
1
1
  // GENERATED by scripts/build-client.mjs from client/ — edit the source there, never this file.
2
- "use strict";(()=>{function Zn(e){if(!e||typeof e!="object")return null;let t=e;return t.error!=="unauthorized"?null:t.reason==="access_not_configured"||t.reason==="access_unidentified"?t.reason:null}function vo(e){let t=document.getElementById(e);if(!t)throw new Error(`the admin page has no #${e}`);return t}var pe=vo("app"),yo=vo("identity"),wo=vo("toasts"),Yi=document.getElementById("navToggle"),bo=document.getElementById("navScrim");function dn(e){document.body.classList.toggle("nav-open",e),Yi?.setAttribute("aria-expanded",e?"true":"false"),bo&&(bo.hidden=!e)}var cn=document.getElementById("navCollapse"),go=null;function Dc(){return window.innerWidth<540?"full":go==="collapsed"?"rail":go==="expanded"?"full":window.innerWidth<=1024?"rail":"full"}function fo(){let e=Dc();if(document.documentElement.dataset.nav=e,!cn)return;let t=e!=="rail",n=t?"Collapse sidebar":"Expand sidebar";cn.setAttribute("aria-expanded",t?"true":"false"),cn.setAttribute("aria-label",n),cn.setAttribute("title",n)}var ho=0,zi=0;function Zi(){Yi?.addEventListener("click",()=>dn(!document.body.classList.contains("nav-open"))),bo?.addEventListener("click",()=>dn(!1)),document.querySelector(".sidebar")?.addEventListener("click",e=>{e.target instanceof Element&&e.target.closest("a")&&dn(!1)}),cn?.addEventListener("click",()=>{go=document.documentElement.dataset.nav!=="rail"?"collapsed":"expanded",fo()}),window.addEventListener("resize",()=>{document.body.classList.add("nav-resizing"),clearTimeout(zi),zi=setTimeout(()=>{document.body.classList.remove("nav-resizing")},200),!ho&&(ho=requestAnimationFrame(()=>{ho=0,fo()}))}),fo()}var Jn=null,Qn=null,Xn="Kestrel";function ie(e,t){e.aborted||(document.title=t.trim()?`${t} - ${Xn}`:Xn)}function ee(e){Jn?.abort(),Qn=null;let t=new AbortController;Jn=t,document.title=Xn;let n=document.createElement("div");n.className="view",pe.replaceChildren(n);let s;try{s=Promise.resolve(e(n,t.signal))}catch(i){s=Promise.reject(i)}return s.then(i=>{!t.signal.aborted&&i&&(Qn=i)},i=>{if(!t.signal.aborted)throw i})}function It(){Jn?.abort(),Jn=null,Qn=null,document.title=Xn,pe.replaceChildren()}function es(){return Qn}function bt(e,t,n){if(n.aborted)return;let s=setInterval(t,e);n.addEventListener("abort",()=>clearInterval(s),{once:!0})}function gt(e,t,n){if(n.aborted)return;let s=()=>clearTimeout(i),i=setTimeout(()=>{n.removeEventListener("abort",s),t()},Math.max(0,e-Date.now()));n.addEventListener("abort",s,{once:!0})}function Ji(e,t,n){let s=()=>{if(n.aborted)return;let i=()=>clearTimeout(r),r=setTimeout(async()=>{n.removeEventListener("abort",i);let a=!0;try{a=await t()??!0}catch(l){Bc(l)||reportError(l)}a!==!1&&s()},e);n.addEventListener("abort",i,{once:!0})};s()}function Nt(e,t){if(e.aborted){t();return}e.addEventListener("abort",t,{once:!0})}function Bc(e){return e instanceof Error&&(e.name==="AbortError"||e.name==="ApiError")}function c(e,t=document){let n=t.querySelector(e);if(!n)throw new Error(`no element matches ${e}`);return n}function N(e,t=document){return[...t.querySelectorAll(e)]}var Uc={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};function X(e){return String(e).replace(/[&<>"']/g,t=>Uc[t]??t)}var vt=class{markup;constructor(t){this.markup=t,Object.freeze(this)}toString(){return this.markup}};function Xi(e){return e instanceof vt?e.markup:e==null||e===!1?"":Array.isArray(e)?e.map(Xi).join(""):X(e)}var Fc=/=\s*$/;function o(e,...t){let n=Qi(e,0);for(let s=0;s<t.length;s++){let i=t[s];if(!(i instanceof vt)&&Fc.test(n))throw new Error("html: an interpolation in an unquoted attribute value cannot be made safe; quote it");n+=Xi(i)+Qi(e,s+1)}return new vt(n)}function Qi(e,t){let n=e[t];if(n===void 0)throw new Error(`html: invalid escape sequence in template text near ${JSON.stringify(e.raw[t])}`);return n}function z(e){return new vt(e)}function h(e,t){if(!(t instanceof vt))throw new Error("setHtml: expected Html; build it with html`\u2026` or vouch for it with unsafeHtml()");e.innerHTML=t.markup}var qc="https://getkestrel.dev/docs/access/",jc="/cdn-cgi/access/logout";function ts(e){if(pe.querySelector(".access-wall"))return;It(),document.body.classList.add("signed-out");let t=o`<p class="hint"><a href="${qc}" target="_blank" rel="noopener">Lock the dashboard with Access <span aria-hidden="true">↗</span><span class="visually-hidden"> (opens in a new tab)</span></a></p>`;e==="access_unidentified"?(h(pe,o`<div class="card auth-wall access-wall" role="alert"><h2 tabindex="-1">Access let you in, but didn’t say who you are</h2><p>Cloudflare Access sent neither an email address nor a service token, and Kestrel needs one of them to tell who is asking, so it doesn’t let this sign-in in. Some other kind of Access policy, or a login method that gives no email, let it through.</p><p>Kestrel accepts two kinds of Access policy: an Allow policy on people’s emails, and a Service Auth policy on a service token. Sign out, then sign in through one of them.</p>${t}<button id="access-signout" class="primary">Sign out</button></div>`),c("#access-signout",pe).onclick=()=>location.assign(jc)):(h(pe,o`<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’t tell who is asking, so it lets no one in. On a new deployment, that’s expected. Your public pages work as usual.</p>${t}<button id="access-reload" class="primary">Reload</button></div>`),c("#access-reload",pe).onclick=()=>location.reload()),c(".access-wall h2",pe).focus()}var er="Kestrel-Client",un="editor",ns="claude-code",ss="claude";function os(e){if(!e||typeof e!="object")return null;let t=e;return t.error==="schema_behind"&&typeof t.message=="string"?{message:t.message}:t.error!=="invalid_config"||typeof t.variable!="string"?null:{variable:t.variable,message:typeof t.message=="string"?t.message:`${t.variable} is refused`}}var Wc="https://getkestrel.dev/docs/configuration/",Vc={PROVIDER:"vars",ALLOW_FAKE_PROVIDER:"vars",APP_ORIGIN:"vars",AWS_REGION:"vars",FROM_ADDRESS:"vars",SENDING_DOMAIN:"vars",ARCHIVE_BASE_PATH:"vars",SES_MAX_SEND_RATE:"vars",SUBREQUEST_BUDGET:"vars",MIN_LEAD_SECONDS:"vars",ARCHIVE_ORIGIN:"vars",MEDIA_PUBLIC_BASE:"vars",NOTIFY_FROM:"vars",ACCESS_TEAM_DOMAIN:"secrets",ACCESS_AUD:"secrets",ACCESS_ALLOWED_EMAILS:"secrets",AWS_ACCESS_KEY_ID:"secrets",AWS_SECRET_ACCESS_KEY:"secrets",SNS_TOPIC_ARN:"secrets",SES_CONFIGURATION_SET:"secrets",SIMULATE_SENDS:"what-the-app-refuses"},Kc="what-the-app-refuses";function Gc(e){return`${Wc}#${Vc[e]??Kc}`}function is(e){if(pe.querySelector(".config-wall"))return;It(),document.body.classList.add("signed-out");let{variable:t}=e,n=t?"A setting needs fixing":"The database needs updating",s=t?o`<p class="hint"><a href="${Gc(t)}" target="_blank" rel="noopener">${t} in the Configuration reference <span aria-hidden="true">↗</span><span class="visually-hidden"> (opens in a new tab)</span></a></p>`:"";h(pe,o`<div class="card auth-wall config-wall" role="alert"><h2 tabindex="-1">${n}</h2><p>Kestrel won’t run until it’s fixed, and nothing is sent in the meantime.</p><p class="config-wall-problem">${e.message}</p>${s}<button id="config-reload" class="primary">Reload</button></div>`),c("#config-reload",pe).onclick=()=>location.reload(),c(".config-wall h2",pe).focus()}var rs="kestrel_token",Y={token:localStorage.getItem(rs)||"",session:null,appConfig:null};function sr(e){Y.token=(e||"").trim();try{Y.token?localStorage.setItem(rs,Y.token):localStorage.removeItem(rs)}catch{}}function So(){return{[er]:un,...Y.token?{Authorization:`Bearer ${Y.token}`}:{}}}function or(){let e=Y.session;if(e?.auth.mode==="access"){let t=e.principal,n=t.email||(t.kind==="service"?"Service token":"Signed in");h(yo,o`<span class="who" title="${n}">${n}</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>`)}else h(yo,o`<span class="who dev" title="Local dev: sign-in is skipped on localhost">Local dev</span>`)}function pn(){pe.querySelector(".reauth-wall")||(It(),document.body.classList.add("signed-out"),h(pe,o`<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>`),c("#reauth",pe).onclick=()=>location.reload(),c(".reauth-wall h2",pe).focus())}async function tr(){try{let e=await fetch("/api/dev/token?kind=human");if(e.ok)return sr((await e.json()).token),!0}catch{}return!1}async function nr(){try{return await fetch("/api/whoami",{headers:So(),redirect:"manual"})}catch{return null}}function zc(e){let t=e;return typeof t?.principal=="object"&&t.principal!==null&&typeof t.auth=="object"}async function ir(){Y.token||await tr();let e=await nr();!e?.ok&&Y.token&&(sr(""),await tr()&&(e=await nr()));let t=e?await e.json().catch(()=>null):null;if(e?.ok&&zc(t))return t;let n=os(t),s=Zn(t);return n?is(n):s?ts(s):pn(),null}var te=class extends Error{constructor(n,s,i=null){super(n);this.status=s;this.data=i;this.name="ApiError"}status;data;get code(){let n=this.data;return typeof n?.error=="string"?n.error:null}get field(){let n=this.data;return typeof n?.field=="string"?n.field:null}},Yc="Not authorized. Sign in again.",Zc=e=>/^[a-z_]+$/i.test(e.trim()),Jc=/^(Failed to fetch|NetworkError when attempting to fetch resource\.?|Load failed|The network connection was lost\.?|The Internet connection appears to be offline\.?)$/;function $o(e){return e instanceof TypeError&&Jc.test(e.message)}function V(e){if(e instanceof te){let t=e.data?.message;return typeof t=="string"&&!Zc(t)?t:e.status===401?e.message:e.status===404?"It isn\u2019t there any more. It may have been deleted elsewhere.":e.status>=500?"Something went wrong on the server. Try again in a moment.":e.status===429?"Too many requests at once. Try again in a moment.":"The server couldn\u2019t do that. Reload the page and try again."}return $o(e)?"Couldn\u2019t reach Kestrel. Check your connection and try again.":e instanceof Error?e.message:String(e)}async function xo(e,t){let n={...So(),...t.headers??{}},s=t.body;t.json!==void 0&&(n["content-type"]="application/json",s=JSON.stringify(t.json));let i=await fetch(e,{method:t.method??"GET",headers:n,body:s,signal:t.signal});if(i.status===401){let r=await i.json().catch(()=>null),a=Zn(r);throw a?ts(a):pn(),new te(Yc,401,r)}return i}function Qc(e){if(e&&typeof e=="object"){let t=e;if(typeof t.message=="string")return t.message;if(typeof t.error=="string")return t.error}}async function Eo(e){let t=await e.text(),n=null;try{n=t?JSON.parse(t):null}catch{}let s=os(n);s&&is(s);let i=e.statusText||`Request failed (HTTP ${e.status})`;return new te(Qc(n)??i,e.status,n)}async function L(e,t={}){let n=await xo(e,t);if(!n.ok)throw await Eo(n);let s=await n.text();return s?JSON.parse(s):null}async function Ot(e,t={}){let n=await xo(e,t);if(!n.ok)throw await Eo(n);return n.text()}async function as(e,t={}){let n=await xo(e,t);if(!n.ok)throw await Eo(n);let s=n.headers.get("content-disposition")??"",i=/filename="([^"]+)"/.exec(s)?.[1]??null;return{text:await n.text(),filename:i}}function ko(e){let t=e.trim(),n=t.lastIndexOf("<");if(n<0||!t.endsWith(">"))return"";let s=t.slice(0,n).trim();return s.length>=2&&s.startsWith('"')&&s.endsWith('"')?s.slice(1,-1).replace(/\\(.)/g,"$1").trim():s}function mn(e,t){return e||ko(t)}function _o(e){let t=e?.settings.publication;return{name:mn(t?.name||"",e?.deployment.from_address??"")||"Your publication",tagline:t?.tagline||"",logoUrl:t?.logo_url||null}}function ls(){let e=_o(Y.appConfig),t=document.getElementById("brandName"),n=document.getElementById("brandTagline"),s=document.getElementById("brandLogo");t&&(t.textContent=e.name),n&&(n.textContent=e.tagline,n.hidden=!e.tagline),s&&(e.logoUrl?(h(s,o`<img src="${e.logoUrl}" alt="">`),s.classList.remove("brand-logo-placeholder")):(s.textContent=(e.name.trim()[0]||"K").toUpperCase(),s.classList.add("brand-logo-placeholder")))}function rr(e,t,n){return`${e}${t}/${n}`}var Xc=["content","invalid_request"];function To(e){return e!==null&&Xc.includes(e)}var Lo={step:"Connect Amazon SES",guide:" (the setup guide walks through it)",after:", then deploy."};var ar="Amazon SES is still in the sandbox, so when this send fires it reaches only addresses verified in SES. It waits for the rest, and other sends wait behind it, until AWS grants production access, then finishes on its own.";var Nf=1800*1e3,cs=300*1e3,Of=60*1e3,Df=1440*60*1e3;function Dt(e){let t=Math.round(e/1e3);if(t%60===0){let n=t/60;return`${n} minute${n===1?"":"s"}`}return`${t} seconds`}function lr(e){return`Pick a time at least ${Dt(e)} from now, so there\u2019s time to cancel.`}var Bf=10080*60*1e3,Uf=3600*1e3;function cr(){let e=Y.appConfig?.deployment.build;if(!e?.version)return null;let t=e.tag_url?o`<a class="build-link" href="${e.tag_url}" target="_blank" rel="noopener">v${e.version}</a>`:o`v${e.version}`,n=e.commit_url?o`<a class="build-link" href="${e.commit_url}" target="_blank" rel="noopener">${e.sha}</a>`:o`${e.sha}`;return{version:t,sha:n}}function ds(){return Y.appConfig?.deployment.min_lead_ms??cs}function us(){return lr(ds())}function dr(){let e=ds();if(e%6e4===0){let t=e/6e4;return`${t} to ${t+1} minutes`}return`${Dt(e)} to ${Dt(e+6e4)}`}function ps(e,t=Date.now()){return e<t+ds()}function ms(e=Date.now()){return new Date(e+ds()+6e4)}function fn(){return Y.appConfig?.deployment.provider==="fake"}function ge(){return Y.appConfig?.deployment.provider==="none"}function ed(){return Y.appConfig?.deployment.free_plan_limits===!0}function td(){return ge()&&Y.appConfig?.deployment.not_connected_preview===!0}var Ro="#/docs/ses";function Ae(e){let t=e?o` class="${e}"`:null;return o`<a${t} href="${Ro}">${Lo.step}</a>`}function Co(){return td()?o`Restart <code>npm run dev</code> without <code>SIMULATE_SENDS=not-connected</code> to send.`:o`${Ae()}${Lo.after}`}function fs(e,t){let n=t===1;return o`No email provider connected, so nothing can be sent and ${e} ${n?"is":"are"} held, with no one marked unsent; ${n?"it goes out at its":"they go out at their"} first retry once one is connected. ${Co()}`}function hs(e,t){return ge()?o`<div class="health amber ${e}" role="status"><span class="health-dot">⚠️</span><div><span>${t}</span></div></div>`:null}function bs(e){if(!ge())return null;let t="Will be held: no email provider is connected.";return e==="card"?o`<div class="sched-held">${t}</div>`:o`<div class="sched-note sched-note-held"><span class="sched-note-mark" aria-hidden="true">⚠️</span><span class="sched-note-msg">${t}</span></div>`}function gs(e,t){return ge()?o`<p class="hint not-connected">No email provider connected, so ${e}. ${Ae()}${t}.</p>`:null}function vs(){return gs("a test can\u2019t be sent yet"," to send one")}function ur(){return gs("a confirmation can\u2019t be sent yet"," to add subscribers")}function Ao(){return ed()&&!ge()?o`<p class="hint daily-writes" id="schDailyWrites" role="status" hidden></p>`:null}function Ho(){return ge()?null:o`<p class="hint daily-writes" id="schSandbox" role="status" hidden></p>`}function pr(){return gs("this can\u2019t be scheduled yet"," to schedule it")}function mr(){return gs("this send is held when it fires, until one is"," to send it")}function Bt(e){return fn()?`${e} (local stand-in: nothing is delivered)`:e}function fr(e){let t=Y.appConfig?.deployment;return t?rr(t.archive_origin,t.archive_base_path,e):null}var hr=`<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>
3
- `;var br=`<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>
4
- `;var gr=`<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>
5
- `;var vr=`<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>
6
- `;var yr=`<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>
7
- `;var wr=`<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>
8
- `;var Sr=`<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>
9
- `;var $r=`<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>
10
- `;var xr=`<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="8" y="8" width="13" height="13" rx="2"/><path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3"/></svg>
11
- `;var Er=`<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>
12
- `;var kr=`<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>
13
- `;var _r=`<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>
14
- `;var Tr=`<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>
15
- `;var Lr=`<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>
16
- `;var Rr=`<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>
17
- `;var Cr=`<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>
18
- `;var Ar=`<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>
19
- `;var Hr=`<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>
20
- `;var Pr=`<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>
21
- `;var Mr=`<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>
22
- `;var Ir=`<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>
23
- `;var Nr=`<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>
24
- `;var Or=`<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>
25
- `;var Dr=`<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>
26
- `;var Br=`<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>
27
- `;var Ur=`<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>
28
- `;var Fr=`<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>
29
- `;var qr=`<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>
30
- `;var jr=`<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>
31
- `;var Wr=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" aria-hidden="true"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>
32
- `;var Vr=`<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>
33
- `;var Kr=`<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>
34
- `;var Gr=`<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>
35
- `;var Id={article:z(hr),bold:z(br),check:z(yr),code:z($r),copyout:z(xr),editable:z(kr),heading:z(_r),indent:z(Tr),info:z(Lr),"info-filled":z(Rr),italic:z(Cr),kestrel:z(Ar),lines:z(Hr),link:z(Pr),ol:z(Ir),paperclip:z(Nr),plus:z(Dr),preview:z(Br),quote:z(Ur),readonly:z(Fr),search:z(qr),send:z(jr),settings:z(Wr),ul:z(Vr),upload:z(Kr),book:z(gr),branch:z(vr),"circle-check":z(wr),"circle-minus":z(Sr),desktop:z(Er),mail:z(Mr),phone:z(Or),x:z(Gr)};function O(e){return Id[e]}function Ut(e,t,n){let s=(v,f)=>e===v?o`<a href="#/${v}" data-room="${v}" data-text="${f}" aria-current="page">${f}</a>`:o`<a href="#/${v}" data-room="${v}" data-text="${f}">${f}</a>`,i=cr(),r=Y.appConfig?.deployment.build.build_time,a=r?`Built ${new Date(r).toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}`:null,l=i?o`${i.version} <span aria-hidden="true">·</span> ${i.sha}`:null,d=l?a?o`<div class="rail-foot" title="${a}">${l}</div>`:o`<div class="rail-foot">${l}</div>`:null,u=l?a?o`<footer class="room-foot" title="${a}">${l}</footer>`:o`<footer class="room-foot">${l}</footer>`:null,m=t==null?o`<div class="room-body norail"><div class="room-main">${n}</div></div>`:o`<div class="room-body"><nav class="room-rail" aria-label="Contents"><div class="rail-inner">${t}${d}</div></nav><div class="room-main">${n}</div></div>`;return o`<div class="room">
2
+ "use strict";(()=>{function ts(e){if(!e||typeof e!="object")return null;let t=e;return t.error!=="unauthorized"?null:t.reason==="access_not_configured"||t.reason==="access_unidentified"?t.reason:null}function _o(e){let t=document.getElementById(e);if(!t)throw new Error(`the admin page has no #${e}`);return t}var ue=_o("app"),To=_o("identity"),Lo=_o("toasts"),nr=document.getElementById("navToggle"),xo=document.getElementById("navScrim");function Ot(e){document.body.classList.toggle("nav-open",e),nr?.setAttribute("aria-expanded",e?"true":"false"),xo&&(xo.hidden=!e)}var pn=document.getElementById("navCollapse"),ko=null;function Yc(){return window.innerWidth<540?"full":ko==="collapsed"?"rail":ko==="expanded"?"full":window.innerWidth<=1024?"rail":"full"}function $o(){let e=Yc();if(document.documentElement.dataset.nav=e,!pn)return;let t=e!=="rail",n=t?"Collapse sidebar":"Expand sidebar";pn.setAttribute("aria-expanded",t?"true":"false"),pn.setAttribute("aria-label",n),pn.setAttribute("title",n)}var Eo=0,tr=0;function sr(){nr?.addEventListener("click",()=>Ot(!document.body.classList.contains("nav-open"))),xo?.addEventListener("click",()=>Ot(!1)),document.querySelector(".sidebar")?.addEventListener("click",e=>{e.target instanceof Element&&e.target.closest("a")&&Ot(!1)}),pn?.addEventListener("click",()=>{ko=document.documentElement.dataset.nav!=="rail"?"collapsed":"expanded",$o()}),window.addEventListener("resize",()=>{document.body.classList.add("nav-resizing"),clearTimeout(tr),tr=setTimeout(()=>{document.body.classList.remove("nav-resizing")},200),!Eo&&(Eo=requestAnimationFrame(()=>{Eo=0,$o()}))}),$o()}var ns=null,ss=null,os="Kestrel";function ie(e,t){e.aborted||(document.title=t.trim()?`${t} - ${os}`:os)}function ne(e){ns?.abort(),ss=null;let t=new AbortController;ns=t,document.title=os;let n=document.createElement("div");n.className="view",ue.replaceChildren(n);let s;try{s=Promise.resolve(e(n,t.signal))}catch(i){s=Promise.reject(i)}return s.then(i=>{!t.signal.aborted&&i&&(ss=i)},i=>{if(!t.signal.aborted)throw i})}function Dt(){ns?.abort(),ns=null,ss=null,document.title=os,ue.replaceChildren()}function mn(){return ss}function gt(e,t,n){if(n.aborted)return;let s=setInterval(t,e);n.addEventListener("abort",()=>clearInterval(s),{once:!0})}function vt(e,t,n){if(n.aborted)return;let s=()=>clearTimeout(i),i=setTimeout(()=>{n.removeEventListener("abort",s),t()},Math.max(0,e-Date.now()));n.addEventListener("abort",s,{once:!0})}function or(e,t,n){let s=()=>{if(n.aborted)return;let i=()=>clearTimeout(r),r=setTimeout(async()=>{n.removeEventListener("abort",i);let a=!0;try{a=await t()??!0}catch(l){Zc(l)||reportError(l)}a!==!1&&s()},e);n.addEventListener("abort",i,{once:!0})};s()}function Bt(e,t){if(e.aborted){t();return}e.addEventListener("abort",t,{once:!0})}function Zc(e){return e instanceof Error&&(e.name==="AbortError"||e.name==="ApiError")}function c(e,t=document){let n=t.querySelector(e);if(!n)throw new Error(`no element matches ${e}`);return n}function D(e,t=document){return[...t.querySelectorAll(e)]}var Jc={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};function te(e){return String(e).replace(/[&<>"']/g,t=>Jc[t]??t)}var yt=class{markup;constructor(t){this.markup=t,Object.freeze(this)}toString(){return this.markup}};function rr(e){return e instanceof yt?e.markup:e==null||e===!1?"":Array.isArray(e)?e.map(rr).join(""):te(e)}var Qc=/=\s*$/;function o(e,...t){let n=ir(e,0);for(let s=0;s<t.length;s++){let i=t[s];if(!(i instanceof yt)&&Qc.test(n))throw new Error("html: an interpolation in an unquoted attribute value cannot be made safe; quote it");n+=rr(i)+ir(e,s+1)}return new yt(n)}function ir(e,t){let n=e[t];if(n===void 0)throw new Error(`html: invalid escape sequence in template text near ${JSON.stringify(e.raw[t])}`);return n}function Y(e){return new yt(e)}function f(e,t){if(!(t instanceof yt))throw new Error("setHtml: expected Html; build it with html`\u2026` or vouch for it with unsafeHtml()");e.innerHTML=t.markup}var Xc="https://getkestrel.dev/docs/access/",ed="/cdn-cgi/access/logout";function is(e){if(ue.querySelector(".access-wall"))return;Dt(),document.body.classList.add("signed-out");let t=o`<p class="hint"><a href="${Xc}" target="_blank" rel="noopener">Lock the dashboard with Access <span aria-hidden="true">↗</span><span class="visually-hidden"> (opens in a new tab)</span></a></p>`;e==="access_unidentified"?(f(ue,o`<div class="card auth-wall access-wall" role="alert"><h2 tabindex="-1">Access let you in, but didn’t say who you are</h2><p>Cloudflare Access sent neither an email address nor a service token, and Kestrel needs one of them to tell who is asking, so it doesn’t let this sign-in in. Some other kind of Access policy, or a login method that gives no email, let it through.</p><p>Kestrel accepts two kinds of Access policy: an Allow policy on people’s emails, and a Service Auth policy on a service token. Sign out, then sign in through one of them.</p>${t}<button id="access-signout" class="primary">Sign out</button></div>`),c("#access-signout",ue).onclick=()=>location.assign(ed)):(f(ue,o`<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’t tell who is asking, so it lets no one in. On a new deployment, that’s expected. Your public pages work as usual.</p>${t}<button id="access-reload" class="primary">Reload</button></div>`),c("#access-reload",ue).onclick=()=>location.reload()),c(".access-wall h2",ue).focus()}var ar="Kestrel-Client",fn="editor",rs="claude-code",lr="kestrel-cli",as="claude";function ls(e){if(!e||typeof e!="object")return null;let t=e;return t.error==="schema_behind"&&typeof t.message=="string"?{message:t.message}:t.error!=="invalid_config"||typeof t.variable!="string"?null:{variable:t.variable,message:typeof t.message=="string"?t.message:`${t.variable} is refused`}}var td="https://getkestrel.dev/docs/configuration/",nd={PROVIDER:"vars",ALLOW_FAKE_PROVIDER:"vars",APP_ORIGIN:"vars",AWS_REGION:"vars",FROM_ADDRESS:"vars",SENDING_DOMAIN:"vars",ARCHIVE_BASE_PATH:"vars",SES_MAX_SEND_RATE:"vars",SUBREQUEST_BUDGET:"vars",MIN_LEAD_SECONDS:"vars",ARCHIVE_ORIGIN:"vars",MEDIA_PUBLIC_BASE:"vars",NOTIFY_FROM:"vars",ACCESS_TEAM_DOMAIN:"secrets",ACCESS_AUD:"secrets",ACCESS_ALLOWED_EMAILS:"secrets",AWS_ACCESS_KEY_ID:"secrets",AWS_SECRET_ACCESS_KEY:"secrets",SNS_TOPIC_ARN:"secrets",SES_CONFIGURATION_SET:"secrets",SIMULATE_SENDS:"what-the-app-refuses"},sd="what-the-app-refuses";function od(e){return`${td}#${nd[e]??sd}`}function cs(e){if(ue.querySelector(".config-wall"))return;Dt(),document.body.classList.add("signed-out");let{variable:t}=e,n=t?"A setting needs fixing":"The database needs updating",s=t?o`<p class="hint"><a href="${od(t)}" target="_blank" rel="noopener">${t} in the Configuration reference <span aria-hidden="true">↗</span><span class="visually-hidden"> (opens in a new tab)</span></a></p>`:"";f(ue,o`<div class="card auth-wall config-wall" role="alert"><h2 tabindex="-1">${n}</h2><p>Kestrel won’t run until it’s fixed, and nothing is sent in the meantime.</p><p class="config-wall-problem">${e.message}</p>${s}<button id="config-reload" class="primary">Reload</button></div>`),c("#config-reload",ue).onclick=()=>location.reload(),c(".config-wall h2",ue).focus()}var ds="kestrel_token",Z={token:localStorage.getItem(ds)||"",session:null,appConfig:null};function ur(e){Z.token=(e||"").trim();try{Z.token?localStorage.setItem(ds,Z.token):localStorage.removeItem(ds)}catch{}}function Ro(){return{[ar]:fn,...Z.token?{Authorization:`Bearer ${Z.token}`}:{}}}function pr(){let e=Z.session;if(e?.auth.mode==="access"){let t=e.principal,n=t.email||(t.kind==="service"?"Service token":"Signed in");f(To,o`<span class="who" title="${n}">${n}</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>`)}else f(To,o`<span class="who dev" title="Local dev: sign-in is skipped on localhost">Local dev</span>`)}function hn(){ue.querySelector(".reauth-wall")||(Dt(),document.body.classList.add("signed-out"),f(ue,o`<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>`),c("#reauth",ue).onclick=()=>location.reload(),c(".reauth-wall h2",ue).focus())}async function cr(){try{let e=await fetch("/api/dev/token?kind=human");if(e.ok)return ur((await e.json()).token),!0}catch{}return!1}async function dr(){try{return await fetch("/api/whoami",{headers:Ro(),redirect:"manual"})}catch{return null}}function id(e){let t=e;return typeof t?.principal=="object"&&t.principal!==null&&typeof t.auth=="object"}async function mr(){Z.token||await cr();let e=await dr();!e?.ok&&Z.token&&(ur(""),await cr()&&(e=await dr()));let t=e?await e.json().catch(()=>null):null;if(e?.ok&&id(t))return t;let n=ls(t),s=ts(t);return n?cs(n):s?is(s):hn(),null}var se=class extends Error{constructor(n,s,i=null){super(n);this.status=s;this.data=i;this.name="ApiError"}status;data;get code(){let n=this.data;return typeof n?.error=="string"?n.error:null}get field(){let n=this.data;return typeof n?.field=="string"?n.field:null}},rd="Not authorized. Sign in again.",ad=e=>/^[a-z_]+$/i.test(e.trim()),ld=/^(Failed to fetch|NetworkError when attempting to fetch resource\.?|Load failed|The network connection was lost\.?|The Internet connection appears to be offline\.?)$/;function Co(e){return e instanceof TypeError&&ld.test(e.message)}function z(e){if(e instanceof se){let t=e.data?.message;return typeof t=="string"&&!ad(t)?t:e.status===401?e.message:e.status===404?"It isn\u2019t there any more. It may have been deleted elsewhere.":e.status>=500?"Something went wrong on the server. Try again in a moment.":e.status===429?"Too many requests at once. Try again in a moment.":"The server couldn\u2019t do that. Reload the page and try again."}return Co(e)?"Couldn\u2019t reach Kestrel. Check your connection and try again.":e instanceof Error?e.message:String(e)}async function Ao(e,t){let n={...Ro(),...t.headers??{}},s=t.body;t.json!==void 0&&(n["content-type"]="application/json",s=JSON.stringify(t.json));let i=await fetch(e,{method:t.method??"GET",headers:n,body:s,signal:t.signal});if(i.status===401){let r=await i.json().catch(()=>null),a=ts(r);throw a?is(a):hn(),new se(rd,401,r)}return i}function cd(e){if(e&&typeof e=="object"){let t=e;if(typeof t.message=="string")return t.message;if(typeof t.error=="string")return t.error}}async function Ho(e){let t=await e.text(),n=null;try{n=t?JSON.parse(t):null}catch{}let s=ls(n);s&&cs(s);let i=e.statusText||`Request failed (HTTP ${e.status})`;return new se(cd(n)??i,e.status,n)}async function C(e,t={}){let n=await Ao(e,t);if(!n.ok)throw await Ho(n);let s=await n.text();return s?JSON.parse(s):null}async function Ut(e,t={}){let n=await Ao(e,t);if(!n.ok)throw await Ho(n);return n.text()}async function us(e,t={}){let n=await Ao(e,t);if(!n.ok)throw await Ho(n);let s=n.headers.get("content-disposition")??"",i=/filename="([^"]+)"/.exec(s)?.[1]??null;return{text:await n.text(),filename:i}}function Po(e){let t=e.trim(),n=t.lastIndexOf("<");if(n<0||!t.endsWith(">"))return"";let s=t.slice(0,n).trim();return s.length>=2&&s.startsWith('"')&&s.endsWith('"')?s.slice(1,-1).replace(/\\(.)/g,"$1").trim():s}function bn(e,t){return e||Po(t)}function Io(e){let t=e?.settings.publication;return{name:bn(t?.name||"",e?.deployment.from_address??"")||"Your publication",tagline:t?.tagline||"",logoUrl:t?.logo_url||null}}function ps(){let e=Io(Z.appConfig),t=document.getElementById("brandName"),n=document.getElementById("brandTagline"),s=document.getElementById("brandLogo");t&&(t.textContent=e.name),n&&(n.textContent=e.tagline,n.hidden=!e.tagline),s&&(e.logoUrl?(f(s,o`<img src="${e.logoUrl}" alt="">`),s.classList.remove("brand-logo-placeholder")):(s.textContent=(e.name.trim()[0]||"K").toUpperCase(),s.classList.add("brand-logo-placeholder")))}function fr(e,t,n){return`${e}${t}/${n}`}var dd=["content","invalid_request"];function Mo(e){return e!==null&&dd.includes(e)}var No={step:"Connect Amazon SES",guide:" (the setup guide walks through it)",after:", then deploy."};var hr="Amazon SES is still in the sandbox, so when this send fires it reaches only addresses verified in SES. It waits for the rest, and other sends wait behind it, until AWS grants production access, then finishes on its own.";var Yf=1800*1e3,ms=300*1e3,Zf=60*1e3,Jf=1440*60*1e3;function Ft(e){let t=Math.round(e/1e3);if(t%60===0){let n=t/60;return`${n} minute${n===1?"":"s"}`}return`${t} seconds`}function br(e){return`Pick a time at least ${Ft(e)} from now, so there\u2019s time to cancel.`}var Qf=10080*60*1e3,Xf=3600*1e3;function gr(){let e=Z.appConfig?.deployment.build;if(!e?.version)return null;let t=e.tag_url?o`<a class="build-link" href="${e.tag_url}" target="_blank" rel="noopener">v${e.version}</a>`:o`v${e.version}`,n=e.commit_url?o`<a class="build-link" href="${e.commit_url}" target="_blank" rel="noopener">${e.sha}</a>`:o`${e.sha}`;return{version:t,sha:n}}function fs(){return Z.appConfig?.deployment.min_lead_ms??ms}function hs(){return br(fs())}function vr(){let e=fs();if(e%6e4===0){let t=e/6e4;return`${t} to ${t+1} minutes`}return`${Ft(e)} to ${Ft(e+6e4)}`}function bs(e,t=Date.now()){return e<t+fs()}function gs(e=Date.now()){return new Date(e+fs()+6e4)}function gn(){return Z.appConfig?.deployment.provider==="fake"}function be(){return Z.appConfig?.deployment.provider==="none"}function ud(){return Z.appConfig?.deployment.free_plan_limits===!0}function pd(){return be()&&Z.appConfig?.deployment.not_connected_preview===!0}var Oo="#/docs/ses";function He(e){let t=e?o` class="${e}"`:null;return o`<a${t} href="${Oo}">${No.step}</a>`}function Do(){return pd()?o`Restart <code>npm run dev</code> without <code>SIMULATE_SENDS=not-connected</code> to send.`:o`${He()}${No.after}`}function vs(e,t){let n=t===1;return o`No email provider connected, so nothing can be sent and ${e} ${n?"is":"are"} held, with no one marked unsent; ${n?"it goes out at its":"they go out at their"} first retry once one is connected. ${Do()}`}function ys(e,t){return be()?o`<div class="health amber ${e}" role="status"><span class="health-dot">⚠️</span><div><span>${t}</span></div></div>`:null}function Ss(e){if(!be())return null;let t="Will be held: no email provider is connected.";return e==="card"?o`<div class="sched-held">${t}</div>`:o`<div class="sched-note sched-note-held"><span class="sched-note-mark" aria-hidden="true">⚠️</span><span class="sched-note-msg">${t}</span></div>`}function ws(e,t){return be()?o`<p class="hint not-connected">No email provider connected, so ${e}. ${He()}${t}.</p>`:null}function $s(){return ws("a test can\u2019t be sent yet"," to send one")}function yr(){return ws("a confirmation can\u2019t be sent yet"," to add subscribers")}function Bo(){return ud()&&!be()?o`<p class="hint daily-writes" id="schDailyWrites" role="status" hidden></p>`:null}function Uo(){return be()?null:o`<p class="hint daily-writes" id="schSandbox" role="status" hidden></p>`}function Sr(){return ws("this can\u2019t be scheduled yet"," to schedule it")}function wr(){return ws("this send is held when it fires, until one is"," to send it")}function qt(e){return gn()?`${e} (local stand-in: nothing is delivered)`:e}function $r(e){let t=Z.appConfig?.deployment;return t?fr(t.archive_origin,t.archive_base_path,e):null}var Er=`<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>
3
+ `;var xr=`<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>
4
+ `;var kr=`<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>
5
+ `;var _r=`<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>
6
+ `;var Tr=`<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>
7
+ `;var Lr=`<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>
8
+ `;var Rr=`<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>
9
+ `;var Cr=`<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>
10
+ `;var Ar=`<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="8" y="8" width="13" height="13" rx="2"/><path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3"/></svg>
11
+ `;var Hr=`<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>
12
+ `;var Pr=`<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>
13
+ `;var Ir=`<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>
14
+ `;var Mr=`<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>
15
+ `;var Nr=`<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>
16
+ `;var Or=`<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>
17
+ `;var Dr=`<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>
18
+ `;var Br=`<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>
19
+ `;var Ur=`<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>
20
+ `;var Fr=`<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>
21
+ `;var qr=`<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>
22
+ `;var jr=`<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>
23
+ `;var Vr=`<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>
24
+ `;var Wr=`<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>
25
+ `;var Kr=`<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>
26
+ `;var Gr=`<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>
27
+ `;var zr=`<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>
28
+ `;var Yr=`<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>
29
+ `;var Zr=`<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>
30
+ `;var Jr=`<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>
31
+ `;var Qr=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" aria-hidden="true"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>
32
+ `;var Xr=`<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>
33
+ `;var ea=`<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>
34
+ `;var ta=`<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>
35
+ `;var Kd={article:Y(Er),bold:Y(xr),check:Y(Tr),code:Y(Cr),copyout:Y(Ar),editable:Y(Pr),heading:Y(Ir),indent:Y(Mr),info:Y(Nr),"info-filled":Y(Or),italic:Y(Dr),kestrel:Y(Br),lines:Y(Ur),link:Y(Fr),ol:Y(jr),paperclip:Y(Vr),plus:Y(Kr),preview:Y(Gr),quote:Y(zr),readonly:Y(Yr),search:Y(Zr),send:Y(Jr),settings:Y(Qr),ul:Y(Xr),upload:Y(ea),book:Y(kr),branch:Y(_r),"circle-check":Y(Lr),"circle-minus":Y(Rr),desktop:Y(Hr),mail:Y(qr),phone:Y(Wr),x:Y(ta)};function B(e){return Kd[e]}function jt(e,t,n){let s=(g,h)=>e===g?o`<a href="#/${g}" data-room="${g}" data-text="${h}" aria-current="page">${h}</a>`:o`<a href="#/${g}" data-room="${g}" data-text="${h}">${h}</a>`,i=gr(),r=Z.appConfig?.deployment.build.build_time,a=r?`Built ${new Date(r).toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}`:null,l=i?o`${i.version} <span aria-hidden="true">·</span> ${i.sha}`:null,d=l?a?o`<div class="rail-foot" title="${a}">${l}</div>`:o`<div class="rail-foot">${l}</div>`:null,u=l?a?o`<footer class="room-foot" title="${a}">${l}</footer>`:o`<footer class="room-foot">${l}</footer>`:null,p=t==null?o`<div class="room-body norail"><div class="room-main">${n}</div></div>`:o`<div class="room-body"><nav class="room-rail" aria-label="Contents"><div class="rail-inner">${t}${d}</div></nav><div class="room-main">${n}</div></div>`;return o`<div class="room">
36
36
  <header class="room-bar">
37
37
  <a class="room-back" href="#/dashboard" aria-label="Back to dashboard"><span aria-hidden="true">←</span><span class="room-back-label">Dashboard</span></a>
38
38
  <div class="room-nav">
39
- <a class="room-brand" href="#/docs">${O("kestrel")}<span>Kestrel</span></a>
39
+ <a class="room-brand" href="#/docs">${B("kestrel")}<span>Kestrel</span></a>
40
40
  <nav class="room-switch" aria-label="Reference">${s("docs","Docs")}${s("api-reference","API")}</nav>
41
41
  <a class="room-close" href="#/dashboard" title="Back to publication" aria-label="Back to publication"><span aria-hidden="true">✕</span></a>
42
42
  </div>
43
43
  </header>
44
- ${m}
44
+ ${p}
45
45
  ${u}
46
- </div>`}function zr(){pe.addEventListener("click",e=>{let t=e.target instanceof Element?e.target.closest(".room-bar a[href^='#/']"):null;!t||t.getAttribute("href")!==location.hash||(e.preventDefault(),window.scrollTo(0,0))})}var Nd=/^(\/[A-Za-z0-9._~%-]+)+$/,Od=/\/(?:\.|%2e){1,2}(?=\/|$)/i;function Po(e){return Nd.test(e)&&!Od.test(e)}function Yr(e){let t=new URLSearchParams(e).get("to");return t&&Po(t)?`#${t}`:null}var Dd=/^(\d+)\.(\d+)$/;function Bd(e){return`${e.seq}.${e.at}`}function Zr(e){let t=Dd.exec(e);if(!t?.[1]||!t[2])return null;let n=Number(t[1]),s=Number(t[2]);return Number.isSafeInteger(n)&&Number.isSafeInteger(s)?{seq:n,at:s}:null}function ys(e,t){let n=Zr(e),s=Zr(t);return!n||!s?n?e:t:Bd({seq:Math.min(n.seq,s.seq),at:Math.min(n.at,s.at)})}function ot(e){switch(e.status){case"scheduled":return e.phase==="due"?"due":"scheduled";case"sending":return"sending";case"sent":return e.phase==="complete"?"complete":"sent";default:return"canceled"}}var Ud=3e3,Fd=6e4,qd=3e4,Ue=new Set,ws=null,yt=null,Je,st=null,hn=!1,Mo=!1,Ss=0;function $s(e){try{e()}catch(t){reportError(t)}}function Io(e,t){return e&&t?ys(e,t):e??t}function Jr(e){clearTimeout(Je),Je=void 0,!(!Ue.size||document.hidden)&&(Je=setTimeout(bn,e))}function bn(){if(clearTimeout(Je),Je=void 0,!Ue.size)return;if(st){hn=!0;return}let e=Io(ws,yt);yt=null;let t=[...Ue],n=new AbortController;st=n;let s=setTimeout(()=>n.abort(),qd),i=e?`?since=${encodeURIComponent(e)}`:"";L(`/api/sends/feed${i}`,{signal:n.signal}).then(r=>{if(clearTimeout(s),st===n){st=null,Ss=0,ws=r.cursor;for(let a of t)Ue.has(a)&&a.take(r);if(hn){hn=!1,bn();return}Jr(Math.max(0,r.read_again_at-r.now))}},r=>{if(clearTimeout(s),st===n){if(st=null,hn=!1,r instanceof te&&r.code==="cursor_ahead"){let a=[...Ue];for(let l of a)ea(l);for(let l of a)$s(()=>l.stale());return}r instanceof te&&r.status===400?ws=null:yt=Io(yt,e),Ss+=1,Jr(Math.min(Ud*2**(Ss-1),Fd))}})}function jd(){Mo||(Mo=!0,queueMicrotask(()=>{Mo=!1,bn()}))}function Qr(){document.hidden?(clearTimeout(Je),Je=void 0):bn()}function Xr(e,t,n){n.aborted||(Ue.add(e),yt=Io(yt,t),Ue.size===1&&document.addEventListener("visibilitychange",Qr),n.addEventListener("abort",()=>ea(e),{once:!0}),jd())}function ea(e){Ue.delete(e),!Ue.size&&(clearTimeout(Je),Je=void 0,st?.abort(),st=null,hn=!1,Ss=0,ws=null,yt=null,document.removeEventListener("visibilitychange",Qr))}function Ft(e,t,n){let s=new Map(e.sends.map(i=>[i.id,ot(i)]));Xr({take(i){if(!i.sends.length&&!i.removed.length)return;let r=[];for(let l of i.sends){let d=ot(l),u=s.get(l.id)??null;u!==d&&r.push({send:l,from:u,to:d}),s.set(l.id,d)}let a=i.removed.map(l=>l.id);for(let l of a)s.delete(l);$s(()=>t.update({sends:i.sends,changes:r,removed:a}))},stale:()=>t.stale()},e.cursor,n)}function gn(e,t,n){let s=e.send.id,i=ot(e.send);Xr({take(r){if(r.removed.some(u=>u.id===s)){$s(()=>t.removed());return}let a=r.sends.find(u=>u.id===s);if(!a)return;let l=ot(a),d=l!==i?{send:a,from:i,to:l}:null;i=l,$s(()=>t.update(a,d))},stale:()=>t.stale()},e.cursor,n)}function Fe(){Ue.size&&bn()}function wt(e){return e.trim().toLowerCase()}function St(e){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}function j(e){if(!e)return"\u2014";let t=new Date(e);return t.toLocaleString(void 0,{year:t.getFullYear()===new Date().getFullYear()?void 0:"numeric",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})}function xs(e){let t=new Set,n=[];for(let s of String(e||"").split(/[\n,]+/)){let i=wt(s);St(i)?t.add(i):i&&n.push(s.trim())}return{valid:[...t],invalid:n}}function Es(e){return e.length?`Not ${e.length===1?"an email address":"email addresses"}: ${e.join(", ")}`:null}function ks(e,t,n){let s=new Map,i=[];for(let a of n)a.message?s.set(a.message,[...s.get(a.message)??[],a.email]):i.push(a.email);if(!s.size)return e?`Sent to ${e} of ${t}. Not sent to ${i.join(", ")}. Send again to try those.`:"Not sent. Send again to try.";let r=[...s].map(([a,l])=>t>1?`${l.join(", ")}: ${a}`:a);return i.length&&r.push(`Not sent to ${i.join(", ")}.`),e?`Sent to ${e} of ${t}. ${r.join(" ")} Send again goes only to those.`:r.join(" ")}function $t(e){let t=n=>String(n).padStart(2,"0");return`${e.getFullYear()}-${t(e.getMonth()+1)}-${t(e.getDate())}T${t(e.getHours())}:${t(e.getMinutes())}`}function vn(e,t=!1){let n=e-Date.now();if(t||n<=0)return"Preparing to send\u2026";let s=Math.floor(n/1e3),i=Math.floor(s/60),r=Math.floor(i/60);if(s<300)return i>0?`Sends in ${i}m ${String(s%60).padStart(2,"0")}s`:`Sends in ${s}s`;if(i<60)return`Sends in ${i}m`;if(r<24){let l=i%60;return l>0?`Sends in ${r}h ${l}m`:`Sends in ${r}h`}let a=Math.round(r/24);return`Sends in ${a} day${a===1?"":"s"}`}function _s(e){return`Missed its fire time \xB7 ${Math.max(0,Math.floor((Date.now()-e)/6e4))} min late`}var yn=e=>Math.max(0,Math.min(100,Math.round(e||0)));function Ts(e){if(e==null||!Number.isFinite(e)||e<=0)return"\u2014";let t=Math.round(e/1e3);if(t<90)return`${t}s`;let n=Math.round(t/60);return n<90?`${n} min`:`${Math.round(n/60)} hr`}function xt(e,t){return t.email_only?o`<span class="title-with-label"><span class="title-text">${e}</span><span class="email-only-mark">${O("mail")}Email only</span></span>`:e}function Ls(e){return e.email_only?o` · <span class="sched-email-only">Email only</span>`:null}function Rs(e){return o`<span class="countdown" data-fire="${e.fire_at}"${e.phase==="due"&&o` data-due`}${Re(e,"missed")&&o` data-missed`}></span>`}function Cs(e,t){let n=0,s=()=>{let i=Date.now(),r=Number.POSITIVE_INFINITY,a=l=>{l>i&&l<r&&(r=l)};for(let l of N("[data-closes]",e)){let d=Number(l.dataset.closes);l.hidden=i>=d,a(d)}for(let l of N("[data-fire]",e)){let d=Number(l.dataset.fire),u=l.dataset.missed!==void 0;l.textContent=u?_s(d):vn(d,l.dataset.due!==void 0),l.classList.toggle("countdown-missed",u),a(d)}r-i<=1e3&&r!==n&&(n=r,gt(r,s,t))};return bt(1e3,s,t),s}var Re=(e,t)=>e.conditions.some(n=>n.kind===t);function he(e,t){return e.conditions.find(n=>n.kind===t)}var Qe=(e,t)=>e.actions.some(n=>n.name===t);function qt(e,t){let n=new Set(e.map(s=>s.id));return[...e.map(s=>t.get(s.id)??s),...[...t.values()].filter(s=>!n.has(s.id))]}function wn(e){return Re(e,"wedged")||Re(e,"refused")}function jt(e){let t=e.delivered||0,n=[];e.complained&&n.push(`${e.complained.toLocaleString()} complained`),e.bounced&&n.push(`${e.bounced.toLocaleString()} bounced`),e.unsent&&n.push(`${e.unsent.toLocaleString()} unsent`);let s=n.length?o`<span class="muted delivered-note">${n.map((i,r)=>o`${r?", ":""}<span class="delivered-kind">${i}</span>`)}</span>`:null;return o`<span class="n">${t.toLocaleString()}</span>${s}`}function Wt(e){if(e===null)return"it is retried on its own";let t=e-Date.now();return t>0?`next retry in ${Ts(t)}`:"next retry due now"}function As(e,{stuckNote:t=!0}={}){let n=e.counts,s=n.accepted+n.delivered+n.bounced+n.complained,i=n.delivered+n.bounced+n.complained,r=e.audience.count,a=r>0?100*s/r:0,l=e.dispatch.eta_ms?` \xB7 ~${Ts(e.dispatch.eta_ms)} left`:"",d=t?he(e,"stuck"):void 0,u=e.provider.halt,m=e.phase==="backing_off"&&u?.reason==="unavailable",v=m?"Backing off":"Sending",f=m?` \xB7 provider unavailable, ${Wt(u.retry_at)}`:"";return o`<div class="card spread clickable active-card" data-watch="${e.id}">
46
+ </div>`}function na(){ue.addEventListener("click",e=>{let t=e.target instanceof Element?e.target.closest(".room-bar a[href^='#/']"):null;!t||t.getAttribute("href")!==location.hash||(e.preventDefault(),window.scrollTo(0,0))})}var Gd=/^(\/[A-Za-z0-9._~%-]+)+$/,zd=/\/(?:\.|%2e){1,2}(?=\/|$)/i;function vn(e){return Gd.test(e)&&!zd.test(e)}function sa(e){let t=new URLSearchParams(e).get("to");return t&&vn(t)?`#${t}`:null}var oa={"publication-identity":"Publication identity","ways-to-subscribe":"Ways to subscribe","confirmation-email":"Confirmation email","reader-pages":"Reader pages","email-template":"Email template","default-test-recipients":"Default test recipients",notifications:"Notifications","email-sender":"Email sender",instance:"Instance"},Yd=Object.keys(oa);function ia(e){return oa[e]}function Es(e){return`/settings/${e}`}function Fo(e){return Yd.includes(e)}var ra="You\u2019ve added a mailing address in Settings, but this template doesn\u2019t include the {{ .Publication.Address }} field. If you send promotional email, add it to the footer. If you\u2019ve written your address into the template yourself, or don\u2019t need to provide one, you can ignore this or remove the address in Settings.",xs=5;function aa(e,t){return e.length===1&&t.length===1?t[0]?.error??"":t.map(n=>`${n.email}: ${n.error}`).join("; ")}var Zd=/^(\d+)\.(\d+)$/;function Jd(e){return`${e.seq}.${e.at}`}function la(e){let t=Zd.exec(e);if(!t?.[1]||!t[2])return null;let n=Number(t[1]),s=Number(t[2]);return Number.isSafeInteger(n)&&Number.isSafeInteger(s)?{seq:n,at:s}:null}function ks(e,t){let n=la(e),s=la(t);return!n||!s?n?e:t:Jd({seq:Math.min(n.seq,s.seq),at:Math.min(n.at,s.at)})}function rt(e){switch(e.status){case"scheduled":return e.phase==="due"?"due":"scheduled";case"sending":return"sending";case"sent":return e.phase==="complete"?"complete":"sent";default:return"canceled"}}var Qd=3e3,Xd=6e4,eu=3e4,Be=new Set,_s=null,St=null,Ze,it=null,yn=!1,qo=!1,Ts=0;function Ls(e){try{e()}catch(t){reportError(t)}}function jo(e,t){return e&&t?ks(e,t):e??t}function ca(e){clearTimeout(Ze),Ze=void 0,!(!Be.size||document.hidden)&&(Ze=setTimeout(Sn,e))}function Sn(){if(clearTimeout(Ze),Ze=void 0,!Be.size)return;if(it){yn=!0;return}let e=jo(_s,St);St=null;let t=[...Be],n=new AbortController;it=n;let s=setTimeout(()=>n.abort(),eu),i=e?`?since=${encodeURIComponent(e)}`:"";C(`/api/sends/feed${i}`,{signal:n.signal}).then(r=>{if(clearTimeout(s),it===n){it=null,Ts=0,_s=r.cursor;for(let a of t)Be.has(a)&&a.take(r);if(yn){yn=!1,Sn();return}ca(Math.max(0,r.read_again_at-r.now))}},r=>{if(clearTimeout(s),it===n){if(it=null,yn=!1,r instanceof se&&r.code==="cursor_ahead"){let a=[...Be];for(let l of a)pa(l);for(let l of a)Ls(()=>l.stale());return}r instanceof se&&r.status===400?_s=null:St=jo(St,e),Ts+=1,ca(Math.min(Qd*2**(Ts-1),Xd))}})}function tu(){qo||(qo=!0,queueMicrotask(()=>{qo=!1,Sn()}))}function da(){document.hidden?(clearTimeout(Ze),Ze=void 0):Sn()}function ua(e,t,n){n.aborted||(Be.add(e),St=jo(St,t),Be.size===1&&document.addEventListener("visibilitychange",da),n.addEventListener("abort",()=>pa(e),{once:!0}),tu())}function pa(e){Be.delete(e),!Be.size&&(clearTimeout(Ze),Ze=void 0,it?.abort(),it=null,yn=!1,Ts=0,_s=null,St=null,document.removeEventListener("visibilitychange",da))}function Vt(e,t,n){let s=new Map(e.sends.map(i=>[i.id,rt(i)]));ua({take(i){if(!i.sends.length&&!i.removed.length)return;let r=[];for(let l of i.sends){let d=rt(l),u=s.get(l.id)??null;u!==d&&r.push({send:l,from:u,to:d}),s.set(l.id,d)}let a=i.removed.map(l=>l.id);for(let l of a)s.delete(l);Ls(()=>t.update({sends:i.sends,changes:r,removed:a}))},stale:()=>t.stale()},e.cursor,n)}function wn(e,t,n){let s=e.send.id,i=rt(e.send);ua({take(r){if(r.removed.some(u=>u.id===s)){Ls(()=>t.removed());return}let a=r.sends.find(u=>u.id===s);if(!a)return;let l=rt(a),d=l!==i?{send:a,from:i,to:l}:null;i=l,Ls(()=>t.update(a,d))},stale:()=>t.stale()},e.cursor,n)}function Ue(){Be.size&&Sn()}function wt(e){return e.trim().toLowerCase()}function $t(e){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}function at(e){return`${e.toLocaleString()} ${e===1?"recipient":"recipients"}`}function W(e){if(!e)return"\u2014";let t=new Date(e);return t.toLocaleString(void 0,{year:t.getFullYear()===new Date().getFullYear()?void 0:"numeric",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})}function Rs(e){let t=new Set,n=[];for(let s of String(e||"").split(/[\n,]+/)){let i=wt(s);$t(i)?t.add(i):i&&n.push(s.trim())}return{valid:[...t],invalid:n}}function Cs(e){return e.length?`Not ${e.length===1?"an email address":"email addresses"}: ${e.join(", ")}`:null}function As(e,t,n){let s=new Map,i=[];for(let a of n)a.message?s.set(a.message,[...s.get(a.message)??[],a.email]):i.push(a.email);if(!s.size)return e?`Sent to ${e} of ${t}. Not sent to ${i.join(", ")}. Send again to try those.`:"Not sent. Send again to try.";let r=[...s].map(([a,l])=>t>1?`${l.join(", ")}: ${a}`:a);return i.length&&r.push(`Not sent to ${i.join(", ")}.`),e?`Sent to ${e} of ${t}. ${r.join(" ")} Send again goes only to those.`:r.join(" ")}function Et(e){let t=n=>String(n).padStart(2,"0");return`${e.getFullYear()}-${t(e.getMonth()+1)}-${t(e.getDate())}T${t(e.getHours())}:${t(e.getMinutes())}`}function $n(e,t=!1){let n=e-Date.now();if(t||n<=0)return"Preparing to send\u2026";let s=Math.floor(n/1e3),i=Math.floor(s/60),r=Math.floor(i/60);if(s<300)return i>0?`Sends in ${i}m ${String(s%60).padStart(2,"0")}s`:`Sends in ${s}s`;if(i<60)return`Sends in ${i}m`;if(r<24){let l=i%60;return l>0?`Sends in ${r}h ${l}m`:`Sends in ${r}h`}let a=Math.round(r/24);return`Sends in ${a} day${a===1?"":"s"}`}function Hs(e){return`Missed its fire time \xB7 ${Math.max(0,Math.floor((Date.now()-e)/6e4))} min late`}var En=e=>Math.max(0,Math.min(100,Math.round(e||0)));function Ps(e){if(e==null||!Number.isFinite(e)||e<=0)return"\u2014";let t=Math.round(e/1e3);if(t<90)return`${t}s`;let n=Math.round(t/60);return n<90?`${n} min`:`${Math.round(n/60)} hr`}function xt(e,t){return t.email_only?o`<span class="title-with-label"><span class="title-text">${e}</span><span class="email-only-mark">${B("mail")}Email only</span></span>`:e}function Is(e){return e.email_only?o` · <span class="sched-email-only">Email only</span>`:null}function Ms(e){return o`<span class="countdown" data-fire="${e.fire_at}"${e.phase==="due"&&o` data-due`}${Ce(e,"missed")&&o` data-missed`}></span>`}function Ns(e,t){let n=0,s=()=>{let i=Date.now(),r=Number.POSITIVE_INFINITY,a=l=>{l>i&&l<r&&(r=l)};for(let l of D("[data-closes]",e)){let d=Number(l.dataset.closes);l.hidden=i>=d,a(d)}for(let l of D("[data-fire]",e)){let d=Number(l.dataset.fire),u=l.dataset.missed!==void 0;l.textContent=u?Hs(d):$n(d,l.dataset.due!==void 0),l.classList.toggle("countdown-missed",u),a(d)}r-i<=1e3&&r!==n&&(n=r,vt(r,s,t))};return gt(1e3,s,t),s}var Ce=(e,t)=>e.conditions.some(n=>n.kind===t);function fe(e,t){return e.conditions.find(n=>n.kind===t)}var Je=(e,t)=>e.actions.some(n=>n.name===t);function Wt(e,t){let n=new Set(e.map(s=>s.id));return[...e.map(s=>t.get(s.id)??s),...[...t.values()].filter(s=>!n.has(s.id))]}function xn(e){return Ce(e,"wedged")||Ce(e,"refused")}function Kt(e){let t=e.delivered||0,n=[];e.complained&&n.push(`${e.complained.toLocaleString()} complained`),e.bounced&&n.push(`${e.bounced.toLocaleString()} bounced`),e.unsent&&n.push(`${e.unsent.toLocaleString()} unsent`);let s=n.length?o`<span class="muted delivered-note">${n.map((i,r)=>o`${r?", ":""}<span class="delivered-kind">${i}</span>`)}</span>`:null;return o`<span class="n">${t.toLocaleString()}</span>${s}`}function Gt(e){if(e===null)return"it is retried on its own";let t=e-Date.now();return t>0?`next retry in ${Ps(t)}`:"next retry due now"}function Os(e,{stuckNote:t=!0}={}){let n=e.counts,s=n.accepted+n.delivered+n.bounced+n.complained,i=n.delivered+n.bounced+n.complained,r=e.audience.count,a=r>0?100*s/r:0,l=e.dispatch.eta_ms?` \xB7 ~${Ps(e.dispatch.eta_ms)} left`:"",d=t?fe(e,"stuck"):void 0,u=e.provider.halt,p=e.phase==="backing_off"&&u?.reason==="unavailable",g=p?"Backing off":"Sending",h=p?` \xB7 provider unavailable, ${Gt(u.retry_at)}`:"";return o`<div class="card spread clickable active-card" data-watch="${e.id}">
47
47
  <div class="active-main">
48
48
  <a class="card-link active-subj" href="#/sent/${e.id}">${e.subject||o`<em>untitled</em>`}</a>
49
- <div class="active-bar"><div class="active-fill" style="width:${yn(a)}%"></div></div>
50
- <div class="muted active-stat">${v}: ${s.toLocaleString()} of ${r.toLocaleString()} accepted${i?` \xB7 ${i.toLocaleString()} confirmed`:""}${l}${f}</div>
49
+ <div class="active-bar"><div class="active-fill" style="width:${En(a)}%"></div></div>
50
+ <div class="muted active-stat">${g}: ${s.toLocaleString()} of ${r.toLocaleString()} accepted${i?` \xB7 ${i.toLocaleString()} confirmed`:""}${l}${h}</div>
51
51
  ${d?o`<div class="active-stuck">${d.message}</div>`:null}
52
52
  </div>
53
53
  <a class="ghost-link" href="#/sent/${e.id}">Watch&nbsp;→</a>
54
- </div>`}function Vt(e){let t=new URLSearchParams;e.status&&t.set("status",e.status),e.suppressed&&t.set("suppressed",e.suppressed),e.failures&&t.set("failures",e.failures);let n=(e.search||"").trim();return n&&t.set("search",n),e.sort&&(t.set("sort",e.sort),t.set("dir",e.dir??"desc")),t.set("limit",String(e.limit)),t.set("offset",String(e.offset)),t.toString()}function Kt(e){let t=e.statuses?.length?o`<select class="lt-status" aria-label="Filter by status"><option value="${e.allValue||""}">All statuses</option>${e.statuses.map(i=>o`<option value="${i.value}">${i.label}</option>`)}</select>`:null,n=e.suppressible?o`<label class="lt-toggle"><input type="checkbox" class="lt-suppressed"><span>Suppressed</span></label>`:null,s=e.failures?o`<label class="lt-toggle"><input type="checkbox" class="lt-failures"><span>With delivery failures</span></label>`:null;return o`<div class="list-toolbar">
54
+ </div>`}function zt(e){let t=new URLSearchParams;e.status&&t.set("status",e.status),e.suppressed&&t.set("suppressed",e.suppressed),e.failures&&t.set("failures",e.failures);let n=(e.search||"").trim();return n&&t.set("search",n),e.sort&&(t.set("sort",e.sort),t.set("dir",e.dir??"desc")),t.set("limit",String(e.limit)),t.set("offset",String(e.offset)),t.toString()}function Yt(e){let t=e.statuses?.length?o`<select class="lt-status" aria-label="Filter by status"><option value="${e.allValue||""}">All statuses</option>${e.statuses.map(i=>o`<option value="${i.value}">${i.label}</option>`)}</select>`:null,n=e.suppressible?o`<label class="lt-toggle"><input type="checkbox" class="lt-suppressed"><span>Suppressed</span></label>`:null,s=e.failures?o`<label class="lt-toggle"><input type="checkbox" class="lt-failures"><span>With delivery failures</span></label>`:null;return o`<div class="list-toolbar">
55
55
  <input class="lt-search" type="search" placeholder="${e.searchPlaceholder||"Search\u2026"}" aria-label="Search" autocomplete="off">
56
56
  <div class="lt-filters">${t}${n}${s}</div>
57
- </div>`}function Gt(e,t,n){let s=e.querySelector(".lt-search"),i=e.querySelector(".lt-status"),r=e.querySelector(".lt-suppressed"),a=e.querySelector(".lt-failures");if(s){s.value=t.search||"";let l;s.oninput=()=>{clearTimeout(l),l=setTimeout(()=>{t.search=s.value,t.offset=0,n()},250)}}i&&(i.value=t.status||"",i.onchange=()=>{t.status=i.value,t.offset=0,n()}),r&&(r.checked=t.suppressed==="only",r.onchange=()=>{t.suppressed=r.checked?"only":"",t.offset=0,n()}),a&&(a.checked=t.failures==="only",a.onchange=()=>{t.failures=a.checked?"only":"",t.offset=0,n()})}function Ee(e,t,n,s){if(!t)return s?o`<th class="${s}">${e}</th>`:o`<th>${e}</th>`;let i=n.sort===t,r=i?n.dir==="asc"?"\u2191":"\u2193":"",a=`${s?`${s} `:""}sortable${i?" sorted":""}`;return o`<th class="${a}"><button type="button" class="th-sort" data-sort="${t}">${e}<span class="th-arrow" aria-hidden="true">${r}</span></button></th>`}function it(e,t,n){for(let s of e.querySelectorAll(".th-sort"))s.onclick=()=>{let i=s.dataset.sort;t.sort===i?t.dir=t.dir==="asc"?"desc":"asc":(t.sort=i,t.dir="desc"),t.offset=0,n()}}function rt(e,t,n,s){let i=n?.limit??t.limit;if(!n||n.total<=i){h(e,o``);return}let r=n.offset+1,a=Math.min(n.offset+i,n.total),l=n.offset>0,d=n.offset+i<n.total;if(h(e,o`<div class="pager"><button type="button" class="pager-prev"${l?null:ta}>← Prev</button><span class="pager-range muted">${r}–${a} of ${n.total}</span><button type="button" class="pager-next"${d?null:ta}>Next →</button></div>`),l){let u=e.querySelector(".pager-prev");u&&(u.onclick=()=>{t.offset=Math.max(0,n.offset-i),s()})}if(d){let u=e.querySelector(".pager-next");u&&(u.onclick=()=>{t.offset=n.offset+i,s()})}}var ta=o` disabled`;var Wd=3600,Vd=300,Kd=3,Oo=new WeakMap;function U(e){let t=[...wo.querySelectorAll(".toast:not(.leaving)")],n=t.find(i=>i.textContent===e);if(n){na(n);return}let s=document.createElement("div");s.className="toast",s.textContent=e,wo.appendChild(s),requestAnimationFrame(()=>s.classList.add("show")),na(s);for(let i of t.slice(0,t.length+1-Kd))oa(i)}function na(e){clearTimeout(Oo.get(e)),Oo.set(e,window.setTimeout(()=>oa(e),Wd))}function oa(e){clearTimeout(Oo.get(e)),e.style.height=`${e.offsetHeight}px`,e.offsetHeight,e.classList.add("leaving"),e.style.height="0",setTimeout(()=>e.remove(),Vd)}var Xe=e=>o`<span class="badge ${e}">${e}</span>`,Gd=0,No=new WeakMap;function ia(e){let t=document.createElement("div");t.className="field-error",t.id=`field-error-${++Gd}`,t.setAttribute("role","alert");let n=document.createElement("span");n.className="field-error-ico",n.setAttribute("aria-hidden","true"),n.textContent="!";let s=document.createElement("span");return s.textContent=e,t.append(n,s),t}function qe(e,t,{after:n}={}){No.get(e)?.();let s=ia(t),i=e.getAttribute("aria-describedby");(n??e).after(s),e.setAttribute("aria-describedby",i?`${i} ${s.id}`:s.id),e.classList.add("is-invalid");let r=!1,a=()=>{r||(r=!0,s.remove(),e.classList.remove("is-invalid"),i===null?e.removeAttribute("aria-describedby"):e.setAttribute("aria-describedby",i),e.removeEventListener("input",a),e.removeEventListener("change",a),No.delete(e))};return e.addEventListener("input",a),e.addEventListener("change",a),No.set(e,a),a}function fe(e,t){let n=document.createElement("div");n.className="modal-backdrop",h(n,o`<div class="modal" role="dialog" aria-modal="true">${e}</div>`);let s=n.firstElementChild,i=document.activeElement instanceof HTMLElement?document.activeElement:null,r=m=>{n===[...document.querySelectorAll(".modal-backdrop")].pop()&&(m.key==="Escape"?u():m.key==="Tab"&&Yd(s,m))},a=null,l=()=>{a?.(),a=null},d=(m,v)=>{if(l(),v){a=qe(v,m),v.focus();return}let f=ia(m);f.classList.add("dialog-error");let p=s.querySelector(".actions");p?p.before(f):s.append(f),a=()=>f.remove()},u=()=>{let m=document.activeElement,v=n.contains(m)||!m||m===document.body;n.remove(),document.removeEventListener("keydown",r),t.removeEventListener("abort",u),v&&i?.isConnected&&i.focus()};return t.aborted?{el:n,close:u,error:d,clearError:l}:(document.body.appendChild(n),n.addEventListener("click",m=>{m.target===n&&u()}),document.addEventListener("keydown",r),t.addEventListener("abort",u,{once:!0}),queueMicrotask(()=>{n.isConnected&&!s.contains(document.activeElement)&&ra(s)[0]?.focus()}),{el:n,close:u,error:d,clearError:l})}var zd='a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';function ra(e){return[...e.querySelectorAll(zd)].filter(t=>!t.closest("[hidden]"))}function Yd(e,t){let n=ra(e),s=n[0],i=n[n.length-1];if(!s||!i){t.preventDefault();return}let r=document.activeElement;e.contains(r)?t.shiftKey&&r===s?(t.preventDefault(),i.focus()):!t.shiftKey&&r===i&&(t.preventDefault(),s.focus()):(t.preventDefault(),s.focus())}var sa=8;function Zd(e){let t=getComputedStyle(e,"::after"),n=parseFloat(t.width)+parseFloat(t.paddingLeft)+parseFloat(t.paddingRight);if(!Number.isFinite(n))return;let s=e.getBoundingClientRect().left,i=document.documentElement.clientWidth,r=Math.min(0,Math.max(i-sa-n-s,sa-s));e.style.setProperty("--tip-x",`${Math.round(r)}px`)}function aa(){for(let e of["pointerover","focusin"])document.addEventListener(e,t=>{let n=t.target instanceof Element?t.target.closest(".info"):null;n&&Zd(n)});document.addEventListener("keydown",e=>{let t=document.activeElement;e.key==="Escape"&&t instanceof HTMLElement&&t.classList.contains("info")&&t.blur()})}var Hs=[],Do=null,zt=null;function Et(){let e=Hs.some(t=>t.contains(document.activeElement));for(let t of Hs)t.remove();Hs=[],Do?.removeEventListener("abort",Et),Do=null,zt?.setAttribute("aria-expanded","false"),e&&zt?.isConnected&&zt.focus(),zt=null}function Yt(e,t,n,{align:s="end"}={}){if(Et(),n.aborted)return;let i=document.createElement("div");i.className="menu-overlay",i.onclick=Et;let r=document.createElement("div");r.className="menu",r.setAttribute("role","menu");for(let l of t){let d=document.createElement("button");if(d.type="button",d.setAttribute("role","menuitem"),d.tabIndex=-1,d.className=`menu-item${l.danger?" danger-item":""}`,l.description){let u=document.createElement("span");u.className="menu-item-name",u.textContent=l.label;let m=document.createElement("span");m.className="menu-item-desc",m.textContent=l.description,d.append(u,m),r.classList.add("menu-described")}else d.textContent=l.label;d.onclick=()=>{Et(),l.onClick()},r.appendChild(d)}r.addEventListener("keydown",l=>Jd(r,l)),document.body.appendChild(i),document.body.appendChild(r),Hs=[i,r],Do=n,zt=e instanceof HTMLElement?e:null,zt?.setAttribute("aria-expanded","true"),n.addEventListener("abort",Et,{once:!0});let a=e.getBoundingClientRect();r.style.top=`${a.bottom+window.scrollY+4}px`,r.style.left=`${(s==="start"?a.left:a.right-r.offsetWidth)+window.scrollX}px`,r.querySelector(".menu-item")?.focus()}function Jd(e,t){let n=[...e.querySelectorAll(".menu-item")],s=n.indexOf(document.activeElement),i=r=>{t.preventDefault(),n[(r+n.length)%n.length]?.focus()};t.key==="ArrowDown"?i(s+1):t.key==="ArrowUp"?i(s-1):t.key==="Home"?i(0):t.key==="End"?i(n.length-1):t.key==="Escape"?(t.preventDefault(),t.stopPropagation(),Et()):t.key==="Tab"&&Et()}async function Z(e,t,n){let s=e.textContent;e.disabled=!0,t&&(e.textContent=t);try{return await n()}finally{e.isConnected&&(e.disabled=!1,e.textContent=s)}}function re(e,t,n){h(e,o`<div class="error"><span>${t}</span><button class="ghost" data-retry>Retry</button></div>`);let s=e.querySelector("[data-retry]");s&&(s.onclick=n)}function Zt(e,t){return e.actions.find(n=>n.name===t)}function Sn(e){let t=e.send;return t&&(t.status==="scheduled"||t.status==="sending")?t:null}var Qd=[{value:"draft",label:"Draft"},{value:"scheduled",label:"Scheduled"}],Bo="draft,scheduled";function Uo(e){return Z(e,"Creating\u2026",async()=>{try{let{post:t}=await L("/api/posts",{method:"POST",json:{}});location.hash=`#/edit/${t.id}`}catch(t){U(V(t))}})}async function Fo(e,t){ie(t,"Drafts");let n={status:Bo,search:"",sort:"",dir:"desc",offset:0,limit:50};h(e,o`<div class="spread page-head"><h1>Drafts</h1><button class="primary" id="newPost">New post</button></div>
58
- ${Kt({statuses:Qd,allValue:Bo,searchPlaceholder:"Search subject\u2026"})}
57
+ </div>`}function Zt(e,t,n){let s=e.querySelector(".lt-search"),i=e.querySelector(".lt-status"),r=e.querySelector(".lt-suppressed"),a=e.querySelector(".lt-failures");if(s){s.value=t.search||"";let l;s.oninput=()=>{clearTimeout(l),l=setTimeout(()=>{t.search=s.value,t.offset=0,n()},250)}}i&&(i.value=t.status||"",i.onchange=()=>{t.status=i.value,t.offset=0,n()}),r&&(r.checked=t.suppressed==="only",r.onchange=()=>{t.suppressed=r.checked?"only":"",t.offset=0,n()}),a&&(a.checked=t.failures==="only",a.onchange=()=>{t.failures=a.checked?"only":"",t.offset=0,n()})}function xe(e,t,n,s){if(!t)return s?o`<th class="${s}">${e}</th>`:o`<th>${e}</th>`;let i=n.sort===t,r=i?n.dir==="asc"?"\u2191":"\u2193":"",a=`${s?`${s} `:""}sortable${i?" sorted":""}`;return o`<th class="${a}"><button type="button" class="th-sort" data-sort="${t}">${e}<span class="th-arrow" aria-hidden="true">${r}</span></button></th>`}function lt(e,t,n){for(let s of e.querySelectorAll(".th-sort"))s.onclick=()=>{let i=s.dataset.sort;t.sort===i?t.dir=t.dir==="asc"?"desc":"asc":(t.sort=i,t.dir="desc"),t.offset=0,n()}}function ct(e,t,n,s){let i=n?.limit??t.limit;if(!n||n.total<=i){f(e,o``);return}let r=n.offset+1,a=Math.min(n.offset+i,n.total),l=n.offset>0,d=n.offset+i<n.total;if(f(e,o`<div class="pager"><button type="button" class="pager-prev"${l?null:ma}>← Prev</button><span class="pager-range muted">${r}–${a} of ${n.total}</span><button type="button" class="pager-next"${d?null:ma}>Next →</button></div>`),l){let u=e.querySelector(".pager-prev");u&&(u.onclick=()=>{t.offset=Math.max(0,n.offset-i),s()})}if(d){let u=e.querySelector(".pager-next");u&&(u.onclick=()=>{t.offset=n.offset+i,s()})}}var ma=o` disabled`;var nu=3600,su=300,ou=3,Wo=new WeakMap;function q(e){let t=[...Lo.querySelectorAll(".toast:not(.leaving)")],n=t.find(i=>i.textContent===e);if(n){fa(n);return}let s=document.createElement("div");s.className="toast",s.textContent=e,Lo.appendChild(s),requestAnimationFrame(()=>s.classList.add("show")),fa(s);for(let i of t.slice(0,t.length+1-ou))ba(i)}function fa(e){clearTimeout(Wo.get(e)),Wo.set(e,window.setTimeout(()=>ba(e),nu))}function ba(e){clearTimeout(Wo.get(e)),e.style.height=`${e.offsetHeight}px`,e.offsetHeight,e.classList.add("leaving"),e.style.height="0",setTimeout(()=>e.remove(),su)}var Qe=e=>o`<span class="badge ${e}">${e}</span>`,iu=0,Vo=new WeakMap;function ga(e){let t=document.createElement("div");t.className="field-error",t.id=`field-error-${++iu}`,t.setAttribute("role","alert");let n=document.createElement("span");n.className="field-error-ico",n.setAttribute("aria-hidden","true"),n.textContent="!";let s=document.createElement("span");return s.textContent=e,t.append(n,s),t}function Fe(e,t,{after:n}={}){Vo.get(e)?.();let s=ga(t),i=e.getAttribute("aria-describedby");(n??e).after(s),e.setAttribute("aria-describedby",i?`${i} ${s.id}`:s.id),e.classList.add("is-invalid");let r=!1,a=()=>{r||(r=!0,s.remove(),e.classList.remove("is-invalid"),i===null?e.removeAttribute("aria-describedby"):e.setAttribute("aria-describedby",i),e.removeEventListener("input",a),e.removeEventListener("change",a),Vo.delete(e))};return e.addEventListener("input",a),e.addEventListener("change",a),Vo.set(e,a),a}function pe(e,t){let n=document.createElement("div");n.className="modal-backdrop",f(n,o`<div class="modal" role="dialog" aria-modal="true">${e}</div>`);let s=n.firstElementChild,i=document.activeElement instanceof HTMLElement?document.activeElement:null,r=p=>{n===[...document.querySelectorAll(".modal-backdrop")].pop()&&(p.key==="Escape"?u():p.key==="Tab"&&au(s,p))},a=null,l=()=>{a?.(),a=null},d=(p,g)=>{if(l(),g){a=Fe(g,p),g.focus();return}let h=ga(p);h.classList.add("dialog-error");let m=s.querySelector(".actions");m?m.before(h):s.append(h),a=()=>h.remove()},u=()=>{let p=document.activeElement,g=n.contains(p)||!p||p===document.body;n.remove(),document.removeEventListener("keydown",r),t.removeEventListener("abort",u),g&&i?.isConnected&&i.focus()};return t.aborted?{el:n,close:u,error:d,clearError:l}:(document.body.appendChild(n),n.addEventListener("click",p=>{p.target===n&&u()}),document.addEventListener("keydown",r),t.addEventListener("abort",u,{once:!0}),queueMicrotask(()=>{n.isConnected&&!s.contains(document.activeElement)&&va(s)[0]?.focus()}),{el:n,close:u,error:d,clearError:l})}var ru='a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';function va(e){return[...e.querySelectorAll(ru)].filter(t=>!t.closest("[hidden]"))}function au(e,t){let n=va(e),s=n[0],i=n[n.length-1];if(!s||!i){t.preventDefault();return}let r=document.activeElement;e.contains(r)?t.shiftKey&&r===s?(t.preventDefault(),i.focus()):!t.shiftKey&&r===i&&(t.preventDefault(),s.focus()):(t.preventDefault(),s.focus())}var ha=8;function lu(e){let t=getComputedStyle(e,"::after"),n=parseFloat(t.width)+parseFloat(t.paddingLeft)+parseFloat(t.paddingRight);if(!Number.isFinite(n))return;let s=e.getBoundingClientRect().left,i=document.documentElement.clientWidth,r=Math.min(0,Math.max(i-ha-n-s,ha-s));e.style.setProperty("--tip-x",`${Math.round(r)}px`)}function ya(){for(let e of["pointerover","focusin"])document.addEventListener(e,t=>{let n=t.target instanceof Element?t.target.closest(".info"):null;n&&lu(n)});document.addEventListener("keydown",e=>{let t=document.activeElement;e.key==="Escape"&&t instanceof HTMLElement&&t.classList.contains("info")&&t.blur()})}var Ds=[],Ko=null,Jt=null;function kt(){let e=Ds.some(t=>t.contains(document.activeElement));for(let t of Ds)t.remove();Ds=[],Ko?.removeEventListener("abort",kt),Ko=null,Jt?.setAttribute("aria-expanded","false"),e&&Jt?.isConnected&&Jt.focus(),Jt=null}function Qt(e,t,n,{align:s="end"}={}){if(kt(),n.aborted)return;let i=document.createElement("div");i.className="menu-overlay",i.onclick=kt;let r=document.createElement("div");r.className="menu",r.setAttribute("role","menu");for(let l of t){let d=document.createElement("button");if(d.type="button",d.setAttribute("role","menuitem"),d.tabIndex=-1,d.className=`menu-item${l.danger?" danger-item":""}`,l.description){let u=document.createElement("span");u.className="menu-item-name",u.textContent=l.label;let p=document.createElement("span");p.className="menu-item-desc",p.textContent=l.description,d.append(u,p),r.classList.add("menu-described")}else d.textContent=l.label;d.onclick=()=>{kt(),l.onClick()},r.appendChild(d)}r.addEventListener("keydown",l=>cu(r,l)),document.body.appendChild(i),document.body.appendChild(r),Ds=[i,r],Ko=n,Jt=e instanceof HTMLElement?e:null,Jt?.setAttribute("aria-expanded","true"),n.addEventListener("abort",kt,{once:!0});let a=e.getBoundingClientRect();r.style.top=`${a.bottom+window.scrollY+4}px`,r.style.left=`${(s==="start"?a.left:a.right-r.offsetWidth)+window.scrollX}px`,r.querySelector(".menu-item")?.focus()}function cu(e,t){let n=[...e.querySelectorAll(".menu-item")],s=n.indexOf(document.activeElement),i=r=>{t.preventDefault(),n[(r+n.length)%n.length]?.focus()};t.key==="ArrowDown"?i(s+1):t.key==="ArrowUp"?i(s-1):t.key==="Home"?i(0):t.key==="End"?i(n.length-1):t.key==="Escape"?(t.preventDefault(),t.stopPropagation(),kt()):t.key==="Tab"&&kt()}async function J(e,t,n){let s=e.textContent;e.disabled=!0,t&&(e.textContent=t);try{return await n()}finally{e.isConnected&&(e.disabled=!1,e.textContent=s)}}function re(e,t,n){f(e,o`<div class="error"><span>${t}</span><button class="ghost" data-retry>Retry</button></div>`);let s=e.querySelector("[data-retry]");s&&(s.onclick=n)}function Xt(e,t){return e.actions.find(n=>n.name===t)}function kn(e){let t=e.send;return t&&(t.status==="scheduled"||t.status==="sending")?t:null}var du=[{value:"draft",label:"Draft"},{value:"scheduled",label:"Scheduled"}],Go="draft,scheduled";function zo(e){return J(e,"Creating\u2026",async()=>{try{let{post:t}=await C("/api/posts",{method:"POST",json:{}});location.hash=`#/edit/${t.id}`}catch(t){q(z(t))}})}async function Yo(e,t){ie(t,"Drafts");let n={status:Go,search:"",sort:"",dir:"desc",offset:0,limit:50};f(e,o`<div class="spread page-head"><h1>Drafts</h1><button class="primary" id="newPost">New post</button></div>
58
+ ${Yt({statuses:du,allValue:Go,searchPlaceholder:"Search subject\u2026"})}
59
59
  <div id="list"><p class="muted">Loading…</p></div>
60
- <div id="postsPager"></div>`);let s=c("#newPost",e);s.onclick=()=>Uo(s);let i=c("#list",e),r=c("#postsPager",e),a=new Map,l=0,d=()=>u(!1);async function u(p){let S=++l;try{let $=await L(`/api/posts?${Vt(n)}`,{signal:t});if(S!==l)return null;let M=$.posts;if(a=new Map(M.map(P=>[P.id,Sn(P)])),!M.length){let P=n.search||n.status&&n.status!==Bo;return h(i,o`<p class="muted">${P?"No drafts match.":"No drafts yet. Create your first draft."}</p>`),h(r,o``),$}h(i,o`<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>${Ee("Title","subject",n)}${Ee("Status",null,n)}${Ee("Scheduled","send.fire_at",n)}${Ee("Updated","updated_at",n)}<th></th></tr></thead><tbody>${M.map(P=>{let A=Sn(P),H=A?.status==="sending",y=A&&H?`#/sent/${A.id}`:`#/edit/${P.id}`,w=A?.fire_at??null,T=Zt(P,"delete")?"1":"";return o`<tr class="clickable" data-id="${P.id}" data-target="${y}"><td>${xt(o`<a href="${y}">${P.subject||o`<em>untitled</em>`}</a>`,P)}</td><td>${H?Xe("sending"):Xe(P.status)}</td><td class="muted when${w?"":" empty"}">${w?j(w):"\u2014"}</td><td class="muted updated">${j(P.updated_at)}</td><td class="act"><button class="icon" data-menu="${P.id}" data-status="${H?"sending":P.status}" data-delete="${T}" data-target="${y}" aria-label="Post actions" aria-haspopup="menu">⋯</button></td></tr>`})}</tbody></table></div>`),it(i,n,d);for(let P of N("tr[data-id]",i))P.onclick=A=>{let H=A.target;H instanceof Element&&H.tagName!=="A"&&!H.closest("[data-menu]")&&P.dataset.target&&(location.hash=P.dataset.target)};for(let P of N("[data-menu]",i))P.onclick=A=>{A.stopPropagation();let H=P.dataset.status,y=P.dataset.target??"",w=[{label:H==="sending"?"Watch send":"Open",onClick:()=>{location.hash=y}}];if(P.dataset.delete&&P.dataset.menu){let T=P.dataset.menu;w.push({label:"Delete draft",danger:!0,onClick:()=>eu(T,d,t)})}Yt(P,w,t)};return rt(r,n,$.page,d),$}catch($){return!p&&S===l&&re(i,V($),()=>m?d():void f()),null}}let m=!1,v=p=>{m=!0,Ft({cursor:p.cursor,sends:[]},{update({sends:S,removed:$}){let M=new Set([...a.values()].flatMap(A=>A?.id??[]));(S.some(A=>a.has(A.post_id)&&Xd(a.get(A.post_id)??null,A))||$.some(A=>M.has(A)))&&u(!0)},stale:()=>ee(Fo)},t)},f=async()=>{let p=await u(!1);p&&!m&&v(p)};Gt(e,n,d),await f()}function Xd(e,t){return e?.id===t.id?t.status!==e.status||t.fire_at!==e.fire_at:t.status==="scheduled"||t.status==="sending"}function eu(e,t,n){let s=fe(o`<h3>Delete draft?</h3><p class="hint">This permanently deletes the draft and its revisions. This can’t be undone.</p><div class="actions"><button type="button" id="dCancel">Cancel</button><button type="button" class="danger" id="dGo">Delete</button></div>`,n),i=c("#dGo",s.el);c("#dCancel",s.el).onclick=s.close,i.onclick=()=>Z(i,"Deleting\u2026",async()=>{s.clearError();try{await L(`/api/posts/${e}`,{method:"DELETE"}),s.close(),U("Draft deleted"),t()}catch(r){s.error(V(r))}})}var kt="#/docs/production-access",tu="#/docs/production-access/3-answer-awss-questions",nu="#/docs/ses/5-create-the-apps-aws-key",$n="#/template";function jo(e){return e?.provider==="ses"||!!e?.simulation}async function at(e,t){if(!jo(e))return null;try{return await L("/api/provider",{signal:t})}catch{return null}}var lt=e=>e?.state==="sandbox";function qo(e){let t=e.account?.max_24_hour_send;return typeof t=="number"?`, up to ${t.toLocaleString()} a day`:""}function la(e,t=null){if(e.state==="production")return o`<span>Amazon SES <span class="set-pill ok" id="providerState">Production</span>${t}</span>`;if(e.state!=="sandbox")return e.unknown_reason==="permission"?o`<span>Amazon SES${t}</span><span class="muted" id="providerUnread">To show whether SES is in the sandbox, Kestrel’s AWS key needs permission to read your account. <a class="set-link" href="${nu}">Add the permission</a></span>`:o`Amazon SES${t}`;let n=o`<span class="set-pill warn" id="providerState">Sandbox</span>`,s=e.review==="pending"?o`AWS is reviewing your request for production access. Until then, it mails only addresses verified in SES${qo(e)}.`:e.review==="denied"?o`<span class="set-warn">AWS hasn’t granted production access yet. ${ca(e)}: read its reply and answer there.</span> <a class="set-link" href="${tu}">What AWS asks for</a>`:e.review==="failed"?o`<span class="set-warn">AWS didn’t receive your request for production access.</span> <a class="set-link" href="${kt}">Request it again</a>`:o`<span class="set-warn">It mails only addresses verified in SES${qo(e)}.</span> <a class="set-link" href="${kt}">Request production access</a>`;return o`<span>Amazon SES ${n}${t}</span><span class="muted" id="providerReview">${s}</span>`}function ca(e){return e.review_case?`It has replied in AWS Support, case ${e.review_case}`:"It has replied in AWS Support"}var su={account:"from your SES account",setting:"set by SES_MAX_SEND_RATE",default:"Kestrel\u2019s default"};function da(e){if(!e.send_rate)return null;let{value:t,source:n}=e.send_rate;return o`<span>${t} ${t===1?"email":"emails"} a second${qo(e)} <span class="muted">(${su[n]})</span></span>`}var ou={configuration_set_missing:"Send bounces and complaints to the app",configuration_set_paused:"Send bounces and complaints to the app",key_rejected:"Create the app\u2019s AWS key",sending_disabled:"A problem with Amazon SES",sender_unverified:"Verify your sending hostname",account_probation:"A problem with Amazon SES",bounces_not_routed:"Send bounces and complaints to the app"};function Ps(e){return o`${e.message} <a href="#/docs/${e.docs.slug}/${e.docs.anchor}">${ou[e.code]}</a>`}var xn=e=>e.severity==="error";function ua(){return o`Amazon SES is still in the sandbox, so it mails only addresses verified in SES. <a href="${kt}">Request production access</a>`}function pa(e,t){return e?.state==="production"?null:e?.state==="sandbox"&&e.review==="pending"?"Waiting for AWS to grant production access.":e?.state==="sandbox"&&e.review==="denied"?`AWS hasn\u2019t granted it yet. ${ca(e)}: answer there.`:e?.state==="sandbox"&&e.review==="failed"?"AWS didn\u2019t receive your request. Ask again.":t?e?.state==="unknown"&&e.unknown_reason==="permission"?"Kestrel can\u2019t read your SES account, so the SES console\u2019s account dashboard says whether AWS granted it.":null:"Name your publication first: AWS\u2019s reviewer looks at it."}function En(e){return e?.sending?.state==="refusing"?e.sending.refusal:null}function ma(e){return e.found_by==="account_check"?`${e.error} ${e.advice}`:`SES says: ${e.error.replace(/[.!?]?$/,".")} ${e.advice}`}function fa(e){return o`<span class="dash-not-going-out"><strong>Email isn’t going out, and signups are paused.</strong> ${ma(e)} Then <a href="${$n}">send a test email</a>: once SES takes one, signups reopen.</span>`}function ha(){return o`Signups are paused: Amazon SES is refusing email, including confirmations. <a href="#/">See what to fix</a>`}function ba(e,t){let n=e.sending;if(!n)return null;let s=n.last_accepted?o`<span class="muted" id="providerLastAccepted">SES accepted an email ${t(n.last_accepted.at)}.</span>`:null;return n.state==="refusing"&&n.refusal?{pill:o`<span class="set-pill danger" id="providerSending">Refusing</span>`,line:o`<span class="set-error">${ma(n.refusal)} Then <a class="set-link" href="${$n}">send a test email</a>.</span>${s}`}:n.state==="unproven"?{pill:o`<span class="set-pill" id="providerSending">Not proven yet</span>`,line:o`<span>SES hasn’t sent anything yet, so signups are closed. <a class="set-link" href="${$n}">Send a test email</a></span>`}:{pill:null,line:s??o``}}var Wo={pending:"Subscribed but hasn\u2019t clicked the confirmation email.",confirmed:"Receives your posts. They confirmed here, or opted in at the service you imported them from.",unsubscribed:"Left the list. They\u2019re sent nothing until they sign up again, or, unless they left by a complaint or an erasure, you add them back and they confirm."},ga={reader:"They unsubscribed themselves, from the link or the button in your email.",publisher:"You unsubscribed them, when they asked you to.",complaint:"They reported your email as spam. Only their own sign-up brings them back, so you can\u2019t add them: ask them to subscribe through your subscribe page.",erased:"You erased them at their request. Only the address is kept, so it isn\u2019t mailed again, and only their own sign-up brings them back.",import:"The list you imported says they left, without saying why."},va="The address isn\u2019t sent your posts, whatever its status. The status stays as it was, greyed while the suppression keeps it from taking effect.",je={bounce:"bounced",import_bounce:"bounced",manual:"blocked"},kn={bounced:"Mail to the address failed, here or at the service it was imported from. You can clear it.",blocked:"Stopped by hand, by you or at the service it was imported from. You can unblock it."},ct={unsubscribe:{label:"Unsubscribe",when:"When they ask you to take them off the list. They can rejoin by subscribing again."},block:{label:"Block",when:"When you decide yourself to stop mailing an address. Their status stays as it was, and you can unblock it."},unblock:{label:"Unblock",when:"When you decide to mail an address you blocked again. They receive your posts only if they\u2019re confirmed."},clear:{label:"Clear",when:"When you know a bounce\u2019s cause is gone, such as a reader who tells you their mailbox is fixed. Mail goes again to an address that failed."},erase:{label:"Erase",when:"When they ask you to delete their data. They\u2019re unsubscribed and only their address is kept. It can\u2019t be undone, and they come back only by subscribing themselves."}};function Ms(e,t){let n=ge(),s=fe(o`<h3>Add subscriber</h3><p class="hint">Starts the normal double opt-in: they get a confirmation email and won’t receive posts until they confirm.</p>${ur()}${n?null:o`<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>`,t),i=c("#aGo",s.el);if(c("#aCancel",s.el).onclick=s.close,i.disabled=n,n){c("#aCancel",s.el).focus();return}let r=c("#addEmail",s.el);r.focus(),i.onclick=()=>Z(i,"Adding\u2026",async()=>{s.clearError();let a=wt(r.value);if(!St(a)){s.error("Enter a valid email address",r);return}try{let l=await L("/api/subscribers",{method:"POST",json:{email:a}});s.close(),U(iu(a,l.action)),e()}catch(l){s.error(V(l),l instanceof te&&l.field==="email"?r:void 0)}})}function iu(e,t){switch(t){case"already_confirmed":return`${e} is already confirmed`;case"suppressed":return`${e} is suppressed, so no confirmation was sent`;case"recently_sent":return`A confirmation went to ${e} a few minutes ago; try again later`;default:return`Confirmation sent to ${e}`}}function ya(e,t,n){let s=fe(o`<h3>Unsubscribe this subscriber?</h3><p class="hint">Use this when <strong>${e.email}</strong> asks you to take them off the list. They stop receiving posts immediately, and can rejoin by subscribing again. To stop mailing them on your own judgment, block the address instead.</p><div class="actions"><button type="button" id="uCancel">Cancel</button><button type="button" class="danger" id="uGo">Unsubscribe</button></div>`,n),i=c("#uGo",s.el);c("#uCancel",s.el).onclick=s.close,i.onclick=()=>Z(i,"Unsubscribing\u2026",async()=>{s.clearError();try{await L(`/api/subscribers/${e.id}/unsubscribe`,{method:"POST"}),s.close(),U(`Unsubscribed ${e.email}`),t()}catch(r){s.error(V(r))}})}function wa(e,t,n){let s=fe(o`<h3>Block this address?</h3><p class="hint">Stops all mail to <strong>${e.email}</strong>, whatever their status, which stays as it is. Use this when you decide yourself to stop mailing them; if they asked to leave, unsubscribe them instead. You can unblock it later.</p><div class="actions"><button type="button" id="bCancel">Cancel</button><button type="button" class="danger" id="bGo">Block</button></div>`,n),i=c("#bGo",s.el);c("#bCancel",s.el).onclick=s.close,i.onclick=()=>Z(i,"Blocking\u2026",async()=>{s.clearError();try{let r=await L("/api/suppressions",{method:"POST",json:{email:e.email}});s.close(),U(r.added?`Blocked ${e.email}`:`${e.email} was already suppressed (${je[r.suppression.reason]})`),t()}catch(r){s.error(V(r))}})}function Sa(e,t,n){let s=e.status==="confirmed"?"They\u2019re confirmed, so they\u2019ll receive your posts again.":"They\u2019ll receive posts again only once they\u2019re confirmed.",i=fe(o`<h3>Unblock this address?</h3><p class="hint">Undoes the block on <strong>${e.email}</strong>. ${s}</p><div class="actions"><button type="button" id="ubCancel">Cancel</button><button type="button" class="primary" id="ubGo">Unblock</button></div>`,n),r=c("#ubGo",i.el);c("#ubCancel",i.el).onclick=i.close,r.onclick=()=>Z(r,"Unblocking\u2026",async()=>{i.clearError();try{await $a(e),i.close(),U(`Unblocked ${e.email}`),t()}catch(a){i.error(V(a))}})}function $a(e){return L("/api/suppressions/lift",{method:"POST",json:{email:e.email,reason:e.suppression?.reason}})}function xa(e,t,n){let s=e.status==="confirmed"?"They\u2019re confirmed, so they\u2019ll receive your posts again.":"They\u2019ll receive posts again only once they\u2019re confirmed.",i=fe(o`<h3>Clear this bounce?</h3><p class="hint">Lets mail go again to <strong>${e.email}</strong>, an address where mail failed. Clear it only when you know the reason is gone, such as a reader who tells you their mailbox is fixed. ${s}</p><div class="actions"><button type="button" id="cCancel">Cancel</button><button type="button" class="danger" id="cGo">Clear</button></div>`,n),r=c("#cGo",i.el);c("#cCancel",i.el).onclick=i.close,r.onclick=()=>Z(r,"Clearing\u2026",async()=>{i.clearError();try{await $a(e),i.close(),U(`Cleared the bounce on ${e.email}`),t()}catch(a){i.error(V(a))}})}function Ea(e,t,n){let s=fe(o`<h3>Erase this subscriber?</h3><p class="hint">Unsubscribes <strong>${e.email}</strong> and deletes how they joined and the evidence for it, and past sends show an erased recipient in its place. Only the address is kept, so it’s never mailed again. This can’t be undone. They can still rejoin by subscribing and confirming themselves, but you can’t add them back.</p><div class="actions"><button type="button" id="eCancel">Cancel</button><button type="button" class="danger" id="eGo">Erase</button></div>`,n),i=c("#eGo",s.el);c("#eCancel",s.el).onclick=s.close,i.onclick=()=>Z(i,"Erasing\u2026",async()=>{s.clearError();try{let r=await L(`/api/subscribers/${e.id}/erase`,{method:"POST"});s.close(),U(au(r)),t()}catch(r){s.error(V(r))}})}var ru={bounce:"bounce",import_bounce:"bounce",manual:"block"};function au({subscriber:e}){let t=e.suppression;return t?`Erased ${e.email}. Its ${ru[t.reason]??"suppression"} still stands`:`Erased ${e.email}`}var lu=1500;function Te(e=!1,t="Copy"){return o`<span>${t}</span>${O(e?"check":"copyout")}`}function _n(e,t){return o`<button type="button" class="copy-btn" data-copy="${e}" aria-label="${t}">${Te()}</button>`}function Jt(e,t,n){return o`<div class="block code-block"><div class="block-head"><span class="block-label">${e}</span>${_n(n,`Copy ${e}`)}</div><pre><code>${t}</code></pre></div>`}function ka(e,t){return o`<div class="code-bare"><pre><code>${e}</code></pre><button type="button" class="copy-btn" data-copy="${t}" data-copy-word="" aria-label="Copy" title="Copy">${Te(!1,"")}</button></div>`}var Vo=new WeakMap;async function Tn(e,t){try{await navigator.clipboard.writeText(t)}catch{U("Couldn\u2019t copy to clipboard");return}let n=Vo.get(e),s=n?.label??e.getAttribute("aria-label")??"Copy",i=e.dataset.copyWord;window.clearTimeout(n?.timer),h(e,Te(!0,i)),e.setAttribute("aria-label","Copied"),e.classList.add("copied");let r=window.setTimeout(()=>{h(e,Te(!1,i)),e.setAttribute("aria-label",s),e.classList.remove("copied"),Vo.delete(e)},lu);Vo.set(e,{label:s,timer:r})}function We(e){e.addEventListener("click",t=>{let n=t.target instanceof Element?t.target.closest("[data-copy]"):null;n&&e.contains(n)&&Tn(n,n.dataset.copy??"")})}var Ko="Connect Amazon SES",Ln="Send a test email",_a="Schedule your first post",Is=e=>`After ${e}`,cu="SES is refusing. See why above.",du="The last notification didn\u2019t arrive. See why above.",Ta=(e,t,n)=>`${e.toLocaleString()} ${e===1?t:n}`;function uu(e){let t=e.provider,n=t?.sending??null,s=lt(t),i=[];i.push({key:"connect",title:Ko,state:e.notConnected?"todo":"done",hint:e.notConnected?"Nothing can be sent, and signups stay closed, until SES is connected.":e.connectedTo}),i.push(e.notConnected?{key:"test",title:Ln,state:"blocked",hint:Is(Ko)}:n?.state==="working"?{key:"test",title:Ln,state:"done",hint:n.last_accepted?`SES accepted an email ${j(n.last_accepted.at)}.`:"SES is accepting email."}:n?.state==="refusing"?{key:"test",title:Ln,state:"attention",hint:cu}:{key:"test",title:Ln,state:"todo",hint:n?`SES is connected but hasn\u2019t sent anything yet. Send one to ${s?"an address verified in SES":"yourself"}.`:"Send one to yourself, and check it arrives."}),i.push({key:"name",title:"Name your publication",state:e.named?"done":"todo",hint:e.named?`Named \u201C${e.name}\u201D.`:`Currently \u201C${e.name}\u201D.`}),i.push({key:"notify",title:"Get notified",state:e.notifyTo?e.notifyFailed?"attention":"done":"todo",hint:e.notifyTo?e.notifyFailed?du:`Notifications go to ${Ta(e.notifyTo,"address","addresses")}.`:null}),(e.notConnected||e.sesAccount)&&i.push({key:"access",title:"Request production access",state:t?.state==="production"?"done":s&&t?.review==="pending"?"waiting":"todo",hint:t?.state==="production"?"AWS granted it.":pa(t,e.named)}),i.push({key:"list",title:"Bring your existing list",state:e.onList?"done":"todo",hint:e.onList?`${Ta(e.onList,"subscriber","subscribers")} on your list.`:"Optional."}),i.push({key:"write",title:"Write your first post",state:e.firstPost!==null?"done":"todo",hint:e.firstPost!==null?`\u201C${e.firstPost||"Untitled"}\u201D.`:null}),i.push({key:"schedule",title:_a,state:e.notConnected?"blocked":e.firstSend?"done":"todo",hint:e.notConnected?Is(Ko):e.firstSend?`Goes out ${j(e.firstSend.fire_at)}.`:s&&n?.state==="working"?"Best after AWS grants access: until then it reaches only verified addresses.":null});let r=e.firstSend,a=r?r.tested_at!==null&&(r.remade_at??0)<=r.tested_at:!1;i.push({key:"post_test",title:"Send a test email for your first post",state:r?a?"done":"todo":"blocked",hint:r?a?"The email that goes out has been tested.":`\u201C${r.subject}\u201D goes out ${j(r.fire_at)}. Send yourself a test of it first.`:Is(_a)});let l=e.notConnected||t!==null&&t.signups!=="open";return i.push({key:"share",title:"Share your subscribe link",state:l?"blocked":"todo",hint:l?Is(Ln):Go(e)?null:"Share it once AWS grants production access."}),i}function pu(e,t,n){if(e.state==="blocked")return null;if(e.state==="done")switch(e.key){case"connect":case"access":case"write":case"schedule":return null;case"list":return o`<button data-nav="#/subscribers">Subscribers</button>`}let s=n?"primary":"";switch(e.key){case"connect":return o`<button class="${s}" data-nav="${Ro}">Connect</button>`;case"test":return o`<button class="${s}" data-nav="${$n}">Send test email</button>`;case"name":case"notify":return o`<button class="${s}" data-nav="#/settings">Settings</button>`;case"access":return o`<button class="${s}" data-nav="${kt}">Docs</button>`;case"list":return o`<button class="${s}" data-nav="#/subscribers/import">Import</button>`;case"write":return o`<button class="${s}" data-act="new-post">New post</button>`;case"schedule":return null;case"post_test":return t.firstSend?o`<button class="${s}" data-nav="#/edit/${t.firstSend.post_id}">Send test</button>`:null;case"share":return Go(t)?o`<button class="${s}" type="button" data-copy="${t.subscribeUrl}" aria-label="Copy the subscribe link">${Te()}</button>`:null}}function La(e,t,n=!1){let s=uu(e),i=s.filter(d=>d.state==="done"),r=n?s:s.filter(d=>d.state!=="done"),a=s.find(d=>(d.state==="todo"||d.state==="attention")&&d.key!=="list"),l=d=>o`<li class="setup-${d.key.replace("_","-")} setup-${d.state}"><div class="setup-step-main"><strong>${d.title}</strong>${d.hint?o`<span class="muted setup-hint">${d.hint}</span>`:null}${d.key==="share"&&d.state!=="blocked"&&Go(e)?o`<code class="setup-url">${e.subscribeUrl}</code>`:null}</div>${pu(d,e,t==="checklist"&&d===a)}</li>`;return o`<div class="card setup${t==="card"?" setup-card":""}">
60
+ <div id="postsPager"></div>`);let s=c("#newPost",e);s.onclick=()=>zo(s);let i=c("#list",e),r=c("#postsPager",e),a=new Map,l=0,d=()=>u(!1);async function u(m){let y=++l;try{let E=await C(`/api/posts?${zt(n)}`,{signal:t});if(y!==l)return null;let R=E.posts;if(a=new Map(R.map(N=>[N.id,kn(N)])),!R.length){let N=n.search||n.status&&n.status!==Go;return f(i,o`<p class="muted">${N?"No drafts match.":"No drafts yet. Create your first draft."}</p>`),f(r,o``),E}f(i,o`<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>${xe("Title","subject",n)}${xe("Status",null,n)}${xe("Scheduled","send.fire_at",n)}${xe("Updated","updated_at",n)}<th></th></tr></thead><tbody>${R.map(N=>{let A=kn(N),P=A?.status==="sending",v=A&&P?`#/sent/${A.id}`:`#/edit/${N.id}`,w=A?.fire_at??null,I=Xt(N,"delete")?"1":"";return o`<tr class="clickable" data-id="${N.id}" data-target="${v}"><td>${xt(o`<a href="${v}">${N.subject||o`<em>untitled</em>`}</a>`,N)}</td><td>${P?Qe("sending"):Qe(N.status)}</td><td class="muted when${w?"":" empty"}">${w?W(w):"\u2014"}</td><td class="muted updated">${W(N.updated_at)}</td><td class="act"><button class="icon" data-menu="${N.id}" data-status="${P?"sending":N.status}" data-delete="${I}" data-target="${v}" aria-label="Post actions" aria-haspopup="menu">⋯</button></td></tr>`})}</tbody></table></div>`),lt(i,n,d);for(let N of D("tr[data-id]",i))N.onclick=A=>{let P=A.target;P instanceof Element&&P.tagName!=="A"&&!P.closest("[data-menu]")&&N.dataset.target&&(location.hash=N.dataset.target)};for(let N of D("[data-menu]",i))N.onclick=A=>{A.stopPropagation();let P=N.dataset.status,v=N.dataset.target??"",w=[{label:P==="sending"?"Watch send":"Open",onClick:()=>{location.hash=v}}];if(N.dataset.delete&&N.dataset.menu){let I=N.dataset.menu;w.push({label:"Delete draft",danger:!0,onClick:()=>pu(I,d,t)})}Qt(N,w,t)};return ct(r,n,E.page,d),E}catch(E){return!m&&y===l&&re(i,z(E),()=>p?d():void h()),null}}let p=!1,g=m=>{p=!0,Vt({cursor:m.cursor,sends:[]},{update({sends:y,removed:E}){let R=new Set([...a.values()].flatMap(A=>A?.id??[]));(y.some(A=>a.has(A.post_id)&&uu(a.get(A.post_id)??null,A))||E.some(A=>R.has(A)))&&u(!0)},stale:()=>ne(Yo)},t)},h=async()=>{let m=await u(!1);m&&!p&&g(m)};Zt(e,n,d),await h()}function uu(e,t){return e?.id===t.id?t.status!==e.status||t.fire_at!==e.fire_at:t.status==="scheduled"||t.status==="sending"}function pu(e,t,n){let s=pe(o`<h3>Delete draft?</h3><p class="hint">This permanently deletes the draft and its revisions. This can’t be undone.</p><div class="actions"><button type="button" id="dCancel">Cancel</button><button type="button" class="danger" id="dGo">Delete</button></div>`,n),i=c("#dGo",s.el);c("#dCancel",s.el).onclick=s.close,i.onclick=()=>J(i,"Deleting\u2026",async()=>{s.clearError();try{await C(`/api/posts/${e}`,{method:"DELETE"}),s.close(),q("Draft deleted"),t()}catch(r){s.error(z(r))}})}var _t="#/docs/production-access",mu="#/docs/production-access/3-answer-awss-questions",fu="#/docs/ses/5-create-the-apps-aws-key",_n="#/template";function Jo(e){return e?.provider==="ses"||!!e?.simulation}async function dt(e,t){if(!Jo(e))return null;try{return await C("/api/provider",{signal:t})}catch{return null}}var ut=e=>e?.state==="sandbox";function Zo(e){let t=e.account?.max_24_hour_send;return typeof t=="number"?`, up to ${t.toLocaleString()} a day`:""}function Sa(e,t=null){if(e.state==="production")return o`<span>Amazon SES <span class="set-pill ok" id="providerState">Production</span>${t}</span>`;if(e.state!=="sandbox")return e.unknown_reason==="permission"?o`<span>Amazon SES${t}</span><span class="muted" id="providerUnread">To show whether SES is in the sandbox, Kestrel’s AWS key needs permission to read your account. <a class="set-link" href="${fu}">Add the permission</a></span>`:o`Amazon SES${t}`;let n=o`<span class="set-pill warn" id="providerState">Sandbox</span>`,s=e.review==="pending"?o`AWS is reviewing your request for production access. Until then, it mails only addresses verified in SES${Zo(e)}.`:e.review==="denied"?o`<span class="set-warn">AWS hasn’t granted production access yet. ${wa(e)}: read its reply and answer there.</span> <a class="set-link" href="${mu}">What AWS asks for</a>`:e.review==="failed"?o`<span class="set-warn">AWS didn’t receive your request for production access.</span> <a class="set-link" href="${_t}">Request it again</a>`:o`<span class="set-warn">It mails only addresses verified in SES${Zo(e)}.</span> <a class="set-link" href="${_t}">Request production access</a>`;return o`<span>Amazon SES ${n}${t}</span><span class="muted" id="providerReview">${s}</span>`}function wa(e){return e.review_case?`It has replied in AWS Support, case ${e.review_case}`:"It has replied in AWS Support"}var hu={account:"from your SES account",setting:"set by SES_MAX_SEND_RATE",default:"Kestrel\u2019s default"};function $a(e){if(!e.send_rate)return null;let{value:t,source:n}=e.send_rate;return o`<span>${t} ${t===1?"email":"emails"} a second${Zo(e)} <span class="muted">(${hu[n]})</span></span>`}var bu={configuration_set_missing:"Send bounces and complaints to the app",configuration_set_paused:"Send bounces and complaints to the app",key_rejected:"Create the app\u2019s AWS key",sending_disabled:"A problem with Amazon SES",sender_unverified:"Verify your sending hostname",account_probation:"A problem with Amazon SES",bounces_not_routed:"Send bounces and complaints to the app"};function Bs(e){return o`${e.message} <a href="#/docs/${e.docs.slug}/${e.docs.anchor}">${bu[e.code]}</a>`}var Tn=e=>e.severity==="error";function Ea(){return o`Amazon SES is still in the sandbox, so it mails only addresses verified in SES. <a href="${_t}">Request production access</a>`}function xa(e,t){return e?.state==="production"?null:e?.state==="sandbox"&&e.review==="pending"?"Waiting for AWS to grant production access.":e?.state==="sandbox"&&e.review==="denied"?`AWS hasn\u2019t granted it yet. ${wa(e)}: answer there.`:e?.state==="sandbox"&&e.review==="failed"?"AWS didn\u2019t receive your request. Ask again.":t?e?.state==="unknown"&&e.unknown_reason==="permission"?"Kestrel can\u2019t read your SES account, so the SES console\u2019s account dashboard says whether AWS granted it.":null:"Name your publication first: AWS\u2019s reviewer looks at it."}function Ln(e){return e?.sending?.state==="refusing"?e.sending.refusal:null}function ka(e){return e.found_by==="account_check"?`${e.error} ${e.advice}`:`SES says: ${e.error.replace(/[.!?]?$/,".")} ${e.advice}`}function _a(e){return o`<span class="dash-not-going-out"><strong>Email isn’t going out, and signups are paused.</strong> ${ka(e)} Then <a href="${_n}">send a test email</a>: once SES takes one, signups reopen.</span>`}function Ta(){return o`Signups are paused: Amazon SES is refusing email, including confirmations. <a href="#/">See what to fix</a>`}function La(e,t){let n=e.sending;if(!n)return null;let s=n.last_accepted?o`<span class="muted" id="providerLastAccepted">SES accepted an email ${t(n.last_accepted.at)}.</span>`:null;return n.state==="refusing"&&n.refusal?{pill:o`<span class="set-pill danger" id="providerSending">Refusing</span>`,line:o`<span class="set-error">${ka(n.refusal)} Then <a class="set-link" href="${_n}">send a test email</a>.</span>${s}`}:n.state==="unproven"?{pill:o`<span class="set-pill" id="providerSending">Not proven yet</span>`,line:o`<span>SES hasn’t sent anything yet, so signups are closed. <a class="set-link" href="${_n}">Send a test email</a></span>`}:{pill:null,line:s??o``}}var Qo={pending:"Subscribed but hasn\u2019t clicked the confirmation email.",confirmed:"Receives your posts. They confirmed here, or opted in at the service you imported them from.",unsubscribed:"Left the list. They\u2019re sent nothing until they sign up again, or, unless they left by a complaint or an erasure, you add them back and they confirm."},Ra={reader:"They unsubscribed themselves, from the link or the button in your email.",publisher:"You unsubscribed them, when they asked you to.",complaint:"They reported your email as spam. Only their own sign-up brings them back, so you can\u2019t add them: ask them to subscribe through your subscribe page.",erased:"You erased them at their request. Only the address is kept, so it isn\u2019t mailed again, and only their own sign-up brings them back.",import:"The list you imported says they left, without saying why."},Ca="The address isn\u2019t sent your posts, whatever its status. The status stays as it was, greyed while the suppression keeps it from taking effect.",qe={bounce:"bounced",import_bounce:"bounced",manual:"blocked"},Rn={bounced:"Mail to the address failed, here or at the service it was imported from. You can clear it.",blocked:"Stopped by hand, by you or at the service it was imported from. You can unblock it."},pt={unsubscribe:{label:"Unsubscribe",when:"When they ask you to take them off the list. They can rejoin by subscribing again."},block:{label:"Block",when:"When you decide yourself to stop mailing an address. Their status stays as it was, and you can unblock it."},unblock:{label:"Unblock",when:"When you decide to mail an address you blocked again. They receive your posts only if they\u2019re confirmed."},clear:{label:"Clear",when:"When you know a bounce\u2019s cause is gone, such as a reader who tells you their mailbox is fixed. Mail goes again to an address that failed."},erase:{label:"Erase",when:"When they ask you to delete their data. They\u2019re unsubscribed and only their address is kept. It can\u2019t be undone, and they come back only by subscribing themselves."}};function Us(e,t){let n=be(),s=pe(o`<h3>Add subscriber</h3><p class="hint">Starts the normal double opt-in: they get a confirmation email and won’t receive posts until they confirm.</p>${yr()}${n?null:o`<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>`,t),i=c("#aGo",s.el);if(c("#aCancel",s.el).onclick=s.close,i.disabled=n,n){c("#aCancel",s.el).focus();return}let r=c("#addEmail",s.el);r.focus(),i.onclick=()=>J(i,"Adding\u2026",async()=>{s.clearError();let a=wt(r.value);if(!$t(a)){s.error("Enter a valid email address",r);return}try{let l=await C("/api/subscribers",{method:"POST",json:{email:a}});s.close(),q(gu(a,l.action)),e()}catch(l){s.error(z(l),l instanceof se&&l.field==="email"?r:void 0)}})}function gu(e,t){switch(t){case"already_confirmed":return`${e} is already confirmed`;case"suppressed":return`${e} is suppressed, so no confirmation was sent`;case"recently_sent":return`A confirmation went to ${e} a few minutes ago; try again later`;default:return`Confirmation sent to ${e}`}}function Aa(e,t,n){let s=pe(o`<h3>Unsubscribe this subscriber?</h3><p class="hint">Use this when <strong>${e.email}</strong> asks you to take them off the list. They stop receiving posts immediately, and can rejoin by subscribing again. To stop mailing them on your own judgment, block the address instead.</p><div class="actions"><button type="button" id="uCancel">Cancel</button><button type="button" class="danger" id="uGo">Unsubscribe</button></div>`,n),i=c("#uGo",s.el);c("#uCancel",s.el).onclick=s.close,i.onclick=()=>J(i,"Unsubscribing\u2026",async()=>{s.clearError();try{await C(`/api/subscribers/${e.id}/unsubscribe`,{method:"POST"}),s.close(),q(`Unsubscribed ${e.email}`),t()}catch(r){s.error(z(r))}})}function Ha(e,t,n){let s=pe(o`<h3>Block this address?</h3><p class="hint">Stops all mail to <strong>${e.email}</strong>, whatever their status, which stays as it is. Use this when you decide yourself to stop mailing them; if they asked to leave, unsubscribe them instead. You can unblock it later.</p><div class="actions"><button type="button" id="bCancel">Cancel</button><button type="button" class="danger" id="bGo">Block</button></div>`,n),i=c("#bGo",s.el);c("#bCancel",s.el).onclick=s.close,i.onclick=()=>J(i,"Blocking\u2026",async()=>{s.clearError();try{let r=await C("/api/suppressions",{method:"POST",json:{email:e.email}});s.close(),q(r.added?`Blocked ${e.email}`:`${e.email} was already suppressed (${qe[r.suppression.reason]})`),t()}catch(r){s.error(z(r))}})}function Pa(e,t,n){let s=e.status==="confirmed"?"They\u2019re confirmed, so they\u2019ll receive your posts again.":"They\u2019ll receive posts again only once they\u2019re confirmed.",i=pe(o`<h3>Unblock this address?</h3><p class="hint">Undoes the block on <strong>${e.email}</strong>. ${s}</p><div class="actions"><button type="button" id="ubCancel">Cancel</button><button type="button" class="primary" id="ubGo">Unblock</button></div>`,n),r=c("#ubGo",i.el);c("#ubCancel",i.el).onclick=i.close,r.onclick=()=>J(r,"Unblocking\u2026",async()=>{i.clearError();try{await Ia(e),i.close(),q(`Unblocked ${e.email}`),t()}catch(a){i.error(z(a))}})}function Ia(e){return C("/api/suppressions/lift",{method:"POST",json:{email:e.email,reason:e.suppression?.reason}})}function Ma(e,t,n){let s=e.status==="confirmed"?"They\u2019re confirmed, so they\u2019ll receive your posts again.":"They\u2019ll receive posts again only once they\u2019re confirmed.",i=pe(o`<h3>Clear this bounce?</h3><p class="hint">Lets mail go again to <strong>${e.email}</strong>, an address where mail failed. Clear it only when you know the reason is gone, such as a reader who tells you their mailbox is fixed. ${s}</p><div class="actions"><button type="button" id="cCancel">Cancel</button><button type="button" class="danger" id="cGo">Clear</button></div>`,n),r=c("#cGo",i.el);c("#cCancel",i.el).onclick=i.close,r.onclick=()=>J(r,"Clearing\u2026",async()=>{i.clearError();try{await Ia(e),i.close(),q(`Cleared the bounce on ${e.email}`),t()}catch(a){i.error(z(a))}})}function Na(e,t,n){let s=pe(o`<h3>Erase this subscriber?</h3><p class="hint">Unsubscribes <strong>${e.email}</strong> and deletes how they joined and the evidence for it, and past sends show an erased recipient in its place. Only the address is kept, so it’s never mailed again. This can’t be undone. They can still rejoin by subscribing and confirming themselves, but you can’t add them back.</p><div class="actions"><button type="button" id="eCancel">Cancel</button><button type="button" class="danger" id="eGo">Erase</button></div>`,n),i=c("#eGo",s.el);c("#eCancel",s.el).onclick=s.close,i.onclick=()=>J(i,"Erasing\u2026",async()=>{s.clearError();try{let r=await C(`/api/subscribers/${e.id}/erase`,{method:"POST"});s.close(),q(yu(r)),t()}catch(r){s.error(z(r))}})}var vu={bounce:"bounce",import_bounce:"bounce",manual:"block"};function yu({subscriber:e}){let t=e.suppression;return t?`Erased ${e.email}. Its ${vu[t.reason]??"suppression"} still stands`:`Erased ${e.email}`}var Su=1500;function Te(e=!1,t="Copy"){return o`<span>${t}</span>${B(e?"check":"copyout")}`}function Cn(e,t){return o`<button type="button" class="copy-btn" data-copy="${e}" aria-label="${t}">${Te()}</button>`}function en(e,t,n){return o`<div class="block code-block"><div class="block-head"><span class="block-label">${e}</span>${Cn(n,`Copy ${e}`)}</div><pre><code>${t}</code></pre></div>`}function Oa(e,t){return o`<div class="code-bare"><pre><code>${e}</code></pre><button type="button" class="copy-btn" data-copy="${t}" data-copy-word="" aria-label="Copy" title="Copy">${Te(!1,"")}</button></div>`}var Xo=new WeakMap;async function An(e,t){try{await navigator.clipboard.writeText(t)}catch{q("Couldn\u2019t copy to clipboard");return}let n=Xo.get(e),s=n?.label??e.getAttribute("aria-label")??"Copy",i=e.dataset.copyWord;window.clearTimeout(n?.timer),f(e,Te(!0,i)),e.setAttribute("aria-label","Copied"),e.classList.add("copied");let r=window.setTimeout(()=>{f(e,Te(!1,i)),e.setAttribute("aria-label",s),e.classList.remove("copied"),Xo.delete(e)},Su);Xo.set(e,{label:s,timer:r})}function je(e){e.addEventListener("click",t=>{let n=t.target instanceof Element?t.target.closest("[data-copy]"):null;n&&e.contains(n)&&An(n,n.dataset.copy??"")})}var ei="Connect Amazon SES",Hn="Send a test email",Da="Schedule your first post",Fs=e=>`After ${e}`,wu="SES is refusing. See why above.",$u="The last notification didn\u2019t arrive. See why above.",Ba=(e,t,n)=>`${e.toLocaleString()} ${e===1?t:n}`;function Eu(e){let t=e.provider,n=t?.sending??null,s=ut(t),i=[];i.push({key:"connect",title:ei,state:e.notConnected?"todo":"done",hint:e.notConnected?"Nothing can be sent, and signups stay closed, until SES is connected.":e.connectedTo}),i.push(e.notConnected?{key:"test",title:Hn,state:"blocked",hint:Fs(ei)}:n?.state==="working"?{key:"test",title:Hn,state:"done",hint:n.last_accepted?`SES accepted an email ${W(n.last_accepted.at)}.`:"SES is accepting email."}:n?.state==="refusing"?{key:"test",title:Hn,state:"attention",hint:wu}:{key:"test",title:Hn,state:"todo",hint:n?`SES is connected but hasn\u2019t sent anything yet. Send one to ${s?"an address verified in SES":"yourself"}.`:"Send one to yourself, and check it arrives."}),i.push({key:"name",title:"Name your publication",state:e.named?"done":"todo",hint:e.named?`Named \u201C${e.name}\u201D.`:`Currently \u201C${e.name}\u201D.`}),i.push({key:"notify",title:"Get notified",state:e.notifyTo?e.notifyFailed?"attention":"done":"todo",hint:e.notifyTo?e.notifyFailed?$u:`Notifications go to ${Ba(e.notifyTo,"address","addresses")}.`:null}),(e.notConnected||e.sesAccount)&&i.push({key:"access",title:"Request production access",state:t?.state==="production"?"done":s&&t?.review==="pending"?"waiting":"todo",hint:t?.state==="production"?"AWS granted it.":xa(t,e.named)}),i.push({key:"list",title:"Bring your existing list",state:e.onList?"done":"todo",hint:e.onList?`${Ba(e.onList,"subscriber","subscribers")} on your list.`:"Optional."}),i.push({key:"write",title:"Write your first post",state:e.firstPost!==null?"done":"todo",hint:e.firstPost!==null?`\u201C${e.firstPost||"Untitled"}\u201D.`:null}),i.push({key:"schedule",title:Da,state:e.notConnected?"blocked":e.firstSend?"done":"todo",hint:e.notConnected?Fs(ei):e.firstSend?`Goes out ${W(e.firstSend.fire_at)}.`:s&&n?.state==="working"?"Best after AWS grants access: until then it reaches only verified addresses.":null});let r=e.firstSend,a=r?r.tested_at!==null&&(r.remade_at??0)<=r.tested_at:!1;i.push({key:"post_test",title:"Send a test email for your first post",state:r?a?"done":"todo":"blocked",hint:r?a?"The email that goes out has been tested.":`\u201C${r.subject}\u201D goes out ${W(r.fire_at)}. Send yourself a test of it first.`:Fs(Da)});let l=e.notConnected||t!==null&&t.signups!=="open";return i.push({key:"share",title:"Share your subscribe link",state:l?"blocked":"todo",hint:l?Fs(Hn):ti(e)?"Share it before your first post goes out, so readers get it.":"Share it once AWS grants production access."}),i}function xu(e,t,n){if(e.state==="blocked")return null;if(e.state==="done")switch(e.key){case"connect":case"access":case"write":case"schedule":return null;case"list":return o`<button data-nav="#/subscribers">Subscribers</button>`}let s=n?"primary":"";switch(e.key){case"connect":return o`<button class="${s}" data-nav="${Oo}">Connect</button>`;case"test":return o`<button class="${s}" data-nav="${_n}">Send test email</button>`;case"name":return o`<button class="${s}" data-nav="#/settings">Settings</button>`;case"notify":return o`<button class="${s}" data-nav="#${Es("notifications")}">Settings</button>`;case"access":return o`<button class="${s}" data-nav="${_t}">Docs</button>`;case"list":return o`<button class="${s}" data-nav="#/subscribers/import">Import</button>`;case"write":return o`<button class="${s}" data-act="new-post">New post</button>`;case"schedule":return null;case"post_test":return t.firstSend?o`<button class="${s}" data-nav="#/edit/${t.firstSend.post_id}">Send test</button>`:null;case"share":return ti(t)?o`<button class="${s}" type="button" data-copy="${t.subscribeUrl}" aria-label="Copy the subscribe link">${Te()}</button>`:null}}function Ua(e,t,n=!1){let s=Eu(e),i=s.filter(d=>d.state==="done"),r=n?s:s.filter(d=>d.state!=="done"),a=s.find(d=>(d.state==="todo"||d.state==="attention")&&d.key!=="list"),l=d=>o`<li class="setup-${d.key.replace("_","-")} setup-${d.state}"><div class="setup-step-main"><strong>${d.title}</strong>${d.hint?o`<span class="muted setup-hint">${d.hint}</span>`:null}${d.key==="share"&&d.state!=="blocked"&&ti(e)?o`<code class="setup-url">${e.subscribeUrl}</code>`:null}</div>${xu(d,e,t==="checklist"&&d===a)}</li>`;return o`<div class="card setup${t==="card"?" setup-card":""}">
61
61
  <h2 class="setup-title">${t==="card"?"Finish setting up":"Set up your publication"}</h2>
62
+ ${t==="card"?o`<p class="muted setup-note">This card stays until your first post goes out to the archive. Email-only sends don’t count.</p>`:null}
62
63
  ${i.length?o`<button type="button" class="setup-fold" data-act="toggle-done" aria-controls="setupSteps" aria-expanded="${n?"true":"false"}" aria-label="${n?"Hide":"Show"} ${i.length} done ${i.length===1?"step":"steps"}"><span class="setup-fold-mark" aria-hidden="true">✓</span><span>${i.length} done</span></button>`:null}
63
64
  <ol class="setup-steps" id="setupSteps">${r.map(l)}</ol>
64
- </div>`}var Go=e=>!e.sesAccount||e.provider?.state==="production";function Ra(e){return e?.provider==="ses"?e.aws_region?`Sends through Amazon SES in ${e.aws_region}.`:"Sends through Amazon SES.":"Sends through local development\u2019s stand-in, which delivers nothing."}function Ca(e){return e.filter(t=>t.status==="scheduled"&&!t.email_only).sort((t,n)=>t.fire_at-n.fire_at)[0]??null}function mu(e,t=!1,n=null,s=!1,i=null){let r=[],a=e.filter(R=>Re(R,"missed"));a.length&&r.push({level:"red",text:`${a.length} scheduled send${a.length===1?"":"s"} passed the fire time without going out.`});let l=e.flatMap(R=>{let F=he(R,"wedged");return F?[{s:R,count:F.count}]:[]}),[d]=l;if(d){let R=l.reduce((K,b)=>K+b.count,0),F=R===1?"delivery":"deliveries";r.push({level:"red",text:l.length===1?o`<a href="#/sent/${d.s.id}">${d.s.subject}</a> has ${R} ambiguous ${F} awaiting a decision; resolve ${R===1?"it":"them"} on its page.`:o`${R} ambiguous ${F} across <a href="#/sent">${l.length} sends</a> await a decision; resolve them on the Sent page.`})}let u=ge(),m=e.flatMap(R=>{let F=he(R,"refused");return F?[{s:R,c:F}]:[]}),v=u?m.filter(R=>R.c.cause==="not_connected"):[],f=m.filter(R=>!v.includes(R)),p=R=>R.length===1&&R[0]?o`<a href="#/sent/${R[0].s.id}">${R[0].s.subject}</a>`:o`<a href="#/sent">${R.length} sends</a>`;v.length&&r.push({level:"red",text:o`<span class="dash-not-connected">${fs(p(v),v.length)}</span>`});let[S]=f;S&&r.push({level:"red",text:o`${p(f)}: ${S.c.message}`});let $=e.filter(R=>Re(R,"stuck")&&!wn(R)),[M]=$;M&&r.push({level:"amber",text:$.length===1?o`<a href="#/sent/${M.id}">${M.subject}</a>: ${he(M,"stuck")?.message}`:o`<a href="#/sent">${$.length} sends</a> have handed off no one for a while, with no retry or provider problem to explain it.`});let P=e.flatMap(R=>{let F=he(R,"provider_unavailable");return F?.severity==="warn"?[{s:R,c:F}]:[]}),[A]=P;A&&r.push({level:"amber",text:o`<a href="#/sent/${A.s.id}">${A.s.subject}</a>: ${A.c.message}`});let H=e.flatMap(R=>{let F=he(R,"bounce_spike");return F?[{s:R,c:F}]:[]}),[y]=H;y&&r.push({level:"amber",text:o`<a href="#/sent/${y.s.id}">${y.s.subject}</a>: ${y.c.message}`});let w=e.flatMap(R=>{let F=he(R,"no_receipts");return F?[{s:R,c:F}]:[]}),[T]=w;T&&r.push({level:"amber",text:w.length===1?o`<a href="#/sent/${T.s.id}">${T.s.subject}</a>: ${T.c.message}`:o`<a href="#/sent">${w.length} sends</a> have had no delivery receipt long after they went out. The provider's events may not be reaching this app, so its bounces suppress no one and its complaints unsubscribe no one.`}),u&&!v.length&&!t&&r.push({level:"amber",text:o`<span class="dash-not-connected">No email provider connected, so nothing can be sent yet. ${Ae()} to start sending.</span>`});let D=En(n);D&&r.push({level:"red",text:fa(D)});for(let R of n?.warnings??[])D&&xn(R)||r.push({level:xn(R)?"red":"amber",text:o`<span class="dash-provider-warning">${Ps(R)}</span>`});return lt(n)&&!s&&r.push({level:"amber",text:o`<span class="dash-sandbox">${ua()}</span>`}),i&&r.push({level:"amber",text:o`<span class="dash-notify-failed">Kestrel’s last notification didn’t arrive: ${i.error.replace(/[.!?]?$/,".")} Check <a href="#/settings">Settings → Notifications</a>.</span>`}),r}function fu(e){if(!e.length)return o``;let t=e.some(n=>n.level==="red")?"red":"amber";return o`<div class="health ${t}"><span class="health-dot">⚠️</span><div>${e.map(n=>o`<div>${n.text}</div>`)}</div></div>`}var Aa=e=>e.filter(t=>t.status==="sending"&&!wn(t));async function zo(e,t){ie(t,"Dashboard");let n=()=>{t.aborted||ee(zo)};h(e,o`<div class="dash" id="dash"><p class="muted">Loading…</p></div>`);let s=c("#dash",e),i,r,a,l,d;try{let[k,G,q,ne]=await Promise.all([L("/api/posts?limit=200",{signal:t}),L("/api/sends?limit=200",{signal:t}),L("/api/subscribers",{signal:t}),at(Y.appConfig?.deployment,t)]);d=ne,i=k.posts,r=G,a=q.counts,l=q.workers_plan_advice}catch(k){re(s,V(k),n);return}let u=r.sends,m=new Map,v=_o(Y.appConfig),f=Y.appConfig?.deployment??null,p=()=>u.some(k=>(k.status==="sending"||k.status==="sent")&&!k.email_only),S=()=>{let k=Y.appConfig,G=k?.notification_status.last_failure??null;return{notConnected:ge(),sesAccount:!ge()&&jo(f),provider:d,name:v.name,named:!!k?.settings.publication?.name?.trim(),notifyTo:k?.settings.notifications.to.length??0,notifyFailed:G!==null,onList:a.confirmed+a.pending,connectedTo:Ra(f),firstPost:i.filter(q=>!q.email_only).sort((q,ne)=>q.created_at-ne.created_at)[0]?.subject??null,firstSend:Ca(u),subscribeUrl:`${f?.app_origin||location.origin}/subscribe`}},$=()=>!p(),M=()=>fu(mu(qt(u,m),$()&&ge(),d,$(),Y.appConfig?.notification_status.last_failure??null)),P=!1,A=(k=!1)=>{let G=c("#dashSetup",s),q=k||document.activeElement?.closest("#dashSetup [data-act='toggle-done']")!=null,ne=i.length?"card":"checklist";h(G,ne==="checklist"||$()?La(S(),ne,P):o``),Fa(G,n,t);let He=G.querySelector("[data-act='toggle-done']");He&&(He.onclick=()=>{P=!P,A(!0)},q&&He.focus())};if(!i.length){h(s,o`<div class="dash-head"><div><h1>${v.name}</h1>${v.tagline?o`<p class="muted dash-tagline">${v.tagline}</p>`:null}<p class="muted">Let’s get your first post out the door.</p></div></div><div id="dashHealth">${M()}</div><div id="dashSetup"></div><section class="dash-section"><h2>API access</h2>${Ua(!1)}</section>`),A(),Ba(s,n,t);return}let H=[{label:"Confirmed",emph:!0,v:a.confirmed,filter:"confirmed"},{label:"Pending",v:a.pending,filter:"pending"},{label:"Unsubscribed",v:a.unsubscribed,filter:"unsubscribed"},{label:"Suppressed",v:a.suppressed,filter:"suppressed"}],y=o`<div class="tiles">${H.map(k=>o`<a class="tile${k.emph?" tile-emph":""}" href="#/subscribers/${k.filter}"><span class="tile-n">${k.v}</span><span class="tile-label">${k.label}${k.sub?o`<span class="tile-sub">${k.sub}</span>`:null}</span></a>`)}</div>`,w=i.filter(k=>k.status==="draft").slice(0,5),T=w.length?o`<div class="table-wrap"><table class="dash-drafts"><tbody>${w.map(k=>o`<tr class="clickable" data-id="${k.id}"><td>${xt(o`<a href="#/edit/${k.id}">${k.subject||o`<em>untitled</em>`}</a>`,k)}</td><td class="muted edited">edited ${j(k.updated_at)}</td></tr>`)}</tbody></table></div>`:o`<p class="muted">No drafts in progress.</p>`,D=f?.app_origin||location.origin,R=o`<div class="card pub-card">
65
- <div class="pub-row"><code class="pub-val">${D}</code>${_n(D,"Copy the publication\u2019s address")}</div>
65
+ </div>`}var ti=e=>!e.sesAccount||e.provider?.state==="production";function Fa(e){return e?.provider==="ses"?e.aws_region?`Sends through Amazon SES in ${e.aws_region}.`:"Sends through Amazon SES.":"Sends through local development\u2019s stand-in, which delivers nothing."}function qa(e){return e.filter(t=>t.status==="scheduled"&&!t.email_only).sort((t,n)=>t.fire_at-n.fire_at)[0]??null}function ku(e,t=!1,n=null,s=!1,i=null){let r=[],a=e.filter(x=>Ce(x,"missed"));a.length&&r.push({level:"red",text:`${a.length} scheduled send${a.length===1?"":"s"} passed the fire time without going out.`});let l=e.flatMap(x=>{let O=fe(x,"wedged");return O?[{s:x,count:O.count}]:[]}),[d]=l;if(d){let x=l.reduce((V,S)=>V+S.count,0),O=x===1?"delivery":"deliveries";r.push({level:"red",text:l.length===1?o`<a href="#/sent/${d.s.id}">${d.s.subject}</a> has ${x} ambiguous ${O} awaiting a decision; resolve ${x===1?"it":"them"} on its page.`:o`${x} ambiguous ${O} across <a href="#/sent">${l.length} sends</a> await a decision; resolve them on the Sent page.`})}let u=be(),p=e.flatMap(x=>{let O=fe(x,"refused");return O?[{s:x,c:O}]:[]}),g=u?p.filter(x=>x.c.cause==="not_connected"):[],h=p.filter(x=>!g.includes(x)),m=x=>x.length===1&&x[0]?o`<a href="#/sent/${x[0].s.id}">${x[0].s.subject}</a>`:o`<a href="#/sent">${x.length} sends</a>`;g.length&&r.push({level:"red",text:o`<span class="dash-not-connected">${vs(m(g),g.length)}</span>`});let[y]=h;y&&r.push({level:"red",text:o`${m(h)}: ${y.c.message}`});let E=e.filter(x=>Ce(x,"stuck")&&!xn(x)),[R]=E;R&&r.push({level:"amber",text:E.length===1?o`<a href="#/sent/${R.id}">${R.subject}</a>: ${fe(R,"stuck")?.message}`:o`<a href="#/sent">${E.length} sends</a> have handed off no one for a while, with no retry or provider problem to explain it.`});let N=e.flatMap(x=>{let O=fe(x,"provider_unavailable");return O?.severity==="warn"?[{s:x,c:O}]:[]}),[A]=N;A&&r.push({level:"amber",text:o`<a href="#/sent/${A.s.id}">${A.s.subject}</a>: ${A.c.message}`});let P=e.flatMap(x=>{let O=fe(x,"bounce_spike");return O?[{s:x,c:O}]:[]}),[v]=P;v&&r.push({level:"amber",text:o`<a href="#/sent/${v.s.id}">${v.s.subject}</a>: ${v.c.message}`});let w=e.flatMap(x=>{let O=fe(x,"no_receipts");return O?[{s:x,c:O}]:[]}),[I]=w;I&&r.push({level:"amber",text:w.length===1?o`<a href="#/sent/${I.s.id}">${I.s.subject}</a>: ${I.c.message}`:o`<a href="#/sent">${w.length} sends</a> have had no delivery receipt long after they went out. The provider's events may not be reaching this app, so its bounces suppress no one and its complaints unsubscribe no one.`}),u&&!g.length&&!t&&r.push({level:"amber",text:o`<span class="dash-not-connected">No email provider connected, so nothing can be sent yet. ${He()} to start sending.</span>`});let j=Ln(n);j&&r.push({level:"red",text:_a(j)});for(let x of n?.warnings??[])j&&Tn(x)||r.push({level:Tn(x)?"red":"amber",text:o`<span class="dash-provider-warning">${Bs(x)}</span>`});return ut(n)&&!s&&r.push({level:"amber",text:o`<span class="dash-sandbox">${Ea()}</span>`}),i&&r.push({level:"amber",text:o`<span class="dash-notify-failed">Kestrel’s last notification didn’t arrive: ${i.error.replace(/[.!?]?$/,".")} Check <a href="#${Es("notifications")}">Settings → Notifications</a>.</span>`}),r}function _u(e){if(!e.length)return o``;let t=e.some(n=>n.level==="red")?"red":"amber";return o`<div class="health ${t}"><span class="health-dot">⚠️</span><div>${e.map(n=>o`<div>${n.text}</div>`)}</div></div>`}var ja=e=>e.filter(t=>t.status==="sending"&&!xn(t));async function ni(e,t){ie(t,"Dashboard");let n=()=>{t.aborted||ne(ni)};f(e,o`<div class="dash" id="dash"><p class="muted">Loading…</p></div>`);let s=c("#dash",e),i,r,a,l,d;try{let[k,K,F,ee]=await Promise.all([C("/api/posts?limit=200",{signal:t}),C("/api/sends?limit=200",{signal:t}),C("/api/subscribers",{signal:t}),dt(Z.appConfig?.deployment,t)]);d=ee,i=k.posts,r=K,a=F.counts,l=F.workers_plan_advice}catch(k){re(s,z(k),n);return}let u=r.sends,p=new Map,g=Io(Z.appConfig),h=Z.appConfig?.deployment??null,m=()=>u.some(k=>(k.status==="sending"||k.status==="sent")&&!k.email_only),y=()=>{let k=Z.appConfig,K=k?.notification_status.last_failure??null;return{notConnected:be(),sesAccount:!be()&&Jo(h),provider:d,name:g.name,named:!!k?.settings.publication?.name?.trim(),notifyTo:k?.settings.notifications.to.length??0,notifyFailed:K!==null,onList:a.confirmed+a.pending,connectedTo:Fa(h),firstPost:i.filter(F=>!F.email_only).sort((F,ee)=>F.created_at-ee.created_at)[0]?.subject??null,firstSend:qa(u),subscribeUrl:`${h?.app_origin||location.origin}/subscribe`}},E=()=>!m(),R=()=>_u(ku(Wt(u,p),E()&&be(),d,E(),Z.appConfig?.notification_status.last_failure??null)),N=!1,A=(k=!1)=>{let K=c("#dashSetup",s),F=k||document.activeElement?.closest("#dashSetup [data-act='toggle-done']")!=null,ee=i.length?"card":"checklist";f(K,ee==="checklist"||E()?Ua(y(),ee,N):o``),Xa(K,n,t);let Le=K.querySelector("[data-act='toggle-done']");Le&&(Le.onclick=()=>{N=!N,A(!0)},F&&Le.focus())};if(!i.length){f(s,o`<div class="dash-head"><div><h1>${g.name}</h1>${g.tagline?o`<p class="muted dash-tagline">${g.tagline}</p>`:null}<p class="muted">Let’s get your first post out the door.</p></div></div><div id="dashHealth">${R()}</div><div id="dashSetup"></div><section class="dash-section"><h2>API access</h2>${Qa(!1)}</section>`),A(),Ja(s,n,t);return}let P=[{label:"Confirmed",emph:!0,v:a.confirmed,filter:"confirmed"},{label:"Pending",v:a.pending,filter:"pending"},{label:"Unsubscribed",v:a.unsubscribed,filter:"unsubscribed"},{label:"Suppressed",v:a.suppressed,filter:"suppressed"}],v=o`<div class="tiles">${P.map(k=>o`<a class="tile${k.emph?" tile-emph":""}" href="#/subscribers/${k.filter}"><span class="tile-n">${k.v}</span><span class="tile-label">${k.label}${k.sub?o`<span class="tile-sub">${k.sub}</span>`:null}</span></a>`)}</div>`,w=i.filter(k=>k.status==="draft").slice(0,5),I=w.length?o`<div class="table-wrap"><table class="dash-drafts"><tbody>${w.map(k=>o`<tr class="clickable" data-id="${k.id}"><td>${xt(o`<a href="#/edit/${k.id}">${k.subject||o`<em>untitled</em>`}</a>`,k)}</td><td class="muted edited">edited ${W(k.updated_at)}</td></tr>`)}</tbody></table></div>`:o`<p class="muted">No drafts in progress.</p>`,j=h?.app_origin||location.origin,x=o`<div class="card pub-card">
66
+ <div class="pub-row"><code class="pub-val">${j}</code>${Cn(j,"Copy the publication\u2019s address")}</div>
66
67
  <div class="pub-foot"><a href="/" target="_blank" rel="noopener">View publication&nbsp;↗</a></div>
67
- </div>`,F=i.some(k=>k.author_client===ns||k.author_client===ss),K=Ua(F),b=o`<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>`;h(s,o`
68
+ </div>`,O=i.some(k=>k.author_client===rs||k.author_client===as),V=Qa(O),S=o`<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>`;f(s,o`
68
69
  <div class="dash-head">
69
- <div><h1>${v.name}</h1>${v.tagline?o`<p class="muted dash-tagline">${v.tagline}</p>`:null}</div>
70
+ <div><h1>${g.name}</h1>${g.tagline?o`<p class="muted dash-tagline">${g.tagline}</p>`:null}</div>
70
71
  <button class="primary" data-act="new-post">New post</button>
71
72
  </div>
72
- <div id="dashHealth">${M()}</div>
73
+ <div id="dashHealth">${R()}</div>
73
74
  <div id="dashSetup"></div>
74
- <div id="dashActive">${Oa(Aa(qt(u,m)))}</div>
75
- <section class="dash-section"><h2>Subscribers</h2>${y}${l?o`<p class="workers-plan" id="dashWorkersPlan" role="status">${l}</p>`:null}</section>
75
+ <div id="dashActive">${Ya(ja(Wt(u,p)))}</div>
76
+ <section class="dash-section"><h2>Subscribers</h2>${v}${l?o`<p class="workers-plan" id="dashWorkersPlan" role="status">${l}</p>`:null}</section>
76
77
  <div class="dash-cols">
77
- <section class="dash-section"><h2>Scheduled</h2><div id="dashScheduled">${Ia(Ha(u))}</div></section>
78
- <section class="dash-section"><h2>Drafts</h2>${T}</section>
78
+ <section class="dash-section"><h2>Scheduled</h2><div id="dashScheduled">${Ga(Va(u))}</div></section>
79
+ <section class="dash-section"><h2>Drafts</h2>${I}</section>
79
80
  </div>
80
- <section class="dash-section"><h2>Sent</h2><div id="dashSent">${Pa(u)}</div></section>
81
- <section class="dash-section"><h2>Quick actions</h2>${b}</section>
81
+ <section class="dash-section"><h2>Sent</h2><div id="dashSent">${Wa(u)}</div></section>
82
+ <section class="dash-section"><h2>Quick actions</h2>${S}</section>
82
83
  <div class="dash-cols">
83
- <section class="dash-section"><h2>Publication</h2>${R}</section>
84
- <section class="dash-section"><h2>API access</h2>${K}</section>
85
- </div>`),A(),Ba(s,n,t);for(let k of N("tr[data-id]",s))k.onclick=G=>{let q=G.target;q instanceof Element&&q.tagName!=="A"&&!q.closest("button")&&(location.hash=`#/edit/${k.dataset.id}`)};Ma(s),Da(s),Na(s);let E=Cs(s,t);E();let C=0,x=async()=>{let k=++C;try{let G=await L("/api/sends?limit=200",{signal:t});if(k!==C)return;u=G.sends,h(c("#dashScheduled",s),Ia(Ha(u))),Na(s),E(),h(c("#dashSent",s),Pa(u)),Ma(s),A(),W()}catch{}},W=()=>{let k=qt(u,m);h(c("#dashHealth",s),M()),h(c("#dashActive",s),Oa(Aa(k))),Da(s),hu(s,[...m.values()])},oe=k=>k==="scheduled"||k==="due";Ft(r,{update:k=>{for(let G of k.sends)m.set(G.id,G);for(let G of k.removed)m.delete(G);W(),(k.changes.length||k.removed.length||k.sends.some(G=>oe(ot(G))))&&x()},stale:n},t)}var Ha=e=>e.filter(t=>t.status==="scheduled").sort((t,n)=>t.fire_at-n.fire_at);function Pa(e){let t=e.filter(n=>n.status==="sent"||n.status==="sending").slice(0,5);return t.length?o`<div class="table-wrap"><table class="dash-sent"><thead><tr><th>Subject</th><th>Status</th><th class="num">Recipients</th><th class="num">Delivered</th><th></th></tr></thead><tbody>${t.map(n=>{let s=n.email_only?n.links.unlisted:n.links.archive;return o`<tr class="clickable" data-send="${n.id}"><td>${xt(o`<a href="#/sent/${n.id}">${n.subject}</a>`,n)}</td><td>${Xe(n.status)}</td><td class="num">${n.audience.count.toLocaleString()}</td><td class="num delivered">${jt(n.counts)}</td><td class="act">${s?o`<a class="ghost-link" href="${s}" target="_blank" rel="noopener">${n.email_only?o`Web version&nbsp;↗`:o`Archive&nbsp;↗`}</a>`:null}</td></tr>`})}</tbody></table></div>`:o`<p class="muted">No sends yet.</p>`}function Ma(e){for(let t of N("tr[data-send]",e))t.onclick=n=>{let s=n.target;s instanceof Element&&s.tagName!=="A"&&(location.hash=`#/sent/${t.dataset.send}`)}}function hu(e,t){for(let n of t){let s=N("#dashSent tr[data-send]",e).find(i=>i.dataset.send===n.id)?.querySelector("td.delivered");s&&h(s,jt(n.counts))}}function Ia(e){if(!e.length)return o`<p class="muted">Nothing scheduled.</p>`;let t=bs("card");return o`${e.map(n=>o`<div class="card spread clickable nextup sched-card" data-post="${n.post_id}"><div><a class="card-link sched-subj" href="#/edit/${n.post_id}">${n.subject}</a><div class="muted">${Rs(n)} · ${j(n.fire_at)} · ${n.audience.count} recipients${Ls(n)}</div>${t}${Re(n,"remade")?o`<div class="sched-remade-mark">Look changed since your last test</div>`:null}</div></div>`)}`}function Na(e){for(let t of N("#dashScheduled .nextup",e))t.onclick=n=>{let s=n.target;s instanceof Element&&s.tagName!=="A"&&(location.hash=`#/edit/${t.dataset.post}`)}}function Oa(e){return e.length?o`<section class="dash-section"><h2>Active send${e.length===1?"":"s"}</h2>${e.map(t=>As(t,{stuckNote:!1}))}</section>`:o``}function Da(e){for(let t of N("#dashActive .active-card[data-watch]",e))t.onclick=n=>{let s=n.target;s instanceof Element&&s.tagName!=="A"&&(location.hash=`#/sent/${t.dataset.watch}`)}}function Ba(e,t,n){Fa(e,t,n),We(e)}function Fa(e,t,n){for(let s of N("[data-nav]",e))s.onclick=()=>{location.hash=s.dataset.nav??""};for(let s of N("[data-act='new-post']",e))s.onclick=()=>Uo(s);for(let s of N("[data-act='add-sub']",e))s.onclick=()=>Ms(t,n)}function Ua(e){return e?o`<div class="card pub-card">
84
+ <section class="dash-section"><h2>Publication</h2>${x}</section>
85
+ <section class="dash-section"><h2>API access</h2>${V}</section>
86
+ </div>`),A(),Ja(s,n,t);for(let k of D("tr[data-id]",s))k.onclick=K=>{let F=K.target;F instanceof Element&&F.tagName!=="A"&&!F.closest("button")&&(location.hash=`#/edit/${k.dataset.id}`)};Ka(s),Za(s),za(s);let T=Ns(s,t);T();let H=0,$=async()=>{let k=++H;try{let K=await C("/api/sends?limit=200",{signal:t});if(k!==H)return;u=K.sends,f(c("#dashScheduled",s),Ga(Va(u))),za(s),T(),f(c("#dashSent",s),Wa(u)),Ka(s),A(),L()}catch{}},L=()=>{let k=Wt(u,p);f(c("#dashHealth",s),R()),f(c("#dashActive",s),Ya(ja(k))),Za(s),Tu(s,[...p.values()])},G=k=>k==="scheduled"||k==="due";Vt(r,{update:k=>{for(let K of k.sends)p.set(K.id,K);for(let K of k.removed)p.delete(K);L(),(k.changes.length||k.removed.length||k.sends.some(K=>G(rt(K))))&&$()},stale:n},t)}var Va=e=>e.filter(t=>t.status==="scheduled").sort((t,n)=>t.fire_at-n.fire_at);function Wa(e){let t=e.filter(n=>n.status==="sent"||n.status==="sending").slice(0,5);return t.length?o`<div class="table-wrap"><table class="dash-sent"><thead><tr><th>Subject</th><th>Status</th><th class="num">Recipients</th><th class="num">Delivered</th><th></th></tr></thead><tbody>${t.map(n=>{let s=n.email_only?n.links.unlisted:n.links.archive;return o`<tr class="clickable" data-send="${n.id}"><td>${xt(o`<a href="#/sent/${n.id}">${n.subject}</a>`,n)}</td><td>${Qe(n.status)}</td><td class="num">${n.audience.count.toLocaleString()}</td><td class="num delivered">${Kt(n.counts)}</td><td class="act">${s?o`<a class="ghost-link" href="${s}" target="_blank" rel="noopener">${n.email_only?o`Web version&nbsp;↗`:o`Archive&nbsp;↗`}</a>`:null}</td></tr>`})}</tbody></table></div>`:o`<p class="muted">No sends yet.</p>`}function Ka(e){for(let t of D("tr[data-send]",e))t.onclick=n=>{let s=n.target;s instanceof Element&&s.tagName!=="A"&&(location.hash=`#/sent/${t.dataset.send}`)}}function Tu(e,t){for(let n of t){let s=D("#dashSent tr[data-send]",e).find(i=>i.dataset.send===n.id)?.querySelector("td.delivered");s&&f(s,Kt(n.counts))}}function Ga(e){if(!e.length)return o`<p class="muted">Nothing scheduled.</p>`;let t=Ss("card");return o`${e.map(n=>o`<div class="card spread clickable nextup sched-card" data-post="${n.post_id}"><div><a class="card-link sched-subj" href="#/edit/${n.post_id}">${n.subject}</a><div class="muted">${Ms(n)} · ${W(n.fire_at)} · ${at(n.audience.count)}${Is(n)}</div>${t}${Ce(n,"remade")?o`<div class="sched-remade-mark">Look changed since your last test</div>`:null}</div></div>`)}`}function za(e){for(let t of D("#dashScheduled .nextup",e))t.onclick=n=>{let s=n.target;s instanceof Element&&s.tagName!=="A"&&(location.hash=`#/edit/${t.dataset.post}`)}}function Ya(e){return e.length?o`<section class="dash-section"><h2>Active send${e.length===1?"":"s"}</h2>${e.map(t=>Os(t,{stuckNote:!1}))}</section>`:o``}function Za(e){for(let t of D("#dashActive .active-card[data-watch]",e))t.onclick=n=>{let s=n.target;s instanceof Element&&s.tagName!=="A"&&(location.hash=`#/sent/${t.dataset.watch}`)}}function Ja(e,t,n){Xa(e,t,n),je(e)}function Xa(e,t,n){for(let s of D("[data-nav]",e))s.onclick=()=>{location.hash=s.dataset.nav??""};for(let s of D("[data-act='new-post']",e))s.onclick=()=>zo(s);for(let s of D("[data-act='add-sub']",e))s.onclick=()=>Us(t,n)}function Qa(e){return e?o`<div class="card pub-card">
86
87
  <p class="conn-status"><span class="conn-dot" aria-hidden="true"></span>Claude is connected.</p>
87
- <div class="pub-foot pub-links"><a href="#/api-reference">${O("code")}API reference →</a><a href="#/docs/connect-claude/working-with-claude">${O("article")}Working with Claude →</a></div>
88
+ <div class="pub-foot pub-links"><a href="#/api-reference">${B("code")}API reference →</a><a href="#/docs/connect-claude/working-with-claude">${B("article")}Working with Claude →</a></div>
88
89
  </div>`:o`<div class="card pub-card">
89
90
  <p class="pub-note">Let Claude draft, proofread, and schedule your posts.</p>
90
- <div class="pub-foot pub-links"><a href="#/docs/connect-claude">${O("article")}Connect Claude →</a><a href="#/api-reference">${O("code")}API reference →</a></div>
91
- </div>`}var Yo=Math.floor(5e3);var Hg=`Workers Free suits a list of up to about ${1e3.toLocaleString("en-US")} subscribers. Past about ${Yo.toLocaleString("en-US")}, one send can use up the day\u2019s database writes and stop this newsletter until midnight UTC.`,bu="Move to Workers Paid, or, if the account is on it already, set SUBREQUEST_BUDGET to 10000.";function qa(e){return`On the Workers Free plan\u2019s limits, a send to ${e.toLocaleString("en-US")} recipients may use up the day\u2019s database writes and stop this newsletter until midnight UTC, the send included. Workers Free suits a list of up to about ${1e3.toLocaleString("en-US")} subscribers. ${bu}`}function Ns(e){return e.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,80).replace(/-+$/g,"")}function Rn(e){return e.replace(/^https?:\/\//,"")}function ja(){let e=fr("");return e?Rn(e):"/archive/"}function et(e,t){return{emailOnly:e.email_only,slug:e.slug,slugAuto:e.slug_auto,subject:e.subject,slugTaken:t,fixed:e.published_at!==null}}function Zo(e){return e.emailOnly?o`Sends by email only`:o`Sends by email and publishes at <code>${ja()}${e.slug}</code>`}function Jo(e){return O(e.emailOnly?"mail":"link")}var gu="Listed on your home page and in your public archive. Unchecked, it\u2019s sent as email only: anyone with the email can still open its web version.",Wa="Fixed once the post is sent, since people may have its link.";function Va(e,t){return e.fixed?o`<p class="pub-note">${t}</p>`:e.slugAuto&&e.slugTaken?o`<p class="pub-taken">${O("info")}<span>Another post has <code>${e.slugTaken}</code>, so a number was added. You can change it above.</span></p>`:e.slugAuto?o`<p class="pub-note">Follows the subject until you type your own.</p>`:null}function vu(e,t,{include:n=!0,fixedNote:s=Wa}){let i=n?o`<label class="listing-check"><input type="checkbox" id="${t}Include"${e.emailOnly?null:o` checked`}><span><strong>Include in archive</strong><span class="listing-help">${gu}</span></span></label>`:null;return e.emailOnly?o`${i}`:o`${i}<div class="pub-address"><label class="pub-label" for="${t}Slug">Archive address</label><label class="url-field${e.fixed?" readonly":""}" id="${t}Field"><span class="url-base">${ja()}</span><input type="text" id="${t}Slug" value="${e.slug}" spellcheck="false" autocomplete="off" autocapitalize="off"${e.fixed?o` readonly`:null}></label><div id="${t}Note">${Va(e,s)}</div></div>`}function yu(e,t,n,s,{fixedNote:i=Wa,onChange:r}){let a=e.querySelector(`#${n}Include`);a&&(a.onchange=()=>{t.emailOnly=!a.checked,s(),r?.()});let l=e.querySelector(`#${n}Slug`),d=e.querySelector(`#${n}Note`);!l||!d||t.fixed||(l.onfocus=()=>l.select(),l.oninput=()=>{t.slug=l.value,t.slugAuto=!1,e.querySelector(`#${n}Field`)?.classList.remove("is-invalid"),h(d,Va(t,i)??o``)},l.onblur=()=>{l.value.trim()===""&&(t.slugAuto=!0,t.slug=Ns(t.subject)||t.slug,s())})}function Cn(e,t){let n={};return t.emailOnly!==e.emailOnly&&(n.email_only=t.emailOnly),!e.fixed&&!t.emailOnly&&(t.slugAuto&&!e.slugAuto?n.slug_auto=!0:!t.slugAuto&&t.slug!==e.slug&&(n.slug=t.slug)),Object.keys(n).length?n:null}function _t(e,t,n,s={}){let i=()=>{h(e,vu(t,n,s)),yu(e,t,n,i,s)};i()}function An(e,t,n){if(!(n instanceof te)||n.field!=="slug"&&n.field!=="slug_auto")return!1;let s=e.querySelector(`#${t}Note`),i=e.querySelector(`#${t}Slug`);return!s||!i?!1:(e.querySelector(`#${t}Field`)?.classList.add("is-invalid"),h(s,o`<p class="pub-error" role="alert">${n.message}</p>`),i.focus(),!0)}var Tt=V;function Os(e,t,n){let s=e.c_in_flight||0,r=fe(o`<h3>Resolve ${s} ambiguous ${s===1?"delivery":"deliveries"}</h3>
91
+ <div class="pub-foot pub-links"><a href="#/docs/connect-claude">${B("article")}Connect Claude →</a><a href="#/api-reference">${B("code")}API reference →</a></div>
92
+ </div>`}var si=Math.floor(5e3);var zg=`Workers Free suits a list of up to about ${1e3.toLocaleString("en-US")} subscribers. Past about ${si.toLocaleString("en-US")}, one send can use up the day\u2019s database writes and stop this newsletter until midnight UTC.`,Lu="Move to Workers Paid, or, if the account is on it already, set SUBREQUEST_BUDGET to 10000.";function el(e){return`On the Workers Free plan\u2019s limits, a send to ${e.toLocaleString("en-US")} recipients may use up the day\u2019s database writes and stop this newsletter until midnight UTC, the send included. Workers Free suits a list of up to about ${1e3.toLocaleString("en-US")} subscribers. ${Lu}`}var qs={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".gif":"image/gif"};function tl(e){return qs[e.slice(e.lastIndexOf(".")).toLowerCase()]}function js(e){return e.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,80).replace(/-+$/g,"")}function Pn(e){return e.replace(/^https?:\/\//,"")}function nl(){let e=$r("");return e?Pn(e):"/archive/"}function Xe(e,t){return{emailOnly:e.email_only,slug:e.slug,slugAuto:e.slug_auto,subject:e.subject,slugTaken:t,fixed:e.published_at!==null}}function oi(e){return e.emailOnly?o`Sends by email only`:o`Sends by email and publishes at <code>${nl()}${e.slug}</code>`}function ii(e){return B(e.emailOnly?"mail":"link")}var Ru="Listed on your home page and in your public archive. Unchecked, it\u2019s sent as email only: anyone with the email can still open its web version.",sl="Fixed once the post is sent, since people may have its link.";function ol(e,t){return e.fixed?o`<p class="pub-note">${t}</p>`:e.slugAuto&&e.slugTaken?o`<p class="pub-taken">${B("info")}<span>Another post has <code>${e.slugTaken}</code>, so a number was added. You can change it above.</span></p>`:e.slugAuto?o`<p class="pub-note">Follows the subject until you type your own.</p>`:null}function Cu(e,t,{include:n=!0,fixedNote:s=sl}){let i=n?o`<label class="listing-check"><input type="checkbox" id="${t}Include"${e.emailOnly?null:o` checked`}><span><strong>Include in archive</strong><span class="listing-help">${Ru}</span></span></label>`:null;return e.emailOnly?o`${i}`:o`${i}<div class="pub-address"><label class="pub-label" for="${t}Slug">Archive address</label><label class="url-field${e.fixed?" readonly":""}" id="${t}Field"><span class="url-base">${nl()}</span><input type="text" id="${t}Slug" value="${e.slug}" spellcheck="false" autocomplete="off" autocapitalize="off"${e.fixed?o` readonly`:null}></label><div id="${t}Note">${ol(e,s)}</div></div>`}function Au(e,t,n,s,{fixedNote:i=sl,onChange:r}){let a=e.querySelector(`#${n}Include`);a&&(a.onchange=()=>{t.emailOnly=!a.checked,s(),r?.()});let l=e.querySelector(`#${n}Slug`),d=e.querySelector(`#${n}Note`);!l||!d||t.fixed||(l.onfocus=()=>l.select(),l.oninput=()=>{t.slug=l.value,t.slugAuto=!1,e.querySelector(`#${n}Field`)?.classList.remove("is-invalid"),f(d,ol(t,i)??o``)},l.onblur=()=>{l.value.trim()===""&&(t.slugAuto=!0,t.slug=js(t.subject)||t.slug,s())})}function In(e,t){let n={};return t.emailOnly!==e.emailOnly&&(n.email_only=t.emailOnly),!e.fixed&&!t.emailOnly&&(t.slugAuto&&!e.slugAuto?n.slug_auto=!0:!t.slugAuto&&t.slug!==e.slug&&(n.slug=t.slug)),Object.keys(n).length?n:null}function Tt(e,t,n,s={}){let i=()=>{f(e,Cu(t,n,s)),Au(e,t,n,i,s)};i()}function Mn(e,t,n){if(!(n instanceof se)||n.field!=="slug"&&n.field!=="slug_auto")return!1;let s=e.querySelector(`#${t}Note`),i=e.querySelector(`#${t}Slug`);return!s||!i?!1:(e.querySelector(`#${t}Field`)?.classList.add("is-invalid"),f(s,o`<p class="pub-error" role="alert">${n.message}</p>`),i.focus(),!0)}var Lt=z;function Vs(e,t,n){let s=e.c_in_flight||0,r=pe(o`<h3>Resolve ${s} ambiguous ${s===1?"delivery":"deliveries"}</h3>
92
93
  <p class="hint">A transport error left ${s} recipient${s===1?"":"s"} in flight: the request went out but the provider never confirmed, so there’s no way to know whether it was accepted. To avoid mailing anyone twice, the send won’t retry ${s===1?"it":"them"} on its own, so it can’t finish until you decide. Neither choice re-sends this post.</p>
93
94
  <p class="hint">Kestrel can’t tell whether ${s===1?"this email":"these emails"} went out, so unless you know, <strong>Assume not sent</strong> is the safe default.</p>
94
95
  <p class="hint"><strong>Assume not sent</strong>: recorded as unsent. ${s===1?"That reader":"Those readers"} may miss this post but ${s===1?"gets":"get"} your next one, and no one is mailed twice.</p>
95
96
  <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>
96
- <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);c("#rCancel",r.el).onclick=r.close;let a=(u,m,v)=>Z(u,"Resolving\u2026",async()=>{r.clearError();try{let f=await L(`/api/sends/${e.id}/resolve`,{method:"POST",json:{resolution:m}});r.close(),U(f.completed?"Send completed":`Marked ${v}`),t()}catch(f){r.error(Tt(f))}}),l=c("#rUnsent",r.el),d=c("#rAccepted",r.el);l.onclick=()=>a(l,"unsent","not sent"),d.onclick=()=>a(d,"accepted","sent")}async function Ds(e,t,n,s){let i=s;if(!i)try{let S=await L(`/api/posts/${e.post_id}`,{signal:n});i=et(S.post,S.subject_slug_held)}catch(S){n.aborted||U(Tt(S));return}let r=i,a={...r},l=$t(ms()),d=new Date($t(new Date(e.fire_at))),u=fe(o`<h3 id="rsHead">Edit schedule</h3>
97
+ <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);c("#rCancel",r.el).onclick=r.close;let a=(u,p,g)=>J(u,"Resolving\u2026",async()=>{r.clearError();try{let h=await C(`/api/sends/${e.id}/resolve`,{method:"POST",json:{resolution:p}});r.close(),q(h.completed?"Send completed":`Marked ${g}`),t()}catch(h){r.error(Lt(h))}}),l=c("#rUnsent",r.el),d=c("#rAccepted",r.el);l.onclick=()=>a(l,"unsent","not sent"),d.onclick=()=>a(d,"accepted","sent")}async function Ws(e,t,n,s){let i=s;if(!i)try{let y=await C(`/api/posts/${e.post_id}`,{signal:n});i=Xe(y.post,y.subject_slug_held)}catch(y){n.aborted||q(Lt(y));return}let r=i,a={...r},l=Et(gs()),d=new Date(Et(new Date(e.fire_at))),u=pe(o`<h3 id="rsHead">Edit schedule</h3>
97
98
  <p class="hint" id="rsHint">It sends at the time you pick, with a cancelable window until then.</p>
98
- ${mr()}
99
- <label for="rsWhen">Send at</label><input type="datetime-local" id="rsWhen" min="${l}" value="${$t(d)}">
99
+ ${wr()}
100
+ <label for="rsWhen">Send at</label><input type="datetime-local" id="rsWhen" min="${l}" value="${Et(d)}">
100
101
  <div class="dialog-settings" id="rsForm"></div>
101
- <div class="actions"><button type="button" id="rsCancel">Cancel</button><button type="button" class="primary" id="rsGo">Save</button></div>`,n),m=c(".modal",u.el);m.setAttribute("aria-labelledby","rsHead"),_t(c("#rsForm",m),a,"rs");let v=c("#rsGo",m),f=c("#rsWhen",m),p=!1;c("#rsCancel",m).onclick=()=>{u.close(),p&&t()},v.onclick=()=>Z(v,"Saving\u2026",async()=>{u.clearError();let S=f.value,$=S?new Date(S).getTime():Number.NaN;if(Number.isNaN($)){u.error("Pick a valid date & time",f);return}let M=$===d.getTime();if(!M&&ps($)){u.error(us(),f);return}let P=Cn(r,a);if(M&&!P){u.close();return}let A=H=>H instanceof te&&H.status===404?(u.close(),U("This post was deleted elsewhere."),Fe(),!0):!1;if(P)try{await L(`/api/posts/${e.post_id}/settings`,{method:"PUT",json:P})}catch(H){!A(H)&&!An(m,"rs",H)&&u.error(Tt(H));return}if(!M)try{await L(`/api/sends/${e.id}/reschedule`,{method:"POST",json:{fire_at:new Date($).toISOString()}})}catch(H){if(A(H))return;let y=H instanceof te&&H.field==="fire_at";if(P){if(r={...a},p=!0,y){u.error(`Settings saved. ${Tt(H)}`,f);return}u.close(),U(`Settings saved. The send time didn\u2019t change: ${Tt(H)}`),t();return}y?u.error(Tt(H),f):u.error(Tt(H));return}u.close(),U(M?"Saved":"Rescheduled"),t()}),f.focus()}function Ka(e,t={}){return["default-src 'none'","script-src 'none'","object-src 'none'","frame-src 'none'","base-uri 'none'",`form-action ${e}`,t.framed?null:"frame-ancestors 'none'","img-src * data:","style-src 'unsafe-inline' https://fonts.googleapis.com","font-src https://fonts.gstatic.com"].filter(n=>n!==null).join("; ")}var Ga=e=>e.preventDefault(),Qt="allow-same-origin allow-popups allow-popups-to-escape-sandbox";function Xt(e){for(let t of e.querySelectorAll("a[href], area[href]")){if(t.getAttribute("href")?.startsWith("#")){t.addEventListener("click",Ga),t.addEventListener("auxclick",Ga);continue}t.target="_blank",t.rel="noopener noreferrer"}}function wu(e,t){let n=/<head(\s[^>]*)?>/i;return n.test(e)?e.replace(n,s=>s+t):t+e}function Bs(e){return wu(e,`<meta http-equiv="Content-Security-Policy" content="${Ka("'none'",{framed:!0})}">`)}function za(e){return e.replace(/\{\{\s*(?:if\s+)?[\w.]+\s*\}\}/g,t=>`<span class="cx-var">${t}</span>`)}function Su(e){let t=X(e);return t=t.replace(/&quot;[^&]*?&quot;/g,n=>`<span class="cx-str">${n}</span>`),t=t.replace(/(&lt;\/?)([a-zA-Z][\w-]*)/g,(n,s,i)=>`<span class="cx-punct">${s}</span><span class="cx-tag">${i}</span>`),t=t.replace(/(\/?)&gt;/g,(n,s)=>`<span class="cx-punct">${s}&gt;</span>`),za(t)}function $u(e){let t=X(e);return t=t.replace(/&quot;[^&]*?&quot;/g,n=>`<span class="cx-str">${n}</span>`),t=t.replace(/#[0-9a-fA-F]{3,8}\b/g,n=>`<span class="cx-num">${n}</span>`),t=t.replace(/@[\w-]+/g,n=>`<span class="cx-at">${n}</span>`),t=t.replace(/^(\s*)(?![@\s])([^\n{}<]+?)(\s*)\{$/gm,(n,s,i,r)=>`${s}<span class="cx-sel">${i}</span>${r}{`),t=t.replace(/^(\s*)([a-z-]+)(\s*:)/gm,(n,s,i,r)=>`${s}<span class="cx-prop">${i}</span>${r}`),za(t)}function Ya(e){return z(String(e).split(/(<style>[\s\S]*?<\/style>)/).map(t=>{let n=t.match(/^<style>([\s\S]*?)<\/style>$/);return n?`<span class="cx-punct">&lt;</span><span class="cx-tag">style</span><span class="cx-punct">&gt;</span>${$u(n[1]??"")}<span class="cx-punct">&lt;/</span><span class="cx-tag">style</span><span class="cx-punct">&gt;</span>`:Su(t)}).join(""))}function Qo(e){return X(e).replace(/(`[^`\n]+`)|(!?)(\[[^\]\n]*\])\(([^)\n]*)\)|(\*\*|__)([^\n]+?)\5|\*([^*\n]+?)\*|\b_([^_\n]+?)_\b/g,(t,n,s,i,r,a,l,d,u)=>{if(n)return`<span class="cx-md-code">${n}</span>`;if(i!==void 0)return`${s||""}${i}(<span class="cx-md-url">${r}</span>)`;if(a)return`<span class="cx-md-mark">${a}</span><span class="cx-md-strong">${l}</span><span class="cx-md-mark">${a}</span>`;let[m,v]=d!==void 0?["*",d]:["_",u];return`<span class="cx-md-mark">${m}</span><span class="cx-md-em">${v}</span><span class="cx-md-mark">${m}</span>`})}function xu(e){let t=e.match(/^(\s{0,3}#{1,6})(\s.*)?$/);if(t)return`<span class="cx-md-hmark">${X(t[1]??"")}</span><span class="cx-md-htext">${X(t[2]||"")}</span>`;if(/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(e))return`<span class="cx-md-bmark">${X(e)}</span>`;let n=e.match(/^(\s{0,3})((?:>\s?)+)(.*)$/);if(n)return`${X(n[1]??"")}<span class="cx-md-bmark">${X(n[2]??"")}</span>${Qo(n[3]??"")}`;let s=e.match(/^(\s*)([-*+]|\d{1,9}[.)])(\s+)(.*)$/);return s?`${s[1]}<span class="cx-md-bmark">${X(s[2]??"")}</span>${s[3]}${Qo(s[4]??"")}`:Qo(e)}function Za(e){let t=!1;return z(String(e).split(`
102
- `).map(n=>{let s,i=!1,r=n.match(/^(\s*)(```+|~~~+)(.*)$/);if(r){t=!t;let a=r[3]?`<span class="cx-md-bmark">${X(r[3])}</span>`:"";s=`${r[1]}${X(r[2]??"")}${a}`}else t?(i=!0,s=X(n)):s=xu(n);return`<span class="cx-md-ln${i?" cx-md-band":""}">${s}</span>`}).join(""))}var Eu=/(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|("(?:\\.|[^"\\\n])*")(\s*:)?|(?<![\w.-])(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)(?![\w.])|\b(true|false|null)\b|([{}[\],])/g;function Xo(e){let t="",n=0;for(let s of e.matchAll(Eu)){t+=X(e.slice(n,s.index));let[i,r,a,l,d,u]=s;r!==void 0?t+=`<span class="cx-punct">${X(r)}</span>`:a!==void 0?t+=l?`<span class="cx-prop">${X(a)}</span><span class="cx-punct">${X(l)}</span>`:`<span class="cx-str">${X(a)}</span>`:d!==void 0?t+=`<span class="cx-num">${d}</span>`:u!==void 0?t+=`<span class="cx-tag">${u}</span>`:t+=`<span class="cx-punct">${X(i)}</span>`,n=s.index+i.length}return z(t+X(e.slice(n)))}function Ja(e){return Xo(JSON.stringify(e,null,2)??"")}var ku=/(\\\n)|(\\[\s\S])|('[^']*')|("(?:\\[\s\S]|[^"\\])*")|(\$\{?[A-Za-z_]\w*\}?)|(&&|\|\||[|;])|(\n)|(#[^\n]*)|([^\s'"$|&;\\]+)/g,_u=/\$\{?[A-Za-z_]\w*\}?/g;function Us(e){let t="",n=0,s=!0;for(let i of e.matchAll(ku)){t+=X(e.slice(n,i.index));let[r,a,l,d,u,m,v,f,p,S]=i;a!==void 0?t+=`<span class="cx-punct">\\</span>
103
- `:l!==void 0?t+=X(l):d!==void 0?(t+=`<span class="cx-str">${X(d)}</span>`,s=!1):u!==void 0?(t+=`<span class="cx-str">${X(u).replace(_u,'<span class="cx-var">$&</span>')}</span>`,s=!1):m!==void 0?(t+=`<span class="cx-var">${m}</span>`,s=!1):v!==void 0?(t+=`<span class="cx-punct">${X(v)}</span>`,s=!0):f!==void 0?(t+=`
104
- `,s=!0):p!==void 0?t+=`<span class="cx-punct">${X(p)}</span>`:S!==void 0&&(s&&/^[A-Za-z_]\w*=/.test(S)?t+=X(S):s?(t+=`<span class="cx-tag">${X(S)}</span>`,s=!1):S.startsWith("-")?t+=`<span class="cx-num">${X(S)}</span>`:t+=X(S)),n=i.index+r.length}return z(t+X(e.slice(n)))}var Tu={[ns]:"Claude Code",[ss]:"Claude"};function en(e,t,n){return e?e==="service"?n?`the service token ${n}`:"a service token":t===un?e:t===null?`${e} via the API`:`${e} via ${Tu[t]??t}`:null}function Qa(e){return e.kind==="service"?en("service",null,e.token_id)??"a service token":en(e.email??null,e.client)??"a person"}var Lu=[["scheduled_by","scheduled"],["rescheduled_by","rescheduled"],["canceled_by","canceled"],["resolved_by","resolved"]];function Xa(e){let n=Lu.flatMap(([s,i])=>{let r=e[s];return r?[`${i} by ${Qa(r)}`]:[]}).join(" \xB7 ");return n?n.charAt(0).toUpperCase()+n.slice(1):null}function Ru(e,t){return e.kind==="human"&&t?.kind==="human"&&e.email===t.email&&e.client===un}function el(e,t){let n=e.rescheduled_by?{verb:"Rescheduled",actor:e.rescheduled_by}:e.scheduled_by?{verb:"Scheduled",actor:e.scheduled_by}:null;return!n||Ru(n.actor,t)?null:`${n.verb} by ${Qa(n.actor)}`}var Cu={idleMs:5e3,maxMs:3e4};function tl(e,t=Cu){let n=null,s=null,i=()=>{n!==null&&clearTimeout(n),s!==null&&clearTimeout(s),n=null,s=null},r=()=>{i(),e()};return{touch(){n!==null&&clearTimeout(n),n=setTimeout(r,t.idleMs),s===null&&(s=setTimeout(r,t.maxMs))},cancel:i,get pending(){return n!==null||s!==null}}}var Fs=class{constructor(t){this.read=t;this.#e=JSON.stringify(t())}read;#e;snapshot(){return JSON.stringify(this.read())}get dirty(){return this.snapshot()!==this.#e}markSaved(t=this.snapshot()){this.#e=t}};var nl=Promise.resolve();function ei(e){nl=e}function sl(){return nl.then(()=>{},()=>{})}function ol(e){if(!(e instanceof te)||e.status!==409)return null;let t=e.data;return t?.error==="stale_revision"&&typeof t.current_revision=="string"?{kind:"stale",revision:t.current_revision,author:typeof t.author=="string"?t.author:null,client:typeof t.author_client=="string"?t.author_client:null}:{kind:"locked"}}var qs=class{base;warnedRevision=null;constructor(t){this.base=t}saved(t){this.base=t}noteWarned(t){t.kind==="stale"&&(this.warnedRevision=t.revision)}adoptWarned(){this.warnedRevision!==null&&(this.base=this.warnedRevision),this.warnedRevision=null}clearWarning(){this.warnedRevision=null}decide(t,n){if(n.saving||this.base!==n.baseAtRequest)return"ignore";if(t.status!=="draft")return{kind:"locked"};let s=t.revision;return s&&s!==this.base&&s!==this.warnedRevision?{kind:"stale",revision:s,author:t.author,client:t.client}:"fresh"}};function rl(e){return`#/web/post/${encodeURIComponent(e)}`}function al(e,t){if(e!=="post"||!t)return null;try{return decodeURIComponent(t)}catch{return null}}function Au(e){let t=o`<span class="web-bar-tag">Web version</span>`;return e?o`${t} <span class="web-bar-text">Not public yet</span>`:t}function il(e){try{return new URL(e).href}catch{return null}}function ll(e,t,n){let s=t?il(t):null;if(s)for(let i of e.querySelectorAll("a[href], area[href]")){let r=i.getAttribute("href");r&&il(r)===s&&i.setAttribute("href",n)}}function Hu(e){location.replace(e)}async function Pu(e,t){try{return await L(`/api/posts/${encodeURIComponent(e)}`,{signal:t})}catch{return null}}async function ti(e,t,n){ie(n,"Web version");try{await sl();let[s,i]=await Promise.all([Ot(`/api/posts/${encodeURIComponent(e)}/web`,{signal:n}),Pu(e,n)]);if(n.aborted)return;if(i&&i.post.published_at!==null){Hu(i.post.unlisted_url);return}let r=Au(i!==null);h(t,o`<div class="web-page">
102
+ <div class="actions"><button type="button" id="rsCancel">Cancel</button><button type="button" class="primary" id="rsGo">Save</button></div>`,n),p=c(".modal",u.el);p.setAttribute("aria-labelledby","rsHead"),Tt(c("#rsForm",p),a,"rs");let g=c("#rsGo",p),h=c("#rsWhen",p),m=!1;c("#rsCancel",p).onclick=()=>{u.close(),m&&t()},g.onclick=()=>J(g,"Saving\u2026",async()=>{u.clearError();let y=h.value,E=y?new Date(y).getTime():Number.NaN;if(Number.isNaN(E)){u.error("Pick a valid date & time",h);return}let R=E===d.getTime();if(!R&&bs(E)){u.error(hs(),h);return}let N=In(r,a);if(R&&!N){u.close();return}let A=P=>P instanceof se&&P.status===404?(u.close(),q("This post was deleted elsewhere."),Ue(),!0):!1;if(N)try{await C(`/api/posts/${e.post_id}/settings`,{method:"PUT",json:N})}catch(P){!A(P)&&!Mn(p,"rs",P)&&u.error(Lt(P));return}if(!R)try{await C(`/api/sends/${e.id}/reschedule`,{method:"POST",json:{fire_at:new Date(E).toISOString()}})}catch(P){if(A(P))return;let v=P instanceof se&&P.field==="fire_at";if(N){if(r={...a},m=!0,v){u.error(`Settings saved. ${Lt(P)}`,h);return}u.close(),q(`Settings saved. The send time didn\u2019t change: ${Lt(P)}`),t();return}v?u.error(Lt(P),h):u.error(Lt(P));return}u.close(),q(R?"Saved":"Rescheduled"),t()}),h.focus()}function il(e,t={}){return["default-src 'none'","script-src 'none'","object-src 'none'","frame-src 'none'","base-uri 'none'",`form-action ${e}`,t.framed?null:"frame-ancestors 'none'","img-src * data:","style-src 'unsafe-inline' https://fonts.googleapis.com","font-src https://fonts.gstatic.com"].filter(n=>n!==null).join("; ")}var rl=e=>e.preventDefault(),tn="allow-same-origin allow-popups allow-popups-to-escape-sandbox";function nn(e){for(let t of e.querySelectorAll("a[href], area[href]")){if(t.getAttribute("href")?.startsWith("#")){t.addEventListener("click",rl),t.addEventListener("auxclick",rl);continue}t.target="_blank",t.rel="noopener noreferrer"}}function Hu(e,t){let n=/<head(\s[^>]*)?>/i;return n.test(e)?e.replace(n,s=>s+t):t+e}function Ks(e){return Hu(e,`<meta http-equiv="Content-Security-Policy" content="${il("'none'",{framed:!0})}">`)}function al(e){return e.replace(/\{\{\s*(?:if\s+)?[\w.]+\s*\}\}/g,t=>`<span class="cx-var">${t}</span>`)}function Pu(e){let t=te(e);return t=t.replace(/&quot;[^&]*?&quot;/g,n=>`<span class="cx-str">${n}</span>`),t=t.replace(/(&lt;\/?)([a-zA-Z][\w-]*)/g,(n,s,i)=>`<span class="cx-punct">${s}</span><span class="cx-tag">${i}</span>`),t=t.replace(/(\/?)&gt;/g,(n,s)=>`<span class="cx-punct">${s}&gt;</span>`),al(t)}function Iu(e){let t=te(e);return t=t.replace(/&quot;[^&]*?&quot;/g,n=>`<span class="cx-str">${n}</span>`),t=t.replace(/#[0-9a-fA-F]{3,8}\b/g,n=>`<span class="cx-num">${n}</span>`),t=t.replace(/@[\w-]+/g,n=>`<span class="cx-at">${n}</span>`),t=t.replace(/^(\s*)(?![@\s])([^\n{}<]+?)(\s*)\{$/gm,(n,s,i,r)=>`${s}<span class="cx-sel">${i}</span>${r}{`),t=t.replace(/^(\s*)([a-z-]+)(\s*:)/gm,(n,s,i,r)=>`${s}<span class="cx-prop">${i}</span>${r}`),al(t)}function ll(e){return Y(String(e).split(/(<style>[\s\S]*?<\/style>)/).map(t=>{let n=t.match(/^<style>([\s\S]*?)<\/style>$/);return n?`<span class="cx-punct">&lt;</span><span class="cx-tag">style</span><span class="cx-punct">&gt;</span>${Iu(n[1]??"")}<span class="cx-punct">&lt;/</span><span class="cx-tag">style</span><span class="cx-punct">&gt;</span>`:Pu(t)}).join(""))}function ri(e){return te(e).replace(/(`[^`\n]+`)|(!?)(\[[^\]\n]*\])\(([^)\n]*)\)|(\*\*|__)([^\n]+?)\5|\*([^*\n]+?)\*|\b_([^_\n]+?)_\b/g,(t,n,s,i,r,a,l,d,u)=>{if(n)return`<span class="cx-md-code">${n}</span>`;if(i!==void 0)return`${s||""}${i}(<span class="cx-md-url">${r}</span>)`;if(a)return`<span class="cx-md-mark">${a}</span><span class="cx-md-strong">${l}</span><span class="cx-md-mark">${a}</span>`;let[p,g]=d!==void 0?["*",d]:["_",u];return`<span class="cx-md-mark">${p}</span><span class="cx-md-em">${g}</span><span class="cx-md-mark">${p}</span>`})}function Mu(e){let t=e.match(/^(\s{0,3}#{1,6})(\s.*)?$/);if(t)return`<span class="cx-md-hmark">${te(t[1]??"")}</span><span class="cx-md-htext">${te(t[2]||"")}</span>`;if(/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(e))return`<span class="cx-md-bmark">${te(e)}</span>`;let n=e.match(/^(\s{0,3})((?:>\s?)+)(.*)$/);if(n)return`${te(n[1]??"")}<span class="cx-md-bmark">${te(n[2]??"")}</span>${ri(n[3]??"")}`;let s=e.match(/^(\s*)([-*+]|\d{1,9}[.)])(\s+)(.*)$/);return s?`${s[1]}<span class="cx-md-bmark">${te(s[2]??"")}</span>${s[3]}${ri(s[4]??"")}`:ri(e)}function cl(e){let t=!1;return Y(String(e).split(`
103
+ `).map(n=>{let s,i=!1,r=n.match(/^(\s*)(```+|~~~+)(.*)$/);if(r){t=!t;let a=r[3]?`<span class="cx-md-bmark">${te(r[3])}</span>`:"";s=`${r[1]}${te(r[2]??"")}${a}`}else t?(i=!0,s=te(n)):s=Mu(n);return`<span class="cx-md-ln${i?" cx-md-band":""}">${s}</span>`}).join(""))}var Nu=/(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|("(?:\\.|[^"\\\n])*")(\s*:)?|(?<![\w.-])(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)(?![\w.])|\b(true|false|null)\b|([{}[\],])/g;function ai(e){let t="",n=0;for(let s of e.matchAll(Nu)){t+=te(e.slice(n,s.index));let[i,r,a,l,d,u]=s;r!==void 0?t+=`<span class="cx-punct">${te(r)}</span>`:a!==void 0?t+=l?`<span class="cx-prop">${te(a)}</span><span class="cx-punct">${te(l)}</span>`:`<span class="cx-str">${te(a)}</span>`:d!==void 0?t+=`<span class="cx-num">${d}</span>`:u!==void 0?t+=`<span class="cx-tag">${u}</span>`:t+=`<span class="cx-punct">${te(i)}</span>`,n=s.index+i.length}return Y(t+te(e.slice(n)))}function dl(e){return ai(JSON.stringify(e,null,2)??"")}var Ou=/(\\\n)|(\\[\s\S])|('[^']*')|("(?:\\[\s\S]|[^"\\])*")|(\$\{?[A-Za-z_]\w*\}?)|(&&|\|\||[|;])|(\n)|(#[^\n]*)|([^\s'"$|&;\\]+)/g,Du=/\$\{?[A-Za-z_]\w*\}?/g;function Gs(e){let t="",n=0,s=!0;for(let i of e.matchAll(Ou)){t+=te(e.slice(n,i.index));let[r,a,l,d,u,p,g,h,m,y]=i;a!==void 0?t+=`<span class="cx-punct">\\</span>
104
+ `:l!==void 0?t+=te(l):d!==void 0?(t+=`<span class="cx-str">${te(d)}</span>`,s=!1):u!==void 0?(t+=`<span class="cx-str">${te(u).replace(Du,'<span class="cx-var">$&</span>')}</span>`,s=!1):p!==void 0?(t+=`<span class="cx-var">${p}</span>`,s=!1):g!==void 0?(t+=`<span class="cx-punct">${te(g)}</span>`,s=!0):h!==void 0?(t+=`
105
+ `,s=!0):m!==void 0?t+=`<span class="cx-punct">${te(m)}</span>`:y!==void 0&&(s&&/^[A-Za-z_]\w*=/.test(y)?t+=te(y):s?(t+=`<span class="cx-tag">${te(y)}</span>`,s=!1):y.startsWith("-")?t+=`<span class="cx-num">${te(y)}</span>`:t+=te(y)),n=i.index+r.length}return Y(t+te(e.slice(n)))}var Bu=new Map([[rs,"via Claude Code"],[lr,"via the Kestrel CLI"],[as,"via Claude"]]);function sn(e,t,n){return e?e==="service"?n?`the service token ${n}`:"a service token":t===fn?e:t===null?`${e} via the API`:`${e} ${Bu.get(t)??`via ${t}`}`:null}function ul(e){return e.kind==="service"?sn("service",null,e.token_id)??"a service token":sn(e.email??null,e.client)??"a person"}var Uu=[["scheduled_by","scheduled"],["rescheduled_by","rescheduled"],["canceled_by","canceled"],["resolved_by","resolved"]];function pl(e){let n=Uu.flatMap(([s,i])=>{let r=e[s];return r?[`${i} by ${ul(r)}`]:[]}).join(" \xB7 ");return n?n.charAt(0).toUpperCase()+n.slice(1):null}function Fu(e,t){return e.kind==="human"&&t?.kind==="human"&&e.email===t.email&&e.client===fn}function ml(e,t){let n=e.rescheduled_by?{verb:"Rescheduled",actor:e.rescheduled_by}:e.scheduled_by?{verb:"Scheduled",actor:e.scheduled_by}:null;return!n||Fu(n.actor,t)?null:`${n.verb} by ${ul(n.actor)}`}var qu={idleMs:5e3,maxMs:3e4};function fl(e,t=qu){let n=null,s=null,i=()=>{n!==null&&clearTimeout(n),s!==null&&clearTimeout(s),n=null,s=null},r=()=>{i(),e()};return{touch(){n!==null&&clearTimeout(n),n=setTimeout(r,t.idleMs),s===null&&(s=setTimeout(r,t.maxMs))},cancel:i,get pending(){return n!==null||s!==null}}}var zs=class{constructor(t){this.read=t;this.#e=JSON.stringify(t())}read;#e;snapshot(){return JSON.stringify(this.read())}get dirty(){return this.snapshot()!==this.#e}markSaved(t=this.snapshot()){this.#e=t}};var hl=Promise.resolve();function li(e){hl=e}function bl(){return hl.then(()=>{},()=>{})}function gl(e){if(!(e instanceof se)||e.status!==409)return null;let t=e.data;return t?.error==="stale_revision"&&typeof t.current_revision=="string"?{kind:"stale",revision:t.current_revision,author:typeof t.author=="string"?t.author:null,client:typeof t.author_client=="string"?t.author_client:null}:{kind:"locked"}}var Ys=class{base;warnedRevision=null;constructor(t){this.base=t}saved(t){this.base=t}noteWarned(t){t.kind==="stale"&&(this.warnedRevision=t.revision)}adoptWarned(){this.warnedRevision!==null&&(this.base=this.warnedRevision),this.warnedRevision=null}clearWarning(){this.warnedRevision=null}decide(t,n){if(n.saving||this.base!==n.baseAtRequest)return"ignore";if(t.status!=="draft")return{kind:"locked"};let s=t.revision;return s&&s!==this.base&&s!==this.warnedRevision?{kind:"stale",revision:s,author:t.author,client:t.client}:"fresh"}};function yl(e){return`#/web/post/${encodeURIComponent(e)}`}function Sl(e,t){if(e!=="post"||!t)return null;try{return decodeURIComponent(t)}catch{return null}}function ju(e){let t=o`<span class="web-bar-tag">Web version</span>`;return e?o`${t} <span class="web-bar-text">Not public yet</span>`:t}function vl(e){try{return new URL(e).href}catch{return null}}function wl(e,t,n){let s=t?vl(t):null;if(s)for(let i of e.querySelectorAll("a[href], area[href]")){let r=i.getAttribute("href");r&&vl(r)===s&&i.setAttribute("href",n)}}function Vu(e){location.replace(e)}async function Wu(e,t){try{return await C(`/api/posts/${encodeURIComponent(e)}`,{signal:t})}catch{return null}}async function ci(e,t,n){ie(n,"Web version");try{await bl();let[s,i]=await Promise.all([Ut(`/api/posts/${encodeURIComponent(e)}/web`,{signal:n}),Wu(e,n)]);if(n.aborted)return;if(i&&i.post.published_at!==null){Vu(i.post.unlisted_url);return}let r=ju(i!==null);f(t,o`<div class="web-page">
105
106
  <div class="web-bar" role="note"><div class="web-bar-in">${r}</div></div>
106
- <iframe class="web-frame" sandbox="${Qt}" title="Web version" scrolling="no"></iframe>
107
- </div>`);let a=t.querySelector(".web-frame");if(!a)return;a.onload=()=>{let l=a.contentDocument;if(!l||n.aborted)return;Xt(l),ie(n,l.title?`${l.title} (web version)`:"Web version");let d=()=>{let u=l.body?.scrollHeight;u&&(a.style.height=`${u}px`)};if(d(),l.body&&typeof ResizeObserver<"u"){let u=new ResizeObserver(d);u.observe(l.body),Nt(n,()=>u.disconnect())}},a.srcdoc=Bs(s)}catch(s){if(n.aborted)return;h(t,o`<div class="web-error"></div>`);let i=t.querySelector(".web-error");if(i){let a=s instanceof te&&s.status===404?"There\u2019s no post here. It may have been deleted.":V(s);re(i,a,()=>ee((l,d)=>ti(e,l,d)))}}}var tt=V,Mu=[[["heading","Heading"],["bold","Bold (\u2318B)"],["italic","Italic (\u2318I)"]],[["quote","Quote"],["code","Code"],["link","Link (\u2318K)"]],[["ul","Bulleted list"],["ol","Numbered list"],["indent","Indent"]]],Iu=o` readonly`,Nu=o` hidden`,Ou=o` aria-disabled="true"`;async function ni(e,t,n){let s=()=>{n.aborted||ee((g,I)=>ni(e,g,I))},i=!1,r=(g="This post was deleted elsewhere.")=>{i||(i=!0,U(g),n.aborted||js("#/drafts"))};ie(n,"Draft"),h(t,o`<p class="muted">Loading…</p>`);let a,l=null;try{if(a=await L(`/api/posts/${e}`,{signal:n}),n.aborted)return;let g=Bu(a);if(g){js(g);return}}catch(g){if(Oe(g)){r("There\u2019s no post here. It may have been deleted.");return}re(t,tt(g),s);return}let d=Sn(a.post);if(d){try{l=await L(`/api/sends/${d.id}`,{signal:n})}catch(g){if(n.aborted)return;if(Oe(g)){r();return}}if(n.aborted||l&&cl(l.send,s))return}let{post:u,markdown:m}=a,v=Y.session?.principal??null,f=et(u,a.subject_slug_held),p=!Zt(u,"edit"),S=Zt(u,"schedule")!==void 0,$=new qs(u.current_revision),M=Mu.map((g,I)=>o`${I?o`<span class="sep"></span>`:null}${g.map(([_,B])=>o`<button type="button" class="tb" data-fmt="${_}" title="${B}" aria-label="${B}"${p?Ou:null}>${O(_)}</button>`)}`),P=p?Iu:null,A=`${location.origin}${location.pathname}${rl(e)}`;h(t,o`
107
+ <iframe class="web-frame" sandbox="${tn}" title="Web version" scrolling="no"></iframe>
108
+ </div>`);let a=t.querySelector(".web-frame");if(!a)return;a.onload=()=>{let l=a.contentDocument;if(!l||n.aborted)return;nn(l),ie(n,l.title?`${l.title} (web version)`:"Web version");let d=()=>{let u=l.body?.scrollHeight;u&&(a.style.height=`${u}px`)};if(d(),l.body&&typeof ResizeObserver<"u"){let u=new ResizeObserver(d);u.observe(l.body),Bt(n,()=>u.disconnect())}},a.srcdoc=Ks(s)}catch(s){if(n.aborted)return;f(t,o`<div class="web-error"></div>`);let i=t.querySelector(".web-error");if(i){let a=s instanceof se&&s.status===404?"There\u2019s no post here. It may have been deleted.":z(s);re(i,a,()=>ne((l,d)=>ci(e,l,d)))}}}var et=z,Ku=[[["heading","Heading"],["bold","Bold (\u2318B)"],["italic","Italic (\u2318I)"]],[["quote","Quote"],["code","Code"],["link","Link (\u2318K)"]],[["ul","Bulleted list"],["ol","Numbered list"],["indent","Indent"]]],Gu=o` readonly`,zu=o` hidden`,Yu=o` aria-disabled="true"`;async function di(e,t,n){let s=()=>{n.aborted||ne((b,M)=>di(e,b,M))},i=!1,r=(b="This post was deleted elsewhere.")=>{i||(i=!0,q(b),n.aborted||Zs("#/drafts"))};ie(n,"Draft"),f(t,o`<p class="muted">Loading…</p>`);let a,l=null;try{if(a=await C(`/api/posts/${e}`,{signal:n}),n.aborted)return;let b=Ju(a);if(b){Zs(b);return}}catch(b){if(Ne(b)){r("There\u2019s no post here. It may have been deleted.");return}re(t,et(b),s);return}let d=kn(a.post);if(d){try{l=await C(`/api/sends/${d.id}`,{signal:n})}catch(b){if(n.aborted)return;if(Ne(b)){r();return}}if(n.aborted||l&&$l(l.send,s))return}let{post:u,markdown:p}=a,g=Z.session?.principal??null,h=Xe(u,a.subject_slug_held),m=!Xt(u,"edit"),y=Xt(u,"schedule")!==void 0,E=new Ys(u.current_revision),R=Ku.map((b,M)=>o`${M?o`<span class="sep"></span>`:null}${b.map(([_,U])=>o`<button type="button" class="tb" data-fmt="${_}" title="${U}" aria-label="${U}"${m?Yu:null}>${B(_)}</button>`)}`),N=m?Gu:null,A=`${location.origin}${location.pathname}${yl(e)}`;f(t,o`
108
109
  <div class="editor-head">
109
110
  <a href="#/drafts" class="back">← Drafts</a>
110
111
  <div class="editor-head-right">
111
- <span class="email-only-mark" id="headEmailOnly"${u.email_only&&!p?null:Nu}>${O("mail")}Email only</span>
112
+ <span class="email-only-mark" id="headEmailOnly"${u.email_only&&!m?null:zu}>${B("mail")}Email only</span>
112
113
  <a class="ghost btn" id="webLink" href="${A}" target="_blank" rel="noopener" aria-label="Web version (opens in a new tab)">Web version ↗</a>
113
- <button type="button" class="ghost" id="settingsBtn">${O("settings")}<span>Post settings</span></button>
114
+ <button type="button" class="ghost" id="settingsBtn">${B("settings")}<span>Post settings</span></button>
114
115
  </div>
115
116
  </div>
116
- ${p&&d?o`<div class="banner banner-scheduled banner-split"><div class="banner-top"><div class="sched-main"><span id="schedWhen"></span><div class="sched-by" id="schedBy" hidden></div>${bs("banner")}<div class="sched-remade" id="schedRemade" hidden><span id="schedRemadeMsg"></span></div><div class="sched-note" id="schedDailyWrites" hidden><span class="sched-note-mark" aria-hidden="true">⚠️</span><span class="sched-note-msg" id="schedDailyWritesMsg"></span></div><div class="sched-note" id="schedSandbox" hidden><span class="sched-note-mark" aria-hidden="true">⚠️</span><span class="sched-note-msg" id="schedSandboxMsg"></span></div></div><span class="row" id="schedControls"><button type="button" class="ghost" id="rescheduleSchedule">Edit schedule</button><button type="button" class="ghost" id="cancelSchedule">Cancel</button></span></div><div class="banner-bottom sched-dest"><span class="sched-dest-icon" id="schedDestIcon">${Jo(f)}</span><span id="schedDest">${Zo(f)}</span></div></div>`:null}
117
+ ${m&&d?o`<div class="banner banner-scheduled banner-split"><div class="banner-top"><div class="sched-main"><span id="schedWhen"></span><div class="sched-by" id="schedBy" hidden></div>${Ss("banner")}<div class="sched-remade" id="schedRemade" hidden><span id="schedRemadeMsg"></span></div><div class="sched-note" id="schedDailyWrites" hidden><span class="sched-note-mark" aria-hidden="true">⚠️</span><span class="sched-note-msg" id="schedDailyWritesMsg"></span></div><div class="sched-note" id="schedSandbox" hidden><span class="sched-note-mark" aria-hidden="true">⚠️</span><span class="sched-note-msg" id="schedSandboxMsg"></span></div></div><span class="row" id="schedControls"><button type="button" class="ghost" id="rescheduleSchedule">Edit schedule</button><button type="button" class="ghost" id="cancelSchedule">Cancel</button></span></div><div class="banner-bottom sched-dest"><span class="sched-dest-icon" id="schedDestIcon">${ii(h)}</span><span id="schedDest">${oi(h)}</span></div></div>`:null}
117
118
  <div id="freshnessBanner" class="banner banner-conflict" role="alert" hidden></div>
118
119
  <div class="editor-form">
119
120
  <label for="f-subject">Subject</label>
120
- <input id="f-subject" value="${u.subject}" placeholder="Untitled"${P}>
121
+ <input id="f-subject" value="${u.subject}" placeholder="Untitled"${N}>
121
122
 
122
123
  <label for="f-markdown">Body</label>
123
- <div class="composer${p?" composer-locked":""}">
124
+ <div class="composer${m?" composer-locked":""}">
124
125
  <div class="composer-head">
125
126
  <div class="ctabs" role="tablist">
126
- <button type="button" class="ctab active" data-tab="edit" data-text="Edit" role="tab" aria-selected="true"><span class="ctab-label">${O("editable")}Edit</span></button>
127
- <button type="button" class="ctab" data-tab="preview" data-text="Preview" role="tab" aria-selected="false"><span class="ctab-label">${O("preview")}Preview</span></button>
127
+ <button type="button" class="ctab active" data-tab="edit" data-text="Edit" role="tab" aria-selected="true"><span class="ctab-label">${B("editable")}Edit</span></button>
128
+ <button type="button" class="ctab" data-tab="preview" data-text="Preview" role="tab" aria-selected="false"><span class="ctab-label">${B("preview")}Preview</span></button>
128
129
  </div>
129
- <div class="toolbar" role="toolbar" aria-label="Formatting">${M}</div>
130
+ <div class="toolbar" role="toolbar" aria-label="Formatting">${R}</div>
130
131
  </div>
131
- <div class="composer-body${p?" locked":""}" id="composerBody">
132
+ <div class="composer-body${m?" locked":""}" id="composerBody">
132
133
  <pre class="md-hl" id="mdHl" aria-hidden="true"><code></code></pre>
133
- <textarea id="f-markdown" class="editor"${p?null:o` placeholder="Type your post in Markdown…"`}${P}>${m}</textarea>
134
- <iframe id="previewFrame" class="preview" sandbox="${Qt}" title="Email preview" hidden></iframe>
134
+ <textarea id="f-markdown" class="editor"${m?null:o` placeholder="Type your post in Markdown…"`}${N}>${p}</textarea>
135
+ <iframe id="previewFrame" class="preview" sandbox="${tn}" title="Email preview" hidden></iframe>
135
136
  </div>
136
- ${p?o`<div class="composer-foot composer-foot-lock" id="lockFoot" role="status">${O("readonly")}<span>Cancel the schedule to edit</span></div>`:o`<div class="composer-foot" id="dropFoot">${O("paperclip")}<span>Paste, drop, or click to add images</span></div>`}
137
+ ${m?o`<div class="composer-foot composer-foot-lock" id="lockFoot" role="status">${B("readonly")}<span>Cancel the schedule to edit</span></div>`:o`<div class="composer-foot" id="dropFoot">${B("paperclip")}<span>Paste, drop, or click to add images</span></div>`}
137
138
  <input type="file" id="imgInput" accept="image/png,image/jpeg,image/webp,image/gif" multiple hidden>
138
139
  </div>
139
140
  <div id="warnings"></div>
140
- ${p?null:hs("editor-not-connected",o`No email provider connected: you can write and preview, not send. ${Ae()}.`)}
141
+ ${m?null:ys("editor-not-connected",o`No email provider connected: you can write and preview, not send. ${He()}.`)}
141
142
 
142
143
  <div class="actions-bar">
143
- ${p?o`<div class="row test-row"><span class="test-hint" id="testHint" hidden>Not tested yet</span><button type="button" class="secondary" id="testBtn">${O("send")}<span>Send test email</span></button></div>`:o`<div class="row">
144
+ ${m?o`<div class="row test-row"><span class="test-hint" id="testHint" hidden>Not tested yet</span><button type="button" class="secondary" id="testBtn">${B("send")}<span>Send test email</span></button></div>`:o`<div class="row">
144
145
  <button type="button" class="ghost" id="saveBtn">Save draft</button>
145
146
  <span class="save-status" id="saveStatus" aria-live="polite"></span>
146
147
  </div>
147
148
  <div class="row">
148
- <button type="button" class="secondary" id="testBtn">${O("send")}<span>Send test email</span></button>
149
+ <button type="button" class="secondary" id="testBtn">${B("send")}<span>Send test email</span></button>
149
150
  <button type="button" class="primary" id="scheduleBtn">Schedule</button>
150
151
  </div>`}
151
152
  </div>
152
- </div>`);let H=c("#f-subject"),y=()=>ie(n,H.value.trim()||"Untitled");y();let w=c("#f-markdown"),T=c(".toolbar",t),D=c("#previewFrame"),R=c("#mdHl"),F=c("code",R),K=()=>{R.scrollTop=w.scrollTop,R.scrollLeft=w.scrollLeft},b=()=>{h(F,Za(w.value)),K()};w.addEventListener("scroll",K),b();let E=()=>({subject:H.value,markdown:w.value}),C=null;function x(){C?.(),C=null}function W(){return H.value.trim()===""?(C=qe(H,"Add a subject before you schedule."),H.focus(),!1):(x(),!0)}let oe=c("#headEmailOnly"),k=()=>{oe.hidden=p||!f.emailOnly;let g=t.querySelector("#schedDestIcon");g&&h(g,Jo(f));let I=t.querySelector("#schedDest");I&&h(I,Zo(f))},G=async g=>{let I=await L(`/api/posts/${e}/settings`,{method:"PUT",json:g});f=et(I.post,I.subject_slug_held),k(),I.changed&&Fe()},q=async()=>{try{let g=await L(`/api/posts/${e}`,{signal:n});f=et(g.post,g.subject_slug_held),k()}catch{}};c("#settingsBtn").onclick=()=>{let g={...f,subject:H.value},I=fe(o`<h3 id="psHead">Post settings</h3><p class="hint">Neither is part of the email, so both stay open while the post is scheduled.</p><div id="psForm"></div><div class="actions"><button type="button" id="psCancel">Cancel</button><button type="button" class="primary" id="psSave">Save</button></div>`,n),_=c(".modal",I.el);_.setAttribute("aria-labelledby","psHead"),_t(c("#psForm",_),g,"ps"),c("#psCancel",_).onclick=I.close;let B=c("#psSave",_);B.onclick=()=>Z(B,"Saving\u2026",async()=>{I.clearError();let se=Cn(f,g);if(!se){I.close();return}try{await G(se),I.close(),U("Post settings saved")}catch(ce){if(n.aborted||Pt(ce)){I.close();return}An(_,"ps",ce)||I.error(tt(ce))}}),c("#psInclude",_).focus()},p||H.addEventListener("input",()=>{y(),H.value.trim()!==""&&x()});let ne=tl(()=>{De(!0).catch(()=>{})});Nt(n,()=>ne.cancel());let He=N(".ctab",t);function Q(g){for(let I of He){let _=I.dataset.tab===g;I.classList.toggle("active",_),I.setAttribute("aria-selected",_?"true":"false")}w.hidden=g!=="edit",R.hidden=g!=="edit",D.hidden=g!=="preview",T.classList.toggle("off",g!=="edit"),t.querySelector("#dropFoot")?.classList.toggle("off",g!=="edit"),g==="edit"&&K()}async function le(){Q("preview");try{p||await De(!0),D.srcdoc=await Ot(`/api/posts/${e}/preview`),D.onload=()=>{try{let g=D.contentDocument;g&&(Xt(g),ll(g,u.unlisted_url,A),D.style.height=`${g.body.scrollHeight+24}px`)}catch{}}}catch(g){if(n.aborted||Pt(g))return;U(tt(g))}}for(let g of He)g.onclick=()=>g.dataset.tab==="preview"?le():Q("edit");p&&le();let de=p?c("#lockFoot"):null,Pe;function Ce(){de&&(de.classList.remove("nudge"),requestAnimationFrame(()=>de.classList.add("nudge")),clearTimeout(Pe),Pe=setTimeout(()=>de.classList.remove("nudge"),900))}function Me(g,I,_){let B=w.selectionStart,se=w.selectionEnd,ce=w.value.slice(B,se)||_;w.value=w.value.slice(0,B)+g+ce+I+w.value.slice(se),w.focus(),w.selectionStart=B+g.length,w.selectionEnd=B+g.length+ce.length}function pt(g){let I=w.selectionStart,_=w.selectionEnd,B=w.value.lastIndexOf(`
153
- `,I-1)+1,se=(w.value.slice(B,_)||"").split(`
154
- `).map(ce=>g+ce).join(`
155
- `);w.value=w.value.slice(0,B)+se+w.value.slice(_),w.focus(),w.selectionStart=B,w.selectionEnd=B+se.length}function mt(g){if(p){Ce();return}if(Q("edit"),g==="bold")Me("**","**","bold text");else if(g==="italic")Me("*","*","italic text");else if(g==="heading")pt("## ");else if(g==="quote")pt("> ");else if(g==="ul")pt("- ");else if(g==="ol")pt("1. ");else if(g==="indent")pt(" ");else if(g==="link")Me("[","](https://)","link text");else if(g==="code"){let I=w.selectionStart,_=w.selectionEnd;I===_||w.value.slice(I,_).includes(`
156
- `)?Me("```\n","\n```","code"):Me("`","`","code")}po()}for(let g of N(".tb[data-fmt]",t))g.onclick=()=>mt(g.dataset.fmt??"");w.addEventListener("keydown",g=>{if(!(g.metaKey||g.ctrlKey))return;let I=g.key.toLowerCase();I==="b"?(g.preventDefault(),mt("bold")):I==="i"?(g.preventDefault(),mt("italic")):I==="k"&&(g.preventDefault(),mt("link"))});let me=!1,Gn=!1,Ke=!1,nt=null,ft=0,zn=!1,co=0,Ie=p?null:c("#saveBtn"),Ne=p?null:c("#saveStatus"),At=new Fs(E),rn=!1;function uo(){Ne&&(p?(Ne.className="save-status",Ne.textContent=""):rn?(Ne.className="save-status is-saving",Ne.textContent="Saving\u2026"):nt&&me?(Ne.className="save-status is-error",Ne.textContent=`Not saved: ${nt}`):me?(Ne.className="save-status is-dirty",Ne.textContent="Unsaved changes"):(Ne.className="save-status is-saved",Ne.textContent="Saved"))}function Yn(){me=At.dirty,me||(nt=null,Gn=!1,ft=0),uo()}uo();function qi(){ne.touch()}function po(){p||(b(),Yn(),Ke||qi())}let Ht=Promise.resolve();function De(g){let I=Ht.catch(()=>{}).then(()=>Hc(g));return Ht=I,ei(Ht),I}async function Hc(g){if(g&&!At.dirty)return"unchanged";if(Ke)return"refused";ne.cancel(),rn=!0,uo();let I=E(),_=JSON.stringify(I);try{let B={...I,base_revision:$.base},{post:se,subject_slug_held:ce}=await L(`/api/posts/${e}`,{method:"PUT",json:B});return f=et(se,ce),k(),$.saved(se.current_revision),At.markSaved(_),Gn=!1,nt=null,ft=0,g||U("Saved"),"saved"}catch(B){let se=ol(B);if(se)return Vi(se),"refused";if(Oe(B))return r(),"refused";Gn=!0;let ce=B instanceof te&&B.field==="subject"?H:void 0;if(ce?(qe(ce,V(B)),nt="fix the field marked above."):nt=V(B),!($o(B)||B instanceof te&&(B.status>=500||B.status===429)))co+=1,zn=!1,ft=0;else if(!zn){zn=!0,ft=ft?Math.min(ft*2,6e4):5e3;let ae=co;gt(Date.now()+ft,()=>{ae===co&&(zn=!1,nt&&!Ke&&De(!0).catch(()=>{}))},n)}throw B}finally{rn=!1,Yn()}}Ie&&(Ie.onclick=()=>{if(Ke){Ge.scrollIntoView({block:"nearest"});return}Z(Ie,"Saving\u2026",()=>De(!1).catch(()=>{})).finally(Yn)});function ji(){Ie&&(Ke?(Ie.setAttribute("aria-disabled","true"),Ie.title="Saving is paused: choose Reload or Keep editing above"):(Ie.removeAttribute("aria-disabled"),Ie.removeAttribute("title")))}let Pc=()=>{if(ne.cancel(),p||!At.dirty)return;let g=E(),I=JSON.stringify(g);me=!1,Ht=Ht.catch(()=>{}).then(async()=>{if(!At.dirty)return;let _={...g,base_revision:$.base},{post:B}=await L(`/api/posts/${e}`,{method:"PUT",json:_});$.saved(B.current_revision),At.markSaved(I)}).catch(_=>{if(Oe(_)){U("This post was deleted elsewhere, so your last edits weren\u2019t saved.");return}U(_ instanceof te&&_.status===409?"Changed elsewhere, so your edits weren\u2019t saved":`Couldn\u2019t save your changes: ${tt(_)}`)}),ei(Ht)},Mc={dirty:()=>me,beforeLeave(){return i?"leave":me&&(Gn||Ke)?"confirm":(Pc(),"leave")},manualSave(){Ie&&!Ie.disabled&&Ie.click()}};if(!p){for(let g of[H,w])g.addEventListener("input",po);for(let g of[H])g.addEventListener("blur",()=>De(!0).catch(()=>{})),g.addEventListener("keydown",I=>{I.key==="Enter"&&!I.isComposing&&I.keyCode!==229&&(I.preventDefault(),De(!0).catch(()=>{}))})}let Ge=c("#freshnessBanner");function Wi(){Ke=!1,ji(),$.clearWarning(),Ge.hidden=!0,h(Ge,o``)}function Vi(g){if(p)return;if(Ke=!0,nt=null,ji(),$.noteWarned(g),g.kind==="locked")h(Ge,o`<span><span aria-hidden="true">⚠️</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>`);else{let _=en(g.author,g.client);h(Ge,o`<span><span aria-hidden="true">⚠️</span> This draft was changed elsewhere${_?o`, last edited by <strong>${_}</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>`)}Ge.hidden=!1,c("#freshReload",Ge).onclick=()=>{Wi(),s()};let I=Ge.querySelector("#freshKeep");I&&(I.onclick=()=>{$.adoptWarned(),Wi(),Yn(),me&&qi()})}function Pt(g){return Oe(g)?(r(),!0):!1}if(!p){let g=async()=>{if(rn||Ke||i||document.hidden)return;let I=$.base;try{let _=await L(`/api/posts/${e}`,{signal:n}),B=$.decide({status:_.post.status,revision:_.post.current_revision,author:_.post.author,client:_.post.author_client},{saving:rn,baseAtRequest:I});B!=="ignore"&&B!=="fresh"&&Vi(B)}catch(_){Oe(_)&&r()}};bt(1e4,g,n),Uu(g,n)}if(c("#webLink").addEventListener("click",()=>{p||De(!0).catch(()=>{})}),p&&d){let g=l?.send??{...d,conditions:[]},I=c("#schedWhen"),_=c("#schedControls"),B="",se=0,ce=!1,ke=c("#rescheduleSchedule"),ae=c("#cancelSchedule"),J=be=>g.actions===void 0||Qe({actions:g.actions},be),$e=()=>{let be=Date.now(),xe=g.phase==="due"||be>=g.fire_at;xe?ce||(ke.hidden=!0,ae.hidden=!0):(ce=!0,ke.hidden=!J("reschedule"),ae.hidden=!J("cancel")),ke.disabled=xe,ae.disabled=xe,_.hidden=ke.hidden&&ae.hidden;let Le=Du(g,xe),Ye=String(Le);Ye!==B&&(B=Ye,h(I,Le)),!xe&&g.fire_at-be<=1e3&&g.fire_at!==se&&(se=g.fire_at,gt(g.fire_at,$e,n))};$e(),bt(1e3,$e,n);let ht=c("#schedRemade"),Be=c("#testHint"),_e=c("#schedDailyWrites"),an=c("#schedBy"),Mt=()=>{let be=el(g,v);an.hidden=be===null,an.textContent=be??"";let xe=he(g,"remade");ht.hidden=!xe,c("#schedRemadeMsg").textContent=xe?.message??"",Be.hidden=!he(g,"untested");let Le=he(g,"daily_writes");_e.hidden=!Le,c("#schedDailyWritesMsg").textContent=Le?.message??"";let Ye=he(g,"sandbox");c("#schedSandbox").hidden=!Ye,c("#schedSandboxMsg").textContent=Ye?.message??""};Mt();let ln=be=>gn(be,{update(xe,Le){let Ye=!Le&&xe.rev!==g.rev;if(g=xe,Ye&&q(),Le?.to==="sending"||Le?.to==="sent"||Le?.to==="complete"){U("Your send started, so this page switched to its live watch."),js(`#/sent/${xe.id}`);return}if(Le?.to==="canceled"){U("The schedule was canceled elsewhere, so this post is a draft again."),s();return}$e(),Mt()},removed:r,stale:s},n);l?ln(l):Ji(5e3,async()=>{let be=await L(`/api/sends/${d.id}`,{signal:n});return cl(be.send,s)||(g=be.send,$e(),Mt(),ln(be)),!1},n),ke.onclick=()=>{Ds({id:g.id,post_id:e,fire_at:g.fire_at},s,n,{...f})},ae.onclick=()=>Z(ae,"Canceling\u2026",async()=>{try{await L(`/api/sends/${d.id}/cancel`,{method:"POST"}),U("Schedule canceled"),s()}catch(be){Oe(be)?r():be instanceof te&&be.code==="window_closed"?(g={...g,phase:"due"},U("Too late to cancel: the send time has passed, so this email is going out."),Fe()):U(tt(be))}}).finally($e)}async function mo(g){if(g.type.startsWith("image/"))try{let I=new FormData;I.append("file",g);let{image:_}=await L(`/api/posts/${e}/images`,{method:"POST",body:I}),B=w.selectionStart,se=`
157
- ![${g.name}](${_.filename})
158
- `;w.value=w.value.slice(0,B)+se+w.value.slice(B),w.selectionStart=w.selectionEnd=B+se.length,po(),U("Image added")}catch(I){Pt(I)||U(tt(I))}}let ze=c("#composerBody");if(p){let g=_=>_.metaKey||_.ctrlKey?["x","z","y"].includes(_.key.toLowerCase()):_.key.length===1||_.key==="Enter"||_.key==="Backspace"||_.key==="Delete",I=[w,H];for(let _ of I)_.addEventListener("keydown",B=>{g(B)&&Ce()}),_.addEventListener("paste",Ce);ze.addEventListener("dragover",_=>_.preventDefault()),ze.addEventListener("drop",_=>{_.preventDefault(),Ce()})}else{let g=c("#imgInput"),I=c("#dropFoot");ze.addEventListener("dragover",_=>{_.preventDefault(),ze.classList.add("dragover")}),ze.addEventListener("dragleave",_=>{_.target===ze&&ze.classList.remove("dragover")}),ze.addEventListener("drop",_=>{_.preventDefault(),ze.classList.remove("dragover"),Q("edit");for(let B of _.dataTransfer?.files??[])mo(B)}),w.addEventListener("paste",_=>{for(let B of _.clipboardData?.items||[])if(B.type.startsWith("image/")){let se=B.getAsFile();se&&(_.preventDefault(),mo(se))}}),I.onclick=()=>g.click(),g.onchange=()=>{for(let _ of g.files??[])mo(_);g.value=""}}let Ic=c("#warnings");function Nc(g){h(Ic,g?.length?o`<div class="warnings"><strong>Warnings:</strong> ${g.join("; ")}</div>`:o``)}c("#testBtn").onclick=()=>{let g=p?"Delivers the frozen copy that will send, exactly as it will fire, so you can check it in a client. One address per line.":"Delivers the rendered email to real inboxes so you can check it in a client. One address per line.",I=ge(),_=fe(o`<h3>Send a test</h3><p class="hint">${g}</p>${vs()}${I?null:o`<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),B=c("#tGo",_.el);if(c("#tCancel",_.el).onclick=_.close,B.disabled=I,I){c("#tCancel",_.el).focus();return}let se=c("#testTo",_.el);se.focus(),L("/api/settings").then(ce=>{let ke=ce.settings.test_recipients||[];if(ke.length&&!se.value.trim()){se.value=ke.join(`
159
- `),_.clearError();let ae=c("#testDefaultsHint",_.el);ae.textContent="Pre-filled from your default test recipients (Settings).",ae.hidden=!1}}).catch(()=>{}),B.onclick=()=>Z(B,"Sending\u2026",async()=>{_.clearError();let{valid:ce,invalid:ke}=xs(se.value),ae=Es(ke);if(ae){_.error(ae,se);return}if(!ce.length){_.error("Enter at least one email address",se);return}try{p||await De(!0);let J=0,$e=[],ht=null;for(let Be of ce)try{let _e=await L(`/api/posts/${e}/test`,{method:"POST",json:{to:Be}});_e.sent?J++:$e.push({email:Be,message:_e.not_sent?.message??""}),ht=_e.warnings}catch(_e){if(Oe(_e)||_e instanceof te&&_e.status===401)throw _e;$e.push({email:Be,message:V(_e)})}if(Nc(ht),p&&J>0&&Fe(),$e.length){se.value=$e.map(Be=>Be.email).join(`
160
- `),_.error(ks(J,ce.length,$e));return}_.close(),U(Bt(`Test sent to ${J} address${J===1?"":"es"}`))}catch(J){if(Oe(J)){_.close(),Pt(J);return}_.error(tt(J))}})};let Ki=p?null:c("#scheduleBtn");return Ki&&(Ki.onclick=()=>{if(!W())return;let g=$t(ms()),I=$t(new Date(Date.now()+24*3600*1e3)),_=!S,B={...f,subject:H.value};B.slugAuto&&!B.slugTaken&&(B.slug=Ns(B.subject)||B.slug);let se="It sends at the time you pick, with a cancelable window until then.",ce=()=>o`<h3 id="schHead">Schedule this post</h3><p class="hint" id="schHint">${se}</p>${pr()}${Ao()}${Ho()}${_?null:o`<label for="schWhen">Send at</label><input type="datetime-local" id="schWhen" min="${g}" value="${I}"><div class="dialog-settings" id="schForm"></div>`}<div class="actions"><button type="button" id="schCancel">Cancel</button><button type="button" class="primary" id="schGo">Schedule</button></div>${_?null:o`<div class="altrow"><span class="altrow-note">Or send as soon as possible.</span><button type="button" class="linkbtn" id="toSendNow">Send now →</button></div>`}`,ke=I,ae=fe(ce(),n),J=c(".modal",ae.el),$e=null,ht=()=>($e??=L("/api/subscribers?status=confirmed&suppressed=hide&limit=1",{signal:n}).then(ue=>ue.page.total).catch(()=>null),$e),Be=async()=>{if(!J.querySelector("#schDailyWrites"))return;let ue=await ht(),ye=J.querySelector("#schDailyWrites");ye&&ue!==null&&ue>Yo&&(ye.textContent=qa(ue),ye.hidden=!1)},_e=null,an=async()=>{_e??=at(Y.appConfig?.deployment,n);let ue=lt(await _e),ye=J.querySelector("#schSandbox");ye&&ue&&(ye.textContent=ar,ye.hidden=!1)},Mt=()=>{ae.close(),Ge.scrollIntoView({block:"nearest"})},ln=()=>_t(c("#schForm",J),B,"sch"),be=()=>({email_only:B.emailOnly,...Cn(f,B)}),xe=ue=>{An(J,"sch",ue)||ae.error(tt(ue))},Le=()=>Z(c("#schGo",J),"Scheduling\u2026",async()=>{ae.clearError();let ue=c("#schWhen",J),ye=ue.value?new Date(ue.value).getTime():Number.NaN;if(Number.isNaN(ye)){ae.error("Pick a valid date & time",ue);return}if(ps(ye)){ae.error(us(),ue);return}try{if(await De(!0)==="refused"){Mt();return}await L(`/api/posts/${e}/schedule`,{method:"POST",json:{fire_at:new Date(ye).toISOString(),...be()}}),ae.close(),U(Bt(`Scheduled for ${j(ye)}. Send yourself a test.`)),s()}catch(Ze){if(Oe(Ze)){ae.close(),Pt(Ze);return}if(Ze instanceof te&&Ze.field==="fire_at"){ae.error(tt(Ze),c("#schWhen",J));return}xe(Ze)}}),Ye=()=>Z(c("#snGo",J),"Queuing\u2026",async()=>{ae.clearError();try{if(await De(!0)==="refused"){Mt();return}let{send:ue}=await L(`/api/posts/${e}/send`,{method:"POST",json:be()});ae.close(),U(Bt(`Sends at ${j(ue.fire_at)}, cancelable until then.`)),s()}catch(ue){if(Oe(ue)){ae.close(),Pt(ue);return}xe(ue)}});function Gi(){if(J.setAttribute("aria-labelledby","schHead"),c("#schCancel",J).onclick=ae.close,c("#schGo",J).onclick=Le,c("#schGo",J).disabled=_,_){c("#schCancel",J).focus();return}c("#toSendNow",J).onclick=Oc,c("#schWhen",J).value=ke,ln(),c("#schWhen",J).focus(),Be(),an()}async function Oc(){let ue=J.querySelector("#schWhen");ue&&(ke=ue.value),h(J,o`<h3 id="snHead">Send now?</h3><p class="hint">Sends this draft, as it is now, to <strong id="snWho">your subscribers</strong> in ${dr()}. The post locks until then, and you can cancel any time before it starts sending.</p><div class="dialog-settings" id="schForm"></div>${Ao()}${Ho()}<div class="actions"><button type="button" class="linkbtn actions-back" id="toSchedule">← Back to schedule</button><button type="button" id="snCancel">Cancel</button><button type="button" class="primary" id="snGo">Send now</button></div>`),J.setAttribute("aria-labelledby","snHead"),c("#snCancel",J).onclick=ae.close,c("#snGo",J).onclick=Ye,ln(),c("#snGo",J).disabled=!Zt(u,"send_now"),c("#toSchedule",J).onclick=()=>{h(J,ce()),Gi()},c("#snGo",J).focus(),Be(),an();let ye=await ht(),Ze=J.querySelector("#snWho");Ze&&ye!==null&&(Ze.textContent=`${ye} recipient${ye===1?"":"s"}`)}Gi()}),Mc}function cl(e,t){return e.status==="sending"||e.status==="sent"?(js(`#/sent/${e.id}`),!0):e.status==="canceled"?(t(),!0):!1}function Du(e,t){return Re(e,"missed")?o`Scheduled for <strong>${j(e.fire_at)}</strong> · <span class="countdown countdown-missed">${_s(e.fire_at)}</span>`:t?o`Scheduled for <strong>${j(e.fire_at)}</strong> · <span class="countdown">${vn(e.fire_at,!0)}</span>`:o`Scheduled for <strong>${j(e.fire_at)}</strong> · <span class="countdown">${vn(e.fire_at)}</span>`}function Oe(e){return e instanceof te&&e.status===404}function Bu(e){let t=e.post.send;return t?.status==="sending"?`#/sent/${t.id}`:e.post.status==="sent"?t?`#/sent/${t.id}`:"#/sent":null}function js(e){location.replace(e)}function Uu(e,t){document.addEventListener("visibilitychange",()=>{document.hidden||e()},{signal:t})}function Fu(e){return e.trim().toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu,"").replace(/\s/g,"-")}function dl(e){let t=new Map;return e.map(n=>{let s=Fu(n),i=t.get(s)??0;return t.set(s,i+1),i?`${s}-${i}`:s})}var si=null;function qu(e){let t=document.createElement("div");h(t,z(e.html||""));let n=(t.querySelector("p")?.textContent||"").trim().replace(/\s+/g," "),s=/[:;,]$/.test(n),i=n.replace(/[\s:;,]+$/,""),r=150;return i.length>r?`${i.slice(0,r).replace(/[\s.,;:]+\S*$/,"")}\u2026`:s?`${i}\u2026`:i}function ju(e){let t=e.querySelector("code"),n=t?.className.match(/(?:^|\s)language-(\S+)/)?.[1]??null,s=(t??e).textContent?.replace(/\n$/,"")??"",i=n==="bash"||n==="sh"||n==="shell"?Us(s):n==="json"||n==="jsonc"?Xo(s):o`${s}`;return ka(i,s)}async function oi(e,t,n,s){if(ie(n,"Docs"),h(t,Ut("docs",null,o`<div class="docs-index"><p class="muted">Loading…</p></div>`)),!si)try{si=await L("/api/docs",{signal:n})}catch(d){n.aborted||re(c(".room-main",t),V(d),()=>ee((u,m)=>oi(e,u,m,s)));return}let{sets:i,docs:r}=si,a=i[0];if(!r.length||!a){h(c(".room-main"),o`<p class="muted">No documentation.</p>`);return}let l=e?i.find(d=>d.id===e):a;l?ml(t,l,i,r,n):e&&Ku(t,i,r,e,n,s?Wu(s):void 0)}function Wu(e){try{return decodeURIComponent(e)}catch{return}}function ml(e,t,n,s,i){ie(i,`${t.name} docs`);let r=(f,p)=>{let S=qu(f);return o`<li><a class="doc-card" href="#/docs/${f.slug}">${p===null?null:o`<span class="doc-card-n">${String(p).padStart(2,"0")}</span>`}<span class="doc-card-main"><span class="doc-card-t">${f.title} <span class="doc-card-go" aria-hidden="true">→</span></span>${S?o`<span class="doc-card-d">${S}</span>`:null}</span></a></li>`},a=t.sections.map(f=>{let p=s.filter(M=>M.set===t.id&&M.section===f.id);if(!p.length)return null;let S=p.map((M,P)=>r(M,f.numbered?P+1:null)),$=f.blurb?o`<div class="doc-section-blurb">${z(f.blurb)}</div>`:null;return o`<section class="doc-section"><h2 class="doc-section-t">${f.title}</h2>${$}${f.numbered?o`<ol class="doc-cards">${S}</ol>`:o`<ul class="doc-cards">${S}</ul>`}</section>`}),l=o`<nav class="docs-tabs" aria-label="Doc sets">${n.map(f=>o`<a href="#/docs/${f.id}" title="${f.title}"${f.id===t.id?o` aria-current="page"`:null}>${f.name}</a>`)}</nav>`,d=o`<div class="docs-index"><h1>Kestrel docs</h1><p class="docs-index-intro"><strong>Publishing</strong> covers writing and sending your newsletter, from naming your publication to what happens after each send. <strong>Self-hosting</strong> covers deploying Kestrel to your own Cloudflare account and keeping it running. One person often does both.</p>${l}${a}</div>`,u=Y.appConfig?.deployment.build.repo_url||"",m=(f,p,S=p)=>o`<a class="rail-link" href="${f}" target="_blank" rel="noopener" aria-label="${p}"><span class="rail-link-full">${p}</span><span class="rail-link-short" aria-hidden="true">${S}</span> <span aria-hidden="true">↗</span></a>`,v=o`<div class="toc-label">Kestrel</div>${m("https://getkestrel.dev","Project site","Project")}${u?[m(u,"Source on GitHub","Source"),m(`${u}/blob/HEAD/LICENSE`,"License")]:null}`;h(e,Ut("docs",v,d)),window.scrollTo(0,0)}var ii=matchMedia("(max-width: 720px)");function fl(e){if(!e)return;let t=ii.matches;e.open=!t,c("summary",e).tabIndex=t?0:-1}var hl=()=>document.querySelector("details#tocOnPage"),ul=!1;function Vu(){ul||(ul=!0,ii.addEventListener("change",()=>fl(hl())))}var pl=e=>`doc-h-${e}`;function Ku(e,t,n,s,i,r){let a=n.findIndex(x=>x.slug===s),l=n[a],d=t.find(x=>x.id===l?.set);if(!l||!d){let x=t[0];U(`No doc named \u201C${s}\u201D, so here\u2019s the index.`),history.replaceState(history.state,"","#/docs"),x&&ml(e,x,t,n,i);return}ie(i,l.title);let u=n.filter(x=>x.set===l.set&&x.section===l.section),m=u.indexOf(l),v=u[m-1],f=u[m+1],p=(x,W)=>o`<a href="#/docs/${x.slug}" aria-label="${W}: ${x.title}">${W==="Previous"?o`<span aria-hidden="true">←</span><span class="doc-topnav-word">Previous</span>`:o`<span class="doc-topnav-word">Next</span><span aria-hidden="true">→</span>`}</a>`,S=v||f?o`<nav class="doc-topnav" aria-label="Adjacent docs">${v?p(v,"Previous"):null}${f?p(f,"Next"):null}</nav>`:null;h(e,Ut("docs",o`<div id="docToc"></div>`,o`<article class="doc" id="docsMain"></article>`));let $=c("#docToc"),M=c("#docsMain");h(M,o`<section class="doc-part" id="doc-${l.slug}">${z(l.html)}</section>`);let P=c("section.doc-part",M),A=N("h1, h2, h3, h4",P),H=dl(A.map(x=>x.textContent||""));for(let[x,W]of A.entries()){let oe=H[x]??"";W.dataset.anchor=oe,W.id=pl(oe)}let y=P.querySelector("h1"),w=[];if(y&&w.push({id:y.id,anchor:"",title:y.textContent||l.title}),S){let x=document.createElement("div");x.className="doc-head",h(x,S),y?(y.before(x),x.prepend(y)):P.prepend(x)}let T=document.createElement("p");T.className="eyebrow doc-set",h(T,o`<a href="#/docs/${d.id}">${d.name}</a>`),P.prepend(T);for(let x of N("h2",P))w.push({id:x.id,anchor:x.dataset.anchor||"",title:x.textContent||""});let D=document.createElement("nav");D.className="doc-pager",h(D,o`${v?o`<a class="doc-pager-btn prev" href="#/docs/${v.slug}"><span class="doc-pager-dir">← Previous</span><span class="doc-pager-title">${v.title}</span></a>`:o`<span></span>`}${f?o`<a class="doc-pager-btn next" href="#/docs/${f.slug}"><span class="doc-pager-dir">Next →</span><span class="doc-pager-title">${f.title}</span></a>`:o`<span></span>`}`),M.appendChild(D);let R=w.map(x=>o`<a class="toc-sub" href="#/docs/${l.slug}${x.anchor?`/${x.anchor}`:""}" data-target="${x.id}">${x.title}</a>`);h($,w.length?o`<details class="toc-onpage" id="tocOnPage"><summary class="toc-label">On this page</summary>${R}</details>`:o``);let F=hl();fl(F),Vu();let K=x=>{if(F)for(let W of N(".toc-sub",F))W.classList.toggle("on",W.dataset.target===x)};$.addEventListener("click",x=>{let W=x.target instanceof Element?x.target.closest("a[data-target]"):null,oe=W?.dataset.target;oe&&(x.preventDefault(),K(oe),F&&ii.matches&&(F.open=!1),history.replaceState(history.state,"",W?.getAttribute("href")??location.hash),document.getElementById(oe)?.scrollIntoView({block:"start"}))});for(let x of N("a[href^='http']",M))x.target="_blank",x.rel="noopener";for(let x of N("pre",P)){let W=document.createElement("div");h(W,ju(x)),x.replaceWith(...W.childNodes)}We(M);let b=w.map(x=>x.id),E=b[0];if(F&&E){let x=b[b.length-1]??E,oe=(document.querySelector(".room-bar")?.getBoundingClientRect().height||54)+26,k=()=>{if(!document.getElementById(E)){document.onscroll=null;return}let G=E;if(Math.ceil(window.innerHeight+window.scrollY)>=document.documentElement.scrollHeight)G=x;else for(let q of b)if((document.getElementById(q)?.getBoundingClientRect().top??Number.POSITIVE_INFINITY)<=oe)G=q;else break;K(G)};document.onscroll=k,k()}let C=r?document.getElementById(pl(r)):null;C?C.scrollIntoView({block:"start"}):window.scrollTo(0,0)}function ri(e){let t=new Set(e.groups.flatMap(n=>n.routes.flatMap(s=>(s.errors??[]).map(i=>i.code))));return e.refusals.flatMap(n=>n.codes.filter(s=>!t.has(s.code)).map(s=>({...s,move:n.move})))}function Lt(e){return e.replace(/\([^)]*\)/g,"").replace(/\{[^}]*\}\??/g,"")||"/"}function tn(e){return e.title.toLowerCase().replace(/'s\b/g,"").split(/[^a-z0-9]+/).filter(t=>t!==""&&t!=="a"&&t!=="an"&&t!=="the").join("-")}var Gu=/`([^`]+)`|\*\*([^*]+)\*\*|\[([^\]]+)\]\(([^)\s]+)\)/g;function zu(e){return/^https:\/\//i.test(e)||/^\/(?![/\\])/.test(e)}function ve(e){let t=[],n=0;for(let s of e.matchAll(Gu)){let i=s.index??0;t.push(e.slice(n,i));let[,r,a,l,d]=s;r!==void 0?t.push(o`<code>${r}</code>`):a!==void 0?t.push(o`<strong>${ve(a)}</strong>`):l!==void 0&&d!==void 0&&t.push(zu(d)?o`<a href="${d}">${ve(l)}</a>`:ve(l)),n=i+s[0].length}return t.push(e.slice(n)),o`${t}`}function Hn(e,t){return o`${e.split(/\n\s*\n/).filter(n=>n.trim()).map(n=>{let s=n.trim().split(`
161
- `),i=s.findIndex(l=>l.startsWith("- "));if(i===-1)return o`<p class="${t}">${ve(n.trim())}</p>`;let r=s.slice(0,i).join(" ").trim(),a=s.slice(i).reduce((l,d)=>(d.startsWith("- ")||l.length===0?l.push(d.replace(/^- /,"")):l[l.length-1]=`${l[l.length-1]} ${d.trim()}`,l),[]);return o`${r?o`<p class="${t}">${ve(r)}</p>`:null}<ul class="${t} prose-list">${a.map(l=>o`<li>${ve(l)}</li>`)}</ul>`})}`}function Yu(e){return e==="access"?['-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID"','-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET"']:['-H "Authorization: Bearer $TOKEN"']}function bl(e,t,n){if(e.access==="webhook")return null;let s=(e.query??[]).filter(l=>l.required),i=s.length?`?${s.map(l=>`${l.name}=:${l.name}`).join("&")}`:"",r=`"${t}${Lt(e.path)}${i}"`,a=[e.method==="GET"?`curl ${r}`:`curl -X ${e.method} ${r}`];if(e.access==="admin"&&a.push(...Yu(n)),e.example?.request!==void 0){let l=JSON.stringify(e.example.request).replaceAll("'","'\\''");a.push('-H "Content-Type: application/json"',`-d '${l}'`)}else if(e.accepts?.includes("multipart/form-data"))for(let l of(e.body??[]).filter(d=>d.required))a.push(`-F "${l.name}=${l.type==="file"?"@":""}:${l.name}"`);return a.join(` \\
162
- `)}function li(e,t){return t===void 0?null:Jt(e,Ja(t),JSON.stringify(t,null,2))}function Zu(e){let t=bl(e,location.origin,Y.session?.auth.mode??"dev");return t===null?null:Jt("curl",Us(t),t)}function Ws(e){return o`${e.split(/(?<=[_./])/).map((t,n)=>o`${n?o`<wbr>`:null}${t}`)}`}function dt(e,t,n,s){return o`<div class="block api-sec"><div class="block-head"><span class="block-label">${e}${n?o`<span class="api-sec-meta">${n}</span>`:null}</span></div>${s??null}${t}</div>`}function ci(e,t,n,s){return o`<div class="api-row"><div class="api-row-head"><code class="api-row-name">${Ws(e)}</code>${t?o`<span class="api-row-type">${t}</span>`:null}${n?o`<span class="api-req">Required</span>`:null}</div><div class="api-row-desc">${s}</div></div>`}function di(e,t,n,s){return o`<div class="api-lrow"><span class="api-st api-st-${String(e)[0]}xx">${e}</span><div><div class="api-row-head"><code class="api-row-name">${t}</code></div>${n?o`<div class="api-with">${n}</div>`:null}<div class="api-row-desc">${s}</div></div></div>`}function Ju(e){if(!e?.length)return null;let t=e.map(n=>ci(n.name,null,n.required,ve(n.description)));return dt("Query",o`<div class="api-list api-query">${t}</div>`)}function Qu(e,t){if(!e?.length)return null;let n=e.map(i=>ci(i.name,i.type,i.required,ve(i.description))),s=t?.length?o` · ${t.map((i,r)=>o`${r?", ":""}<code>${i}</code>`)}`:null;return dt("Body",o`<div class="api-list api-body">${n}</div>`,s)}function Xu(e,t){if(!e?.length)return null;let n=e.map(s=>di(s.status,Ws(s.code),s.details?.length?o`with ${s.details.map((i,r)=>o`${r?", ":""}<code>${i}</code>`)}`:null,o`${ve(s.when??t.get(s.code)??"")}<span class="api-move-next">${s.move}</span>`));return dt("Refusals",o`<div class="api-ledger api-errors">${n}</div>`)}function ep(e){let t=s=>o`<ul class="api-values">${s.map(i=>o`<li><code>${i.value}</code> ${ve(i.description)}</li>`)}</ul>`,n=[...new Set(e.map(s=>s.group).filter(s=>s!==void 0))];return n.length?o`${n.map(s=>o`<div class="api-values-group">${ve(s)}</div>${t(e.filter(i=>i.group===s))}`)}`:t(e)}function ui(e){let t=e.map(n=>ci(n.name,n.object?o`<a href="#/api-reference/objects-${n.object}" class="api-obj-link" data-sec="objects-${n.object}">${n.type}</a>`:n.type,!1,o`${ve(n.description)}${n.values?.length?ep(n.values):null}`));return o`<div class="api-list api-fields">${t}</div>`}function tp(e,t){if(!e)return null;let n=e.object?o`<a href="#/api-reference/objects-${e.object}" class="api-obj-link" data-sec="objects-${e.object}">${t.get(e.object)??e.object}</a>`:null,s=n?e.as?o`<p class="api-returns"><code>${e.as}</code>: ${e.list?o`a list of ${n}`:n}${e.fields?.length?", and:":null}</p>`:o`<p class="api-returns">${n}${e.fields?.length?", and:":null}</p>`:null;return dt("Returns",e.fields?.length?ui(e.fields):null,null,s)}function np(e){return o`${Lt(e).split(/(:[A-Za-z_]+)/).map((t,n)=>n%2?o`<span class="api-param">${t}</span>`:t)}`}function Pn(e,t){return"route"in t?o`<a class="api-self" href="#/api-reference/${t.route}" data-route="${t.route}">${O("link")}${e}</a>`:o`<a class="api-self" href="#/api-reference/${t.sec}" data-sec="${t.sec}">${O("link")}${e}</a>`}function sp(e){let t=new Map;for(let n of e.objects){let s=e.groups.find(i=>i.resources.some(r=>r.key===n.resource));if(s){let i=`${s.access}-${n.resource}`;t.set(i,[...t.get(i)??[],n])}}return t}function op(e,t){let n=e.returns?.object;return[e.title,e.method,Lt(e.path),e.summary,...(e.errors??[]).map(s=>s.code),...n?[n,t.objects.get(n)??"",e.returns?.as??""]:[]].join(" ").toLowerCase()}function ip(e,t){return o`<li data-hay="${op(e,t)}" data-path="${Lt(e.path).toLowerCase()}" id="route-${tn(e)}" data-route="${tn(e)}"><article class="api-op">
153
+ </div>`);let P=c("#f-subject"),v=()=>ie(n,P.value.trim()||"Untitled");v();let w=c("#f-markdown"),I=c(".toolbar",t),j=c("#previewFrame"),x=c("#mdHl"),O=c("code",x),V=()=>{x.scrollTop=w.scrollTop,x.scrollLeft=w.scrollLeft},S=()=>{f(O,cl(w.value)),V()};w.addEventListener("scroll",V),S();let T=()=>({subject:P.value,markdown:w.value}),H=null;function $(){H?.(),H=null}function L(){return P.value.trim()===""?(H=Fe(P,"Add a subject before you schedule."),P.focus(),!1):($(),!0)}let G=c("#headEmailOnly"),k=()=>{G.hidden=m||!h.emailOnly;let b=t.querySelector("#schedDestIcon");b&&f(b,ii(h));let M=t.querySelector("#schedDest");M&&f(M,oi(h))},K=async b=>{let M=await C(`/api/posts/${e}/settings`,{method:"PUT",json:b});h=Xe(M.post,M.subject_slug_held),k(),M.changed&&Ue()},F=async()=>{try{let b=await C(`/api/posts/${e}`,{signal:n});h=Xe(b.post,b.subject_slug_held),k()}catch{}};c("#settingsBtn").onclick=()=>{let b={...h,subject:P.value},M=pe(o`<h3 id="psHead">Post settings</h3><p class="hint">Neither is part of the email, so both stay open while the post is scheduled.</p><div id="psForm"></div><div class="actions"><button type="button" id="psCancel">Cancel</button><button type="button" class="primary" id="psSave">Save</button></div>`,n),_=c(".modal",M.el);_.setAttribute("aria-labelledby","psHead"),Tt(c("#psForm",_),b,"ps"),c("#psCancel",_).onclick=M.close;let U=c("#psSave",_);U.onclick=()=>J(U,"Saving\u2026",async()=>{M.clearError();let oe=In(h,b);if(!oe){M.close();return}try{await K(oe),M.close(),q("Post settings saved")}catch(ce){if(n.aborted||Mt(ce)){M.close();return}Mn(_,"ps",ce)||M.error(et(ce))}}),c("#psInclude",_).focus()},m||P.addEventListener("input",()=>{v(),P.value.trim()!==""&&$()});let ee=fl(()=>{Oe(!0).catch(()=>{})});Bt(n,()=>ee.cancel());let Le=D(".ctab",t);function X(b){for(let M of Le){let _=M.dataset.tab===b;M.classList.toggle("active",_),M.setAttribute("aria-selected",_?"true":"false")}w.hidden=b!=="edit",x.hidden=b!=="edit",j.hidden=b!=="preview",I.classList.toggle("off",b!=="edit"),t.querySelector("#dropFoot")?.classList.toggle("off",b!=="edit"),b==="edit"&&V()}let le=c("#warnings");function me(b){f(le,b?.length?o`<div class="warnings"><strong>Warnings:</strong> ${b.join(" ")}</div>`:o``)}async function Pe(){X("preview");try{m||await Oe(!0);let[b,{warnings:M}]=await Promise.all([Ut(`/api/posts/${e}/preview`),C(`/api/posts/${e}/preview.json`,{signal:n})]);j.srcdoc=b,j.onload=()=>{try{let _=j.contentDocument;_&&(nn(_),wl(_,u.unlisted_url,A),j.style.height=`${_.body.scrollHeight+24}px`)}catch{}},me(M)}catch(b){if(n.aborted||Mt(b))return;q(et(b))}}for(let b of Le)b.onclick=()=>b.dataset.tab==="preview"?Pe():X("edit");m&&Pe();let Ae=m?c("#lockFoot"):null,tt;function Ht(){Ae&&(Ae.classList.remove("nudge"),requestAnimationFrame(()=>Ae.classList.add("nudge")),clearTimeout(tt),tt=setTimeout(()=>Ae.classList.remove("nudge"),900))}function nt(b,M,_){let U=w.selectionStart,oe=w.selectionEnd,ce=w.value.slice(U,oe)||_;w.value=w.value.slice(0,U)+b+ce+M+w.value.slice(oe),w.focus(),w.selectionStart=U+b.length,w.selectionEnd=U+b.length+ce.length}function ge(b){let M=w.selectionStart,_=w.selectionEnd,U=w.value.lastIndexOf(`
154
+ `,M-1)+1,oe=(w.value.slice(U,_)||"").split(`
155
+ `).map(ce=>b+ce).join(`
156
+ `);w.value=w.value.slice(0,U)+oe+w.value.slice(_),w.focus(),w.selectionStart=U,w.selectionEnd=U+oe.length}function Jn(b){if(m){Ht();return}if(X("edit"),b==="bold")nt("**","**","bold text");else if(b==="italic")nt("*","*","italic text");else if(b==="heading")ge("## ");else if(b==="quote")ge("> ");else if(b==="ul")ge("- ");else if(b==="ol")ge("1. ");else if(b==="indent")ge(" ");else if(b==="link")nt("[","](https://)","link text");else if(b==="code"){let M=w.selectionStart,_=w.selectionEnd;M===_||w.value.slice(M,_).includes(`
157
+ `)?nt("```\n","\n```","code"):nt("`","`","code")}So()}for(let b of D(".tb[data-fmt]",t))b.onclick=()=>Jn(b.dataset.fmt??"");w.addEventListener("keydown",b=>{if(!(b.metaKey||b.ctrlKey))return;let M=b.key.toLowerCase();M==="b"?(b.preventDefault(),Jn("bold")):M==="i"?(b.preventDefault(),Jn("italic")):M==="k"&&(b.preventDefault(),Jn("link"))});let st=!1,Qn=!1,We=!1,ot=null,ht=0,Xn=!1,vo=0,Ie=m?null:c("#saveBtn"),Me=m?null:c("#saveStatus"),Pt=new zs(T),cn=!1;function yo(){Me&&(m?(Me.className="save-status",Me.textContent=""):cn?(Me.className="save-status is-saving",Me.textContent="Saving\u2026"):ot&&st?(Me.className="save-status is-error",Me.textContent=`Not saved: ${ot}`):st?(Me.className="save-status is-dirty",Me.textContent="Unsaved changes"):(Me.className="save-status is-saved",Me.textContent="Saved"))}function es(){st=Pt.dirty,st||(ot=null,Qn=!1,ht=0),yo()}yo();function Yi(){ee.touch()}function So(){m||(S(),es(),We||Yi())}let It=Promise.resolve();function Oe(b){let M=It.catch(()=>{}).then(()=>Vc(b));return It=M,li(It),M}async function Vc(b){if(b&&!Pt.dirty)return"unchanged";if(We)return"refused";ee.cancel(),cn=!0,yo();let M=T(),_=JSON.stringify(M);try{let U={...M,base_revision:E.base},{post:oe,subject_slug_held:ce}=await C(`/api/posts/${e}`,{method:"PUT",json:U});return h=Xe(oe,ce),k(),E.saved(oe.current_revision),Pt.markSaved(_),Qn=!1,ot=null,ht=0,b||q("Saved"),"saved"}catch(U){let oe=gl(U);if(oe)return Qi(oe),"refused";if(Ne(U))return r(),"refused";Qn=!0;let ce=U instanceof se&&U.field==="subject"?P:void 0;if(ce?(Fe(ce,z(U)),ot="fix the field marked above."):ot=z(U),!(Co(U)||U instanceof se&&(U.status>=500||U.status===429)))vo+=1,Xn=!1,ht=0;else if(!Xn){Xn=!0,ht=ht?Math.min(ht*2,6e4):5e3;let ae=vo;vt(Date.now()+ht,()=>{ae===vo&&(Xn=!1,ot&&!We&&Oe(!0).catch(()=>{}))},n)}throw U}finally{cn=!1,es()}}Ie&&(Ie.onclick=()=>{if(We){Ke.scrollIntoView({block:"nearest"});return}J(Ie,"Saving\u2026",()=>Oe(!1).catch(()=>{})).finally(es)});function Zi(){Ie&&(We?(Ie.setAttribute("aria-disabled","true"),Ie.title="Saving is paused: choose Reload or Keep editing above"):(Ie.removeAttribute("aria-disabled"),Ie.removeAttribute("title")))}let Wc=()=>{if(ee.cancel(),m||!Pt.dirty)return;let b=T(),M=JSON.stringify(b);st=!1,It=It.catch(()=>{}).then(async()=>{if(!Pt.dirty)return;let _={...b,base_revision:E.base},{post:U}=await C(`/api/posts/${e}`,{method:"PUT",json:_});E.saved(U.current_revision),Pt.markSaved(M)}).catch(_=>{if(Ne(_)){q("This post was deleted elsewhere, so your last edits weren\u2019t saved.");return}q(_ instanceof se&&_.status===409?"Changed elsewhere, so your edits weren\u2019t saved":`Couldn\u2019t save your changes: ${et(_)}`)}),li(It)},Kc={dirty:()=>st,beforeLeave(){return i?"leave":st&&(Qn||We)?"confirm":(Wc(),"leave")},manualSave(){Ie&&!Ie.disabled&&Ie.click()}};if(!m){for(let b of[P,w])b.addEventListener("input",So);for(let b of[P])b.addEventListener("blur",()=>Oe(!0).catch(()=>{})),b.addEventListener("keydown",M=>{M.key==="Enter"&&!M.isComposing&&M.keyCode!==229&&(M.preventDefault(),Oe(!0).catch(()=>{}))})}let Ke=c("#freshnessBanner");function Ji(){We=!1,Zi(),E.clearWarning(),Ke.hidden=!0,f(Ke,o``)}function Qi(b){if(m)return;if(We=!0,ot=null,Zi(),E.noteWarned(b),b.kind==="locked")f(Ke,o`<span><span aria-hidden="true">⚠️</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>`);else{let _=sn(b.author,b.client);f(Ke,o`<span><span aria-hidden="true">⚠️</span> This draft was changed elsewhere${_?o`, last edited by <strong>${_}</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>`)}Ke.hidden=!1,c("#freshReload",Ke).onclick=()=>{Ji(),s()};let M=Ke.querySelector("#freshKeep");M&&(M.onclick=()=>{E.adoptWarned(),Ji(),es(),st&&Yi()})}function Mt(b){return Ne(b)?(r(),!0):!1}if(!m){let b=async()=>{if(cn||We||i||document.hidden)return;let M=E.base;try{let _=await C(`/api/posts/${e}`,{signal:n}),U=E.decide({status:_.post.status,revision:_.post.current_revision,author:_.post.author,client:_.post.author_client},{saving:cn,baseAtRequest:M});U!=="ignore"&&U!=="fresh"&&Qi(U)}catch(_){Ne(_)&&r()}};gt(1e4,b,n),Qu(b,n)}if(c("#webLink").addEventListener("click",()=>{m||Oe(!0).catch(()=>{})}),m&&d){let b=l?.send??{...d,conditions:[]},M=c("#schedWhen"),_=c("#schedControls"),U="",oe=0,ce=!1,ke=c("#rescheduleSchedule"),ae=c("#cancelSchedule"),Q=he=>b.actions===void 0||Je({actions:b.actions},he),$e=()=>{let he=Date.now(),Ee=b.phase==="due"||he>=b.fire_at;Ee?ce||(ke.hidden=!0,ae.hidden=!0):(ce=!0,ke.hidden=!Q("reschedule"),ae.hidden=!Q("cancel")),ke.disabled=Ee,ae.disabled=Ee,_.hidden=ke.hidden&&ae.hidden;let Re=Zu(b,Ee),ze=String(Re);ze!==U&&(U=ze,f(M,Re)),!Ee&&b.fire_at-he<=1e3&&b.fire_at!==oe&&(oe=b.fire_at,vt(b.fire_at,$e,n))};$e(),gt(1e3,$e,n);let bt=c("#schedRemade"),De=c("#testHint"),_e=c("#schedDailyWrites"),dn=c("#schedBy"),Nt=()=>{let he=ml(b,g);dn.hidden=he===null,dn.textContent=he??"";let Ee=fe(b,"remade");bt.hidden=!Ee,c("#schedRemadeMsg").textContent=Ee?.message??"",De.hidden=!fe(b,"untested");let Re=fe(b,"daily_writes");_e.hidden=!Re,c("#schedDailyWritesMsg").textContent=Re?.message??"";let ze=fe(b,"sandbox");c("#schedSandbox").hidden=!ze,c("#schedSandboxMsg").textContent=ze?.message??""};Nt();let un=he=>wn(he,{update(Ee,Re){let ze=!Re&&Ee.rev!==b.rev;if(b=Ee,ze&&F(),Re?.to==="sending"||Re?.to==="sent"||Re?.to==="complete"){q("Your send started, so this page switched to its live watch."),Zs(`#/sent/${Ee.id}`);return}if(Re?.to==="canceled"){q("The schedule was canceled elsewhere, so this post is a draft again."),s();return}$e(),Nt()},removed:r,stale:s},n);l?un(l):or(5e3,async()=>{let he=await C(`/api/sends/${d.id}`,{signal:n});return $l(he.send,s)||(b=he.send,$e(),Nt(),un(he)),!1},n),ke.onclick=()=>{Ws({id:b.id,post_id:e,fire_at:b.fire_at},s,n,{...h})},ae.onclick=()=>J(ae,"Canceling\u2026",async()=>{try{await C(`/api/sends/${d.id}/cancel`,{method:"POST"}),q("Schedule canceled"),s()}catch(he){Ne(he)?r():he instanceof se&&he.code==="window_closed"?(b={...b,phase:"due"},q("Too late to cancel: the send time has passed, so this email is going out."),Ue()):q(et(he))}}).finally($e)}function Gc(b){let M=tl(b.name),_=Object.keys(qs).find(U=>qs[U]===b.type);return!M||M===b.type||!_?b:new File([b],`image${_}`,{type:b.type})}async function wo(b){if(b.type.startsWith("image/"))try{let M=new FormData;M.append("file",b);let{image:_}=await C(`/api/posts/${e}/images`,{method:"POST",body:M}),U=w.selectionStart,oe=`
158
+ ![${_.filename}](${_.filename})
159
+ `;w.value=w.value.slice(0,U)+oe+w.value.slice(U),w.selectionStart=w.selectionEnd=U+oe.length,So(),q("Image added")}catch(M){Mt(M)||q(et(M))}}let Ge=c("#composerBody");if(m){let b=_=>_.metaKey||_.ctrlKey?["x","z","y"].includes(_.key.toLowerCase()):_.key.length===1||_.key==="Enter"||_.key==="Backspace"||_.key==="Delete",M=[w,P];for(let _ of M)_.addEventListener("keydown",U=>{b(U)&&Ht()}),_.addEventListener("paste",Ht);Ge.addEventListener("dragover",_=>_.preventDefault()),Ge.addEventListener("drop",_=>{_.preventDefault(),Ht()})}else{let b=c("#imgInput"),M=c("#dropFoot");Ge.addEventListener("dragover",_=>{_.preventDefault(),Ge.classList.add("dragover")}),Ge.addEventListener("dragleave",_=>{_.target===Ge&&Ge.classList.remove("dragover")}),Ge.addEventListener("drop",_=>{_.preventDefault(),Ge.classList.remove("dragover"),X("edit");for(let U of _.dataTransfer?.files??[])wo(U)}),w.addEventListener("paste",_=>{for(let U of _.clipboardData?.items||[])if(U.type.startsWith("image/")){let oe=U.getAsFile();oe&&(_.preventDefault(),wo(Gc(oe)))}}),M.onclick=()=>b.click(),b.onchange=()=>{for(let _ of b.files??[])wo(_);b.value=""}}c("#testBtn").onclick=()=>{let b=m?"Delivers the frozen copy that will send, exactly as it will fire, so you can check it in a client. One address per line.":"Delivers the rendered email to real inboxes so you can check it in a client. One address per line.",M=be(),_=pe(o`<h3>Send a test</h3><p class="hint">${b}</p>${$s()}${M?null:o`<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),U=c("#tGo",_.el);if(c("#tCancel",_.el).onclick=_.close,U.disabled=M,M){c("#tCancel",_.el).focus();return}let oe=c("#testTo",_.el);oe.focus(),C("/api/settings").then(ce=>{let ke=ce.settings.test_recipients||[];if(ke.length&&!oe.value.trim()){oe.value=ke.join(`
160
+ `),_.clearError();let ae=c("#testDefaultsHint",_.el);ae.textContent="Pre-filled from your default test recipients (Settings).",ae.hidden=!1}}).catch(()=>{}),U.onclick=()=>J(U,"Sending\u2026",async()=>{_.clearError();let{valid:ce,invalid:ke}=Rs(oe.value),ae=Cs(ke);if(ae){_.error(ae,oe);return}if(!ce.length){_.error("Enter at least one email address",oe);return}try{m||await Oe(!0);let Q=0,$e=[],bt=null;for(let De of ce)try{let _e=await C(`/api/posts/${e}/test`,{method:"POST",json:{to:De}});_e.sent?Q++:$e.push({email:De,message:_e.not_sent?.message??""}),bt=_e.warnings}catch(_e){if(Ne(_e)||_e instanceof se&&_e.status===401)throw _e;$e.push({email:De,message:z(_e)})}if(me(bt),m&&Q>0&&Ue(),$e.length){oe.value=$e.map(De=>De.email).join(`
161
+ `),_.error(As(Q,ce.length,$e));return}_.close(),q(qt(`Test sent to ${Q} address${Q===1?"":"es"}`))}catch(Q){if(Ne(Q)){_.close(),Mt(Q);return}_.error(et(Q))}})};let Xi=m?null:c("#scheduleBtn");return Xi&&(Xi.onclick=()=>{if(!L())return;let b=Et(gs()),M=Et(new Date(Date.now()+24*3600*1e3)),_=!y,U={...h,subject:P.value};U.slugAuto&&!U.slugTaken&&(U.slug=js(U.subject)||U.slug);let oe="It sends at the time you pick, with a cancelable window until then.",ce=()=>o`<h3 id="schHead">Schedule this post</h3><p class="hint" id="schHint">${oe}</p>${Sr()}${Bo()}${Uo()}${_?null:o`<label for="schWhen">Send at</label><input type="datetime-local" id="schWhen" min="${b}" value="${M}"><div class="dialog-settings" id="schForm"></div>`}<div class="actions"><button type="button" id="schCancel">Cancel</button><button type="button" class="primary" id="schGo">Schedule</button></div>${_?null:o`<div class="altrow"><span class="altrow-note">Or send as soon as possible.</span><button type="button" class="linkbtn" id="toSendNow">Send now →</button></div>`}`,ke=M,ae=pe(ce(),n),Q=c(".modal",ae.el),$e=null,bt=()=>($e??=C("/api/subscribers?status=confirmed&suppressed=hide&limit=1",{signal:n}).then(de=>de.page.total).catch(()=>null),$e),De=async()=>{if(!Q.querySelector("#schDailyWrites"))return;let de=await bt(),ye=Q.querySelector("#schDailyWrites");ye&&de!==null&&de>si&&(ye.textContent=el(de),ye.hidden=!1)},_e=null,dn=async()=>{_e??=dt(Z.appConfig?.deployment,n);let de=ut(await _e),ye=Q.querySelector("#schSandbox");ye&&de&&(ye.textContent=hr,ye.hidden=!1)},Nt=()=>{ae.close(),Ke.scrollIntoView({block:"nearest"})},un=()=>Tt(c("#schForm",Q),U,"sch"),he=()=>({email_only:U.emailOnly,...In(h,U)}),Ee=de=>{Mn(Q,"sch",de)||ae.error(et(de))},Re=()=>J(c("#schGo",Q),"Scheduling\u2026",async()=>{ae.clearError();let de=c("#schWhen",Q),ye=de.value?new Date(de.value).getTime():Number.NaN;if(Number.isNaN(ye)){ae.error("Pick a valid date & time",de);return}if(bs(ye)){ae.error(hs(),de);return}try{if(await Oe(!0)==="refused"){Nt();return}await C(`/api/posts/${e}/schedule`,{method:"POST",json:{fire_at:new Date(ye).toISOString(),...he()}}),ae.close(),q(qt(`Scheduled for ${W(ye)}. Send yourself a test.`)),s()}catch(Ye){if(Ne(Ye)){ae.close(),Mt(Ye);return}if(Ye instanceof se&&Ye.field==="fire_at"){ae.error(et(Ye),c("#schWhen",Q));return}Ee(Ye)}}),ze=()=>J(c("#snGo",Q),"Queuing\u2026",async()=>{ae.clearError();try{if(await Oe(!0)==="refused"){Nt();return}let{send:de}=await C(`/api/posts/${e}/send`,{method:"POST",json:he()});ae.close(),q(qt(`Sends at ${W(de.fire_at)}, cancelable until then.`)),s()}catch(de){if(Ne(de)){ae.close(),Mt(de);return}Ee(de)}});function er(){if(Q.setAttribute("aria-labelledby","schHead"),c("#schCancel",Q).onclick=ae.close,c("#schGo",Q).onclick=Re,c("#schGo",Q).disabled=_,_){c("#schCancel",Q).focus();return}c("#toSendNow",Q).onclick=zc,c("#schWhen",Q).value=ke,un(),c("#schWhen",Q).focus(),De(),dn()}async function zc(){let de=Q.querySelector("#schWhen");de&&(ke=de.value),f(Q,o`<h3 id="snHead">Send now?</h3><p class="hint">Sends this draft, as it is now, to <strong id="snWho">your subscribers</strong> in ${vr()}. The post locks until then, and you can cancel any time before it starts sending.</p><div class="dialog-settings" id="schForm"></div>${Bo()}${Uo()}<div class="actions"><button type="button" class="linkbtn actions-back" id="toSchedule">← Back to schedule</button><button type="button" id="snCancel">Cancel</button><button type="button" class="primary" id="snGo">Send now</button></div>`),Q.setAttribute("aria-labelledby","snHead"),c("#snCancel",Q).onclick=ae.close,c("#snGo",Q).onclick=ze,un(),c("#snGo",Q).disabled=!Xt(u,"send_now"),c("#toSchedule",Q).onclick=()=>{f(Q,ce()),er()},c("#snGo",Q).focus(),De(),dn();let ye=await bt(),Ye=Q.querySelector("#snWho");Ye&&ye!==null&&(Ye.textContent=`${ye} recipient${ye===1?"":"s"}`)}er()}),Kc}function $l(e,t){return e.status==="sending"||e.status==="sent"?(Zs(`#/sent/${e.id}`),!0):e.status==="canceled"?(t(),!0):!1}function Zu(e,t){return Ce(e,"missed")?o`Scheduled for <strong>${W(e.fire_at)}</strong> · <span class="countdown countdown-missed">${Hs(e.fire_at)}</span>`:t?o`Scheduled for <strong>${W(e.fire_at)}</strong> · <span class="countdown">${$n(e.fire_at,!0)}</span>`:o`Scheduled for <strong>${W(e.fire_at)}</strong> · <span class="countdown">${$n(e.fire_at)}</span>`}function Ne(e){return e instanceof se&&e.status===404}function Ju(e){let t=e.post.send;return t?.status==="sending"?`#/sent/${t.id}`:e.post.status==="sent"?t?`#/sent/${t.id}`:"#/sent":null}function Zs(e){location.replace(e)}function Qu(e,t){document.addEventListener("visibilitychange",()=>{document.hidden||e()},{signal:t})}function Xu(e){return e.trim().toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu,"").replace(/\s/g,"-")}function El(e){let t=new Map;return e.map(n=>{let s=Xu(n),i=t.get(s)??0;return t.set(s,i+1),i?`${s}-${i}`:s})}var ui=null;function ep(e){let t=document.createElement("div");f(t,Y(e.html||""));let n=(t.querySelector("p")?.textContent||"").trim().replace(/\s+/g," "),s=/[:;,]$/.test(n),i=n.replace(/[\s:;,]+$/,""),r=150;return i.length>r?`${i.slice(0,r).replace(/[\s.,;:]+\S*$/,"")}\u2026`:s?`${i}\u2026`:i}function tp(e){let t=e.querySelector("code"),n=t?.className.match(/(?:^|\s)language-(\S+)/)?.[1]??null,s=(t??e).textContent?.replace(/\n$/,"")??"",i=n==="bash"||n==="sh"||n==="shell"?Gs(s):n==="json"||n==="jsonc"?ai(s):o`${s}`;return Oa(i,s)}async function pi(e,t,n,s){if(ie(n,"Docs"),f(t,jt("docs",null,o`<div class="docs-index"><p class="muted">Loading…</p></div>`)),!ui)try{ui=await C("/api/docs",{signal:n})}catch(d){n.aborted||re(c(".room-main",t),z(d),()=>ne((u,p)=>pi(e,u,p,s)));return}let{sets:i,docs:r}=ui,a=i[0];if(!r.length||!a){f(c(".room-main"),o`<p class="muted">No documentation.</p>`);return}let l=e?i.find(d=>d.id===e):a;l?_l(t,l,i,r,n):e&&op(t,i,r,e,n,s?np(s):void 0)}function np(e){try{return decodeURIComponent(e)}catch{return}}function _l(e,t,n,s,i){ie(i,`${t.name} docs`);let r=(h,m)=>{let y=ep(h);return o`<li><a class="doc-card" href="#/docs/${h.slug}">${m===null?null:o`<span class="doc-card-n">${String(m).padStart(2,"0")}</span>`}<span class="doc-card-main"><span class="doc-card-t">${h.title} <span class="doc-card-go" aria-hidden="true">→</span></span>${y?o`<span class="doc-card-d">${y}</span>`:null}</span></a></li>`},a=t.sections.map(h=>{let m=s.filter(R=>R.set===t.id&&R.section===h.id);if(!m.length)return null;let y=m.map((R,N)=>r(R,h.numbered?N+1:null)),E=h.blurb?o`<div class="doc-section-blurb">${Y(h.blurb)}</div>`:null;return o`<section class="doc-section"><h2 class="doc-section-t">${h.title}</h2>${E}${h.numbered?o`<ol class="doc-cards">${y}</ol>`:o`<ul class="doc-cards">${y}</ul>`}</section>`}),l=o`<nav class="docs-tabs" aria-label="Doc sets">${n.map(h=>o`<a href="#/docs/${h.id}" title="${h.title}"${h.id===t.id?o` aria-current="page"`:null}>${h.name}</a>`)}</nav>`,d=o`<div class="docs-index"><h1>Kestrel docs</h1><p class="docs-index-intro"><strong>Publishing</strong> covers writing and sending your newsletter, from naming your publication to what happens after each send. <strong>Self-hosting</strong> covers deploying Kestrel to your own Cloudflare account and keeping it running. One person often does both.</p>${l}${a}</div>`,u=Z.appConfig?.deployment.build.repo_url||"",p=(h,m,y=m)=>o`<a class="rail-link" href="${h}" target="_blank" rel="noopener" aria-label="${m}"><span class="rail-link-full">${m}</span><span class="rail-link-short" aria-hidden="true">${y}</span> <span aria-hidden="true">↗</span></a>`,g=o`<div class="toc-label">Kestrel</div>${p("https://getkestrel.dev","Project site","Project")}${u?[p(u,"Source on GitHub","Source"),p(`${u}/blob/HEAD/LICENSE`,"License")]:null}`;f(e,jt("docs",g,d)),window.scrollTo(0,0)}var mi=matchMedia("(max-width: 720px)");function Tl(e){if(!e)return;let t=mi.matches;e.open=!t,c("summary",e).tabIndex=t?0:-1}var Ll=()=>document.querySelector("details#tocOnPage"),xl=!1;function sp(){xl||(xl=!0,mi.addEventListener("change",()=>Tl(Ll())))}var kl=e=>`doc-h-${e}`;function op(e,t,n,s,i,r){let a=n.findIndex($=>$.slug===s),l=n[a],d=t.find($=>$.id===l?.set);if(!l||!d){let $=t[0];q(`No doc named \u201C${s}\u201D, so here\u2019s the index.`),history.replaceState(history.state,"","#/docs"),$&&_l(e,$,t,n,i);return}ie(i,l.title);let u=n.filter($=>$.set===l.set&&$.section===l.section),p=u.indexOf(l),g=u[p-1],h=u[p+1],m=($,L)=>o`<a href="#/docs/${$.slug}" aria-label="${L}: ${$.title}">${L==="Previous"?o`<span aria-hidden="true">←</span><span class="doc-topnav-word">Previous</span>`:o`<span class="doc-topnav-word">Next</span><span aria-hidden="true">→</span>`}</a>`,y=g||h?o`<nav class="doc-topnav" aria-label="Adjacent docs">${g?m(g,"Previous"):null}${h?m(h,"Next"):null}</nav>`:null;f(e,jt("docs",o`<div id="docToc"></div>`,o`<article class="doc" id="docsMain"></article>`));let E=c("#docToc"),R=c("#docsMain");f(R,o`<section class="doc-part" id="doc-${l.slug}">${Y(l.html)}</section>`);let N=c("section.doc-part",R),A=D("h1, h2, h3, h4",N),P=El(A.map($=>$.textContent||""));for(let[$,L]of A.entries()){let G=P[$]??"";L.dataset.anchor=G,L.id=kl(G)}let v=N.querySelector("h1"),w=[];if(v&&w.push({id:v.id,anchor:"",title:v.textContent||l.title}),y){let $=document.createElement("div");$.className="doc-head",f($,y),v?(v.before($),$.prepend(v)):N.prepend($)}let I=document.createElement("p");I.className="eyebrow doc-set",f(I,o`<a href="#/docs/${d.id}">${d.name}</a>`),N.prepend(I);for(let $ of D("h2",N))w.push({id:$.id,anchor:$.dataset.anchor||"",title:$.textContent||""});let j=document.createElement("nav");j.className="doc-pager",f(j,o`${g?o`<a class="doc-pager-btn prev" href="#/docs/${g.slug}"><span class="doc-pager-dir">← Previous</span><span class="doc-pager-title">${g.title}</span></a>`:o`<span></span>`}${h?o`<a class="doc-pager-btn next" href="#/docs/${h.slug}"><span class="doc-pager-dir">Next →</span><span class="doc-pager-title">${h.title}</span></a>`:o`<span></span>`}`),R.appendChild(j);let x=w.map($=>o`<a class="toc-sub" href="#/docs/${l.slug}${$.anchor?`/${$.anchor}`:""}" data-target="${$.id}">${$.title}</a>`);f(E,w.length?o`<details class="toc-onpage" id="tocOnPage"><summary class="toc-label">On this page</summary>${x}</details>`:o``);let O=Ll();Tl(O),sp();let V=$=>{if(O)for(let L of D(".toc-sub",O))L.classList.toggle("on",L.dataset.target===$)};E.addEventListener("click",$=>{let L=$.target instanceof Element?$.target.closest("a[data-target]"):null,G=L?.dataset.target;G&&($.preventDefault(),V(G),O&&mi.matches&&(O.open=!1),history.replaceState(history.state,"",L?.getAttribute("href")??location.hash),document.getElementById(G)?.scrollIntoView({block:"start"}))});for(let $ of D("a[href^='http']",R))$.target="_blank",$.rel="noopener";for(let $ of D("pre",N)){let L=document.createElement("div");f(L,tp($)),$.replaceWith(...L.childNodes)}je(R);let S=w.map($=>$.id),T=S[0];if(O&&T){let $=S[S.length-1]??T,G=(document.querySelector(".room-bar")?.getBoundingClientRect().height||54)+26,k=()=>{if(!document.getElementById(T)){document.onscroll=null;return}let K=T;if(Math.ceil(window.innerHeight+window.scrollY)>=document.documentElement.scrollHeight)K=$;else for(let F of S)if((document.getElementById(F)?.getBoundingClientRect().top??Number.POSITIVE_INFINITY)<=G)K=F;else break;V(K)};document.onscroll=k,k()}let H=r?document.getElementById(kl(r)):null;H?H.scrollIntoView({block:"start"}):window.scrollTo(0,0)}function fi(e){let t=new Set(e.groups.flatMap(n=>n.routes.flatMap(s=>(s.errors??[]).map(i=>i.code))));return e.refusals.flatMap(n=>n.codes.filter(s=>!t.has(s.code)).map(s=>({...s,move:n.move})))}function Rt(e){return e.replace(/\([^)]*\)/g,"").replace(/\{[^}]*\}\??/g,"")||"/"}function on(e){return e.title.toLowerCase().replace(/'s\b/g,"").split(/[^a-z0-9]+/).filter(t=>t!==""&&t!=="a"&&t!=="an"&&t!=="the").join("-")}var ip=/`([^`]+)`|\*\*([^*]+)\*\*|\[([^\]]+)\]\(([^)\s]+)\)/g;function rp(e){return/^https:\/\//i.test(e)||/^\/(?![/\\])/.test(e)}function ve(e){let t=[],n=0;for(let s of e.matchAll(ip)){let i=s.index??0;t.push(e.slice(n,i));let[,r,a,l,d]=s;r!==void 0?t.push(o`<code>${r}</code>`):a!==void 0?t.push(o`<strong>${ve(a)}</strong>`):l!==void 0&&d!==void 0&&t.push(rp(d)?o`<a href="${d}">${ve(l)}</a>`:ve(l)),n=i+s[0].length}return t.push(e.slice(n)),o`${t}`}function Nn(e,t){return o`${e.split(/\n\s*\n/).filter(n=>n.trim()).map(n=>{let s=n.trim().split(`
162
+ `),i=s.findIndex(l=>l.startsWith("- "));if(i===-1)return o`<p class="${t}">${ve(n.trim())}</p>`;let r=s.slice(0,i).join(" ").trim(),a=s.slice(i).reduce((l,d)=>(d.startsWith("- ")||l.length===0?l.push(d.replace(/^- /,"")):l[l.length-1]=`${l[l.length-1]} ${d.trim()}`,l),[]);return o`${r?o`<p class="${t}">${ve(r)}</p>`:null}<ul class="${t} prose-list">${a.map(l=>o`<li>${ve(l)}</li>`)}</ul>`})}`}function ap(e){return e==="access"?['-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID"','-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET"']:['-H "Authorization: Bearer $TOKEN"']}function Rl(e,t,n){if(e.access==="webhook")return null;let s=(e.query??[]).filter(l=>l.required),i=s.length?`?${s.map(l=>`${l.name}=:${l.name}`).join("&")}`:"",r=`"${t}${Rt(e.path)}${i}"`,a=[e.method==="GET"?`curl ${r}`:`curl -X ${e.method} ${r}`];if(e.access==="admin"&&a.push(...ap(n)),e.example?.request!==void 0){let l=JSON.stringify(e.example.request).replaceAll("'","'\\''");a.push('-H "Content-Type: application/json"',`-d '${l}'`)}else if(e.accepts?.includes("multipart/form-data"))for(let l of(e.body??[]).filter(d=>d.required))a.push(`-F "${l.name}=${l.type==="file"?"@":""}:${l.name}"`);return a.join(` \\
163
+ `)}function bi(e,t){return t===void 0?null:en(e,dl(t),JSON.stringify(t,null,2))}function lp(e){let t=Rl(e,location.origin,Z.session?.auth.mode??"dev");return t===null?null:en("curl",Gs(t),t)}function Js(e){return o`${e.split(/(?<=[_./])/).map((t,n)=>o`${n?o`<wbr>`:null}${t}`)}`}function mt(e,t,n,s){return o`<div class="block api-sec"><div class="block-head"><span class="block-label">${e}${n?o`<span class="api-sec-meta">${n}</span>`:null}</span></div>${s??null}${t}</div>`}function gi(e,t,n,s){return o`<div class="api-row"><div class="api-row-head"><code class="api-row-name">${Js(e)}</code>${t?o`<span class="api-row-type">${t}</span>`:null}${n?o`<span class="api-req">Required</span>`:null}</div><div class="api-row-desc">${s}</div></div>`}function vi(e,t,n,s){return o`<div class="api-lrow"><span class="api-st api-st-${String(e)[0]}xx">${e}</span><div><div class="api-row-head"><code class="api-row-name">${t}</code></div>${n?o`<div class="api-with">${n}</div>`:null}<div class="api-row-desc">${s}</div></div></div>`}function cp(e){if(!e?.length)return null;let t=e.map(n=>gi(n.name,null,n.required,ve(n.description)));return mt("Query",o`<div class="api-list api-query">${t}</div>`)}function dp(e,t){if(!e?.length)return null;let n=e.map(i=>gi(i.name,i.type,i.required,ve(i.description))),s=t?.length?o` · ${t.map((i,r)=>o`${r?", ":""}<code>${i}</code>`)}`:null;return mt("Body",o`<div class="api-list api-body">${n}</div>`,s)}function up(e,t){if(!e?.length)return null;let n=e.map(s=>vi(s.status,Js(s.code),s.details?.length?o`with ${s.details.map((i,r)=>o`${r?", ":""}<code>${i}</code>`)}`:null,o`${ve(s.when??t.get(s.code)??"")}<span class="api-move-next">${s.move}</span>`));return mt("Refusals",o`<div class="api-ledger api-errors">${n}</div>`)}function pp(e){let t=s=>o`<ul class="api-values">${s.map(i=>o`<li><code>${i.value}</code> ${ve(i.description)}</li>`)}</ul>`,n=[...new Set(e.map(s=>s.group).filter(s=>s!==void 0))];return n.length?o`${n.map(s=>o`<div class="api-values-group">${ve(s)}</div>${t(e.filter(i=>i.group===s))}`)}`:t(e)}function yi(e){let t=e.map(n=>gi(n.name,n.object?o`<a href="#/api-reference/objects-${n.object}" class="api-obj-link" data-sec="objects-${n.object}">${n.type}</a>`:n.type,!1,o`${ve(n.description)}${n.values?.length?pp(n.values):null}`));return o`<div class="api-list api-fields">${t}</div>`}function mp(e,t){if(!e)return null;let n=e.object?o`<a href="#/api-reference/objects-${e.object}" class="api-obj-link" data-sec="objects-${e.object}">${t.get(e.object)??e.object}</a>`:null,s=n?e.as?o`<p class="api-returns"><code>${e.as}</code>: ${e.list?o`a list of ${n}`:n}${e.fields?.length?", and:":null}</p>`:o`<p class="api-returns">${n}${e.fields?.length?", and:":null}</p>`:null;return mt("Returns",e.fields?.length?yi(e.fields):null,null,s)}function fp(e){return o`${Rt(e).split(/(:[A-Za-z_]+)/).map((t,n)=>n%2?o`<span class="api-param">${t}</span>`:t)}`}function On(e,t){return"route"in t?o`<a class="api-self" href="#/api-reference/${t.route}" data-route="${t.route}">${B("link")}${e}</a>`:o`<a class="api-self" href="#/api-reference/${t.sec}" data-sec="${t.sec}">${B("link")}${e}</a>`}function hp(e){let t=new Map;for(let n of e.objects){let s=e.groups.find(i=>i.resources.some(r=>r.key===n.resource));if(s){let i=`${s.access}-${n.resource}`;t.set(i,[...t.get(i)??[],n])}}return t}function bp(e,t){let n=e.returns?.object;return[e.title,e.method,Rt(e.path),e.summary,...(e.errors??[]).map(s=>s.code),...n?[n,t.objects.get(n)??"",e.returns?.as??""]:[]].join(" ").toLowerCase()}function gp(e,t){return o`<li data-hay="${bp(e,t)}" data-path="${Rt(e.path).toLowerCase()}" id="route-${on(e)}" data-route="${on(e)}"><article class="api-op">
163
164
  <div class="api-op-head">
164
- <h4 tabindex="-1">${Pn(e.title,{route:tn(e)})}</h4>
165
+ <h4 tabindex="-1">${On(e.title,{route:on(e)})}</h4>
165
166
  </div>
166
- <p class="api-op-call"><span class="api-method m-${e.method}">${e.method}</span><code class="api-path">${np(e.path)}</code><button type="button" class="copy-btn api-copy-path" data-copy="${Lt(e.path)}" data-copy-word="" aria-label="Copy path" title="Copy path">${Te(!1,"")}</button></p>
167
+ <p class="api-op-call"><span class="api-method m-${e.method}">${e.method}</span><code class="api-path">${fp(e.path)}</code><button type="button" class="copy-btn api-copy-path" data-copy="${Rt(e.path)}" data-copy-word="" aria-label="Copy path" title="Copy path">${Te(!1,"")}</button></p>
167
168
  <div class="api-op-body">
168
169
  <div class="api-op-text">
169
170
  <p class="api-summary">${ve(e.summary)}</p>
170
- ${e.description?Hn(e.description,"api-desc"):null}
171
- ${Ju(e.query)}
172
- ${Qu(e.body,e.accepts)}
173
- ${tp(e.returns,t.objects)}
174
- ${Xu(e.errors,t.meanings)}
171
+ ${e.description?Nn(e.description,"api-desc"):null}
172
+ ${cp(e.query)}
173
+ ${dp(e.body,e.accepts)}
174
+ ${mp(e.returns,t.objects)}
175
+ ${up(e.errors,t.meanings)}
175
176
  </div>
176
177
  <div class="api-op-code">
177
- ${li("Request",e.example?.request)}
178
- ${li("Response",e.example?.response)}
179
- ${Zu(e)}
178
+ ${bi("Request",e.example?.request)}
179
+ ${bi("Response",e.example?.response)}
180
+ ${lp(e)}
180
181
  </div>
181
182
  </div>
182
- </article></li>`}var wl=(e,t)=>e.routes.filter(n=>n.resource===t.key),rp=80;function ap(e){let t=e.columns.map((i,r)=>r>0&&e.rows.every(a=>(a[r]??"").length<=rp)),n=e.columns.findIndex((i,r)=>r>0&&!t[r]),s=e.rows.map(i=>{let r=/^`([^`]+)`$/.exec(i[0]??"")?.[1],a=e.columns.flatMap((d,u)=>t[u]?[o`<span class="api-row-meta"><span class="api-row-key">${d}</span> ${ve(i[u]??"")}</span>`]:[]),l=e.columns.flatMap((d,u)=>u>0&&!t[u]?[o`<div class="api-row-desc">${u===n?null:o`<span class="api-row-key">${d}:</span> `}${ve(i[u]??"")}</div>`]:[]);return o`<div class="api-row"><div class="api-row-head"><code class="api-row-name">${r===void 0?ve(i[0]??""):Ws(r)}</code>${a}</div>${l}</div>`});return dt(e.title,o`<div class="api-list api-value-rows">${s}</div>`,null,Hn(e.description,"api-prose"))}function lp(e){return o`<article class="api-op api-object" id="api-objects-${e.key}" data-sec="objects-${e.key}">
183
- <div class="api-op-head"><h4 tabindex="-1">${Pn(e.title,{sec:`objects-${e.key}`})}</h4></div>
183
+ </article></li>`}var Pl=(e,t)=>e.routes.filter(n=>n.resource===t.key),vp=80;function yp(e){let t=e.columns.map((i,r)=>r>0&&e.rows.every(a=>(a[r]??"").length<=vp)),n=e.columns.findIndex((i,r)=>r>0&&!t[r]),s=e.rows.map(i=>{let r=/^`([^`]+)`$/.exec(i[0]??"")?.[1],a=e.columns.flatMap((d,u)=>t[u]?[o`<span class="api-row-meta"><span class="api-row-key">${d}</span> ${ve(i[u]??"")}</span>`]:[]),l=e.columns.flatMap((d,u)=>u>0&&!t[u]?[o`<div class="api-row-desc">${u===n?null:o`<span class="api-row-key">${d}:</span> `}${ve(i[u]??"")}</div>`]:[]);return o`<div class="api-row"><div class="api-row-head"><code class="api-row-name">${r===void 0?ve(i[0]??""):Js(r)}</code>${a}</div>${l}</div>`});return mt(e.title,o`<div class="api-list api-value-rows">${s}</div>`,null,Nn(e.description,"api-prose"))}function Sp(e){return o`<article class="api-op api-object" id="api-objects-${e.key}" data-sec="objects-${e.key}">
184
+ <div class="api-op-head"><h4 tabindex="-1">${On(e.title,{sec:`objects-${e.key}`})}</h4></div>
184
185
  <p class="api-op-call"><span class="api-method m-OBJECT">Object</span><span class="api-path">${e.fields.length} ${e.fields.length===1?"field":"fields"}</span></p>
185
186
  <div class="api-op-body">
186
187
  <div class="api-op-text">
187
- ${Hn(e.description,"api-prose")}
188
- ${ui(e.fields)}
189
- ${(e.tables??[]).map(ap)}
188
+ ${Nn(e.description,"api-prose")}
189
+ ${yi(e.fields)}
190
+ ${(e.tables??[]).map(yp)}
190
191
  </div>
191
- <div class="api-op-code">${li("Example",e.example)}</div>
192
+ <div class="api-op-code">${bi("Example",e.example)}</div>
192
193
  </div>
193
- </article>`}function cp(e,t,n){let s=`${e.access}-${t.key}`,i=wl(e,t),r=n.heads.get(s)??[];return o`<div class="api-res" id="api-${s}" data-sec="${s}">
194
- <div class="api-res-head"><h3 tabindex="-1">${Pn(t.title,{sec:s})}</h3></div>
195
- ${r.map(lp)}
196
- <ul class="api-rows">${i.map(a=>ip(a,n))}</ul>
197
- </div>`}function dp(e,t){return o`<section class="api-section" id="api-${e.access}" data-tier="${e.access}">
198
- <h2 tabindex="-1">${Pn(e.title,{sec:e.access})}</h2>
194
+ </article>`}function wp(e,t,n){let s=`${e.access}-${t.key}`,i=Pl(e,t),r=n.heads.get(s)??[];return o`<div class="api-res" id="api-${s}" data-sec="${s}">
195
+ <div class="api-res-head"><h3 tabindex="-1">${On(t.title,{sec:s})}</h3></div>
196
+ ${r.map(Sp)}
197
+ <ul class="api-rows">${i.map(a=>gp(a,n))}</ul>
198
+ </div>`}function $p(e,t){return o`<section class="api-section" id="api-${e.access}" data-tier="${e.access}">
199
+ <h2 tabindex="-1">${On(e.title,{sec:e.access})}</h2>
199
200
  <p class="api-blurb muted">${ve(e.blurb)}</p>
200
- ${e.resources.map(n=>cp(e,n,t))}
201
- </section>`}function up(e){let t=ri(e);if(!t.length)return null;let n=t.map(s=>di(s.status,Ws(s.code),null,ve(s.meaning)));return dt("Refusals any route can give",o`<div class="api-ledger api-refusals">${n}</div>`)}function pp(e){let t=ri(e).map(s=>s.code),n=s=>o`<div class="api-res" id="api-overview-${s.key}" data-sec="overview-${s.key}" data-hay="${[s.title,...s.key==="errors"?t:[]].join(" ").toLowerCase()}">
202
- <div class="api-res-head"><h3 tabindex="-1">${Pn(s.title,{sec:`overview-${s.key}`})}</h3></div>
203
- ${Hn(s.text,"api-prose")}
204
- ${s.key==="errors"?mp(e.statuses):null}
205
- ${s.fields?.length?dt(s.key==="errors"?"The error body":"Fields",ui(s.fields)):null}
206
- ${s.key==="errors"?up(e):null}
201
+ ${e.resources.map(n=>wp(e,n,t))}
202
+ </section>`}function Ep(e){let t=fi(e);if(!t.length)return null;let n=t.map(s=>vi(s.status,Js(s.code),null,ve(s.meaning)));return mt("Refusals any route can give",o`<div class="api-ledger api-refusals">${n}</div>`)}function xp(e){let t=fi(e).map(s=>s.code),n=s=>o`<div class="api-res" id="api-overview-${s.key}" data-sec="overview-${s.key}" data-hay="${[s.title,...s.key==="errors"?t:[]].join(" ").toLowerCase()}">
203
+ <div class="api-res-head"><h3 tabindex="-1">${On(s.title,{sec:`overview-${s.key}`})}</h3></div>
204
+ ${Nn(s.text,"api-prose")}
205
+ ${s.key==="errors"?kp(e.statuses):null}
206
+ ${s.fields?.length?mt(s.key==="errors"?"The error body":"Fields",yi(s.fields)):null}
207
+ ${s.key==="errors"?Ep(e):null}
207
208
  </div>`;return o`<section class="api-section" id="api-overview" data-tier="overview">
208
209
  ${e.concepts.map(n)}
209
- </section>`}function mp(e){let t=e.map(n=>di(n.status,o`${n.name}`,null,ve(n.meaning)));return dt("Status codes",o`<div class="api-ledger api-statuses">${t}</div>`)}function fp(e,t){let n=(i,r)=>o`<a href="#/api-reference/${i}" class="api-nav-res" data-sec="${i}">${r}</a>`,s=(i,r)=>o`<div class="api-nav-ops" data-of="${i.access}-${r.key}">${[...(t.heads.get(`${i.access}-${r.key}`)??[]).map(a=>o`<a href="#/api-reference/objects-${a.key}" class="api-nav-op" data-sec="objects-${a.key}">${a.title}</a>`),...wl(i,r).map(a=>o`<a href="#/api-reference/${tn(a)}" class="api-nav-op" data-route="${tn(a)}">${a.title}</a>`)]}</div>`;return o`<a href="#/api-reference" class="api-nav-tier" data-sec="overview" data-tier="overview">Introduction</a>${e.concepts.map(i=>n(`overview-${i.key}`,i.title))}${e.groups.map(i=>o`<a href="#/api-reference/${i.access}" class="api-nav-tier" data-sec="${i.access}" data-tier="${i.access}">${i.title}</a>${i.resources.map(r=>[i.resources.length>1?n(`${i.access}-${r.key}`,r.title):null,s(i,r)])}`)}`}function hp(e,t){let n=e.replace(/^[a-z]+:\/\/[^/]*/,"").replace(/[?#].*$/,"");if(!n.startsWith("/"))return!1;let s=n.replace(/(.)\/+$/,"$1").split("/"),i=t.split("/");return s.length===i.length&&i.every((r,a)=>r.startsWith(":")?s[a]!=="":r===s[a])}function bp(e,t){let n=e.dataset.path;return(e.dataset.hay??"").includes(t)||n!==void 0&&hp(t,n)}function gl(e,t,n){let s=e.trim().toLowerCase().split(/\s+/).filter(Boolean);t.classList.toggle("filtering",s.length>0);let i=0;for(let l of N(".api-section",n)){let d=0;for(let m of N(".api-res",l)){let v=m.dataset.hay!==void 0,f=0;for(let S of v?[m]:N("li[data-hay]",m)){let $=s.every(P=>bp(S,P));S.hidden=!$,f+=$?1:0;let M=S.dataset.route?t.querySelector(`a[data-route="${S.dataset.route}"]`):null;M&&(M.hidden=!$)}m.hidden=f===0;for(let S of N(`.api-nav-ops[data-of="${m.dataset.sec}"] a[data-sec]`,t))S.hidden=s.length>0&&f===0;let p=t.querySelector(`a[data-sec="${m.dataset.sec}"]`);p&&(p.hidden=f===0),d+=f}l.hidden=d===0;let u=t.querySelector(`a[data-tier="${l.dataset.tier}"]`);u&&(u.hidden=d===0),i+=d}let r=N(".api-section",n),a=r.filter(l=>!l.hidden).at(-1);for(let l of r)l.classList.toggle("api-last",l===a);c("#apiEmpty",n).hidden=i>0}function ai(e,t,n){let s=(c(".room-bar").getBoundingClientRect().height||54)+8,i=N(".api-section, .api-res, .api-object, li[data-route]",t).filter(S=>!S.closest("[hidden]")),r=window.scrollY>0&&Math.ceil(window.innerHeight+window.scrollY)>=document.documentElement.scrollHeight,a;for(let S of i)if(r||S.getBoundingClientRect().top<=s)a=S;else break;let l=N("a[data-sec], a[data-route]",e).filter(S=>!S.hidden&&getComputedStyle(S).display!=="none"),d=S=>S?(S.dataset.route?l.find(M=>M.dataset.route===S.dataset.route):l.find(M=>M.dataset.sec===(S.dataset.sec??S.dataset.tier)))??d(S.parentElement?.closest(".api-res, .api-section")):void 0,u=a?.closest(".api-res")?.dataset.sec;for(let S of N(".api-nav-ops",e))S.classList.toggle("open",S.dataset.of===u);let m=d(a??i[0]);for(let S of N("a[data-sec], a[data-route]",e))S.classList.toggle("active",S===m);let v=e.closest(".rail-inner");if(m&&v&&v.scrollHeight>v.clientHeight){let S=m.getBoundingClientRect(),$=v.getBoundingClientRect();(S.top<$.top+40||S.bottom>$.bottom-40)&&(v.scrollTop+=S.top-$.top-$.height/3)}let f=a?.dataset.route??a?.dataset.sec??a?.dataset.tier,p=f&&f!=="overview"&&window.scrollY>0?`#/api-reference/${f}`:"#/api-reference";n&&location.hash!==p&&history.replaceState(history.state,"",p)}function vl(e){return document.getElementById(e==="overview"?"api-intro":`api-${e}`)}function yl(e){e.id==="api-intro"?window.scrollTo(0,0):e.scrollIntoView({block:"start"}),e.querySelector(":scope > h1, :scope > h2, :scope > .api-res-head h3, :scope > .api-op-head h4, :scope > article > .api-op-head h4")?.focus({preventScroll:!0})}async function pi(e,t,n){ie(t,"API reference"),h(e,Ut("api-reference",o`<div class="toc-label">API</div><nav class="api-nav" id="apiNav" aria-label="API sections"></nav>`,o`<div class="api-content" id="apiContent"><p class="muted">Loading…</p></div>`));let s=c("#apiNav"),i=c("#apiContent"),r=p=>{let S=p.target instanceof Element?p.target.closest("a[data-sec], a[data-route]"):null,$=S?.dataset.route?document.getElementById(`route-${S.dataset.route}`):S?.dataset.sec?vl(S.dataset.sec):null;!$||p.button!==0||p.metaKey||p.ctrlKey||p.shiftKey||p.altKey||(p.preventDefault(),($.closest("[hidden]")||S?.closest("#apiEmpty"))&&v(""),history.replaceState(history.state,"",S?.getAttribute("href")??location.hash),yl($),ai(s,i,!1))};s.addEventListener("click",r),i.addEventListener("click",r);let a;try{a=await L("/api/reference",{signal:t})}catch(p){re(i,V(p),()=>ee((S,$)=>pi(S,$,n)));return}let l={meanings:new Map(a.refusals.flatMap(p=>p.codes.map(S=>[S.code,S.meaning]))),objects:new Map(a.objects.map(p=>[p.key,p.title])),heads:sp(a)};h(s,fp(a,l)),h(i,o`<header class="api-head" id="api-intro"><h1 tabindex="-1">API reference</h1><p class="muted">Every endpoint this instance serves, the one API the web editor and Claude both call. It's read from the running code, so it always matches this version. Base URL <code>${location.origin}</code>.</p><div class="api-filter">${O("search")}<input type="search" id="apiFilter" placeholder="Filter by name, path, URL, or error code" 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>${O("x")}</button></div></header>${pp(a)}${a.groups.map(p=>dp(p,l))}<p class="api-empty muted" id="apiEmpty" hidden>Nothing matches. Try a path segment like <code>sends</code>, a method like <code>DELETE</code>, or a code like <code>stale_revision</code>, or start from the <a href="#/api-reference" data-sec="overview">Introduction</a>.</p>`);let d=c("#apiFilter"),u=c("#apiFilterClear"),m=()=>ai(s,i,!1),v=p=>{d.value=p,u.hidden=p==="",gl(p,s,i),m()};d.addEventListener("input",()=>v(d.value)),u.addEventListener("click",()=>{v(""),d.focus()}),We(i),document.addEventListener("keydown",p=>{let S=p.target instanceof HTMLElement&&(p.target.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(p.target.tagName));p.key==="/"&&!S&&!p.metaKey&&!p.ctrlKey&&!p.altKey?(p.preventDefault(),d.focus()):p.key==="Escape"&&p.target===d&&d.value&&v("")},{signal:t}),gl("",s,i),window.addEventListener("scroll",()=>ai(s,i,!0),{passive:!0,signal:t}),window.scrollTo(0,0);let f=n?document.getElementById(`route-${n}`)??vl(n):null;f&&yl(f),m()}var Vs=V;function gp(e){let t=[Qe(e,"reschedule")?o`<button class="ghost" data-reschedule="${e.id}">Edit schedule</button>`:null,Qe(e,"cancel")?o`<button class="ghost" data-cancel="${e.id}">Cancel</button>`:null];return t.some(Boolean)?o`<div class="row" data-closes="${e.fire_at}">${t}</div>`:null}async function mi(e,t){ie(t,"Sent");let n={status:"sent",search:"",failures:"",sort:"fire_at",dir:"desc",offset:0,limit:50};h(e,o`<h1>Sent</h1>
210
- ${fn()?o`<p class="muted">A local stand-in sends these: they are recorded here, but nothing is delivered.</p>`:null}
211
- ${hs("sent-not-connected",o`No email provider connected, so nothing can be sent yet. ${Ae()} to start sending.`)}
210
+ </section>`}function kp(e){let t=e.map(n=>vi(n.status,o`${n.name}`,null,ve(n.meaning)));return mt("Status codes",o`<div class="api-ledger api-statuses">${t}</div>`)}function _p(e,t){let n=(i,r)=>o`<a href="#/api-reference/${i}" class="api-nav-res" data-sec="${i}">${r}</a>`,s=(i,r)=>o`<div class="api-nav-ops" data-of="${i.access}-${r.key}">${[...(t.heads.get(`${i.access}-${r.key}`)??[]).map(a=>o`<a href="#/api-reference/objects-${a.key}" class="api-nav-op" data-sec="objects-${a.key}">${a.title}</a>`),...Pl(i,r).map(a=>o`<a href="#/api-reference/${on(a)}" class="api-nav-op" data-route="${on(a)}">${a.title}</a>`)]}</div>`;return o`<a href="#/api-reference" class="api-nav-tier" data-sec="overview" data-tier="overview">Introduction</a>${e.concepts.map(i=>n(`overview-${i.key}`,i.title))}${e.groups.map(i=>o`<a href="#/api-reference/${i.access}" class="api-nav-tier" data-sec="${i.access}" data-tier="${i.access}">${i.title}</a>${i.resources.map(r=>[i.resources.length>1?n(`${i.access}-${r.key}`,r.title):null,s(i,r)])}`)}`}function Tp(e,t){let n=e.replace(/^[a-z]+:\/\/[^/]*/,"").replace(/[?#].*$/,"");if(!n.startsWith("/"))return!1;let s=n.replace(/(.)\/+$/,"$1").split("/"),i=t.split("/");return s.length===i.length&&i.every((r,a)=>r.startsWith(":")?s[a]!=="":r===s[a])}function Lp(e,t){let n=e.dataset.path;return(e.dataset.hay??"").includes(t)||n!==void 0&&Tp(t,n)}function Cl(e,t,n){let s=e.trim().toLowerCase().split(/\s+/).filter(Boolean);t.classList.toggle("filtering",s.length>0);let i=0;for(let l of D(".api-section",n)){let d=0;for(let p of D(".api-res",l)){let g=p.dataset.hay!==void 0,h=0;for(let y of g?[p]:D("li[data-hay]",p)){let E=s.every(N=>Lp(y,N));y.hidden=!E,h+=E?1:0;let R=y.dataset.route?t.querySelector(`a[data-route="${y.dataset.route}"]`):null;R&&(R.hidden=!E)}p.hidden=h===0;for(let y of D(`.api-nav-ops[data-of="${p.dataset.sec}"] a[data-sec]`,t))y.hidden=s.length>0&&h===0;let m=t.querySelector(`a[data-sec="${p.dataset.sec}"]`);m&&(m.hidden=h===0),d+=h}l.hidden=d===0;let u=t.querySelector(`a[data-tier="${l.dataset.tier}"]`);u&&(u.hidden=d===0),i+=d}let r=D(".api-section",n),a=r.filter(l=>!l.hidden).at(-1);for(let l of r)l.classList.toggle("api-last",l===a);c("#apiEmpty",n).hidden=i>0}function hi(e,t,n){let s=(c(".room-bar").getBoundingClientRect().height||54)+8,i=D(".api-section, .api-res, .api-object, li[data-route]",t).filter(y=>!y.closest("[hidden]")),r=window.scrollY>0&&Math.ceil(window.innerHeight+window.scrollY)>=document.documentElement.scrollHeight,a;for(let y of i)if(r||y.getBoundingClientRect().top<=s)a=y;else break;let l=D("a[data-sec], a[data-route]",e).filter(y=>!y.hidden&&getComputedStyle(y).display!=="none"),d=y=>y?(y.dataset.route?l.find(R=>R.dataset.route===y.dataset.route):l.find(R=>R.dataset.sec===(y.dataset.sec??y.dataset.tier)))??d(y.parentElement?.closest(".api-res, .api-section")):void 0,u=a?.closest(".api-res")?.dataset.sec;for(let y of D(".api-nav-ops",e))y.classList.toggle("open",y.dataset.of===u);let p=d(a??i[0]);for(let y of D("a[data-sec], a[data-route]",e))y.classList.toggle("active",y===p);let g=e.closest(".rail-inner");if(p&&g&&g.scrollHeight>g.clientHeight){let y=p.getBoundingClientRect(),E=g.getBoundingClientRect();(y.top<E.top+40||y.bottom>E.bottom-40)&&(g.scrollTop+=y.top-E.top-E.height/3)}let h=a?.dataset.route??a?.dataset.sec??a?.dataset.tier,m=h&&h!=="overview"&&window.scrollY>0?`#/api-reference/${h}`:"#/api-reference";n&&location.hash!==m&&history.replaceState(history.state,"",m)}function Al(e){return document.getElementById(e==="overview"?"api-intro":`api-${e}`)}function Hl(e){e.id==="api-intro"?window.scrollTo(0,0):e.scrollIntoView({block:"start"}),e.querySelector(":scope > h1, :scope > h2, :scope > .api-res-head h3, :scope > .api-op-head h4, :scope > article > .api-op-head h4")?.focus({preventScroll:!0})}async function Si(e,t,n){ie(t,"API reference"),f(e,jt("api-reference",o`<div class="toc-label">API</div><nav class="api-nav" id="apiNav" aria-label="API sections"></nav>`,o`<div class="api-content" id="apiContent"><p class="muted">Loading…</p></div>`));let s=c("#apiNav"),i=c("#apiContent"),r=m=>{let y=m.target instanceof Element?m.target.closest("a[data-sec], a[data-route]"):null,E=y?.dataset.route?document.getElementById(`route-${y.dataset.route}`):y?.dataset.sec?Al(y.dataset.sec):null;!E||m.button!==0||m.metaKey||m.ctrlKey||m.shiftKey||m.altKey||(m.preventDefault(),(E.closest("[hidden]")||y?.closest("#apiEmpty"))&&g(""),history.replaceState(history.state,"",y?.getAttribute("href")??location.hash),Hl(E),hi(s,i,!1))};s.addEventListener("click",r),i.addEventListener("click",r);let a;try{a=await C("/api/reference",{signal:t})}catch(m){re(i,z(m),()=>ne((y,E)=>Si(y,E,n)));return}let l={meanings:new Map(a.refusals.flatMap(m=>m.codes.map(y=>[y.code,y.meaning]))),objects:new Map(a.objects.map(m=>[m.key,m.title])),heads:hp(a)};f(s,_p(a,l)),f(i,o`<header class="api-head" id="api-intro"><h1 tabindex="-1">API reference</h1><p class="muted">Every endpoint this instance serves, the one API the web editor and Claude both call. It's read from the running code, so it always matches this version. Base URL <code>${location.origin}</code>.</p><div class="api-filter">${B("search")}<input type="search" id="apiFilter" placeholder="Filter by name, path, URL, or error code" 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>${B("x")}</button></div></header>${xp(a)}${a.groups.map(m=>$p(m,l))}<p class="api-empty muted" id="apiEmpty" hidden>Nothing matches. Try a path segment like <code>sends</code>, a method like <code>DELETE</code>, or a code like <code>stale_revision</code>, or start from the <a href="#/api-reference" data-sec="overview">Introduction</a>.</p>`);let d=c("#apiFilter"),u=c("#apiFilterClear"),p=()=>hi(s,i,!1),g=m=>{d.value=m,u.hidden=m==="",Cl(m,s,i),p()};d.addEventListener("input",()=>g(d.value)),u.addEventListener("click",()=>{g(""),d.focus()}),je(i),document.addEventListener("keydown",m=>{let y=m.target instanceof HTMLElement&&(m.target.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(m.target.tagName));m.key==="/"&&!y&&!m.metaKey&&!m.ctrlKey&&!m.altKey?(m.preventDefault(),d.focus()):m.key==="Escape"&&m.target===d&&d.value&&g("")},{signal:t}),Cl("",s,i),window.addEventListener("scroll",()=>hi(s,i,!0),{passive:!0,signal:t}),window.scrollTo(0,0);let h=n?document.getElementById(`route-${n}`)??Al(n):null;h&&Hl(h),p()}var Qs=z;function Rp(e){let t=[Je(e,"reschedule")?o`<button class="ghost" data-reschedule="${e.id}">Edit schedule</button>`:null,Je(e,"cancel")?o`<button class="ghost" data-cancel="${e.id}">Cancel</button>`:null];return t.some(Boolean)?o`<div class="row" data-closes="${e.fire_at}">${t}</div>`:null}async function wi(e,t){ie(t,"Sent");let n={status:"sent",search:"",failures:"",sort:"fire_at",dir:"desc",offset:0,limit:50};f(e,o`<h1>Sent</h1>
211
+ ${gn()?o`<p class="muted">A local stand-in sends these: they are recorded here, but nothing is delivered.</p>`:null}
212
+ ${ys("sent-not-connected",o`No email provider connected, so nothing can be sent yet. ${He()} to start sending.`)}
212
213
  <div id="stuck"></div>
213
214
  <h2>Scheduled</h2><div id="scheduled"><p class="muted">Loading…</p></div>
214
215
  <div id="active"></div>
215
216
  <h2>Sent posts</h2>
216
- ${Kt({searchPlaceholder:"Search subject\u2026",failures:!0})}
217
+ ${Yt({searchPlaceholder:"Search subject\u2026",failures:!0})}
217
218
  <div id="sendsList"><p class="muted">Loading…</p></div>
218
- <div id="sendsPager"></div>`);let s=c("#stuck",e),i=c("#scheduled",e),r=c("#active",e),a=c("#sendsList",e),l=c("#sendsPager",e),d=Cs(e,t),u=[],m=!1,v=0,f=0,p=new Map;function S(){F(),K(),Fe()}function $(b){let E=b.filter(C=>C.status==="sending"&&!wn(C));h(r,E.length?o`<h2>In progress</h2>${E.map(C=>As(C))}`:o``);for(let C of N("[data-watch]",r))C.onclick=x=>{x.target instanceof Element&&x.target.tagName==="A"||(location.hash=`#/sent/${C.dataset.watch}`)}}function M(b){let E=b.flatMap(q=>{let ne=he(q,"refused");return ne?[{s:q,c:ne}]:[]}),C=ge()?E.filter(q=>q.c.cause==="not_connected"):[],x=E.filter(q=>!C.includes(q)),W=e.querySelector(".sent-not-connected");W&&(W.hidden=C.length>0);let oe=C.map(({s:q},ne)=>o`${ne===0?"":ne===C.length-1?" and ":", "}<a href="#/sent/${q.id}">${q.subject}</a>`),k=C.length?o`<div class="card stuck-card sent-held"><div class="stuck-head"><span class="stuck-dot">⚠️</span><div>${fs(o`${oe}`,C.length)}</div></div></div>`:null,G=b.flatMap(q=>{let ne=he(q,"wedged");return ne?[{s:q,c:ne}]:[]});h(s,o`${k}${x.map(({s:q,c:ne})=>o`<div class="card stuck-card"><div class="stuck-head"><span class="stuck-dot">⚠️</span><div><strong><a href="#/sent/${q.id}">${q.subject}</a></strong><div class="muted">${ne.message}</div></div></div></div>`)}${G.map(({s:q,c:ne})=>o`<div class="card stuck-card"><div class="stuck-head"><span class="stuck-dot">⚠️</span><div><strong><a href="#/sent/${q.id}">${q.subject}</a></strong><div class="muted">${ne.message}</div></div></div>${Qe(q,"resolve")?o`<button class="primary" data-resolve="${q.id}">Resolve…</button>`:null}</div>`)}`);for(let q of N("[data-resolve]",s)){let ne=G.find(He=>He.s.id===q.dataset.resolve);ne&&(q.onclick=()=>Os({id:ne.s.id,c_in_flight:ne.c.count},S,t))}}function P(){for(let b of p.values()){let E=N("tr[data-id]",a).find(C=>C.dataset.id===b.id)?.querySelector("td.delivered");E&&h(E,jt(b.counts))}}function A(){let b=qt(u,p);M(b),m&&$(b),P()}let H=b=>b==="scheduled"||b==="due",y=b=>b==="sent"||b==="complete";function w(b){for(let E of b.sends)p.set(E.id,E);for(let E of b.removed)p.delete(E);A(),(b.sends.some(E=>H(ot(E)))||b.changes.some(E=>H(E.from)))&&F(),b.removed.length?(F(),K()):b.changes.some(E=>y(E.to)&&!y(E.from))&&K()}let T=!1;function D(b){let E=b.filter(C=>C!==null);T||!E.length||(T=!0,Ft({cursor:E.map(C=>C.cursor).reduce(ys),sends:E.flatMap(C=>C.sends)},{update:w,stale:()=>{t.aborted||ee(mi)}},t))}async function R(){try{let b=await L("/api/sends?status=sending&limit=200",{signal:t});return u=b.sends,m=!0,A(),b}catch(b){return re(r,Vs(b),async()=>D([await R()])),null}}async function F(){let b=++v;try{let E=await L("/api/sends?status=scheduled&sort=fire_at&dir=asc&limit=200",{signal:t});if(b!==v)return E;let{sends:C}=E,x=k=>o`<div class="card spread clickable sched-card" data-post="${k.post_id}"><div><a class="card-link sched-subj" href="#/edit/${k.post_id}">${k.subject}</a><div class="muted">${Rs(k)} · ${j(k.fire_at)} · ${k.audience.count} recipients${Ls(k)}</div></div>${gp(k)}</div>`,[W,...oe]=C;if(!W)h(i,o`<p class="muted">Nothing scheduled.</p>`);else{h(i,o`${x(W)}${oe.length?o`<div id="schedMore" hidden>${oe.map(x)}</div><button type="button" class="ghost sched-toggle" id="schedToggle" aria-expanded="false">Show all ${C.length} scheduled</button>`:null}`);let k=i.querySelector("#schedToggle");k&&(k.onclick=()=>{let G=c("#schedMore",i),q=G.hidden;G.hidden=!q,k.setAttribute("aria-expanded",String(q)),k.textContent=q?"Show fewer":`Show all ${C.length} scheduled`})}for(let k of N(".card.clickable",i))k.onclick=G=>{let q=G.target;q instanceof Element&&q.tagName!=="A"&&!q.closest("button")&&(location.hash=`#/edit/${k.dataset.post}`)};for(let k of N("[data-reschedule]",i)){let G=C.find(q=>q.id===k.dataset.reschedule);G&&(k.onclick=()=>{Ds(G,S,t)})}for(let k of N("[data-cancel]",i))k.onclick=()=>Z(k,"Canceling\u2026",async()=>{try{await L(`/api/sends/${k.dataset.cancel}/cancel`,{method:"POST"}),U("Canceled"),S()}catch(G){U(Vs(G))}});return d(),E}catch(E){return b===v&&re(i,Vs(E),async()=>D([await F()])),null}}async function K(){let b=++f;try{let E=await L(`/api/sends?${Vt(n)}`,{signal:t});if(b!==f)return E;let C=E.sends;if(!C.length)return h(a,o`<p class="muted">${n.search?"No sent posts match.":n.failures?"Every sent post delivered cleanly.":"No sent posts yet."}</p>`),h(l,o``),E;h(a,o`<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>${Ee("Subject","subject",n)}${Ee("When","fire_at",n)}${Ee("Recipients","audience.count",n,"num")}<th class="num">Delivered</th></tr></thead><tbody>${C.map(x=>o`<tr class="clickable" data-id="${x.id}"><td class="subject">${xt(o`<a href="#/sent/${x.id}">${x.subject}</a>`,x)}</td><td class="muted">${j(x.completed_at??x.fire_at)}</td><td class="num recipients"><span class="n">${x.audience.count.toLocaleString()}</span></td><td class="num delivered">${jt(x.counts)}</td></tr>`)}</tbody></table></div>`),it(a,n,K);for(let x of N("tr[data-id]",a))x.onclick=W=>{W.target instanceof Element&&W.target.tagName==="A"||(location.hash=`#/sent/${x.dataset.id}`)};return rt(l,n,E.page,K),P(),E}catch(E){return b===f&&re(a,Vs(E),async()=>D([await K()])),null}}Gt(e,n,K),D(await Promise.all([F(),R(),K()]))}var vp="erased:";function Sl(e){return e.startsWith(vp)}function Rt(e,t,n){let s=URL.createObjectURL(new Blob([e],{type:n})),i=document.createElement("a");i.href=s,i.download=t,i.click(),setTimeout(()=>URL.revokeObjectURL(s),1e4)}function Ks(e){if(e.event==="delivered")return{label:"Delivered",sw:"ok"};if(e.event==="bounced")return e.bounce_kind==="hard"?{label:"Hard bounce",sw:"warn"}:e.bounce_kind==="soft"?{label:"Soft bounce",sw:"warn"}:{label:"Bounce",sw:"warn"};if(e.event==="complained")return{label:"Complained",sw:"danger"};switch(e.status){case"unsent":return{label:"Unsent",sw:"neutral"};case"skipped":return{label:"Skipped",sw:"neutral"};case"accepted":return{label:"Accepted",sw:"sending"};default:return{label:"In flight",sw:"sending"}}}var Mn=V;async function fi(e,t,n){let s=()=>{n.aborted||ee((a,l)=>fi(e,a,l))};ie(n,"Sent"),h(t,o`<p class="muted">Loading…</p>`);let i;try{i=await L(`/api/sends/${e}`,{signal:n})}catch(a){re(t,Mn(a),s);return}if(n.aborted)return;let{send:r}=i;if(ie(n,r.subject||"Untitled"),r.status==="scheduled"){location.replace(`#/edit/${r.post_id}`);return}return r.status==="sending"?_p(i,t,n,s):Hp(e,i,t,n,s)}var yp={scheduled:{label:"Scheduled",tone:"scheduled"},due:{label:"Due",tone:"scheduled"},progressing:{label:"Sending",tone:"sending"},retrying:{label:"Retrying",tone:"warn"},backing_off:{label:"Backing off",tone:"warn"},needs_attention:{label:"Needs attention",tone:"danger"},settling:{label:"Settling",tone:"sending"},complete:{label:"Complete",tone:"ok"},canceled:{label:"Canceled",tone:"muted"}},wp={progressing:"Handing recipients to the provider.",retrying:"Some recipients hit a transient error and will be retried.",backing_off:"Some recipients hit a transient error. They\u2019re retried on the next sweep.",needs_attention:"Some deliveries can\u2019t be confirmed either way, and wait on your decision.",settling:"Dispatch complete, waiting on delivery receipts."};function Sp(e){let t=e.provider.halt;return t&&Cl(e)?`${t.cause==="not_connected"?"No email provider is connected":t.reason==="account"?"The provider is refusing this account":"The provider is unavailable"}, so this post waits with the send already held and hasn\u2019t handed anyone off; ${Wt(t.retry_at)}.`:Re(e,"refused")&&t?.cause==="not_connected"?`No email provider is connected, so nothing goes out until one is; ${Wt(t.retry_at)}.`:Re(e,"refused")&&t?`The provider is refusing ${To(t.cause)?"this send":"this account"}, so nothing more goes out until it\u2019s fixed; ${Wt(t.retry_at)}.`:e.phase==="backing_off"&&t?.reason==="unavailable"?`The provider is unavailable; ${Wt(t.retry_at)}.`:Re(e,"stuck")?"Nothing is being handed off to the provider right now.":e.phase==="progressing"&&e.counts.dispatched===0&&e.counts.pending>0?"Paused between ticks. It resumes on the next sweep.":wp[e.phase]||""}function In(e){let t=yp[e]??{label:e,tone:"muted"};return o`<span class="phase-pill tone-${t.tone}">${t.label}</span>`}function $p(e,t,n,s,i){let r=s>0?yn(100*n/s):0;return o`<div class="wbar-row">
219
+ <div id="sendsPager"></div>`);let s=c("#stuck",e),i=c("#scheduled",e),r=c("#active",e),a=c("#sendsList",e),l=c("#sendsPager",e),d=Ns(e,t),u=[],p=!1,g=0,h=0,m=new Map;function y(){O(),V(),Ue()}function E(S){let T=S.filter(H=>H.status==="sending"&&!xn(H));f(r,T.length?o`<h2>In progress</h2>${T.map(H=>Os(H))}`:o``);for(let H of D("[data-watch]",r))H.onclick=$=>{$.target instanceof Element&&$.target.tagName==="A"||(location.hash=`#/sent/${H.dataset.watch}`)}}function R(S){let T=S.flatMap(F=>{let ee=fe(F,"refused");return ee?[{s:F,c:ee}]:[]}),H=be()?T.filter(F=>F.c.cause==="not_connected"):[],$=T.filter(F=>!H.includes(F)),L=e.querySelector(".sent-not-connected");L&&(L.hidden=H.length>0);let G=H.map(({s:F},ee)=>o`${ee===0?"":ee===H.length-1?" and ":", "}<a href="#/sent/${F.id}">${F.subject}</a>`),k=H.length?o`<div class="card stuck-card sent-held"><div class="stuck-head"><span class="stuck-dot">⚠️</span><div>${vs(o`${G}`,H.length)}</div></div></div>`:null,K=S.flatMap(F=>{let ee=fe(F,"wedged");return ee?[{s:F,c:ee}]:[]});f(s,o`${k}${$.map(({s:F,c:ee})=>o`<div class="card stuck-card"><div class="stuck-head"><span class="stuck-dot">⚠️</span><div><strong><a href="#/sent/${F.id}">${F.subject}</a></strong><div class="muted">${ee.message}</div></div></div></div>`)}${K.map(({s:F,c:ee})=>o`<div class="card stuck-card"><div class="stuck-head"><span class="stuck-dot">⚠️</span><div><strong><a href="#/sent/${F.id}">${F.subject}</a></strong><div class="muted">${ee.message}</div></div></div>${Je(F,"resolve")?o`<button class="primary" data-resolve="${F.id}">Resolve…</button>`:null}</div>`)}`);for(let F of D("[data-resolve]",s)){let ee=K.find(Le=>Le.s.id===F.dataset.resolve);ee&&(F.onclick=()=>Vs({id:ee.s.id,c_in_flight:ee.c.count},y,t))}}function N(){for(let S of m.values()){let T=D("tr[data-id]",a).find(H=>H.dataset.id===S.id)?.querySelector("td.delivered");T&&f(T,Kt(S.counts))}}function A(){let S=Wt(u,m);R(S),p&&E(S),N()}let P=S=>S==="scheduled"||S==="due",v=S=>S==="sent"||S==="complete";function w(S){for(let T of S.sends)m.set(T.id,T);for(let T of S.removed)m.delete(T);A(),(S.sends.some(T=>P(rt(T)))||S.changes.some(T=>P(T.from)))&&O(),S.removed.length?(O(),V()):S.changes.some(T=>v(T.to)&&!v(T.from))&&V()}let I=!1;function j(S){let T=S.filter(H=>H!==null);I||!T.length||(I=!0,Vt({cursor:T.map(H=>H.cursor).reduce(ks),sends:T.flatMap(H=>H.sends)},{update:w,stale:()=>{t.aborted||ne(wi)}},t))}async function x(){try{let S=await C("/api/sends?status=sending&limit=200",{signal:t});return u=S.sends,p=!0,A(),S}catch(S){return re(r,Qs(S),async()=>j([await x()])),null}}async function O(){let S=++g;try{let T=await C("/api/sends?status=scheduled&sort=fire_at&dir=asc&limit=200",{signal:t});if(S!==g)return T;let{sends:H}=T,$=k=>o`<div class="card spread clickable sched-card" data-post="${k.post_id}"><div><a class="card-link sched-subj" href="#/edit/${k.post_id}">${k.subject}</a><div class="muted">${Ms(k)} · ${W(k.fire_at)} · ${at(k.audience.count)}${Is(k)}</div></div>${Rp(k)}</div>`,[L,...G]=H;if(!L)f(i,o`<p class="muted">Nothing scheduled.</p>`);else{f(i,o`${$(L)}${G.length?o`<div id="schedMore" hidden>${G.map($)}</div><button type="button" class="ghost sched-toggle" id="schedToggle" aria-expanded="false">Show all ${H.length} scheduled</button>`:null}`);let k=i.querySelector("#schedToggle");k&&(k.onclick=()=>{let K=c("#schedMore",i),F=K.hidden;K.hidden=!F,k.setAttribute("aria-expanded",String(F)),k.textContent=F?"Show fewer":`Show all ${H.length} scheduled`})}for(let k of D(".card.clickable",i))k.onclick=K=>{let F=K.target;F instanceof Element&&F.tagName!=="A"&&!F.closest("button")&&(location.hash=`#/edit/${k.dataset.post}`)};for(let k of D("[data-reschedule]",i)){let K=H.find(F=>F.id===k.dataset.reschedule);K&&(k.onclick=()=>{Ws(K,y,t)})}for(let k of D("[data-cancel]",i))k.onclick=()=>J(k,"Canceling\u2026",async()=>{try{await C(`/api/sends/${k.dataset.cancel}/cancel`,{method:"POST"}),q("Canceled"),y()}catch(K){q(Qs(K))}});return d(),T}catch(T){return S===g&&re(i,Qs(T),async()=>j([await O()])),null}}async function V(){let S=++h;try{let T=await C(`/api/sends?${zt(n)}`,{signal:t});if(S!==h)return T;let H=T.sends;if(!H.length)return f(a,o`<p class="muted">${n.search?"No sent posts match.":n.failures?"Every sent post delivered cleanly.":"No sent posts yet."}</p>`),f(l,o``),T;f(a,o`<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>${xe("Subject","subject",n)}${xe("When","fire_at",n)}${xe("Recipients","audience.count",n,"num")}<th class="num">Delivered</th></tr></thead><tbody>${H.map($=>o`<tr class="clickable" data-id="${$.id}"><td class="subject">${xt(o`<a href="#/sent/${$.id}">${$.subject}</a>`,$)}</td><td class="muted">${W($.completed_at??$.fire_at)}</td><td class="num recipients"><span class="n">${$.audience.count.toLocaleString()}</span></td><td class="num delivered">${Kt($.counts)}</td></tr>`)}</tbody></table></div>`),lt(a,n,V);for(let $ of D("tr[data-id]",a))$.onclick=L=>{L.target instanceof Element&&L.target.tagName==="A"||(location.hash=`#/sent/${$.dataset.id}`)};return ct(l,n,T.page,V),N(),T}catch(T){return S===h&&re(a,Qs(T),async()=>j([await V()])),null}}Zt(e,n,V),j(await Promise.all([O(),x(),V()]))}var Cp="erased:";function Il(e){return e.startsWith(Cp)}function Ct(e,t,n){let s=URL.createObjectURL(new Blob([e],{type:n})),i=document.createElement("a");i.href=s,i.download=t,i.click(),setTimeout(()=>URL.revokeObjectURL(s),1e4)}function Xs(e){if(e.event==="delivered")return{label:"Delivered",sw:"ok"};if(e.event==="bounced")return e.bounce_kind==="hard"?{label:"Hard bounce",sw:"warn"}:e.bounce_kind==="soft"?{label:"Soft bounce",sw:"warn"}:{label:"Bounce",sw:"warn"};if(e.event==="complained")return{label:"Complained",sw:"danger"};switch(e.status){case"unsent":return{label:"Unsent",sw:"neutral"};case"skipped":return{label:"Skipped",sw:"neutral"};case"accepted":return{label:"Accepted",sw:"sending"};default:return{label:"In flight",sw:"sending"}}}var Dn=z;async function $i(e,t,n){let s=()=>{n.aborted||ne((a,l)=>$i(e,a,l))};ie(n,"Sent"),f(t,o`<p class="muted">Loading…</p>`);let i;try{i=await C(`/api/sends/${e}`,{signal:n})}catch(a){re(t,Dn(a),s);return}if(n.aborted)return;let{send:r}=i;if(ie(n,r.subject||"Untitled"),r.status==="scheduled"){location.replace(`#/edit/${r.post_id}`);return}return r.status==="sending"?Dp(i,t,n,s):Vp(e,i,t,n,s)}var Ap={scheduled:{label:"Scheduled",tone:"scheduled"},due:{label:"Due",tone:"scheduled"},progressing:{label:"Sending",tone:"sending"},retrying:{label:"Retrying",tone:"warn"},backing_off:{label:"Backing off",tone:"warn"},needs_attention:{label:"Needs attention",tone:"danger"},settling:{label:"Settling",tone:"sending"},complete:{label:"Complete",tone:"ok"},canceled:{label:"Canceled",tone:"muted"}},Hp={progressing:"Handing recipients to the provider.",retrying:"Some recipients hit a transient error and will be retried.",backing_off:"Some recipients hit a transient error. They\u2019re retried on the next sweep.",needs_attention:"Some deliveries can\u2019t be confirmed either way, and wait on your decision.",settling:"Dispatch complete, waiting on delivery receipts."};function Pp(e){let t=e.provider.halt;return t&&jl(e)?`${t.cause==="not_connected"?"No email provider is connected":t.reason==="account"?"The provider is refusing this account":"The provider is unavailable"}, so this post waits with the send already held and hasn\u2019t handed anyone off; ${Gt(t.retry_at)}.`:Ce(e,"refused")&&t?.cause==="not_connected"?`No email provider is connected, so nothing goes out until one is; ${Gt(t.retry_at)}.`:Ce(e,"refused")&&t?`The provider is refusing ${Mo(t.cause)?"this send":"this account"}, so nothing more goes out until it\u2019s fixed; ${Gt(t.retry_at)}.`:e.phase==="backing_off"&&t?.reason==="unavailable"?`The provider is unavailable; ${Gt(t.retry_at)}.`:Ce(e,"stuck")?"Nothing is being handed off to the provider right now.":e.phase==="progressing"&&e.counts.dispatched===0&&e.counts.pending>0?"Paused between ticks. It resumes on the next sweep.":Hp[e.phase]||""}function Bn(e){let t=Ap[e]??{label:e,tone:"muted"};return o`<span class="phase-pill tone-${t.tone}">${t.label}</span>`}function Ip(e,t,n,s,i){let r=s>0?En(100*n/s):0;return o`<div class="wbar-row">
219
220
  <div class="wbar-head"><span class="wbar-label">${t}</span><span class="wbar-count">${n.toLocaleString()} / ${s.toLocaleString()}</span></div>
220
221
  <div class="wbar" role="progressbar" aria-valuenow="${r}" aria-valuemin="0" aria-valuemax="100"><div class="wbar-fill tone-${e}" style="width:${r}%"></div></div>
221
222
  ${i?o`<div class="wbar-sub muted">${i}</div>`:null}
222
- </div>`}function xp(e,t,n,s,i){let r=s>0?yn(100*n/s):0,a=s>0?yn(100*t/s):0;return o`<div class="wbar-row">
223
+ </div>`}function Mp(e,t,n,s,i){let r=s>0?En(100*n/s):0,a=s>0?En(100*t/s):0;return o`<div class="wbar-row">
223
224
  <div class="wbar-head"><span class="wbar-label">${e}</span><span class="wbar-count">${t.toLocaleString()} of ${n.toLocaleString()} accepted</span></div>
224
225
  <div class="wbar dual" role="progressbar" aria-valuenow="${a}" aria-valuemin="0" aria-valuemax="100">
225
226
  <div class="wbar-fill tone-accepted" style="width:${r}%"></div>
226
227
  <div class="wbar-fill tone-ok" style="width:${a}%"></div>
227
228
  </div>
228
229
  <div class="wbar-sub muted">${i}</div>
229
- </div>`}var Ep=[{key:"pending",label:"Pending",sw:"neutral"},{key:"dispatched",label:"In flight",sw:"sending"},{key:"accepted",label:"Accepted",sw:"sending"},{key:"delivered",label:"Delivered",sw:"ok"},{key:"bounced",label:"Bounced",sw:"warn"},{key:"complained",label:"Complained",sw:"danger"},{key:"unsent",label:"Unsent",sw:"neutral"},{key:"skipped",label:"Skipped",sw:"neutral"}];function kp(e){return o`<div class="wcounts">${Ep.map(t=>o`<div class="wcount"><div class="wcount-n">${(e[t.key]||0).toLocaleString()}</div><div class="wcount-l"><span class="rec-sw sw-${t.sw}"></span>${t.label}</div></div>`)}</div>`}function Rl(e){let t=e.counts,n=t.accepted+t.delivered+t.bounced+t.complained,s=e.dispatch.rate_per_min,i=e.dispatch.eta_ms,r=e.status==="sending"?`${Sp(e)}${i?`${s?` \xB7 ~${s.toLocaleString()}/min`:""} \xB7 ETA ${Ts(i)}`:""}`:"Dispatch complete.",a=(t.unsent||0)+(t.bounced||0)+(t.complained||0),l=fn()?"Local stand-in: nothing is delivered":ge()?"No email provider connected":`Provider: ${e.provider?.name||"unknown"}`,d=he(e,"refused"),u=d?.cause==="not_connected",m=To(d?.cause??null),v=he(e,"stuck"),f=he(e,"provider_unavailable"),p=f?.severity==="warn"?f:void 0;return o`
230
- ${v?o`<div class="health amber" role="status"><span class="health-dot">⚠️</span><div>${v.message}</div></div>`:null}
231
- ${p?o`<div class="health amber" role="status"><span class="health-dot">⚠️</span><div>${p.message}</div></div>`:null}
232
- ${d?o`<div class="health red" role="alert"><span class="health-dot">⚠️</span><div><strong>${u?"No email provider is connected":m?"The provider is refusing this send":"The provider is refusing this account"}</strong>${u?null:o`<div>${d.error}</div>`}<div class="refused-advice">${u?Co():d.advice}</div><div>${d.since===null?null:`Since ${j(d.since)}. `}No one has been marked unsent because of this, and once ${u?"one is connected":"this is fixed"} the send resumes at its next retry${d.retry_at===null?"":`, ${j(d.retry_at)}`}.</div></div></div>`:null}
230
+ </div>`}var Np=[{key:"pending",label:"Pending",sw:"neutral"},{key:"dispatched",label:"In flight",sw:"sending"},{key:"accepted",label:"Accepted",sw:"sending"},{key:"delivered",label:"Delivered",sw:"ok"},{key:"bounced",label:"Bounced",sw:"warn"},{key:"complained",label:"Complained",sw:"danger"},{key:"unsent",label:"Unsent",sw:"neutral"},{key:"skipped",label:"Skipped",sw:"neutral"}];function Op(e){return o`<div class="wcounts">${Np.map(t=>o`<div class="wcount"><div class="wcount-n">${(e[t.key]||0).toLocaleString()}</div><div class="wcount-l"><span class="rec-sw sw-${t.sw}"></span>${t.label}</div></div>`)}</div>`}function ql(e){let t=e.counts,n=t.accepted+t.delivered+t.bounced+t.complained,s=e.dispatch.rate_per_min,i=e.dispatch.eta_ms,r=e.status==="sending"?`${Pp(e)}${i?`${s?` \xB7 ~${s.toLocaleString()}/min`:""} \xB7 ETA ${Ps(i)}`:""}`:"Dispatch complete.",a=(t.unsent||0)+(t.bounced||0)+(t.complained||0),l=gn()?"Local stand-in: nothing is delivered":be()?"No email provider connected":`Provider: ${e.provider?.name||"unknown"}`,d=fe(e,"refused"),u=d?.cause==="not_connected",p=Mo(d?.cause??null),g=fe(e,"stuck"),h=fe(e,"provider_unavailable"),m=h?.severity==="warn"?h:void 0;return o`
231
+ ${g?o`<div class="health amber" role="status"><span class="health-dot">⚠️</span><div>${g.message}</div></div>`:null}
232
+ ${m?o`<div class="health amber" role="status"><span class="health-dot">⚠️</span><div>${m.message}</div></div>`:null}
233
+ ${d?o`<div class="health red" role="alert"><span class="health-dot">⚠️</span><div><strong>${u?"No email provider is connected":p?"The provider is refusing this send":"The provider is refusing this account"}</strong>${u?null:o`<div>${d.error}</div>`}<div class="refused-advice">${u?Do():d.advice}</div><div>${d.since===null?null:`Since ${W(d.since)}. `}No one has been marked unsent because of this, and once ${u?"one is connected":"this is fixed"} the send resumes at its next retry${d.retry_at===null?"":`, ${W(d.retry_at)}`}.</div></div></div>`:null}
233
234
  <div class="wbars">
234
- ${$p("sending","Dispatch: provider-accepted",n,e.audience.count,r)}
235
- ${xp("Delivery: webhook-confirmed",e.delivery.confirmed,n,e.audience.count,"Delivery lags acceptance: grey is accepted but unconfirmed, and green fills in as receipts arrive.")}
235
+ ${Ip("sending","Dispatch: provider-accepted",n,e.audience.count,r)}
236
+ ${Mp("Delivery: webhook-confirmed",e.delivery.confirmed,n,e.audience.count,"Delivery lags acceptance: grey is accepted but unconfirmed, and green fills in as receipts arrive.")}
236
237
  </div>
237
- ${kp(t)}
238
- <div class="whealth${a?" has-failures":""}"><span class="whealth-dot"></span><span>${l} · ${a?`${a.toLocaleString()} bounced / unsent / complained`:"no delivery failures"}</span></div>`}function Cl(e){return e.status==="sending"&&e.provider.halt!==null&&!e.audience.fixed}function Gs(e){let t=Xa(e);return t===null?null:o`<div class="rec-meta rec-by">${t}</div>`}function Al(e){if(Cl(e))return o`Due ${j(e.fire_at)} · held before handing anyone off · about ${e.audience.count.toLocaleString()} recipients`;let t=e.started_at?`Started ${j(e.started_at)}`:"Sending now";return o`${t} · ${e.audience.count.toLocaleString()} recipients`}function $l(e){let t=e;return o`
238
+ ${Op(t)}
239
+ <div class="whealth${a?" has-failures":""}"><span class="whealth-dot"></span><span>${l} · ${a?`${a.toLocaleString()} bounced / unsent / complained`:"no delivery failures"}</span></div>`}function jl(e){return e.status==="sending"&&e.provider.halt!==null&&!e.audience.fixed}function eo(e){let t=pl(e);return t===null?null:o`<div class="rec-meta rec-by">${t}</div>`}function Vl(e){if(jl(e))return o`Due ${W(e.fire_at)} · held before handing anyone off · about ${at(e.audience.count)}`;let t=e.started_at?`Started ${W(e.started_at)}`:"Sending now";return o`${t} · ${at(e.audience.count)}`}function Ml(e){let t=e;return o`
239
240
  <div class="editor-head">
240
241
  <a href="#/sent" class="back">← Sent</a>
241
- ${Qe(t,"resolve")?o`<button type="button" class="primary" id="resolveBtn">Resolve…</button>`:null}
242
+ ${Je(t,"resolve")?o`<button type="button" class="primary" id="resolveBtn">Resolve…</button>`:null}
242
243
  </div>
243
244
  <div class="card rec-card watch-card">
244
245
  <div class="rec-head">
245
- <div class="watch-title"><h1>${e.subject||o`<em>untitled</em>`}</h1><span id="watchPill">${In(t.phase)}</span></div>
246
- <div class="rec-meta" id="watchMeta">${Al(e)}</div>
247
- <div id="watchBy">${Gs(e)}</div>
246
+ <div class="watch-title"><h1>${e.subject||o`<em>untitled</em>`}</h1><span id="watchPill">${Bn(t.phase)}</span></div>
247
+ <div class="rec-meta" id="watchMeta">${Vl(e)}</div>
248
+ <div id="watchBy">${eo(e)}</div>
248
249
  </div>
249
- <div id="watchBody">${Rl(t)}</div>
250
+ <div id="watchBody">${ql(t)}</div>
250
251
  <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>
251
- </div>`}function xl(e,t,n,s){let i=e.querySelector("#resolveBtn");if(i){let r=he(t,"wedged")?.count??t.counts.dispatched;i.onclick=()=>Os({id:t.id,c_in_flight:r},n,s)}}function El(e,t){let n=e.querySelector("#watchPill");n&&h(n,In(t.phase));let s=e.querySelector("#watchMeta");s&&h(s,Al(t));let i=e.querySelector("#watchBy");i&&h(i,o`${Gs(t)}`);let r=e.querySelector("#watchBody");r&&h(r,Rl(t))}function _p(e,t,n,s){h(t,$l(e.send)),xl(t,e.send,s,n);let i=e.send;bt(1e3,()=>El(t,i),n),gn(e,{update:r=>{if(r.status!=="sending"){s();return}i=r,Qe(r,"resolve")!==!!t.querySelector("#resolveBtn")?(h(t,$l(r)),xl(t,r,s,n)):El(t,r)},removed:s,stale:s},n)}function kl(e){let t=[{n:e.recipients,label:"Recipients",cls:"",sw:"neutral"},{n:e.delivered,label:"Delivered",cls:"ok",sw:"ok"},{n:e.bounced,label:"Bounced",cls:"warn",sw:"warn"},{n:e.complained,label:"Complained",cls:"danger",sw:"danger"},{n:e.unsent,label:"Unsent",cls:"",sw:"neutral"}];return o`${t.map(n=>o`<div class="rec-tile"><div class="rec-n ${n.cls}">${n.n.toLocaleString()}</div><div class="rec-l"><span class="rec-sw sw-${n.sw}"></span>${n.label}</div></div>`)}`}function _l(e){let t=e.recipients,n=[`${e.delivered.toLocaleString()} delivered`];e.bounced&&n.push(`${e.bounced.toLocaleString()} bounced`),e.complained&&n.push(`${e.complained.toLocaleString()} complained`),e.unsent&&n.push(`${e.unsent.toLocaleString()} unsent`),e.accepted&&n.push(`${e.accepted.toLocaleString()} accepted, awaiting a delivery receipt`),e.skipped&&n.push(`${e.skipped.toLocaleString()} skipped (unsubscribed or suppressed before hand-off, never mailed)`),e.pending&&n.push(`${e.pending.toLocaleString()} pending`),e.dispatched&&n.push(`${e.dispatched.toLocaleString()} in flight`);let s=`All ${t.toLocaleString()} accounted for: ${n.join(", ")}.`,i=e.bounced||e.complained?" Hard bounces are suppressed automatically and spam complaints unsubscribe the reader, so those addresses won\u2019t be mailed again.":"",r=e.bounced?" A soft bounce is temporary, so it\u2019s counted and the address stays on the list.":"";return`${s}${i}${r}`}function Tp(e,t){return Object.keys(e).every(n=>e[n]===t[n])}var Lp=[{v:"all",label:"All"},{v:"delivered",label:"Delivered"},{v:"failures",label:"Failures"}];function Rp(e){return e.search?"No recipients match that address.":e.view==="failures"?"No delivery failures. Every recipient was accepted or delivered.":e.view==="delivered"?"No delivery receipts confirmed yet.":"No recipients on this send."}function Cp(e){let t=new URLSearchParams;t.set("view",e.view);let n=(e.search||"").trim();return n&&t.set("search",n),e.sort&&(t.set("sort",e.sort),t.set("dir",e.dir??"asc")),t.set("limit",String(e.limit)),t.set("offset",String(e.offset)),t.toString()}function Ap(e,t){let n=e.map(s=>{let i=Ks(s),r=o`<span class="rec-out"><span class="rec-sw sw-${i.sw}"></span>${i.label}</span>`,a=s.error||s.event_detail||"",l=s.event_at?j(s.event_at):"",d=Sl(s.email)?o`<span class="muted">Erased recipient</span>`:s.email;return o`<tr><td class="rec-email">${d}</td><td>${r}</td><td class="muted">${a||"\u2014"}</td><td class="muted">${l||"\u2014"}</td></tr>`});return o`<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>${Ee("Recipient","email",t)}<th class="c-out">Outcome</th><th>Detail</th><th class="c-date">When</th></tr></thead><tbody>${n}</tbody></table></div>`}function Hp(e,t,n,s,i){let{send:r,outcomes:a}=t;if(r.status==="canceled"){Mp(r,n);return}let l=a.recipients,d=r.completed_at??r.fire_at;h(n,o`
252
+ </div>`}function Nl(e,t,n,s){let i=e.querySelector("#resolveBtn");if(i){let r=fe(t,"wedged")?.count??t.counts.dispatched;i.onclick=()=>Vs({id:t.id,c_in_flight:r},n,s)}}function Ol(e,t){let n=e.querySelector("#watchPill");n&&f(n,Bn(t.phase));let s=e.querySelector("#watchMeta");s&&f(s,Vl(t));let i=e.querySelector("#watchBy");i&&f(i,o`${eo(t)}`);let r=e.querySelector("#watchBody");r&&f(r,ql(t))}function Dp(e,t,n,s){f(t,Ml(e.send)),Nl(t,e.send,s,n);let i=e.send;gt(1e3,()=>Ol(t,i),n),wn(e,{update:r=>{if(r.status!=="sending"){s();return}i=r,Je(r,"resolve")!==!!t.querySelector("#resolveBtn")?(f(t,Ml(r)),Nl(t,r,s,n)):Ol(t,r)},removed:s,stale:s},n)}function Dl(e){let t=[{n:e.recipients,label:"Recipients",cls:"",sw:"neutral"},{n:e.delivered,label:"Delivered",cls:"ok",sw:"ok"},{n:e.bounced,label:"Bounced",cls:"warn",sw:"warn"},{n:e.complained,label:"Complained",cls:"danger",sw:"danger"},{n:e.unsent,label:"Unsent",cls:"",sw:"neutral"}];return o`${t.map(n=>o`<div class="rec-tile"><div class="rec-n ${n.cls}">${n.n.toLocaleString()}</div><div class="rec-l"><span class="rec-sw sw-${n.sw}"></span>${n.label}</div></div>`)}`}function Bl(e){let t=e.recipients,n=[`${e.delivered.toLocaleString()} delivered`];e.bounced&&n.push(`${e.bounced.toLocaleString()} bounced`),e.complained&&n.push(`${e.complained.toLocaleString()} complained`),e.unsent&&n.push(`${e.unsent.toLocaleString()} unsent`),e.accepted&&n.push(`${e.accepted.toLocaleString()} accepted, awaiting a delivery receipt`),e.skipped&&n.push(`${e.skipped.toLocaleString()} skipped (unsubscribed or suppressed before hand-off, never mailed)`),e.pending&&n.push(`${e.pending.toLocaleString()} pending`),e.dispatched&&n.push(`${e.dispatched.toLocaleString()} in flight`);let s=`All ${t.toLocaleString()} accounted for: ${n.join(", ")}.`,i=e.bounced||e.complained?" Hard bounces are suppressed automatically and spam complaints unsubscribe the reader, so those addresses won\u2019t be mailed again.":"",r=e.bounced?" A soft bounce is temporary, so it\u2019s counted and the address stays on the list.":"";return`${s}${i}${r}`}function Bp(e,t){return Object.keys(e).every(n=>e[n]===t[n])}var Up=[{v:"all",label:"All"},{v:"delivered",label:"Delivered"},{v:"failures",label:"Failures"}];function Fp(e){return e.search?"No recipients match that address.":e.view==="failures"?"No delivery failures. Every recipient was accepted or delivered.":e.view==="delivered"?"No delivery receipts confirmed yet.":"No recipients on this send."}function qp(e){let t=new URLSearchParams;t.set("view",e.view);let n=(e.search||"").trim();return n&&t.set("search",n),e.sort&&(t.set("sort",e.sort),t.set("dir",e.dir??"asc")),t.set("limit",String(e.limit)),t.set("offset",String(e.offset)),t.toString()}function jp(e,t){let n=e.map(s=>{let i=Xs(s),r=o`<span class="rec-out"><span class="rec-sw sw-${i.sw}"></span>${i.label}</span>`,a=s.error||s.event_detail||"",l=s.event_at?W(s.event_at):"",d=Il(s.email)?o`<span class="muted">Erased recipient</span>`:s.email;return o`<tr><td class="rec-email">${d}</td><td>${r}</td><td class="muted">${a||"\u2014"}</td><td class="muted">${l||"\u2014"}</td></tr>`});return o`<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>${xe("Recipient","email",t)}<th class="c-out">Outcome</th><th>Detail</th><th class="c-date">When</th></tr></thead><tbody>${n}</tbody></table></div>`}function Vp(e,t,n,s,i){let{send:r,outcomes:a}=t;if(r.status==="canceled"){Kp(r,n);return}let l=a.recipients,d=r.completed_at??r.fire_at;f(n,o`
252
253
  <div class="editor-head">
253
254
  <a href="#/sent" class="back">← Sent</a>
254
255
  </div>
255
256
  <div class="rec-head rec-head-page">
256
- <div class="watch-title"><h1>${r.subject||o`<em>untitled</em>`}</h1><span id="recPill">${In(r.phase)}</span></div>
257
- <div class="rec-meta">Sent ${j(d)} · ${l.toLocaleString()} recipients</div>
258
- ${Gs(r)}
257
+ <div class="watch-title"><h1>${r.subject||o`<em>untitled</em>`}</h1><span id="recPill">${Bn(r.phase)}</span></div>
258
+ <div class="rec-meta">Sent ${W(d)} · ${at(l)}</div>
259
+ ${eo(r)}
259
260
  </div>
260
261
  <section class="card rec-post" aria-labelledby="recPostTitle">
261
262
  <h2 class="rec-part-title" id="recPostTitle">The post</h2>
262
- <div class="rec-web" id="recWeb">${Tl(r)}</div>
263
+ <div class="rec-web" id="recWeb">${Ul(r)}</div>
263
264
  </section>
264
265
  <section class="card rec-card" aria-labelledby="recDeliveryTitle">
265
266
  <h2 class="rec-part-title" id="recDeliveryTitle">Delivery</h2>
266
- <div id="recAlert">${Ll(r)}</div>
267
+ <div id="recAlert">${Fl(r)}</div>
267
268
  <p class="rec-tiles-cap muted">Delivery outcomes keep updating as receipts arrive; the audience at fire and the published post are fixed.</p>
268
- <div class="rec-tiles">${kl(a)}</div>
269
- <p class="rec-recon muted">${_l(a)}</p>
269
+ <div class="rec-tiles">${Dl(a)}</div>
270
+ <p class="rec-recon muted">${Bl(a)}</p>
270
271
  <div class="rec-actions">
271
272
  <button type="button" class="ghost" id="csvBtn">Export recipients (CSV)</button>
272
273
  </div>
@@ -274,7 +275,7 @@
274
275
  <div class="rec-people-head">
275
276
  <h3 class="rec-people-title">Recipients</h3>
276
277
  <div class="rec-views" role="group" aria-label="Which recipients to show">
277
- ${Lp.map((b,E)=>o`<button type="button" class="rec-view-btn" data-view="${b.v}" aria-pressed="${E===0?"true":"false"}">${b.label}</button>`)}
278
+ ${Up.map((S,T)=>o`<button type="button" class="rec-view-btn" data-view="${S.v}" aria-pressed="${T===0?"true":"false"}">${S.label}</button>`)}
278
279
  </div>
279
280
  </div>
280
281
  <input class="rec-people-search" type="search" placeholder="Search email…" aria-label="Search recipients by email" autocomplete="off">
@@ -282,45 +283,45 @@
282
283
  <div id="recPager"></div>
283
284
  </div>
284
285
  <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>
285
- </section>`);let u=c("#recWeb",n),m=r,v=()=>{h(u,Tl(m));let b=u.querySelector("#recOpen"),E=u.querySelector("#recCopy"),C=u.querySelector("#recListing"),x=Hl(m);b&&x&&(b.onclick=()=>window.open(x,"_blank","noopener")),E&&x&&(E.onclick=()=>{Tn(E,x)}),C&&(C.onclick=()=>m.email_only?S():p())},f=(b,E,C)=>Z(b,E.email_only?"Updating\u2026":"Adding\u2026",async()=>{C.clearError();try{let x=await L(`/api/posts/${r.post_id}/settings`,{method:"PUT",json:E});C.close();let W=x.post.archive_live?x.post.archive_url:null;m={...m,email_only:x.post.email_only,links:{...m.links,archive:W}},v(),U(x.post.email_only?"Removed from your archive":`Added to your archive at ${W?Rn(W):x.post.slug}`),Fe()}catch(x){C.error(Mn(x))}}),p=()=>{let b=m.links.archive,E=m.web_link==="archive",C=fe(o`<h3 id="eoHead">Remove this post from your archive?</h3>
286
- ${E?o`<p class="hint">It comes off your home page and your public archive. Its emails link to its archive address, so that address keeps working for anyone who has them, and isn’t listed or indexed.</p>`:o`<p class="hint">It comes off your home page and your public archive, and its archive address stops working, including links people have shared. The link in the email keeps working for anyone who has it.</p>
287
- ${b?o`<div class="url-gone"><s>${Rn(b)}</s></div>`:null}
286
+ </section>`);let u=c("#recWeb",n),p=r,g=()=>{f(u,Ul(p));let S=u.querySelector("#recOpen"),T=u.querySelector("#recCopy"),H=u.querySelector("#recListing"),$=Wl(p);S&&$&&(S.onclick=()=>window.open($,"_blank","noopener")),T&&$&&(T.onclick=()=>{An(T,$)}),H&&(H.onclick=()=>p.email_only?y():m())},h=(S,T,H)=>J(S,T.email_only?"Updating\u2026":"Adding\u2026",async()=>{H.clearError();try{let $=await C(`/api/posts/${r.post_id}/settings`,{method:"PUT",json:T});H.close();let L=$.post.archive_live?$.post.archive_url:null;p={...p,email_only:$.post.email_only,links:{...p.links,archive:L}},g(),q($.post.email_only?"Removed from your archive":`Added to your archive at ${L?Pn(L):$.post.slug}`),Ue()}catch($){H.error(Dn($))}}),m=()=>{let S=p.links.archive,T=p.web_link==="archive",H=pe(o`<h3 id="eoHead">Remove this post from your archive?</h3>
287
+ ${T?o`<p class="hint">It comes off your home page and your public archive. Its emails link to its archive address, so that address keeps working for anyone who has them, and isn’t listed or indexed.</p>`:o`<p class="hint">It comes off your home page and your public archive, and its archive address stops working, including links people have shared. The link in the email keeps working for anyone who has it.</p>
288
+ ${S?o`<div class="url-gone"><s>${Pn(S)}</s></div>`:null}
288
289
  <p class="pub-note">The address stays this post’s. Add it back and the same address, and the links people shared, work again.</p>`}
289
- <div class="actions"><button type="button" id="eoCancel">Cancel</button><button type="button" class="primary" id="eoGo">Remove from archive</button></div>`,s);c(".modal",C.el).setAttribute("aria-labelledby","eoHead"),c("#eoCancel",C.el).onclick=C.close;let x=c("#eoGo",C.el);x.onclick=()=>f(x,{email_only:!0},C),x.focus()},S=async()=>{let b;try{b=await L(`/api/posts/${r.post_id}`,{signal:s})}catch(oe){s.aborted||U(Mn(oe));return}let C={...et(b.post,b.subject_slug_held),emailOnly:!1},x=fe(o`<h3 id="addArchHead">Add this post to your archive?</h3>
290
+ <div class="actions"><button type="button" id="eoCancel">Cancel</button><button type="button" class="primary" id="eoGo">Remove from archive</button></div>`,s);c(".modal",H.el).setAttribute("aria-labelledby","eoHead"),c("#eoCancel",H.el).onclick=H.close;let $=c("#eoGo",H.el);$.onclick=()=>h($,{email_only:!0},H),$.focus()},y=async()=>{let S;try{S=await C(`/api/posts/${r.post_id}`,{signal:s})}catch(G){s.aborted||q(Dn(G));return}let H={...Xe(S.post,S.subject_slug_held),emailOnly:!1},$=pe(o`<h3 id="addArchHead">Add this post to your archive?</h3>
290
291
  <p class="hint">It will be listed on your home page and in your public archive, where anyone visiting can find it. The link in the email keeps working, and forwards to it.</p>
291
292
  <div class="dialog-settings" id="addForm"></div>
292
- <div class="actions"><button type="button" id="addArchCancel">Cancel</button><button type="button" class="primary" id="addArchGo">Add to archive</button></div>`,s);c(".modal",x.el).setAttribute("aria-labelledby","addArchHead"),c("#addArchCancel",x.el).onclick=x.close,_t(c("#addForm",x.el),C,"add",{include:!1,fixedNote:"Its address since it was sent: links people shared work again."});let W=c("#addArchGo",x.el);W.onclick=()=>f(W,{email_only:!1},x),W.focus()};v();let $={view:"all",search:"",sort:"email",dir:"asc",offset:0,limit:50},M=c("#recRows",n),P=c("#recPager",n),A=0,H=!1,y=async b=>{let E=++A;b||(H=!0);try{let C=await L(`/api/sends/${e}/deliveries?${Cp($)}`,{signal:s});if(E!==A)return;let x=C.page.total>0?Math.floor((C.page.total-1)/C.page.limit)*C.page.limit:0;if(!C.deliveries.length&&$.offset>x)return $.offset=x,y(b);H=!1,T(()=>{if(!C.deliveries.length){h(M,o`<p class="rec-people-empty muted">${Rp($)}</p>`),h(P,o``);return}h(M,Ap(C.deliveries,$)),it(M,$,w),rt(P,$,C.page,w)})}catch(C){E===A&&(!b||H)&&(H=!1,re(M,Mn(C),w))}},w=()=>y(!1),T=b=>{let E=document.activeElement,C=E instanceof HTMLElement&&(M.contains(E)||P.contains(E)),x=C?E.dataset.sort:void 0,W=C?["pager-prev","pager-next"].find(k=>E.classList.contains(k)):void 0;b();let oe=x?M.querySelector(`.th-sort[data-sort="${x}"]`):W?P.querySelector(`.${W}`):null;oe&&!oe.disabled&&oe.focus()},D=N(".rec-view-btn",n);for(let b of D)b.onclick=()=>{let E=b.dataset.view;(E==="failures"||E==="delivered"||E==="all")&&($.view=E),$.offset=0;for(let C of D)C.setAttribute("aria-pressed",String(C===b));w()};let R=n.querySelector(".rec-people-search");if(R){let b;R.oninput=()=>{clearTimeout(b),b=setTimeout(()=>{$.search=R.value,$.offset=0,w()},250)}}w();let F=c("#csvBtn");F.onclick=()=>Z(F,"Exporting\u2026",async()=>{try{let b=await as(r.links.deliveries_csv);Rt(b.text,b.filename??"deliveries.csv","text/csv")}catch(b){U(b instanceof te&&b.status===404?"This send no longer exists, so there are no deliveries to export.":Mn(b))}});let K=a;gn(t,{update(b){if(b.status==="sending"){i();return}h(c("#recPill",n),In(b.phase)),h(c("#recAlert",n),Ll(b)),(b.email_only!==m.email_only||b.links.archive!==m.links.archive||b.links.unlisted!==m.links.unlisted)&&(m=b,v());let E=Pp(b);Tp(K,E)||(K=E,h(c(".rec-tiles",n),kl(E)),c(".rec-recon",n).textContent=_l(E),y(!0))},removed:i,stale:i},s)}function Hl(e){return e.email_only&&e.web_link==="unlisted"?e.links.unlisted:e.links.archive??e.links.unlisted}function Tl(e){let t=Hl(e),n=t?Rn(t):null,s=e.web_link==="archive",i=e.email_only?s?"Its emails link here. Anyone with the link can open it; nothing lists or indexes it.":"Anyone with the link can open it. Nothing lists or indexes it.":s?"Its emails link here.":"The link in the email forwards here.",r=t&&n?o`<p class="rec-link-label">Link</p><div class="rec-link"><code title="${n}">${n}</code><button type="button" class="copy-btn" id="recCopy" data-copy-word="Copy" aria-label="Copy link">${Te(!1,"Copy")}</button><button type="button" class="rec-open" id="recOpen">Open&nbsp;↗</button></div><p class="rec-link-note muted">${i}</p>`:null,a=e.email_only?o`<div class="rec-arch-what"><strong>${O("mail")}Email only</strong><span class="rec-arch-note">Not on your home page or in your public archive.</span></div><button type="button" class="secondary" id="recListing">Add to archive…</button>`:o`<div class="rec-arch-what"><strong>${O("link")}In your archive</strong><span class="rec-arch-note">Listed on your home page and in your public archive.</span></div><button type="button" class="secondary" id="recListing">Remove from archive…</button>`;return o`${r}<div class="rec-arch">${a}</div>`}function Ll(e){let t=he(e,"no_receipts");return t?o`<div class="health amber" role="status"><span class="health-dot">⚠️</span><div>${t.message}</div></div>`:o``}function Pp(e){let t=e.counts;return{recipients:e.audience.count,delivered:t.delivered,bounced:t.bounced,complained:t.complained,unsent:t.unsent,skipped:t.skipped,accepted:t.accepted,pending:t.pending,dispatched:t.dispatched}}function Mp(e,t){h(t,o`
293
+ <div class="actions"><button type="button" id="addArchCancel">Cancel</button><button type="button" class="primary" id="addArchGo">Add to archive</button></div>`,s);c(".modal",$.el).setAttribute("aria-labelledby","addArchHead"),c("#addArchCancel",$.el).onclick=$.close,Tt(c("#addForm",$.el),H,"add",{include:!1,fixedNote:"Its address since it was sent: links people shared work again."});let L=c("#addArchGo",$.el);L.onclick=()=>h(L,{email_only:!1},$),L.focus()};g();let E={view:"all",search:"",sort:"email",dir:"asc",offset:0,limit:50},R=c("#recRows",n),N=c("#recPager",n),A=0,P=!1,v=async S=>{let T=++A;S||(P=!0);try{let H=await C(`/api/sends/${e}/deliveries?${qp(E)}`,{signal:s});if(T!==A)return;let $=H.page.total>0?Math.floor((H.page.total-1)/H.page.limit)*H.page.limit:0;if(!H.deliveries.length&&E.offset>$)return E.offset=$,v(S);P=!1,I(()=>{if(!H.deliveries.length){f(R,o`<p class="rec-people-empty muted">${Fp(E)}</p>`),f(N,o``);return}f(R,jp(H.deliveries,E)),lt(R,E,w),ct(N,E,H.page,w)})}catch(H){T===A&&(!S||P)&&(P=!1,re(R,Dn(H),w))}},w=()=>v(!1),I=S=>{let T=document.activeElement,H=T instanceof HTMLElement&&(R.contains(T)||N.contains(T)),$=H?T.dataset.sort:void 0,L=H?["pager-prev","pager-next"].find(k=>T.classList.contains(k)):void 0;S();let G=$?R.querySelector(`.th-sort[data-sort="${$}"]`):L?N.querySelector(`.${L}`):null;G&&!G.disabled&&G.focus()},j=D(".rec-view-btn",n);for(let S of j)S.onclick=()=>{let T=S.dataset.view;(T==="failures"||T==="delivered"||T==="all")&&(E.view=T),E.offset=0;for(let H of j)H.setAttribute("aria-pressed",String(H===S));w()};let x=n.querySelector(".rec-people-search");if(x){let S;x.oninput=()=>{clearTimeout(S),S=setTimeout(()=>{E.search=x.value,E.offset=0,w()},250)}}w();let O=c("#csvBtn");O.onclick=()=>J(O,"Exporting\u2026",async()=>{try{let S=await us(r.links.deliveries_csv);Ct(S.text,S.filename??"deliveries.csv","text/csv")}catch(S){q(S instanceof se&&S.status===404?"This send no longer exists, so there are no deliveries to export.":Dn(S))}});let V=a;wn(t,{update(S){if(S.status==="sending"){i();return}f(c("#recPill",n),Bn(S.phase)),f(c("#recAlert",n),Fl(S)),(S.email_only!==p.email_only||S.links.archive!==p.links.archive||S.links.unlisted!==p.links.unlisted)&&(p=S,g());let T=Wp(S);Bp(V,T)||(V=T,f(c(".rec-tiles",n),Dl(T)),c(".rec-recon",n).textContent=Bl(T),v(!0))},removed:i,stale:i},s)}function Wl(e){return e.email_only&&e.web_link==="unlisted"?e.links.unlisted:e.links.archive??e.links.unlisted}function Ul(e){let t=Wl(e),n=t?Pn(t):null,s=e.web_link==="archive",i=e.email_only?s?"Its emails link here. Anyone with the link can open it; nothing lists or indexes it.":"Anyone with the link can open it. Nothing lists or indexes it.":s?"Its emails link here.":"The link in the email forwards here.",r=t&&n?o`<p class="rec-link-label">Link</p><div class="rec-link"><code title="${n}">${n}</code><button type="button" class="copy-btn" id="recCopy" data-copy-word="Copy" aria-label="Copy link">${Te(!1,"Copy")}</button><button type="button" class="rec-open" id="recOpen">Open&nbsp;↗</button></div><p class="rec-link-note muted">${i}</p>`:null,a=e.email_only?o`<div class="rec-arch-what"><strong>${B("mail")}Email only</strong><span class="rec-arch-note">Not on your home page or in your public archive.</span></div><button type="button" class="secondary" id="recListing">Add to archive…</button>`:o`<div class="rec-arch-what"><strong>${B("link")}In your archive</strong><span class="rec-arch-note">Listed on your home page and in your public archive.</span></div><button type="button" class="secondary" id="recListing">Remove from archive…</button>`;return o`${r}<div class="rec-arch">${a}</div>`}function Fl(e){let t=fe(e,"no_receipts");return t?o`<div class="health amber" role="status"><span class="health-dot">⚠️</span><div>${t.message}</div></div>`:o``}function Wp(e){let t=e.counts;return{recipients:e.audience.count,delivered:t.delivered,bounced:t.bounced,complained:t.complained,unsent:t.unsent,skipped:t.skipped,accepted:t.accepted,pending:t.pending,dispatched:t.dispatched}}function Kp(e,t){f(t,o`
293
294
  <div class="editor-head">
294
295
  <a href="#/sent" class="back">← Sent</a>
295
296
  </div>
296
297
  <div class="card rec-card">
297
298
  <div class="rec-head">
298
- <div class="watch-title"><h1>${e.subject||o`<em>untitled</em>`}</h1><span id="recPill">${In(e.phase)}</span></div>
299
- <div class="rec-meta">Canceled ${j(e.completed_at)} · was scheduled for ${j(e.fire_at)}</div>
300
- ${Gs(e)}
299
+ <div class="watch-title"><h1>${e.subject||o`<em>untitled</em>`}</h1><span id="recPill">${Bn(e.phase)}</span></div>
300
+ <div class="rec-meta">Canceled ${W(e.completed_at)} · was scheduled for ${W(e.fire_at)}</div>
301
+ ${eo(e)}
301
302
  </div>
302
303
  <p class="rec-note muted">This send was canceled before it fired, so no one was mailed.</p>
303
- </div>`)}function hi(e){let t=document.getElementById(e);if(!t)throw new Error(`the admin page has no #${e}`);return t}var zs=(()=>{let e=hi("savebar"),t=e.querySelector(".savebar-msg"),n=hi("savebarSave"),s=hi("savebarDiscard"),i=0,r=u=>{t&&h(t,o`<span class="savebar-dot"></span><span>${u}</span>`)},a=()=>{e.hidden=!1,e.offsetHeight,e.classList.add("show")};function l(){i++,e.classList.remove("show","is-error"),e.hidden=!0,n.onclick=null,s.onclick=null,document.body.classList.remove("has-savebar")}function d({onSave:u,onDiscard:m,saveLabel:v="Save changes",discardLabel:f="Discard"},p){if(p.aborted)return{setDirty(){},showError(){}};let S=++i;p.addEventListener("abort",()=>{i===S&&l()},{once:!0}),n.textContent=v,s.textContent=f,e.classList.remove("show","is-error"),r("You have unsaved changes."),e.hidden=!0,document.body.classList.add("has-savebar"),n.onclick=()=>Z(n,"Saving\u2026",()=>Promise.resolve(u())),s.onclick=()=>m();let $=()=>i===S;return{setDirty(M){$()&&(e.classList.remove("is-error"),M?(r("You have unsaved changes."),a()):e.classList.remove("show"))},showError(M){$()&&(e.classList.add("is-error"),r(M),a())}}}return{attach:d}})();var ut=V,Nn=o` hidden`;function we(e,t){return o`<span class="set-chip ${e}">${O(e)}${t}</span>`}function Se(e,t,n=null){return o`<div class="set-sec-head"><h2 class="set-sec-title">${e}</h2>${t}${n}<span class="set-rule"></span></div>`}function On(e,t){return(String(e||t).trim()[0]||"K").toUpperCase()}function Pl(e){let{state:t,fromName:n}=e;return o`
304
+ </div>`)}function Ei(e){let t=document.getElementById(e);if(!t)throw new Error(`the admin page has no #${e}`);return t}var to=(()=>{let e=Ei("savebar"),t=e.querySelector(".savebar-msg"),n=Ei("savebarSave"),s=Ei("savebarDiscard"),i=0,r=u=>{t&&f(t,o`<span class="savebar-dot"></span><span>${u}</span>`)},a=()=>{e.hidden=!1,e.offsetHeight,e.classList.add("show")};function l(){i++,e.classList.remove("show","is-error"),e.hidden=!0,n.onclick=null,s.onclick=null,document.body.classList.remove("has-savebar")}function d({onSave:u,onDiscard:p,saveLabel:g="Save changes",discardLabel:h="Discard"},m){if(m.aborted)return{setDirty(){},showError(){}};let y=++i;m.addEventListener("abort",()=>{i===y&&l()},{once:!0}),n.textContent=g,s.textContent=h,e.classList.remove("show","is-error"),r("You have unsaved changes."),e.hidden=!0,document.body.classList.add("has-savebar"),n.onclick=()=>J(n,"Saving\u2026",()=>Promise.resolve(u())),s.onclick=()=>p();let E=()=>i===y;return{setDirty(R){E()&&(e.classList.remove("is-error"),R?(r("You have unsaved changes."),a()):e.classList.remove("show"))},showError(R){E()&&(e.classList.add("is-error"),r(R),a())}}}return{attach:d}})();var Gp=["wheel","touchstart","pointerdown","keydown"];function Kl(e,t,n){if(e.scrollIntoView({block:"start"}),n.aborted||typeof ResizeObserver>"u")return()=>{};let s=new AbortController,i=()=>s.abort(),r,a=new ResizeObserver(()=>{e.scrollIntoView({block:"start"}),r??=setTimeout(i,5e3)});a.observe(t);for(let l of Gp)window.addEventListener(l,i,{capture:!0,passive:!0,signal:s.signal});return n.addEventListener("abort",i,{signal:s.signal}),s.signal.addEventListener("abort",()=>{a.disconnect(),clearTimeout(r)},{once:!0}),i}var ft=z,Un=o` hidden`;function Se(e,t){return o`<span class="set-chip ${e}">${B(e)}${t}</span>`}function xi(e){return`settings-${e}`}function we(e,t,n=null){return o`<div class="set-sec-head" id="${xi(e)}"><h2 class="set-sec-title">${ia(e)}</h2>${t}${n}<span class="set-rule"></span></div>`}function Fn(e,t){return(String(e||t).trim()[0]||"K").toUpperCase()}function Gl(e){let{state:t,fromName:n}=e;return o`
304
305
  <section class="set-sec">
305
- ${Se("Confirmation email",we("editable","Editable"))}
306
+ ${we("confirmation-email",Se("editable","Editable"))}
306
307
  <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>
307
308
  <div class="set-preview">
308
309
  <div class="set-preview-bar set-ce-bar">
309
310
  <div class="set-ce-modetog" role="group" aria-label="Confirmation email view">
310
- <button type="button" class="set-ce-modebtn" id="ceTabPreview" aria-pressed="true">${O("preview")}Preview</button>
311
- <button type="button" class="set-ce-modebtn" id="ceTabEdit" aria-pressed="false">${O("editable")}Edit</button>
311
+ <button type="button" class="set-ce-modebtn" id="ceTabPreview" aria-pressed="true">${B("preview")}Preview</button>
312
+ <button type="button" class="set-ce-modebtn" id="ceTabEdit" aria-pressed="false">${B("editable")}Edit</button>
312
313
  </div>
313
314
  </div>
314
315
  <div id="cePreviewBody">
315
316
  <div class="set-inbox">
316
- <div class="set-inbox-avatar" id="cePvAvatar">${On(t.name,n)}</div>
317
+ <div class="set-inbox-avatar" id="cePvAvatar">${Fn(t.name,n)}</div>
317
318
  <div class="set-inbox-body">
318
319
  <div class="set-inbox-top"><span class="set-inbox-from" id="cePvFrom">${t.name||n}</span><span class="set-inbox-time">now</span></div>
319
320
  <div class="set-inbox-subj" id="cePvSubject"></div>
320
321
  </div>
321
322
  </div>
322
323
  <div class="set-ce-stage">
323
- <div class="set-ce-card">
324
+ <div class="set-ce-col">
324
325
  <div class="set-ce-mast" id="cePvMast" hidden>
325
326
  <div class="set-ce-mast-logo" id="cePvMastLogo"></div>
326
327
  <div>
@@ -362,75 +363,75 @@
362
363
  </div>
363
364
  </div>
364
365
  </div>
365
- </section>`}function Ml(e,t){let{state:n}=t,s=t.data.settings.confirmation_email_default,i={subject:c("#ceSubject",e),message:c("#ceMessage",e),button:c("#ceButton",e),footer:c("#ceFooter",e)},r={avatar:c("#cePvAvatar",e),from:c("#cePvFrom",e),subject:c("#cePvSubject",e),body:c("#cePvBody",e),button:c("#cePvButton",e),foot:c("#cePvFoot",e),mast:c("#cePvMast",e),mastLogo:c("#cePvMastLogo",e),mastName:c("#cePvMastName",e),mastTag:c("#cePvMastTag",e)},a=()=>{let p=n.confirmation;r.avatar.textContent=On(n.name,t.fromName),r.from.textContent=n.name||t.fromName,r.subject.textContent=p.subject.trim()||s.subject||"",r.body.textContent=p.body.trim()||s.body||"",r.button.textContent=p.button_label.trim()||s.button_label||"Confirm",r.foot.textContent=p.reassurance,r.foot.hidden=!p.reassurance.trim();let S=n.name||t.fromDisplay,$=!!n.logoUrl,M=$||!!S;r.mast.hidden=!M,M&&(r.mastLogo.hidden=!$,$&&h(r.mastLogo,o`<img src="${n.logoUrl}" alt="">`),r.mastName.textContent=S,r.mastName.hidden=!S,r.mastTag.textContent=n.tagline,r.mastTag.hidden=!n.tagline)},l=()=>{n.confirmation.subject=i.subject.value,n.confirmation.body=i.message.value,n.confirmation.button_label=i.button.value,n.confirmation.reassurance=i.footer.value,t.changed()};for(let p of Object.values(i))p.addEventListener("input",l);let d=c("#ceEditBody",e),u=c("#cePreviewBody",e),m=c("#ceTabEdit",e),v=c("#ceTabPreview",e),f=p=>{d.hidden=!p,u.hidden=p,m.setAttribute("aria-pressed",String(p)),v.setAttribute("aria-pressed",String(!p)),p&&i.subject.focus()};return v.onclick=()=>f(!1),m.onclick=()=>f(!0),c("#ceToPreview",e).onclick=()=>f(!1),c("#ceReset",e).onclick=()=>{i.subject.value=s.subject||"",i.message.value=s.body||"",i.button.value=s.button_label||"",i.footer.value=s.reassurance||"",l()},a(),{repaint:a,reset(){i.subject.value=n.confirmation.subject,i.message.value=n.confirmation.body,i.button.value=n.confirmation.button_label,i.footer.value=n.confirmation.reassurance,a()}}}var Ip={none:"No email provider connected",fake:"Fake (dev, dead-end)",ses:"Amazon SES"};function Ys(e){return Ip[e]||e}function Np(e,t){if(e.provider==="none")return o`${Ys(e.provider)}. ${Ae("set-link")} to start sending.`;let n=e.simulation?o`<span class="set-pill">Simulating ${Op[e.simulation.profile]}${e.simulation.faults==="none"?", no failures":""}</span>`:null,s=t?ba(t,j):null;if(t&&t.unknown_reason!=="stand_in"){let i=[s?.pill,n].filter(r=>!!r);return o`${la(t,i.length?o`${i.map(r=>o` ${r}`)}`:null)}${s?.line??null}`}return o`<span>${Ys(e.provider)}${s?.pill?o` ${s.pill}`:null}${n}</span>${s?.line??null}`}var Op={generic:"a generic provider",ses:"Amazon SES"},Dp="#/docs/ses/8-receive-replies-at-your-from-address";function Bp(e){let t=String(e||"").match(/<([^>]+)>/);return t?.[1]?t[1]:String(e||"")}function Il(e){let t=e.data.deployment,{fromName:n}=e,s=e.provider,i=s?da(s):null;return o`
366
+ </section>`}function zl(e,t){let{state:n}=t,s=t.data.settings.confirmation_email_default,i={subject:c("#ceSubject",e),message:c("#ceMessage",e),button:c("#ceButton",e),footer:c("#ceFooter",e)},r={avatar:c("#cePvAvatar",e),from:c("#cePvFrom",e),subject:c("#cePvSubject",e),body:c("#cePvBody",e),button:c("#cePvButton",e),foot:c("#cePvFoot",e),mast:c("#cePvMast",e),mastLogo:c("#cePvMastLogo",e),mastName:c("#cePvMastName",e),mastTag:c("#cePvMastTag",e)},a=()=>{let m=n.confirmation;r.avatar.textContent=Fn(n.name,t.fromName),r.from.textContent=n.name||t.fromName,r.subject.textContent=m.subject.trim()||s.subject||"",r.body.textContent=m.body.trim()||s.body||"",r.button.textContent=m.button_label.trim()||s.button_label||"Confirm",r.foot.textContent=m.reassurance,r.foot.hidden=!m.reassurance.trim();let y=n.name||t.fromDisplay,E=!!n.logoUrl,R=E||!!y;r.mast.hidden=!R,R&&(r.mastLogo.hidden=!E,E&&f(r.mastLogo,o`<img src="${n.logoUrl}" alt="">`),r.mastName.textContent=y,r.mastName.hidden=!y,r.mastTag.textContent=n.tagline,r.mastTag.hidden=!n.tagline)},l=()=>{n.confirmation.subject=i.subject.value,n.confirmation.body=i.message.value,n.confirmation.button_label=i.button.value,n.confirmation.reassurance=i.footer.value,t.changed()};for(let m of Object.values(i))m.addEventListener("input",l);let d=c("#ceEditBody",e),u=c("#cePreviewBody",e),p=c("#ceTabEdit",e),g=c("#ceTabPreview",e),h=m=>{d.hidden=!m,u.hidden=m,p.setAttribute("aria-pressed",String(m)),g.setAttribute("aria-pressed",String(!m)),m&&i.subject.focus()};return g.onclick=()=>h(!1),p.onclick=()=>h(!0),c("#ceToPreview",e).onclick=()=>h(!1),c("#ceReset",e).onclick=()=>{i.subject.value=s.subject||"",i.message.value=s.body||"",i.button.value=s.button_label||"",i.footer.value=s.reassurance||"",l()},a(),{repaint:a,reset(){i.subject.value=n.confirmation.subject,i.message.value=n.confirmation.body,i.button.value=n.confirmation.button_label,i.footer.value=n.confirmation.reassurance,a()}}}var zp={none:"No email provider connected",fake:"Fake (dev, dead-end)",ses:"Amazon SES"};function no(e){return zp[e]||e}function Yp(e,t){if(e.provider==="none")return o`${no(e.provider)}. ${He("set-link")} to start sending.`;let n=e.simulation?o`<span class="set-pill">Simulating ${Zp[e.simulation.profile]}${e.simulation.faults==="none"?", no failures":""}</span>`:null,s=t?La(t,W):null;if(t&&t.unknown_reason!=="stand_in"){let i=[s?.pill,n].filter(r=>!!r);return o`${Sa(t,i.length?o`${i.map(r=>o` ${r}`)}`:null)}${s?.line??null}`}return o`<span>${no(e.provider)}${s?.pill?o` ${s.pill}`:null}${n}</span>${s?.line??null}`}var Zp={generic:"a generic provider",ses:"Amazon SES"},Jp="#/docs/ses/8-receive-replies-at-your-from-address";function Qp(e){let t=String(e||"").match(/<([^>]+)>/);return t?.[1]?t[1]:String(e||"")}function Yl(e){let t=e.data.deployment,{fromName:n}=e,s=e.provider,i=s?$a(s):null;return o`
366
367
  <section class="set-sec">
367
- ${Se("Email sender",we("readonly","Read-only \xB7 set at deploy"))}
368
+ ${we("email-sender",Se("readonly","Read-only \xB7 set at deploy"))}
368
369
  <div class="set-card sunken">
369
- <div class="set-ro-note">${O("readonly")}<span>The sender is set at deploy in <code>wrangler.jsonc</code>, with the AWS keys stored as secrets, so it can’t be edited here. Change it in the <a class="set-link" href="#/docs/self-hosting">setup guide</a>, then redeploy. Credentials are never shown.</span></div>
370
+ <div class="set-ro-note">${B("readonly")}<span>The sender is set at deploy in <code>wrangler.jsonc</code>, with the AWS keys stored as secrets, so it can’t be edited here. Change it in the <a class="set-link" href="#/docs/self-hosting">setup guide</a>, then redeploy. Credentials are never shown.</span></div>
370
371
  <div class="set-preview-bar" style="background:transparent">
371
372
  <span class="set-preview-lbl">Inbox preview</span>
372
373
  <span class="set-preview-dot">How readers see the sender</span>
373
374
  </div>
374
375
  <div class="set-inbox">
375
- <div class="set-inbox-avatar">${On(n,n)}</div>
376
+ <div class="set-inbox-avatar">${Fn(n,n)}</div>
376
377
  <div class="set-inbox-body">
377
378
  <div class="set-inbox-top"><span class="set-inbox-from">${n}</span><span class="set-inbox-time">9:02 AM</span></div>
378
379
  <div class="set-inbox-subj">Your latest post: a sample subject line</div>
379
380
  <div class="set-inbox-snip">The opening lines of your post show here as the inbox preview…</div>
380
- <div class="set-inbox-addr">${Bp(t.from_address)}</div>
381
+ <div class="set-inbox-addr">${Qp(t.from_address)}</div>
381
382
  </div>
382
383
  </div>
383
384
  <div class="set-kv" style="border-top:1px solid var(--line)">
384
- <div class="set-kv-k">From address</div><div class="set-kv-v set-kv-stack"><span class="mono">${t.from_address}</span><span class="muted" id="replyNote">Readers’ replies go to this address. <a class="set-link" href="${Dp}">Forward it to your inbox</a>.</span></div>
385
+ <div class="set-kv-k">From address</div><div class="set-kv-v set-kv-stack"><span class="mono">${t.from_address}</span><span class="muted" id="replyNote">Readers’ replies go to this address. <a class="set-link" href="${Jp}">Forward it to your inbox</a>.</span></div>
385
386
  <div class="set-kv-k">Sending domain</div><div class="set-kv-v"><span class="mono">${t.sending_domain}</span></div>
386
- <div class="set-kv-k">Email provider</div><div class="set-kv-v set-kv-stack" id="providerValue">${Np(t,s)}${s?.warnings.map(r=>o`<span class="${xn(r)?"set-error":"set-warn"}">${Ps(r)}</span>`)}</div>
387
+ <div class="set-kv-k">Email provider</div><div class="set-kv-v set-kv-stack" id="providerValue">${Yp(t,s)}${s?.warnings.map(r=>o`<span class="${Tn(r)?"set-error":"set-warn"}">${Bs(r)}</span>`)}</div>
387
388
  ${i?o`<div class="set-kv-k">Send rate</div><div class="set-kv-v" id="sendRate">${i}</div>`:null}
388
389
  </div>
389
390
  </div>
390
- </section>`}function Nl(e){let t=e.data.deployment,n=`${t.archive_origin||""}${t.archive_base_path||""}`,s=t.archive_origin===t.app_origin;return o`
391
+ </section>`}function Zl(e){let t=e.data.deployment,n=`${t.archive_origin||""}${t.archive_base_path||""}`,s=t.archive_origin===t.app_origin;return o`
391
392
  <section class="set-sec">
392
- ${Se("Instance",we("readonly","Read-only \xB7 set at deploy"))}
393
+ ${we("instance",Se("readonly","Read-only \xB7 set at deploy"))}
393
394
  <div class="set-card sunken">
394
- <div class="set-ro-note">${O("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>
395
+ <div class="set-ro-note">${B("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>
395
396
  <div class="set-kv">
396
397
  <div class="set-kv-k">App origin</div><div class="set-kv-v"><span class="mono">${t.app_origin}</span></div>
397
398
  <div class="set-kv-k">Archive URL base</div><div class="set-kv-v"><span class="mono">${n}</span>${s?o`<span class="set-pill">default: app origin</span>`:null}</div>
398
399
  <div class="set-kv-k">Image URL base</div><div class="set-kv-v"><span class="mono">${t.media_public_base}</span></div>
399
400
  <div class="set-kv-k">Auth mode</div><div class="set-kv-v">${t.auth_mode==="access"?"Cloudflare Access":"Local dev token"}</div>
400
- <div class="set-kv-k">Cloudflare Access</div><div class="set-kv-v">${t.access_configured?o`<span class="set-pill ok">${O("check")}Configured</span>`:o`<span class="set-pill">Not configured</span>`}</div>
401
- <div class="set-kv-k">Minimum lead</div><div class="set-kv-v">${Dt(t.min_lead_ms)}${t.min_lead_ms===cs?o`<span class="set-pill">default</span>`:null}</div>
401
+ <div class="set-kv-k">Cloudflare Access</div><div class="set-kv-v">${t.access_configured?o`<span class="set-pill ok">${B("check")}Configured</span>`:o`<span class="set-pill">Not configured</span>`}</div>
402
+ <div class="set-kv-k">Minimum lead</div><div class="set-kv-v">${Ft(t.min_lead_ms)}${t.min_lead_ms===ms?o`<span class="set-pill">default</span>`:null}</div>
402
403
  </div>
403
404
  </div>
404
- </section>`}function Zs(e){return o`
405
+ </section>`}function so(e){return o`
405
406
  <div class="set-chips" id="${e.chips}"></div>
406
407
  <div class="set-recip-add">
407
408
  <input type="email" id="${e.input}" placeholder="you@example.com" autocomplete="off">
408
409
  <button type="button" id="${e.add}">Add inbox</button>
409
- </div>`}function Js(e,t,n){let s=c(`#${n.ids.chips}`,e),i=c(`#${n.ids.input}`,e),r=()=>{let l=n.list();if(!l.length){h(s,o`<span class="set-recip-empty">${n.empty}</span>`);return}h(s,o`${l.map((d,u)=>o`<span class="set-recip-chip">${d}<button type="button" data-rm="${u}" aria-label="Remove ${d}">${O("x")}</button></span>`)}`)};s.addEventListener("click",l=>{let d=l.target instanceof Element?l.target.closest("[data-rm]"):null;d&&(n.list().splice(Number(d.dataset.rm),1),r(),t.changed())});let a=()=>{let l=wt(i.value);if(!l)return;let d=n.list(),u=St(l)?d.includes(l)?"That inbox is already in the list.":n.max&&d.length>=n.max.count?n.max.full:"":"That doesn\u2019t look like an email address.";if(u){qe(i,u),i.focus();return}d.push(l),i.value="",r(),t.changed()};return c(`#${n.ids.add}`,e).onclick=a,i.addEventListener("keydown",l=>{l.key==="Enter"&&(l.preventDefault(),a())}),r(),{reset:r}}function Up(e){return e instanceof te&&e.status===409&&e.data?.error==="remake_required"?e.data:null}var Fp="Not saved: the change needed your confirmation";async function Ct(e,t,n){let s=null;for(;;)try{return await e(s)}catch(i){let r=Up(i);if(!r)throw i;let a=r.sends||[];if(!await qp(a,t,n))return n.aborted&&U(Fp),null;s=a.map(d=>d.id)}}function qp(e,t,n){let s=e.length,i=s===1,r=`${s} scheduled email${i?"":"s"}`;return new Promise(a=>{let l=fe(o`<h3>Apply this change to ${r}?</h3>
410
+ </div>`}function oo(e,t,n){let s=c(`#${n.ids.chips}`,e),i=c(`#${n.ids.input}`,e),r=()=>{let l=n.list();if(!l.length){f(s,o`<span class="set-recip-empty">${n.empty}</span>`);return}f(s,o`${l.map((d,u)=>o`<span class="set-recip-chip">${d}<button type="button" data-rm="${u}" aria-label="Remove ${d}">${B("x")}</button></span>`)}`)};s.addEventListener("click",l=>{let d=l.target instanceof Element?l.target.closest("[data-rm]"):null;d&&(n.list().splice(Number(d.dataset.rm),1),r(),t.changed())});let a=()=>{let l=wt(i.value);if(!l)return;let d=n.list(),u=$t(l)?d.includes(l)?"That inbox is already in the list.":n.max&&d.length>=n.max.count?n.max.full:"":"That doesn\u2019t look like an email address.";if(u){Fe(i,u),i.focus();return}d.push(l),i.value="",r(),t.changed()};return c(`#${n.ids.add}`,e).onclick=a,i.addEventListener("keydown",l=>{l.key==="Enter"&&(l.preventDefault(),a())}),r(),{reset:r}}function Xp(e){return e instanceof se&&e.status===409&&e.data?.error==="remake_required"?e.data:null}var em="Not saved: the change needed your confirmation";async function At(e,t,n){let s=null;for(;;)try{return await e(s)}catch(i){let r=Xp(i);if(!r)throw i;let a=r.sends||[];if(!await tm(a,t,n))return n.aborted&&q(em),null;s=a.map(d=>d.id)}}function tm(e,t,n){let s=e.length,i=s===1,r=`${s} scheduled email${i?"":"s"}`;return new Promise(a=>{let l=pe(o`<h3>Apply this change to ${r}?</h3>
410
411
  <p class="hint">${t.lead(s)} ${t.gerund} applies the change to ${i?"its email":"their emails"} too.</p>
411
- <ul class="remake-list">${e.map(p=>o`<li><span>${p.subject||o`<em>untitled</em>`}</span><span>sends ${j(p.fire_at)}</span></li>`)}</ul>
412
+ <ul class="remake-list">${e.map(m=>o`<li><span>${m.subject||o`<em>untitled</em>`}</span><span>sends ${W(m.fire_at)}</span></li>`)}</ul>
412
413
  <dl class="remake-facts">
413
414
  <div><dt>Changes</dt><dd>the look of ${i?"its email":"their emails"}</dd></div>
414
415
  <div><dt>Stays</dt><dd>${i?"its content and fire time":"their content and fire times"}</dd></div>
415
416
  <div><dt>Next</dt><dd>send yourself a fresh test${i?"":" of each"}</dd></div>
416
417
  </dl>
417
418
  <p class="hint">To leave scheduled emails as they are while future posts change, you’d need a second template, which Kestrel doesn’t offer yet.</p>
418
- <div class="actions"><button type="button" id="rmCancel">Cancel</button><button type="button" class="primary" id="rmGo">${t.action} and apply</button></div>`,n);if(!l.el.isConnected){a(!1);return}let d=!1,u=p=>{d||(d=!0,a(p))},m=l.el.querySelector("#rmCancel"),v=l.el.querySelector("#rmGo");m&&(m.onclick=()=>{l.close(),u(!1)}),v&&(v.onclick=()=>{l.close(),u(!0)});let f=new MutationObserver(()=>{l.el.isConnected||(f.disconnect(),u(!1))});f.observe(document.body,{childList:!0}),v?.focus()})}var Ol={action:"Save",gerund:"Saving",lead:e=>`This template is used by ${e} post${e===1?" that\u2019s":"s that are"} already scheduled.`},Dn=(e,t)=>({action:e,gerund:t,lead:n=>`Your name, tagline, address, and logo ride inside every email, including ${n} post${n===1?" that\u2019s":"s that are"} already scheduled.`});function Qs(e){let t=e?.sends||[];if(!t.length)return o`<span class="set-chip">${O("info")}No posts scheduled</span>`;let n=t.length;return o`<span class="set-chip inuse"><span class="set-chip-dot"></span>In use by ${n} scheduled post${n===1?"":"s"}</span>`}function jp(e,t=Date.now()){let n=e?.retry_after;if(!e||!n||t>=n)return"";let s=Math.max(1,e.sends.filter(i=>i.fire_at<=n).length);return s===1?`A scheduled post sends at ${j(n)}. You can save once it has sent.`:`${s} scheduled posts send by ${j(n)}. You can save once they have sent.`}function nn(e){let t=jp(e);return t?o`<span data-remake-wait> ${t}</span>`:null}function Xs(e,t,n){let s=t?.retry_after;s&&gt(s,()=>{for(let i of e.querySelectorAll("[data-remake-wait]"))i.remove()},n)}function sn(e,t){let n=(t||[]).length;return n?`${e} and applied to ${n} scheduled email${n===1?"":"s"}. Send yourself a test of each.`:e}var Dl=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");function Bl(e,t){return e?`<img class="logo" src="${Dl(e)}" alt="${Dl(t)}" width="44" height="44" />`:""}var Ul="You\u2019ve added a mailing address in Settings, but this template doesn\u2019t include the {{ .Publication.Address }} field. If you send promotional email, add it to the footer. If you\u2019ve written your address into the template yourself, or don\u2019t need to provide one, you can ignore this or remove the address in Settings.";function Fl(e,t){return e.length===1&&t.length===1?t[0]?.error??"":t.map(n=>`${n.email}: ${n.error}`).join("; ")}function Wp(e,t){let n=Math.min(e,t);return{x:Math.floor((e-n)/2),y:Math.floor((t-n)/2),side:n}}var bi=class extends Error{constructor(){super("Couldn\u2019t read that image. Pick a PNG, JPEG, WebP, or GIF.")}};async function ql(e,t){let n=await createImageBitmap(e,{imageOrientation:"from-image"}).catch(()=>{throw new bi});try{let{x:s,y:i,side:r}=Wp(n.width,n.height),a=document.createElement("canvas");a.width=t,a.height=t;let l=a.getContext("2d");if(!l)throw new Error("This browser can\u2019t prepare the image.");return l.imageSmoothingQuality="high",l.drawImage(n,s,i,r,r,0,0,t,t),{png:await new Promise((u,m)=>a.toBlob(v=>v?u(v):m(new Error("This browser can\u2019t prepare the image.")),"image/png")),width:n.width,height:n.height}}finally{n.close()}}function Vp(e){if(!(e.identity_fields||[]).length)return o`Your email template doesn’t show any of these.`;let t="Shown in every email.",n=e.sends.length;if(!n)return o`${t}`;let s=`Saving a change updates ${n} scheduled email${n===1?"":"s"} too; you\u2019ll confirm first.`;return o`${t} ${s}${nn(e)}`}var Kp="#/docs/name-your-publication/when-to-provide-a-mailing-address";function Bn(){return o`<a class="set-link" href="${Kp}" target="_blank" rel="noopener">When to provide a mailing address <span aria-hidden="true">↗</span><span class="visually-hidden"> (opens in a new tab)</span></a>`}var gi=256;function jl(e){let{state:t,fromDisplay:n,fromName:s}=e,i=!!t.address;return o`
419
+ <div class="actions"><button type="button" id="rmCancel">Cancel</button><button type="button" class="primary" id="rmGo">${t.action} and apply</button></div>`,n);if(!l.el.isConnected){a(!1);return}let d=!1,u=m=>{d||(d=!0,a(m))},p=l.el.querySelector("#rmCancel"),g=l.el.querySelector("#rmGo");p&&(p.onclick=()=>{l.close(),u(!1)}),g&&(g.onclick=()=>{l.close(),u(!0)});let h=new MutationObserver(()=>{l.el.isConnected||(h.disconnect(),u(!1))});h.observe(document.body,{childList:!0}),g?.focus()})}var Jl={action:"Save",gerund:"Saving",lead:e=>`This template is used by ${e} post${e===1?" that\u2019s":"s that are"} already scheduled.`},qn=(e,t)=>({action:e,gerund:t,lead:n=>`Your name, tagline, address, and logo ride inside every email, including ${n} post${n===1?" that\u2019s":"s that are"} already scheduled.`});function io(e){let t=e?.sends||[];if(!t.length)return o`<span class="set-chip">${B("info")}No posts scheduled</span>`;let n=t.length;return o`<span class="set-chip inuse"><span class="set-chip-dot"></span>In use by ${n} scheduled post${n===1?"":"s"}</span>`}function nm(e,t=Date.now()){let n=e?.retry_after;if(!e||!n||t>=n)return"";let s=Math.max(1,e.sends.filter(i=>i.fire_at<=n).length);return s===1?`A scheduled post sends at ${W(n)}. You can save once it has sent.`:`${s} scheduled posts send by ${W(n)}. You can save once they have sent.`}function rn(e){let t=nm(e);return t?o`<span data-remake-wait> ${t}</span>`:null}function ro(e,t,n){let s=t?.retry_after;s&&vt(s,()=>{for(let i of e.querySelectorAll("[data-remake-wait]"))i.remove()},n)}function an(e,t){let n=(t||[]).length;return n?`${e} and applied to ${n} scheduled email${n===1?"":"s"}. Send yourself a test of each.`:e}var Ql=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");function Xl(e,t){return e?`<img class="logo" src="${Ql(e)}" alt="${Ql(t)}" width="44" height="44" />`:""}function sm(e,t){let n=Math.min(e,t);return{x:Math.floor((e-n)/2),y:Math.floor((t-n)/2),side:n}}var ki=class extends Error{constructor(){super("Couldn\u2019t read that image. Pick a PNG, JPEG, WebP, or GIF.")}};async function ec(e,t){let n=await createImageBitmap(e,{imageOrientation:"from-image"}).catch(()=>{throw new ki});try{let{x:s,y:i,side:r}=sm(n.width,n.height),a=document.createElement("canvas");a.width=t,a.height=t;let l=a.getContext("2d");if(!l)throw new Error("This browser can\u2019t prepare the image.");return l.imageSmoothingQuality="high",l.drawImage(n,s,i,r,r,0,0,t,t),{png:await new Promise((u,p)=>a.toBlob(g=>g?u(g):p(new Error("This browser can\u2019t prepare the image.")),"image/png")),width:n.width,height:n.height}}finally{n.close()}}function om(e){if(!(e.identity_fields||[]).length)return o`Your email template doesn’t show any of these.`;let t="Shown in every email.",n=e.sends.length;if(!n)return o`${t}`;let s=`Saving a change updates ${n} scheduled email${n===1?"":"s"} too; you\u2019ll confirm first.`;return o`${t} ${s}${rn(e)}`}var im="#/docs/name-your-publication/when-to-provide-a-mailing-address";function jn(){return o`<a class="set-link" href="${im}" target="_blank" rel="noopener">When to provide a mailing address <span aria-hidden="true">↗</span><span class="visually-hidden"> (opens in a new tab)</span></a>`}var _i=256;function tc(e){let{state:t,fromDisplay:n,fromName:s}=e,i=!!t.address;return o`
419
420
  <section class="set-sec">
420
- ${Se("Publication identity",we("editable","Editable"))}
421
+ ${we("publication-identity",Se("editable","Editable"))}
421
422
  <div class="set-card">
422
423
  <div class="set-id-grid">
423
424
  <div class="set-logo-slot">
424
425
  <div class="set-logo-tile${t.logoUrl?" has-img":""}" id="logoTile" role="button" tabindex="0" aria-label="Upload logo">
425
- <span class="set-logo-ph" id="logoPh"${t.logoUrl?Nn:null}>${O("upload")}Upload</span>
426
+ <span class="set-logo-ph" id="logoPh"${t.logoUrl?Un:null}>${B("upload")}Upload</span>
426
427
  </div>
427
428
  <input type="file" id="logoInput" accept="image/png,image/jpeg,image/webp,image/gif" hidden>
428
429
  <div class="set-logo-actions">
429
430
  <button type="button" id="logoReplace">${t.logoUrl?"Replace":"Upload"}</button>
430
- <button type="button" class="danger-subtle" id="logoRemove"${t.logoUrl?null:Nn}>Remove</button>
431
+ <button type="button" class="danger-subtle" id="logoRemove"${t.logoUrl?null:Un}>Remove</button>
431
432
  </div>
432
433
  <div class="set-logo-notes">
433
- <p class="field-hint">Shown as a square. Pick an image at least ${String(gi)}&nbsp;×&nbsp;${String(gi)}&nbsp;px; a wider or taller one is cropped to its center square.</p>
434
+ <p class="field-hint">Shown as a square. Pick or drop an image at least ${String(_i)}&nbsp;×&nbsp;${String(_i)}&nbsp;px; a wider or taller one is cropped to its center square.</p>
434
435
  <p class="set-field-warn" id="logoShape" hidden>Your logo isn’t square, so emails show it stretched. Replace it to have it cropped square.</p>
435
436
  <div class="field-error" id="logoError" role="alert" hidden><span class="field-error-ico" aria-hidden="true">!</span><span></span></div>
436
437
  </div>
@@ -446,24 +447,24 @@
446
447
  <input id="setTagline" value="${t.tagline}" placeholder="A one-line description" maxlength="200" autocomplete="off">
447
448
  <p class="field-hint">A short line under the name on your public pages.</p>
448
449
  </div>
449
- <div class="set-field set-addr-add" id="addrAdd"${i?Nn:null}>
450
- <button type="button" class="ghost" id="addrAddBtn" aria-describedby="addrAddHint">${O("plus")}Add a mailing address</button>
451
- <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. ${Bn()}</p>
450
+ <div class="set-field set-addr-add" id="addrAdd"${i?Un:null}>
451
+ <button type="button" class="ghost" id="addrAddBtn" aria-describedby="addrAddHint">${B("plus")}Add a mailing address</button>
452
+ <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. ${jn()}</p>
452
453
  </div>
453
- <div class="set-field" id="addrField"${i?null:Nn}>
454
+ <div class="set-field" id="addrField"${i?null:Un}>
454
455
  <div class="set-field-head">
455
456
  <label for="setAddress">Mailing address</label>
456
457
  <button type="button" class="danger-subtle set-field-remove" id="addrRemove">Remove</button>
457
458
  </div>
458
459
  <input id="setAddress" value="${t.address}" placeholder="PO Box 123, City, ST 00000" maxlength="300" autocomplete="off">
459
- <p class="field-hint">${Bn()}</p>
460
- <p class="set-field-warn" id="addrWarn" hidden>Your email template currently doesn’t include this field. If you send promotional email, add it to the template. ${Bn()}</p>
460
+ <p class="field-hint">${jn()}</p>
461
+ <p class="set-field-warn" id="addrWarn" hidden>Your email template currently doesn’t include this field. If you send promotional email, add it to the template. ${jn()}</p>
461
462
  </div>
462
463
  </div>
463
464
  </div>
464
- <div class="set-note">${O("info")}<span>${Vp(e.data.in_use)}</span></div>
465
+ <div class="set-note">${B("info")}<span>${om(e.data.in_use)}</span></div>
465
466
  </div>
466
- </section>`}function Wl(e,t,n){let{state:s}=t,i=c("#setName",e),r=c("#setTagline",e),a=c("#setAddress",e),l=()=>{s.name=i.value.trim(),s.tagline=r.value.trim(),s.address=a.value.trim(),t.changed()};i.addEventListener("input",l),r.addEventListener("input",l),a.addEventListener("input",l);let d=!!s.address,u=(t.data.in_use.identity_fields||[]).includes("address"),m=c("#addrAdd",e),v=c("#addrField",e),f=c("#addrWarn",e),p=()=>{m.hidden=d,v.hidden=!d,f.hidden=!(d&&s.address&&!u),f.hidden?a.removeAttribute("aria-describedby"):a.setAttribute("aria-describedby","addrWarn")};a.addEventListener("input",p),c("#addrAddBtn",e).onclick=()=>{d=!0,p(),a.focus()},c("#addrRemove",e).onclick=()=>{a.value="",d=!1,l(),p(),c("#addrAddBtn",e).focus()};let S=c("#logoInput",e),$=c("#logoTile",e),M=c("#logoPh",e),P=c("#logoReplace",e),A=c("#logoRemove",e),H=c("#logoShape",e),y=b=>{if(H.hidden=!0,!b)return;let E=new Image;E.onload=()=>{H.hidden=s.logoUrl!==b||E.naturalWidth===E.naturalHeight},E.src=b},w=()=>{let b=!!s.logoUrl;y(s.logoUrl),$.classList.toggle("has-img",b),$.style.backgroundImage=b?`url('${s.logoUrl}')`:"",M.hidden=b,A.hidden=!b,P.textContent=b?"Replace":"Upload"};P.onclick=()=>S.click(),$.addEventListener("click",()=>S.click()),$.addEventListener("keydown",b=>{(b.key==="Enter"||b.key===" ")&&(b.preventDefault(),S.click())});let T=c("#logoError",e),D=c("span:last-of-type",T),R=b=>{D.textContent=b,T.hidden=!b},F=(b,E)=>{s.logoUrl=b.settings.publication.logo_url||"",t.applySaved(b.settings),w(),t.changed(),U(sn(E,b.remade))},K=b=>R(ut(b));return S.onchange=async()=>{let b=S.files?.[0];if(S.value="",!!b){R("");try{let E=await ql(b,gi),C=E.width!==E.height,x=await Ct(W=>{let oe=new FormData;oe.append("file",new File([E.png],"logo.png",{type:"image/png"}));let k=W?`?remake=${encodeURIComponent(W.join(","))}`:"";return L(`/api/settings/logo${k}`,{method:"POST",body:oe})},Dn("Upload","Uploading"),n);x&&F(x,C?"Logo updated, cropped to its center square":"Logo updated")}catch(E){K(E)}}},A.onclick=()=>Z(A,"Removing\u2026",async()=>{R("");try{let b=await Ct(E=>{let C=E?`?remake=${encodeURIComponent(E.join(","))}`:"";return L(`/api/settings/logo${C}`,{method:"DELETE"})},Dn("Remove","Removing"),n);b&&F(b,"Logo removed")}catch(b){K(b)}}),w(),p(),{reset(){i.value=s.name,r.value=s.tagline,a.value=s.address,d=!!s.address,p()}}}var eo=V,Gp=[{group:"Post",vars:[{token:"{{ .Post.Body }}",desc:"Your post\u2019s Markdown, rendered to HTML."},{token:"{{ .Post.Subject }}",desc:"The post\u2019s subject line."}]},{group:"Publication",vars:[{token:"{{ .Publication.Name }}",desc:"Publication name (from Identity, above)."},{token:"{{ .Publication.Tagline }}",desc:"Your tagline."},{token:"{{ .Publication.Logo }}",desc:"Your logo as an image, or nothing when no logo is set."},{token:"{{ .Publication.LogoURL }}",desc:"Absolute URL of your logo, or empty."},{token:"{{ .Publication.Address }}",desc:"Your mailing address, if you\u2019ve added one in Settings; empty otherwise. Keep it in the footer if you send promotional email."}]},{group:"Email",vars:[{token:"{{ .Email.SentTo }}",desc:"The recipient\u2019s address (filled per send)."},{token:"{{ .Email.UnsubscribeURL }}",desc:"Their one-click unsubscribe link."},{token:"{{ .Email.ViewInBrowserURL }}",desc:"The web version\u2019s permanent URL."}]},{group:"Email only",vars:[{token:"{{ if .IsEmail }} \u2026 {{ end }}",desc:"Reports whether this is the email or the web version. Put your unsubscribe link and address inside; the web version leaves them out."}]}],vi={signed:{label:"Signed",description:"Your post, then your logo, name, and tagline as a sign-off. Your mailing address sits in the footer once you add one.",html:`<style>
467
+ </section>`}function nc(e,t,n){let{state:s}=t,i=c("#setName",e),r=c("#setTagline",e),a=c("#setAddress",e),l=()=>{s.name=i.value.trim(),s.tagline=r.value.trim(),s.address=a.value.trim(),t.changed()};i.addEventListener("input",l),r.addEventListener("input",l),a.addEventListener("input",l);let d=!!s.address,u=(t.data.in_use.identity_fields||[]).includes("address"),p=c("#addrAdd",e),g=c("#addrField",e),h=c("#addrWarn",e),m=()=>{p.hidden=d,g.hidden=!d,h.hidden=!(d&&s.address&&!u),h.hidden?a.removeAttribute("aria-describedby"):a.setAttribute("aria-describedby","addrWarn")};a.addEventListener("input",m),c("#addrAddBtn",e).onclick=()=>{d=!0,m(),a.focus()},c("#addrRemove",e).onclick=()=>{a.value="",d=!1,l(),m(),c("#addrAddBtn",e).focus()};let y=c("#logoInput",e),E=c("#logoTile",e),R=c("#logoPh",e),N=c("#logoReplace",e),A=c("#logoRemove",e),P=c("#logoShape",e),v=L=>{if(P.hidden=!0,!L)return;let G=new Image;G.onload=()=>{P.hidden=s.logoUrl!==L||G.naturalWidth===G.naturalHeight},G.src=L},w=()=>{let L=!!s.logoUrl;v(s.logoUrl),E.classList.toggle("has-img",L),E.style.backgroundImage=L?`url('${s.logoUrl}')`:"",R.hidden=L,A.hidden=!L,N.textContent=L?"Replace":"Upload"};N.onclick=()=>y.click(),E.addEventListener("click",()=>y.click()),E.addEventListener("keydown",L=>{(L.key==="Enter"||L.key===" ")&&(L.preventDefault(),y.click())});let I=c("#logoError",e),j=c("span:last-of-type",I),x=L=>{j.textContent=L,I.hidden=!L},O=(L,G)=>{s.logoUrl=L.settings.publication.logo_url||"",t.applySaved(L.settings),w(),t.changed(),q(an(G,L.remade))},V=L=>x(ft(L)),S=!1,T=async L=>{if(S){x("The last logo change is still saving. Try again once it\u2019s done.");return}S=!0,x("");try{await L(),x("")}catch(G){V(G)}finally{S=!1}},H=L=>T(async()=>{let G=await ec(L,_i),k=G.width!==G.height,K=await At(F=>{let ee=new FormData;ee.append("file",new File([G.png],"logo.png",{type:"image/png"}));let Le=F?`?remake=${encodeURIComponent(F.join(","))}`:"";return C(`/api/settings/logo${Le}`,{method:"POST",body:ee})},qn("Upload","Uploading"),n);K&&O(K,k?"Logo updated, cropped to its center square":"Logo updated")});y.onchange=()=>{let L=y.files?.[0];y.value="",L&&H(L)};let $=c(".set-logo-slot",e);return $.addEventListener("dragover",L=>{L.dataTransfer?.types.includes("Files")&&(L.preventDefault(),L.dataTransfer.dropEffect="copy",E.classList.add("dragover"))}),$.addEventListener("dragleave",L=>{L.relatedTarget instanceof Node&&$.contains(L.relatedTarget)||E.classList.remove("dragover")}),$.addEventListener("drop",L=>{L.preventDefault(),E.classList.remove("dragover");let G=L.dataTransfer?.files??[],k=G[0];G.length>1?x("Drop one image at a time."):k?H(k):x("Couldn\u2019t read a file from that drop. Save the image first, then pick it.")}),A.onclick=()=>T(()=>J(A,"Removing\u2026",async()=>{let L=await At(G=>{let k=G?`?remake=${encodeURIComponent(G.join(","))}`:"";return C(`/api/settings/logo${k}`,{method:"DELETE"})},qn("Remove","Removing"),n);L&&O(L,"Logo removed")})),w(),m(),{reset(){i.value=s.name,r.value=s.tagline,a.value=s.address,d=!!s.address,m()}}}var ao=z,rm=[{group:"Post",vars:[{token:"{{ .Post.Body }}",desc:"Your post\u2019s Markdown, rendered to HTML."},{token:"{{ .Post.Subject }}",desc:"The post\u2019s subject line."}]},{group:"Publication",vars:[{token:"{{ .Publication.Name }}",desc:"Publication name (from Identity, above)."},{token:"{{ .Publication.Tagline }}",desc:"Your tagline."},{token:"{{ .Publication.Logo }}",desc:"Your logo as an image, or nothing when no logo is set."},{token:"{{ .Publication.LogoURL }}",desc:"Absolute URL of your logo, or empty."},{token:"{{ .Publication.Address }}",desc:"Your mailing address, if you\u2019ve added one in Settings; empty otherwise. Keep it in the footer if you send promotional email."}]},{group:"Email",vars:[{token:"{{ .Email.SentTo }}",desc:"The recipient\u2019s address (filled per send)."},{token:"{{ .Email.UnsubscribeURL }}",desc:"Their unsubscribe link."},{token:"{{ .Email.ViewInBrowserURL }}",desc:"The web version\u2019s permanent URL."}]},{group:"Email only",vars:[{token:"{{ if .IsEmail }} \u2026 {{ end }}",desc:"Reports whether this is the email or the web version. Put your unsubscribe link and address inside; the web version leaves them out."}]}],Ti={signed:{label:"Signed",description:"Your post, then your logo, name, and tagline as a sign-off. Your mailing address sits in the footer once you add one.",html:`<style>
467
468
  .email {
468
469
  font: 16px/1.6 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
469
470
  color: #18181b;
@@ -618,7 +619,7 @@
618
619
  <a href="{{ .Email.ViewInBrowserURL }}">View in browser</a>
619
620
  <div class="address">{{ .Publication.Address }}</div>{{ end }}
620
621
  </div>
621
- </div>`}},zp=e=>Object.hasOwn(vi,e),Yp=`<div style="position:relative;border:1px dashed #93a7e6;border-radius:8px;padding:20px 14px 8px;margin:0 0 6px"><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><h2 style="margin:0 0 10px">Lorem ipsum,</h2><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></div>`,Zp=new Set([".Post.Body",".Publication.Logo"]);function Jp(e,t){return String(e).replace(/\{\{\s*(?:if\b[^}]*|end)\s*\}\}/g,"").replace(/\{\{\s*([\w.]+)\s*\}\}/g,(s,i)=>Zp.has(i)?t[i]||"":X(t[i]??""))}function Qp(e){let t=e.name||"Your publication";return{".Post.Body":Yp,".Post.Subject":"The starlings are back",".Publication.Name":t,".Publication.Tagline":e.tagline||"Your tagline",".Publication.LogoURL":e.logoUrl,".Publication.Logo":Bl(e.logoUrl,t),".Publication.Address":e.address,".Email.SentTo":"you@example.com",".Email.UnsubscribeURL":"#unsubscribe",".Email.ViewInBrowserURL":"#view-in-browser"}}function Xp(e,t){return Jp(e,Qp(t))}var em='<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>html,body{margin:0}body{background:#fff}@media (prefers-color-scheme:dark){body{background:#18181b}}.kestrel-email{max-width:640px;margin:0 auto;padding:26px 20px;box-sizing:border-box}.kestrel-email img{max-width:100%}</style></head><body><div class="kestrel-email"></div></body></html>';function yi(e,t,n){e.setAttribute("sandbox",Qt);let s=!1,i=()=>{try{let a=e.contentDocument?.body?.scrollHeight;a&&(e.style.height=`${Math.max(200,a)}px`)}catch{}},r=()=>{let a=e.contentDocument,l=s&&a?a.querySelector(".kestrel-email"):null;l&&(h(l,z(Xp(t(),n()))),Xt(l.ownerDocument),i())};return e.addEventListener("load",()=>{s=!0,r();try{let a=e.contentDocument;a?.body&&typeof ResizeObserver<"u"&&new ResizeObserver(i).observe(a.body)}catch{}}),e.srcdoc=em,{repaint:r}}function tm(){return o`${Gp.map(e=>o`<div class="set-tpl-vargroup"><h4>${e.group}</h4>${e.vars.map(t=>o`<div class="set-tpl-var"><code>${t.token}</code><span class="set-tpl-var-desc">${t.desc}</span></div>`)}</div>`)}`}async function wi(e,t){ie(t,"Template"),h(e,o`<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>&lt;style&gt;</code> block and <code>{{ variables }}</code> Kestrel fills in; your post’s Markdown is rendered into <code>{{ .Post.Body }}</code>.<span id="tplWait"></span></p></div><div id="tplBody" class="muted">Loading…</div></div>`);let n=c("#tplBody",e),s;try{s=await L("/api/settings",{signal:t})}catch(Q){re(n,eo(Q),()=>ee(wi));return}let i=s.settings,r=s.deployment,a=i.publication,l={name:mn(a.name||"",r.from_address),tagline:a.tagline||"",logoUrl:a.logo_url||"",address:a.address||""},d=i.email_template||"",u=Array.isArray(i.test_recipients)?i.test_recipients:[];h(c("#tplInUse"),Qs(s.in_use)),h(c("#tplWait"),o`${nn(s.in_use)}`),Xs(e,s.in_use,t),h(n,o`
622
+ </div>`}},am=e=>Object.hasOwn(Ti,e),lm=`<div style="position:relative;border:1px dashed #93a7e6;border-radius:8px;padding:20px 14px 8px;margin:0 0 6px"><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><h2 style="margin:0 0 10px">Lorem ipsum,</h2><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></div>`,cm=new Set([".Post.Body",".Publication.Logo"]);function dm(e,t){return String(e).replace(/\{\{\s*(?:if\b[^}]*|end)\s*\}\}/g,"").replace(/\{\{\s*([\w.]+)\s*\}\}/g,(s,i)=>cm.has(i)?t[i]||"":te(t[i]??""))}function um(e){let t=e.name||"Your publication";return{".Post.Body":lm,".Post.Subject":"How your posts will look",".Publication.Name":t,".Publication.Tagline":e.tagline||"Your tagline",".Publication.LogoURL":e.logoUrl,".Publication.Logo":Xl(e.logoUrl,t),".Publication.Address":e.address,".Email.SentTo":"you@example.com",".Email.UnsubscribeURL":"#unsubscribe",".Email.ViewInBrowserURL":"#view-in-browser"}}function pm(e,t){return dm(e,um(t))}var mm='<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>html,body{margin:0}body{background:#fff}@media (prefers-color-scheme:dark){body{background:#18181b}}.kestrel-email{max-width:640px;margin:0 auto;padding:26px 20px;box-sizing:border-box}.kestrel-email img{max-width:100%}</style></head><body><div class="kestrel-email"></div></body></html>';function Li(e,t,n){e.setAttribute("sandbox",tn);let s=!1,i=()=>{try{let a=e.contentDocument?.body?.scrollHeight;a&&(e.style.height=`${Math.max(200,a)}px`)}catch{}},r=()=>{let a=e.contentDocument,l=s&&a?a.querySelector(".kestrel-email"):null;l&&(f(l,Y(pm(t(),n()))),nn(l.ownerDocument),i())};return e.addEventListener("load",()=>{s=!0,r();try{let a=e.contentDocument;a?.body&&typeof ResizeObserver<"u"&&new ResizeObserver(i).observe(a.body)}catch{}}),e.srcdoc=mm,{repaint:r}}function fm(){return o`${rm.map(e=>o`<div class="set-tpl-vargroup"><h4>${e.group}</h4>${e.vars.map(t=>o`<div class="set-tpl-var"><code>${t.token}</code><span class="set-tpl-var-desc">${t.desc}</span></div>`)}</div>`)}`}async function Ri(e,t){ie(t,"Template"),f(e,o`<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>&lt;style&gt;</code> block and <code>{{ variables }}</code> Kestrel fills in; your post’s Markdown is rendered into <code>{{ .Post.Body }}</code>.<span id="tplWait"></span></p></div><div id="tplBody" class="muted">Loading…</div></div>`);let n=c("#tplBody",e),s;try{s=await C("/api/settings",{signal:t})}catch(X){re(n,ao(X),()=>ne(Ri));return}let i=s.settings,r=s.deployment,a=i.publication,l={name:bn(a.name||"",r.from_address),tagline:a.tagline||"",logoUrl:a.logo_url||"",address:a.address||""},d=i.email_template||"",u=Array.isArray(i.test_recipients)?i.test_recipients:[];f(c("#tplInUse"),io(s.in_use)),f(c("#tplWait"),o`${rn(s.in_use)}`),ro(e,s.in_use,t),f(n,o`
622
623
  <div class="set-preview set-tpl-sample">
623
624
  <div class="set-preview-bar">
624
625
  <span class="set-preview-titles">
@@ -630,7 +631,7 @@
630
631
  <button type="button" class="wtog-btn" data-w="640" aria-pressed="true">640</button>
631
632
  <button type="button" class="wtog-btn" data-w="375" aria-pressed="false">375</button>
632
633
  </span>
633
- <button type="button" class="secondary" id="tplTest">${O("send")}<span id="tplTestLbl">Send test email</span></button>
634
+ <button type="button" class="secondary" id="tplTest">${B("send")}<span id="tplTestLbl">Send test email</span></button>
634
635
  </span>
635
636
  </div>
636
637
  <div class="set-email-stage" id="tplStage">
@@ -645,7 +646,7 @@
645
646
  <div class="set-tpl-editor-head">
646
647
  <div class="set-tpl-tools">
647
648
  <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>
648
- <button type="button" class="set-icon-btn" id="tplLineNums" aria-pressed="false" title="Show line numbers" aria-label="Show line numbers">${O("lines")}</button>
649
+ <button type="button" class="set-icon-btn" id="tplLineNums" aria-pressed="false" title="Show line numbers" aria-label="Show line numbers">${B("lines")}</button>
649
650
  <button type="button" class="ghost" id="tplCopyAll" title="Copy the whole template to the clipboard" aria-label="Copy the whole template">${Te()}</button>
650
651
  </div>
651
652
  <div class="set-tpl-required" aria-label="Required variables">
@@ -662,7 +663,7 @@
662
663
  <div class="set-tpl-msgs" id="tplMsgs" hidden></div>
663
664
  </div>
664
665
  </div>
665
- <div class="set-note">${O("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>
666
+ <div class="set-note">${B("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>
666
667
  </div>
667
668
 
668
669
  <div class="set-card set-tpl-varcard">
@@ -671,16 +672,16 @@
671
672
  <h3 class="set-tpl-vartitle">Variables</h3>
672
673
  <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>
673
674
  </div>
674
- <div class="set-tpl-vars-body">${tm()}</div>
675
+ <div class="set-tpl-vars-body">${fm()}</div>
675
676
  </div>
676
- </div>`);let m=c("#tplEditor"),v=yi(c("#tplPreview"),()=>m.value,()=>l),f=c("#tplMsgs"),p=c("#tplTest"),S=c("#tplTestLbl"),$=()=>m.value!==d,M=c("#tplHl"),P=c("code",M),A=c("#tplGutter"),H=()=>{let Q=m.value;h(P,o`${Ya(Q)}\n`);let le="",de=Q.split(`
677
- `).length;for(let Pe=1;Pe<=de;Pe++)le+=`${Pe}
678
- `;A.textContent=le},y=()=>{M.scrollTop=m.scrollTop,M.scrollLeft=m.scrollLeft,A.scrollTop=m.scrollTop};m.addEventListener("scroll",y);let w=c("#reqBody"),T=c("#reqUnsub"),D=()=>{w.className=`set-req-pill ${/\{\{\s*\.Post\.Body\s*\}\}/.test(m.value)?"ok":"bad"}`,T.className=`set-req-pill ${/\{\{\s*\.Email\.UnsubscribeURL\s*\}\}/.test(m.value)?"ok":"bad"}`},R=zs.attach({onSave:C,onDiscard:b},t);function F(){H(),D(),y(),R.setDirty($()),S.textContent=$()?"Save & send test":"Send test email",p.title=$()?"Saves your changes first, then sends: a test always reflects the saved template that will ship.":"Sends a sample post through the saved template so you can see it in a real inbox."}function K(Q){if(!Q.length){f.hidden=!0,h(f,o``);return}f.hidden=!1,f.className="set-tpl-msgs warn",h(f,o`${Q.map(le=>o`<div>${le}${le===Ul?o` ${Bn()}`:null}</div>`)}`)}function b(){m.value=d,K([]),v.repaint(),F()}async function E(){let Q=m.value,le=await Ct(de=>L("/api/settings",{method:"PUT",json:de?{email_template:Q,remake:de}:{email_template:Q}}),Ol,t);return le?(d=le.settings.email_template,m.value=d,Y.appConfig={...s,settings:le.settings},v.repaint(),F(),{warnings:Array.isArray(le.warnings)?le.warnings:[],remade:le.remade||[]}):null}async function C(){try{let Q=await E();if(!Q)return;K(Q.warnings),U(sn(Q.warnings.length?"Template saved with warnings":"Template saved",Q.remade))}catch(Q){R.showError(eo(Q))}}let x=Q=>{let le=vi[zp(Q)?Q:"signed"];m.value=le.html,K([]),v.repaint(),F()};m.addEventListener("input",()=>{v.repaint(),F()});let W=c("#tplExamplesBtn");W.onclick=()=>Yt(W,Object.entries(vi).map(([Q,le])=>({label:le.label,description:le.description,onClick:()=>x(Q)})),t,{align:"start"});let oe=c("#tplPreview");for(let Q of N(".wtog-btn",n))Q.onclick=()=>{for(let le of N(".wtog-btn",n))le.setAttribute("aria-pressed",String(le===Q));oe.style.maxWidth=`${Q.dataset.w}px`,v.repaint()};let k=c("#tplEditorWrap"),G=c("#tplLineNums"),q=Q=>{k.classList.toggle("show-lines",Q),G.setAttribute("aria-pressed",String(Q)),y()},ne=!1;try{ne=localStorage.getItem("kestrel.tpl.lineNums")==="1"}catch{}q(ne),G.onclick=()=>{ne=!ne,q(ne);try{localStorage.setItem("kestrel.tpl.lineNums",ne?"1":"0")}catch{}};let He=c("#tplCopyAll");He.onclick=()=>Tn(He,m.value),p.onclick=()=>{let Q=ge(),le=!Q&&$(),de=fe(o`<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>${vs()}${le?o`<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>`:null}${Q?null:o`<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">${le?"Save & send test":"Send test"}</button></div>`,t),Pe=c("#ttGo",de.el);if(c("#ttCancel",de.el).onclick=de.close,Pe.disabled=Q,Q){c("#ttCancel",de.el).focus();return}let Ce=c("#tplTestTo",de.el);if(Ce.focus(),u.length&&!Ce.value.trim()){Ce.value=u.join(`
679
- `);let Me=c("#tplTestHint",de.el);Me.textContent="Pre-filled from your default test recipients (Settings).",Me.hidden=!1}Pe.onclick=()=>Z(Pe,$()?"Saving\u2026":"Sending\u2026",async()=>{de.clearError();let{valid:Me,invalid:pt}=xs(Ce.value),mt=Es(pt);if(mt){de.error(mt,Ce);return}if(!Me.length){de.error("Enter at least one email address",Ce);return}if($())try{let me=await E();if(!me){de.close();return}K(me.warnings)}catch(me){R.showError(eo(me)),de.close();return}try{let me=await L("/api/settings/template/test",{method:"POST",json:{to:Me}});if(me.failed.length){Ce.value=me.failed.join(`
680
- `),de.error(ks(me.sent,me.total,me.not_sent));return}de.close(),U(Bt(`Test sent to ${me.sent} address${me.sent===1?"":"es"}`))}catch(me){de.error(eo(me))}})},m.value=d,v.repaint(),F()}function Vl(e){return o`
677
+ </div>`);let p=c("#tplEditor"),g=Li(c("#tplPreview"),()=>p.value,()=>l),h=c("#tplMsgs"),m=c("#tplTest"),y=c("#tplTestLbl"),E=()=>p.value!==d,R=c("#tplHl"),N=c("code",R),A=c("#tplGutter"),P=()=>{let X=p.value;f(N,o`${ll(X)}\n`);let le="",me=X.split(`
678
+ `).length;for(let Pe=1;Pe<=me;Pe++)le+=`${Pe}
679
+ `;A.textContent=le},v=()=>{R.scrollTop=p.scrollTop,R.scrollLeft=p.scrollLeft,A.scrollTop=p.scrollTop};p.addEventListener("scroll",v);let w=c("#reqBody"),I=c("#reqUnsub"),j=()=>{w.className=`set-req-pill ${/\{\{\s*\.Post\.Body\s*\}\}/.test(p.value)?"ok":"bad"}`,I.className=`set-req-pill ${/\{\{\s*\.Email\.UnsubscribeURL\s*\}\}/.test(p.value)?"ok":"bad"}`},x=to.attach({onSave:H,onDiscard:S},t);function O(){P(),j(),v(),x.setDirty(E()),y.textContent=E()?"Save & send test":"Send test email",m.title=E()?"Saves your changes first, then sends: a test always reflects the saved template that will ship.":"Sends a sample post through the saved template so you can see it in a real inbox."}function V(X){if(!X.length){h.hidden=!0,f(h,o``);return}h.hidden=!1,h.className="set-tpl-msgs warn",f(h,o`${X.map(le=>o`<div>${le}${le===ra?o` ${jn()}`:null}</div>`)}`)}function S(){p.value=d,V([]),g.repaint(),O()}async function T(){let X=p.value,le=await At(me=>C("/api/settings",{method:"PUT",json:me?{email_template:X,remake:me}:{email_template:X}}),Jl,t);return le?(d=le.settings.email_template,p.value=d,Z.appConfig={...s,settings:le.settings},g.repaint(),O(),{warnings:Array.isArray(le.warnings)?le.warnings:[],remade:le.remade||[]}):null}async function H(){try{let X=await T();if(!X)return;V(X.warnings),q(an(X.warnings.length?"Template saved with warnings":"Template saved",X.remade))}catch(X){x.showError(ao(X))}}let $=X=>{let le=Ti[am(X)?X:"signed"];p.value=le.html,V([]),g.repaint(),O()};p.addEventListener("input",()=>{g.repaint(),O()});let L=c("#tplExamplesBtn");L.onclick=()=>Qt(L,Object.entries(Ti).map(([X,le])=>({label:le.label,description:le.description,onClick:()=>$(X)})),t,{align:"start"});let G=c("#tplPreview");for(let X of D(".wtog-btn",n))X.onclick=()=>{for(let le of D(".wtog-btn",n))le.setAttribute("aria-pressed",String(le===X));G.style.maxWidth=`${X.dataset.w}px`,g.repaint()};let k=c("#tplEditorWrap"),K=c("#tplLineNums"),F=X=>{k.classList.toggle("show-lines",X),K.setAttribute("aria-pressed",String(X)),v()},ee=!1;try{ee=localStorage.getItem("kestrel.tpl.lineNums")==="1"}catch{}F(ee),K.onclick=()=>{ee=!ee,F(ee);try{localStorage.setItem("kestrel.tpl.lineNums",ee?"1":"0")}catch{}};let Le=c("#tplCopyAll");Le.onclick=()=>An(Le,p.value),m.onclick=()=>{let X=be(),le=!X&&E(),me=pe(o`<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>${$s()}${le?o`<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>`:null}${X?null:o`<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">${le?"Save & send test":"Send test"}</button></div>`,t),Pe=c("#ttGo",me.el);if(c("#ttCancel",me.el).onclick=me.close,Pe.disabled=X,X){c("#ttCancel",me.el).focus();return}let Ae=c("#tplTestTo",me.el);if(Ae.focus(),u.length&&!Ae.value.trim()){Ae.value=u.join(`
680
+ `);let tt=c("#tplTestHint",me.el);tt.textContent="Pre-filled from your default test recipients (Settings).",tt.hidden=!1}Pe.onclick=()=>J(Pe,E()?"Saving\u2026":"Sending\u2026",async()=>{me.clearError();let{valid:tt,invalid:Ht}=Rs(Ae.value),nt=Cs(Ht);if(nt){me.error(nt,Ae);return}if(!tt.length){me.error("Enter at least one email address",Ae);return}if(E())try{let ge=await T();if(!ge){me.close();return}V(ge.warnings)}catch(ge){x.showError(ao(ge)),me.close();return}try{let ge=await C("/api/settings/template/test",{method:"POST",json:{to:tt}});if(ge.failed.length){Ae.value=ge.failed.join(`
681
+ `),me.error(As(ge.sent,ge.total,ge.not_sent));return}me.close(),q(qt(`Test sent to ${ge.sent} address${ge.sent===1?"":"es"}`))}catch(ge){me.error(ao(ge))}})},p.value=d,g.repaint(),O()}function sc(e){return o`
681
682
  <section class="set-sec">
682
- ${Se("Email template",we("editable","Editable"),Qs(e.data.in_use))}
683
- <p class="set-lede">The template controls the look and feel of the emails you send. Edit it on the Template page.${nn(e.data.in_use)}</p>
683
+ ${we("email-template",Se("editable","Editable"),io(e.data.in_use))}
684
+ <p class="set-lede">The template controls the look and feel of the emails you send. Edit it on the Template page.${rn(e.data.in_use)}</p>
684
685
  <div class="set-preview set-tpl-sample">
685
686
  <div class="set-preview-bar">
686
687
  <span class="set-preview-lbl">Sample email</span>
@@ -689,52 +690,52 @@
689
690
  <iframe class="set-email-frame" id="tplPreview" title="Sample email preview" scrolling="no"></iframe>
690
691
  </div>
691
692
  <div class="row" style="margin-top:12px"><button type="button" id="tplEditLink">Edit template →</button></div>
692
- </section>`}function Kl(e,t){let n=t.data.settings.email_template||"",s=yi(c("#tplPreview",e),()=>n,()=>({name:t.state.name||t.fromDisplay,tagline:t.state.tagline,logoUrl:t.state.logoUrl,address:t.state.address}));return c("#tplEditLink",e).onclick=()=>{location.hash="#/template"},{repaint:()=>s.repaint()}}var Gl={chips:"recipChips",input:"recipInput",add:"recipAdd"};function zl(){return o`
693
+ </section>`}function oc(e,t){let n=t.data.settings.email_template||"",s=Li(c("#tplPreview",e),()=>n,()=>({name:t.state.name||t.fromDisplay,tagline:t.state.tagline,logoUrl:t.state.logoUrl,address:t.state.address}));return c("#tplEditLink",e).onclick=()=>{location.hash="#/template"},{repaint:()=>s.repaint()}}var ic={chips:"recipChips",input:"recipInput",add:"recipAdd"};function rc(){return o`
693
694
  <section class="set-sec">
694
- ${Se("Default test recipients",we("editable","Editable"))}
695
+ ${we("default-test-recipients",Se("editable","Editable"))}
695
696
  <div class="set-card">
696
697
  <div class="set-recip">
697
698
  <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’t go through the subscribe-and-confirm flow.</p>
698
- ${Zs(Gl)}
699
+ ${so(ic)}
699
700
  </div>
700
701
  </div>
701
- </section>`}function Yl(e,t){return Js(e,t,{ids:Gl,list:()=>t.state.recipients,empty:"No default recipients yet. Add one below."})}function nm(e,t){switch(e){case"cloudflare":return"Cloudflare Email, separate from your newsletter\u2019s provider";case"provider":return t==="none"?"Your email provider, once one is connected":`Your email provider (${Ys(t)})`;default:return"Local stand-in: nothing is delivered"}}var Zl={finished:"went out",refused:"provider refusing",stuck:"in flight too long",wedged:"waiting to be resolved",missed:"missed fire time",import:"list imported",sandbox:"signup refused by the SES sandbox",not_sending:"email not going out",test:"test"};function Jl(e){return e.kind==="test"?"test notification":e.subject?`${e.subject} (${Zl[e.kind]})`:Zl[e.kind]}function $i(e){let{last_sent:t,last_failure:n}=e;return n?o`<span class="set-pill danger">Not delivered</span> <span class="muted">${j(n.at)}, ${Jl(n)}: ${n.error}</span>`:t?o`<span class="set-pill ok">${O("check")}Delivered</span> <span class="muted">${j(t.at)}, ${Jl(t)}</span>`:o`<span class="muted">None yet</span>`}var Ql={chips:"notifyChips",input:"notifyTo",add:"notifyAdd"};function Xl(e){let t=e.data.deployment;return o`
702
+ </section>`}function ac(e,t){return oo(e,t,{ids:ic,list:()=>t.state.recipients,empty:"No default recipients yet. Add one below."})}function hm(e,t){switch(e){case"cloudflare":return"Cloudflare Email, separate from your newsletter\u2019s provider";case"provider":return t==="none"?"Your email provider, once one is connected":`Your email provider (${no(t)})`;default:return"Local stand-in: nothing is delivered"}}var lc={finished:"went out",refused:"provider refusing",stuck:"in flight too long",wedged:"waiting to be resolved",missed:"missed fire time",no_receipts:"no receipts from the provider",import:"list imported",sandbox:"signup refused by the SES sandbox",not_sending:"email not going out",not_sending_held:"email not going out",test:"test"};function cc(e){return e.kind==="test"?"test notification":e.subject?`${e.subject} (${lc[e.kind]})`:lc[e.kind]}function Ci(e){let{last_sent:t,last_failure:n}=e;return n?o`<span class="set-pill danger">Not delivered</span> <span class="muted">${W(n.at)}, ${cc(n)}: ${n.error}</span>`:t?o`<span class="set-pill ok">${B("check")}Delivered</span> <span class="muted">${W(t.at)}, ${cc(t)}</span>`:o`<span class="muted">None yet</span>`}var dc={chips:"notifyChips",input:"notifyTo",add:"notifyAdd"};function uc(e){let t=e.data.deployment;return o`
702
703
  <section class="set-sec">
703
- ${Se("Notifications",we("editable","Editable"))}
704
+ ${we("notifications",Se("editable","Editable"))}
704
705
  <p class="set-lede">Kestrel emails you when a send goes out, and right away if a send runs into a problem.</p>
705
706
  <div class="set-card">
706
707
  <div class="set-recip">
707
708
  <div class="set-field">
708
709
  <label for="notifyTo">Send notifications to</label>
709
- <p class="field-hint" style="margin:0">Each address gets every notification. Add whoever runs Kestrel for you, so they’re told too. Up to ${5}.</p>
710
+ <p class="field-hint" style="margin:0">Each address gets every notification. Add whoever runs Kestrel for you, so they’re told too. Up to ${xs}.</p>
710
711
  </div>
711
- ${Zs(Ql)}
712
+ ${so(dc)}
712
713
  <div class="row"><button type="button" class="ghost" id="notifyTest">Send a test notification</button><span class="field-hint" id="notifyTestHint"></span></div>
713
714
  </div>
714
715
  <div class="set-kv" style="border-top:1px solid var(--line)">
715
- <div class="set-kv-k">Sent through</div><div class="set-kv-v">${nm(t.notify_channel,t.provider)}</div>
716
+ <div class="set-kv-k">Sent through</div><div class="set-kv-v">${hm(t.notify_channel,t.provider)}</div>
716
717
  <div class="set-kv-k">From address</div><div class="set-kv-v"><span class="mono">${t.notify_from}</span></div>
717
- <div class="set-kv-k">Last notification</div><div class="set-kv-v" id="notifyStatus">${$i(e.data.notification_status)}</div>
718
+ <div class="set-kv-k">Last notification</div><div class="set-kv-v" id="notifyStatus">${Ci(e.data.notification_status)}</div>
718
719
  </div>
719
- <div class="set-note">${O("readonly")}<span>The channel and its sender are set at deploy. ${t.notify_channel==="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>
720
+ <div class="set-note">${B("readonly")}<span>The channel and its sender are set at deploy. ${t.notify_channel==="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>
720
721
  </div>
721
- </section>`}function ec(e,t){let n=c("#notifyTest",e),s=c("#notifyTestHint",e),i=c("#notifyStatus",e),r=Js(e,t,{ids:Ql,list:()=>t.state.notifyTo,empty:"No addresses yet, so Kestrel emails no one. Add yours below.",max:{count:5,full:`Notifications go to at most ${5} addresses. Remove one to add another.`}}),a=()=>{let l=t.saved().notifyTo,d=l.length===0?"Save an address to send a test.":t.state.notifyTo.join(`
722
+ </section>`}function pc(e,t){let n=c("#notifyTest",e),s=c("#notifyTestHint",e),i=c("#notifyStatus",e),r=oo(e,t,{ids:dc,list:()=>t.state.notifyTo,empty:"No addresses yet, so Kestrel emails no one. Add yours below.",max:{count:xs,full:`Notifications go to at most ${xs} addresses. Remove one to add another.`}}),a=()=>{let l=t.saved().notifyTo,d=l.length===0?"Save an address to send a test.":t.state.notifyTo.join(`
722
723
  `)!==l.join(`
723
- `)?"Save first: the test goes to the saved addresses.":"";n.disabled=!!d,s.textContent=d};return n.onclick=()=>Z(n,"Sending\u2026",async()=>{try{let l=await L("/api/settings/notifications/test",{method:"POST"}),d=l.to.filter(m=>!l.failed.some(v=>v.email===m));U(l.channel==="fake"?`Test notification recorded for ${d.join(", ")} (local stand-in: nothing is delivered)`:`Test notification sent to ${d.join(", ")}`);let u=Fl(l.to,l.failed);h(i,$i({last_sent:{kind:"test",subject:"",at:Date.now()},last_failure:u?{kind:"test",subject:"",at:Date.now(),error:u}:null}))}catch(l){h(i,$i({last_sent:null,last_failure:{kind:"test",subject:"",at:Date.now(),error:ut(l)}}))}}),a(),{repaint:a,reset(){r.reset?.(),a()}}}var Un={base:"app",path:"/subscribe"},to={base:"app",path:"/confirm?token=\u2026"},xi={base:"app",path:"/unsubscribe?token=\u2026"},Fn=[{id:"subscribe",label:"Subscribe",flow:"steps",states:[{id:"form",label:"Form",at:Un},{id:"check-inbox",label:"Check your inbox",at:Un},{id:"not-an-email",label:"Not an email",offPath:!0,at:Un},{id:"not-open",label:"Not open yet",offPath:!0,at:Un},{id:"paused",label:"Paused",offPath:!0,at:Un}]},{id:"confirm",label:"Confirm",flow:"steps",states:[{id:"ready",label:"Ready",at:to},{id:"confirmed",label:"Confirmed",at:to},{id:"expired",label:"Expired",offPath:!0,at:to},{id:"invalid",label:"Invalid link",offPath:!0,at:to}]},{id:"unsubscribe",label:"Unsubscribe",flow:"steps",states:[{id:"ask",label:"Ask",at:xi},{id:"done",label:"Unsubscribed",at:xi},{id:"invalid",label:"Invalid link",offPath:!0,at:xi}]},{id:"reading",label:"Reading",flow:"peers",states:[{id:"landing",label:"Landing page",at:{base:"app",path:"/"}},{id:"archive",label:"Archive",at:{base:"archive",path:""}},{id:"post",label:"Post page",at:{base:"archive",path:"/sample-post"}},{id:"landing-empty",label:"Landing, no posts yet",offPath:!0,at:{base:"app",path:"/"}},{id:"archive-empty",label:"Archive, no posts yet",offPath:!0,at:{base:"archive",path:""}},{id:"not-found",label:"Nothing here yet",offPath:!0,at:{base:"archive",path:"/\u2026"}}]}];function tc(e,t){return`/api/reader-pages/${encodeURIComponent(e)}/${encodeURIComponent(t)}`}var no="Shown with signups open; until they are, readers get no Subscribe link or form here.",sm={subscribe:{form:"Your subscribe page, where a reader enters their email address.","check-inbox":"What the reader is shown after pressing Subscribe, here, on your website before your first post, or on a form embedded on your site.","not-an-email":"When the email address a reader sent isn\u2019t one that can be mailed. The browser catches most mistakes first.","not-open":"Shown in place of the form until your email provider accepts your first email, such as a test.",paused:"Shown in place of the form while your email provider won\u2019t send for your account, as when SES pauses your sending."},confirm:{ready:"When a reader opens the link in your confirmation email.",confirmed:"After they confirm. They\u2019re on your list and get your next post.",expired:"When a reader opens a confirmation link that\u2019s too old. They can ask for a new one.",invalid:"When a confirmation link is broken, mistyped, or replaced by a newer one."},unsubscribe:{ask:"When a reader opens the unsubscribe link in one of your emails. A mail application with its own one-click Unsubscribe button skips this page and removes them right away.",done:"After they press Unsubscribe. They get no more of your emails.",invalid:"When an unsubscribe link is broken or mistyped."},reading:{landing:`What anyone sees when they visit your newsletter\u2019s website. The posts are samples. ${no}`,archive:`Every post in your archive. These are samples. ${no}`,post:"The web version of a post that went out. Readers reach it from View in browser in its email, or from your archive.","landing-empty":`Your newsletter\u2019s website before your first post goes out. ${no}`,"archive-empty":`Your archive before your first post goes out. ${no}`,"not-found":"When a reader follows a link to a page that isn\u2019t there."}};function sc(e,t){return sm[e.id][t.id]??""}var om={subscribe:"mail",confirm:"circle-check",unsubscribe:"circle-minus",reading:"book"},im=o`<span class="set-rp-dot"></span>`;function nc(e,t,n){return o`<li><button type="button" class="set-rp-item${t.offPath?" off":""}" data-journey="${e.id}" data-state="${t.id}" aria-current="false" title="${sc(e,t)}"><span class="set-rp-mark" aria-hidden="true">${n}</span>${t.label}</button></li>`}function rm(){return o`${Fn.map(e=>{let t=e.states,n=e.flow==="steps";return o`<div class="set-rp-journey" data-journey="${e.id}">
724
- <h3 class="set-rp-jhead" id="rpJourney-${e.id}">${O(om[e.id])}${e.label}</h3>
724
+ `)?"Save first: the test goes to the saved addresses.":"";n.disabled=!!d,s.textContent=d};return n.onclick=()=>J(n,"Sending\u2026",async()=>{try{let l=await C("/api/settings/notifications/test",{method:"POST"}),d=l.to.filter(p=>!l.failed.some(g=>g.email===p));q(l.channel==="fake"?`Test notification recorded for ${d.join(", ")} (local stand-in: nothing is delivered)`:`Test notification sent to ${d.join(", ")}`);let u=aa(l.to,l.failed);f(i,Ci({last_sent:{kind:"test",subject:"",at:Date.now()},last_failure:u?{kind:"test",subject:"",at:Date.now(),error:u}:null}))}catch(l){f(i,Ci({last_sent:null,last_failure:{kind:"test",subject:"",at:Date.now(),error:ft(l)}}))}}),a(),{repaint:a,reset(){r.reset?.(),a()}}}var Vn={base:"app",path:"/subscribe"},lo={base:"app",path:"/confirm?token=\u2026"},Ai={base:"app",path:"/unsubscribe?token=\u2026"},Wn=[{id:"subscribe",label:"Subscribe",flow:"steps",states:[{id:"form",label:"Form",at:Vn},{id:"check-inbox",label:"Check your inbox",at:Vn},{id:"not-an-email",label:"Not an email",offPath:!0,at:Vn},{id:"not-open",label:"Not open yet",offPath:!0,at:Vn},{id:"paused",label:"Paused",offPath:!0,at:Vn}]},{id:"confirm",label:"Confirm",flow:"steps",states:[{id:"ready",label:"Ready",at:lo},{id:"confirmed",label:"Confirmed",at:lo},{id:"expired",label:"Expired",offPath:!0,at:lo},{id:"invalid",label:"Invalid link",offPath:!0,at:lo}]},{id:"unsubscribe",label:"Unsubscribe",flow:"steps",states:[{id:"ask",label:"Ask",at:Ai},{id:"done",label:"Unsubscribed",at:Ai},{id:"invalid",label:"Invalid link",offPath:!0,at:Ai}]},{id:"reading",label:"Reading",flow:"peers",states:[{id:"landing",label:"Landing page",at:{base:"app",path:"/"}},{id:"archive",label:"Archive",at:{base:"archive",path:""}},{id:"post",label:"Post page",at:{base:"archive",path:"/sample-post"}},{id:"landing-empty",label:"Landing, no posts yet",offPath:!0,at:{base:"app",path:"/"}},{id:"archive-empty",label:"Archive, no posts yet",offPath:!0,at:{base:"archive",path:""}},{id:"not-found",label:"Nothing here yet",offPath:!0,at:{base:"archive",path:"/\u2026"}}]}];function mc(e,t){return`/api/reader-pages/${encodeURIComponent(e)}/${encodeURIComponent(t)}`}var co="Shown with signups open; until they are, readers get no Subscribe link or form here.",bm={subscribe:{form:"Your subscribe page, where a reader enters their email address.","check-inbox":"What the reader is shown after pressing Subscribe, here, on your website before your first post, or on a form embedded on your site.","not-an-email":"When the email address a reader sent isn\u2019t one that can be mailed. The browser catches most mistakes first.","not-open":"Shown in place of the form until your email provider accepts your first email, such as a test.",paused:"Shown in place of the form while your email provider won\u2019t send for your account, as when SES pauses your sending."},confirm:{ready:"When a reader opens the link in your confirmation email.",confirmed:"After they confirm. They\u2019re on your list and get your next post.",expired:"When a reader opens a confirmation link that\u2019s too old. They can ask for a new one.",invalid:"When a confirmation link is broken, mistyped, or replaced by a newer one."},unsubscribe:{ask:"When a reader opens the unsubscribe link in one of your emails. A mail application with its own one-click Unsubscribe button skips this page and removes them right away.",done:"After they press Unsubscribe. They get no more of your emails.",invalid:"When an unsubscribe link is broken or mistyped."},reading:{landing:`What anyone sees when they visit your newsletter\u2019s website. The posts are samples. ${co}`,archive:`Every post in your archive. These are samples. ${co}`,post:"The web version of a post that went out. Readers reach it from View in browser in its email, or from your archive.","landing-empty":`Your newsletter\u2019s website before your first post goes out. ${co}`,"archive-empty":`Your archive before your first post goes out. ${co}`,"not-found":"When a reader follows a link to a page that isn\u2019t there."}};function hc(e,t){return bm[e.id][t.id]??""}var gm={subscribe:"mail",confirm:"circle-check",unsubscribe:"circle-minus",reading:"book"},vm=o`<span class="set-rp-dot"></span>`;function fc(e,t,n){return o`<li><button type="button" class="set-rp-item${t.offPath?" off":""}" data-journey="${e.id}" data-state="${t.id}" aria-current="false" title="${hc(e,t)}"><span class="set-rp-mark" aria-hidden="true">${n}</span>${t.label}</button></li>`}function ym(){return o`${Wn.map(e=>{let t=e.states,n=e.flow==="steps";return o`<div class="set-rp-journey" data-journey="${e.id}">
725
+ <h3 class="set-rp-jhead" id="rpJourney-${e.id}">${B(gm[e.id])}${e.label}</h3>
725
726
  <ul class="set-rp-list${n?" steps":""}" aria-labelledby="rpJourney-${e.id}">
726
- ${t.filter(s=>!s.offPath).map((s,i)=>nc(e,s,n?String(i+1):im))}
727
+ ${t.filter(s=>!s.offPath).map((s,i)=>fc(e,s,n?String(i+1):vm))}
727
728
  </ul>
728
729
  <ul class="set-rp-list" aria-label="${e.label}, off the main path">
729
- ${t.filter(s=>s.offPath).map(s=>nc(e,s,O("branch")))}
730
+ ${t.filter(s=>s.offPath).map(s=>fc(e,s,B("branch")))}
730
731
  </ul>
731
- </div>`})}`}function am(){return o`<select id="rpSelect" aria-label="Reader page">${Fn.map(e=>{let t=e.states,n=0;return o`<optgroup label="${e.label}">${t.map(s=>{let i=e.flow==="steps"&&!s.offPath?`${++n}. `:"";return o`<option value="${e.id}/${s.id}">${i}${s.label}</option>`})}</optgroup>`})}</select>`}function oc(e){return o`
732
+ </div>`})}`}function Sm(){return o`<select id="rpSelect" aria-label="Reader page">${Wn.map(e=>{let t=e.states,n=0;return o`<optgroup label="${e.label}">${t.map(s=>{let i=e.flow==="steps"&&!s.offPath?`${++n}. `:"";return o`<option value="${e.id}/${s.id}">${i}${s.label}</option>`})}</optgroup>`})}</select>`}function bc(e){return o`
732
733
  <section class="set-sec set-rp-sec" id="readerPages">
733
734
  ${e}
734
735
  <p class="set-lede">Every page a reader can land on, exactly as they see it, with your publication’s identity and sample values. Nothing here changes a page; it shows the ones that are hard to reach by hand.</p>
735
736
  <div class="set-card set-rp">
736
- <nav class="set-rp-nav" aria-label="Reader pages">${rm()}</nav>
737
- <div class="set-rp-pick">${am()}</div>
737
+ <nav class="set-rp-nav" aria-label="Reader pages">${ym()}</nav>
738
+ <div class="set-rp-pick">${Sm()}</div>
738
739
  <div class="set-rp-main">
739
740
  <div class="set-rp-top">
740
741
  <div>
@@ -742,8 +743,8 @@
742
743
  <p class="set-rp-note" id="rpNote"></p>
743
744
  </div>
744
745
  <span class="set-wtog" id="rpWidth" role="group" aria-label="Preview width">
745
- <button type="button" class="wtog-btn" data-width="desktop" aria-pressed="true">${O("desktop")}Desktop</button>
746
- <button type="button" class="wtog-btn" data-width="phone" aria-pressed="false">${O("phone")}Phone</button>
746
+ <button type="button" class="wtog-btn" data-width="desktop" aria-pressed="true">${B("desktop")}Desktop</button>
747
+ <button type="button" class="wtog-btn" data-width="phone" aria-pressed="false">${B("phone")}Phone</button>
747
748
  </span>
748
749
  </div>
749
750
  <div class="set-rp-at"><span class="mono" id="rpAt"></span><span>Sample · links and forms do nothing</span></div>
@@ -753,7 +754,7 @@
753
754
  </div>
754
755
  </div>
755
756
  </div>
756
- </section>`}function lm(e,t){return`${e.base==="app"?t.app_origin:`${t.archive_origin}${t.archive_base_path}`}${e.path}`.replace(/^https?:\/\//,"")}function ic(e,t,n){let s=c(".set-rp-nav",e),i=c("#rpSelect",e),r=c("#rpWidth",e),a=c("#rpStage",e),l=c("#rpFrame",e),d=c("#rpError",e),u=Fn[0],m=u.states[0],v="desktop",f=0,p=null;Nt(n,()=>p?.disconnect());let S=()=>{for(let A of N(".set-rp-item",s)){let H=A.dataset.journey===u.id&&A.dataset.state===m.id;A.setAttribute("aria-current",H?"true":"false")}for(let A of N(".set-rp-journey",s))A.classList.toggle("current",A.dataset.journey===u.id);i.value=`${u.id}/${m.id}`,c("#rpJourney",e).textContent=u.label,c("#rpState",e).textContent=m.label,c("#rpNote",e).textContent=sc(u,m),c("#rpAt",e).textContent=lm(m.at,t)},$=()=>{let A=l.contentDocument?.body?.scrollHeight;A&&(l.style.height=`${A}px`)},M=async()=>{let A=++f;try{let H=await Ot(tc(u.id,m.id),{signal:n});if(A!==f||n.aborted)return;d.hidden=!0,a.hidden=!1,l.srcdoc=Bs(H)}catch(H){if(A!==f||n.aborted)return;l.srcdoc="",a.hidden=!0,d.textContent=`Couldn\u2019t load this page: ${V(H)}`,d.hidden=!1}},P=(A,H)=>{let y=Fn.find(D=>D.id===A),T=(y?.states??[]).find(D=>D.id===H);!y||!T||y===u&&T===m||(u=y,m=T,S(),M())};return l.onload=()=>{let A=l.contentDocument;!A||n.aborted||(A.addEventListener("click",H=>{H.target?.closest?.("a[href], area[href]")&&H.preventDefault()}),p?.disconnect(),$(),A.body&&typeof ResizeObserver<"u"&&(p=new ResizeObserver($),p.observe(A.body)))},s.addEventListener("click",A=>{let H=A.target.closest(".set-rp-item");P(H?.dataset.journey,H?.dataset.state)}),i.addEventListener("change",()=>{let[A,H]=i.value.split("/");P(A,H)}),r.addEventListener("click",A=>{let y=A.target.closest("button[data-width]")?.dataset.width;if(!(y!=="desktop"&&y!=="phone"||y===v)){v=y;for(let w of N("button",r))w.setAttribute("aria-pressed",String(w.dataset.width===y));a.classList.toggle("phone",y==="phone")}}),S(),M(),{refresh:()=>{M()}}}function ac(e){return e?`Subscribe to ${e}`:"Subscribe"}function cm(e,t,n){let s=X(ac(t)),i=`${X(n)}/subscribe`;return e==="styled"?`<form action="${i}" method="post" style="max-width:460px;font:16px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">
757
+ </section>`}function wm(e,t){return`${e.base==="app"?t.app_origin:`${t.archive_origin}${t.archive_base_path}`}${e.path}`.replace(/^https?:\/\//,"")}function gc(e,t,n){let s=c(".set-rp-nav",e),i=c("#rpSelect",e),r=c("#rpWidth",e),a=c("#rpStage",e),l=c("#rpFrame",e),d=c("#rpError",e),u=Wn[0],p=u.states[0],g="desktop",h=0,m=null;Bt(n,()=>m?.disconnect());let y=()=>{for(let A of D(".set-rp-item",s)){let P=A.dataset.journey===u.id&&A.dataset.state===p.id;A.setAttribute("aria-current",P?"true":"false")}for(let A of D(".set-rp-journey",s))A.classList.toggle("current",A.dataset.journey===u.id);i.value=`${u.id}/${p.id}`,c("#rpJourney",e).textContent=u.label,c("#rpState",e).textContent=p.label,c("#rpNote",e).textContent=hc(u,p),c("#rpAt",e).textContent=wm(p.at,t)},E=()=>{let A=l.contentDocument?.body?.scrollHeight;A&&(l.style.height=`${A}px`)},R=async()=>{let A=++h;try{let P=await Ut(mc(u.id,p.id),{signal:n});if(A!==h||n.aborted)return;d.hidden=!0,a.hidden=!1,l.srcdoc=Ks(P)}catch(P){if(A!==h||n.aborted)return;l.srcdoc="",a.hidden=!0,d.textContent=`Couldn\u2019t load this page: ${z(P)}`,d.hidden=!1}},N=(A,P)=>{let v=Wn.find(j=>j.id===A),I=(v?.states??[]).find(j=>j.id===P);!v||!I||v===u&&I===p||(u=v,p=I,y(),R())};return l.onload=()=>{let A=l.contentDocument;!A||n.aborted||(A.addEventListener("click",P=>{P.target?.closest?.("a[href], area[href]")&&P.preventDefault()}),m?.disconnect(),E(),A.body&&typeof ResizeObserver<"u"&&(m=new ResizeObserver(E),m.observe(A.body)))},s.addEventListener("click",A=>{let P=A.target.closest(".set-rp-item");N(P?.dataset.journey,P?.dataset.state)}),i.addEventListener("change",()=>{let[A,P]=i.value.split("/");N(A,P)}),r.addEventListener("click",A=>{let v=A.target.closest("button[data-width]")?.dataset.width;if(!(v!=="desktop"&&v!=="phone"||v===g)){g=v;for(let w of D("button",r))w.setAttribute("aria-pressed",String(w.dataset.width===v));a.classList.toggle("phone",v==="phone")}}),y(),R(),{refresh:()=>{R()}}}function yc(e){return e?`Subscribe to ${e}`:"Subscribe"}function $m(e,t,n){let s=te(yc(t)),i=`${te(n)}/subscribe`;return e==="styled"?`<form action="${i}" method="post" style="max-width:460px;font:16px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">
757
758
  <div style="font:600 22px/1.2 Georgia,'Times New Roman',serif;letter-spacing:-0.01em;margin-bottom:12px">${s}</div>
758
759
  <div style="display:flex;gap:10px;flex-wrap:wrap">
759
760
  <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">
@@ -766,14 +767,14 @@
766
767
  <input type="email" name="email" placeholder="you@example.com" required>
767
768
  </label>
768
769
  <button type="submit">Subscribe</button>
769
- </form>`}function dm(e,t){let n=ac(t);return e==="styled"?o`<form class="set-pf-styled"><span class="l">${n}</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’ll email you a link to confirm. Unsubscribe anytime.</p></form>`:o`<form class="set-pf-plain"><label>${n} <input type="email" placeholder="you@example.com" disabled></label> <button type="button" tabindex="-1">Subscribe</button></form>`}function lc(e){return e.data.deployment.app_origin||location.origin}function um(e){return e?"A public page with your publication\u2019s name and a sign-up form ready to go.":"A public page with a sign-up form ready to go."}function rc(e,t,n,s,i=null){return o`<div class="set-way"><div class="set-way-head"><div class="set-way-name"><h3 class="set-way-title">${O(e)}${t}</h3><p class="set-way-line">${n}</p></div>${i}</div>${s}</div>`}function cc(e){let t=`${lc(e)}/subscribe`;return o`
770
+ </form>`}function Em(e,t){let n=yc(t);return e==="styled"?o`<form class="set-pf-styled"><span class="l">${n}</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’ll email you a link to confirm. Unsubscribe anytime.</p></form>`:o`<form class="set-pf-plain"><label>${n} <input type="email" placeholder="you@example.com" disabled></label> <button type="button" tabindex="-1">Subscribe</button></form>`}function Sc(e){return e.data.deployment.app_origin||location.origin}function xm(e){return e?"A public page with your publication\u2019s name and a sign-up form ready to go.":"A public page with a sign-up form ready to go."}function vc(e,t,n,s,i=null){return o`<div class="set-way"><div class="set-way-head"><div class="set-way-name"><h3 class="set-way-title">${B(e)}${t}</h3><p class="set-way-line">${n}</p></div>${i}</div>${s}</div>`}function wc(e){let t=`${Sc(e)}/subscribe`;return o`
770
771
  <section class="set-sec set-sec-subscribe">
771
- ${Se("Ways to subscribe",we("copyout","Copy-out"))}
772
+ ${we("ways-to-subscribe",Se("copyout","Copy-out"))}
772
773
  <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>
773
- ${e.data.deployment.provider==="none"?o`<div class="health amber set-sub-not-connected" role="status"><span class="health-dot">⚠️</span><div><span>No email provider connected, so signups aren’t open yet: the page below says so, and the form takes no address. ${Ae()} to open signups.</span></div></div>`:null}
774
+ ${e.data.deployment.provider==="none"?o`<div class="health amber set-sub-not-connected" role="status"><span class="health-dot">⚠️</span><div><span>No email provider connected, so signups aren’t open yet: the page below says so, and the form takes no address. ${He()} to open signups.</span></div></div>`:null}
774
775
  <div class="set-card">
775
- ${rc("link","Share the page",o`<span id="subscribePageLine"></span>`,o`<div class="set-way-body set-way-addr"><code class="set-addr">${t}</code>${_n(t,"Copy the subscribe page\u2019s address")}<a class="ghost-link" href="${t}" target="_blank" rel="noopener">Open&nbsp;↗</a></div>`)}
776
- ${rc("code","Embed a form",o`Put a sign-up form on another site you own.`,o`<div class="set-embed">
776
+ ${vc("link","Share the page",o`<span id="subscribePageLine"></span>`,o`<div class="set-way-body set-way-addr"><code class="set-addr">${t}</code>${Cn(t,"Copy the subscribe page\u2019s address")}<a class="ghost-link" href="${t}" target="_blank" rel="noopener">Open&nbsp;↗</a></div>`)}
777
+ ${vc("code","Embed a form",o`Put a sign-up form on another site you own.`,o`<div class="set-embed">
777
778
  <div class="set-embed-preview" id="embedPreview"></div>
778
779
  <pre class="set-embed-code"><code id="embedCode"></code></pre>
779
780
  </div>
@@ -782,23 +783,23 @@
782
783
  <button type="button" class="wtog-btn" data-embed="plain" aria-pressed="false">Plain</button>
783
784
  </span>`)}
784
785
  </div>
785
- </section>`}function pm(e,t){return o`<button type="button" class="secondary set-embed-copy" data-copy="${t}" data-copy-word="Copy HTML" aria-label="Copy HTML, ${e==="styled"?"Styled":"Plain"} form">${Te(!1,"Copy HTML")}</button>`}function dc(e,t){let n=lc(t),s=c("#embedCode",e),i=c("#embedCopy",e),r=c("#embedPreview",e),a=c("#subscribePageLine",e),l="styled",d=()=>t.state.name||t.fromDisplay,u=()=>{let f=cm(l,d(),n);s.textContent=f,h(i,pm(l,f)),h(r,dm(l,d())),a.textContent=um(d())},m=N("[data-embed]",e);for(let f of m)f.onclick=()=>{l=f.dataset.embed==="styled"?"styled":"plain";for(let p of m)p.setAttribute("aria-pressed",String(p.dataset.embed===l));u()};We(c(".set-sec-subscribe",e)),u();let v=d();return{repaint(){d()!==v&&(v=d(),u())}}}function qn(e,t){return o`<div class="set-group" data-group="${e}">${t}</div>`}function Ei(e){return{name:e.name,tagline:e.tagline,address:e.address,recipients:[...e.recipients],confirmation:{...e.confirmation},notifyTo:[...e.notifyTo]}}var uc=(e,t)=>e.length===t.length&&e.every((n,s)=>n===t[s]),mm=(e,t)=>e.subject===t.subject&&e.body===t.body&&e.button_label===t.button_label&&e.reassurance===t.reassurance;async function ki(e,t){ie(t,"Settings"),h(e,o`<div class="settings"><div class="page-head"><h1>Settings</h1><p class="set-lede set-page-lede">Your publication’s 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…</div></div>`);let n=c("#settingsBody",e),s;try{s=await L("/api/settings",{signal:t})}catch(w){re(n,ut(w),()=>ee(ki));return}let i=await at(s.deployment,t),r=s.settings,a=r.publication,l=r.confirmation_email,d={name:a.name||"",tagline:a.tagline||"",address:a.address||"",logoUrl:a.logo_url||"",recipients:[...r.test_recipients||[]],confirmation:{subject:l.subject||"",body:l.body||"",button_label:l.button_label||"",reassurance:l.reassurance||""},notifyTo:[...r.notifications.to]},u=Ei(d),m=ko(s.deployment.from_address),v=()=>d.name!==u.name||d.tagline!==u.tagline||d.address!==u.address||!uc(d.recipients,u.recipients)||!mm(d.confirmation,u.confirmation)||!uc(d.notifyTo,u.notifyTo),f=[],p=zs.attach({onSave:H,onDiscard:y},t),S=()=>{},$={data:s,provider:i,state:d,saved:()=>u,fromDisplay:m,fromName:m||"Your publication",changed(){for(let w of f)w.repaint?.();p.setDirty(v())},applySaved(w){Y.appConfig={...s,settings:w},ls(),S()}};h(n,o`
786
- ${qn("publication",[jl($)])}
787
- ${qn("readers",[cc($),Pl($),oc(Se("Reader pages",we("preview","Preview")))])}
788
- ${qn("emails",[Vl($),zl()])}
789
- ${qn("you",[Xl($)])}
790
- ${qn("deploy",[Il($),Nl($)])}
791
- `),f.push(Wl(n,$,t),dc(n,$),Ml(n,$),Kl(n,$),Yl(n,$),ec(n,$)),Xs(n,s.in_use,t),S=ic(n,s.deployment,t).refresh;let P=null,A=()=>{P?.(),P=null};async function H(){A();try{let w={publication:{name:d.name,tagline:d.tagline,address:d.address},test_recipients:d.recipients,confirmation_email:d.confirmation,notifications:{to:d.notifyTo}},T=await Ct(R=>L("/api/settings",{method:"PUT",json:R?{...w,remake:R}:w}),Dn("Save","Saving"),t);if(!T)return;let D=T.settings;d.name=D.publication.name,d.tagline=D.publication.tagline,d.address=D.publication.address,d.recipients=[...D.test_recipients],d.confirmation={...D.confirmation_email},d.notifyTo=[...D.notifications.to],u=Ei(d);for(let R of f)R.reset?.();$.applySaved(D),$.changed(),U(sn("Settings saved",T.remade))}catch(w){let T=w instanceof te?hm(e,w.field):null;T&&w instanceof te?(P=qe(T,ut(w)),T.focus(),p.showError("Not saved: fix the field marked above.")):p.showError(ut(w))}}function y(){A(),Object.assign(d,Ei(u));for(let w of f)w.reset?.();$.changed()}$.changed()}var fm={"publication.name":"#setName","publication.tagline":"#setTagline","publication.address":"#setAddress",test_recipients:"#recipInput","notifications.to":"#notifyTo","confirmation_email.subject":"#ceSubject","confirmation_email.body":"#ceMessage","confirmation_email.button_label":"#ceButton","confirmation_email.reassurance":"#ceFooter"};function hm(e,t){let n=t?fm[t]:void 0,s=n?e.querySelector(n):null;return(s instanceof HTMLInputElement||s instanceof HTMLTextAreaElement)&&!s.closest("[hidden]")?s:null}var bm="kestrel-subscribers/1",jn="kestrel-subscribers/2",gm=[bm,jn],vm=["subscribed","pending","unsubscribed","none"],_i=["double","single","unknown"],ym=["bounce","manual"];var wm=["reader","publisher","complaint","erased"],pc=["subscribed","unsubscribed","bounced"],Sm={maxRows:1e4,maxRowsFree:1e3,maxBodyBytes:10*1024*1024,maxEvidenceBytes:500};var mc=[{name:"email",required:!0,values:null,takes:"an email address; trimmed and lowercased before it is matched",meaning:"The address. An invalid one is still a row, counted as invalid and reported, never dropped."},{name:"status",required:!0,values:vm,takes:"one of the values",meaning:"The consent claim. `none` is a known address with no consent claim, present only to carry a suppression."},{name:"opt_in",required:!0,values:_i,takes:"one of the values",meaning:"How consent was collected. `unknown` unless the source states it."},{name:"origin",required:!0,values:null,takes:"a short lowercase name (letters, digits, `.`, `_`, `-`, up to 40): `sendy`, `mailchimp`, `kestrel`, `unknown`",meaning:"Where consent was first collected, as the file claims it."},{name:"unsubscribe_cause",required:!1,values:wm,takes:"one of the values, or empty; only on an `unsubscribed` row",meaning:"Why the person left: their own unsubscribe (`reader`), the publisher on their request (`publisher`), a complaint about the mail (`complaint`), or an erasure at their request (`erased`), which is recorded with the address alone. Empty when the source doesn\u2019t say, recorded as having left elsewhere. Version 2 only."},{name:"suppression",required:!1,values:ym,takes:"one of the values, or empty",meaning:"A suppression on top of `status`, the sender\u2019s side: the address bounced, or was blocked by hand. In version 1 it also took `complaint`, read as an `unsubscribed` row whose cause is `complaint`, and `erased`, read as one whose cause is `erased`."},{name:"subscribed_at",required:!1,values:null,takes:"a UTC ISO-8601 instant with its zone (`2024-03-01T09:30:00Z`), or empty",meaning:"When the person subscribed, if the source states it."},{name:"confirmed_at",required:!1,values:null,takes:"an instant with its zone, or empty",meaning:"When the person confirmed, if the source states it."},{name:"unsubscribed_at",required:!1,values:null,takes:"an instant with its zone, or empty",meaning:"When the person left, if the source states it. A reader who confirmed here after it stays confirmed."},{name:"suppressed_at",required:!1,values:null,takes:"an instant with its zone, or empty",meaning:"When the address was suppressed, if the source states it."},{name:"evidence",required:!1,values:null,takes:`a JSON object of at most ${Sm.maxEvidenceBytes} bytes, or empty`,meaning:"The source\u2019s consent-relevant columns, verbatim (a signup IP, a form name, a confirmation time). Nothing reads it to decide anything: it is the proof of consent and the audit trail of a conversion."},{name:"format",required:!1,values:gm,takes:"the format\u2019s name and version, or empty",meaning:"The version the row is written in. A row without it may use either version\u2019s values, which every version reads alike; a version this release doesn\u2019t know is refused by name."}],h$=new Set(mc.map(e=>e.name));var $m=/^[=+\-@\t\r]/;function xm(e){return $m.test(e)?`'${e}`:e}function Em(e){let t=xm(e);return/[",\r\n]/.test(t)||t!==t.trim()?`"${t.replace(/"/g,'""')}"`:t}function Ti(e){return`${e.map(t=>t.map(Em).join(",")).join(`\r
786
+ </section>`}function km(e,t){return o`<button type="button" class="secondary set-embed-copy" data-copy="${t}" data-copy-word="Copy HTML" aria-label="Copy HTML, ${e==="styled"?"Styled":"Plain"} form">${Te(!1,"Copy HTML")}</button>`}function $c(e,t){let n=Sc(t),s=c("#embedCode",e),i=c("#embedCopy",e),r=c("#embedPreview",e),a=c("#subscribePageLine",e),l="styled",d=()=>t.state.name||t.fromDisplay,u=()=>{let h=$m(l,d(),n);s.textContent=h,f(i,km(l,h)),f(r,Em(l,d())),a.textContent=xm(d())},p=D("[data-embed]",e);for(let h of p)h.onclick=()=>{l=h.dataset.embed==="styled"?"styled":"plain";for(let m of p)m.setAttribute("aria-pressed",String(m.dataset.embed===l));u()};je(c(".set-sec-subscribe",e)),u();let g=d();return{repaint(){d()!==g&&(g=d(),u())}}}function Kn(e,t){return o`<div class="set-group" data-group="${e}">${t}</div>`}function Hi(e){return{name:e.name,tagline:e.tagline,address:e.address,recipients:[...e.recipients],confirmation:{...e.confirmation},notifyTo:[...e.notifyTo]}}var Ec=(e,t)=>e.length===t.length&&e.every((n,s)=>n===t[s]),_m=(e,t)=>e.subject===t.subject&&e.body===t.body&&e.button_label===t.button_label&&e.reassurance===t.reassurance;async function Pi(e,t,n){ie(t,"Settings"),f(e,o`<div class="settings"><div class="page-head"><h1>Settings</h1><p class="set-lede set-page-lede">Your publication’s 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…</div></div>`);let s=c("#settingsBody",e),i;try{i=await C("/api/settings",{signal:t})}catch(x){re(s,ft(x),()=>ne((O,V)=>Pi(O,V,n)));return}let r=await dt(i.deployment,t),a=i.settings,l=a.publication,d=a.confirmation_email,u={name:l.name||"",tagline:l.tagline||"",address:l.address||"",logoUrl:l.logo_url||"",recipients:[...a.test_recipients||[]],confirmation:{subject:d.subject||"",body:d.body||"",button_label:d.button_label||"",reassurance:d.reassurance||""},notifyTo:[...a.notifications.to]},p=Hi(u),g=Po(i.deployment.from_address),h=()=>u.name!==p.name||u.tagline!==p.tagline||u.address!==p.address||!Ec(u.recipients,p.recipients)||!_m(u.confirmation,p.confirmation)||!Ec(u.notifyTo,p.notifyTo),m=[],y=to.attach({onSave:v,onDiscard:w},t),E=()=>{},R={data:i,provider:r,state:u,saved:()=>p,fromDisplay:g,fromName:g||"Your publication",changed(){for(let x of m)x.repaint?.();y.setDirty(h())},applySaved(x){Z.appConfig={...i,settings:x},ps(),E()}};f(s,o`
787
+ ${Kn("publication",[tc(R)])}
788
+ ${Kn("readers",[wc(R),Gl(R),bc(we("reader-pages",Se("preview","Preview")))])}
789
+ ${Kn("emails",[sc(R),rc()])}
790
+ ${Kn("you",[uc(R)])}
791
+ ${Kn("deploy",[Yl(R),Zl(R)])}
792
+ `),m.push(nc(s,R,t),$c(s,R),zl(s,R),oc(s,R),ac(s,R),pc(s,R)),ro(s,i.in_use,t),E=gc(s,i.deployment,t).refresh;let A=null,P=()=>{A?.(),A=null};async function v(){P();try{let x={publication:{name:u.name,tagline:u.tagline,address:u.address},test_recipients:u.recipients,confirmation_email:u.confirmation,notifications:{to:u.notifyTo}},O=await At(S=>C("/api/settings",{method:"PUT",json:S?{...x,remake:S}:x}),qn("Save","Saving"),t);if(!O)return;let V=O.settings;u.name=V.publication.name,u.tagline=V.publication.tagline,u.address=V.publication.address,u.recipients=[...V.test_recipients],u.confirmation={...V.confirmation_email},u.notifyTo=[...V.notifications.to],p=Hi(u);for(let S of m)S.reset?.();R.applySaved(V),R.changed(),q(an("Settings saved",O.remade))}catch(x){let O=x instanceof se?Lm(e,x.field):null;O&&x instanceof se?(A=Fe(O,ft(x)),O.focus(),y.showError("Not saved: fix the field marked above.")):y.showError(ft(x))}}function w(){P(),Object.assign(u,Hi(p));for(let x of m)x.reset?.();R.changed()}R.changed();let I=()=>{},j=x=>{I(),Fo(x)?I=Kl(c(`#${xi(x)}`,e),e,t):(I=()=>{},window.scrollTo(0,0))};return Fo(n)&&j(n),{navigate(x){let[,O,V]=x.split("/");return O!=="settings"||!vn(x.slice(1))?!1:(j(V),!0)}}}var Tm={"publication.name":"#setName","publication.tagline":"#setTagline","publication.address":"#setAddress",test_recipients:"#recipInput","notifications.to":"#notifyTo","confirmation_email.subject":"#ceSubject","confirmation_email.body":"#ceMessage","confirmation_email.button_label":"#ceButton","confirmation_email.reassurance":"#ceFooter"};function Lm(e,t){let n=t?Tm[t]:void 0,s=n?e.querySelector(n):null;return(s instanceof HTMLInputElement||s instanceof HTMLTextAreaElement)&&!s.closest("[hidden]")?s:null}var Rm="kestrel-subscribers/1",Gn="kestrel-subscribers/2",Cm=[Rm,Gn],Am=["subscribed","pending","unsubscribed","none"],Ii=["double","single","unknown"],Hm=["bounce","manual"];var Pm=["reader","publisher","complaint","erased"],xc=["subscribed","unsubscribed","bounced"],Im={maxRows:1e4,maxRowsFree:1e3,maxBodyBytes:10*1024*1024,maxEvidenceBytes:500};var kc=[{name:"email",required:!0,values:null,takes:"an email address; trimmed and lowercased before it is matched",meaning:"The address. An invalid one is still a row, counted as invalid and reported, never dropped."},{name:"status",required:!0,values:Am,takes:"one of the values",meaning:"The consent claim. `none` is a known address with no consent claim, present only to carry a suppression."},{name:"opt_in",required:!0,values:Ii,takes:"one of the values",meaning:"How consent was collected. `unknown` unless the source states it."},{name:"origin",required:!0,values:null,takes:"a short lowercase name (letters, digits, `.`, `_`, `-`, up to 40): `sendy`, `mailchimp`, `kestrel`, `unknown`",meaning:"Where consent was first collected, as the file claims it."},{name:"unsubscribe_cause",required:!1,values:Pm,takes:"one of the values, or empty; only on an `unsubscribed` row",meaning:"Why the person left: their own unsubscribe (`reader`), the publisher on their request (`publisher`), a complaint about the mail (`complaint`), or an erasure at their request (`erased`), which is recorded with the address alone. Empty when the source doesn\u2019t say, recorded as having left elsewhere. Version 2 only."},{name:"suppression",required:!1,values:Hm,takes:"one of the values, or empty",meaning:"A suppression on top of `status`, the sender\u2019s side: the address bounced, or was blocked by hand. In version 1 it also took `complaint`, read as an `unsubscribed` row whose cause is `complaint`, and `erased`, read as one whose cause is `erased`."},{name:"subscribed_at",required:!1,values:null,takes:"a UTC ISO-8601 instant with its zone (`2024-03-01T09:30:00Z`), or empty",meaning:"When the person subscribed, if the source states it."},{name:"confirmed_at",required:!1,values:null,takes:"an instant with its zone, or empty",meaning:"When the person confirmed, if the source states it."},{name:"unsubscribed_at",required:!1,values:null,takes:"an instant with its zone, or empty",meaning:"When the person left, if the source states it. A reader who confirmed here after it stays confirmed."},{name:"suppressed_at",required:!1,values:null,takes:"an instant with its zone, or empty",meaning:"When the address was suppressed, if the source states it."},{name:"evidence",required:!1,values:null,takes:`a JSON object of at most ${Im.maxEvidenceBytes} bytes, or empty`,meaning:"The source\u2019s consent-relevant columns, verbatim (a signup IP, a form name, a confirmation time). Nothing reads it to decide anything: it is the proof of consent and the audit trail of a conversion."},{name:"format",required:!1,values:Cm,takes:"the format\u2019s name and version, or empty",meaning:"The version the row is written in. A row without it may use either version\u2019s values, which every version reads alike; a version this release doesn\u2019t know is refused by name."}],O$=new Set(kc.map(e=>e.name));var Mm=/^[=+\-@\t\r]/;function Nm(e){return Mm.test(e)?`'${e}`:e}function Om(e){let t=Nm(e);return/[",\r\n]/.test(t)||t!==t.trim()?`"${t.replace(/"/g,'""')}"`:t}function Mi(e){return`${e.map(t=>t.map(Om).join(",")).join(`\r
792
793
  `)}\r
793
- `}function Li(e){let t=mc.map(n=>n.name);return Ti([t,...e.map(n=>t.map(s=>n[s]??""))])}var Ri=[{email:"ada@example.com",status:"subscribed",opt_in:"double",origin:"sendy",unsubscribe_cause:null,suppression:null,subscribed_at:"2024-03-01T09:30:00Z",confirmed_at:"2024-03-01T09:42:00Z",unsubscribed_at:null,suppressed_at:null,evidence:'{"signup_ip":"203.0.113.7","list":"Field Notes"}',format:jn},{email:"grace@example.com",status:"subscribed",opt_in:"single",origin:"mailchimp",unsubscribe_cause:null,suppression:null,subscribed_at:"2023-11-12T16:05:00Z",confirmed_at:null,unsubscribed_at:null,suppressed_at:null,evidence:null,format:jn},{email:"edsger@example.com",status:"unsubscribed",opt_in:"unknown",origin:"unknown",unsubscribe_cause:"reader",suppression:null,subscribed_at:null,confirmed_at:null,unsubscribed_at:"2025-01-20T08:00:00Z",suppressed_at:null,evidence:null,format:jn},{email:"bounced@example.com",status:"none",opt_in:"unknown",origin:"unknown",unsubscribe_cause:null,suppression:"bounce",subscribed_at:null,confirmed_at:null,unsubscribed_at:null,suppressed_at:"2025-02-02T12:00:00Z",evidence:null,format:jn}],b$=Li(Ri),Ci=`ada@example.com\r
794
+ `}function Ni(e){let t=kc.map(n=>n.name);return Mi([t,...e.map(n=>t.map(s=>n[s]??""))])}var Oi=[{email:"ada@example.com",status:"subscribed",opt_in:"double",origin:"sendy",unsubscribe_cause:null,suppression:null,subscribed_at:"2024-03-01T09:30:00Z",confirmed_at:"2024-03-01T09:42:00Z",unsubscribed_at:null,suppressed_at:null,evidence:'{"signup_ip":"203.0.113.7","list":"Field Notes"}',format:Gn},{email:"grace@example.com",status:"subscribed",opt_in:"single",origin:"mailchimp",unsubscribe_cause:null,suppression:null,subscribed_at:"2023-11-12T16:05:00Z",confirmed_at:null,unsubscribed_at:null,suppressed_at:null,evidence:null,format:Gn},{email:"edsger@example.com",status:"unsubscribed",opt_in:"unknown",origin:"unknown",unsubscribe_cause:"reader",suppression:null,subscribed_at:null,confirmed_at:null,unsubscribed_at:"2025-01-20T08:00:00Z",suppressed_at:null,evidence:null,format:Gn},{email:"bounced@example.com",status:"none",opt_in:"unknown",origin:"unknown",unsubscribe_cause:null,suppression:"bounce",subscribed_at:null,confirmed_at:null,unsubscribed_at:null,suppressed_at:"2025-02-02T12:00:00Z",evidence:null,format:Gn}],D$=Ni(Oi),Di=`ada@example.com\r
794
795
  grace@example.com\r
795
796
  linus@example.com\r
796
- `,fc="kestrel-import-example.csv",Ai="kestrel-import-example.txt";var km={buttondown:"Buttondown",convertkit:"ConvertKit",kit:"Kit",kestrel:"Kestrel",mailchimp:"Mailchimp",mailerlite:"MailerLite",sendy:"Sendy",substack:"Substack",beehiiv:"beehiiv",ghost:"Ghost"};function Wn(e){return e===null||e===""||e==="unknown"?null:km[e]??e.charAt(0).toUpperCase()+e.slice(1)}function Vn(e){return e===null||e==="unknown"?"opt-in unknown":`${e} opt-in`}function hc(e){return e.import_id!==null?`Imported \xB7 ${[Wn(e.origin),Vn(e.opt_in)].filter(Boolean).join(", ")}`:e.confirmed_at!==null?"Double opt-in":null}function bc(e){if(!e)return null;try{return JSON.stringify(JSON.parse(e),null,2)}catch{return e}}function so(e){return e.principal.kind==="service"?"a service token":en(e.principal.email,e.principal.client)??"you"}var gc=(e,t,n)=>`${e.toLocaleString("en-US")} ${e===1?t:n}`,_m=[{outcome:"added_confirmed",one:"address added as a confirmed subscriber",many:"addresses added as confirmed subscribers"},{outcome:"confirmed_pending",one:"pending subscriber here confirmed",many:"pending subscribers here confirmed"},{outcome:"unsubscribed",one:"subscriber here unsubscribed",many:"subscribers here unsubscribed"},{outcome:"erased",one:"subscriber here erased, as the file says: only the address is kept, and past sends show an erased recipient",many:"subscribers here erased, as the file says: only their addresses are kept, and past sends show an erased recipient"},{outcome:"added_unsubscribed",one:"address recorded as having left (unsubscribed), so the list remembers them",many:"addresses recorded as having left (unsubscribed), so the list remembers them"},{outcome:"suppressed",one:"address suppressed as the file says (bounced or blocked), so it\u2019s never mailed",many:"addresses suppressed as the file says (bounced or blocked), so they\u2019re never mailed"},{outcome:"unchanged",one:"address already here as the file says, so it\u2019s unchanged",many:"addresses already here as the file says, so they\u2019re unchanged"},{outcome:"left_out",one:"row still pending, or with no status, left out, since an import sends no email",many:"rows still pending, or with no status, left out, since an import sends no email"},{outcome:"invalid",one:"row isn\u2019t a valid address",many:"rows aren\u2019t valid addresses"},{outcome:"merged",one:"row repeats an address earlier in the file, and is folded into it",many:"rows repeat an address earlier in the file, and are folded into it"},{outcome:"kept_suppressed",one:"address is suppressed here, so it isn\u2019t imported",many:"addresses are suppressed here, so they aren\u2019t imported",kept:!0},{outcome:"kept_unsubscribed",one:"reader unsubscribed here, and stays unsubscribed",many:"readers unsubscribed here, and stay unsubscribed",kept:!0},{outcome:"kept_confirmed_newer",one:"reader confirmed here after the file says they left, and stays confirmed",many:"readers confirmed here after the file says they left, and stay confirmed",kept:!0}];function Hi(e){return _m.filter(t=>e[t.outcome]>0).map(t=>({outcome:t.outcome,count:e[t.outcome],text:gc(e[t.outcome],t.one,t.many),kept:t.kept===!0}))}function vc(e){return e.added_confirmed+e.confirmed_pending}function Pi(e,t){let n=t-e.suppressed;return n>0?`${gc(n,"of the addresses above also gains","of the addresses above also gain")} a suppression, so ${n===1?"it\u2019s":"they\u2019re"} never mailed.`:null}function yc(e){let t=_i.filter(s=>(e.opt_in[s]??0)>0).map(s=>`${e.opt_in[s].toLocaleString("en-US")} ${Vn(s)}`).join(", "),n=Object.entries(e.origin).filter(([,s])=>s>0).sort((s,i)=>i[1]-s[1]||s[0].localeCompare(i[0])).map(([s,i])=>`${Wn(s)??"unknown"} ${i.toLocaleString("en-US")}`).join(", ");return{optIn:t,origin:n}}var on={added_confirmed:"Imported as subscribed",confirmed_pending:"Imported as subscribed",unsubscribed:"Unsubscribed",erased:"Erased",added_unsubscribed:"Recorded as having left",suppressed:"Suppressed",unchanged:"Unchanged",left_out:"Left out",invalid:"Invalid",merged:"Folded into an earlier row",kept_suppressed:"Already suppressed here",kept_unsubscribed:"Kept unsubscribed",kept_confirmed_newer:"Kept confirmed"};function wc(e,t,n){let s=new Map(e.map(a=>[a.row,a])),i=new Map(t.map(a=>[a.row,a])),r=new Map;for(let a of new Set([...s.keys(),...i.keys()])){let l=s.get(a),d=i.get(a),u=l?on[l.outcome]:on.added_confirmed,m=d?on[d.outcome]:on.added_confirmed;u!==m&&r.set(a,{row:a,email:(d??l)?.email??"",planned:u,applied:m})}for(let a of n){let l=on[a.planned],d=on[a.applied];!r.has(a.row)&&l!==d&&r.set(a.row,{row:a.row,email:a.email,planned:l,applied:d})}return[...r.values()].sort((a,l)=>a.row-l.row)}function Sc(e){return Ti([["row","email","outcome","suppression_added"],...e.map(t=>[String(t.row),t.email,t.outcome,t.suppression_added?"yes":"no"])])}var $c="Please import my Sendy subscribers from ~/Downloads/sendy-export.csv into the newsletter.",Tm="Your list or its scheduled posts changed since this plan, so here is the plan again as things stand now. Review it, then import.",Mi=["email","status","opt_in","origin","subscribed_at"],Lm={subscribed:"Subscribed",unsubscribed:"Unsubscribed",bounced:"Bounced"};async function Ni(e,t){ie(t,"Import subscribers"),h(e,o`<div class="imp">
797
+ `,_c="kestrel-import-example.csv",Bi="kestrel-import-example.txt";var Dm={buttondown:"Buttondown",convertkit:"ConvertKit",kit:"Kit",kestrel:"Kestrel",mailchimp:"Mailchimp",mailerlite:"MailerLite",sendy:"Sendy",substack:"Substack",beehiiv:"beehiiv",ghost:"Ghost"};function zn(e){return e===null||e===""||e==="unknown"?null:Dm[e]??e.charAt(0).toUpperCase()+e.slice(1)}function Yn(e){return e===null||e==="unknown"?"opt-in unknown":`${e} opt-in`}function Tc(e){return e.import_id!==null?`Imported \xB7 ${[zn(e.origin),Yn(e.opt_in)].filter(Boolean).join(", ")}`:e.confirmed_at!==null?"Double opt-in":null}function Lc(e){if(!e)return null;try{return JSON.stringify(JSON.parse(e),null,2)}catch{return e}}function uo(e){return e.principal.kind==="service"?"a service token":sn(e.principal.email,e.principal.client)??"you"}var Rc=(e,t,n)=>`${e.toLocaleString("en-US")} ${e===1?t:n}`,Bm=[{outcome:"added_confirmed",one:"address added as a confirmed subscriber",many:"addresses added as confirmed subscribers"},{outcome:"confirmed_pending",one:"pending subscriber here confirmed",many:"pending subscribers here confirmed"},{outcome:"unsubscribed",one:"subscriber here unsubscribed",many:"subscribers here unsubscribed"},{outcome:"erased",one:"subscriber here erased, as the file says: only the address is kept, and past sends show an erased recipient",many:"subscribers here erased, as the file says: only their addresses are kept, and past sends show an erased recipient"},{outcome:"added_unsubscribed",one:"address recorded as having left (unsubscribed), so the list remembers them",many:"addresses recorded as having left (unsubscribed), so the list remembers them"},{outcome:"suppressed",one:"address suppressed as the file says (bounced or blocked), so it\u2019s never mailed",many:"addresses suppressed as the file says (bounced or blocked), so they\u2019re never mailed"},{outcome:"unchanged",one:"address already here as the file says, so it\u2019s unchanged",many:"addresses already here as the file says, so they\u2019re unchanged"},{outcome:"left_out",one:"row still pending, or with no status, left out, since an import sends no email",many:"rows still pending, or with no status, left out, since an import sends no email"},{outcome:"invalid",one:"row isn\u2019t a valid address",many:"rows aren\u2019t valid addresses"},{outcome:"merged",one:"row repeats an address earlier in the file, and is folded into it",many:"rows repeat an address earlier in the file, and are folded into it"},{outcome:"kept_suppressed",one:"address is suppressed here, so it isn\u2019t imported",many:"addresses are suppressed here, so they aren\u2019t imported",kept:!0},{outcome:"kept_unsubscribed",one:"reader unsubscribed here, and stays unsubscribed",many:"readers unsubscribed here, and stay unsubscribed",kept:!0},{outcome:"kept_confirmed_newer",one:"reader confirmed here after the file says they left, and stays confirmed",many:"readers confirmed here after the file says they left, and stay confirmed",kept:!0}];function Ui(e){return Bm.filter(t=>e[t.outcome]>0).map(t=>({outcome:t.outcome,count:e[t.outcome],text:Rc(e[t.outcome],t.one,t.many),kept:t.kept===!0}))}function Cc(e){return e.added_confirmed+e.confirmed_pending}function Fi(e,t){let n=t-e.suppressed;return n>0?`${Rc(n,"of the addresses above also gains","of the addresses above also gain")} a suppression, so ${n===1?"it\u2019s":"they\u2019re"} never mailed.`:null}function Ac(e){let t=Ii.filter(s=>(e.opt_in[s]??0)>0).map(s=>`${e.opt_in[s].toLocaleString("en-US")} ${Yn(s)}`).join(", "),n=Object.entries(e.origin).filter(([,s])=>s>0).sort((s,i)=>i[1]-s[1]||s[0].localeCompare(i[0])).map(([s,i])=>`${zn(s)??"unknown"} ${i.toLocaleString("en-US")}`).join(", ");return{optIn:t,origin:n}}var ln={added_confirmed:"Imported as subscribed",confirmed_pending:"Imported as subscribed",unsubscribed:"Unsubscribed",erased:"Erased",added_unsubscribed:"Recorded as having left",suppressed:"Suppressed",unchanged:"Unchanged",left_out:"Left out",invalid:"Invalid",merged:"Folded into an earlier row",kept_suppressed:"Already suppressed here",kept_unsubscribed:"Kept unsubscribed",kept_confirmed_newer:"Kept confirmed"};function Hc(e,t,n){let s=new Map(e.map(a=>[a.row,a])),i=new Map(t.map(a=>[a.row,a])),r=new Map;for(let a of new Set([...s.keys(),...i.keys()])){let l=s.get(a),d=i.get(a),u=l?ln[l.outcome]:ln.added_confirmed,p=d?ln[d.outcome]:ln.added_confirmed;u!==p&&r.set(a,{row:a,email:(d??l)?.email??"",planned:u,applied:p})}for(let a of n){let l=ln[a.planned],d=ln[a.applied];!r.has(a.row)&&l!==d&&r.set(a.row,{row:a.row,email:a.email,planned:l,applied:d})}return[...r.values()].sort((a,l)=>a.row-l.row)}function Pc(e){return Mi([["row","email","outcome","suppression_added"],...e.map(t=>[String(t.row),t.email,t.outcome,t.suppression_added?"yes":"no"])])}var Ic="Please import my Sendy subscribers from ~/Downloads/sendy-export.csv into the newsletter.",Um="Your list or its scheduled posts changed since this plan, so here is the plan again as things stand now. Review it, then import.",qi=["email","status","opt_in","origin","subscribed_at"],Fm={subscribed:"Subscribed",unsubscribed:"Unsubscribed",bounced:"Bounced"};async function Vi(e,t){ie(t,"Import subscribers"),f(e,o`<div class="imp">
797
798
  <div class="editor-head"><a href="#/subscribers" class="back">← Subscribers</a></div>
798
799
  <div class="page-head"><h1>Import subscribers</h1><p class="set-lede" id="impLede">Nothing changes until you review what will happen and confirm.</p></div>
799
800
  <div id="impBody" class="muted">Loading…</div>
800
801
  <input type="file" id="impInput" accept=".csv,.txt,text/csv,text/plain" hidden>
801
- </div>`);let n=c("#impBody",e),s=c("#impInput",e);We(n);let i;try{i=await L("/api/imports/format",{signal:t})}catch(y){t.aborted||re(n,V(y),()=>{ee(Ni)});return}if(t.aborted)return;let r="full";function a(y){c("#impLede",e).hidden=!1;let w=i.limits.max_rows.toLocaleString("en-US");h(n,o`<section class="card imp-card">
802
+ </div>`);let n=c("#impBody",e),s=c("#impInput",e);je(n);let i;try{i=await C("/api/imports/format",{signal:t})}catch(v){t.aborted||re(n,z(v),()=>{ne(Vi)});return}if(t.aborted)return;let r="full";function a(v){c("#impLede",e).hidden=!1;let w=i.limits.max_rows.toLocaleString("en-US");f(n,o`<section class="card imp-card">
802
803
  <div class="imp-card-head"><h2>The file Kestrel reads</h2>
803
804
  <span class="set-wtog" role="group" aria-label="Which file">
804
805
  <button type="button" class="wtog-btn" data-sample="full" aria-pressed="${String(r==="full")}">Full list</button>
@@ -813,120 +814,120 @@ linus@example.com\r
813
814
  ${u()}
814
815
  </section>
815
816
  <div class="imp-drop" id="impDrop">
816
- ${O("upload")}
817
+ ${B("upload")}
817
818
  <p class="imp-drop-line">Drop your file here, or</p>
818
- <button type="button" class="primary" id="impChoose" aria-describedby="impLimit${y?" impFileError":""}">Choose file</button>
819
+ <button type="button" class="primary" id="impChoose" aria-describedby="impLimit${v?" impFileError":""}">Choose file</button>
819
820
  <p class="field-hint" id="impLimit">CSV or .txt, up to ${w} rows.${i.limits.max_rows_reason?o` ${i.limits.max_rows_reason}`:null}</p>
820
821
  </div>
821
- ${y?o`<div class="imp-file-error"><div class="field-error" id="impFileError" role="alert"><span class="field-error-ico" aria-hidden="true">!</span><span>${y}</span></div>
822
- <p class="imp-text">Exported from another service? Claude can convert it for you. Ask Claude Code:</p>${u()}</div>`:null}`);for(let D of N("[data-sample]",n))D.onclick=()=>{r=D.dataset.sample==="plain"?"plain":"full";for(let R of N("[data-sample]",n))R.setAttribute("aria-pressed",String(R.dataset.sample===r));h(c("#impSample",n),r==="full"?l():d()),m()};m(),c("#impChoose",n).onclick=()=>s.click();let T=c("#impDrop",n);T.addEventListener("dragover",D=>{D.preventDefault(),T.classList.add("over")}),T.addEventListener("dragleave",()=>T.classList.remove("over")),T.addEventListener("drop",D=>{D.preventDefault(),T.classList.remove("over");let R=D.dataTransfer?.files[0];R&&v(R)})}function l(){let y=Ri.slice(0,3),w=i.columns.filter(T=>!T.required&&!Mi.includes(T.name)).map(T=>T.name);return o`<p class="imp-text">A CSV with a header row, one row per address. A row says what your old service recorded about each person; Kestrel decides what that means here.</p>
823
- <div class="table-wrap"><table class="imp-sample"><thead><tr>${Mi.map(T=>o`<th><code>${T}</code></th>`)}</tr></thead>
824
- <tbody>${y.map(T=>o`<tr>${Mi.map(D=>o`<td>${T[D]??""}</td>`)}</tr>`)}</tbody></table></div>
825
- <p class="field-hint">Optional columns: ${w.map((T,D)=>o`${D?", ":""}<code>${T}</code>`)}.</p>
822
+ ${v?o`<div class="imp-file-error"><div class="field-error" id="impFileError" role="alert"><span class="field-error-ico" aria-hidden="true">!</span><span>${v}</span></div>
823
+ <p class="imp-text">Exported from another service? Claude can convert it for you. Ask Claude Code:</p>${u()}</div>`:null}`);for(let j of D("[data-sample]",n))j.onclick=()=>{r=j.dataset.sample==="plain"?"plain":"full";for(let x of D("[data-sample]",n))x.setAttribute("aria-pressed",String(x.dataset.sample===r));f(c("#impSample",n),r==="full"?l():d()),p()};p(),c("#impChoose",n).onclick=()=>s.click();let I=c("#impDrop",n);I.addEventListener("dragover",j=>{j.preventDefault(),I.classList.add("over")}),I.addEventListener("dragleave",()=>I.classList.remove("over")),I.addEventListener("drop",j=>{j.preventDefault(),I.classList.remove("over");let x=j.dataTransfer?.files[0];x&&g(x)})}function l(){let v=Oi.slice(0,3),w=i.columns.filter(I=>!I.required&&!qi.includes(I.name)).map(I=>I.name);return o`<p class="imp-text">A CSV with a header row, one row per address. A row says what your old service recorded about each person; Kestrel decides what that means here.</p>
824
+ <div class="table-wrap"><table class="imp-sample"><thead><tr>${qi.map(I=>o`<th><code>${I}</code></th>`)}</tr></thead>
825
+ <tbody>${v.map(I=>o`<tr>${qi.map(j=>o`<td>${I[j]??""}</td>`)}</tr>`)}</tbody></table></div>
826
+ <p class="field-hint">Optional columns: ${w.map((I,j)=>o`${j?", ":""}<code>${I}</code>`)}.</p>
826
827
  <div class="row imp-links"><button type="button" class="ghost" data-template="csv">Download example file (CSV)</button><a class="ghost-link" href="#/docs/your-list/the-file-format">Import format reference →</a></div>`}function d(){return o`<p class="imp-text">A <code>.txt</code> file with one address per line.</p>
827
- ${Jt(Ai,o`${Ci.replace(/\r\n/g,`
828
- `).trim()}`,Ci)}
828
+ ${en(Bi,o`${Di.replace(/\r\n/g,`
829
+ `).trim()}`,Di)}
829
830
  <p class="imp-text">Next you’ll say what they are: subscribed (the default), unsubscribed, or bounced.</p>
830
- <div class="row imp-links"><button type="button" class="ghost" data-template="txt">Download example file (.txt)</button></div>`}function u(){return Jt("Prompt for Claude Code",o`${$c}`,$c)}function m(){for(let y of N("[data-template]",n))y.onclick=()=>Z(y,null,async()=>{let w=y.dataset.template==="txt"?"txt":"csv";try{let T=await as(`/api/imports/template.${w}`);Rt(T.text,T.filename??(w==="txt"?Ai:fc),w==="txt"?"text/plain":"text/csv")}catch(T){U(V(T))}})}s.onchange=()=>{let y=s.files?.[0];s.value="",y&&v(y)};async function v(y){let w=await y.text();if(t.aborted)return;let T={name:y.name,text:w},D=Rm(T)?i.plain_list.default:null,R=n.querySelector("#impChoose, #impReplace"),F=await(R?Z(R,"Reading\u2026",()=>f(T,D)):f(T,D));F!==null&&(p(F,T),c(".imp-file",n).focus())}async function f(y,w){try{return await L("/api/imports/plan",{method:"POST",json:w===null?{file:y}:{file:y,plain_list_status:w},signal:t})}catch(T){return t.aborted||(a(V(T)),c("#impChoose",n).focus()),null}}function p(y,w,T){let D=vc(y.counts),R=Hi(y.counts),F=yc(y.confirmed),K=Pi(y.counts,y.suppressions_added),b=y.rows.length,E=y.file.kind==="txt"?"Plain list (.txt)":`CSV, ${i.format}`;h(n,o`<section class="card imp-card imp-file" tabindex="-1" aria-label="The file chosen and its plan">
831
+ <div class="row imp-links"><button type="button" class="ghost" data-template="txt">Download example file (.txt)</button></div>`}function u(){return en("Prompt for Claude Code",o`${Ic}`,Ic)}function p(){for(let v of D("[data-template]",n))v.onclick=()=>J(v,null,async()=>{let w=v.dataset.template==="txt"?"txt":"csv";try{let I=await us(`/api/imports/template.${w}`);Ct(I.text,I.filename??(w==="txt"?Bi:_c),w==="txt"?"text/plain":"text/csv")}catch(I){q(z(I))}})}s.onchange=()=>{let v=s.files?.[0];s.value="",v&&g(v)};async function g(v){let w=await v.text();if(t.aborted)return;let I={name:v.name,text:w},j=qm(I)?i.plain_list.default:null,x=n.querySelector("#impChoose, #impReplace"),O=await(x?J(x,"Reading\u2026",()=>h(I,j)):h(I,j));O!==null&&(m(O,I),c(".imp-file",n).focus())}async function h(v,w){try{return await C("/api/imports/plan",{method:"POST",json:w===null?{file:v}:{file:v,plain_list_status:w},signal:t})}catch(I){return t.aborted||(a(z(I)),c("#impChoose",n).focus()),null}}function m(v,w,I){let j=Cc(v.counts),x=Ui(v.counts),O=Ac(v.confirmed),V=Fi(v.counts,v.suppressions_added),S=v.rows.length,T=v.file.kind==="txt"?"Plain list (.txt)":`CSV, ${i.format}`;f(n,o`<section class="card imp-card imp-file" tabindex="-1" aria-label="The file chosen and its plan">
831
832
  <div class="imp-file-row">
832
- <div><strong class="imp-file-name">${y.file.name}</strong><div class="muted">${E} · ${Ii(y.file.rows)} · ${Cm(y.file.addresses)}</div></div>
833
+ <div><strong class="imp-file-name">${v.file.name}</strong><div class="muted">${T} · ${ji(v.file.rows)} · ${jm(v.file.addresses)}</div></div>
833
834
  <button type="button" class="ghost" id="impReplace">Replace file</button>
834
835
  </div>
835
- ${y.file.kind==="txt"?S(y.file.plain_list_status??i.plain_list.default):null}
836
+ ${v.file.kind==="txt"?y(v.file.plain_list_status??i.plain_list.default):null}
836
837
  </section>
837
838
  <section class="card imp-card">
838
839
  <h2>What will happen</h2>
839
- ${Ec(R,F,D)}
840
- ${K?o`<p class="imp-text">${K}</p>`:null}
841
- ${b>0?o`<div class="row imp-links"><button type="button" class="ghost" id="impNotImported">Download the ${Ii(b)} not imported as subscribed (CSV)</button></div>`:null}
842
- ${y.workers_plan_advice?o`<p class="workers-plan" role="status">${y.workers_plan_advice}</p>`:null}
840
+ ${Nc(x,O,j)}
841
+ ${V?o`<p class="imp-text">${V}</p>`:null}
842
+ ${S>0?o`<div class="row imp-links"><button type="button" class="ghost" id="impNotImported">Download the ${ji(S)} not imported as subscribed (CSV)</button></div>`:null}
843
+ ${v.workers_plan_advice?o`<p class="workers-plan" role="status">${v.workers_plan_advice}</p>`:null}
843
844
  </section>
844
845
  <section class="card imp-card imp-confirm">
845
846
  <h2>Confirm</h2>
846
- ${y.sends_growing.length>0?o`<p class="imp-text">A scheduled post goes to the list as it stands when it sends, so this import adds to its audience. Acknowledge each one.</p>
847
- ${y.sends_growing.map(k=>o`<label class="imp-check"><input type="checkbox" data-ack="${k.id}"><span>“${k.subject||"Untitled"}”, sending ${j(k.fire_at)}, goes to ${k.gains.toLocaleString("en-US")} more ${k.gains===1?"person":"people"}.</span></label>`)}`:null}
848
- ${y.attestation?o`<label class="imp-check"><input type="checkbox" id="impAttest"><span>${y.attestation.text}</span></label>
849
- <p class="field-hint imp-attest-hint">Only import people who asked to receive ${y.attestation.publication}. If you’re unsure about some, take them out of the file before importing.</p>`:null}
850
- ${y.attestation===null&&y.sends_growing.length===0?o`<p class="imp-text">Nobody is imported as a subscriber, so there’s nothing to attest.</p>`:null}
851
- ${T?xc(T):null}
847
+ ${v.sends_growing.length>0?o`<p class="imp-text">A scheduled post goes to the list as it stands when it sends, so this import adds to its audience. Acknowledge each one.</p>
848
+ ${v.sends_growing.map(k=>o`<label class="imp-check"><input type="checkbox" data-ack="${k.id}"><span>“${k.subject||"Untitled"}”, sending ${W(k.fire_at)}, goes to ${k.gains.toLocaleString("en-US")} more ${k.gains===1?"person":"people"}.</span></label>`)}`:null}
849
+ ${v.attestation?o`<label class="imp-check"><input type="checkbox" id="impAttest"><span>${v.attestation.text}</span></label>
850
+ <p class="field-hint imp-attest-hint">Only import people who asked to receive ${v.attestation.publication}. If you’re unsure about some, take them out of the file before importing.</p>`:null}
851
+ ${v.attestation===null&&v.sends_growing.length===0?o`<p class="imp-text">Nobody is imported as a subscriber, so there’s nothing to attest.</p>`:null}
852
+ ${I?Mc(I):null}
852
853
  <div class="actions imp-actions"><a class="ghost-link" href="#/subscribers">Cancel</a><button type="button" class="primary" id="impGo">Import</button></div>
853
- </section>`);let C=c("#impReplace",n);C.onclick=()=>s.click(),b>0&&(c("#impNotImported",n).onclick=()=>$(y.file.name,y.rows));let x=c("#impGo",n),W=N("[data-plain]",n),oe=k=>{C.disabled=k;for(let G of W)G.disabled=k};for(let k of W)k.onclick=()=>{let G=k.dataset.plain;G!==y.file.plain_list_status&&(x.disabled=!0,oe(!0),(async()=>{let q=await f(w,G);q&&!t.aborted&&(p(q,w),c(`[data-plain="${G}"]`,n).focus())})())};x.onclick=()=>M(y,w,x,oe)}function S(y){return o`<div class="imp-plain">
854
+ </section>`);let H=c("#impReplace",n);H.onclick=()=>s.click(),S>0&&(c("#impNotImported",n).onclick=()=>E(v.file.name,v.rows));let $=c("#impGo",n),L=D("[data-plain]",n),G=k=>{H.disabled=k;for(let K of L)K.disabled=k};for(let k of L)k.onclick=()=>{let K=k.dataset.plain;K!==v.file.plain_list_status&&($.disabled=!0,G(!0),(async()=>{let F=await h(w,K);F&&!t.aborted&&(m(F,w),c(`[data-plain="${K}"]`,n).focus())})())};$.onclick=()=>R(v,w,$,G)}function y(v){return o`<div class="imp-plain">
854
855
  <span class="imp-plain-label" id="impPlainLabel">These addresses are</span>
855
- <span class="set-wtog" role="group" aria-labelledby="impPlainLabel">${pc.map(w=>o`<button type="button" class="wtog-btn" data-plain="${w}" aria-pressed="${String(w===y)}">${Lm[w]}</button>`)}</span>
856
+ <span class="set-wtog" role="group" aria-labelledby="impPlainLabel">${xc.map(w=>o`<button type="button" class="wtog-btn" data-plain="${w}" aria-pressed="${String(w===v)}">${Fm[w]}</button>`)}</span>
856
857
  <p class="field-hint">A plain list doesn’t say how each person joined, so it’s recorded as unknown.</p>
857
- </div>`}function $(y,w){let T=y.replace(/\.(csv|txt)$/i,"");Rt(Sc(w),`${T}-not-imported.csv`,"text/csv")}async function M(y,w,T,D){n.querySelector(".imp-act-error")?.remove();let R=N("[data-ack]",n),F=n.querySelector("#impAttest");for(let K of R)if(!K.checked){P(K,"Tick this to acknowledge that this post will go to the people this import adds.");return}if(F&&!F.checked){P(F,"Tick this to attest that everyone you\u2019re importing as subscribed opted in.");return}D(!0),await Z(T,"Importing\u2026",async()=>{try{let K=await L("/api/imports",{method:"POST",json:{file:w,plain_list_status:y.file.plain_list_status??void 0,plan_hash:y.plan_hash,...y.attestation?{attestation:{version:y.attestation.version,accepted:!0}}:{},acknowledge_sends:y.sends_growing.map(b=>b.id)}});t.aborted||H(K,y)}catch(K){if(t.aborted)return;if(K instanceof te&&K.code==="plan_stale"){let b=await f(w,y.file.plain_list_status);b&&!t.aborted&&(p(b,w,Tm),c(".imp-act-error",n).focus());return}A(V(K)),D(!1)}})}function P(y,w){qe(y,w,{after:y.closest("label")??y}),y.focus()}function A(y){n.querySelector(".imp-act-error")?.remove();let w=document.createElement("div");h(w,xc(y)),c(".imp-actions",n).before(...w.childNodes)}function H(y,w){c("#impLede",e).hidden=!0;let T=y.import,D=so(T),R=Hi(y.counts),F=Pi(y.counts,y.suppressions_added),K=y.replayed?[]:wc(w.rows,y.rows,y.differed);h(n,o`<section class="card imp-card imp-done">
858
- <h2 id="impDoneHead" tabindex="-1">${y.replayed?"This import was already done":"Import done"}</h2>
859
- ${y.replayed?o`<p class="imp-text">This plan was already imported, so nothing was written again. Here is its record.</p>`:null}
860
- ${Ec(R,null,0)}
861
- ${F?o`<p class="imp-text">${F}</p>`:null}
858
+ </div>`}function E(v,w){let I=v.replace(/\.(csv|txt)$/i,"");Ct(Pc(w),`${I}-not-imported.csv`,"text/csv")}async function R(v,w,I,j){n.querySelector(".imp-act-error")?.remove();let x=D("[data-ack]",n),O=n.querySelector("#impAttest");for(let V of x)if(!V.checked){N(V,"Tick this to acknowledge that this post will go to the people this import adds.");return}if(O&&!O.checked){N(O,"Tick this to attest that everyone you\u2019re importing as subscribed opted in.");return}j(!0),await J(I,"Importing\u2026",async()=>{try{let V=await C("/api/imports",{method:"POST",json:{file:w,plain_list_status:v.file.plain_list_status??void 0,plan_hash:v.plan_hash,...v.attestation?{attestation:{version:v.attestation.version,accepted:!0}}:{},acknowledge_sends:v.sends_growing.map(S=>S.id)}});t.aborted||P(V,v)}catch(V){if(t.aborted)return;if(V instanceof se&&V.code==="plan_stale"){let S=await h(w,v.file.plain_list_status);S&&!t.aborted&&(m(S,w,Um),c(".imp-act-error",n).focus());return}A(z(V)),j(!1)}})}function N(v,w){Fe(v,w,{after:v.closest("label")??v}),v.focus()}function A(v){n.querySelector(".imp-act-error")?.remove();let w=document.createElement("div");f(w,Mc(v)),c(".imp-actions",n).before(...w.childNodes)}function P(v,w){c("#impLede",e).hidden=!0;let I=v.import,j=uo(I),x=Ui(v.counts),O=Fi(v.counts,v.suppressions_added),V=v.replayed?[]:Hc(w.rows,v.rows,v.differed);f(n,o`<section class="card imp-card imp-done">
859
+ <h2 id="impDoneHead" tabindex="-1">${v.replayed?"This import was already done":"Import done"}</h2>
860
+ ${v.replayed?o`<p class="imp-text">This plan was already imported, so nothing was written again. Here is its record.</p>`:null}
861
+ ${Nc(x,null,0)}
862
+ ${O?o`<p class="imp-text">${O}</p>`:null}
862
863
  <p class="imp-text imp-nobody">Nobody was emailed. To tell your readers about the move, write a post as usual.</p>
863
864
  </section>
864
- ${K.length>0?o`<section class="card imp-card imp-differed">
865
+ ${V.length>0?o`<section class="card imp-card imp-differed">
865
866
  <h2>Rows that turned out differently</h2>
866
- <p class="imp-text">Your list changed between the plan and the import, so ${Ii(K.length)} had a different result. Each was applied to the list as it stood.</p>
867
- <div class="table-wrap"><table class="imp-ref"><thead><tr><th>Row</th><th>Email</th><th>Planned</th><th>Applied</th></tr></thead><tbody>${K.map(b=>o`<tr><td>${String(b.row)}</td><td>${b.email}</td><td>${b.planned}</td><td>${b.applied}</td></tr>`)}</tbody></table></div>
867
+ <p class="imp-text">Your list changed between the plan and the import, so ${ji(V.length)} had a different result. Each was applied to the list as it stood.</p>
868
+ <div class="table-wrap"><table class="imp-ref"><thead><tr><th>Row</th><th>Email</th><th>Planned</th><th>Applied</th></tr></thead><tbody>${V.map(S=>o`<tr><td>${String(S.row)}</td><td>${S.email}</td><td>${S.planned}</td><td>${S.applied}</td></tr>`)}</tbody></table></div>
868
869
  </section>`:null}
869
870
  <section class="card imp-card imp-record">
870
871
  <h2>The import’s record</h2>
871
872
  <dl class="imp-dl">
872
- <dt>File</dt><dd>${T.file_name}</dd>
873
- <dt>Fingerprint</dt><dd><code>${T.file_sha256.slice(0,16)}</code></dd>
874
- <dt>Imported</dt><dd>${j(T.created_at)} by ${D}</dd>
875
- <dt>Attestation</dt><dd>${T.attestation_version===null?"None needed: nobody was imported as a subscriber.":w.attestation?o`“${w.attestation.text}” <span class="muted">(version ${String(T.attestation_version)})</span>`:`Version ${T.attestation_version}`}</dd>
873
+ <dt>File</dt><dd>${I.file_name}</dd>
874
+ <dt>Fingerprint</dt><dd><code>${I.file_sha256.slice(0,16)}</code></dd>
875
+ <dt>Imported</dt><dd>${W(I.created_at)} by ${j}</dd>
876
+ <dt>Attestation</dt><dd>${I.attestation_version===null?"None needed: nobody was imported as a subscriber.":w.attestation?o`“${w.attestation.text}” <span class="muted">(version ${String(I.attestation_version)})</span>`:`Version ${I.attestation_version}`}</dd>
876
877
  </dl>
877
878
  <div class="actions imp-actions"><button type="button" class="ghost" id="impAnother">Import another file</button><button type="button" class="primary" id="impToList">See your subscribers</button></div>
878
- </section>`),c("#impAnother",n).onclick=()=>{a(),c("#impChoose",n).focus()},c("#impToList",n).onclick=()=>{location.hash="#/subscribers"},c("#impDoneHead",n).focus()}a()}function xc(e){return o`<div class="field-error imp-act-error" role="alert" tabindex="-1"><span class="field-error-ico" aria-hidden="true">!</span><span>${e}</span></div>`}function Rm(e){return e.name.trim().toLowerCase().endsWith(".txt")}function Ii(e){return`${e.toLocaleString("en-US")} ${e===1?"row":"rows"}`}function Cm(e){return`${e.toLocaleString("en-US")} ${e===1?"address":"addresses"}`}function Ec(e,t,n){let s=e.filter(a=>!a.kept),i=e.filter(a=>a.kept),r=s.find(a=>a.outcome==="added_confirmed"||a.outcome==="confirmed_pending")?.outcome;return o`<ul class="imp-outcomes">${s.map(a=>o`<li data-outcome="${a.outcome}"><span>${a.text}</span>${a.outcome===r&&t&&n>0?o`<div class="field-hint imp-breakdown">Of the ${n.toLocaleString("en-US")} confirmed, by opt-in: ${t.optIn}. From: ${t.origin}.</div>`:null}</li>`)}</ul>
879
- ${i.length>0?o`<p class="imp-kept-head">Kept as they are here</p><ul class="imp-outcomes imp-kept">${i.map(a=>o`<li data-outcome="${a.outcome}"><span>${a.text}</span></li>`)}</ul>`:null}`}function Am(e){return e.suppression&&je[e.suppression.reason]||"suppressed"}function oo(e){return e.suppression?o` <span class="badge suppressed row-flag">${Am(e)}</span>`:null}function io(e){return e.suppression?o`<span class="badge ${e.status} held">${e.status}</span>`:Xe(e.status)}function kc(e){let t=e.lastIndexOf("@");return t>0?o`${e.slice(0,t)}<wbr>${e.slice(t)}`:o`${e}`}function Kn(e){return e.status==="unsubscribed"&&e.unsubscribe_cause==="erased"}function ro(e){return`#/subscribers/${e}`}function ao(e,t,n){let s=[];return e.status!=="unsubscribed"&&s.push({label:ct.unsubscribe.label,danger:!0,onClick:()=>ya(e,t,n)}),e.suppression?e.suppression.reason==="manual"?s.push({label:ct.unblock.label,onClick:()=>Sa(e,t,n)}):je[e.suppression.reason]==="bounced"&&s.push({label:ct.clear.label,danger:!0,onClick:()=>xa(e,t,n)}):s.push({label:ct.block.label,danger:!0,onClick:()=>wa(e,t,n)}),Kn(e)||s.push({label:ct.erase.label,danger:!0,onClick:()=>Ea(e,t,n)}),s}async function Hm(){let e=[],t=null,n=Date.now();do{let s=t===null?"":`?after=${encodeURIComponent(t)}`,i=await L(`/api/subscribers/export${s}`);if(i.next!==null&&i.next===t)throw new Error("The export repeated a page. Try again.");e.push(...i.subscribers),n=i.exported_at,t=i.next}while(t!==null);return{rows:e,exportedAt:n}}async function _c(){let{rows:e,exportedAt:t}=await Hm();return{name:`kestrel-subscribers-${new Date(t).toISOString().slice(0,10)}.csv`,text:Li(e)}}var Tc=[{value:"confirmed",label:"Confirmed"},{value:"pending",label:"Pending"},{value:"unsubscribed",label:"Unsubscribed"}],Bi=["confirmed","pending","unsubscribed","suppressed"];async function Lc(e,t,n){ie(n,"Subscribers");let s={status:"",search:"",suppressed:"",sort:"created_at",dir:"desc",offset:0,limit:50};e==="suppressed"?s.suppressed="only":e&&Bi.includes(e)&&(s.status=e),h(t,o`
879
+ </section>`),c("#impAnother",n).onclick=()=>{a(),c("#impChoose",n).focus()},c("#impToList",n).onclick=()=>{location.hash="#/subscribers"},c("#impDoneHead",n).focus()}a()}function Mc(e){return o`<div class="field-error imp-act-error" role="alert" tabindex="-1"><span class="field-error-ico" aria-hidden="true">!</span><span>${e}</span></div>`}function qm(e){return e.name.trim().toLowerCase().endsWith(".txt")}function ji(e){return`${e.toLocaleString("en-US")} ${e===1?"row":"rows"}`}function jm(e){return`${e.toLocaleString("en-US")} ${e===1?"address":"addresses"}`}function Nc(e,t,n){let s=e.filter(a=>!a.kept),i=e.filter(a=>a.kept),r=s.find(a=>a.outcome==="added_confirmed"||a.outcome==="confirmed_pending")?.outcome;return o`<ul class="imp-outcomes">${s.map(a=>o`<li data-outcome="${a.outcome}"><span>${a.text}</span>${a.outcome===r&&t&&n>0?o`<div class="field-hint imp-breakdown">Of the ${n.toLocaleString("en-US")} confirmed, by opt-in: ${t.optIn}. From: ${t.origin}.</div>`:null}</li>`)}</ul>
880
+ ${i.length>0?o`<p class="imp-kept-head">Kept as they are here</p><ul class="imp-outcomes imp-kept">${i.map(a=>o`<li data-outcome="${a.outcome}"><span>${a.text}</span></li>`)}</ul>`:null}`}function Vm(e){return e.suppression&&qe[e.suppression.reason]||"suppressed"}function po(e){return e.suppression?o` <span class="badge suppressed row-flag">${Vm(e)}</span>`:null}function mo(e){return e.suppression?o`<span class="badge ${e.status} held">${e.status}</span>`:Qe(e.status)}function Oc(e){let t=e.lastIndexOf("@");return t>0?o`${e.slice(0,t)}<wbr>${e.slice(t)}`:o`${e}`}function Zn(e){return e.status==="unsubscribed"&&e.unsubscribe_cause==="erased"}function fo(e){return`#/subscribers/${e}`}function ho(e,t,n){let s=[];return e.status!=="unsubscribed"&&s.push({label:pt.unsubscribe.label,danger:!0,onClick:()=>Aa(e,t,n)}),e.suppression?e.suppression.reason==="manual"?s.push({label:pt.unblock.label,onClick:()=>Pa(e,t,n)}):qe[e.suppression.reason]==="bounced"&&s.push({label:pt.clear.label,danger:!0,onClick:()=>Ma(e,t,n)}):s.push({label:pt.block.label,danger:!0,onClick:()=>Ha(e,t,n)}),Zn(e)||s.push({label:pt.erase.label,danger:!0,onClick:()=>Na(e,t,n)}),s}async function Wm(){let e=[],t=null,n=Date.now();do{let s=t===null?"":`?after=${encodeURIComponent(t)}`,i=await C(`/api/subscribers/export${s}`);if(i.next!==null&&i.next===t)throw new Error("The export repeated a page. Try again.");e.push(...i.subscribers),n=i.exported_at,t=i.next}while(t!==null);return{rows:e,exportedAt:n}}async function Dc(){let{rows:e,exportedAt:t}=await Wm();return{name:`kestrel-subscribers-${new Date(t).toISOString().slice(0,10)}.csv`,text:Ni(e)}}var Bc=[{value:"confirmed",label:"Confirmed"},{value:"pending",label:"Pending"},{value:"unsubscribed",label:"Unsubscribed"}],Gi=["confirmed","pending","unsubscribed","suppressed"];async function Uc(e,t,n){ie(n,"Subscribers");let s={status:"",search:"",suppressed:"",sort:"created_at",dir:"desc",offset:0,limit:50};e==="suppressed"?s.suppressed="only":e&&Gi.includes(e)&&(s.status=e),f(t,o`
880
881
  <div class="spread page-head"><h1>Subscribers</h1><span class="row"><button type="button" class="ghost" id="importSubs">Import</button><button type="button" class="ghost" id="exportSubs">Export</button><button class="primary" id="addSub">Add subscriber</button></span></div>
881
882
  <div class="health red" id="subSignupsPaused" role="status" hidden></div>
882
- <div class="card sub-summary"><div class="sub-summary-head"><div id="subCounts" class="row muted" role="group" aria-label="Show">Loading…</div><button type="button" class="sub-legend-toggle" id="subLegendToggle" aria-expanded="false" aria-controls="subLegend">Legend</button></div><p class="workers-plan" id="subWorkersPlan" role="status" hidden></p>${Nm()}</div>
883
- ${Kt({statuses:Tc,suppressible:!0,searchPlaceholder:"Search email\u2026"})}
883
+ <div class="card sub-summary"><div class="sub-summary-head"><div id="subCounts" class="row muted" role="group" aria-label="Show">Loading…</div><button type="button" class="sub-legend-toggle" id="subLegendToggle" aria-expanded="false" aria-controls="subLegend">Legend</button></div><p class="workers-plan" id="subWorkersPlan" role="status" hidden></p>${Ym()}</div>
884
+ ${Yt({statuses:Bc,suppressible:!0,searchPlaceholder:"Search email\u2026"})}
884
885
  <div id="subList" class="muted">Loading…</div>
885
- <div id="subPager"></div>`);let i=c("#subList"),r=c("#subPager"),a=at(Y.appConfig?.deployment,n);async function l(){try{let[m,v]=await Promise.all([L(`/api/subscribers?${Vt(s)}`,{signal:n}),a]),f=m.counts;Mm(c("#subCounts"),f,s);let p=c("#subWorkersPlan");p.hidden=!m.workers_plan_advice,p.textContent=m.workers_plan_advice??"";let S=f.pending+f.confirmed+f.unsubscribed===0;Om(i,m.subscribers,s,l,n,S,lt(v));let $=c("#subSignupsPaused");$.hidden=!En(v),h($,En(v)?o`<span class="health-dot">⚠️</span><div>${ha()}</div>`:o``),rt(r,s,m.page,l)}catch(m){re(i,V(m),l)}}Im(t,s,l);let d=c("#subLegendToggle");d.onclick=()=>{let m=d.getAttribute("aria-expanded")!=="true";d.setAttribute("aria-expanded",String(m)),c("#subLegend").hidden=!m},c("#addSub").onclick=()=>Ms(l,n),c("#importSubs").onclick=()=>{location.hash="#/subscribers/import"};let u=c("#exportSubs");u.onclick=()=>Z(u,"Exporting\u2026",async()=>{try{let m=await _c();Rt(m.text,m.name,"text/csv")}catch(m){U(V(m))}}),Gt(t,s,l),l()}var Pm=["confirmed","pending","unsubscribed","suppressed"];function Di(e,t){return e.search?!1:t==="suppressed"?e.suppressed==="only"&&!e.status:e.status===t&&!e.suppressed}function Mm(e,t,n){e.querySelector("[data-filter]")||h(e,o`${Pm.map(s=>o`<button type="button" class="sub-count" data-filter="${s}" aria-pressed="false"></button>`)}`);for(let s of N("[data-filter]",e)){let i=s.dataset.filter;h(s,o`${i==="confirmed"?o`<strong>${t[i]}</strong>`:t[i]} ${i}`),s.setAttribute("aria-pressed",String(Di(n,i)))}}function Im(e,t,n){let s=c("#subCounts",e);s.onclick=i=>{let r=i.target instanceof Element?i.target.closest("[data-filter]"):null;if(!r)return;let a=r.dataset.filter,l=!Di(t,a);t.status=l&&a!=="suppressed"?a:"",t.suppressed=l&&a==="suppressed"?"only":"",t.search="",t.offset=0,c(".lt-search",e).value="",c(".lt-status",e).value=t.status,c(".lt-suppressed",e).checked=t.suppressed==="only";for(let d of N("[data-filter]",s))d.setAttribute("aria-pressed",String(Di(t,d.dataset.filter)));n()}}function Oi(e,t){return o`<dt>${e}</dt><dd>${t}</dd>`}function Nm(){let e=Object.keys(kn);return o`<div class="sub-legend" id="subLegend" role="region" aria-labelledby="subLegendToggle" hidden><div class="sub-legend-body">
886
+ <div id="subPager"></div>`);let i=c("#subList"),r=c("#subPager"),a=dt(Z.appConfig?.deployment,n);async function l(){try{let[p,g]=await Promise.all([C(`/api/subscribers?${zt(s)}`,{signal:n}),a]),h=p.counts;Gm(c("#subCounts"),h,s);let m=c("#subWorkersPlan");m.hidden=!p.workers_plan_advice,m.textContent=p.workers_plan_advice??"";let y=h.pending+h.confirmed+h.unsubscribed===0;Zm(i,p.subscribers,s,l,n,y,ut(g));let E=c("#subSignupsPaused");E.hidden=!Ln(g),f(E,Ln(g)?o`<span class="health-dot">⚠️</span><div>${Ta()}</div>`:o``),ct(r,s,p.page,l)}catch(p){re(i,z(p),l)}}zm(t,s,l);let d=c("#subLegendToggle");d.onclick=()=>{let p=d.getAttribute("aria-expanded")!=="true";d.setAttribute("aria-expanded",String(p)),c("#subLegend").hidden=!p},c("#addSub").onclick=()=>Us(l,n),c("#importSubs").onclick=()=>{location.hash="#/subscribers/import"};let u=c("#exportSubs");u.onclick=()=>J(u,"Exporting\u2026",async()=>{try{let p=await Dc();Ct(p.text,p.name,"text/csv")}catch(p){q(z(p))}}),Zt(t,s,l),l()}var Km=["confirmed","pending","unsubscribed","suppressed"];function Ki(e,t){return e.search?!1:t==="suppressed"?e.suppressed==="only"&&!e.status:e.status===t&&!e.suppressed}function Gm(e,t,n){e.querySelector("[data-filter]")||f(e,o`${Km.map(s=>o`<button type="button" class="sub-count" data-filter="${s}" aria-pressed="false"></button>`)}`);for(let s of D("[data-filter]",e)){let i=s.dataset.filter;f(s,o`${i==="confirmed"?o`<strong>${t[i]}</strong>`:t[i]} ${i}`),s.setAttribute("aria-pressed",String(Ki(n,i)))}}function zm(e,t,n){let s=c("#subCounts",e);s.onclick=i=>{let r=i.target instanceof Element?i.target.closest("[data-filter]"):null;if(!r)return;let a=r.dataset.filter,l=!Ki(t,a);t.status=l&&a!=="suppressed"?a:"",t.suppressed=l&&a==="suppressed"?"only":"",t.search="",t.offset=0,c(".lt-search",e).value="",c(".lt-status",e).value=t.status,c(".lt-suppressed",e).checked=t.suppressed==="only";for(let d of D("[data-filter]",s))d.setAttribute("aria-pressed",String(Ki(t,d.dataset.filter)));n()}}function Wi(e,t){return o`<dt>${e}</dt><dd>${t}</dd>`}function Ym(){let e=Object.keys(Rn);return o`<div class="sub-legend" id="subLegend" role="region" aria-labelledby="subLegendToggle" hidden><div class="sub-legend-body">
886
887
  <h2>Status</h2>
887
- <dl>${Tc.map(({value:t})=>Oi(Xe(t),t==="unsubscribed"?`${Wo[t]} Why they left is on their page.`:Wo[t]))}</dl>
888
+ <dl>${Bc.map(({value:t})=>Wi(Qe(t),t==="unsubscribed"?`${Qo[t]} Why they left is on their page.`:Qo[t]))}</dl>
888
889
  <h2>Suppressed</h2>
889
- <p>${va} The red flag beside the address says why:</p>
890
- <dl>${e.map(t=>Oi(o`<span class="badge suppressed">${t}</span>`,kn[t]))}</dl>
890
+ <p>${Ca} The red flag beside the address says why:</p>
891
+ <dl>${e.map(t=>Wi(o`<span class="badge suppressed">${t}</span>`,Rn[t]))}</dl>
891
892
  <h2>What you can do</h2>
892
- <dl>${Object.values(ct).map(({label:t,when:n})=>Oi(o`<strong class="sub-legend-act">${t}</strong>`,n))}</dl>
893
- </div></div>`}function Om(e,t,n,s,i,r,a){if(!t.length){let l=o`<a href="#/subscribers/import">Import your list</a>`;h(e,r?a?o`<p class="muted">No subscribers yet. ${l} from another service. Once AWS grants production access, share your subscribe page. <a class="set-link" href="${kt}">Request production access</a></p>`:o`<p class="muted">No subscribers yet. ${l} from another service, or share your subscribe page.</p>`:o`<p class="muted">No subscribers match.</p>`);return}h(e,o`<div class="table-wrap subs-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>${Ee("Email","email",n)}${Ee("Status",null,n)}${Ee("Joined","created_at",n)}<th></th></tr></thead><tbody>${t.map(l=>o`<tr class="clickable" data-id="${l.id}"><td><a class="sub-email" href="${ro(l.id)}">${kc(l.email)}</a>${oo(l)}</td><td>${io(l)}</td><td class="muted joined">${j(l.created_at)}</td><td class="act"><button class="icon" data-menu="${l.id}" aria-label="Subscriber actions" aria-haspopup="menu">⋯</button></td></tr>`)}</tbody></table></div>`),it(e,n,s);for(let l of N("tr[data-id]",e))l.onclick=d=>{d.target instanceof Element&&d.target.closest("a, button")||(location.hash=ro(l.dataset.id??""))};for(let l of N("[data-menu]",e))l.onclick=d=>{d.stopPropagation();let u=t.find(v=>v.id===l.dataset.menu);if(!u)return;let m=[{label:"View subscriber",onClick:()=>{location.hash=ro(u.id)}},...ao(u,s,i)];Yt(l,m,i)}}var lo=o`<div class="editor-head"><a href="#/subscribers" class="back">← Subscribers</a></div>`;async function Rc(e,t,n){ie(n,"Subscriber"),h(t,o`${lo}<p class="muted">Loading…</p>`);let s=`/api/subscribers/${encodeURIComponent(e)}`;async function i(a){let l,d,u;try{let m=L(s,{signal:n}),v=m.then(({subscriber:$})=>$.import_id===null?null:L(`/api/imports/${encodeURIComponent($.import_id)}`,{signal:n}).then(M=>M.import,()=>null)),[f,p,S]=await Promise.all([m,L(`${s}/deliveries`,{signal:n}),v]);l=f.subscriber,d=p,u=S}catch(m){let v=document.createElement("div");h(t,lo),t.append(v),re(v,V(m),()=>{h(t,o`${lo}<p class="muted">Loading…</p>`),i(!1)});return}n.aborted||(r(l,d,u),a&&c("h1.sub-title",t).focus())}function r(a,l,d){let u=ao(a,()=>{i(!0)},n),m=Kn(a);h(t,o`${lo}
893
+ <dl>${Object.values(pt).map(({label:t,when:n})=>Wi(o`<strong class="sub-legend-act">${t}</strong>`,n))}</dl>
894
+ </div></div>`}function Zm(e,t,n,s,i,r,a){if(!t.length){let l=o`<a href="#/subscribers/import">Import your list</a>`;f(e,r?a?o`<p class="muted">No subscribers yet. ${l} from another service. Once AWS grants production access, share your subscribe page. <a class="set-link" href="${_t}">Request production access</a></p>`:o`<p class="muted">No subscribers yet. ${l} from another service, or share your subscribe page.</p>`:o`<p class="muted">No subscribers match.</p>`);return}f(e,o`<div class="table-wrap subs-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>${xe("Email","email",n)}${xe("Status",null,n)}${xe("Joined","created_at",n)}<th></th></tr></thead><tbody>${t.map(l=>o`<tr class="clickable" data-id="${l.id}"><td><a class="sub-email" href="${fo(l.id)}">${Oc(l.email)}</a>${po(l)}</td><td>${mo(l)}</td><td class="muted joined">${W(l.created_at)}</td><td class="act"><button class="icon" data-menu="${l.id}" aria-label="Subscriber actions" aria-haspopup="menu">⋯</button></td></tr>`)}</tbody></table></div>`),lt(e,n,s);for(let l of D("tr[data-id]",e))l.onclick=d=>{d.target instanceof Element&&d.target.closest("a, button")||(location.hash=fo(l.dataset.id??""))};for(let l of D("[data-menu]",e))l.onclick=d=>{d.stopPropagation();let u=t.find(g=>g.id===l.dataset.menu);if(!u)return;let p=[{label:"View subscriber",onClick:()=>{location.hash=fo(u.id)}},...ho(u,s,i)];Qt(l,p,i)}}var bo=o`<div class="editor-head"><a href="#/subscribers" class="back">← Subscribers</a></div>`;async function Fc(e,t,n){ie(n,"Subscriber"),f(t,o`${bo}<p class="muted">Loading…</p>`);let s=`/api/subscribers/${encodeURIComponent(e)}`;async function i(a){let l,d,u;try{let p=C(s,{signal:n}),g=p.then(({subscriber:E})=>E.import_id===null?null:C(`/api/imports/${encodeURIComponent(E.import_id)}`,{signal:n}).then(R=>R.import,()=>null)),[h,m,y]=await Promise.all([p,C(`${s}/deliveries`,{signal:n}),g]);l=h.subscriber,d=m,u=y}catch(p){let g=document.createElement("div");f(t,bo),t.append(g),re(g,z(p),()=>{f(t,o`${bo}<p class="muted">Loading…</p>`),i(!1)});return}n.aborted||(r(l,d,u),a&&c("h1.sub-title",t).focus())}function r(a,l,d){let u=ho(a,()=>{i(!0)},n),p=Zn(a);f(t,o`${bo}
894
895
  <div class="spread page-head sub-page-head">
895
- <div class="sub-page-title"><h1 class="sub-title" tabindex="-1">${a.email}</h1><span class="sub-pills">${io(a)}${oo(a)}</span></div>
896
- <span class="row sub-acts">${u.map((p,S)=>o`<button type="button" class="${p.danger?"danger-subtle":""}" data-act="${S}">${p.label}…</button>`)}</span>
896
+ <div class="sub-page-title"><h1 class="sub-title" tabindex="-1">${a.email}</h1><span class="sub-pills">${mo(a)}${po(a)}</span></div>
897
+ <span class="row sub-acts">${u.map((m,y)=>o`<button type="button" class="${m.danger?"danger-subtle":""}" data-act="${y}">${m.label}…</button>`)}</span>
897
898
  </div>
898
- <p class="sub-receives${a.receives_posts?" yes":""}">${Dm(a)}</p>
899
- ${m?null:Bm(a,d)}
900
- ${Um(a)}
901
- ${a.unsubscribe_cause?jm(a):null}
899
+ <p class="sub-receives${a.receives_posts?" yes":""}">${Jm(a)}</p>
900
+ ${p?null:Qm(a,d)}
901
+ ${Xm(a)}
902
+ ${a.unsubscribe_cause?nf(a):null}
902
903
  <section class="sub-sec sub-sends" aria-labelledby="subSendsTitle">
903
904
  <h2 class="rec-part-title" id="subSendsTitle">Sends</h2>
904
905
  <div id="subSends"></div>
905
906
  <div id="subMore"></div>
906
- </section>`);for(let p of N("[data-act]",t))p.onclick=()=>u[Number(p.dataset.act)]?.onClick();let v=t.querySelector("#subComplaint");Wm(t,s,l,m,n,p=>{let S=p.find($=>$.delivery.event==="complained");v?.hidden&&S&&(h(v,o`The report was about <a href="#/sent/${S.send.id}">${S.send.subject||"an untitled post"}</a>.`),v.hidden=!1)})}await i(!1)}function Dm(e){if(e.receives_posts)return"Receives your posts.";let t=[];return e.status==="pending"?t.push("they haven\u2019t confirmed"):e.status==="unsubscribed"&&t.push(`they left the list on ${j(e.unsubscribed_at)}`),e.suppression&&t.push(je[e.suppression.reason]==="bounced"?"the address bounced":"the address is blocked"),t.length>0?`Not sent your posts: ${t.join(", and ")}.`:"Not sent your posts."}function Ve(e,t){return o`<dt>${e}</dt><dd>${t}</dd>`}function Bm(e,t){let n=bc(e.consent_evidence),s=hc(e)??(e.status==="pending"?"Signed up here, not confirmed yet":"Signed up here, never confirmed");return o`<section class="card sub-sec" aria-labelledby="subJoinedTitle">
907
+ </section>`);for(let m of D("[data-act]",t))m.onclick=()=>u[Number(m.dataset.act)]?.onClick();let g=t.querySelector("#subComplaint");sf(t,s,l,p,n,m=>{let y=m.find(E=>E.delivery.event==="complained");g?.hidden&&y&&(f(g,o`The report was about <a href="#/sent/${y.send.id}">${y.send.subject||"an untitled post"}</a>.`),g.hidden=!1)})}await i(!1)}function Jm(e){if(e.receives_posts)return"Receives your posts.";let t=[];return e.status==="pending"?t.push("they haven\u2019t confirmed"):e.status==="unsubscribed"&&t.push(`they left the list on ${W(e.unsubscribed_at)}`),e.suppression&&t.push(qe[e.suppression.reason]==="bounced"?"the address bounced":"the address is blocked"),t.length>0?`Not sent your posts: ${t.join(", and ")}.`:"Not sent your posts."}function Ve(e,t){return o`<dt>${e}</dt><dd>${t}</dd>`}function Qm(e,t){let n=Lc(e.consent_evidence),s=Tc(e)??(e.status==="pending"?"Signed up here, not confirmed yet":"Signed up here, never confirmed");return o`<section class="card sub-sec" aria-labelledby="subJoinedTitle">
907
908
  <h2 class="rec-part-title" id="subJoinedTitle">How they joined</h2>
908
909
  <dl class="sub-dl">
909
910
  ${Ve("How they joined",s)}
910
- ${e.import_id!==null?o`${Ve("Origin",Wn(e.origin)??"Unknown")}
911
- ${Ve("Opt-in",Vn(e.opt_in))}
911
+ ${e.import_id!==null?o`${Ve("Origin",zn(e.origin)??"Unknown")}
912
+ ${Ve("Opt-in",Yn(e.opt_in))}
912
913
  ${t?o`${Ve("Import",o`<code>${t.file_name}</code>`)}
913
- ${Ve("Imported",`${j(t.created_at)}, by ${so(t)}`)}
914
+ ${Ve("Imported",`${W(t.created_at)}, by ${uo(t)}`)}
914
915
  ${Ve("Attestation",t.attestation_version===null?"None: the import confirmed no one":`Version ${t.attestation_version}`)}`:null}`:null}
915
- ${Ve("Joined",j(e.created_at))}
916
- ${Ve("Confirmed",j(e.confirmed_at))}
916
+ ${Ve("Joined",W(e.created_at))}
917
+ ${Ve("Confirmed",W(e.confirmed_at))}
917
918
  ${Ve("Evidence",n?o`<pre class="sub-evidence">${n}</pre>`:"None recorded")}
918
919
  </dl>
919
920
  ${e.import_id!==null?o`<p class="sub-note">The origin, opt-in, confirmation date, and evidence are what the imported file said, not something Kestrel recorded itself.</p>`:null}
920
- </section>`}function Um(e){let t=e.suppression,n;if(!t)n=o`<p class="muted">None.</p>`;else{let s=je[t.reason];n=o`<p class="sub-sup-line"><span class="badge suppressed">${s}</span> <span class="muted">since ${j(t.created_at)}</span></p>
921
- <p class="sub-meaning">${qm(e,t.reason)}</p>
921
+ </section>`}function Xm(e){let t=e.suppression,n;if(!t)n=o`<p class="muted">None.</p>`;else{let s=qe[t.reason];n=o`<p class="sub-sup-line"><span class="badge suppressed">${s}</span> <span class="muted">since ${W(t.created_at)}</span></p>
922
+ <p class="sub-meaning">${tf(e,t.reason)}</p>
922
923
  ${t.detail?o`<dl class="sub-dl">${Ve("Detail",t.detail)}</dl>`:null}`}return o`<section class="card sub-sec" aria-labelledby="subSupTitle">
923
924
  <h2 class="rec-part-title" id="subSupTitle">Suppression</h2>
924
925
  ${n}
925
- </section>`}var Fm={reader:"They had unsubscribed themselves, from the link or the button in your email.",publisher:"You had unsubscribed them, when they asked you to.",complaint:"They had reported your email as spam.",erased:"You had erased them at their request.",import:"The list you imported said they had left."};function qm(e,t){switch(t){case"bounce":return"Your email provider reported that mail to this address failed. You can clear it.";case"import_bounce":return"Mail to this address failed at the service your list was imported from. You can clear it.";default:return e.import_id===null&&!Kn(e)?"You blocked this address. You can unblock it.":kn[je[t]]}}function jm(e){let t=e.unsubscribe_cause;if(!t)return null;let n=e.status!=="unsubscribed";return o`<section class="card sub-sec" aria-labelledby="subLeftTitle">
926
+ </section>`}var ef={reader:"They had unsubscribed themselves, from the link or the button in your email.",publisher:"You had unsubscribed them, when they asked you to.",complaint:"They had reported your email as spam.",erased:"You had erased them at their request.",import:"The list you imported said they had left."};function tf(e,t){switch(t){case"bounce":return"Your email provider reported that mail to this address failed. You can clear it.";case"import_bounce":return"Mail to this address failed at the service your list was imported from. You can clear it.";default:return e.import_id===null&&!Zn(e)?"You blocked this address. You can unblock it.":Rn[qe[t]]}}function nf(e){let t=e.unsubscribe_cause;if(!t)return null;let n=e.status!=="unsubscribed";return o`<section class="card sub-sec" aria-labelledby="subLeftTitle">
926
927
  <h2 class="rec-part-title" id="subLeftTitle">${n?"Left before":"Why they left"}</h2>
927
- <p class="sub-left-line"><span class="muted">Left ${j(e.unsubscribed_at)}</span></p>
928
- <p class="sub-meaning">${n?Fm[t]:ga[t]}</p>
928
+ <p class="sub-left-line"><span class="muted">Left ${W(e.unsubscribed_at)}</span></p>
929
+ <p class="sub-meaning">${n?ef[t]:Ra[t]}</p>
929
930
  ${t==="complaint"?o`<p class="sub-meaning" id="subComplaint" hidden></p>`:null}
930
931
  ${e.status==="pending"?o`<p class="sub-meaning">They signed up again, and haven’t confirmed yet.</p>`:null}
931
- </section>`}function Wm(e,t,n,s,i,r){let a=c("#subSends",e),l=c("#subMore",e);if(n.deliveries.length===0){h(a,s?o`<p class="muted">Their past sends show an erased recipient in place of the address, so none is listed here.</p>`:o`<p class="muted">Not in any send yet.</p>`);return}h(a,o`<div class="table-wrap"><table class="list-table sub-sends-table"><colgroup><col><col class="c-out"><col><col class="c-date"></colgroup><thead><tr><th>Send</th><th class="c-out">Outcome</th><th>Detail</th><th class="c-date">When</th></tr></thead><tbody></tbody></table></div>`);let d=c("tbody",a),u=v=>{r(v.deliveries);let f=document.createElement("tbody");h(f,o`${v.deliveries.map(Km)}`),d.append(...f.childNodes),m(v.next)},m=v=>{if(!v){h(l,o``);return}h(l,o`<button type="button" class="sub-more">Show more</button>`);let f=c(".sub-more",l);f.onclick=()=>Z(f,"Loading\u2026",async()=>{try{u(await L(`${t}/deliveries?after=${encodeURIComponent(v)}`,{signal:i}))}catch(p){re(l,V(p),()=>m(v))}})};u(n)}var Vm="Left out: unsubscribed or suppressed before it went";function Km({send:e,delivery:t}){let n=Ks(t),s=t.error||t.event_detail||(t.event===null&&t.status==="skipped"?Vm:"");return o`<tr><td class="sub-send"><a href="#/sent/${e.id}">${e.subject||o`<em>untitled</em>`}</a><div class="muted">${j(e.fire_at)}</div></td><td><span class="rec-out"><span class="rec-sw sw-${n.sw}"></span>${n.label}</span></td><td class="muted">${s||"\u2014"}</td><td class="muted">${t.event_at?j(t.event_at):"\u2014"}</td></tr>`}var Gm="You have unsaved changes. Leave without saving?",Ui="";function Fi(){let e=location.hash||"#/dashboard";(e==="#/reference"||e.startsWith("#/reference/"))&&(e=`#/api-reference${e.slice(11)}`,history.replaceState(history.state,"",e)),Ui=e;let[,t,n,s]=(Po(e.slice(1))?e:"#/dashboard").split("/");document.body.classList.toggle("editor-mode",t==="edit");let i=t==="web"?al(n,s):null;document.body.classList.toggle("web-mode",i!==null);let r=t==="docs"||t==="api-reference";document.body.classList.toggle("tool-mode",r);for(let a of document.querySelectorAll(".sidebar a[data-view]"))a.dataset.view===t?a.setAttribute("aria-current","page"):a.removeAttribute("aria-current");return dn(!1),t==="edit"&&n?ee((a,l)=>ni(n,a,l)):i!==null?ee((a,l)=>ti(i,a,l)):t==="drafts"?ee(Fo):t==="subscribers"&&n==="import"?ee(Ni):t==="subscribers"&&n&&!Bi.includes(n)?ee((a,l)=>Rc(n,a,l)):t==="subscribers"?ee((a,l)=>Lc(n,a,l)):t==="sent"?n?ee((a,l)=>fi(n,a,l)):ee(mi):t==="template"?ee(wi):t==="settings"?ee(ki):t==="api-reference"?ee((a,l)=>pi(a,l,n)):t==="docs"?ee((a,l)=>oi(n,a,l,s)):ee(zo)}function Cc(){let e=Yr(location.search);if(e){let t=new URL(location.href);t.search="",t.hash=e,history.replaceState(null,"",t)}}function Ac(){window.addEventListener("hashchange",()=>{if(location.hash!==Ui){if(es()?.beforeLeave?.()==="confirm"&&!confirm(Gm)){location.hash=Ui;return}Fi()}}),window.addEventListener("beforeunload",e=>{es()?.dirty?.()&&(e.preventDefault(),e.returnValue="")}),window.addEventListener("keydown",e=>{let t=es()?.manualSave;(e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==="s"&&t&&(e.preventDefault(),t())})}async function zm(){Zi(),aa(),zr(),Ac();let e=await ir();if(e){Y.session=e,or();try{Y.appConfig=await L("/api/settings")}catch{}return ls(),document.body.classList.remove("signed-out"),Cc(),Fi()}}zm().catch(e=>{document.body.classList.contains("signed-out")&&!pe.querySelector(".auth-wall")&&pn(),reportError(e)});})();
932
+ </section>`}function sf(e,t,n,s,i,r){let a=c("#subSends",e),l=c("#subMore",e);if(n.deliveries.length===0){f(a,s?o`<p class="muted">Their past sends show an erased recipient in place of the address, so none is listed here.</p>`:o`<p class="muted">Not in any send yet.</p>`);return}f(a,o`<div class="table-wrap"><table class="list-table sub-sends-table"><colgroup><col><col class="c-out"><col><col class="c-date"></colgroup><thead><tr><th>Send</th><th class="c-out">Outcome</th><th>Detail</th><th class="c-date">When</th></tr></thead><tbody></tbody></table></div>`);let d=c("tbody",a),u=g=>{r(g.deliveries);let h=document.createElement("tbody");f(h,o`${g.deliveries.map(rf)}`),d.append(...h.childNodes),p(g.next)},p=g=>{if(!g){f(l,o``);return}f(l,o`<button type="button" class="sub-more">Show more</button>`);let h=c(".sub-more",l);h.onclick=()=>J(h,"Loading\u2026",async()=>{try{u(await C(`${t}/deliveries?after=${encodeURIComponent(g)}`,{signal:i}))}catch(m){re(l,z(m),()=>p(g))}})};u(n)}var of="Left out: unsubscribed or suppressed before it went";function rf({send:e,delivery:t}){let n=Xs(t),s=t.error||t.event_detail||(t.event===null&&t.status==="skipped"?of:"");return o`<tr><td class="sub-send"><a href="#/sent/${e.id}">${e.subject||o`<em>untitled</em>`}</a><div class="muted">${W(e.fire_at)}</div></td><td><span class="rec-out"><span class="rec-sw sw-${n.sw}"></span>${n.label}</span></td><td class="muted">${s||"\u2014"}</td><td class="muted">${t.event_at?W(t.event_at):"\u2014"}</td></tr>`}var af="You have unsaved changes. Leave without saving?",go="";function zi(){let e=location.hash||"#/dashboard";(e==="#/reference"||e.startsWith("#/reference/"))&&(e=`#/api-reference${e.slice(11)}`,history.replaceState(history.state,"",e)),go=e;let[,t,n,s]=(vn(e.slice(1))?e:"#/dashboard").split("/");document.body.classList.toggle("editor-mode",t==="edit");let i=t==="web"?Sl(n,s):null;document.body.classList.toggle("web-mode",i!==null);let r=t==="docs"||t==="api-reference";document.body.classList.toggle("tool-mode",r);for(let a of document.querySelectorAll(".sidebar a[data-view]"))a.dataset.view===t?a.setAttribute("aria-current","page"):a.removeAttribute("aria-current");return Ot(!1),t==="edit"&&n?ne((a,l)=>di(n,a,l)):i!==null?ne((a,l)=>ci(i,a,l)):t==="drafts"?ne(Yo):t==="subscribers"&&n==="import"?ne(Vi):t==="subscribers"&&n&&!Gi.includes(n)?ne((a,l)=>Fc(n,a,l)):t==="subscribers"?ne((a,l)=>Uc(n,a,l)):t==="sent"?n?ne((a,l)=>$i(n,a,l)):ne(wi):t==="template"?ne(Ri):t==="settings"?ne((a,l)=>Pi(a,l,n)):t==="api-reference"?ne((a,l)=>Si(a,l,n)):t==="docs"?ne((a,l)=>pi(n,a,l,s)):ne(ni)}function qc(){let e=sa(location.search);if(e){let t=new URL(location.href);t.search="",t.hash=e,history.replaceState(null,"",t)}}function jc(){window.addEventListener("hashchange",()=>{if(location.hash!==go){if(mn()?.navigate?.(location.hash)){go=location.hash,Ot(!1);return}if(mn()?.beforeLeave?.()==="confirm"&&!confirm(af)){location.hash=go;return}zi()}}),window.addEventListener("beforeunload",e=>{mn()?.dirty?.()&&(e.preventDefault(),e.returnValue="")}),window.addEventListener("keydown",e=>{let t=mn()?.manualSave;(e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==="s"&&t&&(e.preventDefault(),t())})}async function lf(){sr(),ya(),na(),jc();let e=await mr();if(e){Z.session=e,pr();try{Z.appConfig=await C("/api/settings")}catch{}return ps(),document.body.classList.remove("signed-out"),qc(),zi()}}lf().catch(e=>{document.body.classList.contains("signed-out")&&!ue.querySelector(".auth-wall")&&hn(),reportError(e)});})();
932
933
  //# sourceMappingURL=app.js.map