@kurtbruns/kestrel 1.5.0 → 1.6.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.
@@ -1,127 +1,137 @@
1
1
  // GENERATED by scripts/build-client.mjs from client/ — edit the source there, never this file.
2
- "use strict";(()=>{function Mn(e){let t=document.getElementById(e);if(!t)throw new Error(`the admin page has no #${e}`);return t}var je=Mn("app"),Rn=Mn("identity"),ms=Mn("toasts"),fs=document.getElementById("navToggle"),Cn=document.getElementById("navScrim");function vt(e){document.body.classList.toggle("nav-open",e),fs?.setAttribute("aria-expanded",e?"true":"false"),Cn&&(Cn.hidden=!e)}var ht=document.getElementById("navCollapse"),Hn=null;function ui(){return window.innerWidth<540?"full":Hn==="collapsed"?"rail":Hn==="expanded"?"full":window.innerWidth<=1024?"rail":"full"}function Tn(){let e=ui();if(document.documentElement.dataset.nav=e,!ht)return;let t=e!=="rail",n=t?"Collapse sidebar":"Expand sidebar";ht.setAttribute("aria-expanded",t?"true":"false"),ht.setAttribute("aria-label",n),ht.setAttribute("title",n)}var Ln=0,ps=0;function hs(){fs?.addEventListener("click",()=>vt(!document.body.classList.contains("nav-open"))),Cn?.addEventListener("click",()=>vt(!1)),document.querySelector(".sidebar")?.addEventListener("click",e=>{e.target instanceof Element&&e.target.closest("a")&&vt(!1)}),ht?.addEventListener("click",()=>{Hn=document.documentElement.dataset.nav!=="rail"?"collapsed":"expanded",Tn()}),window.addEventListener("resize",()=>{document.body.classList.add("nav-resizing"),clearTimeout(ps),ps=setTimeout(()=>{document.body.classList.remove("nav-resizing")},200),!Ln&&(Ln=requestAnimationFrame(()=>{Ln=0,Tn()}))}),Tn()}var _t=null,Bt=null;function Q(e){_t?.abort(),Bt=null;let t=new AbortController;_t=t;let n=document.createElement("div");n.className="view",je.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&&(Bt=i)},i=>{if(!t.signal.aborted)throw i})}function vs(){_t?.abort(),_t=null,Bt=null,je.replaceChildren()}function Dt(){return Bt}function Ge(e,t,n){if(n.aborted)return;let s=setInterval(t,e);n.addEventListener("abort",()=>clearInterval(s),{once:!0})}function Nt(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 gs(e,t,n){let s=()=>{if(n.aborted)return;let i=()=>clearTimeout(a),a=setTimeout(async()=>{n.removeEventListener("abort",i);let l=!0;try{l=await t()??!0}catch(c){pi(c)||reportError(c)}l!==!1&&s()},e);n.addEventListener("abort",i,{once:!0})};s()}function bs(e,t){if(e.aborted){t();return}e.addEventListener("abort",t,{once:!0})}function pi(e){return e instanceof Error&&(e.name==="AbortError"||e.name==="ApiError")}var It="kestrel_token",z={token:localStorage.getItem(It)||"",session:null,appConfig:null};function d(e,t=document){let n=t.querySelector(e);if(!n)throw new Error(`no element matches ${e}`);return n}function M(e,t=document){return[...t.querySelectorAll(e)]}var mi={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};function ee(e){return String(e).replace(/[&<>"']/g,t=>mi[t]??t)}var ze=class{markup;constructor(t){this.markup=t,Object.freeze(this)}toString(){return this.markup}};function ws(e){return e instanceof ze?e.markup:e==null||e===!1?"":Array.isArray(e)?e.map(ws).join(""):ee(e)}var fi=/=\s*$/;function o(e,...t){let n=ys(e,0);for(let s=0;s<t.length;s++){let i=t[s];if(!(i instanceof ze)&&fi.test(n))throw new Error("html: an interpolation in an unquoted attribute value cannot be made safe; quote it");n+=ws(i)+ys(e,s+1)}return new ze(n)}function ys(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 F(e){return new ze(e)}function g(e,t){if(!(t instanceof ze))throw new Error("setHtml: expected Html; build it with html`\u2026` or vouch for it with unsafeHtml()");e.innerHTML=t.markup}function Pn(e){z.token=(e||"").trim();try{z.token?localStorage.setItem(It,z.token):localStorage.removeItem(It)}catch{}}function qt(){return z.token?{Authorization:`Bearer ${z.token}`}:{}}function Ss(){let e=z.session;if(e?.auth.mode==="access"){let t=e.principal,n=t.email||(t.kind==="service"?"Service token":"Signed in");g(Rn,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 g(Rn,o`<span class="who dev" title="Local dev — auth is bypassed on localhost">Local dev</span>`)}function Ut(){vs(),document.body.classList.add("signed-out"),g(je,o`<div class="card auth-wall"><h2>Session expired</h2><p class="hint">Your access session ended. Sign in again to continue.</p><button id="reauth">Sign in</button></div>`),d("#reauth",je).onclick=()=>location.reload()}var he=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}},hi="Not authorized \u2014 please sign in again.";async function An(e,t){let n={...qt(),...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)throw Ut(),new he(hi,401);return i}function vi(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 T(e,t={}){let n=await An(e,t),s=await n.text(),i=s?JSON.parse(s):null;if(!n.ok)throw new he(vi(i)??n.statusText,n.status,i);return i}async function _n(e,t={}){let n=await An(e,t);if(!n.ok)throw new he(n.statusText,n.status);return n.text()}async function $s(e,t={}){let n=await An(e,t);if(!n.ok)throw new he(n.statusText,n.status);let s=n.headers.get("content-disposition")??"",i=/filename="([^"]+)"/.exec(s)?.[1]??null;return{text:await n.text(),filename:i}}function gt(e){if(!e)return null;let t=String(e).match(/^\s*"?([^"<]*?)"?\s*<[^>]+>\s*$/);return(t?.[1]?t[1].trim():"")||null}function Bn(e){let t=e?.settings.publication;return{name:t?.name||gt(e?.deployment.fromAddress)||"Your publication",tagline:t?.tagline||"",logoUrl:t?.logoUrl||null}}function Ot(){let e=Bn(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?(g(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")))}var _r=1800*1e3,jt=300*1e3,Br=60*1e3,Dr=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`}var Nr=10080*60*1e3;function ks(){let e=z.appConfig?.deployment.build;if(!e?.version)return null;let t=e.tagUrl?o`<a class="build-link" href="${e.tagUrl}" target="_blank" rel="noopener">v${e.version}</a>`:o`v${e.version}`,n=e.commitUrl?o`<a class="build-link" href="${e.commitUrl}" target="_blank" rel="noopener">${e.sha}</a>`:o`${e.sha}`;return{version:t,sha:n}}function xs(){return z.appConfig?.deployment.minLeadMs??jt}function bt(){return Ft(xs())}function Vt(e=Date.now()){return new Date(e+xs()+6e4)}function yt(){return(z.appConfig?.deployment.provider||"")==="fake"}function et(e){return yt()?`${e} (no email provider configured \u2014 nothing is delivered)`:e}var Es=`<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 Ts=`<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 Ls=`<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>
5
- `;var Cs=`<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>
6
- `;var Hs=`<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>
7
- `;var Ms=`<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>
8
- `;var Rs=`<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>
9
- `;var Ps=`<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>
10
- `;var As=`<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>
11
- `;var _s=`<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>
12
- `;var Bs=`<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>
13
- `;var Ds=`<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>
14
- `;var Ns=`<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>
15
- `;var Is=`<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>
16
- `;var qs=`<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>
17
- `;var Us=`<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>
18
- `;var Os=`<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>
19
- `;var js=`<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>
20
- `;var Fs=`<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>
21
- `;var Vs=`<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>
22
- `;var Ks=`<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>
23
- `;var Ws=`<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>
24
- `;var Zs=`<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>
25
- `;var Gs=`<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>
26
- `;var zs=`<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>
27
- `;var ji={article:F(Es),bold:F(Ts),check:F(Ls),code:F(Cs),copyout:F(Hs),editable:F(Ms),heading:F(Rs),indent:F(Ps),info:F(As),"info-filled":F(_s),italic:F(Bs),kestrel:F(Ds),lines:F(Ns),link:F(Is),ol:F(qs),paperclip:F(Us),plus:F(Os),preview:F(js),quote:F(Fs),readonly:F(Vs),search:F(Ks),send:F(Ws),ul:F(Zs),upload:F(Gs),x:F(zs)};function D(e){return ji[e]}function tt(e,t,n){let s=(r,b)=>e===r?o`<a href="#/${r}" data-room="${r}" data-text="${b}" aria-current="page">${b}</a>`:o`<a href="#/${r}" data-room="${r}" data-text="${b}">${b}</a>`,i=ks(),a=z.appConfig?.deployment.build.buildTime,l=a?`Built ${new Date(a).toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}`:null,c=i?o`${i.version} <span aria-hidden="true">·</span> ${i.sha}`:null,f=c?l?o`<div class="rail-foot" title="${l}">${c}</div>`:o`<div class="rail-foot">${c}</div>`:null,m=c?l?o`<footer class="room-foot" title="${l}">${c}</footer>`:o`<footer class="room-foot">${c}</footer>`:null,u=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}${f}</div></nav><div class="room-main">${n}</div></div>`;return o`<div class="room">
2
+ "use strict";(()=>{function An(e){let t=document.getElementById(e);if(!t)throw new Error(`the admin page has no #${e}`);return t}var Ne=An("app"),_n=An("identity"),Bn=An("toasts"),Ms=document.getElementById("navToggle"),Mn=document.getElementById("navScrim");function $t(e){document.body.classList.toggle("nav-open",e),Ms?.setAttribute("aria-expanded",e?"true":"false"),Mn&&(Mn.hidden=!e)}var St=document.getElementById("navCollapse"),Pn=null;function pa(){return window.innerWidth<540?"full":Pn==="collapsed"?"rail":Pn==="expanded"?"full":window.innerWidth<=1024?"rail":"full"}function Rn(){let e=pa();if(document.documentElement.dataset.nav=e,!St)return;let t=e!=="rail",n=t?"Collapse sidebar":"Expand sidebar";St.setAttribute("aria-expanded",t?"true":"false"),St.setAttribute("aria-label",n),St.setAttribute("title",n)}var Cn=0,Cs=0;function Ps(){Ms?.addEventListener("click",()=>$t(!document.body.classList.contains("nav-open"))),Mn?.addEventListener("click",()=>$t(!1)),document.querySelector(".sidebar")?.addEventListener("click",e=>{e.target instanceof Element&&e.target.closest("a")&&$t(!1)}),St?.addEventListener("click",()=>{Pn=document.documentElement.dataset.nav!=="rail"?"collapsed":"expanded",Rn()}),window.addEventListener("resize",()=>{document.body.classList.add("nav-resizing"),clearTimeout(Cs),Cs=setTimeout(()=>{document.body.classList.remove("nav-resizing")},200),!Cn&&(Cn=requestAnimationFrame(()=>{Cn=0,Rn()}))}),Rn()}var It=null,Dt=null;function K(e){It?.abort(),Dt=null;let t=new AbortController;It=t;let n=document.createElement("div");n.className="view",Ne.replaceChildren(n);let s;try{s=Promise.resolve(e(n,t.signal))}catch(o){s=Promise.reject(o)}return s.then(o=>{!t.signal.aborted&&o&&(Dt=o)},o=>{if(!t.signal.aborted)throw o})}function As(){It?.abort(),It=null,Dt=null,Ne.replaceChildren()}function qt(){return Dt}function We(e,t,n){if(n.aborted)return;let s=setInterval(t,e);n.addEventListener("abort",()=>clearInterval(s),{once:!0})}function Ot(e,t,n){if(n.aborted)return;let s=()=>clearTimeout(o),o=setTimeout(()=>{n.removeEventListener("abort",s),t()},Math.max(0,e-Date.now()));n.addEventListener("abort",s,{once:!0})}function _s(e,t,n){let s=()=>{if(n.aborted)return;let o=()=>clearTimeout(a),a=setTimeout(async()=>{n.removeEventListener("abort",o);let l=!0;try{l=await t()??!0}catch(r){ma(r)||reportError(r)}l!==!1&&s()},e);n.addEventListener("abort",o,{once:!0})};s()}function Ke(e,t){if(e.aborted){t();return}e.addEventListener("abort",t,{once:!0})}function ma(e){return e instanceof Error&&(e.name==="AbortError"||e.name==="ApiError")}var Ut="kestrel_token",Z={token:localStorage.getItem(Ut)||"",session:null,appConfig:null};function d(e,t=document){let n=t.querySelector(e);if(!n)throw new Error(`no element matches ${e}`);return n}function P(e,t=document){return[...t.querySelectorAll(e)]}var fa={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};function Q(e){return String(e).replace(/[&<>"']/g,t=>fa[t]??t)}var Ze=class{markup;constructor(t){this.markup=t,Object.freeze(this)}toString(){return this.markup}};function Ns(e){return e instanceof Ze?e.markup:e==null||e===!1?"":Array.isArray(e)?e.map(Ns).join(""):Q(e)}var ha=/=\s*$/;function i(e,...t){let n=Bs(e,0);for(let s=0;s<t.length;s++){let o=t[s];if(!(o instanceof Ze)&&ha.test(n))throw new Error("html: an interpolation in an unquoted attribute value cannot be made safe; quote it");n+=Ns(o)+Bs(e,s+1)}return new Ze(n)}function Bs(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 D(e){return new Ze(e)}function g(e,t){if(!(t instanceof Ze))throw new Error("setHtml: expected Html; build it with html`\u2026` or vouch for it with unsafeHtml()");e.innerHTML=t.markup}function Nn(e){Z.token=(e||"").trim();try{Z.token?localStorage.setItem(Ut,Z.token):localStorage.removeItem(Ut)}catch{}}function jt(){return Z.token?{Authorization:`Bearer ${Z.token}`}:{}}function Is(){let e=Z.session;if(e?.auth.mode==="access"){let t=e.principal,n=t.email||(t.kind==="service"?"Service token":"Signed in");g(_n,i`<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 g(_n,i`<span class="who dev" title="Local dev — auth is bypassed on localhost">Local dev</span>`)}function Ft(){As(),document.body.classList.add("signed-out"),g(Ne,i`<div class="card auth-wall"><h2>Session expired</h2><p class="hint">Your access session ended. Sign in again to continue.</p><button id="reauth">Sign in</button></div>`),d("#reauth",Ne).onclick=()=>location.reload()}var se=class extends Error{constructor(n,s,o=null){super(n);this.status=s;this.data=o;this.name="ApiError"}status;data;get code(){let n=this.data;return typeof n?.error=="string"?n.error:null}},va="Not authorized \u2014 please sign in again.";async function In(e,t){let n={...jt(),...t.headers??{}},s=t.body;t.json!==void 0&&(n["content-type"]="application/json",s=JSON.stringify(t.json));let o=await fetch(e,{method:t.method??"GET",headers:n,body:s,signal:t.signal});if(o.status===401)throw Ft(),new se(va,401);return o}function ga(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 Dn(e){let t=await e.text(),n=null;try{n=t?JSON.parse(t):null}catch{}let s=e.statusText||`Request failed (HTTP ${e.status})`;return new se(ga(n)??s,e.status,n)}async function T(e,t={}){let n=await In(e,t);if(!n.ok)throw await Dn(n);let s=await n.text();return s?JSON.parse(s):null}async function Qe(e,t={}){let n=await In(e,t);if(!n.ok)throw await Dn(n);return n.text()}async function Ds(e,t={}){let n=await In(e,t);if(!n.ok)throw await Dn(n);let s=n.headers.get("content-disposition")??"",o=/filename="([^"]+)"/.exec(s)?.[1]??null;return{text:await n.text(),filename:o}}function qn(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 Vt(e,t){return e||qn(t)}function On(e){let t=e?.settings.publication;return{name:Vt(t?.name||"",e?.deployment.fromAddress??"")||"Your publication",tagline:t?.tagline||"",logoUrl:t?.logoUrl||null}}function Wt(){let e=On(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?(g(s,i`<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 qs(e,t,n){return`${e}${t}/${n}`}var bd=1800*1e3,Kt=300*1e3,yd=60*1e3,wd=1440*60*1e3;function Zt(e){let t=Math.round(e/1e3);if(t%60===0){let n=t/60;return`${n} minute${n===1?"":"s"}`}return`${t} seconds`}var Sd=10080*60*1e3;function Os(){let e=Z.appConfig?.deployment.build;if(!e?.version)return null;let t=e.tagUrl?i`<a class="build-link" href="${e.tagUrl}" target="_blank" rel="noopener">v${e.version}</a>`:i`v${e.version}`,n=e.commitUrl?i`<a class="build-link" href="${e.commitUrl}" target="_blank" rel="noopener">${e.sha}</a>`:i`${e.sha}`;return{version:t,sha:n}}function Us(){return Z.appConfig?.deployment.minLeadMs??Kt}function kt(){return Zt(Us())}function Yt(e=Date.now()){return new Date(e+Us()+6e4)}function xt(){return(Z.appConfig?.deployment.provider||"")==="fake"}function Xe(e){return xt()?`${e} (no email provider configured, so nothing is delivered)`:e}function Gt(e){let t=Z.appConfig?.deployment;return t?qs(t.archiveOrigin,t.archiveBasePath,e):null}var js=`<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 Fs=`<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 Vs=`<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 Ws=`<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 Ks=`<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 Zs=`<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 Ys=`<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 Gs=`<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 zs=`<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>
11
+ `;var Js=`<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 Qs=`<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 Xs=`<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 eo=`<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 to=`<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 no=`<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 so=`<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 oo=`<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 io=`<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 ao=`<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 ro=`<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 lo=`<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 co=`<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 uo=`<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 po=`<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 mo=`<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 fo=`<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 ho=`<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 vo=`<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 go=`<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 bo=`<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>
32
+ `;var yo=`<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>
33
+ `;var wo=`<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>
34
+ `;var za={article:D(js),bold:D(Fs),check:D(Ks),code:D(Gs),copyout:D(zs),editable:D(Qs),heading:D(Xs),indent:D(eo),info:D(to),"info-filled":D(no),italic:D(so),kestrel:D(oo),lines:D(io),link:D(ao),ol:D(lo),paperclip:D(co),plus:D(po),preview:D(mo),quote:D(fo),readonly:D(ho),search:D(vo),send:D(go),ul:D(bo),upload:D(yo),book:D(Vs),branch:D(Ws),"circle-check":D(Zs),"circle-minus":D(Ys),desktop:D(Js),mail:D(ro),phone:D(uo),x:D(wo)};function M(e){return za[e]}function et(e,t,n){let s=(u,y)=>e===u?i`<a href="#/${u}" data-room="${u}" data-text="${y}" aria-current="page">${y}</a>`:i`<a href="#/${u}" data-room="${u}" data-text="${y}">${y}</a>`,o=Os(),a=Z.appConfig?.deployment.build.buildTime,l=a?`Built ${new Date(a).toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}`:null,r=o?i`${o.version} <span aria-hidden="true">·</span> ${o.sha}`:null,c=r?l?i`<div class="rail-foot" title="${l}">${r}</div>`:i`<div class="rail-foot">${r}</div>`:null,p=r?l?i`<footer class="room-foot" title="${l}">${r}</footer>`:i`<footer class="room-foot">${r}</footer>`:null,h=t==null?i`<div class="room-body norail"><div class="room-main">${n}</div></div>`:i`<div class="room-body"><nav class="room-rail" aria-label="Contents"><div class="rail-inner">${t}${c}</div></nav><div class="room-main">${n}</div></div>`;return i`<div class="room">
28
35
  <header class="room-bar">
29
36
  <a class="room-back" href="#/dashboard" aria-label="Back to dashboard"><span aria-hidden="true">←</span><span class="room-back-label">Dashboard</span></a>
30
37
  <div class="room-nav">
31
- <a class="room-brand" href="#/docs">${D("kestrel")}<span>Kestrel</span></a>
38
+ <a class="room-brand" href="#/docs">${M("kestrel")}<span>Kestrel</span></a>
32
39
  <nav class="room-switch" aria-label="Reference">${s("docs","Docs")}${s("reference","API")}</nav>
33
40
  <a class="room-close" href="#/dashboard" title="Back to publication" aria-label="Back to publication"><span aria-hidden="true">✕</span></a>
34
41
  </div>
35
42
  </header>
36
- ${u}
37
- ${m}
38
- </div>`}function Ys(){je.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 Fi=/^(\d+)\.(\d+)$/;function Vi(e){return`${e.seq}.${e.at}`}function Qs(e){let t=Fi.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 Kt(e,t){let n=Qs(e),s=Qs(t);return!n||!s?n?e:t:Vi({seq:Math.min(n.seq,s.seq),at:Math.min(n.at,s.at)})}function Ve(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 Ki=3e3,Wi=6e4,Zi=3e4,Be=new Set,Wt=null,Ye=null,Ne,Fe=null,wt=!1,Dn=!1,Zt=0;function Gt(e){try{e()}catch(t){reportError(t)}}function Nn(e,t){return e&&t?Kt(e,t):e??t}function Js(e){clearTimeout(Ne),Ne=void 0,!(!Be.size||document.hidden)&&(Ne=setTimeout(St,e))}function St(){if(clearTimeout(Ne),Ne=void 0,!Be.size)return;if(Fe){wt=!0;return}let e=Nn(Wt,Ye);Ye=null;let t=[...Be],n=new AbortController;Fe=n;let s=setTimeout(()=>n.abort(),Zi),i=e?`?since=${encodeURIComponent(e)}`:"";T(`/api/sends/feed${i}`,{signal:n.signal}).then(a=>{if(clearTimeout(s),Fe===n){Fe=null,Zt=0,Wt=a.cursor;for(let l of t)Be.has(l)&&l.take(a);if(wt){wt=!1,St();return}Js(Math.max(0,a.read_again_at-a.now))}},a=>{if(clearTimeout(s),Fe===n){if(Fe=null,wt=!1,a instanceof he&&a.code==="cursor_ahead"){let l=[...Be];for(let c of l)to(c);for(let c of l)Gt(()=>c.stale());return}a instanceof he&&a.status===400?Wt=null:Ye=Nn(Ye,e),Zt+=1,Js(Math.min(Ki*2**(Zt-1),Wi))}})}function Gi(){Dn||(Dn=!0,queueMicrotask(()=>{Dn=!1,St()}))}function Xs(){document.hidden?(clearTimeout(Ne),Ne=void 0):St()}function eo(e,t,n){n.aborted||(Be.add(e),Ye=Nn(Ye,t),Be.size===1&&document.addEventListener("visibilitychange",Xs),n.addEventListener("abort",()=>to(e),{once:!0}),Gi())}function to(e){Be.delete(e),!Be.size&&(clearTimeout(Ne),Ne=void 0,Fe?.abort(),Fe=null,wt=!1,Zt=0,Wt=null,Ye=null,document.removeEventListener("visibilitychange",Xs))}function nt(e,t,n){let s=new Map(e.sends.map(i=>[i.id,Ve(i)]));eo({take(i){if(!i.sends.length&&!i.removed.length)return;let a=[];for(let c of i.sends){let f=Ve(c),m=s.get(c.id)??null;m!==f&&a.push({send:c,from:m,to:f}),s.set(c.id,f)}let l=i.removed.map(c=>c.id);for(let c of l)s.delete(c);Gt(()=>t.update({sends:i.sends,changes:a,removed:l}))},stale:()=>t.stale()},e.cursor,n)}function $t(e,t,n){let s=e.send.id,i=Ve(e.send);eo({take(a){if(a.removed.some(m=>m.id===s)){Gt(()=>t.removed());return}let l=a.sends.find(m=>m.id===s);if(!l)return;let c=Ve(l),f=c!==i?{send:l,from:i,to:c}:null;i=c,Gt(()=>t.update(l,f))},stale:()=>t.stale()},e.cursor,n)}function no(){Be.size&&St()}function st(e){return e.trim().toLowerCase()}function ot(e){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}var Z=e=>e?new Date(e).toLocaleString(void 0,{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}):"\u2014";function zt(e){let t=new Set,n=[];for(let s of String(e||"").split(/[\n,]+/)){let i=st(s);ot(i)?t.add(i):i&&n.push(s.trim())}return{valid:[...t],invalid:n}}function Yt(e){return e.length?`Not ${e.length===1?"an email address":"email addresses"}: ${e.join(", ")}`:null}function it(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 kt(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),a=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(a<24){let c=i%60;return c>0?`Sends in ${a}h ${c}m`:`Sends in ${a}h`}let l=Math.round(a/24);return`Sends in ${l} day${l===1?"":"s"}`}function Qt(e){return`Missed its fire time \xB7 ${Math.max(0,Math.floor((Date.now()-e)/6e4))} min late`}function at(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 rt(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">
43
+ ${h}
44
+ ${p}
45
+ </div>`}function So(){Ne.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 Ja=/^(\/[A-Za-z0-9._~%-]+)+$/,Qa=/\/(?:\.|%2e){1,2}(?=\/|$)/i;function Un(e){return Ja.test(e)&&!Qa.test(e)}function $o(e){let t=new URLSearchParams(e).get("to");return t&&Un(t)?`#${t}`:null}var Xa=/^(\d+)\.(\d+)$/;function er(e){return`${e.seq}.${e.at}`}function ko(e){let t=Xa.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 zt(e,t){let n=ko(e),s=ko(t);return!n||!s?n?e:t:er({seq:Math.min(n.seq,s.seq),at:Math.min(n.at,s.at)})}function De(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 tr=3e3,nr=6e4,sr=3e4,$e=new Set,Jt=null,Ye=null,Me,Ie=null,Et=!1,jn=!1,Qt=0;function Xt(e){try{e()}catch(t){reportError(t)}}function Fn(e,t){return e&&t?zt(e,t):e??t}function xo(e){clearTimeout(Me),Me=void 0,!(!$e.size||document.hidden)&&(Me=setTimeout(Tt,e))}function Tt(){if(clearTimeout(Me),Me=void 0,!$e.size)return;if(Ie){Et=!0;return}let e=Fn(Jt,Ye);Ye=null;let t=[...$e],n=new AbortController;Ie=n;let s=setTimeout(()=>n.abort(),sr),o=e?`?since=${encodeURIComponent(e)}`:"";T(`/api/sends/feed${o}`,{signal:n.signal}).then(a=>{if(clearTimeout(s),Ie===n){Ie=null,Qt=0,Jt=a.cursor;for(let l of t)$e.has(l)&&l.take(a);if(Et){Et=!1,Tt();return}xo(Math.max(0,a.read_again_at-a.now))}},a=>{if(clearTimeout(s),Ie===n){if(Ie=null,Et=!1,a instanceof se&&a.code==="cursor_ahead"){let l=[...$e];for(let r of l)Lo(r);for(let r of l)Xt(()=>r.stale());return}a instanceof se&&a.status===400?Jt=null:Ye=Fn(Ye,e),Qt+=1,xo(Math.min(tr*2**(Qt-1),nr))}})}function or(){jn||(jn=!0,queueMicrotask(()=>{jn=!1,Tt()}))}function Eo(){document.hidden?(clearTimeout(Me),Me=void 0):Tt()}function To(e,t,n){n.aborted||($e.add(e),Ye=Fn(Ye,t),$e.size===1&&document.addEventListener("visibilitychange",Eo),n.addEventListener("abort",()=>Lo(e),{once:!0}),or())}function Lo(e){$e.delete(e),!$e.size&&(clearTimeout(Me),Me=void 0,Ie?.abort(),Ie=null,Et=!1,Qt=0,Jt=null,Ye=null,document.removeEventListener("visibilitychange",Eo))}function tt(e,t,n){let s=new Map(e.sends.map(o=>[o.id,De(o)]));To({take(o){if(!o.sends.length&&!o.removed.length)return;let a=[];for(let r of o.sends){let c=De(r),p=s.get(r.id)??null;p!==c&&a.push({send:r,from:p,to:c}),s.set(r.id,c)}let l=o.removed.map(r=>r.id);for(let r of l)s.delete(r);Xt(()=>t.update({sends:o.sends,changes:a,removed:l}))},stale:()=>t.stale()},e.cursor,n)}function Lt(e,t,n){let s=e.send.id,o=De(e.send);To({take(a){if(a.removed.some(p=>p.id===s)){Xt(()=>t.removed());return}let l=a.sends.find(p=>p.id===s);if(!l)return;let r=De(l),c=r!==o?{send:l,from:o,to:r}:null;o=r,Xt(()=>t.update(l,c))},stale:()=>t.stale()},e.cursor,n)}function nt(){$e.size&&Tt()}function st(e){return e.trim().toLowerCase()}function ot(e){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}var W=e=>e?new Date(e).toLocaleString(void 0,{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}):"\u2014";function en(e){let t=new Set,n=[];for(let s of String(e||"").split(/[\n,]+/)){let o=st(s);ot(o)?t.add(o):o&&n.push(s.trim())}return{valid:[...t],invalid:n}}function tn(e){return e.length?`Not ${e.length===1?"an email address":"email addresses"}: ${e.join(", ")}`:null}function it(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 Ht(e,t=!1){let n=e-Date.now();if(t||n<=0)return"Preparing to send\u2026";let s=Math.floor(n/1e3),o=Math.floor(s/60),a=Math.floor(o/60);if(s<300)return o>0?`Sends in ${o}m ${String(s%60).padStart(2,"0")}s`:`Sends in ${s}s`;if(o<60)return`Sends in ${o}m`;if(a<24){let r=o%60;return r>0?`Sends in ${a}h ${r}m`:`Sends in ${a}h`}let l=Math.round(a/24);return`Sends in ${l} day${l===1?"":"s"}`}function nn(e){return`Missed its fire time \xB7 ${Math.max(0,Math.floor((Date.now()-e)/6e4))} min late`}function at(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 rt(e){let t=e.statuses?.length?i`<select class="lt-status" aria-label="Filter by status"><option value="${e.allValue||""}">All statuses</option>${e.statuses.map(o=>i`<option value="${o.value}">${o.label}</option>`)}</select>`:null,n=e.suppressible?i`<label class="lt-toggle"><input type="checkbox" class="lt-suppressed"><span>Suppressed</span></label>`:null,s=e.failures?i`<label class="lt-toggle"><input type="checkbox" class="lt-failures"><span>With delivery failures</span></label>`:null;return i`<div class="list-toolbar">
39
46
  <input class="lt-search" type="search" placeholder="${e.searchPlaceholder||"Search\u2026"}" aria-label="Search" autocomplete="off">
40
47
  <div class="lt-filters">${t}${n}${s}</div>
41
- </div>`}function lt(e,t,n){let s=e.querySelector(".lt-search"),i=e.querySelector(".lt-status"),a=e.querySelector(".lt-suppressed"),l=e.querySelector(".lt-failures");if(s){s.value=t.search||"";let c;s.oninput=()=>{clearTimeout(c),c=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()}),a&&(a.checked=t.suppressed==="only",a.onchange=()=>{t.suppressed=a.checked?"only":"",t.offset=0,n()}),l&&(l.checked=t.failures==="only",l.onchange=()=>{t.failures=l.checked?"only":"",t.offset=0,n()})}function Se(e,t,n,s){if(!t)return s?o`<th class="${s}">${e}</th>`:o`<th>${e}</th>`;let i=n.sort===t,a=i?n.dir==="asc"?"\u2191":"\u2193":"",l=`${s?`${s} `:""}sortable${i?" sorted":""}`;return o`<th class="${l}"><button type="button" class="th-sort" data-sort="${t}">${e}<span class="th-arrow" aria-hidden="true">${a}</span></button></th>`}function Ke(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 We(e,t,n,s){let i=n?.limit??t.limit;if(!n||n.total<=i){g(e,o``);return}let a=n.offset+1,l=Math.min(n.offset+i,n.total),c=n.offset>0,f=n.offset+i<n.total;if(g(e,o`<div class="pager"><button type="button" class="pager-prev"${c?null:so}>← Prev</button><span class="pager-range muted">${a}–${l} of ${n.total}</span><button type="button" class="pager-next"${f?null:so}>Next →</button></div>`),c){let m=e.querySelector(".pager-prev");m&&(m.onclick=()=>{t.offset=Math.max(0,n.offset-i),s()})}if(f){let m=e.querySelector(".pager-next");m&&(m.onclick=()=>{t.offset=n.offset+i,s()})}}var so=o` disabled`;function k(e){let t=document.createElement("div");t.className="toast",t.textContent=e,ms.appendChild(t),requestAnimationFrame(()=>t.classList.add("show")),setTimeout(()=>t.classList.remove("show"),3600),setTimeout(()=>t.remove(),3900)}async function Qe(e){try{await navigator.clipboard.writeText(e),k("Copied")}catch{k("Couldn't copy to clipboard")}}var Je=e=>o`<span class="badge ${e}">${e}</span>`;function $e(e){let t=document.createElement("div");t.className="modal-backdrop",g(t,o`<div class="modal" role="dialog" aria-modal="true">${e}</div>`),document.body.appendChild(t);let n=()=>t.remove();return t.addEventListener("click",s=>{s.target===t&&n()}),document.addEventListener("keydown",function s(i){i.key==="Escape"&&(n(),document.removeEventListener("keydown",s))}),{el:t,close:n}}var oo=8;function zi(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,a=Math.min(0,Math.max(i-oo-n-s,oo-s));e.style.setProperty("--tip-x",`${Math.round(a)}px`)}function io(){for(let e of["pointerover","focusin"])document.addEventListener(e,t=>{let n=t.target instanceof Element?t.target.closest(".info"):null;n&&zi(n)});document.addEventListener("keydown",e=>{let t=document.activeElement;e.key==="Escape"&&t instanceof HTMLElement&&t.classList.contains("info")&&t.blur()})}function Jt(e,{below:t=!1}={}){return o`<span class="info${t?" tip-below":""}" role="img" tabindex="0" aria-label="${e}" data-tip="${e}">${D("info-filled")}</span>`}var qn=[];function In(){for(let e of qn)e.remove();qn=[]}function Xt(e,t){In();let n=document.createElement("div");n.className="menu-overlay",n.onclick=In;let s=document.createElement("div");s.className="menu";for(let a of t){let l=document.createElement("button");l.type="button",l.className=`menu-item${a.danger?" danger-item":""}`,l.textContent=a.label,l.onclick=()=>{In(),a.onClick()},s.appendChild(l)}document.body.appendChild(n),document.body.appendChild(s),qn=[n,s];let i=e.getBoundingClientRect();s.style.top=`${i.bottom+window.scrollY+4}px`,s.style.left=`${i.right+window.scrollX-s.offsetWidth}px`}async function X(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 oe(e,t,n){g(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)}var Yi=[{value:"draft",label:"Draft"},{value:"scheduled",label:"Scheduled"}],Un="draft,scheduled";function On(e){return X(e,"Creating\u2026",async()=>{try{let{post:t}=await T("/api/posts",{method:"POST",json:{}});location.hash=`#/edit/${t.id}`}catch(t){k(t instanceof Error?t.message:String(t))}})}async function jn(e,t){let n={status:Un,search:"",sort:"",dir:"desc",offset:0,limit:50};g(e,o`<div class="spread page-head"><h1>Drafts</h1><button class="primary" id="newPost">New post</button></div>
42
- ${rt({statuses:Yi,allValue:Un,searchPlaceholder:"Search subject\u2026"})}
48
+ </div>`}function lt(e,t,n){let s=e.querySelector(".lt-search"),o=e.querySelector(".lt-status"),a=e.querySelector(".lt-suppressed"),l=e.querySelector(".lt-failures");if(s){s.value=t.search||"";let r;s.oninput=()=>{clearTimeout(r),r=setTimeout(()=>{t.search=s.value,t.offset=0,n()},250)}}o&&(o.value=t.status||"",o.onchange=()=>{t.status=o.value,t.offset=0,n()}),a&&(a.checked=t.suppressed==="only",a.onchange=()=>{t.suppressed=a.checked?"only":"",t.offset=0,n()}),l&&(l.checked=t.failures==="only",l.onchange=()=>{t.failures=l.checked?"only":"",t.offset=0,n()})}function fe(e,t,n,s){if(!t)return s?i`<th class="${s}">${e}</th>`:i`<th>${e}</th>`;let o=n.sort===t,a=o?n.dir==="asc"?"\u2191":"\u2193":"",l=`${s?`${s} `:""}sortable${o?" sorted":""}`;return i`<th class="${l}"><button type="button" class="th-sort" data-sort="${t}">${e}<span class="th-arrow" aria-hidden="true">${a}</span></button></th>`}function qe(e,t,n){for(let s of e.querySelectorAll(".th-sort"))s.onclick=()=>{let o=s.dataset.sort;t.sort===o?t.dir=t.dir==="asc"?"desc":"asc":(t.sort=o,t.dir="desc"),t.offset=0,n()}}function Oe(e,t,n,s){let o=n?.limit??t.limit;if(!n||n.total<=o){g(e,i``);return}let a=n.offset+1,l=Math.min(n.offset+o,n.total),r=n.offset>0,c=n.offset+o<n.total;if(g(e,i`<div class="pager"><button type="button" class="pager-prev"${r?null:Ho}>← Prev</button><span class="pager-range muted">${a}–${l} of ${n.total}</span><button type="button" class="pager-next"${c?null:Ho}>Next →</button></div>`),r){let p=e.querySelector(".pager-prev");p&&(p.onclick=()=>{t.offset=Math.max(0,n.offset-o),s()})}if(c){let p=e.querySelector(".pager-next");p&&(p.onclick=()=>{t.offset=n.offset+o,s()})}}var Ho=i` disabled`;var ir=3600,ar=300,rr=3,Wn=new WeakMap;function $(e){let t=[...Bn.querySelectorAll(".toast:not(.leaving)")],n=t.find(o=>o.textContent===e);if(n){Ro(n);return}let s=document.createElement("div");s.className="toast",s.textContent=e,Bn.appendChild(s),requestAnimationFrame(()=>s.classList.add("show")),Ro(s);for(let o of t.slice(0,t.length+1-rr))Mo(o)}function Ro(e){clearTimeout(Wn.get(e)),Wn.set(e,window.setTimeout(()=>Mo(e),ir))}function Mo(e){clearTimeout(Wn.get(e)),e.style.height=`${e.offsetHeight}px`,e.offsetHeight,e.classList.add("leaving"),e.style.height="0",setTimeout(()=>e.remove(),ar)}var Ge=e=>i`<span class="badge ${e}">${e}</span>`;function he(e){let t=document.createElement("div");t.className="modal-backdrop",g(t,i`<div class="modal" role="dialog" aria-modal="true">${e}</div>`),document.body.appendChild(t);let n=o=>{o.key==="Escape"&&s()},s=()=>{t.remove(),document.removeEventListener("keydown",n)};return t.addEventListener("click",o=>{o.target===t&&s()}),document.addEventListener("keydown",n),{el:t,close:s}}var Co=8;function lr(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,o=document.documentElement.clientWidth,a=Math.min(0,Math.max(o-Co-n-s,Co-s));e.style.setProperty("--tip-x",`${Math.round(a)}px`)}function Po(){for(let e of["pointerover","focusin"])document.addEventListener(e,t=>{let n=t.target instanceof Element?t.target.closest(".info"):null;n&&lr(n)});document.addEventListener("keydown",e=>{let t=document.activeElement;e.key==="Escape"&&t instanceof HTMLElement&&t.classList.contains("info")&&t.blur()})}function sn(e,{below:t=!1}={}){return i`<span class="info${t?" tip-below":""}" role="img" tabindex="0" aria-label="${e}" data-tip="${e}">${M("info-filled")}</span>`}var Kn=[];function Vn(){for(let e of Kn)e.remove();Kn=[]}function on(e,t){Vn();let n=document.createElement("div");n.className="menu-overlay",n.onclick=Vn;let s=document.createElement("div");s.className="menu";for(let a of t){let l=document.createElement("button");l.type="button",l.className=`menu-item${a.danger?" danger-item":""}`,l.textContent=a.label,l.onclick=()=>{Vn(),a.onClick()},s.appendChild(l)}document.body.appendChild(n),document.body.appendChild(s),Kn=[n,s];let o=e.getBoundingClientRect();s.style.top=`${o.bottom+window.scrollY+4}px`,s.style.left=`${o.right+window.scrollX-s.offsetWidth}px`}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 X(e,t,n){g(e,i`<div class="error"><span>${t}</span><button class="ghost" data-retry>Retry</button></div>`);let s=e.querySelector("[data-retry]");s&&(s.onclick=n)}var dr=[{value:"draft",label:"Draft"},{value:"scheduled",label:"Scheduled"}],Zn="draft,scheduled";function Yn(e){return z(e,"Creating\u2026",async()=>{try{let{post:t}=await T("/api/posts",{method:"POST",json:{}});location.hash=`#/edit/${t.id}`}catch(t){$(t instanceof Error?t.message:String(t))}})}async function Gn(e,t){let n={status:Zn,search:"",sort:"",dir:"desc",offset:0,limit:50};g(e,i`<div class="spread page-head"><h1>Drafts</h1><button class="primary" id="newPost">New post</button></div>
49
+ ${rt({statuses:dr,allValue:Zn,searchPlaceholder:"Search subject\u2026"})}
43
50
  <div id="list" class="muted">Loading…</div>
44
- <div id="postsPager"></div>`);let s=d("#newPost",e);s.onclick=()=>On(s);let i=d("#list",e),a=d("#postsPager",e),l=new Map,c=new Set,f=0,m=()=>u(!1);async function u(E){let y=++f;try{let K=await T(`/api/posts?${at(n)}`,{signal:t});if(y!==f)return null;let V=K.posts;if(l=new Map(V.map(L=>[L.id,L.fire_at])),c=new Set(V.flatMap(L=>L.active_send_id?[L.active_send_id]:[])),!V.length){let L=n.search||n.status&&n.status!==Un;return g(i,o`<p class="muted">${L?"No drafts match.":"No drafts yet \u2014 create your first draft."}</p>`),g(a,o``),K}g(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>${Se("Title","title",n)}${Se("Status",null,n)}${Se("Scheduled","scheduled",n)}${Se("Updated","updated",n)}<th></th></tr></thead><tbody>${V.map(L=>{let O=L.active_send_status==="sending",j=O?`#/sent/${L.active_send_id}`:`#/edit/${L.id}`;return o`<tr class="clickable" data-id="${L.id}" data-target="${j}"><td><a href="${j}">${L.subject||o`<em>untitled</em>`}</a></td><td>${O?Je("sending"):Je(L.status)}</td><td class="muted when${L.fire_at?"":" empty"}">${L.fire_at?Z(L.fire_at):"\u2014"}</td><td class="muted updated">${Z(L.updated_at)}</td><td class="act"><button class="icon" data-menu="${L.id}" data-status="${O?"sending":L.status}" data-target="${j}" aria-label="Post actions">⋯</button></td></tr>`})}</tbody></table></div>`),Ke(i,n,m);for(let L of M("tr[data-id]",i))L.onclick=O=>{let j=O.target;j instanceof Element&&j.tagName!=="A"&&!j.closest("[data-menu]")&&L.dataset.target&&(location.hash=L.dataset.target)};for(let L of M("[data-menu]",i))L.onclick=O=>{O.stopPropagation();let j=L.dataset.status,J=L.dataset.target??"",ie=[{label:j==="sending"?"Watch send":"Open",onClick:()=>{location.hash=J}}];if(j==="draft"&&L.dataset.menu){let H=L.dataset.menu;ie.push({label:"Delete draft",danger:!0,onClick:()=>Qi(H,m)})}Xt(L,ie)};return We(a,n,K.page,m),K}catch(K){return!E&&y===f&&oe(i,K instanceof Error?K.message:String(K),()=>r?m():void R()),null}}let r=!1,b=E=>{r=!0,nt({cursor:E.cursor,sends:E.posts.flatMap(y=>y.active_send_id&&y.active_send_status?[{id:y.active_send_id,status:y.active_send_status,phase:y.active_send_status==="sending"?"progressing":"scheduled"}]:[])},{update({sends:y,changes:K,removed:V}){(K.some(O=>l.has(O.send.post_id))||y.some(O=>O.status==="scheduled"&&l.has(O.post_id)&&l.get(O.post_id)!==O.fire_at)||V.some(O=>c.has(O)))&&u(!0)},stale:()=>Q(jn)},t)},R=async()=>{let E=await u(!1);E&&!r&&b(E)};lt(e,n,m),await R()}function Qi(e,t){let n=$e(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>`),s=d("#dGo",n.el);d("#dCancel",n.el).onclick=n.close,s.onclick=()=>X(s,"Deleting\u2026",async()=>{try{await T(`/api/posts/${e}`,{method:"DELETE"}),n.close(),k("Draft deleted"),t()}catch(i){k(i instanceof Error?i.message:String(i))}})}var xt=e=>Math.max(0,Math.min(100,Math.round(e||0)));function en(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 tn(e){return o`<span class="countdown" data-fire="${e.fire_at}"${e.phase==="due"&&o` data-due`}${De(e,"missed")&&o` data-missed`}></span>`}function nn(e,t){let n=0,s=()=>{let i=Date.now(),a=Number.POSITIVE_INFINITY,l=c=>{c>i&&c<a&&(a=c)};for(let c of M("[data-closes]",e)){let f=Number(c.dataset.closes);c.hidden=i>=f,l(f)}for(let c of M("[data-fire]",e)){let f=Number(c.dataset.fire),m=c.dataset.missed!==void 0;c.textContent=m?Qt(f):kt(f,c.dataset.due!==void 0),c.classList.toggle("countdown-missed",m),l(f)}a-i<=1e3&&a!==n&&(n=a,Nt(a,s,t))};return Ge(1e3,s,t),s}var De=(e,t)=>e.conditions.some(n=>n.kind===t);function Le(e,t){return e.conditions.find(n=>n.kind===t)}var Ie=(e,t)=>e.actions.some(n=>n.name===t);function dt(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 Et(e){return De(e,"wedged")||De(e,"refused")}function ct(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,a)=>o`${a?", ":""}<span class="delivered-kind">${i}</span>`)}</span>`:null;return o`<span class="n">${t.toLocaleString()}</span>${s}`}function sn(e){let t=e.counts,n=t.accepted+t.delivered+t.bounced+t.complained,s=t.delivered+t.bounced+t.complained,i=e.audience.count,a=i>0?100*n/i:0,l=e.dispatch.eta_ms?` \xB7 ~${en(e.dispatch.eta_ms)} left`:"",c=Le(e,"stuck");return o`<div class="card spread clickable active-card" data-watch="${e.id}">
51
+ <div id="postsPager"></div>`);let s=d("#newPost",e);s.onclick=()=>Yn(s);let o=d("#list",e),a=d("#postsPager",e),l=new Map,r=new Set,c=0,p=()=>h(!1);async function h(k){let B=++c;try{let A=await T(`/api/posts?${at(n)}`,{signal:t});if(B!==c)return null;let H=A.posts;if(l=new Map(H.map(m=>[m.id,m.fire_at])),r=new Set(H.flatMap(m=>m.active_send_id?[m.active_send_id]:[])),!H.length){let m=n.search||n.status&&n.status!==Zn;return g(o,i`<p class="muted">${m?"No drafts match.":"No drafts yet \u2014 create your first draft."}</p>`),g(a,i``),A}g(o,i`<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>${fe("Title","title",n)}${fe("Status",null,n)}${fe("Scheduled","scheduled",n)}${fe("Updated","updated",n)}<th></th></tr></thead><tbody>${H.map(m=>{let L=m.active_send_status==="sending",R=L?`#/sent/${m.active_send_id}`:`#/edit/${m.id}`;return i`<tr class="clickable" data-id="${m.id}" data-target="${R}"><td><a href="${R}">${m.subject||i`<em>untitled</em>`}</a></td><td>${L?Ge("sending"):Ge(m.status)}</td><td class="muted when${m.fire_at?"":" empty"}">${m.fire_at?W(m.fire_at):"\u2014"}</td><td class="muted updated">${W(m.updated_at)}</td><td class="act"><button class="icon" data-menu="${m.id}" data-status="${L?"sending":m.status}" data-target="${R}" aria-label="Post actions">⋯</button></td></tr>`})}</tbody></table></div>`),qe(o,n,p);for(let m of P("tr[data-id]",o))m.onclick=L=>{let R=L.target;R instanceof Element&&R.tagName!=="A"&&!R.closest("[data-menu]")&&m.dataset.target&&(location.hash=m.dataset.target)};for(let m of P("[data-menu]",o))m.onclick=L=>{L.stopPropagation();let R=m.dataset.status,V=m.dataset.target??"",ee=[{label:R==="sending"?"Watch send":"Open",onClick:()=>{location.hash=V}}];if(R==="draft"&&m.dataset.menu){let C=m.dataset.menu;ee.push({label:"Delete draft",danger:!0,onClick:()=>cr(C,p)})}on(m,ee)};return Oe(a,n,A.page,p),A}catch(A){return!k&&B===c&&X(o,A instanceof Error?A.message:String(A),()=>u?p():void x()),null}}let u=!1,y=k=>{u=!0,tt({cursor:k.cursor,sends:k.posts.flatMap(B=>B.active_send_id&&B.active_send_status?[{id:B.active_send_id,status:B.active_send_status,phase:B.active_send_status==="sending"?"progressing":"scheduled"}]:[])},{update({sends:B,changes:A,removed:H}){(A.some(L=>l.has(L.send.post_id))||B.some(L=>L.status==="scheduled"&&l.has(L.post_id)&&l.get(L.post_id)!==L.fire_at)||H.some(L=>r.has(L)))&&h(!0)},stale:()=>K(Gn)},t)},x=async()=>{let k=await h(!1);k&&!u&&y(k)};lt(e,n,p),await x()}function cr(e,t){let n=he(i`<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>`),s=d("#dGo",n.el);d("#dCancel",n.el).onclick=n.close,s.onclick=()=>z(s,"Deleting\u2026",async()=>{try{await T(`/api/posts/${e}`,{method:"DELETE"}),n.close(),$("Draft deleted"),t()}catch(o){$(o instanceof Error?o.message:String(o))}})}var Rt=e=>Math.max(0,Math.min(100,Math.round(e||0)));function an(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 rn(e){return i`<span class="countdown" data-fire="${e.fire_at}"${e.phase==="due"&&i` data-due`}${ke(e,"missed")&&i` data-missed`}></span>`}function ln(e,t){let n=0,s=()=>{let o=Date.now(),a=Number.POSITIVE_INFINITY,l=r=>{r>o&&r<a&&(a=r)};for(let r of P("[data-closes]",e)){let c=Number(r.dataset.closes);r.hidden=o>=c,l(c)}for(let r of P("[data-fire]",e)){let c=Number(r.dataset.fire),p=r.dataset.missed!==void 0;r.textContent=p?nn(c):Ht(c,r.dataset.due!==void 0),r.classList.toggle("countdown-missed",p),l(c)}a-o<=1e3&&a!==n&&(n=a,Ot(a,s,t))};return We(1e3,s,t),s}var ke=(e,t)=>e.conditions.some(n=>n.kind===t);function be(e,t){return e.conditions.find(n=>n.kind===t)}var Pe=(e,t)=>e.actions.some(n=>n.name===t);function dt(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 Ct(e){return ke(e,"wedged")||ke(e,"refused")}function ct(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?i`<span class="muted delivered-note">${n.map((o,a)=>i`${a?", ":""}<span class="delivered-kind">${o}</span>`)}</span>`:null;return i`<span class="n">${t.toLocaleString()}</span>${s}`}function dn(e){let t=e.counts,n=t.accepted+t.delivered+t.bounced+t.complained,s=t.delivered+t.bounced+t.complained,o=e.audience.count,a=o>0?100*n/o:0,l=e.dispatch.eta_ms?` \xB7 ~${an(e.dispatch.eta_ms)} left`:"",r=be(e,"stuck");return i`<div class="card spread clickable active-card" data-watch="${e.id}">
45
52
  <div class="active-main">
46
- <a class="card-link active-subj" href="#/sent/${e.id}">${e.subject||o`<em>untitled</em>`}</a>
47
- <div class="active-bar"><div class="active-fill" style="width:${xt(a)}%"></div></div>
48
- <div class="muted active-stat">Sending — ${n.toLocaleString()} of ${i.toLocaleString()} accepted${s?` \xB7 ${s.toLocaleString()} confirmed`:""}${l}</div>
49
- ${c?o`<div class="active-stuck">${c.message}</div>`:null}
53
+ <a class="card-link active-subj" href="#/sent/${e.id}">${e.subject||i`<em>untitled</em>`}</a>
54
+ <div class="active-bar"><div class="active-fill" style="width:${Rt(a)}%"></div></div>
55
+ <div class="muted active-stat">Sending — ${n.toLocaleString()} of ${o.toLocaleString()} accepted${s?` \xB7 ${s.toLocaleString()} confirmed`:""}${l}</div>
56
+ ${r?i`<div class="active-stuck">${r.message}</div>`:null}
50
57
  </div>
51
58
  <a class="ghost-link" href="#/sent/${e.id}">Watch&nbsp;→</a>
52
- </div>`}function Ji(e){return e instanceof he&&e.status===409&&e.data?.error==="remake_required"?e.data:null}function on(e){return!(e instanceof he)||e.status!==409?!1:e.data?.error==="remake_too_close"}async function ut(e,t){let n=null;for(;;)try{return await e(n)}catch(s){let i=Ji(s);if(!i)throw s;let a=i.sends||[];if(!await Xi(a,t))return null;n=a.map(c=>c.id)}}function Xi(e,t){let n=e.length,s=n===1,i=`${n} scheduled email${s?"":"s"}`;return new Promise(a=>{let l=$e(o`<h3>Apply this change to ${i}?</h3>
59
+ </div>`}function ur(e){return e instanceof se&&e.status===409&&e.data?.error==="remake_required"?e.data:null}function cn(e){return!(e instanceof se)||e.status!==409?!1:e.data?.error==="remake_too_close"}async function ze(e,t){let n=null;for(;;)try{return await e(n)}catch(s){let o=ur(s);if(!o)throw s;let a=o.sends||[];if(!await pr(a,t))return null;n=a.map(r=>r.id)}}function pr(e,t){let n=e.length,s=n===1,o=`${n} scheduled email${s?"":"s"}`;return new Promise(a=>{let l=he(i`<h3>Apply this change to ${o}?</h3>
53
60
  <p class="hint">${t.lead(n)} ${t.gerund} applies the change to ${s?"its email":"their emails"} too.</p>
54
- <ul class="remake-list">${e.map(b=>o`<li><span>${b.subject||o`<em>untitled</em>`}</span><span>sends ${Z(b.fire_at)}</span></li>`)}</ul>
61
+ <ul class="remake-list">${e.map(y=>i`<li><span>${y.subject||i`<em>untitled</em>`}</span><span>sends ${W(y.fire_at)}</span></li>`)}</ul>
55
62
  <dl class="remake-facts">
56
63
  <div><dt>Changes</dt><dd>the look of ${s?"its email":"their emails"}</dd></div>
57
64
  <div><dt>Stays</dt><dd>${s?"its content and fire time":"their content and fire times"}</dd></div>
58
65
  <div><dt>Next</dt><dd>send yourself a fresh test${s?"":" of each"}</dd></div>
59
66
  </dl>
60
67
  <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>
61
- <div class="actions"><button type="button" id="rmCancel">Cancel</button><button type="button" class="primary" id="rmGo">${t.action} and apply</button></div>`),c=!1,f=b=>{c||(c=!0,a(b))},m=l.el.querySelector("#rmCancel"),u=l.el.querySelector("#rmGo");m&&(m.onclick=()=>{l.close(),f(!1)}),u&&(u.onclick=()=>{l.close(),f(!0)});let r=new MutationObserver(()=>{l.el.isConnected||(r.disconnect(),f(!1))});r.observe(document.body,{childList:!0}),u?.focus()})}var ao={action:"Save",gerund:"Saving",lead:e=>`This template is used by ${e} post${e===1?" that's":"s that are"} already scheduled.`},an=(e,t)=>({action:e,gerund:t,lead:n=>`Your name, tagline, address, and logo ride inside every email, including ${n} post${n===1?" that's":"s that are"} already scheduled.`});function rn(e){let t=e?.sends||[];if(!t.length)return o`<span class="set-chip">${D("info")}No posts scheduled</span>`;let n=t.length,s=e?.retry_after?`One sends at ${Z(e.retry_after)}; saving waits until it has sent.`:null;return s?o`<span class="set-chip inuse" title="${s}"><span class="set-chip-dot"></span>In use by ${n} scheduled post${n===1?"":"s"}</span>`:o`<span class="set-chip inuse"><span class="set-chip-dot"></span>In use by ${n} scheduled post${n===1?"":"s"}</span>`}var ea=e=>`${new Date(e).toLocaleString(void 0,{month:"short",day:"numeric"})} at ${new Date(e).toLocaleString(void 0,{hour:"numeric",minute:"2-digit"})}`;function ln(e,t,n){let s=n?"this post":`${t} scheduled post${t===1?"":"s"}`;return o`A template or identity change made <strong>${ea(e)}</strong> was applied to ${s}. Send a fresh test email to review the changes.`}function pt(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}function dn(e){let t=$e(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><label for="addEmail">Email address</label><input type="email" id="addEmail" placeholder="person@example.com"><div class="actions"><button type="button" id="aCancel">Cancel</button><button type="button" class="primary" id="aGo">Send confirmation</button></div>`),n=d("#addEmail",t.el),s=d("#aGo",t.el);n.focus(),d("#aCancel",t.el).onclick=t.close,s.onclick=()=>X(s,"Adding\u2026",async()=>{let i=st(n.value);if(!ot(i)){k("Enter a valid email");return}try{let a=await T("/api/subscribers",{method:"POST",json:{email:i}});t.close(),k(ta(i,a.action)),e?.()}catch(a){k(a instanceof Error?a.message:String(a))}})}function ta(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 ro(e,t){let n=$e(o`<h3>Unsubscribe this subscriber?</h3><p class="hint">Removes <strong>${e.email}</strong> from the send audience immediately. They can re-subscribe later through the double opt-in.</p><div class="actions"><button type="button" id="uCancel">Cancel</button><button type="button" class="danger" id="uGo">Unsubscribe</button></div>`),s=d("#uGo",n.el);d("#uCancel",n.el).onclick=n.close,s.onclick=()=>X(s,"Unsubscribing\u2026",async()=>{try{await T(`/api/subscribers/${e.id}/unsubscribe`,{method:"POST"}),n.close(),k(`Unsubscribed ${e.email}`),t()}catch(i){k(i instanceof Error?i.message:String(i))}})}var co="kestrel.notices",lo=100;function uo(){try{let e=JSON.parse(localStorage.getItem(co)||"{}");return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}function na(e){try{localStorage.setItem(co,JSON.stringify(e))}catch{}}var po=(e,t)=>`${e}|${t}`;function sa(e,t){let n=uo(),s=Date.now();for(let{subject:a,version:l}of t)n[po(e,a)]={v:String(l),t:s};let i=Object.keys(n);if(i.length>lo){i.sort((a,l)=>(n[a]?.t||0)-(n[l]?.t||0));for(let a of i.slice(0,i.length-lo))delete n[a]}na(n)}function cn(e,t){if(!e)return null;let{kind:n,members:s,markup:i}=t,a=s??[{subject:t.subject??"",version:t.version??""}],l=s?`${n}|*`:`${n}|${t.subject??""}`,c=a.map(b=>`${b.subject}@${b.version}`).join(","),f=[...e.children].find(b=>b instanceof HTMLElement&&b.dataset.notice===l),m=uo();if(a.every(b=>m[po(n,b.subject)]?.v===String(b.version)))return f?.remove(),null;if(f?.dataset.noticeEvents===c)return f;let u=document.createElement("div");u.className="notice",u.setAttribute("role","status"),u.dataset.notice=l,u.dataset.noticeEvents=c,g(u,o`<span class="notice-text">${i}</span><button type="button" class="icon notice-dismiss" aria-label="Dismiss">${D("x")}</button>`);let r=u.querySelector(".notice-dismiss");if(r&&(r.onclick=()=>{sa(n,a),u.remove()}),f){let b=f.querySelector(".notice-dismiss")===document.activeElement;f.replaceWith(u),b&&r?.focus()}else e.appendChild(u);return u}function mo(e){let t=[],n=e.filter(r=>De(r,"missed"));n.length&&t.push({level:"red",text:`${n.length} scheduled send${n.length===1?"":"s"} passed the fire time without going out.`});let s=e.flatMap(r=>{let b=Le(r,"wedged");return b?[{s:r,count:b.count}]:[]}),[i]=s;if(i){let r=s.reduce((R,E)=>R+E.count,0),b=r===1?"delivery":"deliveries";t.push({level:"red",text:s.length===1?o`<a href="#/sent/${i.s.id}">${i.s.subject}</a> has ${r} ambiguous ${b} awaiting a decision; resolve ${r===1?"it":"them"} on its page.`:o`${r} ambiguous ${b} across <a href="#/sent">${s.length} sends</a> await a decision; resolve them on the Sent page.`})}let a=e.flatMap(r=>{let b=Le(r,"refused");return b?[{s:r,c:b}]:[]}),[l]=a;if(l){let r=a.length===1?o`<a href="#/sent/${l.s.id}">${l.s.subject}</a>`:o`<a href="#/sent">${a.length} sends</a>`;t.push({level:"red",text:o`${r}: ${l.c.message}`})}let c=e.filter(r=>De(r,"stuck")&&!Et(r)),[f]=c;f&&t.push({level:"amber",text:c.length===1?o`<a href="#/sent/${f.id}">${f.subject}</a>: ${Le(f,"stuck")?.message}`:o`<a href="#/sent">${c.length} sends</a> have been sending longer than they should; they may be retrying.`});let m=e.flatMap(r=>{let b=Le(r,"bounce_spike");return b?[{s:r,c:b}]:[]}),[u]=m;return u&&t.push({level:"amber",text:o`<a href="#/sent/${u.s.id}">${u.s.subject}</a>: ${u.c.message}`}),t}function fo(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 ho=e=>e.filter(t=>t.status==="sending"&&!Et(t));async function Fn(e,t){let n=()=>{t.aborted||Q(Fn)};g(e,o`<div class="dash" id="dash"><p class="muted">Loading…</p></div>`);let s=d("#dash",e),i,a,l;try{let[v,w,q]=await Promise.all([T("/api/posts?limit=200",{signal:t}),T("/api/sends?limit=200",{signal:t}),T("/api/subscribers",{signal:t})]);i=v.posts,a=w,l=q.counts}catch(v){oe(s,v instanceof Error?v.message:String(v),n);return}let c=a.sends,f=new Map,m=Bn(z.appConfig),u=z.appConfig?.deployment??null,r=l.confirmed+l.pending+l.unsubscribed+l.suppressed;if(!i.length&&r===0){g(s,o`<div class="dash-head"><div><h1>${m.name}</h1>${m.tagline?o`<p class="muted dash-tagline">${m.tagline}</p>`:null}<p class="muted">Let's get your first post out the door.</p></div></div>${ia(m,u)}<section class="dash-section"><h2>API access</h2>${xo(!1)}</section>`),ko(s,n);return}let b=[{label:"Confirmed",emph:!0,v:l.confirmed,filter:"confirmed"},{label:"Pending",v:l.pending,filter:"pending"},{label:"Unsubscribed",v:l.unsubscribed,filter:"unsubscribed"},{label:"Suppressed",v:l.suppressed,filter:"suppressed"}],R=o`<div class="tiles">${b.map(v=>o`<a class="tile${v.emph?" tile-emph":""}" href="#/subscribers/${v.filter}"><span class="tile-n">${v.v}</span><span class="tile-label">${v.label}${v.sub?o`<span class="tile-sub">${v.sub}</span>`:null}</span></a>`)}</div>`,E=i.filter(v=>v.status==="draft").slice(0,5),y=E.length?o`<div class="table-wrap"><table><tbody>${E.map(v=>o`<tr class="clickable" data-id="${v.id}"><td><a href="#/edit/${v.id}">${v.subject||o`<em>untitled</em>`}</a></td><td class="muted">edited ${Z(v.updated_at)}</td></tr>`)}</tbody></table></div>`:o`<p class="muted">No drafts in progress.</p>`,K=u?.appOrigin||location.origin,V=(u?.archiveOrigin||location.origin)+(u?.archiveBasePath||""),L=o`<div class="card pub-card">
62
- <div class="pub-row"><span class="pub-key muted">Publication</span><code class="pub-val">${K}</code><button class="ghost" data-copy="${K}">Copy</button></div>
63
- <div class="pub-row"><span class="pub-key muted">Archive</span><code class="pub-val">${V}</code><button class="ghost" data-copy="${V}">Copy</button></div>
68
+ <div class="actions"><button type="button" id="rmCancel">Cancel</button><button type="button" class="primary" id="rmGo">${t.action} and apply</button></div>`),r=!1,c=y=>{r||(r=!0,a(y))},p=l.el.querySelector("#rmCancel"),h=l.el.querySelector("#rmGo");p&&(p.onclick=()=>{l.close(),c(!1)}),h&&(h.onclick=()=>{l.close(),c(!0)});let u=new MutationObserver(()=>{l.el.isConnected||(u.disconnect(),c(!1))});u.observe(document.body,{childList:!0}),h?.focus()})}var Ao={action:"Save",gerund:"Saving",lead:e=>`This template is used by ${e} post${e===1?" that's":"s that are"} already scheduled.`},Mt=(e,t)=>({action:e,gerund:t,lead:n=>`Your name, tagline, address, and logo ride inside every email, including ${n} post${n===1?" that's":"s that are"} already scheduled.`});function un(e){let t=e?.sends||[];if(!t.length)return i`<span class="set-chip">${M("info")}No posts scheduled</span>`;let n=t.length,s=e?.retry_after?`One sends at ${W(e.retry_after)}; saving waits until it has sent.`:null;return s?i`<span class="set-chip inuse" title="${s}"><span class="set-chip-dot"></span>In use by ${n} scheduled post${n===1?"":"s"}</span>`:i`<span class="set-chip inuse"><span class="set-chip-dot"></span>In use by ${n} scheduled post${n===1?"":"s"}</span>`}var mr=e=>`${new Date(e).toLocaleString(void 0,{month:"short",day:"numeric"})} at ${new Date(e).toLocaleString(void 0,{hour:"numeric",minute:"2-digit"})}`;function pn(e,t,n){let s=n?"this post":`${t} scheduled post${t===1?"":"s"}`;return i`A template or identity change made <strong>${mr(e)}</strong> was applied to ${s}. Send a fresh test email to review the changes.`}function ut(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}function mn(e){let t=he(i`<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><label for="addEmail">Email address</label><input type="email" id="addEmail" placeholder="person@example.com"><div class="actions"><button type="button" id="aCancel">Cancel</button><button type="button" class="primary" id="aGo">Send confirmation</button></div>`),n=d("#addEmail",t.el),s=d("#aGo",t.el);n.focus(),d("#aCancel",t.el).onclick=t.close,s.onclick=()=>z(s,"Adding\u2026",async()=>{let o=st(n.value);if(!ot(o)){$("Enter a valid email");return}try{let a=await T("/api/subscribers",{method:"POST",json:{email:o}});t.close(),$(fr(o,a.action)),e?.()}catch(a){$(a instanceof Error?a.message:String(a))}})}function fr(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 _o(e,t){let n=he(i`<h3>Unsubscribe this subscriber?</h3><p class="hint">Removes <strong>${e.email}</strong> from the send audience immediately. They can re-subscribe later through the double opt-in.</p><div class="actions"><button type="button" id="uCancel">Cancel</button><button type="button" class="danger" id="uGo">Unsubscribe</button></div>`),s=d("#uGo",n.el);d("#uCancel",n.el).onclick=n.close,s.onclick=()=>z(s,"Unsubscribing\u2026",async()=>{try{await T(`/api/subscribers/${e.id}/unsubscribe`,{method:"POST"}),n.close(),$(`Unsubscribed ${e.email}`),t()}catch(o){$(o instanceof Error?o.message:String(o))}})}var hr=1500;function Ae(e=!1,t="Copy"){return i`<span>${t}</span>${M(e?"check":"copyout")}`}function pt(e,t){return i`<button type="button" class="copy-btn" data-copy="${e}" aria-label="${t}">${Ae()}</button>`}function Jn(e,t,n){return i`<div class="block"><div class="block-head"><span class="block-label">${e}</span>${pt(n,`Copy ${e}`)}</div><pre><code>${t}</code></pre></div>`}var zn=new WeakMap;async function Qn(e,t){try{await navigator.clipboard.writeText(t)}catch{$("Couldn't copy to clipboard");return}let n=zn.get(e),s=n?.label??e.getAttribute("aria-label")??"Copy",o=e.dataset.copyWord;window.clearTimeout(n?.timer),g(e,Ae(!0,o)),e.setAttribute("aria-label","Copied"),e.classList.add("copied");let a=window.setTimeout(()=>{g(e,Ae(!1,o)),e.setAttribute("aria-label",s),e.classList.remove("copied"),zn.delete(e)},hr);zn.set(e,{label:s,timer:a})}function mt(e){e.addEventListener("click",t=>{let n=t.target instanceof Element?t.target.closest("[data-copy]"):null;n&&e.contains(n)&&Qn(n,n.dataset.copy??"")})}var No="kestrel.notices",Bo=100;function Io(){try{let e=JSON.parse(localStorage.getItem(No)||"{}");return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}function vr(e){try{localStorage.setItem(No,JSON.stringify(e))}catch{}}var Do=(e,t)=>`${e}|${t}`;function gr(e,t){let n=Io(),s=Date.now();for(let{subject:a,version:l}of t)n[Do(e,a)]={v:String(l),t:s};let o=Object.keys(n);if(o.length>Bo){o.sort((a,l)=>(n[a]?.t||0)-(n[l]?.t||0));for(let a of o.slice(0,o.length-Bo))delete n[a]}vr(n)}function fn(e,t){if(!e)return null;let{kind:n,members:s,markup:o}=t,a=s??[{subject:t.subject??"",version:t.version??""}],l=s?`${n}|*`:`${n}|${t.subject??""}`,r=a.map(y=>`${y.subject}@${y.version}`).join(","),c=[...e.children].find(y=>y instanceof HTMLElement&&y.dataset.notice===l),p=Io();if(a.every(y=>p[Do(n,y.subject)]?.v===String(y.version)))return c?.remove(),null;if(c?.dataset.noticeEvents===r)return c;let h=document.createElement("div");h.className="notice",h.setAttribute("role","status"),h.dataset.notice=l,h.dataset.noticeEvents=r,g(h,i`<span class="notice-text">${o}</span><button type="button" class="icon notice-dismiss" aria-label="Dismiss">${M("x")}</button>`);let u=h.querySelector(".notice-dismiss");if(u&&(u.onclick=()=>{gr(n,a),h.remove()}),c){let y=c.querySelector(".notice-dismiss")===document.activeElement;c.replaceWith(h),y&&u?.focus()}else e.appendChild(h);return h}function qo(e){let t=[],n=e.filter(u=>ke(u,"missed"));n.length&&t.push({level:"red",text:`${n.length} scheduled send${n.length===1?"":"s"} passed the fire time without going out.`});let s=e.flatMap(u=>{let y=be(u,"wedged");return y?[{s:u,count:y.count}]:[]}),[o]=s;if(o){let u=s.reduce((x,k)=>x+k.count,0),y=u===1?"delivery":"deliveries";t.push({level:"red",text:s.length===1?i`<a href="#/sent/${o.s.id}">${o.s.subject}</a> has ${u} ambiguous ${y} awaiting a decision; resolve ${u===1?"it":"them"} on its page.`:i`${u} ambiguous ${y} across <a href="#/sent">${s.length} sends</a> await a decision; resolve them on the Sent page.`})}let a=e.flatMap(u=>{let y=be(u,"refused");return y?[{s:u,c:y}]:[]}),[l]=a;if(l){let u=a.length===1?i`<a href="#/sent/${l.s.id}">${l.s.subject}</a>`:i`<a href="#/sent">${a.length} sends</a>`;t.push({level:"red",text:i`${u}: ${l.c.message}`})}let r=e.filter(u=>ke(u,"stuck")&&!Ct(u)),[c]=r;c&&t.push({level:"amber",text:r.length===1?i`<a href="#/sent/${c.id}">${c.subject}</a>: ${be(c,"stuck")?.message}`:i`<a href="#/sent">${r.length} sends</a> have been sending longer than they should; they may be retrying.`});let p=e.flatMap(u=>{let y=be(u,"bounce_spike");return y?[{s:u,c:y}]:[]}),[h]=p;return h&&t.push({level:"amber",text:i`<a href="#/sent/${h.s.id}">${h.s.subject}</a>: ${h.c.message}`}),t}function Oo(e){if(!e.length)return i``;let t=e.some(n=>n.level==="red")?"red":"amber";return i`<div class="health ${t}"><span class="health-dot">⚠️</span><div>${e.map(n=>i`<div>${n.text}</div>`)}</div></div>`}var Uo=e=>e.filter(t=>t.status==="sending"&&!Ct(t));async function Xn(e,t){let n=()=>{t.aborted||K(Xn)};g(e,i`<div class="dash" id="dash"><p class="muted">Loading…</p></div>`);let s=d("#dash",e),o,a,l;try{let[v,w,j]=await Promise.all([T("/api/posts?limit=200",{signal:t}),T("/api/sends?limit=200",{signal:t}),T("/api/subscribers",{signal:t})]);o=v.posts,a=w,l=j.counts}catch(v){X(s,v instanceof Error?v.message:String(v),n);return}let r=a.sends,c=new Map,p=On(Z.appConfig),h=Z.appConfig?.deployment??null,u=l.confirmed+l.pending+l.unsubscribed+l.suppressed;if(!o.length&&u===0){g(s,i`<div class="dash-head"><div><h1>${p.name}</h1>${p.tagline?i`<p class="muted dash-tagline">${p.tagline}</p>`:null}<p class="muted">Let's get your first post out the door.</p></div></div>${yr(p,h)}<section class="dash-section"><h2>API access</h2>${zo(!1)}</section>`),Go(s,n);return}let y=[{label:"Confirmed",emph:!0,v:l.confirmed,filter:"confirmed"},{label:"Pending",v:l.pending,filter:"pending"},{label:"Unsubscribed",v:l.unsubscribed,filter:"unsubscribed"},{label:"Suppressed",v:l.suppressed,filter:"suppressed"}],x=i`<div class="tiles">${y.map(v=>i`<a class="tile${v.emph?" tile-emph":""}" href="#/subscribers/${v.filter}"><span class="tile-n">${v.v}</span><span class="tile-label">${v.label}${v.sub?i`<span class="tile-sub">${v.sub}</span>`:null}</span></a>`)}</div>`,k=o.filter(v=>v.status==="draft").slice(0,5),B=k.length?i`<div class="table-wrap"><table><tbody>${k.map(v=>i`<tr class="clickable" data-id="${v.id}"><td><a href="#/edit/${v.id}">${v.subject||i`<em>untitled</em>`}</a></td><td class="muted">edited ${W(v.updated_at)}</td></tr>`)}</tbody></table></div>`:i`<p class="muted">No drafts in progress.</p>`,A=h?.appOrigin||location.origin,H=(h?.archiveOrigin||location.origin)+(h?.archiveBasePath||""),m=i`<div class="card pub-card">
69
+ <div class="pub-row"><span class="pub-key muted">Publication</span><code class="pub-val">${A}</code>${pt(A,"Copy the publication's address")}</div>
70
+ <div class="pub-row"><span class="pub-key muted">Archive</span><code class="pub-val">${H}</code>${pt(H,"Copy the archive's address")}</div>
64
71
  <div class="pub-foot"><a href="/" target="_blank" rel="noopener">View publication&nbsp;↗</a></div>
65
- </div>`,O=i.some(v=>v.author==="service"),j=xo(O),J=o`<div class="row quick-actions"><button class="primary" data-act="new-post">New post</button><button data-act="add-sub">Add subscriber</button><button data-nav="#/settings">Edit publication</button></div>`;g(s,o`
72
+ </div>`,L=o.some(v=>v.author==="service"),R=zo(L),V=i`<div class="row quick-actions"><button class="primary" data-act="new-post">New post</button><button data-act="add-sub">Add subscriber</button><button data-nav="#/settings">Edit publication</button></div>`;g(s,i`
66
73
  <div class="dash-head">
67
- <div><h1>${m.name}</h1>${m.tagline?o`<p class="muted dash-tagline">${m.tagline}</p>`:null}</div>
74
+ <div><h1>${p.name}</h1>${p.tagline?i`<p class="muted dash-tagline">${p.tagline}</p>`:null}</div>
68
75
  <button class="primary" data-act="new-post">New post</button>
69
76
  </div>
70
- <div id="dashHealth">${fo(mo(dt(c,f)))}</div>
71
- <div id="dashActive">${So(ho(dt(c,f)))}</div>
72
- <section class="dash-section"><h2>Subscribers</h2>${R}</section>
77
+ <div id="dashHealth">${Oo(qo(dt(r,c)))}</div>
78
+ <div id="dashActive">${Zo(Uo(dt(r,c)))}</div>
79
+ <section class="dash-section"><h2>Subscribers</h2>${x}</section>
73
80
  <div id="dashNotices"></div>
74
81
  <div class="dash-cols">
75
- <section class="dash-section"><h2>Scheduled</h2><div id="dashScheduled">${yo(un(c))}</div></section>
76
- <section class="dash-section"><h2>Drafts</h2>${y}</section>
82
+ <section class="dash-section"><h2>Scheduled</h2><div id="dashScheduled">${Wo(hn(r))}</div></section>
83
+ <section class="dash-section"><h2>Drafts</h2>${B}</section>
77
84
  </div>
78
- <section class="dash-section"><h2>Sent</h2><div id="dashSent">${vo(c)}</div></section>
79
- <section class="dash-section"><h2>Quick actions</h2>${J}</section>
85
+ <section class="dash-section"><h2>Sent</h2><div id="dashSent">${jo(r)}</div></section>
86
+ <section class="dash-section"><h2>Quick actions</h2>${V}</section>
80
87
  <div class="dash-cols">
81
- <section class="dash-section"><h2>Publication</h2>${L}</section>
82
- <section class="dash-section"><h2>API access</h2>${j}</section>
83
- </div>`),ko(s,n);for(let v of M("tr[data-id]",s))v.onclick=w=>{let q=w.target;q instanceof Element&&q.tagName!=="A"&&!q.closest("button")&&(location.hash=`#/edit/${v.dataset.id}`)};go(s),$o(s),wo(s),bo(s,un(c));let ie=nn(s,t);ie();let H=0,N=async()=>{let v=++H;try{let w=await T("/api/sends?limit=200",{signal:t});if(v!==H)return;c=w.sends,g(d("#dashScheduled",s),yo(un(c))),wo(s),bo(s,un(c)),ie(),g(d("#dashSent",s),vo(c)),go(s),I()}catch{}},I=()=>{let v=dt(c,f);g(d("#dashHealth",s),fo(mo(v))),g(d("#dashActive",s),So(ho(v))),$o(s),oa(s,[...f.values()])},$=v=>v==="scheduled"||v==="due";nt(a,{update:v=>{for(let w of v.sends)f.set(w.id,w);for(let w of v.removed)f.delete(w);I(),(v.changes.length||v.removed.length||v.sends.some(w=>$(Ve(w))))&&N()},stale:n},t)}var un=e=>e.filter(t=>t.status==="scheduled").sort((t,n)=>t.fire_at-n.fire_at);function vo(e){let t=e.filter(n=>n.status==="sent"||n.status==="sending").slice(0,5);return t.length?o`<div class="table-wrap"><table><thead><tr><th>Subject</th><th>Status</th><th class="num">Recipients</th><th class="num">Delivered</th><th></th></tr></thead><tbody>${t.map(n=>{let s=n.links.archive;return o`<tr class="clickable" data-send="${n.id}"><td><a href="#/sent/${n.id}">${n.subject}</a></td><td>${Je(n.status)}</td><td class="num">${n.audience.count.toLocaleString()}</td><td class="num delivered">${ct(n.counts)}</td><td class="act">${s?o`<a class="ghost-link" href="${s}" target="_blank" rel="noopener">Archive&nbsp;↗</a>`:null}</td></tr>`})}</tbody></table></div>`:o`<p class="muted">No sends yet.</p>`}function go(e){for(let t of M("tr[data-send]",e))t.onclick=n=>{let s=n.target;s instanceof Element&&s.tagName!=="A"&&(location.hash=`#/sent/${t.dataset.send}`)}}function oa(e,t){for(let n of t){let s=M("#dashSent tr[data-send]",e).find(i=>i.dataset.send===n.id)?.querySelector("td.delivered");s&&g(s,ct(n.counts))}}function bo(e,t){let n=d("#dashNotices",e),s=t.flatMap(a=>a.remade_at?[{id:a.id,at:a.remade_at}]:[]),i=s.length?Math.max(...s.map(a=>a.at)):0;cn(n,{kind:"applied",members:s.map(a=>({subject:a.id,version:a.at})),markup:ln(i,s.length,!1)})}function yo(e){return e.length?o`${e.map(t=>o`<div class="card spread clickable nextup sched-card" data-post="${t.post_id}"><div><a class="card-link sched-subj" href="#/edit/${t.post_id}">${t.subject}</a><div class="muted">${tn(t)} · ${Z(t.fire_at)} · ${t.audience.count} recipients</div></div></div>`)}`:o`<p class="muted">Nothing scheduled.</p>`}function wo(e){for(let t of M("#dashScheduled .nextup",e))t.onclick=n=>{let s=n.target;s instanceof Element&&s.tagName!=="A"&&(location.hash=`#/edit/${t.dataset.post}`)}}function So(e){return e.length?o`<section class="dash-section"><h2>Active send${e.length===1?"":"s"}</h2>${e.map(sn)}</section>`:o``}function $o(e){for(let t of M("#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 ko(e,t){for(let n of M("[data-nav]",e))n.onclick=()=>{location.hash=n.dataset.nav??""};for(let n of M("[data-act='new-post']",e))n.onclick=()=>On(n);for(let n of M("[data-act='add-sub']",e))n.onclick=()=>dn(t);for(let n of M("[data-copy]",e))n.onclick=()=>Qe(n.dataset.copy??"")}function xo(e){return e?o`<div class="card pub-card">
88
+ <section class="dash-section"><h2>Publication</h2>${m}</section>
89
+ <section class="dash-section"><h2>API access</h2>${R}</section>
90
+ </div>`),Go(s,n);for(let v of P("tr[data-id]",s))v.onclick=w=>{let j=w.target;j instanceof Element&&j.tagName!=="A"&&!j.closest("button")&&(location.hash=`#/edit/${v.dataset.id}`)};Fo(s),Yo(s),Ko(s),Vo(s,hn(r));let ee=ln(s,t);ee();let C=0,U=async()=>{let v=++C;try{let w=await T("/api/sends?limit=200",{signal:t});if(v!==C)return;r=w.sends,g(d("#dashScheduled",s),Wo(hn(r))),Ko(s),Vo(s,hn(r)),ee(),g(d("#dashSent",s),jo(r)),Fo(s),_()}catch{}},_=()=>{let v=dt(r,c);g(d("#dashHealth",s),Oo(qo(v))),g(d("#dashActive",s),Zo(Uo(v))),Yo(s),br(s,[...c.values()])},b=v=>v==="scheduled"||v==="due";tt(a,{update:v=>{for(let w of v.sends)c.set(w.id,w);for(let w of v.removed)c.delete(w);_(),(v.changes.length||v.removed.length||v.sends.some(w=>b(De(w))))&&U()},stale:n},t)}var hn=e=>e.filter(t=>t.status==="scheduled").sort((t,n)=>t.fire_at-n.fire_at);function jo(e){let t=e.filter(n=>n.status==="sent"||n.status==="sending").slice(0,5);return t.length?i`<div class="table-wrap"><table><thead><tr><th>Subject</th><th>Status</th><th class="num">Recipients</th><th class="num">Delivered</th><th></th></tr></thead><tbody>${t.map(n=>{let s=n.links.archive;return i`<tr class="clickable" data-send="${n.id}"><td><a href="#/sent/${n.id}">${n.subject}</a></td><td>${Ge(n.status)}</td><td class="num">${n.audience.count.toLocaleString()}</td><td class="num delivered">${ct(n.counts)}</td><td class="act">${s?i`<a class="ghost-link" href="${s}" target="_blank" rel="noopener">Archive&nbsp;↗</a>`:null}</td></tr>`})}</tbody></table></div>`:i`<p class="muted">No sends yet.</p>`}function Fo(e){for(let t of P("tr[data-send]",e))t.onclick=n=>{let s=n.target;s instanceof Element&&s.tagName!=="A"&&(location.hash=`#/sent/${t.dataset.send}`)}}function br(e,t){for(let n of t){let s=P("#dashSent tr[data-send]",e).find(o=>o.dataset.send===n.id)?.querySelector("td.delivered");s&&g(s,ct(n.counts))}}function Vo(e,t){let n=d("#dashNotices",e),s=t.flatMap(a=>a.remade_at?[{id:a.id,at:a.remade_at}]:[]),o=s.length?Math.max(...s.map(a=>a.at)):0;fn(n,{kind:"applied",members:s.map(a=>({subject:a.id,version:a.at})),markup:pn(o,s.length,!1)})}function Wo(e){return e.length?i`${e.map(t=>i`<div class="card spread clickable nextup sched-card" data-post="${t.post_id}"><div><a class="card-link sched-subj" href="#/edit/${t.post_id}">${t.subject}</a><div class="muted">${rn(t)} · ${W(t.fire_at)} · ${t.audience.count} recipients</div></div></div>`)}`:i`<p class="muted">Nothing scheduled.</p>`}function Ko(e){for(let t of P("#dashScheduled .nextup",e))t.onclick=n=>{let s=n.target;s instanceof Element&&s.tagName!=="A"&&(location.hash=`#/edit/${t.dataset.post}`)}}function Zo(e){return e.length?i`<section class="dash-section"><h2>Active send${e.length===1?"":"s"}</h2>${e.map(dn)}</section>`:i``}function Yo(e){for(let t of P("#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 Go(e,t){for(let n of P("[data-nav]",e))n.onclick=()=>{location.hash=n.dataset.nav??""};for(let n of P("[data-act='new-post']",e))n.onclick=()=>Yn(n);for(let n of P("[data-act='add-sub']",e))n.onclick=()=>mn(t);mt(e)}function zo(e){return e?i`<div class="card pub-card">
84
91
  <p class="conn-status"><span class="conn-dot" aria-hidden="true"></span>Claude is connected.</p>
85
- <div class="pub-foot pub-links"><a href="#/reference">${D("code")}API reference →</a><a href="#/docs/connect-claude">${D("article")}Connection guide →</a></div>
86
- </div>`:o`<div class="card pub-card">
92
+ <div class="pub-foot pub-links"><a href="#/reference">${M("code")}API reference →</a><a href="#/docs/connect-claude">${M("article")}Connection guide →</a></div>
93
+ </div>`:i`<div class="card pub-card">
87
94
  <p class="pub-note">Let Claude draft, proofread, and schedule your posts.</p>
88
- <div class="pub-foot pub-links"><a href="#/docs/connect-claude">${D("article")}Connect Claude →</a><a href="#/reference">${D("code")}API reference →</a></div>
89
- </div>`}function ia(e,t){let n=`${t?.appOrigin||location.origin}/subscribe`;return o`<div class="card setup">
95
+ <div class="pub-foot pub-links"><a href="#/docs/connect-claude">${M("article")}Connect Claude →</a><a href="#/reference">${M("code")}API reference →</a></div>
96
+ </div>`}function yr(e,t){let n=`${t?.appOrigin||location.origin}/subscribe`;return i`<div class="card setup">
90
97
  <h2 class="setup-title">Set up your publication</h2>
91
98
  <ol class="setup-steps">
92
99
  <li><div class="setup-step-main"><strong>Name your publication</strong><span class="muted">Currently “${e.name}”. Set the name, tagline, and brand in Settings.</span></div><button data-nav="#/settings">Settings</button></li>
93
100
  <li><div class="setup-step-main"><strong>Write your first post</strong><span class="muted">Draft a post in Markdown and preview it exactly as the email.</span></div><button class="primary" data-act="new-post">New post</button></li>
94
101
  <li><div class="setup-step-main"><strong>Confirm your sending domain</strong><span class="muted">SPF, DKIM, and DMARC on your From address — the setup guide walks through it.</span></div><button data-nav="#/docs/sending-domain-dns">Docs</button></li>
95
- <li><div class="setup-step-main"><strong>Share your subscribe link</strong><code class="setup-url">${n}</code></div><button data-copy="${n}">Copy</button></li>
102
+ <li><div class="setup-step-main"><strong>Share your subscribe link</strong><code class="setup-url">${n}</code></div><button type="button" data-copy="${n}" aria-label="Copy the subscribe link">${Ae()}</button></li>
96
103
  </ol>
97
- </div>`}function Eo(e){return e.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,80).replace(/-+$/g,"")}var To="post";var Lo=e=>e instanceof Error?e.message:String(e);function pn(e,t){let n=e.c_in_flight||0,i=$e(o`<h3>Resolve ${n} ambiguous ${n===1?"delivery":"deliveries"}</h3>
104
+ </div>`}var Jo=e=>e instanceof Error?e.message:String(e);function vn(e,t){let n=e.c_in_flight||0,o=he(i`<h3>Resolve ${n} ambiguous ${n===1?"delivery":"deliveries"}</h3>
98
105
  <p class="hint">A transport error left ${n} recipient${n===1?"":"s"} in flight: the request went out but the provider never confirmed, so we can't know if it was accepted. To avoid mailing anyone twice, the send won't retry ${n===1?"it":"them"} on its own — so it can't finish until you decide. Neither choice re-sends this post.</p>
99
106
  <p class="hint"><strong>Assume not sent</strong> — recorded as unsent; ${n===1?"the address is":"the addresses are"} simply picked up by your next post.</p>
100
107
  <p class="hint"><strong>Assume sent</strong> — recorded as delivered. Choose this only if you've confirmed it in your provider's console.</p>
101
- <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>`);d("#rCancel",i.el).onclick=i.close;let a=(f,m,u)=>X(f,"Resolving\u2026",async()=>{try{let r=await T(`/api/sends/${e.id}/resolve`,{method:"POST",json:{resolution:m,expected_count:n}});i.close(),k(r.completed?"Send completed":`Marked ${u}`),t()}catch(r){k(Lo(r))}}),l=d("#rUnsent",i.el),c=d("#rAccepted",i.el);l.onclick=()=>a(l,"unsent","not sent"),c.onclick=()=>a(c,"accepted","sent")}function mn(e,t,n){let s=it(Vt()),i=it(new Date(t)),a=$e(o`<h3 id="rsHead">Reschedule this post</h3>
102
- <p class="hint">Move when it sends (at least ${bt()} out). The content stays frozen and the cancelable window is kept — only the time changes.</p>
103
- <label for="rsWhen">Send at</label><input type="datetime-local" id="rsWhen" min="${s}" value="${i}">
104
- <div class="actions"><button type="button" id="rsCancel">Cancel</button><button type="button" class="primary" id="rsGo">Reschedule</button></div>`),l=d(".modal",a.el);l.setAttribute("aria-labelledby","rsHead");let c=d("#rsGo",l),f=d("#rsWhen",l);d("#rsCancel",l).onclick=a.close,c.onclick=()=>X(c,"Rescheduling\u2026",async()=>{let m=f.value,u=m?new Date(m).getTime():Number.NaN;if(Number.isNaN(u)){k("Pick a valid date & time");return}try{await T(`/api/sends/${e}/reschedule`,{method:"POST",json:{fire_at:new Date(u).toISOString()}}),a.close(),k("Rescheduled"),n()}catch(r){k(Lo(r))}}),f.focus()}function Co(e){return e.replace(/\{\{\s*(?:if\s+)?[\w.]+\s*\}\}/g,t=>`<span class="cx-var">${t}</span>`)}function aa(e){let t=ee(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>`),Co(t)}function ra(e){let t=ee(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,a)=>`${s}<span class="cx-sel">${i}</span>${a}{`),t=t.replace(/^(\s*)([a-z-]+)(\s*:)/gm,(n,s,i,a)=>`${s}<span class="cx-prop">${i}</span>${a}`),Co(t)}function Ho(e){return F(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>${ra(n[1]??"")}<span class="cx-punct">&lt;/</span><span class="cx-tag">style</span><span class="cx-punct">&gt;</span>`:aa(t)}).join(""))}function Vn(e){return ee(e).replace(/(`[^`\n]+`)|(!?)(\[[^\]\n]*\])\(([^)\n]*)\)|(\*\*|__)([^\n]+?)\5|\*([^*\n]+?)\*|\b_([^_\n]+?)_\b/g,(t,n,s,i,a,l,c,f,m)=>{if(n)return`<span class="cx-md-code">${n}</span>`;if(i!==void 0)return`${s||""}${i}(<span class="cx-md-url">${a}</span>)`;if(l)return`<span class="cx-md-mark">${l}</span><span class="cx-md-strong">${c}</span><span class="cx-md-mark">${l}</span>`;let[u,r]=f!==void 0?["*",f]:["_",m];return`<span class="cx-md-mark">${u}</span><span class="cx-md-em">${r}</span><span class="cx-md-mark">${u}</span>`})}function la(e){let t=e.match(/^(\s{0,3}#{1,6})(\s.*)?$/);if(t)return`<span class="cx-md-hmark">${ee(t[1]??"")}</span><span class="cx-md-htext">${ee(t[2]||"")}</span>`;if(/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(e))return`<span class="cx-md-bmark">${ee(e)}</span>`;let n=e.match(/^(\s{0,3})((?:>\s?)+)(.*)$/);if(n)return`${ee(n[1]??"")}<span class="cx-md-bmark">${ee(n[2]??"")}</span>${Vn(n[3]??"")}`;let s=e.match(/^(\s*)([-*+]|\d{1,9}[.)])(\s+)(.*)$/);return s?`${s[1]}<span class="cx-md-bmark">${ee(s[2]??"")}</span>${s[3]}${Vn(s[4]??"")}`:Vn(e)}function Mo(e){let t=!1;return F(String(e).split(`
105
- `).map(n=>{let s,i=!1,a=n.match(/^(\s*)(```+|~~~+)(.*)$/);if(a){t=!t;let l=a[3]?`<span class="cx-md-bmark">${ee(a[3])}</span>`:"";s=`${a[1]}${ee(a[2]??"")}${l}`}else t?(i=!0,s=ee(n)):s=la(n);return`<span class="cx-md-ln${i?" cx-md-band":""}">${s}</span>`}).join(""))}var da=/("(?:\\.|[^"\\])*")(\s*:)?|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|\b(true|false|null)\b|([{}[\],])/g;function Ro(e){let t=JSON.stringify(e,null,2)??"",n="",s=0;for(let i of t.matchAll(da)){n+=ee(t.slice(s,i.index));let[a,l,c,f,m]=i;l!==void 0?n+=c?`<span class="cx-prop">${ee(l)}</span><span class="cx-punct">${ee(c)}</span>`:`<span class="cx-str">${ee(l)}</span>`:f!==void 0?n+=`<span class="cx-num">${f}</span>`:m!==void 0?n+=`<span class="cx-tag">${m}</span>`:n+=`<span class="cx-punct">${ee(a)}</span>`,s=i.index+a.length}return F(n+ee(t.slice(s)))}function Po(e){return F(ee(e).replace(/^curl\b/,'<span class="cx-tag">curl</span>').replace(/(^|\s)(-[A-Za-z])\b/g,'$1<span class="cx-num">$2</span>').replace(/\$[A-Z_]+/g,'<span class="cx-var">$&</span>'))}var ca={idleMs:5e3,maxMs:3e4};function Ao(e,t=ca){let n=null,s=null,i=()=>{n!==null&&clearTimeout(n),s!==null&&clearTimeout(s),n=null,s=null},a=()=>{i(),e()};return{touch(){n!==null&&clearTimeout(n),n=setTimeout(a,t.idleMs),s===null&&(s=setTimeout(a,t.maxMs))},cancel:i,get pending(){return n!==null||s!==null}}}var fn=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}};function _o(e){if(!(e instanceof he)||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}:{kind:"locked"}}var hn=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}:"fresh"}};var Me=e=>e instanceof Error?e.message:String(e),ua=[[["heading","Heading"],["bold","Bold (\u2318B)"],["italic","Italic (\u2318I)"]],[["quote","Quote"],["code","Code"],["link","Link (\u2318K)"]],[["ul","Bulleted list"],["ol","Numbered list"],["indent","Indent"]]],pa=o` readonly`,ma=o` aria-disabled="true"`;async function Kn(e,t,n){let s=()=>{n.aborted||Q((p,x)=>Kn(e,p,x))};g(t,o`<p class="muted">Loading…</p>`);let i,a=null;try{if(i=await T(`/api/posts/${e}`,{signal:n}),n.aborted)return;let p=ha(i);if(p){Tt(p);return}}catch(p){oe(t,Me(p),s);return}if(i.scheduled){try{a=await T(`/api/sends/${i.scheduled.id}`,{signal:n})}catch(p){if(n.aborted)return;if(p instanceof he&&p.status===404){k("This post was deleted elsewhere."),Tt("#/drafts");return}}if(n.aborted||a&&Bo(a.send,s))return}let{post:l,markdown:c,scheduled:f}=i,m=l.status!=="draft",u=new hn(l.current_revision),r=ua.map((p,x)=>o`${x?o`<span class="sep"></span>`:null}${p.map(([S,C])=>o`<button type="button" class="tb" data-fmt="${S}" title="${C}" aria-label="${C}"${m?ma:null}>${D(S)}</button>`)}`),b=m?pa:null;g(t,o`
108
+ <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>`);d("#rCancel",o.el).onclick=o.close;let a=(c,p,h)=>z(c,"Resolving\u2026",async()=>{try{let u=await T(`/api/sends/${e.id}/resolve`,{method:"POST",json:{resolution:p}});o.close(),$(u.completed?"Send completed":`Marked ${h}`),t()}catch(u){$(Jo(u))}}),l=d("#rUnsent",o.el),r=d("#rAccepted",o.el);l.onclick=()=>a(l,"unsent","not sent"),r.onclick=()=>a(r,"accepted","sent")}function gn(e,t,n){let s=it(Yt()),o=it(new Date(t)),a=he(i`<h3 id="rsHead">Reschedule this post</h3>
109
+ <p class="hint">Move when it sends (at least ${kt()} out). The content stays frozen and the cancelable window is kept — only the time changes.</p>
110
+ <label for="rsWhen">Send at</label><input type="datetime-local" id="rsWhen" min="${s}" value="${o}">
111
+ <div class="actions"><button type="button" id="rsCancel">Cancel</button><button type="button" class="primary" id="rsGo">Reschedule</button></div>`),l=d(".modal",a.el);l.setAttribute("aria-labelledby","rsHead");let r=d("#rsGo",l),c=d("#rsWhen",l);d("#rsCancel",l).onclick=a.close,r.onclick=()=>z(r,"Rescheduling\u2026",async()=>{let p=c.value,h=p?new Date(p).getTime():Number.NaN;if(Number.isNaN(h)){$("Pick a valid date & time");return}try{await T(`/api/sends/${e}/reschedule`,{method:"POST",json:{fire_at:new Date(h).toISOString()}}),a.close(),$("Rescheduled"),n()}catch(u){if(u instanceof se&&u.status===404){a.close(),$("This post was deleted elsewhere."),nt();return}$(Jo(u))}}),c.focus()}function Qo(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 Xo=e=>e.preventDefault(),ft="allow-same-origin allow-popups allow-popups-to-escape-sandbox";function ht(e){for(let t of e.querySelectorAll("a[href], area[href]")){if(t.getAttribute("href")?.startsWith("#")){t.addEventListener("click",Xo),t.addEventListener("auxclick",Xo);continue}t.target="_blank",t.rel="noopener noreferrer"}}function wr(e,t){let n=/<head(\s[^>]*)?>/i;return n.test(e)?e.replace(n,s=>s+t):t+e}function bn(e){return wr(e,`<meta http-equiv="Content-Security-Policy" content="${Qo("'none'",{framed:!0})}">`)}function ei(e){return e.replace(/\{\{\s*(?:if\s+)?[\w.]+\s*\}\}/g,t=>`<span class="cx-var">${t}</span>`)}function Sr(e){let t=Q(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,o)=>`<span class="cx-punct">${s}</span><span class="cx-tag">${o}</span>`),t=t.replace(/(\/?)&gt;/g,(n,s)=>`<span class="cx-punct">${s}&gt;</span>`),ei(t)}function $r(e){let t=Q(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,o,a)=>`${s}<span class="cx-sel">${o}</span>${a}{`),t=t.replace(/^(\s*)([a-z-]+)(\s*:)/gm,(n,s,o,a)=>`${s}<span class="cx-prop">${o}</span>${a}`),ei(t)}function ti(e){return D(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>${$r(n[1]??"")}<span class="cx-punct">&lt;/</span><span class="cx-tag">style</span><span class="cx-punct">&gt;</span>`:Sr(t)}).join(""))}function es(e){return Q(e).replace(/(`[^`\n]+`)|(!?)(\[[^\]\n]*\])\(([^)\n]*)\)|(\*\*|__)([^\n]+?)\5|\*([^*\n]+?)\*|\b_([^_\n]+?)_\b/g,(t,n,s,o,a,l,r,c,p)=>{if(n)return`<span class="cx-md-code">${n}</span>`;if(o!==void 0)return`${s||""}${o}(<span class="cx-md-url">${a}</span>)`;if(l)return`<span class="cx-md-mark">${l}</span><span class="cx-md-strong">${r}</span><span class="cx-md-mark">${l}</span>`;let[h,u]=c!==void 0?["*",c]:["_",p];return`<span class="cx-md-mark">${h}</span><span class="cx-md-em">${u}</span><span class="cx-md-mark">${h}</span>`})}function kr(e){let t=e.match(/^(\s{0,3}#{1,6})(\s.*)?$/);if(t)return`<span class="cx-md-hmark">${Q(t[1]??"")}</span><span class="cx-md-htext">${Q(t[2]||"")}</span>`;if(/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(e))return`<span class="cx-md-bmark">${Q(e)}</span>`;let n=e.match(/^(\s{0,3})((?:>\s?)+)(.*)$/);if(n)return`${Q(n[1]??"")}<span class="cx-md-bmark">${Q(n[2]??"")}</span>${es(n[3]??"")}`;let s=e.match(/^(\s*)([-*+]|\d{1,9}[.)])(\s+)(.*)$/);return s?`${s[1]}<span class="cx-md-bmark">${Q(s[2]??"")}</span>${s[3]}${es(s[4]??"")}`:es(e)}function ni(e){let t=!1;return D(String(e).split(`
112
+ `).map(n=>{let s,o=!1,a=n.match(/^(\s*)(```+|~~~+)(.*)$/);if(a){t=!t;let l=a[3]?`<span class="cx-md-bmark">${Q(a[3])}</span>`:"";s=`${a[1]}${Q(a[2]??"")}${l}`}else t?(o=!0,s=Q(n)):s=kr(n);return`<span class="cx-md-ln${o?" cx-md-band":""}">${s}</span>`}).join(""))}var xr=/("(?:\\.|[^"\\])*")(\s*:)?|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|\b(true|false|null)\b|([{}[\],])/g;function si(e){let t=JSON.stringify(e,null,2)??"",n="",s=0;for(let o of t.matchAll(xr)){n+=Q(t.slice(s,o.index));let[a,l,r,c,p]=o;l!==void 0?n+=r?`<span class="cx-prop">${Q(l)}</span><span class="cx-punct">${Q(r)}</span>`:`<span class="cx-str">${Q(l)}</span>`:c!==void 0?n+=`<span class="cx-num">${c}</span>`:p!==void 0?n+=`<span class="cx-tag">${p}</span>`:n+=`<span class="cx-punct">${Q(a)}</span>`,s=o.index+a.length}return D(n+Q(t.slice(s)))}function oi(e){return D(Q(e).replace(/^curl\b/,'<span class="cx-tag">curl</span>').replace(/(^|\s)(-[A-Za-z])\b/g,'$1<span class="cx-num">$2</span>').replace(/\$[A-Z_]+/g,'<span class="cx-var">$&</span>'))}var Er={idleMs:5e3,maxMs:3e4};function ii(e,t=Er){let n=null,s=null,o=()=>{n!==null&&clearTimeout(n),s!==null&&clearTimeout(s),n=null,s=null},a=()=>{o(),e()};return{touch(){n!==null&&clearTimeout(n),n=setTimeout(a,t.idleMs),s===null&&(s=setTimeout(a,t.maxMs))},cancel:o,get pending(){return n!==null||s!==null}}}var yn=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 ai=Promise.resolve();function ts(e){ai=e}function ri(){return ai.then(()=>{},()=>{})}function li(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}:{kind:"locked"}}var wn=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}:"fresh"}};function ci(e){return`#/web/post/${encodeURIComponent(e)}`}function ui(e,t){if(e!=="post"||!t)return null;try{return decodeURIComponent(t)}catch{return null}}function Tr(e){return e.replace(/^https?:\/\//,"")}function Lr(e){let t=i`<span class="web-bar-tag">Web version</span>`;if(e===null)return t;let n=e.scheduled?"Scheduled":"Draft",s=e.url?i` · will be published at ${Tr(e.url)}`:null;return i`${t} <span class="web-bar-text">${n}${s}</span>`}function di(e){try{return new URL(e).href}catch{return null}}function pi(e,t,n){let s=t?di(t):null;if(s)for(let o of e.querySelectorAll("a[href], area[href]")){let a=o.getAttribute("href");a&&di(a)===s&&o.setAttribute("href",n)}}function Hr(e){location.replace(e)}async function Rr(e,t){try{return await T(`/api/posts/${encodeURIComponent(e)}`,{signal:t})}catch{return null}}async function ns(e,t,n){let s=document.title;document.title="Web version",Ke(n,()=>{document.title=s});try{await ri();let[o,a]=await Promise.all([Qe(`/api/posts/${encodeURIComponent(e)}/web`,{signal:n}),Rr(e,n)]);if(n.aborted)return;let l=a?Gt(a.post.slug):null;if(a&&(a.sending||a.post.status==="sent")&&l){Hr(l);return}let r=Lr(a?{scheduled:a.scheduled!=null,url:l}:null);g(t,i`<div class="web-page">
113
+ <div class="web-bar" role="note"><div class="web-bar-in">${r}</div></div>
114
+ <iframe class="web-frame" sandbox="${ft}" title="Web version" scrolling="no"></iframe>
115
+ </div>`);let c=t.querySelector(".web-frame");if(!c)return;c.onload=()=>{let p=c.contentDocument;if(!p||n.aborted)return;ht(p),p.title&&(document.title=`Web version \xB7 ${p.title}`);let h=()=>{let u=p.body?.scrollHeight;u&&(c.style.height=`${u}px`)};if(h(),p.body&&typeof ResizeObserver<"u"){let u=new ResizeObserver(h);u.observe(p.body),Ke(n,()=>u.disconnect())}},c.srcdoc=bn(o)}catch(o){if(n.aborted)return;g(t,i`<div class="web-error"></div>`);let a=t.querySelector(".web-error");if(a){let r=o instanceof se&&o.status===404?"There's no post here. It may have been deleted.":o instanceof Error?o.message:String(o);X(a,r,()=>K((c,p)=>ns(e,c,p)))}}}var ge=e=>e instanceof Error?e.message:String(e),Cr=[[["heading","Heading"],["bold","Bold (\u2318B)"],["italic","Italic (\u2318I)"]],[["quote","Quote"],["code","Code"],["link","Link (\u2318K)"]],[["ul","Bulleted list"],["ol","Numbered list"],["indent","Indent"]]],Mr=i` readonly`,Pr=i` aria-disabled="true"`;async function ss(e,t,n){let s=()=>{n.aborted||K((f,E)=>ss(e,f,E))},o=!1,a=(f="This post was deleted elsewhere.")=>{o||(o=!0,$(f),n.aborted||Sn("#/drafts"))};g(t,i`<p class="muted">Loading…</p>`);let l,r=null;try{if(l=await T(`/api/posts/${e}`,{signal:n}),n.aborted)return;let f=_r(l);if(f){Sn(f);return}}catch(f){if(xe(f)){a("There's no post here. It may have been deleted.");return}X(t,ge(f),s);return}if(l.scheduled){try{r=await T(`/api/sends/${l.scheduled.id}`,{signal:n})}catch(f){if(n.aborted)return;if(xe(f)){a();return}}if(n.aborted||r&&mi(r.send,s))return}let{post:c,markdown:p,scheduled:h}=l,u=c.status!=="draft",y=new wn(c.current_revision),x=Cr.map((f,E)=>i`${E?i`<span class="sep"></span>`:null}${f.map(([S,N])=>i`<button type="button" class="tb" data-fmt="${S}" title="${N}" aria-label="${N}"${u?Pr:null}>${M(S)}</button>`)}`),k=u?Mr:null,B=`${location.origin}${location.pathname}${ci(e)}`;g(t,i`
106
116
  <div class="editor-head">
107
117
  <a href="#/drafts" class="back">← Drafts</a>
108
118
  <div class="editor-head-right">
109
- <button type="button" class="ghost" id="openBtn">Open in browser ↗</button>
119
+ <a class="ghost btn" id="webLink" href="${B}" target="_blank" rel="noopener" aria-label="Web version (opens in a new tab)">Web version ↗</a>
110
120
  </div>
111
121
  </div>
112
- ${m&&f?o`<div class="banner banner-scheduled"><span id="schedWhen"></span><span class="row" id="schedControls"><button type="button" class="ghost" id="rescheduleSchedule">Reschedule</button><button type="button" class="ghost" id="cancelSchedule">Cancel</button></span></div>`:null}
122
+ ${u&&h?i`<div class="banner banner-scheduled"><span id="schedWhen"></span><span class="row" id="schedControls"><button type="button" class="ghost" id="rescheduleSchedule">Reschedule</button><button type="button" class="ghost" id="cancelSchedule">Cancel</button></span></div>`:null}
113
123
  <div id="editorNotices"></div>
114
124
  <div id="freshnessBanner" class="banner banner-conflict" role="alert" hidden></div>
115
125
  <div class="card">
116
126
  <div class="grid2">
117
- <div><label for="f-subject">Subject</label><input id="f-subject" value="${l.subject}" placeholder="Untitled" aria-describedby="f-subject-error"${b}><div class="field-error" id="f-subject-error" role="alert" hidden><span class="field-error-ico" aria-hidden="true">!</span><span>Add a subject before you schedule.</span></div></div>
127
+ <div><label for="f-subject">Subject</label><input id="f-subject" value="${c.subject}" placeholder="Untitled" aria-describedby="f-subject-error"${k}><div class="field-error" id="f-subject-error" role="alert" hidden><span class="field-error-ico" aria-hidden="true">!</span><span>Add a subject before you schedule.</span></div></div>
118
128
  <div>
119
129
  <div class="label-row">
120
130
  <label for="f-slug">Slug</label>
121
- ${Jt("The web address of this post's archive page.")}
131
+ ${sn("The web address of this post's archive page.")}
122
132
  </div>
123
- <input id="f-slug" value="${l.slug}"${b}>
124
- ${m?null:o`<label class="slug-auto-toggle"><input type="checkbox" id="f-slug-auto">Auto-generate from subject</label>`}
133
+ <input id="f-slug" value="${c.slug}"${k}>
134
+ ${u?null:i`<label class="slug-auto-toggle"><input type="checkbox" id="f-slug-auto">Auto-generate from subject</label>`}
125
135
  </div>
126
136
  </div>
127
137
 
@@ -129,114 +139,114 @@
129
139
  <div class="composer">
130
140
  <div class="composer-head">
131
141
  <div class="ctabs" role="tablist">
132
- <button type="button" class="ctab active" data-tab="edit" data-text="Edit" role="tab" aria-selected="true"><span class="ctab-label">${D("editable")}Edit</span></button>
133
- <button type="button" class="ctab" data-tab="preview" data-text="Preview" role="tab" aria-selected="false"><span class="ctab-label">${D("preview")}Preview</span></button>
142
+ <button type="button" class="ctab active" data-tab="edit" data-text="Edit" role="tab" aria-selected="true"><span class="ctab-label">${M("editable")}Edit</span></button>
143
+ <button type="button" class="ctab" data-tab="preview" data-text="Preview" role="tab" aria-selected="false"><span class="ctab-label">${M("preview")}Preview</span></button>
134
144
  </div>
135
- <div class="toolbar" role="toolbar" aria-label="Formatting">${r}</div>
145
+ <div class="toolbar" role="toolbar" aria-label="Formatting">${x}</div>
136
146
  </div>
137
- <div class="composer-body${m?" locked":""}" id="composerBody">
147
+ <div class="composer-body${u?" locked":""}" id="composerBody">
138
148
  <pre class="md-hl" id="mdHl" aria-hidden="true"><code></code></pre>
139
- <textarea id="f-markdown" class="editor"${m?null:o` placeholder="Type your post in Markdown…"`}${b}>${c}</textarea>
140
- <iframe id="previewFrame" class="preview" sandbox="allow-same-origin" title="Email preview" hidden></iframe>
149
+ <textarea id="f-markdown" class="editor"${u?null:i` placeholder="Type your post in Markdown…"`}${k}>${p}</textarea>
150
+ <iframe id="previewFrame" class="preview" sandbox="${ft}" title="Email preview" hidden></iframe>
141
151
  </div>
142
- ${m?o`<div class="composer-foot composer-foot-lock" id="lockFoot" role="status">${D("readonly")}<span>Cancel the schedule to edit</span></div>`:o`<div class="composer-foot" id="dropFoot">${D("paperclip")}<span>Paste, drop, or click to add images</span></div>`}
152
+ ${u?i`<div class="composer-foot composer-foot-lock" id="lockFoot" role="status">${M("readonly")}<span>Cancel the schedule to edit</span></div>`:i`<div class="composer-foot" id="dropFoot">${M("paperclip")}<span>Paste, drop, or click to add images</span></div>`}
143
153
  <input type="file" id="imgInput" accept="image/png,image/jpeg,image/webp,image/gif" multiple hidden>
144
154
  </div>
145
155
  <div id="warnings"></div>
146
156
 
147
157
  <div class="actions-bar">
148
- ${m?o`<div class="row"><button type="button" class="secondary" id="testBtn">${D("send")}<span>Send test email</span></button></div>`:o`<div class="row">
158
+ ${u?i`<div class="row"><button type="button" class="secondary" id="testBtn">${M("send")}<span>Send test email</span></button></div>`:i`<div class="row">
149
159
  <button type="button" class="ghost" id="saveBtn">Save draft</button>
150
160
  <span class="save-status" id="saveStatus" aria-live="polite"></span>
151
161
  </div>
152
162
  <div class="row">
153
- <button type="button" class="secondary" id="testBtn">${D("send")}<span>Send test email</span></button>
163
+ <button type="button" class="secondary" id="testBtn">${M("send")}<span>Send test email</span></button>
154
164
  <button type="button" class="primary" id="scheduleBtn">Schedule</button>
155
165
  </div>`}
156
166
  </div>
157
- </div>`);let R=d("#f-subject"),E=d("#f-slug"),y=d("#f-markdown"),K=d(".toolbar",t),V=d("#previewFrame"),L=d("#mdHl"),O=d("code",L),j=()=>{L.scrollTop=y.scrollTop,L.scrollLeft=y.scrollLeft},J=()=>{g(O,Mo(y.value)),j()};y.addEventListener("scroll",j),J();let ie=()=>({subject:R.value,slug:E.value,markdown:y.value}),H=d("#f-subject-error");function N(){R.classList.remove("is-invalid"),H.hidden=!0}function I(){return R.value.trim()===""?(R.classList.add("is-invalid"),H.hidden=!1,R.focus(),!1):(N(),!0)}if(!m){let p=d("#f-slug-auto"),x=()=>Eo(R.value),S=x()||To,C=E.value.trim();p.checked=C===""||C===S||new RegExp(`^${S}-\\d+$`).test(C);let P=()=>E.classList.toggle("slug-auto",p.checked);P(),p.checked&&C===""&&(E.value=x()),R.addEventListener("input",()=>{p.checked&&(E.value=x())}),E.addEventListener("input",()=>{p.checked=E.value.trim()==="",P()}),p.addEventListener("change",()=>{P(),p.checked?E.value=x():(E.focus(),E.select()),be()}),E.addEventListener("blur",()=>{E.value.trim()===""&&(p.checked=!0,P(),E.value=x(),be())}),R.addEventListener("input",()=>{R.value.trim()!==""&&N()})}let $=Ao(()=>{Y(!0).catch(p=>k(`Couldn't autosave \u2014 ${Me(p)}`))});bs(n,()=>$.cancel());let v=M(".ctab",t);function w(p){for(let x of v){let S=x.dataset.tab===p;x.classList.toggle("active",S),x.setAttribute("aria-selected",S?"true":"false")}y.hidden=p!=="edit",L.hidden=p!=="edit",V.hidden=p!=="preview",K.classList.toggle("off",p!=="edit"),p==="edit"&&j()}async function q(){w("preview");try{m||await Y(!0),V.srcdoc=await _n(`/api/posts/${e}/preview`),V.onload=()=>{try{let p=V.contentDocument;p&&(V.style.height=`${p.body.scrollHeight+24}px`)}catch{}}}catch(p){k(Me(p))}}for(let p of v)p.onclick=()=>p.dataset.tab==="preview"?q():w("edit");m&&q();let _=m?d("#lockFoot"):null,G;function ve(){_&&(_.classList.remove("nudge"),requestAnimationFrame(()=>_.classList.add("nudge")),clearTimeout(G),G=setTimeout(()=>_.classList.remove("nudge"),900))}function W(p,x,S){let C=y.selectionStart,P=y.selectionEnd,fe=y.value.slice(C,P)||S;y.value=y.value.slice(0,C)+p+fe+x+y.value.slice(P),y.focus(),y.selectionStart=C+p.length,y.selectionEnd=C+p.length+fe.length}function le(p){let x=y.selectionStart,S=y.selectionEnd,C=y.value.lastIndexOf(`
158
- `,x-1)+1,P=(y.value.slice(C,S)||"").split(`
159
- `).map(fe=>p+fe).join(`
160
- `);y.value=y.value.slice(0,C)+P+y.value.slice(S),y.focus(),y.selectionStart=C,y.selectionEnd=C+P.length}function de(p){if(m){ve();return}if(w("edit"),p==="bold")W("**","**","bold text");else if(p==="italic")W("*","*","italic text");else if(p==="heading")le("## ");else if(p==="quote")le("> ");else if(p==="ul")le("- ");else if(p==="ol")le("1. ");else if(p==="indent")le(" ");else if(p==="link")W("[","](https://)","link text");else if(p==="code"){let x=y.selectionStart,S=y.selectionEnd;x===S||y.value.slice(x,S).includes(`
161
- `)?W("```\n","\n```","code"):W("`","`","code")}be()}for(let p of M(".tb[data-fmt]",t))p.onclick=()=>de(p.dataset.fmt??"");y.addEventListener("keydown",p=>{if(!(p.metaKey||p.ctrlKey))return;let x=p.key.toLowerCase();x==="b"?(p.preventDefault(),de("bold")):x==="i"?(p.preventDefault(),de("italic")):x==="k"&&(p.preventDefault(),de("link"))});let ke=!1,Re=!1,pe=!1,me=m?null:d("#saveBtn"),xe=m?null:d("#saveStatus"),B=new fn(ie),U=!1;function re(){xe&&(m?(xe.className="save-status",xe.textContent=""):U?(xe.className="save-status is-saving",xe.textContent="Saving\u2026"):ke?(xe.className="save-status is-dirty",xe.textContent="Unsaved changes"):(xe.className="save-status is-saved",xe.textContent="Saved"))}function ge(){ke=B.dirty,re()}re();function Pe(){$.touch()}function be(){m||(J(),ge(),pe||Pe())}let Ee=Promise.resolve();function Y(p){let x=Ee.catch(()=>{}).then(()=>Xe(p));return Ee=x,x}async function Xe(p){if(p&&!B.dirty)return"unchanged";if(pe)return"refused";$.cancel(),U=!0,re();let x=ie(),S=JSON.stringify(x);try{let C={...x,base_revision:u.base},{post:P}=await T(`/api/posts/${e}`,{method:"PUT",json:C});return P.slug&&document.activeElement!==E&&(E.value=P.slug),u.saved(P.current_revision),B.markSaved(S),Re=!1,p||k("Saved"),"saved"}catch(C){let P=_o(C);if(P)return Ze(P),"refused";throw Re=!0,C}finally{U=!1,ge()}}me&&(me.onclick=()=>{if(pe){Te.scrollIntoView({block:"nearest"});return}X(me,"Saving\u2026",()=>Y(!1).catch(p=>k(Me(p)))).finally(ge)});function Ct(){me&&(pe?(me.setAttribute("aria-disabled","true"),me.title="Saving is paused: choose Reload or Keep editing above"):(me.removeAttribute("aria-disabled"),me.removeAttribute("title")))}let Ht=()=>{if($.cancel(),m||!B.dirty)return;let p={...ie(),base_revision:u.base};B.markSaved(),ke=!1,Ee=Ee.catch(()=>{}).then(()=>T(`/api/posts/${e}`,{method:"PUT",json:p})).catch(x=>k(x instanceof he&&x.status===409?"Changed elsewhere \u2014 your edits weren't saved":`Couldn't save your changes \u2014 ${Me(x)}`))},mt={dirty:()=>ke,beforeLeave(){return ke&&(Re||pe)?"confirm":(Ht(),"leave")},manualSave(){me&&!me.disabled&&me.click()}};if(!m){for(let p of[R,E,y])p.addEventListener("input",be);for(let p of[R,E])p.addEventListener("blur",()=>Y(!0).catch(x=>k(`Couldn't save \u2014 ${Me(x)}`)))}let Te=d("#freshnessBanner"),Mt=p=>p==="service"?"Claude":p||null;function Rt(){pe=!1,Ct(),u.clearWarning(),Te.hidden=!0,g(Te,o``)}function Ze(p){if(m)return;if(pe=!0,Ct(),u.noteWarned(p),p.kind==="locked")g(Te,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 S=Mt(p.author);g(Te,o`<span><span aria-hidden="true">⚠️</span> This draft was changed elsewhere${S?o` — last edited by <strong>${S}</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>`)}Te.hidden=!1,d("#freshReload",Te).onclick=()=>{Rt(),s()};let x=Te.querySelector("#freshKeep");x&&(x.onclick=()=>{u.adoptWarned(),Rt(),ge(),ke&&Pe()})}if(!m){let p=async()=>{if(U||pe||document.hidden)return;let x=u.base;try{let S=await T(`/api/posts/${e}`,{signal:n}),C=u.decide({status:S.post.status,revision:S.post.current_revision,author:S.author},{saving:U,baseAtRequest:x});C!=="ignore"&&C!=="fresh"&&Ze(C)}catch{}};Ge(1e4,p,n),va(p,n)}let Pt=d("#openBtn");if(Pt.onclick=()=>X(Pt,"Opening\u2026",async()=>{try{m||await Y(!0);let p=await _n(`/api/posts/${e}/preview`),x=URL.createObjectURL(new Blob([p],{type:"text/html"}));window.open(x,"_blank"),setTimeout(()=>URL.revokeObjectURL(x),1e4)}catch(p){k(Me(p))}}),m&&f){let p=a?.send??{...f,phase:"scheduled",conditions:[]},x=d("#schedWhen"),S=d("#schedControls"),C="",P=0,fe=!1,ye=d("#rescheduleSchedule"),te=d("#cancelSchedule"),Ce=ue=>p.actions===void 0||Ie({actions:p.actions},ue),we=()=>{let ue=Date.now(),ne=p.phase==="due"||ue>=p.fire_at;ne?fe||(ye.hidden=!0,te.hidden=!0):(fe=!0,ye.hidden=!Ce("reschedule"),te.hidden=!Ce("cancel")),ye.disabled=ne,te.disabled=ne,S.hidden=ye.hidden&&te.hidden;let ae=fa(p,ne),Oe=String(ae);Oe!==C&&(C=Oe,g(x,ae)),!ne&&p.fire_at-ue<=1e3&&p.fire_at!==P&&(P=p.fire_at,Nt(p.fire_at,we,n))};we(),Ge(1e3,we,n);let ce=()=>{p.remade_at&&cn(d("#editorNotices"),{kind:"applied",subject:p.id,version:p.remade_at,markup:ln(p.remade_at,1,!0)})};ce();let He=ue=>$t(ue,{update(ne,ae){if(p=ne,ae?.to==="sending"||ae?.to==="sent"||ae?.to==="complete"){k("Your send started, so this page switched to its live watch."),Tt(`#/sent/${ne.id}`);return}if(ae?.to==="canceled"){k("The schedule was canceled elsewhere, so this post is a draft again."),s();return}we(),ce()},removed(){k("This post was deleted elsewhere."),Tt("#/drafts")},stale:s},n);a?He(a):gs(5e3,async()=>{let ue=await T(`/api/sends/${f.id}`,{signal:n});return Bo(ue.send,s)||(p=ue.send,we(),ce(),He(ue)),!1},n),ye.onclick=()=>mn(p.id,p.fire_at,s),te.onclick=()=>X(te,"Canceling\u2026",async()=>{try{await T(`/api/sends/${f.id}/cancel`,{method:"POST"}),k("Schedule canceled"),s()}catch(ue){k(Me(ue))}}).finally(we)}async function ft(p){if(p.type.startsWith("image/"))try{let x=new FormData;x.append("file",p);let{image:S}=await T(`/api/posts/${e}/images`,{method:"POST",body:x}),C=y.selectionStart,P=`
162
- ![${p.name}](${S.filename})
163
- `;y.value=y.value.slice(0,C)+P+y.value.slice(C),y.selectionStart=y.selectionEnd=C+P.length,be(),k("Image added")}catch(x){k(Me(x))}}let Ae=d("#composerBody");if(m){let p=S=>S.metaKey||S.ctrlKey?["x","z","y"].includes(S.key.toLowerCase()):S.key.length===1||S.key==="Enter"||S.key==="Backspace"||S.key==="Delete",x=[y,R,E];for(let S of x)S.addEventListener("keydown",C=>{p(C)&&ve()}),S.addEventListener("paste",ve);Ae.addEventListener("dragover",S=>S.preventDefault()),Ae.addEventListener("drop",S=>{S.preventDefault(),ve()})}else{let p=d("#imgInput"),x=d("#dropFoot");Ae.addEventListener("dragover",S=>{S.preventDefault(),Ae.classList.add("dragover")}),Ae.addEventListener("dragleave",S=>{S.target===Ae&&Ae.classList.remove("dragover")}),Ae.addEventListener("drop",S=>{S.preventDefault(),Ae.classList.remove("dragover"),w("edit");for(let C of S.dataTransfer?.files??[])ft(C)}),y.addEventListener("paste",S=>{for(let C of S.clipboardData?.items||[])if(C.type.startsWith("image/")){let P=C.getAsFile();P&&(S.preventDefault(),ft(P))}}),x.onclick=()=>p.click(),p.onchange=()=>{for(let S of p.files??[])ft(S);p.value=""}}let Sn=d("#warnings");function Ue(p){g(Sn,p?.length?o`<div class="warnings"><strong>Warnings:</strong> ${p.join("; ")}</div>`:o``)}d("#testBtn").onclick=()=>{let x=$e(o`<h3>Send a test</h3><p class="hint">${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."}</p><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>`),S=d("#testTo",x.el);S.focus(),T("/api/settings").then(P=>{let fe=P.settings.testRecipients||[];if(fe.length&&!S.value.trim()){S.value=fe.join(`
164
- `);let ye=d("#testDefaultsHint",x.el);ye.textContent="Pre-filled from your default test recipients (Settings).",ye.hidden=!1}}).catch(()=>{});let C=d("#tGo",x.el);d("#tCancel",x.el).onclick=x.close,C.onclick=()=>X(C,"Sending\u2026",async()=>{let{valid:P,invalid:fe}=zt(S.value),ye=Yt(fe);if(ye){k(ye);return}if(!P.length){k("Enter at least one email address");return}try{m||await Y(!0);let te=0,Ce=null;for(let we of P){let ce=await T(`/api/posts/${e}/test`,{method:"POST",json:{to:we}});ce.sent&&te++,Ce=ce.warnings}Ue(Ce),x.close(),k(et(te===P.length?`Test sent to ${te} address${te===1?"":"es"}`:`Sent ${te}/${P.length} \u2014 some failed`))}catch(te){k(Me(te))}})};let At=m?null:d("#scheduleBtn");return At&&(At.onclick=()=>{if(!I())return;let p=it(Vt()),x=it(new Date(Date.now()+24*3600*1e3)),S=o`<h3 id="schHead">Schedule this post</h3><p class="hint">It sends at the time you pick (at least ${bt()} out), with a cancelable window until then.</p><label for="schWhen">Send at</label><input type="datetime-local" id="schWhen" min="${p}" value="${x}"><div class="actions"><button type="button" id="schCancel">Cancel</button><button type="button" class="primary" id="schGo">Schedule</button></div><div class="altrow"><span class="altrow-note">Skip the review window?</span><button type="button" class="linkbtn" id="toSendNow">Send now →</button></div>`,C=$e(S),P=d(".modal",C.el),fe=()=>{C.close(),Te.scrollIntoView({block:"nearest"})},ye=()=>X(d("#schGo",P),"Scheduling\u2026",async()=>{let ce=d("#schWhen",P).value,He=ce?new Date(ce).getTime():Number.NaN;if(Number.isNaN(He)){k("Pick a valid date & time");return}try{if(await Y(!0)==="refused"){fe();return}await T(`/api/posts/${e}/schedule`,{method:"POST",json:{fire_at:new Date(He).toISOString()}}),C.close(),k(et(`Scheduled for ${Z(He)}. Send yourself a test.`)),s()}catch(ue){k(Me(ue))}}),te=()=>X(d("#snGo",P),"Queuing\u2026",async()=>{try{if(await Y(!0)==="refused"){fe();return}let{send:ce}=await T(`/api/posts/${e}/send`,{method:"POST"});C.close(),k(et(`Sends at ${Z(ce.fire_at)}, cancelable until then.`)),s()}catch(ce){k(Me(ce))}});function Ce(){P.setAttribute("aria-labelledby","schHead"),d("#schCancel",P).onclick=C.close,d("#schGo",P).onclick=ye,d("#toSendNow",P).onclick=we,d("#schWhen",P).focus()}async function we(){g(P,o`<h3 id="snHead">Send now?</h3><p class="hint">Freezes the current draft and sends it to <strong id="snWho">your confirmed subscribers</strong> after a cancelable window of at least ${bt()}, on the minute. You can cancel until it fires.</p><div class="altrow altrow-top"><button type="button" class="linkbtn" id="toSchedule">← Back to schedule</button></div><div class="actions"><button type="button" id="snCancel">Cancel</button><button type="button" class="primary" id="snGo">Send now</button></div>`),P.setAttribute("aria-labelledby","snHead"),d("#snCancel",P).onclick=C.close,d("#snGo",P).onclick=te,d("#toSchedule",P).onclick=()=>{g(P,S),Ce()},d("#snGo",P).focus();try{let He=(await T("/api/subscribers")).counts.confirmed,ue=P.querySelector("#snWho");ue&&(ue.textContent=`${He} confirmed subscriber${He===1?"":"s"}`)}catch{}}Ce()}),mt}function Bo(e,t){return e.status==="sending"||e.status==="sent"?(Tt(`#/sent/${e.id}`),!0):e.status==="canceled"?(t(),!0):!1}function fa(e,t){return De(e,"missed")?o`Scheduled for <strong>${Z(e.fire_at)}</strong> · <span class="countdown countdown-missed">${Qt(e.fire_at)}</span>`:t?o`Scheduled for <strong>${Z(e.fire_at)}</strong> · <span class="countdown">${kt(e.fire_at,!0)}</span>`:o`Scheduled for <strong>${Z(e.fire_at)}</strong> · <span class="countdown">${kt(e.fire_at)}</span> · cancelable until then.`}function ha(e){return e.sending?`#/sent/${e.sending.id}`:e.post.status==="sent"?e.sent?`#/sent/${e.sent.id}`:"#/sent":null}function Tt(e){location.replace(e)}function va(e,t){document.addEventListener("visibilitychange",()=>{document.hidden||e()},{signal:t})}var Wn=null;function ga(e){let t=document.createElement("div");g(t,F(e.html||""));let n=(t.querySelector("p")?.textContent||"").trim().replace(/\s+/g," "),s=/[:;,]$/.test(n),i=n.replace(/[\s:;,]+$/,""),a=150;return i.length>a?`${i.slice(0,a).replace(/[\s.,;:]+\S*$/,"")}\u2026`:s?`${i}\u2026`:i}async function Zn(e,t,n,s){if(g(t,tt("docs",null,o`<div class="docs-index"><p class="muted">Loading…</p></div>`)),!Wn)try{Wn=await T("/api/docs",{signal:n})}catch(l){n.aborted||oe(d(".room-main",t),l instanceof Error?l.message:String(l),()=>Q((c,f)=>Zn(e,c,f,s)));return}let{landing:i,docs:a}=Wn;if(!a.length){g(d(".room-main"),o`<p class="muted">No documentation.</p>`);return}e?Sa(t,i,a,e,s?ba(s):void 0):Io(t,i,a)}function ba(e){try{return decodeURIComponent(e)}catch{return}}function Io(e,t,n){let s=(m,u)=>{let r=ga(m);return o`<li><a class="doc-card" href="#/docs/${m.slug}">${u===null?null:o`<span class="doc-card-n">${String(u).padStart(2,"0")}</span>`}<span class="doc-card-main"><span class="doc-card-t">${m.title} <span class="doc-card-go" aria-hidden="true">→</span></span>${r?o`<span class="doc-card-d">${r}</span>`:null}</span></a></li>`},i=t.sections.map(m=>{let u=n.filter(R=>R.section===m.id);if(!u.length)return null;let r=u.map((R,E)=>s(R,m.numbered?E+1:null)),b=m.blurb?o`<div class="doc-section-blurb">${F(m.blurb)}</div>`:null;return o`<section class="doc-section"><h2 class="doc-section-t">${m.title}</h2>${b}${m.numbered?o`<ol class="doc-cards">${r}</ol>`:o`<ul class="doc-cards">${r}</ul>`}</section>`}),a=o`<div class="docs-index"><p class="eyebrow">Documentation</p><h1>${t.title}</h1><div class="docs-index-intro">${F(t.intro)}</div>${i}</div>`,l=z.appConfig?.deployment.build.repoUrl||"",c=(m,u,r=u)=>o`<a class="rail-link" href="${m}" target="_blank" rel="noopener" aria-label="${u}"><span class="rail-link-full">${u}</span><span class="rail-link-short" aria-hidden="true">${r}</span> <span aria-hidden="true">↗</span></a>`,f=o`<div class="toc-label">Kestrel</div>${c("https://getkestrel.dev","Project site","Project")}${l?[c(l,"Source on GitHub","Source"),c(`${l}/blob/HEAD/LICENSE`,"License")]:null}`;g(e,tt("docs",f,a)),window.scrollTo(0,0)}var Gn=matchMedia("(max-width: 720px)");function qo(e){if(!e)return;let t=Gn.matches;e.open=!t,d("summary",e).tabIndex=t?0:-1}var Uo=()=>document.querySelector("details#tocOnPage"),Do=!1;function ya(){Do||(Do=!0,Gn.addEventListener("change",()=>qo(Uo())))}function wa(e){return e.trim().toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu,"").replace(/\s/g,"-")}var No=e=>`doc-h-${e}`;function Sa(e,t,n,s,i){let a=n.findIndex($=>$.slug===s),l=n[a];if(!l){k(`No doc named \u201C${s}\u201D \u2014 showing the index.`),history.replaceState(history.state,"","#/docs"),Io(e,t,n);return}let c=n.filter($=>$.section===l.section),f=c.indexOf(l),m=c[f-1],u=c[f+1],r=($,v)=>o`<a href="#/docs/${$.slug}" aria-label="${v}: ${$.title}">${v==="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>`,b=m||u?o`<nav class="doc-topnav" aria-label="Adjacent docs">${m?r(m,"Previous"):null}${u?r(u,"Next"):null}</nav>`:null;g(e,tt("docs",o`<div id="docToc"></div>`,o`<article class="doc" id="docsMain"></article>`));let R=d("#docToc"),E=d("#docsMain");g(E,o`<section class="doc-part" id="doc-${l.slug}">${F(l.html)}</section>`);let y=d("section.doc-part",E),K=new Map;for(let $ of M("h1, h2, h3, h4",y)){let v=wa($.textContent||""),w=K.get(v)??0;K.set(v,w+1),$.dataset.anchor=w?`${v}-${w}`:v,$.id=No($.dataset.anchor)}let V=y.querySelector("h1"),L=[];if(V&&L.push({id:V.id,anchor:"",title:V.textContent||l.title}),b){let $=document.createElement("div");$.className="doc-head",g($,b),V?(V.before($),$.prepend(V)):y.prepend($)}for(let $ of M("h2",y))L.push({id:$.id,anchor:$.dataset.anchor||"",title:$.textContent||""});let O=document.createElement("nav");O.className="doc-pager",g(O,o`${m?o`<a class="doc-pager-btn prev" href="#/docs/${m.slug}"><span class="doc-pager-dir">← Previous</span><span class="doc-pager-title">${m.title}</span></a>`:o`<span></span>`}${u?o`<a class="doc-pager-btn next" href="#/docs/${u.slug}"><span class="doc-pager-dir">Next →</span><span class="doc-pager-title">${u.title}</span></a>`:o`<span></span>`}`),E.appendChild(O);let j=L.map($=>o`<a class="toc-sub" href="#/docs/${l.slug}${$.anchor?`/${$.anchor}`:""}" data-target="${$.id}">${$.title}</a>`);g(R,L.length?o`<details class="toc-onpage" id="tocOnPage"><summary class="toc-label">On this page</summary>${j}</details>`:o``);let J=Uo();qo(J),ya();let ie=$=>{if(J)for(let v of M(".toc-sub",J))v.classList.toggle("on",v.dataset.target===$)};R.addEventListener("click",$=>{let v=$.target instanceof Element?$.target.closest("a[data-target]"):null,w=v?.dataset.target;w&&($.preventDefault(),ie(w),J&&Gn.matches&&(J.open=!1),history.replaceState(history.state,"",v?.getAttribute("href")??location.hash),document.getElementById(w)?.scrollIntoView({block:"start"}))});for(let $ of M("a[href^='http']",E))$.target="_blank",$.rel="noopener";for(let $ of M("pre",E)){$.classList.add("has-copy");let v=document.createElement("button");v.type="button",v.className="code-copy",v.textContent="Copy",v.addEventListener("click",async()=>{let w=$.querySelector("code");try{await navigator.clipboard.writeText((w||$).innerText),v.textContent="Copied",setTimeout(()=>{v.textContent="Copy"},1500)}catch{k("Couldn't copy to clipboard")}}),$.appendChild(v)}let H=L.map($=>$.id),N=H[0];if(J&&N){let $=H[H.length-1]??N,w=(document.querySelector(".room-bar")?.getBoundingClientRect().height||54)+26,q=()=>{if(!document.getElementById(N)){document.onscroll=null;return}let _=N;if(Math.ceil(window.innerHeight+window.scrollY)>=document.documentElement.scrollHeight)_=$;else for(let G of H)if((document.getElementById(G)?.getBoundingClientRect().top??Number.POSITIVE_INFINITY)<=w)_=G;else break;ie(_)};document.onscroll=q,q()}let I=i?document.getElementById(No(i)):null;I?I.scrollIntoView({block:"start"}):window.scrollTo(0,0)}function $a(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 vn(e){return e.replace(/\([^)]*\)/g,"").replace(/\{[^}]*\}\??/g,"")||"/"}function Oo(e,t,n){if(e.access==="webhook")return null;let s=(e.query??[]).filter(c=>c.required),i=s.length?`?${s.map(c=>`${c.name}=:${c.name}`).join("&")}`:"",a=`"${t}${vn(e.path)}${i}"`,l=[e.method==="GET"?`curl ${a}`:`curl -X ${e.method} ${a}`];if(e.access==="admin"&&l.push(...$a(n)),e.example?.request!==void 0){let c=JSON.stringify(e.example.request).replaceAll("'","'\\''");l.push('-H "Content-Type: application/json"',`-d '${c}'`)}return l.join(` \\
165
- `)}function zn(e){return o`<span>Copy</span>${D(e)}`}function Vo(e,t,n){return o`<div class="api-ex"><div class="api-ex-head"><span class="api-ex-label">${e}</span><button type="button" class="api-copy" data-copy="${n}" aria-label="Copy ${e}">${zn("copyout")}</button></div><pre><code>${t}</code></pre></div>`}function jo(e,t){return t===void 0?null:Vo(e,Ro(t),JSON.stringify(t,null,2))}function ka(e){let t=Oo(e,location.origin,z.session?.auth.mode??"dev");return t===null?null:Vo("curl",Po(t),t)}function xa(e){if(!e?.length)return null;let t=e.map(n=>o`<tr><td><code>${n.name}</code>${n.required?o` <span class="api-req">required</span>`:null}</td><td class="muted">${n.description}</td></tr>`);return o`<div class="api-ex"><div class="api-ex-head"><span class="api-ex-label">Query</span></div><table class="api-query"><tbody>${t}</tbody></table></div>`}function Ea(e){if(!e?.length)return null;let t=e.map(n=>o`<tr><td><code>${n}</code></td></tr>`);return o`<div class="api-ex"><div class="api-ex-head"><span class="api-ex-label">Content-Type</span></div><table class="api-query api-accepts"><tbody>${t}</tbody></table></div>`}function Ta(e){return o`${vn(e).split(/(:[A-Za-z_]+)/).map((t,n)=>n%2?o`<span class="api-param">${t}</span>`:t)}`}function La(e){let t=`${e.method} ${vn(e.path)} ${e.summary}`.toLowerCase();return o`<li data-hay="${t}"><details class="api-route">
167
+ </div>`);let A=d("#f-subject"),H=d("#f-slug"),m=d("#f-markdown"),L=d(".toolbar",t),R=d("#previewFrame"),V=d("#mdHl"),ee=d("code",V),C=()=>{V.scrollTop=m.scrollTop,V.scrollLeft=m.scrollLeft},U=()=>{g(ee,ni(m.value)),C()};m.addEventListener("scroll",C),U();let _=u?null:d("#f-slug-auto"),b=()=>({subject:A.value,..._?.checked?{slug_auto:!0}:{slug:H.value,slug_auto:!1},markdown:m.value}),v=null,w=()=>{v&&H.value===v.shown&&(H.value=v.slug,bt()),v=null},j=d("#f-subject-error");function I(){A.classList.remove("is-invalid"),j.hidden=!0}function Y(){return A.value.trim()===""?(A.classList.add("is-invalid"),j.hidden=!1,A.focus(),!1):(I(),!0)}if(_){_.checked=c.slug_auto;let f=()=>H.classList.toggle("slug-auto",_.checked);f(),H.addEventListener("input",()=>{_.checked=H.value.trim()==="",f()}),_.addEventListener("change",()=>{f(),yt(),_.checked?we(!0).catch(E=>$(`Couldn't save \u2014 ${ge(E)}`)):(H.focus(),H.select())}),H.addEventListener("blur",()=>{w(),H.value.trim()===""&&(_.checked=!0,f(),yt(),Fe.dirty||(H.value=me))}),A.addEventListener("input",()=>{A.value.trim()!==""&&I()})}let re=ii(()=>{we(!0).catch(f=>$(`Couldn't autosave \u2014 ${ge(f)}`))});Ke(n,()=>re.cancel());let G=P(".ctab",t);function ae(f){for(let E of G){let S=E.dataset.tab===f;E.classList.toggle("active",S),E.setAttribute("aria-selected",S?"true":"false")}m.hidden=f!=="edit",V.hidden=f!=="edit",R.hidden=f!=="preview",L.classList.toggle("off",f!=="edit"),t.querySelector("#dropFoot")?.classList.toggle("off",f!=="edit"),f==="edit"&&C()}let me=c.slug;async function vt(){ae("preview");try{u||await we(!0),R.srcdoc=await Qe(`/api/posts/${e}/preview`),R.onload=()=>{try{let f=R.contentDocument;f&&(ht(f),pi(f,Gt(me),B),R.style.height=`${f.body.scrollHeight+24}px`)}catch{}}}catch(f){if(n.aborted||wt(f))return;$(ge(f))}}for(let f of G)f.onclick=()=>f.dataset.tab==="preview"?vt():ae("edit");u&&vt();let Ue=u?d("#lockFoot"):null,Te;function je(){Ue&&(Ue.classList.remove("nudge"),requestAnimationFrame(()=>Ue.classList.add("nudge")),clearTimeout(Te),Te=setTimeout(()=>Ue.classList.remove("nudge"),900))}function q(f,E,S){let N=m.selectionStart,O=m.selectionEnd,J=m.value.slice(N,O)||S;m.value=m.value.slice(0,N)+f+J+E+m.value.slice(O),m.focus(),m.selectionStart=N+f.length,m.selectionEnd=N+f.length+J.length}function F(f){let E=m.selectionStart,S=m.selectionEnd,N=m.value.lastIndexOf(`
168
+ `,E-1)+1,O=(m.value.slice(N,S)||"").split(`
169
+ `).map(J=>f+J).join(`
170
+ `);m.value=m.value.slice(0,N)+O+m.value.slice(S),m.focus(),m.selectionStart=N,m.selectionEnd=N+O.length}function oe(f){if(u){je();return}if(ae("edit"),f==="bold")q("**","**","bold text");else if(f==="italic")q("*","*","italic text");else if(f==="heading")F("## ");else if(f==="quote")F("> ");else if(f==="ul")F("- ");else if(f==="ol")F("1. ");else if(f==="indent")F(" ");else if(f==="link")q("[","](https://)","link text");else if(f==="code"){let E=m.selectionStart,S=m.selectionEnd;E===S||m.value.slice(E,S).includes(`
171
+ `)?q("```\n","\n```","code"):q("`","`","code")}yt()}for(let f of P(".tb[data-fmt]",t))f.onclick=()=>oe(f.dataset.fmt??"");m.addEventListener("keydown",f=>{if(!(f.metaKey||f.ctrlKey))return;let E=f.key.toLowerCase();E==="b"?(f.preventDefault(),oe("bold")):E==="i"?(f.preventDefault(),oe("italic")):E==="k"&&(f.preventDefault(),oe("link"))});let le=!1,Le=!1,ye=!1,ve=u?null:d("#saveBtn"),te=u?null:d("#saveStatus"),Fe=new yn(b),gt=!1;function Ln(){te&&(u?(te.className="save-status",te.textContent=""):gt?(te.className="save-status is-saving",te.textContent="Saving\u2026"):le?(te.className="save-status is-dirty",te.textContent="Unsaved changes"):(te.className="save-status is-saved",te.textContent="Saved"))}function bt(){le=Fe.dirty,Ln()}Ln();function xs(){re.touch()}function yt(){u||(U(),bt(),ye||xs())}let Je=Promise.resolve();function we(f){let E=Je.catch(()=>{}).then(()=>aa(f));return Je=E,ts(Je),E}async function aa(f){if(f&&!Fe.dirty)return"unchanged";if(ye)return"refused";re.cancel(),gt=!0,Ln();let E=b(),S=JSON.stringify(E),N=H.value;try{let O={...E,base_revision:y.base},{post:J}=await T(`/api/posts/${e}`,{method:"PUT",json:O});if(J.slug&&H.value===N){let de="slug"in E;de&&document.activeElement===H?v={shown:N,slug:J.slug}:H.value=J.slug,de&&(S=JSON.stringify({...E,slug:J.slug}))}return J.slug&&(me=J.slug),y.saved(J.current_revision),Fe.markSaved(S),Le=!1,f||$("Saved"),"saved"}catch(O){let J=li(O);if(J)return Ls(J),"refused";if(xe(O))return a(),"refused";throw Le=!0,O}finally{gt=!1,bt()}}ve&&(ve.onclick=()=>{if(ye){He.scrollIntoView({block:"nearest"});return}z(ve,"Saving\u2026",()=>we(!1).catch(f=>$(ge(f)))).finally(bt)});function Es(){ve&&(ye?(ve.setAttribute("aria-disabled","true"),ve.title="Saving is paused: choose Reload or Keep editing above"):(ve.removeAttribute("aria-disabled"),ve.removeAttribute("title")))}let ra=()=>{if(re.cancel(),u||!Fe.dirty)return;let f=b(),E=JSON.stringify(f);le=!1,Je=Je.catch(()=>{}).then(async()=>{if(!Fe.dirty)return;let S={...f,base_revision:y.base},{post:N}=await T(`/api/posts/${e}`,{method:"PUT",json:S});y.saved(N.current_revision),Fe.markSaved(E)}).catch(S=>{if(xe(S)){$("This post was deleted elsewhere, so your last edits weren't saved.");return}$(S instanceof se&&S.status===409?"Changed elsewhere \u2014 your edits weren't saved":`Couldn't save your changes \u2014 ${ge(S)}`)}),ts(Je)},la={dirty:()=>le,beforeLeave(){return o?"leave":le&&(Le||ye)?"confirm":(ra(),"leave")},manualSave(){ve&&!ve.disabled&&ve.click()}};if(!u){for(let f of[A,H,m])f.addEventListener("input",yt);for(let f of[A,H])f.addEventListener("blur",()=>we(!0).catch(E=>$(`Couldn't save \u2014 ${ge(E)}`))),f.addEventListener("keydown",E=>{E.key==="Enter"&&!E.isComposing&&E.keyCode!==229&&(E.preventDefault(),we(!0).then(w).catch(S=>$(`Couldn't save \u2014 ${ge(S)}`)))})}let He=d("#freshnessBanner"),da=f=>f==="service"?"Claude":f||null;function Ts(){ye=!1,Es(),y.clearWarning(),He.hidden=!0,g(He,i``)}function Ls(f){if(u)return;if(ye=!0,Es(),y.noteWarned(f),f.kind==="locked")g(He,i`<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 S=da(f.author);g(He,i`<span><span aria-hidden="true">⚠️</span> This draft was changed elsewhere${S?i` — last edited by <strong>${S}</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>`)}He.hidden=!1,d("#freshReload",He).onclick=()=>{Ts(),s()};let E=He.querySelector("#freshKeep");E&&(E.onclick=()=>{y.adoptWarned(),Ts(),bt(),le&&xs()})}function wt(f){return xe(f)?(a(),!0):!1}if(!u){let f=async()=>{if(gt||ye||o||document.hidden)return;let E=y.base;try{let S=await T(`/api/posts/${e}`,{signal:n}),N=y.decide({status:S.post.status,revision:S.post.current_revision,author:S.author},{saving:gt,baseAtRequest:E});N!=="ignore"&&N!=="fresh"&&Ls(N)}catch(S){xe(S)&&a()}};We(1e4,f,n),Br(f,n)}if(d("#webLink").addEventListener("click",()=>{u||we(!0).catch(f=>$(`Couldn't save \u2014 ${ge(f)}`))}),u&&h){let f=r?.send??{...h,phase:"scheduled",conditions:[]},E=d("#schedWhen"),S=d("#schedControls"),N="",O=0,J=!1,de=d("#rescheduleSchedule"),ie=d("#cancelSchedule"),_e=ne=>f.actions===void 0||Pe({actions:f.actions},ne),Se=()=>{let ne=Date.now(),Be=f.phase==="due"||ne>=f.fire_at;Be?J||(de.hidden=!0,ie.hidden=!0):(J=!0,de.hidden=!_e("reschedule"),ie.hidden=!_e("cancel")),de.disabled=Be,ie.disabled=Be,S.hidden=de.hidden&&ie.hidden;let Ve=Ar(f,Be),Rs=String(Ve);Rs!==N&&(N=Rs,g(E,Ve)),!Be&&f.fire_at-ne<=1e3&&f.fire_at!==O&&(O=f.fire_at,Ot(f.fire_at,Se,n))};Se(),We(1e3,Se,n);let ce=()=>{f.remade_at&&fn(d("#editorNotices"),{kind:"applied",subject:f.id,version:f.remade_at,markup:pn(f.remade_at,1,!0)})};ce();let Ce=ne=>Lt(ne,{update(Be,Ve){if(f=Be,Ve?.to==="sending"||Ve?.to==="sent"||Ve?.to==="complete"){$("Your send started, so this page switched to its live watch."),Sn(`#/sent/${Be.id}`);return}if(Ve?.to==="canceled"){$("The schedule was canceled elsewhere, so this post is a draft again."),s();return}Se(),ce()},removed:a,stale:s},n);r?Ce(r):_s(5e3,async()=>{let ne=await T(`/api/sends/${h.id}`,{signal:n});return mi(ne.send,s)||(f=ne.send,Se(),ce(),Ce(ne)),!1},n),de.onclick=()=>gn(f.id,f.fire_at,s),ie.onclick=()=>z(ie,"Canceling\u2026",async()=>{try{await T(`/api/sends/${h.id}/cancel`,{method:"POST"}),$("Schedule canceled"),s()}catch(ne){xe(ne)?a():ne instanceof se&&ne.code==="window_closed"?(f={...f,phase:"due"},$("Too late to cancel: the send time has passed, so this email is going out."),nt()):$(ge(ne))}}).finally(Se)}async function Hn(f){if(f.type.startsWith("image/"))try{let E=new FormData;E.append("file",f);let{image:S}=await T(`/api/posts/${e}/images`,{method:"POST",body:E}),N=m.selectionStart,O=`
172
+ ![${f.name}](${S.filename})
173
+ `;m.value=m.value.slice(0,N)+O+m.value.slice(N),m.selectionStart=m.selectionEnd=N+O.length,yt(),$("Image added")}catch(E){wt(E)||$(ge(E))}}let Re=d("#composerBody");if(u){let f=S=>S.metaKey||S.ctrlKey?["x","z","y"].includes(S.key.toLowerCase()):S.key.length===1||S.key==="Enter"||S.key==="Backspace"||S.key==="Delete",E=[m,A,H];for(let S of E)S.addEventListener("keydown",N=>{f(N)&&je()}),S.addEventListener("paste",je);Re.addEventListener("dragover",S=>S.preventDefault()),Re.addEventListener("drop",S=>{S.preventDefault(),je()})}else{let f=d("#imgInput"),E=d("#dropFoot");Re.addEventListener("dragover",S=>{S.preventDefault(),Re.classList.add("dragover")}),Re.addEventListener("dragleave",S=>{S.target===Re&&Re.classList.remove("dragover")}),Re.addEventListener("drop",S=>{S.preventDefault(),Re.classList.remove("dragover"),ae("edit");for(let N of S.dataTransfer?.files??[])Hn(N)}),m.addEventListener("paste",S=>{for(let N of S.clipboardData?.items||[])if(N.type.startsWith("image/")){let O=N.getAsFile();O&&(S.preventDefault(),Hn(O))}}),E.onclick=()=>f.click(),f.onchange=()=>{for(let S of f.files??[])Hn(S);f.value=""}}let ca=d("#warnings");function ua(f){g(ca,f?.length?i`<div class="warnings"><strong>Warnings:</strong> ${f.join("; ")}</div>`:i``)}d("#testBtn").onclick=()=>{let E=he(i`<h3>Send a test</h3><p class="hint">${u?"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."}</p><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>`),S=d("#testTo",E.el);S.focus(),T("/api/settings").then(O=>{let J=O.settings.testRecipients||[];if(J.length&&!S.value.trim()){S.value=J.join(`
174
+ `);let de=d("#testDefaultsHint",E.el);de.textContent="Pre-filled from your default test recipients (Settings).",de.hidden=!1}}).catch(()=>{});let N=d("#tGo",E.el);d("#tCancel",E.el).onclick=E.close,N.onclick=()=>z(N,"Sending\u2026",async()=>{let{valid:O,invalid:J}=en(S.value),de=tn(J);if(de){$(de);return}if(!O.length){$("Enter at least one email address");return}try{u||await we(!0);let ie=0,_e=null;for(let Se of O){let ce=await T(`/api/posts/${e}/test`,{method:"POST",json:{to:Se}});ce.sent&&ie++,_e=ce.warnings}ua(_e),E.close(),$(Xe(ie===O.length?`Test sent to ${ie} address${ie===1?"":"es"}`:`Sent ${ie} of ${O.length}; the rest failed`))}catch(ie){if(xe(ie)){E.close(),wt(ie);return}$(ge(ie))}})};let Hs=u?null:d("#scheduleBtn");return Hs&&(Hs.onclick=()=>{if(!Y())return;let f=it(Yt()),E=it(new Date(Date.now()+24*3600*1e3)),S=i`<h3 id="schHead">Schedule this post</h3><p class="hint">It sends at the time you pick (at least ${kt()} out), with a cancelable window until then.</p><label for="schWhen">Send at</label><input type="datetime-local" id="schWhen" min="${f}" value="${E}"><div class="actions"><button type="button" id="schCancel">Cancel</button><button type="button" class="primary" id="schGo">Schedule</button></div><div class="altrow"><span class="altrow-note">Skip the review window?</span><button type="button" class="linkbtn" id="toSendNow">Send now →</button></div>`,N=he(S),O=d(".modal",N.el),J=()=>{N.close(),He.scrollIntoView({block:"nearest"})},de=()=>z(d("#schGo",O),"Scheduling\u2026",async()=>{let ce=d("#schWhen",O).value,Ce=ce?new Date(ce).getTime():Number.NaN;if(Number.isNaN(Ce)){$("Pick a valid date & time");return}try{if(await we(!0)==="refused"){J();return}await T(`/api/posts/${e}/schedule`,{method:"POST",json:{fire_at:new Date(Ce).toISOString()}}),N.close(),$(Xe(`Scheduled for ${W(Ce)}. Send yourself a test.`)),s()}catch(ne){if(xe(ne)){N.close(),wt(ne);return}$(ge(ne))}}),ie=()=>z(d("#snGo",O),"Queuing\u2026",async()=>{try{if(await we(!0)==="refused"){J();return}let{send:ce}=await T(`/api/posts/${e}/send`,{method:"POST"});N.close(),$(Xe(`Sends at ${W(ce.fire_at)}, cancelable until then.`)),s()}catch(ce){if(xe(ce)){N.close(),wt(ce);return}$(ge(ce))}});function _e(){O.setAttribute("aria-labelledby","schHead"),d("#schCancel",O).onclick=N.close,d("#schGo",O).onclick=de,d("#toSendNow",O).onclick=Se,d("#schWhen",O).focus()}async function Se(){g(O,i`<h3 id="snHead">Send now?</h3><p class="hint">Freezes the current draft and sends it to <strong id="snWho">your confirmed subscribers</strong> after a cancelable window of at least ${kt()}, on the minute. You can cancel until it fires.</p><div class="altrow altrow-top"><button type="button" class="linkbtn" id="toSchedule">← Back to schedule</button></div><div class="actions"><button type="button" id="snCancel">Cancel</button><button type="button" class="primary" id="snGo">Send now</button></div>`),O.setAttribute("aria-labelledby","snHead"),d("#snCancel",O).onclick=N.close,d("#snGo",O).onclick=ie,d("#toSchedule",O).onclick=()=>{g(O,S),_e()},d("#snGo",O).focus();try{let Ce=(await T("/api/subscribers")).counts.confirmed,ne=O.querySelector("#snWho");ne&&(ne.textContent=`${Ce} confirmed subscriber${Ce===1?"":"s"}`)}catch{}}_e()}),la}function mi(e,t){return e.status==="sending"||e.status==="sent"?(Sn(`#/sent/${e.id}`),!0):e.status==="canceled"?(t(),!0):!1}function Ar(e,t){return ke(e,"missed")?i`Scheduled for <strong>${W(e.fire_at)}</strong> · <span class="countdown countdown-missed">${nn(e.fire_at)}</span>`:t?i`Scheduled for <strong>${W(e.fire_at)}</strong> · <span class="countdown">${Ht(e.fire_at,!0)}</span>`:i`Scheduled for <strong>${W(e.fire_at)}</strong> · <span class="countdown">${Ht(e.fire_at)}</span> · cancelable until then.`}function xe(e){return e instanceof se&&e.status===404}function _r(e){return e.sending?`#/sent/${e.sending.id}`:e.post.status==="sent"?e.sent?`#/sent/${e.sent.id}`:"#/sent":null}function Sn(e){location.replace(e)}function Br(e,t){document.addEventListener("visibilitychange",()=>{document.hidden||e()},{signal:t})}var os=null;function Nr(e){let t=document.createElement("div");g(t,D(e.html||""));let n=(t.querySelector("p")?.textContent||"").trim().replace(/\s+/g," "),s=/[:;,]$/.test(n),o=n.replace(/[\s:;,]+$/,""),a=150;return o.length>a?`${o.slice(0,a).replace(/[\s.,;:]+\S*$/,"")}\u2026`:s?`${o}\u2026`:o}async function is(e,t,n,s){if(g(t,et("docs",null,i`<div class="docs-index"><p class="muted">Loading…</p></div>`)),!os)try{os=await T("/api/docs",{signal:n})}catch(l){n.aborted||X(d(".room-main",t),l instanceof Error?l.message:String(l),()=>K((r,c)=>is(e,r,c,s)));return}let{landing:o,docs:a}=os;if(!a.length){g(d(".room-main"),i`<p class="muted">No documentation.</p>`);return}e?Or(t,o,a,e,s?Ir(s):void 0):vi(t,o,a)}function Ir(e){try{return decodeURIComponent(e)}catch{return}}function vi(e,t,n){let s=(p,h)=>{let u=Nr(p);return i`<li><a class="doc-card" href="#/docs/${p.slug}">${h===null?null:i`<span class="doc-card-n">${String(h).padStart(2,"0")}</span>`}<span class="doc-card-main"><span class="doc-card-t">${p.title} <span class="doc-card-go" aria-hidden="true">→</span></span>${u?i`<span class="doc-card-d">${u}</span>`:null}</span></a></li>`},o=t.sections.map(p=>{let h=n.filter(x=>x.section===p.id);if(!h.length)return null;let u=h.map((x,k)=>s(x,p.numbered?k+1:null)),y=p.blurb?i`<div class="doc-section-blurb">${D(p.blurb)}</div>`:null;return i`<section class="doc-section"><h2 class="doc-section-t">${p.title}</h2>${y}${p.numbered?i`<ol class="doc-cards">${u}</ol>`:i`<ul class="doc-cards">${u}</ul>`}</section>`}),a=i`<div class="docs-index"><p class="eyebrow">Documentation</p><h1>${t.title}</h1><div class="docs-index-intro">${D(t.intro)}</div>${o}</div>`,l=Z.appConfig?.deployment.build.repoUrl||"",r=(p,h,u=h)=>i`<a class="rail-link" href="${p}" target="_blank" rel="noopener" aria-label="${h}"><span class="rail-link-full">${h}</span><span class="rail-link-short" aria-hidden="true">${u}</span> <span aria-hidden="true">↗</span></a>`,c=i`<div class="toc-label">Kestrel</div>${r("https://getkestrel.dev","Project site","Project")}${l?[r(l,"Source on GitHub","Source"),r(`${l}/blob/HEAD/LICENSE`,"License")]:null}`;g(e,et("docs",c,a)),window.scrollTo(0,0)}var as=matchMedia("(max-width: 720px)");function gi(e){if(!e)return;let t=as.matches;e.open=!t,d("summary",e).tabIndex=t?0:-1}var bi=()=>document.querySelector("details#tocOnPage"),fi=!1;function Dr(){fi||(fi=!0,as.addEventListener("change",()=>gi(bi())))}function qr(e){return e.trim().toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu,"").replace(/\s/g,"-")}var hi=e=>`doc-h-${e}`;function Or(e,t,n,s,o){let a=n.findIndex(b=>b.slug===s),l=n[a];if(!l){$(`No doc named \u201C${s}\u201D \u2014 showing the index.`),history.replaceState(history.state,"","#/docs"),vi(e,t,n);return}let r=n.filter(b=>b.section===l.section),c=r.indexOf(l),p=r[c-1],h=r[c+1],u=(b,v)=>i`<a href="#/docs/${b.slug}" aria-label="${v}: ${b.title}">${v==="Previous"?i`<span aria-hidden="true">←</span><span class="doc-topnav-word">Previous</span>`:i`<span class="doc-topnav-word">Next</span><span aria-hidden="true">→</span>`}</a>`,y=p||h?i`<nav class="doc-topnav" aria-label="Adjacent docs">${p?u(p,"Previous"):null}${h?u(h,"Next"):null}</nav>`:null;g(e,et("docs",i`<div id="docToc"></div>`,i`<article class="doc" id="docsMain"></article>`));let x=d("#docToc"),k=d("#docsMain");g(k,i`<section class="doc-part" id="doc-${l.slug}">${D(l.html)}</section>`);let B=d("section.doc-part",k),A=new Map;for(let b of P("h1, h2, h3, h4",B)){let v=qr(b.textContent||""),w=A.get(v)??0;A.set(v,w+1),b.dataset.anchor=w?`${v}-${w}`:v,b.id=hi(b.dataset.anchor)}let H=B.querySelector("h1"),m=[];if(H&&m.push({id:H.id,anchor:"",title:H.textContent||l.title}),y){let b=document.createElement("div");b.className="doc-head",g(b,y),H?(H.before(b),b.prepend(H)):B.prepend(b)}for(let b of P("h2",B))m.push({id:b.id,anchor:b.dataset.anchor||"",title:b.textContent||""});let L=document.createElement("nav");L.className="doc-pager",g(L,i`${p?i`<a class="doc-pager-btn prev" href="#/docs/${p.slug}"><span class="doc-pager-dir">← Previous</span><span class="doc-pager-title">${p.title}</span></a>`:i`<span></span>`}${h?i`<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>`:i`<span></span>`}`),k.appendChild(L);let R=m.map(b=>i`<a class="toc-sub" href="#/docs/${l.slug}${b.anchor?`/${b.anchor}`:""}" data-target="${b.id}">${b.title}</a>`);g(x,m.length?i`<details class="toc-onpage" id="tocOnPage"><summary class="toc-label">On this page</summary>${R}</details>`:i``);let V=bi();gi(V),Dr();let ee=b=>{if(V)for(let v of P(".toc-sub",V))v.classList.toggle("on",v.dataset.target===b)};x.addEventListener("click",b=>{let v=b.target instanceof Element?b.target.closest("a[data-target]"):null,w=v?.dataset.target;w&&(b.preventDefault(),ee(w),V&&as.matches&&(V.open=!1),history.replaceState(history.state,"",v?.getAttribute("href")??location.hash),document.getElementById(w)?.scrollIntoView({block:"start"}))});for(let b of P("a[href^='http']",k))b.target="_blank",b.rel="noopener";for(let b of P("pre",k)){b.classList.add("has-copy");let v=document.createElement("button");v.type="button",v.className="code-copy",v.textContent="Copy",v.addEventListener("click",async()=>{let w=b.querySelector("code");try{await navigator.clipboard.writeText((w||b).innerText),v.textContent="Copied",setTimeout(()=>{v.textContent="Copy"},1500)}catch{$("Couldn't copy to clipboard")}}),b.appendChild(v)}let C=m.map(b=>b.id),U=C[0];if(V&&U){let b=C[C.length-1]??U,w=(document.querySelector(".room-bar")?.getBoundingClientRect().height||54)+26,j=()=>{if(!document.getElementById(U)){document.onscroll=null;return}let I=U;if(Math.ceil(window.innerHeight+window.scrollY)>=document.documentElement.scrollHeight)I=b;else for(let Y of C)if((document.getElementById(Y)?.getBoundingClientRect().top??Number.POSITIVE_INFINITY)<=w)I=Y;else break;ee(I)};document.onscroll=j,j()}let _=o?document.getElementById(hi(o)):null;_?_.scrollIntoView({block:"start"}):window.scrollTo(0,0)}function Ur(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 $n(e){return e.replace(/\([^)]*\)/g,"").replace(/\{[^}]*\}\??/g,"")||"/"}function yi(e,t,n){if(e.access==="webhook")return null;let s=(e.query??[]).filter(r=>r.required),o=s.length?`?${s.map(r=>`${r.name}=:${r.name}`).join("&")}`:"",a=`"${t}${$n(e.path)}${o}"`,l=[e.method==="GET"?`curl ${a}`:`curl -X ${e.method} ${a}`];if(e.access==="admin"&&l.push(...Ur(n)),e.example?.request!==void 0){let r=JSON.stringify(e.example.request).replaceAll("'","'\\''");l.push('-H "Content-Type: application/json"',`-d '${r}'`)}return l.join(` \\
175
+ `)}function wi(e,t){return t===void 0?null:Jn(e,si(t),JSON.stringify(t,null,2))}function jr(e){let t=yi(e,location.origin,Z.session?.auth.mode??"dev");return t===null?null:Jn("curl",oi(t),t)}function Fr(e){if(!e?.length)return null;let t=e.map(n=>i`<tr><td><code>${n.name}</code>${n.required?i` <span class="api-req">required</span>`:null}</td><td class="muted">${n.description}</td></tr>`);return i`<div class="block"><div class="block-head"><span class="block-label">Query</span></div><table class="api-query"><tbody>${t}</tbody></table></div>`}function Vr(e){if(!e?.length)return null;let t=e.map(n=>i`<tr><td><code>${n}</code></td></tr>`);return i`<div class="block"><div class="block-head"><span class="block-label">Content-Type</span></div><table class="api-query api-accepts"><tbody>${t}</tbody></table></div>`}function Wr(e){return i`${$n(e).split(/(:[A-Za-z_]+)/).map((t,n)=>n%2?i`<span class="api-param">${t}</span>`:t)}`}function Kr(e){let t=`${e.method} ${$n(e.path)} ${e.summary}`.toLowerCase();return i`<li data-hay="${t}"><details class="api-route">
166
176
  <summary class="api-route-sum">
167
177
  <span class="api-method m-${e.method}">${e.method}</span>
168
- <code class="api-path">${Ta(e.path)}</code>
178
+ <code class="api-path">${Wr(e.path)}</code>
169
179
  <span class="api-route-line">${e.summary}</span>
170
180
  <span class="api-chev" aria-hidden="true"></span>
171
181
  </summary>
172
182
  <div class="api-route-detail">
173
183
  <p class="api-summary">${e.summary}</p>
174
- ${e.description?o`<p class="api-desc muted">${e.description}</p>`:null}
175
- ${xa(e.query)}
176
- ${Ea(e.accepts)}
177
- ${jo("Request",e.example?.request)}
178
- ${jo("Response",e.example?.response)}
179
- ${ka(e)}
184
+ ${e.description?i`<p class="api-desc muted">${e.description}</p>`:null}
185
+ ${Fr(e.query)}
186
+ ${Vr(e.accepts)}
187
+ ${wi("Request",e.example?.request)}
188
+ ${wi("Response",e.example?.response)}
189
+ ${jr(e)}
180
190
  </div>
181
- </details></li>`}var Ca=(e,t)=>e.routes.filter(n=>n.resource===t.key);function Ha(e,t){let n=`${e.access}-${t.key}`,s=Ca(e,t);return o`<div class="api-res" id="api-${n}" data-sec="${n}">
191
+ </details></li>`}var Zr=(e,t)=>e.routes.filter(n=>n.resource===t.key);function Yr(e,t){let n=`${e.access}-${t.key}`,s=Zr(e,t);return i`<div class="api-res" id="api-${n}" data-sec="${n}">
182
192
  <div class="api-res-head"><h3>${t.title}</h3></div>
183
- <ul class="api-rows">${s.map(La)}</ul>
184
- </div>`}function Ma(e){return o`<section class="api-section" id="api-${e.access}" data-tier="${e.access}">
193
+ <ul class="api-rows">${s.map(Kr)}</ul>
194
+ </div>`}function Gr(e){return i`<section class="api-section" id="api-${e.access}" data-tier="${e.access}">
185
195
  <h2>${e.title}</h2>
186
196
  <p class="api-blurb muted">${e.blurb}</p>
187
- ${e.resources.map(t=>Ha(e,t))}
188
- </section>`}function Ra(e){return o`${e.map(t=>o`<a href="#/reference" class="api-nav-tier" data-sec="${t.access}" data-tier="${t.access}">${t.title}</a>${t.resources.length>1?t.resources.map(n=>o`<a href="#/reference" class="api-nav-res" data-sec="${t.access}-${n.key}">${n.title}</a>`):null}`)}`}function Fo(e,t,n){let s=e.trim().toLowerCase().split(/\s+/).filter(Boolean),i=0;for(let c of M(".api-section",n)){let f=0;for(let u of M(".api-res",c)){let r=0;for(let R of M("li[data-hay]",u)){let E=s.every(y=>(R.dataset.hay??"").includes(y));R.hidden=!E,r+=E?1:0}u.hidden=r===0;let b=t.querySelector(`a[data-sec="${u.dataset.sec}"]`);b&&(b.hidden=r===0),f+=r}c.hidden=f===0;let m=t.querySelector(`a[data-tier="${c.dataset.tier}"]`);m&&(m.hidden=f===0),i+=f}let a=M(".api-section",n),l=a.filter(c=>!c.hidden).at(-1);for(let c of a)c.classList.toggle("api-last",c===l);d("#apiEmpty",n).hidden=i>0}function Pa(e,t){let n=(d(".room-bar").getBoundingClientRect().height||54)+26,s=M(".api-section, .api-res",t).filter(u=>!u.closest("[hidden]")),i=window.scrollY>0&&Math.ceil(window.innerHeight+window.scrollY)>=document.documentElement.scrollHeight,a=s[0];for(let u of s)if(i||u.getBoundingClientRect().top<=n)a=u;else break;let l=a?.dataset.sec??a?.dataset.tier,c=a?.closest(".api-section")?.dataset.tier,f=M("a[data-sec]",e).filter(u=>getComputedStyle(u).display!=="none"),m=f.find(u=>u.dataset.sec===l)??(c===void 0?void 0:f.find(u=>u.dataset.tier===c));for(let u of M("a[data-sec]",e))u.classList.toggle("active",u===m)}async function Yn(e,t){g(e,tt("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 n=d("#apiNav"),s=d("#apiContent");n.addEventListener("click",u=>{let b=(u.target instanceof Element?u.target.closest("a[data-sec]"):null)?.dataset.sec;b&&(u.preventDefault(),document.getElementById(`api-${b}`)?.scrollIntoView({block:"start"}))});let i;try{({groups:i}=await T("/api/reference",{signal:t}))}catch(u){oe(s,u instanceof Error?u.message:String(u),()=>Q(Yn));return}g(n,Ra(i)),g(s,o`<header class="api-head"><h1>API reference</h1><p class="muted">Generated from the route registration, so every endpoint the app and Claude can call is listed here. Base URL <code>${location.origin}</code>.</p><div class="api-filter">${D("search")}<input type="search" id="apiFilter" placeholder="Filter routes by method, path, or summary" autocomplete="off" aria-label="Filter routes" aria-keyshortcuts="/"><kbd aria-hidden="true">/</kbd><button type="button" class="icon api-filter-clear" id="apiFilterClear" aria-label="Clear filter" hidden>${D("x")}</button></div></header>${i.map(Ma)}<p class="api-empty muted" id="apiEmpty" hidden>No routes match. Try a path segment like <code>sends</code> or a method like <code>DELETE</code>.</p>`);let a=d("#apiFilter"),l=d("#apiFilterClear"),c=()=>Pa(n,s),f=u=>{a.value=u,l.hidden=u==="",Fo(u,n,s),c()};a.addEventListener("input",()=>f(a.value)),l.addEventListener("click",()=>{f(""),a.focus()});let m=new WeakMap;s.addEventListener("click",async u=>{let r=u.target instanceof Element?u.target.closest("[data-copy]"):null;if(!r)return;try{await navigator.clipboard.writeText(r.dataset.copy??"")}catch{k("Couldn't copy to clipboard");return}let b=m.get(r),R=b?.label??r.getAttribute("aria-label")??"Copy";window.clearTimeout(b?.timer),g(r,zn("check")),r.setAttribute("aria-label","Copied"),r.classList.add("copied");let E=window.setTimeout(()=>{g(r,zn("copyout")),r.setAttribute("aria-label",R),r.classList.remove("copied"),m.delete(r)},1500);m.set(r,{label:R,timer:E})}),document.addEventListener("keydown",u=>{let r=u.target instanceof HTMLElement&&(u.target.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(u.target.tagName));u.key==="/"&&!r&&!u.metaKey&&!u.ctrlKey&&!u.altKey?(u.preventDefault(),a.focus()):u.key==="Escape"&&u.target===a&&a.value&&f("")},{signal:t}),Fo("",n,s),window.addEventListener("scroll",c,{passive:!0,signal:t}),window.scrollTo(0,0),c()}var gn=e=>e instanceof Error?e.message:String(e);function Aa(e){let t=[Ie(e,"reschedule")?o`<button class="ghost" data-reschedule="${e.id}">Reschedule</button>`:null,Ie(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 Qn(e,t){let n={status:"sent",search:"",failures:"",sort:"fire",dir:"desc",offset:0,limit:50};g(e,o`<h1>Sent</h1>
189
- ${yt()?o`<p class="muted">No email provider is configured, so these sends are recorded here but nothing is delivered.</p>`:null}
197
+ ${e.resources.map(t=>Yr(e,t))}
198
+ </section>`}function zr(e){return i`${e.map(t=>i`<a href="#/reference" class="api-nav-tier" data-sec="${t.access}" data-tier="${t.access}">${t.title}</a>${t.resources.length>1?t.resources.map(n=>i`<a href="#/reference" class="api-nav-res" data-sec="${t.access}-${n.key}">${n.title}</a>`):null}`)}`}function Si(e,t,n){let s=e.trim().toLowerCase().split(/\s+/).filter(Boolean),o=0;for(let r of P(".api-section",n)){let c=0;for(let h of P(".api-res",r)){let u=0;for(let x of P("li[data-hay]",h)){let k=s.every(B=>(x.dataset.hay??"").includes(B));x.hidden=!k,u+=k?1:0}h.hidden=u===0;let y=t.querySelector(`a[data-sec="${h.dataset.sec}"]`);y&&(y.hidden=u===0),c+=u}r.hidden=c===0;let p=t.querySelector(`a[data-tier="${r.dataset.tier}"]`);p&&(p.hidden=c===0),o+=c}let a=P(".api-section",n),l=a.filter(r=>!r.hidden).at(-1);for(let r of a)r.classList.toggle("api-last",r===l);d("#apiEmpty",n).hidden=o>0}function Jr(e,t){let n=(d(".room-bar").getBoundingClientRect().height||54)+26,s=P(".api-section, .api-res",t).filter(h=>!h.closest("[hidden]")),o=window.scrollY>0&&Math.ceil(window.innerHeight+window.scrollY)>=document.documentElement.scrollHeight,a=s[0];for(let h of s)if(o||h.getBoundingClientRect().top<=n)a=h;else break;let l=a?.dataset.sec??a?.dataset.tier,r=a?.closest(".api-section")?.dataset.tier,c=P("a[data-sec]",e).filter(h=>getComputedStyle(h).display!=="none"),p=c.find(h=>h.dataset.sec===l)??(r===void 0?void 0:c.find(h=>h.dataset.tier===r));for(let h of P("a[data-sec]",e))h.classList.toggle("active",h===p)}async function rs(e,t){g(e,et("reference",i`<div class="toc-label">API</div><nav class="api-nav" id="apiNav" aria-label="API sections"></nav>`,i`<div class="api-content" id="apiContent"><p class="muted">Loading…</p></div>`));let n=d("#apiNav"),s=d("#apiContent");n.addEventListener("click",p=>{let u=(p.target instanceof Element?p.target.closest("a[data-sec]"):null)?.dataset.sec;u&&(p.preventDefault(),document.getElementById(`api-${u}`)?.scrollIntoView({block:"start"}))});let o;try{({groups:o}=await T("/api/reference",{signal:t}))}catch(p){X(s,p instanceof Error?p.message:String(p),()=>K(rs));return}g(n,zr(o)),g(s,i`<header class="api-head"><h1>API reference</h1><p class="muted">Generated from the route registration, so every endpoint the app and Claude can call is listed here. Base URL <code>${location.origin}</code>.</p><div class="api-filter">${M("search")}<input type="search" id="apiFilter" placeholder="Filter routes by method, path, or summary" autocomplete="off" aria-label="Filter routes" aria-keyshortcuts="/"><kbd aria-hidden="true">/</kbd><button type="button" class="icon api-filter-clear" id="apiFilterClear" aria-label="Clear filter" hidden>${M("x")}</button></div></header>${o.map(Gr)}<p class="api-empty muted" id="apiEmpty" hidden>No routes match. Try a path segment like <code>sends</code> or a method like <code>DELETE</code>.</p>`);let a=d("#apiFilter"),l=d("#apiFilterClear"),r=()=>Jr(n,s),c=p=>{a.value=p,l.hidden=p==="",Si(p,n,s),r()};a.addEventListener("input",()=>c(a.value)),l.addEventListener("click",()=>{c(""),a.focus()}),mt(s),document.addEventListener("keydown",p=>{let h=p.target instanceof HTMLElement&&(p.target.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(p.target.tagName));p.key==="/"&&!h&&!p.metaKey&&!p.ctrlKey&&!p.altKey?(p.preventDefault(),a.focus()):p.key==="Escape"&&p.target===a&&a.value&&c("")},{signal:t}),Si("",n,s),window.addEventListener("scroll",r,{passive:!0,signal:t}),window.scrollTo(0,0),r()}var kn=e=>e instanceof Error?e.message:String(e);function Qr(e){let t=[Pe(e,"reschedule")?i`<button class="ghost" data-reschedule="${e.id}">Reschedule</button>`:null,Pe(e,"cancel")?i`<button class="ghost" data-cancel="${e.id}">Cancel</button>`:null];return t.some(Boolean)?i`<div class="row" data-closes="${e.fire_at}">${t}</div>`:null}async function ls(e,t){let n={status:"sent",search:"",failures:"",sort:"fire",dir:"desc",offset:0,limit:50};g(e,i`<h1>Sent</h1>
199
+ ${xt()?i`<p class="muted">No email provider is configured, so these sends are recorded here but nothing is delivered.</p>`:null}
190
200
  <div id="stuck"></div>
191
201
  <h2>Scheduled</h2><div id="scheduled" class="muted">Loading…</div>
192
202
  <div id="active"></div>
193
203
  <h2>Sent posts</h2>
194
204
  ${rt({searchPlaceholder:"Search subject\u2026",failures:!0})}
195
205
  <div id="sendsList" class="muted">Loading…</div>
196
- <div id="sendsPager"></div>`);let s=d("#stuck",e),i=d("#scheduled",e),a=d("#active",e),l=d("#sendsList",e),c=d("#sendsPager",e),f=nn(e,t),m=[],u=!1,r=0,b=0,R=new Map;function E(){I(),$(),no()}function y(v){let w=v.filter(q=>q.status==="sending"&&!Et(q));g(a,w.length?o`<h2>In progress</h2>${w.map(sn)}`:o``);for(let q of M("[data-watch]",a))q.onclick=_=>{_.target instanceof Element&&_.target.tagName==="A"||(location.hash=`#/sent/${q.dataset.watch}`)}}function K(v){let w=v.flatMap(_=>{let G=Le(_,"refused");return G?[{s:_,c:G}]:[]}),q=v.flatMap(_=>{let G=Le(_,"wedged");return G?[{s:_,c:G}]:[]});g(s,o`${w.map(({s:_,c:G})=>o`<div class="card stuck-card"><div class="stuck-head"><span class="stuck-dot">⚠️</span><div><strong><a href="#/sent/${_.id}">${_.subject}</a></strong><div class="muted">${G.message}</div></div></div></div>`)}${q.map(({s:_,c:G})=>o`<div class="card stuck-card"><div class="stuck-head"><span class="stuck-dot">⚠️</span><div><strong><a href="#/sent/${_.id}">${_.subject}</a></strong><div class="muted">${G.message}</div></div></div>${Ie(_,"resolve")?o`<button class="primary" data-resolve="${_.id}">Resolve…</button>`:null}</div>`)}`);for(let _ of M("[data-resolve]",s)){let G=q.find(ve=>ve.s.id===_.dataset.resolve);G&&(_.onclick=()=>pn({id:G.s.id,c_in_flight:G.c.count},E))}}function V(){for(let v of R.values()){let w=M("tr[data-id]",l).find(q=>q.dataset.id===v.id)?.querySelector("td.delivered");w&&g(w,ct(v.counts))}}function L(){let v=dt(m,R);K(v),u&&y(v),V()}let O=v=>v==="scheduled"||v==="due",j=v=>v==="sent"||v==="complete";function J(v){for(let w of v.sends)R.set(w.id,w);for(let w of v.removed)R.delete(w);L(),(v.sends.some(w=>O(Ve(w)))||v.changes.some(w=>O(w.from)))&&I(),v.removed.length?(I(),$()):v.changes.some(w=>j(w.to)&&!j(w.from))&&$()}let ie=!1;function H(v){let w=v.filter(q=>q!==null);ie||!w.length||(ie=!0,nt({cursor:w.map(q=>q.cursor).reduce(Kt),sends:w.flatMap(q=>q.sends)},{update:J,stale:()=>{t.aborted||Q(Qn)}},t))}async function N(){try{let v=await T("/api/sends?status=sending&limit=200",{signal:t});return m=v.sends,u=!0,L(),v}catch(v){return oe(a,gn(v),async()=>H([await N()])),null}}async function I(){let v=++r;try{let w=await T("/api/sends?status=scheduled&sort=fire&dir=asc&limit=200",{signal:t});if(v!==r)return w;let{sends:q}=w,_=W=>o`<div class="card spread clickable sched-card" data-post="${W.post_id}"><div><a class="card-link sched-subj" href="#/edit/${W.post_id}">${W.subject}</a><div class="muted">${tn(W)} · ${Z(W.fire_at)} · ${W.audience.count} recipients</div></div>${Aa(W)}</div>`,[G,...ve]=q;if(!G)g(i,o`<p class="muted">Nothing scheduled.</p>`);else{g(i,o`${_(G)}${ve.length?o`<div id="schedMore" hidden>${ve.map(_)}</div><button type="button" class="ghost sched-toggle" id="schedToggle" aria-expanded="false">Show all ${q.length} scheduled</button>`:null}`);let W=i.querySelector("#schedToggle");W&&(W.onclick=()=>{let le=d("#schedMore",i),de=le.hidden;le.hidden=!de,W.setAttribute("aria-expanded",String(de)),W.textContent=de?"Show fewer":`Show all ${q.length} scheduled`})}for(let W of M(".card.clickable",i))W.onclick=le=>{let de=le.target;de instanceof Element&&de.tagName!=="A"&&!de.closest("button")&&(location.hash=`#/edit/${W.dataset.post}`)};for(let W of M("[data-reschedule]",i)){let le=q.find(de=>de.id===W.dataset.reschedule);le&&(W.onclick=()=>mn(le.id,le.fire_at,E))}for(let W of M("[data-cancel]",i))W.onclick=()=>X(W,"Canceling\u2026",async()=>{try{await T(`/api/sends/${W.dataset.cancel}/cancel`,{method:"POST"}),k("Canceled"),E()}catch(le){k(gn(le))}});return f(),w}catch(w){return v===r&&oe(i,gn(w),async()=>H([await I()])),null}}async function $(){let v=++b;try{let w=await T(`/api/sends?${at(n)}`,{signal:t});if(v!==b)return w;let q=w.sends;if(!q.length)return g(l,o`<p class="muted">${n.search?"No sent posts match.":n.failures?"Every sent post delivered cleanly.":"No sent posts yet."}</p>`),g(c,o``),w;g(l,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>${Se("Subject","subject",n)}${Se("When","fire",n)}${Se("Recipients","recipients",n,"num")}<th class="num">Delivered</th></tr></thead><tbody>${q.map(_=>o`<tr class="clickable" data-id="${_.id}"><td class="subject"><a href="#/sent/${_.id}">${_.subject}</a></td><td class="muted">${Z(_.completed_at??_.fire_at)}</td><td class="num recipients"><span class="n">${_.audience.count.toLocaleString()}</span></td><td class="num delivered">${ct(_.counts)}</td></tr>`)}</tbody></table></div>`),Ke(l,n,$);for(let _ of M("tr[data-id]",l))_.onclick=G=>{G.target instanceof Element&&G.target.tagName==="A"||(location.hash=`#/sent/${_.dataset.id}`)};return We(c,n,w.page,$),V(),w}catch(w){return v===b&&oe(l,gn(w),async()=>H([await $()])),null}}lt(e,n,$),H(await Promise.all([I(),N(),$()]))}var Jn=e=>e instanceof Error?e.message:String(e);async function Xn(e,t,n){let s=()=>{n.aborted||Q((l,c)=>Xn(e,l,c))};g(t,o`<p class="muted">Loading…</p>`);let i;try{i=await T(`/api/sends/${e}`,{signal:n})}catch(l){oe(t,Jn(l),s);return}if(n.aborted)return;let{send:a}=i;if(a.status==="scheduled"){location.replace(`#/edit/${a.post_id}`);return}return a.status==="sending"?Oa(i,t,n,s):Ga(e,i,t,n,s)}var _a={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"}},Ba={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 \u2014 they are retried on the next sweep.","needs-attention":"Some deliveries are stuck and need a decision.",settling:"Dispatch complete \u2014 waiting on delivery receipts."};function Ko(e){if(e===null)return"it is retried on its own";let t=e-Date.now();return t>0?`next retry in ${en(t)}`:"next retry due now"}function Da(e){let t=e.provider.halt;return De(e,"refused")&&t?`The provider is refusing this account \u2014 nothing more goes out until it is fixed; ${Ko(t.retry_at)}.`:e.phase==="backing-off"&&t?.reason==="unavailable"?`The provider is unavailable \u2014 ${Ko(t.retry_at)}.`:e.phase==="progressing"&&e.counts.in_flight===0&&e.counts.pending>0?"Paused between ticks \u2014 it resumes on the next sweep.":Ba[e.phase]||""}function Lt(e){let t=_a[e]??{label:e,tone:"muted"};return o`<span class="phase-pill tone-${t.tone}">${t.label}</span>`}function Na(e,t,n,s,i){let a=s>0?xt(100*n/s):0;return o`<div class="wbar-row">
206
+ <div id="sendsPager"></div>`);let s=d("#stuck",e),o=d("#scheduled",e),a=d("#active",e),l=d("#sendsList",e),r=d("#sendsPager",e),c=ln(e,t),p=[],h=!1,u=0,y=0,x=new Map;function k(){_(),b(),nt()}function B(v){let w=v.filter(j=>j.status==="sending"&&!Ct(j));g(a,w.length?i`<h2>In progress</h2>${w.map(dn)}`:i``);for(let j of P("[data-watch]",a))j.onclick=I=>{I.target instanceof Element&&I.target.tagName==="A"||(location.hash=`#/sent/${j.dataset.watch}`)}}function A(v){let w=v.flatMap(I=>{let Y=be(I,"refused");return Y?[{s:I,c:Y}]:[]}),j=v.flatMap(I=>{let Y=be(I,"wedged");return Y?[{s:I,c:Y}]:[]});g(s,i`${w.map(({s:I,c:Y})=>i`<div class="card stuck-card"><div class="stuck-head"><span class="stuck-dot">⚠️</span><div><strong><a href="#/sent/${I.id}">${I.subject}</a></strong><div class="muted">${Y.message}</div></div></div></div>`)}${j.map(({s:I,c:Y})=>i`<div class="card stuck-card"><div class="stuck-head"><span class="stuck-dot">⚠️</span><div><strong><a href="#/sent/${I.id}">${I.subject}</a></strong><div class="muted">${Y.message}</div></div></div>${Pe(I,"resolve")?i`<button class="primary" data-resolve="${I.id}">Resolve…</button>`:null}</div>`)}`);for(let I of P("[data-resolve]",s)){let Y=j.find(re=>re.s.id===I.dataset.resolve);Y&&(I.onclick=()=>vn({id:Y.s.id,c_in_flight:Y.c.count},k))}}function H(){for(let v of x.values()){let w=P("tr[data-id]",l).find(j=>j.dataset.id===v.id)?.querySelector("td.delivered");w&&g(w,ct(v.counts))}}function m(){let v=dt(p,x);A(v),h&&B(v),H()}let L=v=>v==="scheduled"||v==="due",R=v=>v==="sent"||v==="complete";function V(v){for(let w of v.sends)x.set(w.id,w);for(let w of v.removed)x.delete(w);m(),(v.sends.some(w=>L(De(w)))||v.changes.some(w=>L(w.from)))&&_(),v.removed.length?(_(),b()):v.changes.some(w=>R(w.to)&&!R(w.from))&&b()}let ee=!1;function C(v){let w=v.filter(j=>j!==null);ee||!w.length||(ee=!0,tt({cursor:w.map(j=>j.cursor).reduce(zt),sends:w.flatMap(j=>j.sends)},{update:V,stale:()=>{t.aborted||K(ls)}},t))}async function U(){try{let v=await T("/api/sends?status=sending&limit=200",{signal:t});return p=v.sends,h=!0,m(),v}catch(v){return X(a,kn(v),async()=>C([await U()])),null}}async function _(){let v=++u;try{let w=await T("/api/sends?status=scheduled&sort=fire&dir=asc&limit=200",{signal:t});if(v!==u)return w;let{sends:j}=w,I=G=>i`<div class="card spread clickable sched-card" data-post="${G.post_id}"><div><a class="card-link sched-subj" href="#/edit/${G.post_id}">${G.subject}</a><div class="muted">${rn(G)} · ${W(G.fire_at)} · ${G.audience.count} recipients</div></div>${Qr(G)}</div>`,[Y,...re]=j;if(!Y)g(o,i`<p class="muted">Nothing scheduled.</p>`);else{g(o,i`${I(Y)}${re.length?i`<div id="schedMore" hidden>${re.map(I)}</div><button type="button" class="ghost sched-toggle" id="schedToggle" aria-expanded="false">Show all ${j.length} scheduled</button>`:null}`);let G=o.querySelector("#schedToggle");G&&(G.onclick=()=>{let ae=d("#schedMore",o),me=ae.hidden;ae.hidden=!me,G.setAttribute("aria-expanded",String(me)),G.textContent=me?"Show fewer":`Show all ${j.length} scheduled`})}for(let G of P(".card.clickable",o))G.onclick=ae=>{let me=ae.target;me instanceof Element&&me.tagName!=="A"&&!me.closest("button")&&(location.hash=`#/edit/${G.dataset.post}`)};for(let G of P("[data-reschedule]",o)){let ae=j.find(me=>me.id===G.dataset.reschedule);ae&&(G.onclick=()=>gn(ae.id,ae.fire_at,k))}for(let G of P("[data-cancel]",o))G.onclick=()=>z(G,"Canceling\u2026",async()=>{try{await T(`/api/sends/${G.dataset.cancel}/cancel`,{method:"POST"}),$("Canceled"),k()}catch(ae){$(kn(ae))}});return c(),w}catch(w){return v===u&&X(o,kn(w),async()=>C([await _()])),null}}async function b(){let v=++y;try{let w=await T(`/api/sends?${at(n)}`,{signal:t});if(v!==y)return w;let j=w.sends;if(!j.length)return g(l,i`<p class="muted">${n.search?"No sent posts match.":n.failures?"Every sent post delivered cleanly.":"No sent posts yet."}</p>`),g(r,i``),w;g(l,i`<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>${fe("Subject","subject",n)}${fe("When","fire",n)}${fe("Recipients","recipients",n,"num")}<th class="num">Delivered</th></tr></thead><tbody>${j.map(I=>i`<tr class="clickable" data-id="${I.id}"><td class="subject"><a href="#/sent/${I.id}">${I.subject}</a></td><td class="muted">${W(I.completed_at??I.fire_at)}</td><td class="num recipients"><span class="n">${I.audience.count.toLocaleString()}</span></td><td class="num delivered">${ct(I.counts)}</td></tr>`)}</tbody></table></div>`),qe(l,n,b);for(let I of P("tr[data-id]",l))I.onclick=Y=>{Y.target instanceof Element&&Y.target.tagName==="A"||(location.hash=`#/sent/${I.dataset.id}`)};return Oe(r,n,w.page,b),H(),w}catch(w){return v===y&&X(l,kn(w),async()=>C([await b()])),null}}lt(e,n,b),C(await Promise.all([_(),U(),b()]))}var ds=e=>e instanceof Error?e.message:String(e);async function cs(e,t,n){let s=()=>{n.aborted||K((l,r)=>cs(e,l,r))};g(t,i`<p class="muted">Loading…</p>`);let o;try{o=await T(`/api/sends/${e}`,{signal:n})}catch(l){X(t,ds(l),s);return}if(n.aborted)return;let{send:a}=o;if(a.status==="scheduled"){location.replace(`#/edit/${a.post_id}`);return}return a.status==="sending"?al(o,t,n,s):ml(e,o,t,n,s)}var Xr={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"}},el={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 \u2014 they are retried on the next sweep.","needs-attention":"Some deliveries are stuck and need a decision.",settling:"Dispatch complete \u2014 waiting on delivery receipts."};function $i(e){if(e===null)return"it is retried on its own";let t=e-Date.now();return t>0?`next retry in ${an(t)}`:"next retry due now"}function tl(e){let t=e.provider.halt;return ke(e,"refused")&&t?`The provider is refusing this account \u2014 nothing more goes out until it is fixed; ${$i(t.retry_at)}.`:e.phase==="backing-off"&&t?.reason==="unavailable"?`The provider is unavailable \u2014 ${$i(t.retry_at)}.`:e.phase==="progressing"&&e.counts.in_flight===0&&e.counts.pending>0?"Paused between ticks \u2014 it resumes on the next sweep.":el[e.phase]||""}function Pt(e){let t=Xr[e]??{label:e,tone:"muted"};return i`<span class="phase-pill tone-${t.tone}">${t.label}</span>`}function nl(e,t,n,s,o){let a=s>0?Rt(100*n/s):0;return i`<div class="wbar-row">
197
207
  <div class="wbar-head"><span class="wbar-label">${t}</span><span class="wbar-count">${n.toLocaleString()} / ${s.toLocaleString()}</span></div>
198
208
  <div class="wbar" role="progressbar" aria-valuenow="${a}" aria-valuemin="0" aria-valuemax="100"><div class="wbar-fill tone-${e}" style="width:${a}%"></div></div>
199
- ${i?o`<div class="wbar-sub muted">${i}</div>`:null}
200
- </div>`}function Ia(e,t,n,s,i){let a=s>0?xt(100*n/s):0,l=s>0?xt(100*t/s):0;return o`<div class="wbar-row">
209
+ ${o?i`<div class="wbar-sub muted">${o}</div>`:null}
210
+ </div>`}function sl(e,t,n,s,o){let a=s>0?Rt(100*n/s):0,l=s>0?Rt(100*t/s):0;return i`<div class="wbar-row">
201
211
  <div class="wbar-head"><span class="wbar-label">${e}</span><span class="wbar-count">${t.toLocaleString()} of ${n.toLocaleString()} accepted</span></div>
202
212
  <div class="wbar dual" role="progressbar" aria-valuenow="${l}" aria-valuemin="0" aria-valuemax="100">
203
213
  <div class="wbar-fill tone-accepted" style="width:${a}%"></div>
204
214
  <div class="wbar-fill tone-ok" style="width:${l}%"></div>
205
215
  </div>
206
- <div class="wbar-sub muted">${i}</div>
207
- </div>`}var qa=[{key:"pending",label:"Pending",sw:"neutral"},{key:"in_flight",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 Ua(e){return o`<div class="wcounts">${qa.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 Qo(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,a=e.status==="sending"?`${Da(e)}${i?`${s?` \xB7 ~${s.toLocaleString()}/min`:""} \xB7 ETA ${en(i)}`:""}`:"Dispatch complete.",l=(t.unsent||0)+(t.bounced||0)+(t.complained||0),c=yt()?"No email provider configured \u2014 nothing is delivered":`Provider: ${e.provider?.name||"\u2014"}`,f=Le(e,"refused"),m=Le(e,"stuck");return o`
208
- ${m?o`<div class="health amber" role="status"><span class="health-dot">⚠️</span><div>${m.message}</div></div>`:null}
209
- ${f?o`<div class="health red" role="alert"><span class="health-dot">⚠️</span><div><strong>The provider is refusing this account</strong><div>${f.error}</div><div>${f.advice}</div><div>${f.since===null?null:`Since ${Z(f.since)}. `}No one has been marked unsent, and once this is fixed the send resumes at its next retry${f.retry_at===null?"":`, ${Z(f.retry_at)}`}.</div></div></div>`:null}
216
+ <div class="wbar-sub muted">${o}</div>
217
+ </div>`}var ol=[{key:"pending",label:"Pending",sw:"neutral"},{key:"in_flight",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 il(e){return i`<div class="wcounts">${ol.map(t=>i`<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 Hi(e){let t=e.counts,n=t.accepted+t.delivered+t.bounced+t.complained,s=e.dispatch.rate_per_min,o=e.dispatch.eta_ms,a=e.status==="sending"?`${tl(e)}${o?`${s?` \xB7 ~${s.toLocaleString()}/min`:""} \xB7 ETA ${an(o)}`:""}`:"Dispatch complete.",l=(t.unsent||0)+(t.bounced||0)+(t.complained||0),r=xt()?"No email provider configured, so nothing is delivered":`Provider: ${e.provider?.name||"\u2014"}`,c=be(e,"refused"),p=be(e,"stuck");return i`
218
+ ${p?i`<div class="health amber" role="status"><span class="health-dot">⚠️</span><div>${p.message}</div></div>`:null}
219
+ ${c?i`<div class="health red" role="alert"><span class="health-dot">⚠️</span><div><strong>The provider is refusing this account</strong><div>${c.error}</div><div>${c.advice}</div><div>${c.since===null?null:`Since ${W(c.since)}. `}No one has been marked unsent, and once this is fixed the send resumes at its next retry${c.retry_at===null?"":`, ${W(c.retry_at)}`}.</div></div></div>`:null}
210
220
  <div class="wbars">
211
- ${Na("sending","Dispatch \u2014 provider-accepted",n,e.audience.count,a)}
212
- ${Ia("Delivery \u2014 webhook-confirmed",e.delivery.confirmed,n,e.audience.count,"Delivery lags acceptance \u2014 grey is accepted-but-unconfirmed, green fills in as receipts arrive.")}
221
+ ${nl("sending","Dispatch \u2014 provider-accepted",n,e.audience.count,a)}
222
+ ${sl("Delivery \u2014 webhook-confirmed",e.delivery.confirmed,n,e.audience.count,"Delivery lags acceptance \u2014 grey is accepted-but-unconfirmed, green fills in as receipts arrive.")}
213
223
  </div>
214
- ${Ua(t)}
215
- <div class="whealth${l?" has-failures":""}"><span class="whealth-dot"></span><span>${c} · ${l?`${l.toLocaleString()} bounced / unsent / complained`:"no delivery failures"}</span></div>`}function Jo(e){let t=e.started_at?`Started ${Z(e.started_at)}`:"Sending now";return o`${t} · ${e.audience.count.toLocaleString()} recipients`}function Wo(e){let t=e;return o`
224
+ ${il(t)}
225
+ <div class="whealth${l?" has-failures":""}"><span class="whealth-dot"></span><span>${r} · ${l?`${l.toLocaleString()} bounced / unsent / complained`:"no delivery failures"}</span></div>`}function Ri(e){let t=e.started_at?`Started ${W(e.started_at)}`:"Sending now";return i`${t} · ${e.audience.count.toLocaleString()} recipients`}function ki(e){let t=e;return i`
216
226
  <div class="editor-head">
217
227
  <a href="#/sent" class="back">← Sent</a>
218
- ${Ie(t,"resolve")?o`<button type="button" class="primary" id="resolveBtn">Resolve…</button>`:null}
228
+ ${Pe(t,"resolve")?i`<button type="button" class="primary" id="resolveBtn">Resolve…</button>`:null}
219
229
  </div>
220
230
  <div class="card rec-card watch-card">
221
231
  <div class="rec-head">
222
- <div class="watch-title"><h1>${e.subject||o`<em>untitled</em>`}</h1><span id="watchPill">${Lt(t.phase)}</span></div>
223
- <div class="rec-meta" id="watchMeta">${Jo(e)}</div>
232
+ <div class="watch-title"><h1>${e.subject||i`<em>untitled</em>`}</h1><span id="watchPill">${Pt(t.phase)}</span></div>
233
+ <div class="rec-meta" id="watchMeta">${Ri(e)}</div>
224
234
  </div>
225
- <div id="watchBody">${Qo(t)}</div>
235
+ <div id="watchBody">${Hi(t)}</div>
226
236
  <p class="rec-note muted">This view updates live while the send is in flight. Delivery receipts keep arriving after dispatch finishes — the record stays accurate as they settle.</p>
227
- </div>`}function Zo(e,t,n){let s=e.querySelector("#resolveBtn");if(s){let i=Le(t,"wedged")?.count??t.counts.in_flight;s.onclick=()=>pn({id:t.id,c_in_flight:i},n)}}function Go(e,t){let n=e.querySelector("#watchPill");n&&g(n,Lt(t.phase));let s=e.querySelector("#watchMeta");s&&g(s,Jo(t));let i=e.querySelector("#watchBody");i&&g(i,Qo(t))}function Oa(e,t,n,s){g(t,Wo(e.send)),Zo(t,e.send,s);let i=e.send;Ge(1e3,()=>Go(t,i),n),$t(e,{update:a=>{if(a.status!=="sending"){s();return}i=a,Ie(a,"resolve")!==!!t.querySelector("#resolveBtn")?(g(t,Wo(a)),Zo(t,a,s)):Go(t,a)},removed:s,stale:s},n)}function zo(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 Yo(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.in_flight&&n.push(`${e.in_flight.toLocaleString()} in flight`);let s=`All ${t.toLocaleString()} accounted for: ${n.join(", ")}.`,i=e.bounced||e.complained?" Hard bounces and spam complaints are suppressed automatically, so those addresses won't be mailed again.":"";return`${s}${i}`}function ja(e,t){return Object.keys(e).every(n=>e[n]===t[n])}function Fa(e){if(e.event==="delivered")return{label:"Delivered",sw:"ok"};if(e.event==="bounced")return e.bounce_kind==="hard"?{label:"Hard bounce",sw:"warn",hint:"permanent \u2014 suppressed the address"}:e.bounce_kind==="soft"?{label:"Soft bounce",sw:"warn",hint:"transient \u2014 counted, not suppressed"}:{label:"Bounce",sw:"warn"};if(e.event==="complained")return{label:"Complained",sw:"danger",hint:"suppressed"};switch(e.status){case"unsent":return{label:"Unsent",sw:"neutral"};case"skipped":return{label:"Skipped",sw:"neutral"};case"accepted":return{label:"Accepted",sw:"sending",hint:"awaiting a delivery receipt"};default:return{label:"In flight",sw:"sending"}}}var Va=[{v:"failures",label:"Failures"},{v:"delivered",label:"Delivered"},{v:"all",label:"All"}];function Ka(e){return e.search?"No recipients match that address.":e.view==="failures"?"No delivery failures \u2014 every recipient was accepted or delivered.":e.view==="delivered"?"No delivery receipts confirmed yet.":"No recipients on this send."}function Wa(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 Za(e,t){let n=e.map(s=>{let i=Fa(s),a=i.hint?o`<span class="rec-out" title="${i.hint}"><span class="rec-sw sw-${i.sw}"></span>${i.label}</span>`:o`<span class="rec-out"><span class="rec-sw sw-${i.sw}"></span>${i.label}</span>`,l=s.error||s.event_detail||"",c=s.event_at?Z(s.event_at):"";return o`<tr><td class="rec-email">${s.email}</td><td>${a}</td><td class="muted">${l||"\u2014"}</td><td class="muted">${c||"\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>${Se("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 Ga(e,t,n,s,i){let{send:a,outcomes:l}=t;if(a.status==="canceled"){Ya(a,n);return}let c=a.links.archive,f=l.recipients,m=a.completed_at??a.fire_at;g(n,o`
237
+ </div>`}function xi(e,t,n){let s=e.querySelector("#resolveBtn");if(s){let o=be(t,"wedged")?.count??t.counts.in_flight;s.onclick=()=>vn({id:t.id,c_in_flight:o},n)}}function Ei(e,t){let n=e.querySelector("#watchPill");n&&g(n,Pt(t.phase));let s=e.querySelector("#watchMeta");s&&g(s,Ri(t));let o=e.querySelector("#watchBody");o&&g(o,Hi(t))}function al(e,t,n,s){g(t,ki(e.send)),xi(t,e.send,s);let o=e.send;We(1e3,()=>Ei(t,o),n),Lt(e,{update:a=>{if(a.status!=="sending"){s();return}o=a,Pe(a,"resolve")!==!!t.querySelector("#resolveBtn")?(g(t,ki(a)),xi(t,a,s)):Ei(t,a)},removed:s,stale:s},n)}function Ti(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 i`${t.map(n=>i`<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 Li(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.in_flight&&n.push(`${e.in_flight.toLocaleString()} in flight`);let s=`All ${t.toLocaleString()} accounted for: ${n.join(", ")}.`,o=e.bounced||e.complained?" Hard bounces and spam complaints are suppressed automatically, so those addresses won't be mailed again.":"";return`${s}${o}`}function rl(e,t){return Object.keys(e).every(n=>e[n]===t[n])}function ll(e){if(e.event==="delivered")return{label:"Delivered",sw:"ok"};if(e.event==="bounced")return e.bounce_kind==="hard"?{label:"Hard bounce",sw:"warn",hint:"permanent \u2014 suppressed the address"}:e.bounce_kind==="soft"?{label:"Soft bounce",sw:"warn",hint:"transient \u2014 counted, not suppressed"}:{label:"Bounce",sw:"warn"};if(e.event==="complained")return{label:"Complained",sw:"danger",hint:"suppressed"};switch(e.status){case"unsent":return{label:"Unsent",sw:"neutral"};case"skipped":return{label:"Skipped",sw:"neutral"};case"accepted":return{label:"Accepted",sw:"sending",hint:"awaiting a delivery receipt"};default:return{label:"In flight",sw:"sending"}}}var dl=[{v:"failures",label:"Failures"},{v:"delivered",label:"Delivered"},{v:"all",label:"All"}];function cl(e){return e.search?"No recipients match that address.":e.view==="failures"?"No delivery failures \u2014 every recipient was accepted or delivered.":e.view==="delivered"?"No delivery receipts confirmed yet.":"No recipients on this send."}function ul(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 pl(e,t){let n=e.map(s=>{let o=ll(s),a=o.hint?i`<span class="rec-out" title="${o.hint}"><span class="rec-sw sw-${o.sw}"></span>${o.label}</span>`:i`<span class="rec-out"><span class="rec-sw sw-${o.sw}"></span>${o.label}</span>`,l=s.error||s.event_detail||"",r=s.event_at?W(s.event_at):"";return i`<tr><td class="rec-email">${s.email}</td><td>${a}</td><td class="muted">${l||"\u2014"}</td><td class="muted">${r||"\u2014"}</td></tr>`});return i`<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>${fe("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 ml(e,t,n,s,o){let{send:a,outcomes:l}=t;if(a.status==="canceled"){hl(a,n);return}let r=a.links.archive,c=l.recipients,p=a.completed_at??a.fire_at;g(n,i`
228
238
  <div class="editor-head">
229
239
  <a href="#/sent" class="back">← Sent</a>
230
- ${c?o`<button type="button" class="primary" id="viewPublished">View published post&nbsp;↗</button>`:null}
240
+ ${r?i`<button type="button" class="primary" id="viewPublished">View published post&nbsp;↗</button>`:null}
231
241
  </div>
232
242
  <div class="card rec-card">
233
243
  <div class="rec-head">
234
- <div class="watch-title"><h1>${a.subject||o`<em>untitled</em>`}</h1><span id="recPill">${Lt(a.phase)}</span></div>
235
- <div class="rec-meta">Sent ${Z(m)} · ${f.toLocaleString()} recipients</div>
244
+ <div class="watch-title"><h1>${a.subject||i`<em>untitled</em>`}</h1><span id="recPill">${Pt(a.phase)}</span></div>
245
+ <div class="rec-meta">Sent ${W(p)} · ${c.toLocaleString()} recipients</div>
236
246
  </div>
237
247
  <p class="rec-tiles-cap muted">Delivery outcomes — these keep updating as receipts arrive; the audience at fire and the published post are fixed.</p>
238
- <div class="rec-tiles">${zo(l)}</div>
239
- <p class="rec-recon muted">${Yo(l)}</p>
248
+ <div class="rec-tiles">${Ti(l)}</div>
249
+ <p class="rec-recon muted">${Li(l)}</p>
240
250
  <div class="rec-actions">
241
251
  <button type="button" class="ghost" id="csvBtn">Export recipients (CSV)</button>
242
252
  </div>
@@ -244,7 +254,7 @@
244
254
  <div class="rec-people-head">
245
255
  <h2 class="rec-people-title">Recipients</h2>
246
256
  <div class="rec-views" role="group" aria-label="Which recipients to show">
247
- ${Va.map((H,N)=>o`<button type="button" class="rec-view-btn" data-view="${H.v}" aria-pressed="${N===0?"true":"false"}">${H.label}</button>`)}
257
+ ${dl.map((C,U)=>i`<button type="button" class="rec-view-btn" data-view="${C.v}" aria-pressed="${U===0?"true":"false"}">${C.label}</button>`)}
248
258
  </div>
249
259
  </div>
250
260
  <input class="rec-people-search" type="search" placeholder="Search email…" aria-label="Search recipients by email" autocomplete="off">
@@ -252,17 +262,117 @@
252
262
  <div id="recPager"></div>
253
263
  </div>
254
264
  <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>
255
- </div>`);let u=document.getElementById("viewPublished");u&&c&&(u.onclick=()=>window.open(c,"_blank","noopener"));let r={view:"failures",search:"",sort:"email",dir:"asc",offset:0,limit:50},b=d("#recRows",n),R=d("#recPager",n),E=0,y=!1,K=async H=>{let N=++E;H||(y=!0);try{let I=await T(`/api/sends/${e}/deliveries?${Wa(r)}`,{signal:s});if(N!==E)return;let $=I.page.total>0?Math.floor((I.page.total-1)/I.page.limit)*I.page.limit:0;if(!I.deliveries.length&&r.offset>$)return r.offset=$,K(H);y=!1,L(()=>{if(!I.deliveries.length){g(b,o`<p class="rec-people-empty muted">${Ka(r)}</p>`),g(R,o``);return}g(b,Za(I.deliveries,r)),Ke(b,r,V),We(R,r,I.page,V)})}catch(I){N===E&&(!H||y)&&(y=!1,oe(b,Jn(I),V))}},V=()=>K(!1),L=H=>{let N=document.activeElement,I=N instanceof HTMLElement&&(b.contains(N)||R.contains(N)),$=I?N.dataset.sort:void 0,v=I?["pager-prev","pager-next"].find(q=>N.classList.contains(q)):void 0;H();let w=$?b.querySelector(`.th-sort[data-sort="${$}"]`):v?R.querySelector(`.${v}`):null;w&&!w.disabled&&w.focus()},O=M(".rec-view-btn",n);for(let H of O)H.onclick=()=>{let N=H.dataset.view;(N==="failures"||N==="delivered"||N==="all")&&(r.view=N),r.offset=0;for(let I of O)I.setAttribute("aria-pressed",String(I===H));V()};let j=n.querySelector(".rec-people-search");if(j){let H;j.oninput=()=>{clearTimeout(H),H=setTimeout(()=>{r.search=j.value,r.offset=0,V()},250)}}V();let J=d("#csvBtn");J.onclick=()=>X(J,"Exporting\u2026",async()=>{try{let H=await $s(a.links.deliveries_csv),N=URL.createObjectURL(new Blob([H.text],{type:"text/csv"})),I=document.createElement("a");I.href=N,I.download=H.filename??"deliveries.csv",I.click(),setTimeout(()=>URL.revokeObjectURL(N),1e4)}catch(H){k(Jn(H))}});let ie=l;$t(t,{update(H){if(H.status==="sending"){i();return}g(d("#recPill",n),Lt(H.phase));let N=za(H);ja(ie,N)||(ie=N,g(d(".rec-tiles",n),zo(N)),d(".rec-recon",n).textContent=Yo(N),K(!0))},removed:i,stale:i},s)}function za(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,in_flight:t.pending+t.in_flight}}function Ya(e,t){g(t,o`
265
+ </div>`);let h=document.getElementById("viewPublished");h&&r&&(h.onclick=()=>window.open(r,"_blank","noopener"));let u={view:"failures",search:"",sort:"email",dir:"asc",offset:0,limit:50},y=d("#recRows",n),x=d("#recPager",n),k=0,B=!1,A=async C=>{let U=++k;C||(B=!0);try{let _=await T(`/api/sends/${e}/deliveries?${ul(u)}`,{signal:s});if(U!==k)return;let b=_.page.total>0?Math.floor((_.page.total-1)/_.page.limit)*_.page.limit:0;if(!_.deliveries.length&&u.offset>b)return u.offset=b,A(C);B=!1,m(()=>{if(!_.deliveries.length){g(y,i`<p class="rec-people-empty muted">${cl(u)}</p>`),g(x,i``);return}g(y,pl(_.deliveries,u)),qe(y,u,H),Oe(x,u,_.page,H)})}catch(_){U===k&&(!C||B)&&(B=!1,X(y,ds(_),H))}},H=()=>A(!1),m=C=>{let U=document.activeElement,_=U instanceof HTMLElement&&(y.contains(U)||x.contains(U)),b=_?U.dataset.sort:void 0,v=_?["pager-prev","pager-next"].find(j=>U.classList.contains(j)):void 0;C();let w=b?y.querySelector(`.th-sort[data-sort="${b}"]`):v?x.querySelector(`.${v}`):null;w&&!w.disabled&&w.focus()},L=P(".rec-view-btn",n);for(let C of L)C.onclick=()=>{let U=C.dataset.view;(U==="failures"||U==="delivered"||U==="all")&&(u.view=U),u.offset=0;for(let _ of L)_.setAttribute("aria-pressed",String(_===C));H()};let R=n.querySelector(".rec-people-search");if(R){let C;R.oninput=()=>{clearTimeout(C),C=setTimeout(()=>{u.search=R.value,u.offset=0,H()},250)}}H();let V=d("#csvBtn");V.onclick=()=>z(V,"Exporting\u2026",async()=>{try{let C=await Ds(a.links.deliveries_csv),U=URL.createObjectURL(new Blob([C.text],{type:"text/csv"})),_=document.createElement("a");_.href=U,_.download=C.filename??"deliveries.csv",_.click(),setTimeout(()=>URL.revokeObjectURL(U),1e4)}catch(C){$(C instanceof se&&C.status===404?"This send no longer exists, so there are no deliveries to export.":ds(C))}});let ee=l;Lt(t,{update(C){if(C.status==="sending"){o();return}g(d("#recPill",n),Pt(C.phase));let U=fl(C);rl(ee,U)||(ee=U,g(d(".rec-tiles",n),Ti(U)),d(".rec-recon",n).textContent=Li(U),A(!0))},removed:o,stale:o},s)}function fl(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,in_flight:t.pending+t.in_flight}}function hl(e,t){g(t,i`
256
266
  <div class="editor-head">
257
267
  <a href="#/sent" class="back">← Sent</a>
258
268
  </div>
259
269
  <div class="card rec-card">
260
270
  <div class="rec-head">
261
- <div class="watch-title"><h1>${e.subject||o`<em>untitled</em>`}</h1><span id="recPill">${Lt(e.phase)}</span></div>
262
- <div class="rec-meta">Canceled ${Z(e.completed_at)} · was scheduled for ${Z(e.fire_at)}</div>
271
+ <div class="watch-title"><h1>${e.subject||i`<em>untitled</em>`}</h1><span id="recPill">${Pt(e.phase)}</span></div>
272
+ <div class="rec-meta">Canceled ${W(e.completed_at)} · was scheduled for ${W(e.fire_at)}</div>
263
273
  </div>
264
274
  <p class="rec-note muted">This send was canceled before it fired, so no one was mailed.</p>
265
- </div>`)}function es(e){let t=document.getElementById(e);if(!t)throw new Error(`the admin page has no #${e}`);return t}var bn=(()=>{let e=es("savebar"),t=e.querySelector(".savebar-msg"),n=es("savebarSave"),s=es("savebarDiscard"),i=0,a=m=>{t&&g(t,o`<span class="savebar-dot"></span><span>${m}</span>`)},l=()=>{e.hidden=!1,e.offsetHeight,e.classList.add("show")};function c(){i++,e.classList.remove("show","is-error"),e.hidden=!0,n.onclick=null,s.onclick=null,document.body.classList.remove("has-savebar")}function f({onSave:m,onDiscard:u,saveLabel:r="Save changes",discardLabel:b="Discard"},R){if(R.aborted)return{setDirty(){},showError(){}};let E=++i;R.addEventListener("abort",()=>{i===E&&c()},{once:!0}),n.textContent=r,s.textContent=b,e.classList.remove("show","is-error"),a("You have unsaved changes."),e.hidden=!0,document.body.classList.add("has-savebar"),n.onclick=()=>X(n,"Saving\u2026",()=>Promise.resolve(m())),s.onclick=()=>u();let y=()=>i===E;return{setDirty(K){y()&&(e.classList.remove("is-error"),K?(a("You have unsaved changes."),l()):e.classList.remove("show"))},showError(K){y()&&(e.classList.add("is-error"),a(K),l())}}}return{attach:f}})();var Xo=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");function ei(e,t){return e?`<img class="logo" src="${Xo(e)}" alt="${Xo(t)}" width="44" height="44" />`:""}var yn=e=>e instanceof Error?e.message:String(e),ti=!1,Qa=[{group:"Post",vars:[{token:"{{ .Post.Body }}",desc:"Your post's Markdown, rendered to HTML."},{token:"{{ .Post.Subject }}",desc:"The post's 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's address (filled per send)."},{token:"{{ .Email.UnsubscribeURL }}",desc:"Their one-click unsubscribe link."},{token:"{{ .Email.ViewInBrowserURL }}",desc:"The web version's 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."}]}],ni={signed:{label:"Signed",html:`<style>
275
+ </div>`)}function us(e){let t=document.getElementById(e);if(!t)throw new Error(`the admin page has no #${e}`);return t}var xn=(()=>{let e=us("savebar"),t=e.querySelector(".savebar-msg"),n=us("savebarSave"),s=us("savebarDiscard"),o=0,a=p=>{t&&g(t,i`<span class="savebar-dot"></span><span>${p}</span>`)},l=()=>{e.hidden=!1,e.offsetHeight,e.classList.add("show")};function r(){o++,e.classList.remove("show","is-error"),e.hidden=!0,n.onclick=null,s.onclick=null,document.body.classList.remove("has-savebar")}function c({onSave:p,onDiscard:h,saveLabel:u="Save changes",discardLabel:y="Discard"},x){if(x.aborted)return{setDirty(){},showError(){}};let k=++o;x.addEventListener("abort",()=>{o===k&&r()},{once:!0}),n.textContent=u,s.textContent=y,e.classList.remove("show","is-error"),a("You have unsaved changes."),e.hidden=!0,document.body.classList.add("has-savebar"),n.onclick=()=>z(n,"Saving\u2026",()=>Promise.resolve(p())),s.onclick=()=>h();let B=()=>o===k;return{setDirty(A){B()&&(e.classList.remove("is-error"),A?(a("You have unsaved changes."),l()):e.classList.remove("show"))},showError(A){B()&&(e.classList.add("is-error"),a(A),l())}}}return{attach:c}})();var Ee=e=>e instanceof Error?e.message:String(e),At=i` hidden`;function ue(e,t){return i`<span class="set-chip ${e}">${M(e)}${t}</span>`}function pe(e,t,n=null){return i`<div class="set-sec-head"><h2 class="set-sec-title">${e}</h2>${t}${n}<span class="set-rule"></span></div>`}function _t(e,t){return(String(e||t).trim()[0]||"K").toUpperCase()}function Ci(e){let{state:t,fromName:n}=e;return i`
276
+ <section class="set-sec">
277
+ ${pe("Confirmation email",ue("editable","Editable"))}
278
+ <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>
279
+ <div class="set-preview">
280
+ <div class="set-preview-bar set-ce-bar">
281
+ <div class="set-ce-modetog" role="group" aria-label="Confirmation email view">
282
+ <button type="button" class="set-ce-modebtn" id="ceTabPreview" aria-pressed="true">${M("preview")}Preview</button>
283
+ <button type="button" class="set-ce-modebtn" id="ceTabEdit" aria-pressed="false">${M("editable")}Edit</button>
284
+ </div>
285
+ </div>
286
+ <div id="cePreviewBody">
287
+ <div class="set-inbox">
288
+ <div class="set-inbox-avatar" id="cePvAvatar">${_t(t.name,n)}</div>
289
+ <div class="set-inbox-body">
290
+ <div class="set-inbox-top"><span class="set-inbox-from" id="cePvFrom">${t.name||n}</span><span class="set-inbox-time">now</span></div>
291
+ <div class="set-inbox-subj" id="cePvSubject"></div>
292
+ </div>
293
+ </div>
294
+ <div class="set-ce-stage">
295
+ <div class="set-ce-card">
296
+ <div class="set-ce-mast" id="cePvMast" hidden>
297
+ <div class="set-ce-mast-logo" id="cePvMastLogo"></div>
298
+ <div>
299
+ <div class="set-ce-mast-name" id="cePvMastName"></div>
300
+ <div class="set-ce-mast-tag" id="cePvMastTag"></div>
301
+ </div>
302
+ </div>
303
+ <p class="set-ce-msg" id="cePvBody"></p>
304
+ <p class="set-ce-btnrow"><a class="set-ce-btn" id="cePvButton" href="#" onclick="return false"></a></p>
305
+ <p class="set-ce-foot" id="cePvFoot"></p>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ <div id="ceEditBody" hidden>
310
+ <div class="set-ce-edit">
311
+ <div class="set-field">
312
+ <label for="ceSubject">Subject line</label>
313
+ <input id="ceSubject" value="${t.confirmation.subject}" maxlength="200" autocomplete="off">
314
+ </div>
315
+ <div class="set-field">
316
+ <label for="ceMessage">Message</label>
317
+ <textarea id="ceMessage" rows="3" maxlength="1000">${t.confirmation.body}</textarea>
318
+ <p class="field-hint">The line above the confirm button.</p>
319
+ </div>
320
+ <div class="set-field">
321
+ <label for="ceButton">Button label</label>
322
+ <input id="ceButton" value="${t.confirmation.buttonLabel}" maxlength="80" autocomplete="off">
323
+ </div>
324
+ <div class="set-field">
325
+ <label for="ceFooter">Reassurance line</label>
326
+ <input id="ceFooter" value="${t.confirmation.reassurance}" maxlength="400" autocomplete="off">
327
+ <p class="field-hint">The quiet footer for anyone who didn’t sign up. Leave blank to omit it.</p>
328
+ </div>
329
+ </div>
330
+ <div class="set-ce-editfoot">
331
+ <button type="button" class="ghost" id="ceReset">Reset to Kestrel default</button>
332
+ <span class="grow"></span>
333
+ <button type="button" class="ghost" id="ceToPreview">See preview →</button>
334
+ </div>
335
+ </div>
336
+ </div>
337
+ </section>`}function Mi(e,t){let{state:n}=t,s=t.data.settings.confirmationEmailDefault,o={subject:d("#ceSubject",e),message:d("#ceMessage",e),button:d("#ceButton",e),footer:d("#ceFooter",e)},a={avatar:d("#cePvAvatar",e),from:d("#cePvFrom",e),subject:d("#cePvSubject",e),body:d("#cePvBody",e),button:d("#cePvButton",e),foot:d("#cePvFoot",e),mast:d("#cePvMast",e),mastLogo:d("#cePvMastLogo",e),mastName:d("#cePvMastName",e),mastTag:d("#cePvMastTag",e)},l=()=>{let x=n.confirmation;a.avatar.textContent=_t(n.name,t.fromName),a.from.textContent=n.name||t.fromName,a.subject.textContent=x.subject.trim()||s.subject||"",a.body.textContent=x.body.trim()||s.body||"",a.button.textContent=x.buttonLabel.trim()||s.buttonLabel||"Confirm",a.foot.textContent=x.reassurance,a.foot.hidden=!x.reassurance.trim();let k=n.name||t.fromDisplay,B=!!n.logoUrl,A=B||!!k;a.mast.hidden=!A,A&&(a.mastLogo.hidden=!B,B&&g(a.mastLogo,i`<img src="${n.logoUrl}" alt="">`),a.mastName.textContent=k,a.mastName.hidden=!k,a.mastTag.textContent=n.tagline,a.mastTag.hidden=!n.tagline)},r=()=>{n.confirmation.subject=o.subject.value,n.confirmation.body=o.message.value,n.confirmation.buttonLabel=o.button.value,n.confirmation.reassurance=o.footer.value,t.changed()};for(let x of Object.values(o))x.addEventListener("input",r);let c=d("#ceEditBody",e),p=d("#cePreviewBody",e),h=d("#ceTabEdit",e),u=d("#ceTabPreview",e),y=x=>{c.hidden=!x,p.hidden=x,h.setAttribute("aria-pressed",String(x)),u.setAttribute("aria-pressed",String(!x)),x&&o.subject.focus()};return u.onclick=()=>y(!1),h.onclick=()=>y(!0),d("#ceToPreview",e).onclick=()=>y(!1),d("#ceReset",e).onclick=()=>{o.subject.value=s.subject||"",o.message.value=s.body||"",o.button.value=s.buttonLabel||"",o.footer.value=s.reassurance||"",r()},l(),{repaint:l,reset(){o.subject.value=n.confirmation.subject,o.message.value=n.confirmation.body,o.button.value=n.confirmation.buttonLabel,o.footer.value=n.confirmation.reassurance,l()}}}var vl={fake:"Fake (dev, dead-end)",ses:"Amazon SES",resend:"Resend"};function ps(e){return vl[e]||e}var gl={generic:"a generic provider",resend:"Resend",ses:"Amazon SES"};function bl(e){let t=String(e||"").match(/<([^>]+)>/);return t?.[1]?t[1]:String(e||"")}function Pi(e){let t=e.data.deployment,{fromName:n}=e;return i`
338
+ <section class="set-sec">
339
+ ${pe("Email sender",ue("readonly","Read-only \xB7 set at deploy"))}
340
+ <div class="set-card sunken">
341
+ <div class="set-ro-note">${M("readonly")}<span>The sender is fixed at deploy via environment secrets, so it can’t be edited here. Change it in the <a class="set-link" href="#/docs">setup guide</a>, then redeploy. Credentials are never shown.</span></div>
342
+ <div class="set-preview-bar" style="background:transparent">
343
+ <span class="set-preview-lbl">Inbox preview</span>
344
+ <span class="set-preview-dot">How readers see the sender</span>
345
+ </div>
346
+ <div class="set-inbox">
347
+ <div class="set-inbox-avatar">${_t(n,n)}</div>
348
+ <div class="set-inbox-body">
349
+ <div class="set-inbox-top"><span class="set-inbox-from">${n}</span><span class="set-inbox-time">9:02 AM</span></div>
350
+ <div class="set-inbox-subj">Your latest post: a sample subject line</div>
351
+ <div class="set-inbox-snip">The opening lines of your post show here as the inbox preview…</div>
352
+ <div class="set-inbox-addr">${bl(t.fromAddress)}</div>
353
+ </div>
354
+ </div>
355
+ <div class="set-kv" style="border-top:1px solid var(--line)">
356
+ <div class="set-kv-k">From address</div><div class="set-kv-v"><span class="mono">${t.fromAddress}</span></div>
357
+ <div class="set-kv-k">Sending domain</div><div class="set-kv-v"><span class="mono">${t.sendingDomain}</span></div>
358
+ <div class="set-kv-k">Email provider</div><div class="set-kv-v">${ps(t.provider)}${t.simulation?i`<span class="set-pill">Simulating ${gl[t.simulation.profile]}${t.simulation.faults==="none"?", no failures":""}</span>`:null}</div>
359
+ </div>
360
+ </div>
361
+ </section>`}function Ai(e){let t=e.data.deployment,n=`${t.archiveOrigin||""}${t.archiveBasePath||""}`,s=t.archiveOrigin===t.appOrigin;return i`
362
+ <section class="set-sec">
363
+ ${pe("Instance",ue("readonly","Read-only \xB7 set at deploy"))}
364
+ <div class="set-card sunken">
365
+ <div class="set-ro-note">${M("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>
366
+ <div class="set-kv">
367
+ <div class="set-kv-k">App origin</div><div class="set-kv-v"><span class="mono">${t.appOrigin}</span></div>
368
+ <div class="set-kv-k">Archive URL base</div><div class="set-kv-v"><span class="mono">${n}</span>${s?i`<span class="set-pill">default: app origin</span>`:null}</div>
369
+ <div class="set-kv-k">Image URL base</div><div class="set-kv-v"><span class="mono">${t.mediaPublicBase}</span></div>
370
+ <div class="set-kv-k">Auth mode</div><div class="set-kv-v">${t.authMode==="access"?"Cloudflare Access":"Local dev token"}</div>
371
+ <div class="set-kv-k">Cloudflare Access</div><div class="set-kv-v">${t.accessConfigured?i`<span class="set-pill ok">${M("check")}Configured</span>`:i`<span class="set-pill">Not configured</span>`}</div>
372
+ <div class="set-kv-k">Minimum lead</div><div class="set-kv-v">${Zt(t.minLeadMs)}${t.minLeadMs===Kt?i`<span class="set-pill">default</span>`:null}</div>
373
+ </div>
374
+ </div>
375
+ </section>`}var _i=e=>e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");function Bi(e,t){return e?`<img class="logo" src="${_i(e)}" alt="${_i(t)}" width="44" height="44" />`:""}var En=e=>e instanceof Error?e.message:String(e),Ni=!1,yl=[{group:"Post",vars:[{token:"{{ .Post.Body }}",desc:"Your post's Markdown, rendered to HTML."},{token:"{{ .Post.Subject }}",desc:"The post's 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's address (filled per send)."},{token:"{{ .Email.UnsubscribeURL }}",desc:"Their one-click unsubscribe link."},{token:"{{ .Email.ViewInBrowserURL }}",desc:"The web version's 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."}]}],Ii={signed:{label:"Signed",html:`<style>
266
376
  .email {
267
377
  font: 16px/1.6 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
268
378
  color: #18181b;
@@ -417,7 +527,7 @@
417
527
  <a href="{{ .Email.ViewInBrowserURL }}">View in browser</a>
418
528
  <div class="address">{{ .Publication.Address }}</div>{{ end }}
419
529
  </div>
420
- </div>`}},Ja=e=>Object.hasOwn(ni,e),Xa=`<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>`,er=new Set([".Post.Body",".Publication.Logo"]);function tr(e,t){return String(e).replace(/\{\{\s*(?:if\b[^}]*|end)\s*\}\}/g,"").replace(/\{\{\s*([\w.]+)\s*\}\}/g,(s,i)=>er.has(i)?t[i]||"":ee(t[i]??""))}function nr(e){let t=e.name||"Your publication";return{".Post.Body":Xa,".Post.Subject":"The starlings are back",".Publication.Name":t,".Publication.Tagline":e.tagline||"Your tagline",".Publication.LogoURL":e.logoUrl,".Publication.Logo":ei(e.logoUrl,t),".Publication.Address":e.address,".Email.SentTo":"you@example.com",".Email.UnsubscribeURL":"#unsubscribe",".Email.ViewInBrowserURL":"#view-in-browser"}}function sr(e,t){return tr(e,nr(t))}var or='<!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 ts(e,t,n){let s=!1,i=()=>{try{let l=e.contentDocument?.body?.scrollHeight;l&&(e.style.height=`${Math.max(200,l)}px`)}catch{}},a=()=>{let l=e.contentDocument,c=s&&l?l.querySelector(".kestrel-email"):null;c&&(g(c,F(sr(t(),n()))),i())};return e.addEventListener("load",()=>{s=!0,a();try{let l=e.contentDocument;l?.body&&typeof ResizeObserver<"u"&&new ResizeObserver(i).observe(l.body)}catch{}}),e.srcdoc=or,{repaint:a}}function ir(){return o`${Qa.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 ns(e,t){g(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>.</p></div><div id="tplBody" class="muted">Loading…</div></div>`);let n=d("#tplBody",e),s;try{s=await T("/api/settings",{signal:t})}catch(B){oe(n,yn(B),()=>Q(ns));return}let i=s.settings,a=s.deployment,l=i.publication,c={name:l.name||gt(a.fromAddress)||"",tagline:l.tagline||"",logoUrl:l.logoUrl||"",address:l.address||""},f=i.emailTemplate||"",m=Array.isArray(i.testRecipients)?i.testRecipients:[];g(d("#tplInUse"),rn(s.inUse)),g(n,o`
530
+ </div>`}},wl=e=>Object.hasOwn(Ii,e),Sl=`<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>`,$l=new Set([".Post.Body",".Publication.Logo"]);function kl(e,t){return String(e).replace(/\{\{\s*(?:if\b[^}]*|end)\s*\}\}/g,"").replace(/\{\{\s*([\w.]+)\s*\}\}/g,(s,o)=>$l.has(o)?t[o]||"":Q(t[o]??""))}function xl(e){let t=e.name||"Your publication";return{".Post.Body":Sl,".Post.Subject":"The starlings are back",".Publication.Name":t,".Publication.Tagline":e.tagline||"Your tagline",".Publication.LogoURL":e.logoUrl,".Publication.Logo":Bi(e.logoUrl,t),".Publication.Address":e.address,".Email.SentTo":"you@example.com",".Email.UnsubscribeURL":"#unsubscribe",".Email.ViewInBrowserURL":"#view-in-browser"}}function El(e,t){return kl(e,xl(t))}var Tl='<!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 ms(e,t,n){e.setAttribute("sandbox",ft);let s=!1,o=()=>{try{let l=e.contentDocument?.body?.scrollHeight;l&&(e.style.height=`${Math.max(200,l)}px`)}catch{}},a=()=>{let l=e.contentDocument,r=s&&l?l.querySelector(".kestrel-email"):null;r&&(g(r,D(El(t(),n()))),ht(r.ownerDocument),o())};return e.addEventListener("load",()=>{s=!0,a();try{let l=e.contentDocument;l?.body&&typeof ResizeObserver<"u"&&new ResizeObserver(o).observe(l.body)}catch{}}),e.srcdoc=Tl,{repaint:a}}function Ll(){return i`${yl.map(e=>i`<div class="set-tpl-vargroup"><h4>${e.group}</h4>${e.vars.map(t=>i`<div class="set-tpl-var"><code>${t.token}</code><span class="set-tpl-var-desc">${t.desc}</span></div>`)}</div>`)}`}async function fs(e,t){g(e,i`<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>.</p></div><div id="tplBody" class="muted">Loading…</div></div>`);let n=d("#tplBody",e),s;try{s=await T("/api/settings",{signal:t})}catch(q){X(n,En(q),()=>K(fs));return}let o=s.settings,a=s.deployment,l=o.publication,r={name:Vt(l.name||"",a.fromAddress),tagline:l.tagline||"",logoUrl:l.logoUrl||"",address:l.address||""},c=o.emailTemplate||"",p=Array.isArray(o.testRecipients)?o.testRecipients:[];g(d("#tplInUse"),un(s.inUse)),g(n,i`
421
531
  <div class="set-preview set-tpl-sample">
422
532
  <div class="set-preview-bar">
423
533
  <span class="set-preview-titles">
@@ -429,7 +539,7 @@
429
539
  <button type="button" class="wtog-btn" data-w="640" aria-pressed="true">640</button>
430
540
  <button type="button" class="wtog-btn" data-w="375" aria-pressed="false">375</button>
431
541
  </span>
432
- <button type="button" class="secondary" id="tplTest">${D("send")}<span id="tplTestLbl">Send test email</span></button>
542
+ <button type="button" class="secondary" id="tplTest">${M("send")}<span id="tplTestLbl">Send test email</span></button>
433
543
  </span>
434
544
  </div>
435
545
  <div class="set-email-stage" id="tplStage">
@@ -450,8 +560,8 @@
450
560
  <button type="button" role="menuitem" data-example="plain"><span class="set-menu-name">Plain</span><span class="set-menu-desc">Just your post and the footer links. Your mailing address sits in the footer once you add one.</span></button>
451
561
  </div>
452
562
  </div>
453
- <button type="button" class="set-icon-btn" id="tplLineNums" aria-pressed="false" title="Show line numbers" aria-label="Show line numbers">${D("lines")}</button>
454
- <button type="button" class="ghost" id="tplCopyAll" title="Copy the whole template to the clipboard">${D("copyout")}<span id="tplCopyLbl">Copy</span></button>
563
+ <button type="button" class="set-icon-btn" id="tplLineNums" aria-pressed="false" title="Show line numbers" aria-label="Show line numbers">${M("lines")}</button>
564
+ <button type="button" class="ghost" id="tplCopyAll" title="Copy the whole template to the clipboard" aria-label="Copy the whole template">${Ae()}</button>
455
565
  </div>
456
566
  <div class="set-tpl-required" aria-label="Required variables">
457
567
  <span class="set-req-lbl">Required</span>
@@ -467,7 +577,7 @@
467
577
  <div class="set-tpl-msgs" id="tplMsgs" hidden></div>
468
578
  </div>
469
579
  </div>
470
- <div class="set-note">${D("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>
580
+ <div class="set-note">${M("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>
471
581
  </div>
472
582
 
473
583
  <div class="set-card set-tpl-varcard">
@@ -476,246 +586,174 @@
476
586
  <h3 class="set-tpl-vartitle">Variables</h3>
477
587
  <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>
478
588
  </div>
479
- <div class="set-tpl-vars-body">${ir()}</div>
589
+ <div class="set-tpl-vars-body">${Ll()}</div>
480
590
  </div>
481
- </div>`);let u=d("#tplEditor"),r=ts(d("#tplPreview"),()=>u.value,()=>c),b=d("#tplMsgs"),R=d("#tplTest"),E=d("#tplTestLbl"),y=()=>u.value!==f,K=d("#tplHl"),V=d("code",K),L=d("#tplGutter"),O=()=>{let B=u.value;g(V,o`${Ho(B)}\n`);let U="",re=B.split(`
482
- `).length;for(let ge=1;ge<=re;ge++)U+=`${ge}
483
- `;L.textContent=U},j=()=>{K.scrollTop=u.scrollTop,K.scrollLeft=u.scrollLeft,L.scrollTop=u.scrollTop};u.addEventListener("scroll",j);let J=d("#reqBody"),ie=d("#reqUnsub"),H=()=>{J.className=`set-req-pill ${/\{\{\s*\.Post\.Body\s*\}\}/.test(u.value)?"ok":"bad"}`,ie.className=`set-req-pill ${/\{\{\s*\.Email\.UnsubscribeURL\s*\}\}/.test(u.value)?"ok":"bad"}`},N=bn.attach({onSave:q,onDiscard:v},t);function I(){O(),H(),j(),N.setDirty(y()),E.textContent=y()?"Save & send test":"Send test email",R.title=y()?"Saves your changes first, then sends \u2014 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 $(B){if(!B.length){b.hidden=!0,g(b,o``);return}b.hidden=!1,b.className="set-tpl-msgs warn",g(b,o`${B.map(U=>o`<div>${U}</div>`)}`)}function v(){u.value=f,$([]),r.repaint(),I()}async function w(){let B=u.value,U=await ut(re=>T("/api/settings",{method:"PUT",json:re?{emailTemplate:B,remake:re}:{emailTemplate:B}}),ao);return U?(f=U.settings.emailTemplate,u.value=f,z.appConfig={...s,settings:U.settings},r.repaint(),I(),{warnings:Array.isArray(U.warnings)?U.warnings:[],remade:U.remade||[]}):null}async function q(){try{let B=await w();if(!B)return;$(B.warnings),k(pt(B.warnings.length?"Template saved with warnings":"Template saved",B.remade))}catch(B){N.showError(yn(B))}}let _=B=>{let U=ni[Ja(B)?B:"signed"];u.value=U.html,$([]),r.repaint(),I()};u.addEventListener("input",()=>{r.repaint(),I()});let G=d("#tplExamplesBtn"),ve=d("#tplExamplesList"),W=()=>{ve.hidden=!0,G.setAttribute("aria-expanded","false")};G.onclick=B=>{B.stopPropagation();let U=ve.hidden;ve.hidden=!U,G.setAttribute("aria-expanded",String(U))};for(let B of M("[data-example]",ve))B.onclick=()=>{_(B.dataset.example??""),W()};ti||(ti=!0,document.addEventListener("click",B=>{let U=document.getElementById("tplExamples"),re=document.getElementById("tplExamplesList"),ge=B.target instanceof Node&&U?.contains(B.target);U&&re&&!re.hidden&&!ge&&(re.hidden=!0,document.getElementById("tplExamplesBtn")?.setAttribute("aria-expanded","false"))}));let le=d("#tplPreview");for(let B of M(".wtog-btn",n))B.onclick=()=>{for(let U of M(".wtog-btn",n))U.setAttribute("aria-pressed",String(U===B));le.style.maxWidth=`${B.dataset.w}px`,r.repaint()};let de=d("#tplEditorWrap"),ke=d("#tplLineNums"),Re=B=>{de.classList.toggle("show-lines",B),ke.setAttribute("aria-pressed",String(B)),j()},pe=!1;try{pe=localStorage.getItem("kestrel.tpl.lineNums")==="1"}catch{}Re(pe),ke.onclick=()=>{pe=!pe,Re(pe);try{localStorage.setItem("kestrel.tpl.lineNums",pe?"1":"0")}catch{}};let me=d("#tplCopyAll"),xe=d("#tplCopyLbl");me.onclick=async()=>{await Qe(u.value),me.classList.add("copied"),xe.textContent="Copied",setTimeout(()=>{me.classList.remove("copied"),xe.textContent="Copy"},1100)},R.onclick=()=>{let B=y(),U=$e(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>${B?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}<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">${B?"Save & send test":"Send test"}</button></div>`),re=d("#tplTestTo",U.el);if(re.focus(),m.length&&!re.value.trim()){re.value=m.join(`
484
- `);let Pe=d("#tplTestHint",U.el);Pe.textContent="Pre-filled from your default test recipients (Settings).",Pe.hidden=!1}let ge=d("#ttGo",U.el);d("#ttCancel",U.el).onclick=U.close,ge.onclick=()=>X(ge,y()?"Saving\u2026":"Sending\u2026",async()=>{let{valid:Pe,invalid:be}=zt(re.value),Ee=Yt(be);if(Ee){k(Ee);return}if(!Pe.length){k("Enter at least one email address");return}if(y())try{let Y=await w();if(!Y){U.close();return}$(Y.warnings)}catch(Y){N.showError(yn(Y)),U.close();return}try{let Y=await T("/api/settings/template/test",{method:"POST",json:{to:Pe}});U.close(),k(et(Y.sent===Y.total?`Test sent to ${Y.sent} address${Y.sent===1?"":"es"}`:`Sent ${Y.sent}/${Y.total} \u2014 some failed`))}catch(Y){k(yn(Y))}})},u.value=f,r.repaint(),I()}var qe=e=>e instanceof Error?e.message:String(e),si={fake:"Fake (dev, dead-end)",ses:"Amazon SES",resend:"Resend"},ar={generic:"a generic provider",resend:"Resend",ses:"Amazon SES"},wn=o` hidden`;async function os(e,t){g(e,o`<div class="settings"><div class="page-head"><h1>Settings</h1><p class="set-lede set-page-lede">Your publication's identity, the email each post is sent inside, how mail is sent, and the ways readers subscribe. Facts set when Kestrel was deployed are shown read-only.</p></div><div id="settingsBody" class="muted">Loading…</div></div>`);let n=d("#settingsBody",e),s;try{s=await T("/api/settings",{signal:t})}catch(h){oe(n,qe(h),()=>Q(os));return}let i=s.settings,a=s.deployment,l=i.publication,c=gt(a.fromAddress)||"",f=c||"Your publication",m=i.confirmationEmail,u=i.confirmationEmailDefault,r={name:l.name||"",tagline:l.tagline||"",address:l.address||"",logoUrl:l.logoUrl||"",recipients:[...i.testRecipients||[]],template:i.emailTemplate||"",confirmation:{subject:m.subject||"",body:m.body||"",buttonLabel:m.buttonLabel||"",reassurance:m.reassurance||""},notifyTo:i.notifications.to||""},b={name:r.name,tagline:r.tagline,address:r.address,recipients:[...r.recipients],confirmation:{...r.confirmation},notifyTo:r.notifyTo},R=h=>(String(h||f).trim()[0]||"K").toUpperCase(),E=h=>{let A=String(h||"").match(/<([^>]+)>/);return A?.[1]?A[1]:String(h||"")},y=a.appOrigin||location.origin,K=`${y}/subscribe`,V=`${ee(y)}/subscribe`,L=(h,A)=>{let se=ee(A||f);return h==="styled"?`<form action="${V}" method="post" style="max-width:420px;font:15px/1.4 system-ui,-apple-system,'Segoe UI',sans-serif">
485
- <div style="font-weight:600;margin-bottom:6px">Subscribe to ${se}</div>
486
- <div style="display:flex;gap:8px;flex-wrap:wrap">
487
- <input type="email" name="email" required placeholder="you@example.com" aria-label="Email address" style="flex:1 1 200px;padding:10px 12px;border:1px solid #d4d4d8;border-radius:8px;font:inherit">
488
- <button type="submit" style="padding:10px 18px;border:0;border-radius:8px;background:#18181b;color:#fff;font:inherit;font-weight:600;cursor:pointer">Subscribe</button>
489
- </div>
490
- <p style="margin:8px 0 0;font-size:13px;color:#71717a">Double opt-in \u2014 we'll email a confirmation link. Unsubscribe anytime.</p>
491
- </form>`:`<form action="${V}" method="post">
492
- <label>
493
- Subscribe to ${se}
494
- <input type="email" name="email" placeholder="you@example.com" required>
495
- </label>
496
- <button type="submit">Subscribe</button>
497
- </form>`},O=(h,A)=>o`<span class="set-chip ${h}">${D(h)}${A}</span>`,j=(h,A,se=null)=>o`<div class="set-sec-head"><h2 class="set-sec-title">${h}</h2>${A}${se}<span class="set-rule"></span></div>`,J=s.inUse,ie=(()=>{if(!(J.identityFields||[]).length)return"Your email template doesn\u2019t show any of these.";let h="Shown in every email.",A=J.sends.length;if(!A)return h;let se=`Saving a change updates ${A} scheduled email${A===1?"":"s"} too; you\u2019ll confirm first.`,_e=J.retry_after?` One sends at ${Z(J.retry_after)}, so saving waits until it has sent.`:"";return`${h} ${se}${_e}`})(),H=!!r.address,N=(J.identityFields||[]).includes("address"),I=o`
591
+ </div>`);let h=d("#tplEditor"),u=ms(d("#tplPreview"),()=>h.value,()=>r),y=d("#tplMsgs"),x=d("#tplTest"),k=d("#tplTestLbl"),B=()=>h.value!==c,A=d("#tplHl"),H=d("code",A),m=d("#tplGutter"),L=()=>{let q=h.value;g(H,i`${ti(q)}\n`);let F="",oe=q.split(`
592
+ `).length;for(let le=1;le<=oe;le++)F+=`${le}
593
+ `;m.textContent=F},R=()=>{A.scrollTop=h.scrollTop,A.scrollLeft=h.scrollLeft,m.scrollTop=h.scrollTop};h.addEventListener("scroll",R);let V=d("#reqBody"),ee=d("#reqUnsub"),C=()=>{V.className=`set-req-pill ${/\{\{\s*\.Post\.Body\s*\}\}/.test(h.value)?"ok":"bad"}`,ee.className=`set-req-pill ${/\{\{\s*\.Email\.UnsubscribeURL\s*\}\}/.test(h.value)?"ok":"bad"}`},U=xn.attach({onSave:j,onDiscard:v},t);function _(){L(),C(),R(),U.setDirty(B()),k.textContent=B()?"Save & send test":"Send test email",x.title=B()?"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 b(q){if(!q.length){y.hidden=!0,g(y,i``);return}y.hidden=!1,y.className="set-tpl-msgs warn",g(y,i`${q.map(F=>i`<div>${F}</div>`)}`)}function v(){h.value=c,b([]),u.repaint(),_()}async function w(){let q=h.value,F=await ze(oe=>T("/api/settings",{method:"PUT",json:oe?{emailTemplate:q,remake:oe}:{emailTemplate:q}}),Ao);return F?(c=F.settings.emailTemplate,h.value=c,Z.appConfig={...s,settings:F.settings},u.repaint(),_(),{warnings:Array.isArray(F.warnings)?F.warnings:[],remade:F.remade||[]}):null}async function j(){try{let q=await w();if(!q)return;b(q.warnings),$(ut(q.warnings.length?"Template saved with warnings":"Template saved",q.remade))}catch(q){U.showError(En(q))}}let I=q=>{let F=Ii[wl(q)?q:"signed"];h.value=F.html,b([]),u.repaint(),_()};h.addEventListener("input",()=>{u.repaint(),_()});let Y=d("#tplExamplesBtn"),re=d("#tplExamplesList"),G=()=>{re.hidden=!0,Y.setAttribute("aria-expanded","false")};Y.onclick=q=>{q.stopPropagation();let F=re.hidden;re.hidden=!F,Y.setAttribute("aria-expanded",String(F))};for(let q of P("[data-example]",re))q.onclick=()=>{I(q.dataset.example??""),G()};Ni||(Ni=!0,document.addEventListener("click",q=>{let F=document.getElementById("tplExamples"),oe=document.getElementById("tplExamplesList"),le=q.target instanceof Node&&F?.contains(q.target);F&&oe&&!oe.hidden&&!le&&(oe.hidden=!0,document.getElementById("tplExamplesBtn")?.setAttribute("aria-expanded","false"))}));let ae=d("#tplPreview");for(let q of P(".wtog-btn",n))q.onclick=()=>{for(let F of P(".wtog-btn",n))F.setAttribute("aria-pressed",String(F===q));ae.style.maxWidth=`${q.dataset.w}px`,u.repaint()};let me=d("#tplEditorWrap"),vt=d("#tplLineNums"),Ue=q=>{me.classList.toggle("show-lines",q),vt.setAttribute("aria-pressed",String(q)),R()},Te=!1;try{Te=localStorage.getItem("kestrel.tpl.lineNums")==="1"}catch{}Ue(Te),vt.onclick=()=>{Te=!Te,Ue(Te);try{localStorage.setItem("kestrel.tpl.lineNums",Te?"1":"0")}catch{}};let je=d("#tplCopyAll");je.onclick=()=>Qn(je,h.value),x.onclick=()=>{let q=B(),F=he(i`<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>${q?i`<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}<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">${q?"Save & send test":"Send test"}</button></div>`),oe=d("#tplTestTo",F.el);if(oe.focus(),p.length&&!oe.value.trim()){oe.value=p.join(`
594
+ `);let Le=d("#tplTestHint",F.el);Le.textContent="Pre-filled from your default test recipients (Settings).",Le.hidden=!1}let le=d("#ttGo",F.el);d("#ttCancel",F.el).onclick=F.close,le.onclick=()=>z(le,B()?"Saving\u2026":"Sending\u2026",async()=>{let{valid:Le,invalid:ye}=en(oe.value),ve=tn(ye);if(ve){$(ve);return}if(!Le.length){$("Enter at least one email address");return}if(B())try{let te=await w();if(!te){F.close();return}b(te.warnings)}catch(te){U.showError(En(te)),F.close();return}try{let te=await T("/api/settings/template/test",{method:"POST",json:{to:Le}});F.close(),$(Xe(te.sent===te.total?`Test sent to ${te.sent} address${te.sent===1?"":"es"}`:`Sent ${te.sent} of ${te.total}; the rest failed`))}catch(te){$(En(te))}})},h.value=c,u.repaint(),_()}function Di(e){return i`
595
+ <section class="set-sec">
596
+ ${pe("Email template",ue("editable","Editable"),un(e.data.inUse))}
597
+ <p class="set-lede">The template controls the look and feel of the emails you send. Edit it on the Template page.</p>
598
+ <div class="set-preview set-tpl-sample">
599
+ <div class="set-preview-bar">
600
+ <span class="set-preview-lbl">Sample email</span>
601
+ <span class="set-preview-dot">Current template</span>
602
+ </div>
603
+ <iframe class="set-email-frame" id="tplPreview" title="Sample email preview" scrolling="no"></iframe>
604
+ </div>
605
+ <div class="row" style="margin-top:12px"><button type="button" class="primary" id="tplEditLink">Edit template →</button></div>
606
+ </section>`}function qi(e,t){let n=t.data.settings.emailTemplate||"",s=ms(d("#tplPreview",e),()=>n,()=>({name:t.state.name||t.fromDisplay,tagline:t.state.tagline,logoUrl:t.state.logoUrl,address:t.state.address}));return d("#tplEditLink",e).onclick=()=>{location.hash="#/template"},{repaint:()=>s.repaint()}}function Oi(){return i`
607
+ <section class="set-sec">
608
+ ${pe("Default test recipients",ue("editable","Editable"))}
609
+ <div class="set-card">
610
+ <div class="set-recip">
611
+ <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/consent flow.</p>
612
+ <div class="set-chips" id="recipChips"></div>
613
+ <div class="set-recip-add">
614
+ <input type="email" id="recipInput" placeholder="you@example.com" autocomplete="off">
615
+ <button type="button" id="recipAdd">Add inbox</button>
616
+ </div>
617
+ </div>
618
+ </div>
619
+ </section>`}function Ui(e,t){let{state:n}=t,s=d("#recipChips",e),o=d("#recipInput",e),a=()=>{if(!n.recipients.length){g(s,i`<span class="set-recip-empty">No default recipients yet. Add one below.</span>`);return}g(s,i`${n.recipients.map((r,c)=>i`<span class="set-recip-chip">${r}<button type="button" data-rm="${c}" aria-label="Remove ${r}">${M("x")}</button></span>`)}`)};s.addEventListener("click",r=>{let c=r.target instanceof Element?r.target.closest("[data-rm]"):null;c&&(n.recipients.splice(Number(c.dataset.rm),1),a(),t.changed())});let l=()=>{let r=st(o.value);if(r){if(!ot(r)){$("That doesn\u2019t look like an email address.");return}if(n.recipients.includes(r)){$("That inbox is already in the list."),o.value="";return}n.recipients.push(r),o.value="",a(),t.changed()}};return d("#recipAdd",e).onclick=l,o.addEventListener("keydown",r=>{r.key==="Enter"&&(r.preventDefault(),l())}),a(),{reset:a}}function Hl(e,t){let n=Math.min(e,t);return{x:Math.floor((e-n)/2),y:Math.floor((t-n)/2),side:n}}var hs=class extends Error{constructor(){super("Couldn\u2019t read that image. Pick a PNG, JPEG, WebP, or GIF.")}};async function ji(e,t){let n=await createImageBitmap(e,{imageOrientation:"from-image"}).catch(()=>{throw new hs});try{let{x:s,y:o,side:a}=Hl(n.width,n.height),l=document.createElement("canvas");l.width=t,l.height=t;let r=l.getContext("2d");if(!r)throw new Error("This browser can't prepare the image.");return r.imageSmoothingQuality="high",r.drawImage(n,s,o,a,a,0,0,t,t),{png:await new Promise((p,h)=>l.toBlob(u=>u?p(u):h(new Error("This browser can't prepare the image.")),"image/png")),width:n.width,height:n.height}}finally{n.close()}}function Rl(e){if(!(e.identityFields||[]).length)return"Your email template doesn\u2019t show any of these.";let t="Shown in every email.",n=e.sends.length;if(!n)return t;let s=`Saving a change updates ${n} scheduled email${n===1?"":"s"} too; you\u2019ll confirm first.`,o=e.retry_after?` One sends at ${W(e.retry_after)}, so saving waits until it has sent.`:"";return`${t} ${s}${o}`}var vs=256;function Fi(e){let{state:t,fromDisplay:n,fromName:s}=e,o=!!t.address;return i`
498
620
  <section class="set-sec">
499
- ${j("Publication identity",O("editable","Editable"))}
621
+ ${pe("Publication identity",ue("editable","Editable"))}
500
622
  <div class="set-card">
501
623
  <div class="set-id-grid">
502
624
  <div class="set-logo-slot">
503
- <div class="set-logo-tile${r.logoUrl?" has-img":""}" id="logoTile" role="button" tabindex="0" aria-label="Upload logo">
504
- <span class="set-logo-ph" id="logoPh"${r.logoUrl?wn:null}>${D("upload")}Upload</span>
625
+ <div class="set-logo-tile${t.logoUrl?" has-img":""}" id="logoTile" role="button" tabindex="0" aria-label="Upload logo">
626
+ <span class="set-logo-ph" id="logoPh"${t.logoUrl?At:null}>${M("upload")}Upload</span>
505
627
  </div>
506
628
  <input type="file" id="logoInput" accept="image/png,image/jpeg,image/webp,image/gif" hidden>
507
629
  <div class="set-logo-actions">
508
- <button type="button" id="logoReplace">${r.logoUrl?"Replace":"Upload"}</button>
509
- <button type="button" class="danger-subtle" id="logoRemove"${r.logoUrl?null:wn}>Remove</button>
630
+ <button type="button" id="logoReplace">${t.logoUrl?"Replace":"Upload"}</button>
631
+ <button type="button" class="danger-subtle" id="logoRemove"${t.logoUrl?null:At}>Remove</button>
632
+ </div>
633
+ <div class="set-logo-notes">
634
+ <p class="field-hint">Shown as a square. Pick an image at least ${String(vs)}&nbsp;×&nbsp;${String(vs)}&nbsp;px; a wider or taller one is cropped to its center square.</p>
635
+ <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>
636
+ <div class="field-error" id="logoError" role="alert" hidden><span class="field-error-ico" aria-hidden="true">!</span><span></span></div>
510
637
  </div>
511
- <p class="field-hint">PNG, JPEG, WebP, or GIF, up to 512&nbsp;KB. Saves immediately.</p>
512
- <div class="field-error" id="logoError" role="alert" hidden><span class="field-error-ico" aria-hidden="true">!</span><span></span></div>
513
638
  </div>
514
639
  <div class="set-id-fields">
515
640
  <div class="set-field">
516
641
  <label for="setName">Name</label>
517
- <input id="setName" value="${r.name}" placeholder="${f}" maxlength="120" autocomplete="off">
518
- <p class="field-hint">Blank falls back to the email “From” name (“${f}”).</p>
642
+ <input id="setName" value="${t.name}" placeholder="${s}" maxlength="120" autocomplete="off">
643
+ <p class="field-hint">${n?i`Blank falls back to the email “From” name (“${n}”).`:"Blank leaves the publication unnamed, since the email \u201CFrom\u201D has no name."}</p>
519
644
  </div>
520
645
  <div class="set-field">
521
646
  <label for="setTagline">Tagline</label>
522
- <input id="setTagline" value="${r.tagline}" placeholder="A one-line description" maxlength="200" autocomplete="off">
647
+ <input id="setTagline" value="${t.tagline}" placeholder="A one-line description" maxlength="200" autocomplete="off">
523
648
  <p class="field-hint">A short line under the name on your public pages.</p>
524
649
  </div>
525
- <div class="set-field set-addr-add" id="addrAdd"${H?wn:null}>
526
- <button type="button" class="ghost" id="addrAddBtn" aria-describedby="addrAddHint">${D("plus")}Add a mailing address</button>
650
+ <div class="set-field set-addr-add" id="addrAdd"${o?At:null}>
651
+ <button type="button" class="ghost" id="addrAddBtn" aria-describedby="addrAddHint">${M("plus")}Add a mailing address</button>
527
652
  <p class="field-hint" id="addrAddHint">For promotional email, US law (CAN-SPAM) requires a postal address in the footer. A P.O. box works.</p>
528
653
  </div>
529
- <div class="set-field" id="addrField"${H?null:wn}>
654
+ <div class="set-field" id="addrField"${o?null:At}>
530
655
  <div class="set-field-head">
531
656
  <label for="setAddress">Mailing address</label>
532
657
  <button type="button" class="danger-subtle set-field-remove" id="addrRemove">Remove</button>
533
658
  </div>
534
- <input id="setAddress" value="${r.address}" placeholder="PO Box 123, City, ST 00000" maxlength="300" autocomplete="off">
659
+ <input id="setAddress" value="${t.address}" placeholder="PO Box 123, City, ST 00000" maxlength="300" autocomplete="off">
535
660
  <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.</p>
536
661
  </div>
537
662
  </div>
538
663
  </div>
539
- <div class="set-note">${D("info")}<span>${ie}</span></div>
540
- </div>
541
- </section>`,$=o`
542
- <section class="set-sec">
543
- ${j("Email template",O("editable","Editable"),rn(J))}
544
- <p class="set-lede">The template controls the look and feel of the emails you send. Edit it on the Template page.</p>
545
- <div class="set-preview set-tpl-sample">
546
- <div class="set-preview-bar">
547
- <span class="set-preview-lbl">Sample email</span>
548
- <span class="set-preview-dot">Current template</span>
549
- </div>
550
- <iframe class="set-email-frame" id="tplPreview" title="Sample email preview" scrolling="no"></iframe>
551
- </div>
552
- <div class="row" style="margin-top:12px"><button type="button" class="primary" id="tplEditLink">Edit template →</button></div>
553
- </section>`,v=o`
554
- <section class="set-sec">
555
- ${j("Email sender",O("readonly","Read-only \xB7 set at deploy"))}
556
- <div class="set-card sunken">
557
- <div class="set-ro-note">${D("readonly")}<span>The sender is fixed at deploy via environment secrets, so it can’t be edited here. Change it in the <a class="set-link" href="#/docs">setup guide</a>, then redeploy. Credentials are never shown.</span></div>
558
- <div class="set-preview-bar" style="background:transparent">
559
- <span class="set-preview-lbl">Inbox preview</span>
560
- <span class="set-preview-dot">How readers see the sender</span>
561
- </div>
562
- <div class="set-inbox">
563
- <div class="set-inbox-avatar">${R(f)}</div>
564
- <div class="set-inbox-body">
565
- <div class="set-inbox-top"><span class="set-inbox-from">${f}</span><span class="set-inbox-time">9:02 AM</span></div>
566
- <div class="set-inbox-subj">Your latest post — a sample subject line</div>
567
- <div class="set-inbox-snip">The opening lines of your post show here as the inbox preview…</div>
568
- <div class="set-inbox-addr">${E(a.fromAddress)}</div>
569
- </div>
570
- </div>
571
- <div class="set-kv" style="border-top:1px solid var(--line)">
572
- <div class="set-kv-k">From address</div><div class="set-kv-v"><span class="mono">${a.fromAddress}</span></div>
573
- <div class="set-kv-k">Sending domain</div><div class="set-kv-v"><span class="mono">${a.sendingDomain}</span></div>
574
- <div class="set-kv-k">Email provider</div><div class="set-kv-v">${si[a.provider]||a.provider}${a.simulation?o`<span class="set-pill">Simulating ${ar[a.simulation.profile]}${a.simulation.faults==="none"?", no failures":""}</span>`:null}</div>
575
- </div>
664
+ <div class="set-note">${M("info")}<span>${Rl(e.data.inUse)}</span></div>
576
665
  </div>
577
- </section>`,w=o`
666
+ </section>`}function Vi(e,t){let{state:n}=t,s=d("#setName",e),o=d("#setTagline",e),a=d("#setAddress",e),l=()=>{n.name=s.value.trim(),n.tagline=o.value.trim(),n.address=a.value.trim(),t.changed()};s.addEventListener("input",l),o.addEventListener("input",l),a.addEventListener("input",l);let r=!!n.address,c=(t.data.inUse.identityFields||[]).includes("address"),p=d("#addrAdd",e),h=d("#addrField",e),u=d("#addrWarn",e),y=()=>{p.hidden=r,h.hidden=!r,u.hidden=!(r&&n.address&&!c),u.hidden?a.removeAttribute("aria-describedby"):a.setAttribute("aria-describedby","addrWarn")};a.addEventListener("input",y),d("#addrAddBtn",e).onclick=()=>{r=!0,y(),a.focus()},d("#addrRemove",e).onclick=()=>{a.value="",r=!1,l(),y(),d("#addrAddBtn",e).focus()};let x=d("#logoInput",e),k=d("#logoTile",e),B=d("#logoPh",e),A=d("#logoReplace",e),H=d("#logoRemove",e),m=d("#logoShape",e),L=b=>{if(m.hidden=!0,!b)return;let v=new Image;v.onload=()=>{m.hidden=n.logoUrl!==b||v.naturalWidth===v.naturalHeight},v.src=b},R=()=>{let b=!!n.logoUrl;L(n.logoUrl),k.classList.toggle("has-img",b),k.style.backgroundImage=b?`url('${n.logoUrl}')`:"",B.hidden=b,H.hidden=!b,A.textContent=b?"Replace":"Upload"};A.onclick=()=>x.click(),k.addEventListener("click",()=>x.click()),k.addEventListener("keydown",b=>{(b.key==="Enter"||b.key===" ")&&(b.preventDefault(),x.click())});let V=d("#logoError",e),ee=d("span:last-of-type",V),C=b=>{ee.textContent=b,V.hidden=!b},U=(b,v)=>{n.logoUrl=b.settings.publication.logoUrl||"",t.applySaved(b.settings),R(),t.changed(),$(ut(v,b.remade))},_=b=>{cn(b)?C(Ee(b)):$(Ee(b))};return x.onchange=async()=>{let b=x.files?.[0];if(x.value="",!!b){C("");try{let v=await ji(b,vs),w=v.width!==v.height,j=await ze(I=>{let Y=new FormData;Y.append("file",new File([v.png],"logo.png",{type:"image/png"}));let re=I?`?remake=${encodeURIComponent(I.join(","))}`:"";return T(`/api/settings/logo${re}`,{method:"POST",body:Y})},Mt("Upload","Uploading"));j&&U(j,w?"Logo updated, cropped to its center square":"Logo updated")}catch(v){_(v)}}},H.onclick=()=>z(H,"Removing\u2026",async()=>{C("");try{let b=await ze(v=>{let w=v?`?remake=${encodeURIComponent(v.join(","))}`:"";return T(`/api/settings/logo${w}`,{method:"DELETE"})},Mt("Remove","Removing"));b&&U(b,"Logo removed")}catch(b){_(b)}}),R(),y(),{reset(){s.value=n.name,o.value=n.tagline,a.value=n.address,r=!!n.address,y()}}}function Cl(e,t){switch(e){case"cloudflare":return"Cloudflare Email, separate from your newsletter's provider";case"provider":return`Your email provider (${t})`;default:return"No email provider configured, so nothing is delivered"}}var Ml={finished:"went out",refused:"provider refusing",stuck:"in flight too long",wedged:"waiting to be resolved",missed:"missed fire time",test:"test"};function Wi(e){return e.kind==="test"?"test notification":`${e.subject} (${Ml[e.kind]})`}function gs(e){let{lastSent:t,lastFailure:n}=e;return n?i`<span class="set-pill danger">Not delivered</span> <span class="muted">${W(n.at)}, ${Wi(n)}: ${n.error}</span>`:t?i`<span class="set-pill ok">${M("check")}Delivered</span> <span class="muted">${W(t.at)}, ${Wi(t)}</span>`:i`<span class="muted">None yet</span>`}function Ki(e){let t=e.data.deployment;return i`
578
667
  <section class="set-sec">
579
- ${j("Notifications",O("editable","Editable"))}
668
+ ${pe("Notifications",ue("editable","Editable"))}
580
669
  <p class="set-lede">Kestrel emails you when a send goes out, and right away if a send runs into a problem.</p>
581
670
  <div class="set-card">
582
671
  <div class="set-recip">
583
672
  <div class="set-field" style="margin:0">
584
673
  <label for="notifyTo">Send notifications to</label>
585
- <input type="email" id="notifyTo" value="${r.notifyTo}" placeholder="you@example.com" autocomplete="off">
674
+ <input type="email" id="notifyTo" value="${e.state.notifyTo}" placeholder="you@example.com" autocomplete="off">
586
675
  <p class="field-hint">One address. Leave blank for no notifications.</p>
587
676
  </div>
588
677
  <div class="row"><button type="button" class="ghost" id="notifyTest">Send a test notification</button><span class="field-hint" id="notifyTestHint"></span></div>
589
678
  </div>
590
679
  <div class="set-kv" style="border-top:1px solid var(--line)">
591
- <div class="set-kv-k">Sent through</div><div class="set-kv-v">${rr(a.notifyChannel,si[a.provider]||a.provider)}</div>
592
- <div class="set-kv-k">From address</div><div class="set-kv-v"><span class="mono">${a.notifyFrom}</span></div>
593
- <div class="set-kv-k">Last notification</div><div class="set-kv-v" id="notifyStatus">${ss(s.notificationStatus)}</div>
680
+ <div class="set-kv-k">Sent through</div><div class="set-kv-v">${Cl(t.notifyChannel,ps(t.provider))}</div>
681
+ <div class="set-kv-k">From address</div><div class="set-kv-v"><span class="mono">${t.notifyFrom}</span></div>
682
+ <div class="set-kv-k">Last notification</div><div class="set-kv-v" id="notifyStatus">${gs(e.data.notificationStatus)}</div>
594
683
  </div>
595
- <div class="set-note">${D("readonly")}<span>The channel and its sender are set at deploy. ${a.notifyChannel==="provider"?"Notifications go through your newsletter's own provider, so one about the provider refusing your account cannot reach you; Cloudflare's email avoids that. ":""}See the <a class="set-link" href="#/docs/notifications">setup guide</a>.</span></div>
684
+ <div class="set-note">${M("readonly")}<span>The channel and its sender are set at deploy. ${t.notifyChannel==="provider"?"Notifications go through your newsletter's own provider, so one about the provider refusing your account cannot reach you; Cloudflare's email avoids that. ":""}See the <a class="set-link" href="#/docs/notifications">setup guide</a>.</span></div>
596
685
  </div>
597
- </section>`,q=o`
598
- <section class="set-sec">
599
- ${j("Default test recipients",O("editable","Editable"))}
600
- <div class="set-card">
601
- <div class="set-recip">
602
- <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/consent flow.</p>
603
- <div class="set-chips" id="recipChips"></div>
604
- <div class="set-recip-add">
605
- <input type="email" id="recipInput" placeholder="you@example.com" autocomplete="off">
606
- <button type="button" id="recipAdd">Add inbox</button>
607
- </div>
608
- </div>
609
- </div>
610
- </section>`,_=o`
611
- <section class="set-sec">
612
- ${j("Ways to subscribe",O("copyout","Copy-out"))}
613
- <div class="set-card set-card-pad">
614
- <div class="set-field">
615
- <label>Public subscribe page</label>
616
- <div class="pub-row">
617
- <code class="pub-val">${K}</code>
618
- <button class="ghost" data-copy="${K}">Copy</button>
619
- <a class="ghost-link" href="${K}" target="_blank" rel="noopener">Open&nbsp;↗</a>
620
- </div>
621
- <p class="field-hint">The double opt-in page Kestrel hosts. Share it directly, or embed the form below.</p>
622
- </div>
623
- <div class="set-embed-head" style="margin-top:18px">
624
- <label style="margin:0">Embeddable subscribe form</label>
625
- <div class="seg" role="group" aria-label="Snippet style">
626
- <button type="button" class="seg-btn active" data-embed="plain">Plain HTML</button>
627
- <button type="button" class="seg-btn" data-embed="styled">Styled</button>
628
- </div>
629
- </div>
630
- <p class="field-hint" id="embedHint" style="margin:0 0 10px"></p>
631
- <div class="set-embed-preview">
632
- <div class="set-embed-cap">Rendered preview</div>
633
- <div id="embedPreview"></div>
634
- </div>
635
- <div class="set-embed" style="margin-top:14px"><pre><code id="embedCode"></code></pre></div>
636
- <div class="row" style="justify-content:flex-end;margin-top:10px"><button class="ghost" id="embedCopy">Copy code</button></div>
637
- </div>
638
- </section>`,G=o`
639
- <section class="set-sec">
640
- ${j("Confirmation email",O("editable","Editable"))}
641
- <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>
642
- <div class="set-preview">
643
- <div class="set-preview-bar set-ce-bar">
644
- <div class="set-ce-modetog" role="group" aria-label="Confirmation email view">
645
- <button type="button" class="set-ce-modebtn" id="ceTabPreview" aria-pressed="true">${D("preview")}Preview</button>
646
- <button type="button" class="set-ce-modebtn" id="ceTabEdit" aria-pressed="false">${D("editable")}Edit</button>
647
- </div>
648
- </div>
649
- <div id="cePreviewBody">
650
- <div class="set-inbox">
651
- <div class="set-inbox-avatar" id="cePvAvatar">${R(r.name)}</div>
652
- <div class="set-inbox-body">
653
- <div class="set-inbox-top"><span class="set-inbox-from" id="cePvFrom">${r.name||f}</span><span class="set-inbox-time">now</span></div>
654
- <div class="set-inbox-subj" id="cePvSubject"></div>
686
+ </section>`}function Zi(e,t){let n=d("#notifyTo",e),s=d("#notifyTest",e),o=d("#notifyTestHint",e),a=d("#notifyStatus",e);n.addEventListener("input",()=>{t.state.notifyTo=n.value.trim().toLowerCase(),t.changed()});let l=()=>{let r=t.saved().notifyTo,c=r?t.state.notifyTo!==r?"Save first: the test goes to the saved address.":"":"Save an address to send a test.";s.disabled=!!c,o.textContent=c};return s.onclick=()=>z(s,"Sending\u2026",async()=>{try{let r=await T("/api/settings/notifications/test",{method:"POST"});$(r.channel==="fake"?`Test notification recorded for ${r.to} (no email provider configured, so nothing is delivered)`:`Test notification sent to ${r.to}`),g(a,gs({lastSent:{kind:"test",subject:"",at:Date.now()},lastFailure:null}))}catch(r){$(Ee(r)),g(a,gs({lastSent:null,lastFailure:{kind:"test",subject:"",at:Date.now(),error:Ee(r)}}))}}),l(),{repaint:l,reset(){n.value=t.state.notifyTo,l()}}}var bs={base:"app",path:"/subscribe"},Tn={base:"app",path:"/confirm?token=\u2026"},ys={base:"app",path:"/unsubscribe?token=\u2026"},Bt=[{id:"subscribe",label:"Subscribe",flow:"steps",states:[{id:"form",label:"Form",at:bs},{id:"check-inbox",label:"Check your inbox",at:bs},{id:"not-an-email",label:"Not an email",offPath:!0,at:bs}]},{id:"confirm",label:"Confirm",flow:"steps",states:[{id:"ready",label:"Ready",at:Tn},{id:"confirmed",label:"Confirmed",at:Tn},{id:"expired",label:"Expired",offPath:!0,at:Tn},{id:"invalid",label:"Invalid link",offPath:!0,at:Tn}]},{id:"unsubscribe",label:"Unsubscribe",flow:"steps",states:[{id:"ask",label:"Ask",at:ys},{id:"done",label:"Unsubscribed",at:ys},{id:"invalid",label:"Invalid link",offPath:!0,at:ys}]},{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 Yi(e,t){return`/api/reader-pages/${encodeURIComponent(e)}/${encodeURIComponent(t)}`}var Pl={subscribe:i`Your name, tagline, and logo head each page; the words are Kestrel’s. The embeddable form in Ways to subscribe posts to these pages too.`,confirm:i`Your identity heads each page. You word the email that links here, under Confirmation email; these pages’ words are fixed, so what a reader agrees to can’t be edited away.`,unsubscribe:i`Your identity heads each page; the words are fixed, so leaving always reads the same. A mail client’s own Unsubscribe button leaves without a page.`,reading:i`Your identity heads each page, and a post page is your <a class="set-link" href="#/template">email template</a> around the post, as its web version shows it. Where a page lists posts, they are samples.`},Al={subscribe:"mail",confirm:"circle-check",unsubscribe:"circle-minus",reading:"book"},_l=i`<span class="set-rp-dot"></span>`;function Gi(e,t,n){return i`<li><button type="button" class="set-rp-item${t.offPath?" off":""}" data-journey="${e.id}" data-state="${t.id}" aria-current="false"><span class="set-rp-mark" aria-hidden="true">${n}</span>${t.label}</button></li>`}function Bl(){return i`${Bt.map(e=>{let t=e.states,n=e.flow==="steps";return i`<div class="set-rp-journey" data-journey="${e.id}">
687
+ <h3 class="set-rp-jhead" id="rpJourney-${e.id}">${M(Al[e.id])}${e.label}</h3>
688
+ <ul class="set-rp-list${n?" steps":""}" aria-labelledby="rpJourney-${e.id}">
689
+ ${t.filter(s=>!s.offPath).map((s,o)=>Gi(e,s,n?String(o+1):_l))}
690
+ </ul>
691
+ <ul class="set-rp-list" aria-label="${e.label}, off the main path">
692
+ ${t.filter(s=>s.offPath).map(s=>Gi(e,s,M("branch")))}
693
+ </ul>
694
+ </div>`})}`}function Nl(){return i`<select id="rpSelect" aria-label="Reader page">${Bt.map(e=>{let t=e.states,n=0;return i`<optgroup label="${e.label}">${t.map(s=>{let o=e.flow==="steps"&&!s.offPath?`${++n}. `:"";return i`<option value="${e.id}/${s.id}">${o}${s.label}</option>`})}</optgroup>`})}</select>`}function zi(e){return i`
695
+ <section class="set-sec set-rp-sec" id="readerPages">
696
+ ${e}
697
+ <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>
698
+ <div class="set-card set-rp">
699
+ <nav class="set-rp-nav" aria-label="Reader pages">${Bl()}</nav>
700
+ <div class="set-rp-pick">${Nl()}</div>
701
+ <div class="set-rp-main">
702
+ <div class="set-rp-top">
703
+ <div>
704
+ <p class="set-rp-crumb"><span class="set-rp-crumb-j" id="rpJourney"></span><span class="set-rp-crumb-sep">/</span><span id="rpState"></span></p>
705
+ <p class="set-rp-note" id="rpNote"></p>
655
706
  </div>
707
+ <span class="set-wtog" id="rpWidth" role="group" aria-label="Preview width">
708
+ <button type="button" class="wtog-btn" data-width="desktop" aria-pressed="true">${M("desktop")}Desktop</button>
709
+ <button type="button" class="wtog-btn" data-width="phone" aria-pressed="false">${M("phone")}Phone</button>
710
+ </span>
656
711
  </div>
657
- <div class="set-ce-stage">
658
- <div class="set-ce-card">
659
- <div class="set-ce-mast" id="cePvMast" hidden>
660
- <div class="set-ce-mast-logo" id="cePvMastLogo"></div>
661
- <div>
662
- <div class="set-ce-mast-name" id="cePvMastName"></div>
663
- <div class="set-ce-mast-tag" id="cePvMastTag"></div>
664
- </div>
665
- </div>
666
- <p class="set-ce-msg" id="cePvBody"></p>
667
- <p class="set-ce-btnrow"><a class="set-ce-btn" id="cePvButton" href="#" onclick="return false"></a></p>
668
- <p class="set-ce-foot" id="cePvFoot"></p>
669
- </div>
670
- </div>
671
- </div>
672
- <div id="ceEditBody" hidden>
673
- <div class="set-ce-edit">
674
- <div class="set-field">
675
- <label for="ceSubject">Subject line</label>
676
- <input id="ceSubject" value="${r.confirmation.subject}" maxlength="200" autocomplete="off">
677
- </div>
678
- <div class="set-field">
679
- <label for="ceMessage">Message</label>
680
- <textarea id="ceMessage" rows="3" maxlength="1000">${r.confirmation.body}</textarea>
681
- <p class="field-hint">The line above the confirm button. Keep it short — this is a one-click step, not a letter.</p>
682
- </div>
683
- <div class="set-field">
684
- <label for="ceButton">Button label</label>
685
- <input id="ceButton" value="${r.confirmation.buttonLabel}" maxlength="80" autocomplete="off">
686
- <p class="field-hint set-ce-lock">${D("readonly")}<span>Kestrel fills in the confirmation link — you set the words, never the URL.</span></p>
687
- </div>
688
- <div class="set-field">
689
- <label for="ceFooter">Reassurance line</label>
690
- <input id="ceFooter" value="${r.confirmation.reassurance}" maxlength="400" autocomplete="off">
691
- <p class="field-hint">The quiet footer for anyone who didn’t sign up. Leave blank to omit it.</p>
692
- </div>
693
- </div>
694
- <div class="set-ce-editfoot">
695
- <button type="button" class="ghost" id="ceReset">Reset to Kestrel default</button>
696
- <span class="grow"></span>
697
- <button type="button" class="ghost" id="ceToPreview">See preview →</button>
712
+ <div class="set-rp-at"><span class="mono" id="rpAt"></span><span>Sample · links and forms do nothing</span></div>
713
+ <p class="set-rp-error" id="rpError" role="alert" hidden></p>
714
+ <div class="set-rp-stage" id="rpStage">
715
+ <iframe class="set-rp-frame" id="rpFrame" sandbox="allow-same-origin" title="Reader page preview" scrolling="no"></iframe>
698
716
  </div>
699
717
  </div>
700
718
  </div>
701
- </section>`,ve=`${a.archiveOrigin||""}${a.archiveBasePath||""}`,W=a.archiveOrigin===a.appOrigin,le=o`
702
- <section class="set-sec">
703
- ${j("Instance",O("readonly","Read-only \xB7 set at deploy"))}
704
- <div class="set-card sunken">
705
- <div class="set-ro-note">${D("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>
706
- <div class="set-kv">
707
- <div class="set-kv-k">App origin</div><div class="set-kv-v"><span class="mono">${a.appOrigin}</span></div>
708
- <div class="set-kv-k">Archive URL base</div><div class="set-kv-v"><span class="mono">${ve}</span>${W?o`<span class="set-pill">default: app origin</span>`:null}</div>
709
- <div class="set-kv-k">Image URL base</div><div class="set-kv-v"><span class="mono">${a.mediaPublicBase}</span></div>
710
- <div class="set-kv-k">Auth mode</div><div class="set-kv-v">${a.authMode==="access"?"Cloudflare Access":"Local dev token"}</div>
711
- <div class="set-kv-k">Cloudflare Access</div><div class="set-kv-v">${a.accessConfigured?o`<span class="set-pill ok">${D("check")}Configured</span>`:o`<span class="set-pill">Not configured</span>`}</div>
712
- <div class="set-kv-k">Minimum lead</div><div class="set-kv-v">${Ft(a.minLeadMs)}${a.minLeadMs===jt?o`<span class="set-pill">default</span>`:null}</div>
713
- </div>
719
+ </section>`}function Il(e,t){return`${e.base==="app"?t.appOrigin:`${t.archiveOrigin}${t.archiveBasePath}`}${e.path}`.replace(/^https?:\/\//,"")}function Ji(e,t,n){let s=d(".set-rp-nav",e),o=d("#rpSelect",e),a=d("#rpWidth",e),l=d("#rpStage",e),r=d("#rpFrame",e),c=d("#rpError",e),p=Bt[0],h=p.states[0],u="desktop",y=0,x=null;Ke(n,()=>x?.disconnect());let k=()=>{for(let m of P(".set-rp-item",s)){let L=m.dataset.journey===p.id&&m.dataset.state===h.id;m.setAttribute("aria-current",L?"true":"false")}for(let m of P(".set-rp-journey",s))m.classList.toggle("current",m.dataset.journey===p.id);o.value=`${p.id}/${h.id}`,d("#rpJourney",e).textContent=p.label,d("#rpState",e).textContent=h.label,g(d("#rpNote",e),Pl[p.id]),d("#rpAt",e).textContent=Il(h.at,t)},B=()=>{let m=r.contentDocument?.body?.scrollHeight;m&&(r.style.height=`${m}px`)},A=async()=>{let m=++y;try{let L=await Qe(Yi(p.id,h.id),{signal:n});if(m!==y||n.aborted)return;c.hidden=!0,l.hidden=!1,r.srcdoc=bn(L)}catch(L){if(m!==y||n.aborted)return;r.srcdoc="",l.hidden=!0,c.textContent=`Couldn\u2019t load this page: ${L instanceof Error?L.message:String(L)}`,c.hidden=!1}},H=(m,L)=>{let R=Bt.find(C=>C.id===m),ee=(R?.states??[]).find(C=>C.id===L);!R||!ee||R===p&&ee===h||(p=R,h=ee,k(),A())};return r.onload=()=>{let m=r.contentDocument;!m||n.aborted||(m.addEventListener("click",L=>{L.target?.closest?.("a[href], area[href]")&&L.preventDefault()}),x?.disconnect(),B(),m.body&&typeof ResizeObserver<"u"&&(x=new ResizeObserver(B),x.observe(m.body)))},s.addEventListener("click",m=>{let L=m.target.closest(".set-rp-item");H(L?.dataset.journey,L?.dataset.state)}),o.addEventListener("change",()=>{let[m,L]=o.value.split("/");H(m,L)}),a.addEventListener("click",m=>{let R=m.target.closest("button[data-width]")?.dataset.width;if(!(R!=="desktop"&&R!=="phone"||R===u)){u=R;for(let V of P("button",a))V.setAttribute("aria-pressed",String(V.dataset.width===R));l.classList.toggle("phone",R==="phone")}}),k(),A(),{refresh:()=>{A()}}}function Xi(e){return e?`Subscribe to ${e}`:"Subscribe"}function Dl(e,t,n){let s=Q(Xi(t)),o=`${Q(n)}/subscribe`;return e==="styled"?`<form action="${o}" method="post" style="max-width:460px;font:16px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">
720
+ <div style="font:600 22px/1.2 Georgia,'Times New Roman',serif;letter-spacing:-0.01em;margin-bottom:12px">${s}</div>
721
+ <div style="display:flex;gap:10px;flex-wrap:wrap">
722
+ <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">
723
+ <button type="submit" style="padding:11px 20px;border:1px solid #1b1a17;border-radius:9px;background:#1b1a17;color:#fbfbfa;font:inherit;font-size:15px;font-weight:600;cursor:pointer;white-space:nowrap">Subscribe</button>
724
+ </div>
725
+ <p style="margin:12px 0 0;font-size:12.5px;color:#6b6a63">We'll email you a link to confirm. Unsubscribe anytime.</p>
726
+ </form>`:`<form action="${o}" method="post">
727
+ <label>
728
+ ${s}
729
+ <input type="email" name="email" placeholder="you@example.com" required>
730
+ </label>
731
+ <button type="submit">Subscribe</button>
732
+ </form>`}function ql(e,t){let n=Xi(t);return e==="styled"?i`<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>`:i`<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 ea(e){return e.data.deployment.appOrigin||location.origin}function Ol(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 Qi(e,t,n,s,o=null){return i`<div class="set-way"><div class="set-way-head"><div class="set-way-name"><h3 class="set-way-title">${M(e)}${t}</h3><p class="set-way-line">${n}</p></div>${o}</div>${s}</div>`}function ta(e){let t=`${ea(e)}/subscribe`;return i`
733
+ <section class="set-sec set-sec-subscribe">
734
+ ${pe("Ways to subscribe",ue("copyout","Copy-out"))}
735
+ <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>
736
+ <div class="set-card">
737
+ ${Qi("link","Share the page",i`<span id="subscribePageLine"></span>`,i`<div class="set-way-body set-way-addr"><code class="set-addr">${t}</code>${pt(t,"Copy the subscribe page's address")}<a class="ghost-link" href="${t}" target="_blank" rel="noopener">Open&nbsp;↗</a></div>`)}
738
+ ${Qi("code","Embed a form",i`Put a sign-up form on another site you own.`,i`<div class="set-embed">
739
+ <div class="set-embed-preview" id="embedPreview"></div>
740
+ <pre class="set-embed-code"><code id="embedCode"></code></pre>
741
+ </div>
742
+ <div class="set-embed-foot" id="embedCopy"></div>`,i`<span class="set-wtog" role="group" aria-label="Snippet style">
743
+ <button type="button" class="wtog-btn" data-embed="styled" aria-pressed="true">Styled</button>
744
+ <button type="button" class="wtog-btn" data-embed="plain" aria-pressed="false">Plain</button>
745
+ </span>`)}
714
746
  </div>
715
- </section>`;g(n,o`${I}${$}${v}${w}${q}${_}${G}${le}`);let de=h=>{z.appConfig={...s,settings:h},Ot()},ke=d("#setName"),Re=d("#setTagline"),pe=bn.attach({onSave:di,onDiscard:ci},t),me=(h,A)=>h.length===A.length&&h.every((se,_e)=>se===A[_e]),xe=(h,A)=>h.subject===A.subject&&h.body===A.body&&h.buttonLabel===A.buttonLabel&&h.reassurance===A.reassurance,B=()=>r.name!==b.name||r.tagline!==b.tagline||r.address!==b.address||!me(r.recipients,b.recipients)||!xe(r.confirmation,b.confirmation)||r.notifyTo!==b.notifyTo,U=d("#notifyTo"),re=d("#notifyTest"),ge=d("#notifyTestHint"),Pe=d("#notifyStatus"),be=()=>{pe.setDirty(B()),ue()},Ee=ts(d("#tplPreview"),()=>r.template,()=>({name:r.name||f,tagline:r.tagline,logoUrl:r.logoUrl,address:r.address}));d("#tplEditLink").onclick=()=>{location.hash="#/template"};let Y=d("#logoInput"),Xe=d("#logoTile"),Ct=d("#logoPh"),Ht=d("#logoReplace"),mt=d("#logoRemove"),Te=()=>{let h=!!r.logoUrl;Xe.classList.toggle("has-img",h),Xe.style.backgroundImage=h?`url('${r.logoUrl}')`:"",Ct.hidden=h,mt.hidden=!h,Ht.textContent=h?"Replace":"Upload",Ee.repaint(),Oe()};Ht.onclick=()=>Y.click(),Xe.addEventListener("click",()=>Y.click()),Xe.addEventListener("keydown",h=>{(h.key==="Enter"||h.key===" ")&&(h.preventDefault(),Y.click())});let Mt=d("#logoError"),Rt=d("span:last-of-type",Mt),Ze=h=>{Rt.textContent=h,Mt.hidden=!h};Y.onchange=async()=>{let h=Y.files?.[0];if(Y.value="",!!h){if(h.size>512*1024){k("That image is over 512 KB \u2014 pick a smaller one.");return}Ze("");try{let A=await ut(se=>{let _e=new FormData;_e.append("file",h);let kn=se?`?remake=${encodeURIComponent(se.join(","))}`:"";return T(`/api/settings/logo${kn}`,{method:"POST",body:_e})},an("Upload","Uploading"));if(!A)return;r.logoUrl=A.settings.publication.logoUrl||"",de(A.settings),Te(),k(pt("Logo updated",A.remade))}catch(A){on(A)?Ze(qe(A)):k(qe(A))}}},mt.onclick=()=>X(mt,"Removing\u2026",async()=>{Ze("");try{let h=await ut(A=>{let se=A?`?remake=${encodeURIComponent(A.join(","))}`:"";return T(`/api/settings/logo${se}`,{method:"DELETE"})},an("Remove","Removing"));if(!h)return;r.logoUrl=h.settings.publication.logoUrl||"",de(h.settings),Te(),k(pt("Logo removed",h.remade))}catch(h){on(h)?Ze(qe(h)):k(qe(h))}});let Pt=d("#embedCode"),ft=d("#embedHint"),Ae=d("#embedPreview"),Sn={styled:"Self-contained \u2014 inline styles, ready to paste anywhere.",plain:"Minimal markup, no styles \u2014 style it to match your site."},Ue="plain",At=(h,A)=>h==="styled"?o`<form class="set-pf-styled"><span class="l">Subscribe to ${A}</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">Double opt-in — we’ll email a confirmation link.</p></form>`:o`<form class="set-pf-plain"><label>Subscribe to ${A}</label><input type="email" placeholder="you@example.com" disabled><button type="button" tabindex="-1">Subscribe</button></form>`,p=()=>{let h=r.name||f;Pt.textContent=L(Ue,h),ft.textContent=Sn[Ue],g(Ae,At(Ue,h))},x=h=>{Ue=h==="styled"?"styled":"plain";for(let A of M("[data-embed]",n))A.classList.toggle("active",A.dataset.embed===Ue);p()};for(let h of M("[data-embed]",n))h.onclick=()=>x(h.dataset.embed??"");d("#embedCopy").onclick=()=>Qe(L(Ue,r.name||f));let S=d("#setAddress"),C=()=>{r.name=ke.value.trim(),r.tagline=Re.value.trim(),r.address=S.value.trim(),Ee.repaint(),p(),Oe(),be()};ke.addEventListener("input",C),Re.addEventListener("input",C),S.addEventListener("input",C);let P=d("#addrAdd"),fe=d("#addrField"),ye=d("#addrWarn"),te=()=>{P.hidden=H,fe.hidden=!H,ye.hidden=!(H&&r.address&&!N),ye.hidden?S.removeAttribute("aria-describedby"):S.setAttribute("aria-describedby","addrWarn")};S.addEventListener("input",te),d("#addrAddBtn").onclick=()=>{H=!0,te(),S.focus()},d("#addrRemove").onclick=()=>{S.value="",H=!1,C(),te(),d("#addrAddBtn").focus()};let Ce=d("#recipChips"),we=d("#recipInput"),ce=()=>{if(!r.recipients.length){g(Ce,o`<span class="set-recip-empty">No default recipients yet — add one below.</span>`);return}g(Ce,o`${r.recipients.map((h,A)=>o`<span class="set-recip-chip">${h}<button type="button" data-rm="${A}" aria-label="Remove ${h}">${D("x")}</button></span>`)}`)};Ce.addEventListener("click",h=>{let A=h.target instanceof Element?h.target.closest("[data-rm]"):null;A&&(r.recipients.splice(Number(A.dataset.rm),1),ce(),be())});let He=()=>{let h=st(we.value);if(h){if(!ot(h)){k("That doesn\u2019t look like an email address.");return}if(r.recipients.includes(h)){k("That inbox is already in the list."),we.value="";return}r.recipients.push(h),we.value="",ce(),be()}};d("#recipAdd").onclick=He,we.addEventListener("keydown",h=>{h.key==="Enter"&&(h.preventDefault(),He())}),U.addEventListener("input",()=>{r.notifyTo=U.value.trim().toLowerCase(),be()});function ue(){let h=b.notifyTo?r.notifyTo!==b.notifyTo?"Save first: the test goes to the saved address.":"":"Save an address to send a test.";re.disabled=!!h,ge.textContent=h}re.onclick=()=>X(re,"Sending\u2026",async()=>{try{let h=await T("/api/settings/notifications/test",{method:"POST"});k(h.channel==="fake"?`Test notification recorded for ${h.to} (no email provider configured \u2014 nothing is delivered)`:`Test notification sent to ${h.to}`),g(Pe,ss({lastSent:{kind:"test",subject:"",at:Date.now()},lastFailure:null}))}catch(h){k(qe(h)),g(Pe,ss({lastSent:null,lastFailure:{kind:"test",subject:"",at:Date.now(),error:qe(h)}}))}});for(let h of M("[data-copy]",n))h.onclick=()=>Qe(h.dataset.copy??"");let ne={subject:d("#ceSubject"),message:d("#ceMessage"),button:d("#ceButton"),footer:d("#ceFooter")},ae={avatar:d("#cePvAvatar"),from:d("#cePvFrom"),subject:d("#cePvSubject"),body:d("#cePvBody"),button:d("#cePvButton"),foot:d("#cePvFoot"),mast:d("#cePvMast"),mastLogo:d("#cePvMastLogo"),mastName:d("#cePvMastName"),mastTag:d("#cePvMastTag")},Oe=()=>{let h=r.confirmation,A=h.subject.trim()||u.subject||"",se=h.body.trim()||u.body||"",_e=h.buttonLabel.trim()||u.buttonLabel||"Confirm",kn=r.name||f;ae.avatar.textContent=R(r.name),ae.from.textContent=kn,ae.subject.textContent=A,ae.body.textContent=se,ae.button.textContent=_e,ae.foot.textContent=h.reassurance,ae.foot.hidden=!h.reassurance.trim();let xn=r.name||c,En=!!r.logoUrl,us=En||!!xn;ae.mast.hidden=!us,us&&(ae.mastLogo.hidden=!En,En&&g(ae.mastLogo,o`<img src="${r.logoUrl}" alt="">`),ae.mastName.textContent=xn,ae.mastName.hidden=!xn,ae.mastTag.textContent=r.tagline,ae.mastTag.hidden=!r.tagline)},rs=()=>{r.confirmation.subject=ne.subject.value,r.confirmation.body=ne.message.value,r.confirmation.buttonLabel=ne.button.value,r.confirmation.reassurance=ne.footer.value,Oe(),be()};for(let h of Object.values(ne))h.addEventListener("input",rs);let ri=d("#ceEditBody"),li=d("#cePreviewBody"),ls=d("#ceTabEdit"),ds=d("#ceTabPreview"),$n=h=>{ri.hidden=!h,li.hidden=h,ls.setAttribute("aria-pressed",String(h)),ds.setAttribute("aria-pressed",String(!h)),h&&ne.subject.focus()};ds.onclick=()=>$n(!1),ls.onclick=()=>$n(!0),d("#ceToPreview").onclick=()=>$n(!1),d("#ceReset").onclick=()=>{ne.subject.value=u.subject||"",ne.message.value=u.body||"",ne.button.value=u.buttonLabel||"",ne.footer.value=u.reassurance||"",rs()};let cs=()=>{ne.subject.value=r.confirmation.subject,ne.message.value=r.confirmation.body,ne.button.value=r.confirmation.buttonLabel,ne.footer.value=r.confirmation.reassurance,Oe()};async function di(){try{let h={publication:{name:r.name,tagline:r.tagline,address:r.address},testRecipients:r.recipients,confirmationEmail:r.confirmation,notifications:{to:r.notifyTo}},A=await ut(_e=>T("/api/settings",{method:"PUT",json:_e?{...h,remake:_e}:h}),an("Save","Saving"));if(!A)return;let se=A.settings;r.name=se.publication.name,r.tagline=se.publication.tagline,r.address=se.publication.address,r.recipients=[...se.testRecipients],r.confirmation={...se.confirmationEmail},r.notifyTo=se.notifications.to,U.value=r.notifyTo,ke.value=r.name,Re.value=r.tagline,S.value=r.address,H=!!r.address,te(),cs(),b={name:r.name,tagline:r.tagline,address:r.address,recipients:[...r.recipients],confirmation:{...r.confirmation},notifyTo:r.notifyTo},de(se),ce(),p(),Ee.repaint(),be(),k(pt("Settings saved",A.remade))}catch(h){on(h)?pe.showError(qe(h)):k(qe(h))}}function ci(){r.name=b.name,r.tagline=b.tagline,r.address=b.address,r.recipients=[...b.recipients],r.confirmation={...b.confirmation},r.notifyTo=b.notifyTo,U.value=r.notifyTo,ke.value=r.name,Re.value=r.tagline,S.value=r.address,H=!!r.address,te(),cs(),ce(),p(),Ee.repaint(),be()}Te(),te(),ce(),x("plain"),Ee.repaint(),Oe(),be()}function rr(e,t){switch(e){case"cloudflare":return"Cloudflare Email, separate from your newsletter's provider";case"provider":return`Your email provider (${t})`;default:return"No email provider configured \u2014 nothing is delivered"}}var lr={finished:"went out",refused:"provider refusing",stuck:"in flight too long",wedged:"waiting to be resolved",missed:"missed fire time",test:"test"};function oi(e){return e.kind==="test"?"test notification":`${e.subject} (${lr[e.kind]})`}function ss(e){let{lastSent:t,lastFailure:n}=e;return n?o`<span class="set-pill danger">Not delivered</span> <span class="muted">${Z(n.at)}, ${oi(n)}: ${n.error}</span>`:t?o`<span class="set-pill ok">${D("check")}Delivered</span> <span class="muted">${Z(t.at)}, ${oi(t)}</span>`:o`<span class="muted">None yet</span>`}var dr=[{value:"confirmed",label:"Confirmed"},{value:"pending",label:"Pending"},{value:"unsubscribed",label:"Unsubscribed"}];async function ii(e,t,n){let s={status:"",search:"",suppressed:"",sort:"joined",dir:"desc",offset:0,limit:50};e==="suppressed"?s.suppressed="only":(e==="confirmed"||e==="pending"||e==="unsubscribed")&&(s.status=e),g(t,o`
747
+ </section>`}function Ul(e,t){return i`<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">${Ae(!1,"Copy HTML")}</button>`}function na(e,t){let n=ea(t),s=d("#embedCode",e),o=d("#embedCopy",e),a=d("#embedPreview",e),l=d("#subscribePageLine",e),r="styled",c=()=>t.state.name||t.fromDisplay,p=()=>{let y=Dl(r,c(),n);s.textContent=y,g(o,Ul(r,y)),g(a,ql(r,c())),l.textContent=Ol(c())},h=P("[data-embed]",e);for(let y of h)y.onclick=()=>{r=y.dataset.embed==="styled"?"styled":"plain";for(let x of h)x.setAttribute("aria-pressed",String(x.dataset.embed===r));p()};mt(d(".set-sec-subscribe",e)),p();let u=c();return{repaint(){c()!==u&&(u=c(),p())}}}function Nt(e,t){return i`<div class="set-group" data-group="${e}">${t}</div>`}function ws(e){return{name:e.name,tagline:e.tagline,address:e.address,recipients:[...e.recipients],confirmation:{...e.confirmation},notifyTo:e.notifyTo}}var jl=(e,t)=>e.length===t.length&&e.every((n,s)=>n===t[s]),Fl=(e,t)=>e.subject===t.subject&&e.body===t.body&&e.buttonLabel===t.buttonLabel&&e.reassurance===t.reassurance;async function Ss(e,t){g(e,i`<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=d("#settingsBody",e),s;try{s=await T("/api/settings",{signal:t})}catch(m){X(n,Ee(m),()=>K(Ss));return}let o=s.settings,a=o.publication,l=o.confirmationEmail,r={name:a.name||"",tagline:a.tagline||"",address:a.address||"",logoUrl:a.logoUrl||"",recipients:[...o.testRecipients||[]],confirmation:{subject:l.subject||"",body:l.body||"",buttonLabel:l.buttonLabel||"",reassurance:l.reassurance||""},notifyTo:o.notifications.to||""},c=ws(r),p=qn(s.deployment.fromAddress),h=()=>r.name!==c.name||r.tagline!==c.tagline||r.address!==c.address||!jl(r.recipients,c.recipients)||!Fl(r.confirmation,c.confirmation)||r.notifyTo!==c.notifyTo,u=[],y=xn.attach({onSave:A,onDiscard:H},t),x=()=>{},k={data:s,state:r,saved:()=>c,fromDisplay:p,fromName:p||"Your publication",changed(){for(let m of u)m.repaint?.();y.setDirty(h())},applySaved(m){Z.appConfig={...s,settings:m},Wt(),x()}};g(n,i`
748
+ ${Nt("publication",[Fi(k)])}
749
+ ${Nt("readers",[ta(k),Ci(k),zi(pe("Reader pages",ue("preview","Preview")))])}
750
+ ${Nt("emails",[Di(k),Oi()])}
751
+ ${Nt("you",[Ki(k)])}
752
+ ${Nt("deploy",[Pi(k),Ai(k)])}
753
+ `),u.push(Vi(n,k),na(n,k),Mi(n,k),qi(n,k),Ui(n,k),Zi(n,k)),x=Ji(n,s.deployment,t).refresh;async function A(){try{let m={publication:{name:r.name,tagline:r.tagline,address:r.address},testRecipients:r.recipients,confirmationEmail:r.confirmation,notifications:{to:r.notifyTo}},L=await ze(V=>T("/api/settings",{method:"PUT",json:V?{...m,remake:V}:m}),Mt("Save","Saving"));if(!L)return;let R=L.settings;r.name=R.publication.name,r.tagline=R.publication.tagline,r.address=R.publication.address,r.recipients=[...R.testRecipients],r.confirmation={...R.confirmationEmail},r.notifyTo=R.notifications.to,c=ws(r);for(let V of u)V.reset?.();k.applySaved(R),k.changed(),$(ut("Settings saved",L.remade))}catch(m){cn(m)?y.showError(Ee(m)):$(Ee(m))}}function H(){Object.assign(r,ws(c));for(let m of u)m.reset?.();k.changed()}k.changed()}var Vl=[{value:"confirmed",label:"Confirmed"},{value:"pending",label:"Pending"},{value:"unsubscribed",label:"Unsubscribed"}];async function sa(e,t,n){let s={status:"",search:"",suppressed:"",sort:"joined",dir:"desc",offset:0,limit:50};e==="suppressed"?s.suppressed="only":(e==="confirmed"||e==="pending"||e==="unsubscribed")&&(s.status=e),g(t,i`
716
754
  <div class="spread page-head"><h1>Subscribers</h1><button class="primary" id="addSub">Add subscriber</button></div>
717
755
  <div id="subCounts" class="muted">Loading…</div>
718
- ${rt({statuses:dr,suppressible:!0,searchPlaceholder:"Search email\u2026"})}
756
+ ${rt({statuses:Vl,suppressible:!0,searchPlaceholder:"Search email\u2026"})}
719
757
  <div id="subList" class="muted">Loading…</div>
720
- <div id="subPager"></div>`);let i=d("#subList"),a=d("#subPager");async function l(){try{let c=await T(`/api/subscribers?${at(s)}`,{signal:n}),f=c.counts;g(d("#subCounts"),o`<div class="card row" style="gap:24px"><span><strong>${f.confirmed}</strong> confirmed</span><span>${f.pending} pending</span><span>${f.unsubscribed} unsubscribed</span><span>${f.suppressed} suppressed</span>${Jt("Pending: subscribed but hasn't clicked the confirmation email. Confirmed: consented \u2014 receives sends. Unsubscribed: opted out. Suppressed: bounced or complained \u2014 never mailed, whatever the consent state.",{below:!0})}</div>`),mr(i,c.subscribers,s,l),We(a,s,c.page,l)}catch(c){oe(i,c instanceof Error?c.message:String(c),l)}}d("#addSub").onclick=()=>dn(l),lt(t,s,l),l()}var cr={bounce:"bounced",complaint:"complaint",manual:"blocked"},ur={bounce:"Hard bounce \u2014 mail to this address failed, so it won't be mailed again.",complaint:"Marked as spam \u2014 won't be mailed again, whatever the consent state.",manual:"Manually blocked \u2014 won't be mailed."};function pr(e){if(!e.suppressed)return null;let t=e.suppression_reason??"",n=cr[t]||"suppressed",s=ur[t]||"Suppressed \u2014 won't be mailed, whatever the consent state.",i=e.suppression_detail?`${s} (${e.suppression_detail})`:s;return o` <span class="badge suppressed row-flag" title="${i}">${n}</span>`}function mr(e,t,n,s){if(!t.length){g(e,o`<p class="muted">No subscribers match.</p>`);return}g(e,o`<div class="table-wrap"><table class="list-table subs-table stacks has-actions"><colgroup><col><col class="c-status"><col class="c-date"><col class="c-act"></colgroup><thead><tr>${Se("Email","email",n)}${Se("Status",null,n)}${Se("Joined","joined",n)}<th></th></tr></thead><tbody>${t.map(i=>o`<tr data-id="${i.id}"><td>${i.email}${pr(i)}</td><td>${Je(i.status)}</td><td class="muted joined">${Z(i.created_at)}</td><td class="act">${i.status==="confirmed"?o`<button class="icon" data-menu="${i.id}" aria-label="Subscriber actions">⋯</button>`:null}</td></tr>`)}</tbody></table></div>`),Ke(e,n,s);for(let i of M("[data-menu]",e))i.onclick=a=>{a.stopPropagation();let l=t.find(c=>c.id===i.dataset.menu);l&&Xt(i,[{label:"Unsubscribe",danger:!0,onClick:()=>ro(l,s)}])}}var fr="You have unsaved changes. Leave without saving?",is="";function as(){let e=location.hash||"#/dashboard";is=e;let[,t,n,s]=e.split("/");document.body.classList.toggle("editor-mode",t==="edit");let i=t==="docs"||t==="reference";document.body.classList.toggle("tool-mode",i);for(let a of document.querySelectorAll(".sidebar a[data-view]"))a.dataset.view===t?a.setAttribute("aria-current","page"):a.removeAttribute("aria-current");return vt(!1),t==="edit"&&n?Q((a,l)=>Kn(n,a,l)):t==="drafts"?Q(jn):t==="subscribers"?Q((a,l)=>ii(n,a,l)):t==="sent"?n?Q((a,l)=>Xn(n,a,l)):Q(Qn):t==="template"?Q(ns):t==="settings"?Q(os):t==="reference"?Q(Yn):t==="docs"?Q((a,l)=>Zn(n,a,l,s)):Q(Fn)}function ai(){window.addEventListener("hashchange",()=>{if(location.hash!==is){if(Dt()?.beforeLeave?.()==="confirm"&&!confirm(fr)){location.hash=is;return}as()}}),window.addEventListener("beforeunload",e=>{Dt()?.dirty?.()&&(e.preventDefault(),e.returnValue="")}),window.addEventListener("keydown",e=>{let t=Dt()?.manualSave;(e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==="s"&&t&&(e.preventDefault(),t())})}async function hr(){hs(),io(),Ys(),ai();async function e(){try{let s=await fetch("/api/dev/token?kind=human");if(s.ok)return Pn((await s.json()).token),!0}catch{}return!1}async function t(){try{return await fetch("/api/whoami",{headers:qt(),redirect:"manual"})}catch{return null}}z.token||await e();let n=await t();if(!n?.ok&&z.token&&(Pn(""),await e()&&(n=await t())),n?.ok){z.session=await n.json(),document.body.classList.remove("signed-out"),Ss();try{z.appConfig=await T("/api/settings")}catch{}return Ot(),as()}return Ut()}hr();})();
758
+ <div id="subPager"></div>`);let o=d("#subList"),a=d("#subPager");async function l(){try{let r=await T(`/api/subscribers?${at(s)}`,{signal:n}),c=r.counts;g(d("#subCounts"),i`<div class="card row" style="gap:24px"><span><strong>${c.confirmed}</strong> confirmed</span><span>${c.pending} pending</span><span>${c.unsubscribed} unsubscribed</span><span>${c.suppressed} suppressed</span>${sn("Pending: subscribed but hasn't clicked the confirmation email. Confirmed: consented \u2014 receives sends. Unsubscribed: opted out. Suppressed: bounced or complained \u2014 never mailed, whatever the consent state.",{below:!0})}</div>`),Yl(o,r.subscribers,s,l),Oe(a,s,r.page,l)}catch(r){X(o,r instanceof Error?r.message:String(r),l)}}d("#addSub").onclick=()=>mn(l),lt(t,s,l),l()}var Wl={bounce:"bounced",complaint:"complaint",manual:"blocked"},Kl={bounce:"Hard bounce \u2014 mail to this address failed, so it won't be mailed again.",complaint:"Marked as spam \u2014 won't be mailed again, whatever the consent state.",manual:"Manually blocked \u2014 won't be mailed."};function Zl(e){if(!e.suppressed)return null;let t=e.suppression_reason??"",n=Wl[t]||"suppressed",s=Kl[t]||"Suppressed \u2014 won't be mailed, whatever the consent state.",o=e.suppression_detail?`${s} (${e.suppression_detail})`:s;return i` <span class="badge suppressed row-flag" title="${o}">${n}</span>`}function Yl(e,t,n,s){if(!t.length){g(e,i`<p class="muted">No subscribers match.</p>`);return}g(e,i`<div class="table-wrap"><table class="list-table subs-table stacks has-actions"><colgroup><col><col class="c-status"><col class="c-date"><col class="c-act"></colgroup><thead><tr>${fe("Email","email",n)}${fe("Status",null,n)}${fe("Joined","joined",n)}<th></th></tr></thead><tbody>${t.map(o=>i`<tr data-id="${o.id}"><td>${o.email}${Zl(o)}</td><td>${Ge(o.status)}</td><td class="muted joined">${W(o.created_at)}</td><td class="act">${o.status==="confirmed"?i`<button class="icon" data-menu="${o.id}" aria-label="Subscriber actions">⋯</button>`:null}</td></tr>`)}</tbody></table></div>`),qe(e,n,s);for(let o of P("[data-menu]",e))o.onclick=a=>{a.stopPropagation();let l=t.find(r=>r.id===o.dataset.menu);l&&on(o,[{label:"Unsubscribe",danger:!0,onClick:()=>_o(l,s)}])}}var Gl="You have unsaved changes. Leave without saving?",$s="";function ks(){let e=location.hash||"#/dashboard";$s=e;let[,t,n,s]=(Un(e.slice(1))?e:"#/dashboard").split("/");document.body.classList.toggle("editor-mode",t==="edit");let o=t==="web"?ui(n,s):null;document.body.classList.toggle("web-mode",o!==null);let a=t==="docs"||t==="reference";document.body.classList.toggle("tool-mode",a);for(let l of document.querySelectorAll(".sidebar a[data-view]"))l.dataset.view===t?l.setAttribute("aria-current","page"):l.removeAttribute("aria-current");return $t(!1),t==="edit"&&n?K((l,r)=>ss(n,l,r)):o!==null?K((l,r)=>ns(o,l,r)):t==="drafts"?K(Gn):t==="subscribers"?K((l,r)=>sa(n,l,r)):t==="sent"?n?K((l,r)=>cs(n,l,r)):K(ls):t==="template"?K(fs):t==="settings"?K(Ss):t==="reference"?K(rs):t==="docs"?K((l,r)=>is(n,l,r,s)):K(Xn)}function oa(){let e=$o(location.search);if(e){let t=new URL(location.href);t.search="",t.hash=e,history.replaceState(null,"",t)}}function ia(){window.addEventListener("hashchange",()=>{if(location.hash!==$s){if(qt()?.beforeLeave?.()==="confirm"&&!confirm(Gl)){location.hash=$s;return}ks()}}),window.addEventListener("beforeunload",e=>{qt()?.dirty?.()&&(e.preventDefault(),e.returnValue="")}),window.addEventListener("keydown",e=>{let t=qt()?.manualSave;(e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==="s"&&t&&(e.preventDefault(),t())})}async function zl(){Ps(),Po(),So(),ia();async function e(){try{let s=await fetch("/api/dev/token?kind=human");if(s.ok)return Nn((await s.json()).token),!0}catch{}return!1}async function t(){try{return await fetch("/api/whoami",{headers:jt(),redirect:"manual"})}catch{return null}}Z.token||await e();let n=await t();if(!n?.ok&&Z.token&&(Nn(""),await e()&&(n=await t())),n?.ok){Z.session=await n.json(),document.body.classList.remove("signed-out"),Is();try{Z.appConfig=await T("/api/settings")}catch{}return Wt(),oa(),ks()}return Ft()}zl();})();
721
759
  //# sourceMappingURL=app.js.map