@kurtbruns/kestrel 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dev.vars.example +5 -17
- package/CHANGELOG.md +26 -1
- package/README.md +1 -1
- package/dist/public/dashboard/app.js +196 -193
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +2 -2
- package/dist/public/dashboard/styles.css +40 -0
- package/dist/worker/index.js +210 -69
- package/docs/README.md +3 -3
- package/docs/get-started/02-deploy.md +29 -13
- package/docs/get-started/03-access.md +3 -3
- package/docs/get-started/04-resend.md +8 -6
- package/docs/get-started/05-verify.md +2 -2
- package/docs/get-started/06-go-live.md +2 -2
- package/docs/guides/02-ses.md +2 -2
- package/docs/guides/08-try-locally.md +108 -0
- package/docs/reference/01-configuration.md +17 -6
- package/docs/reference/02-sending-domain-dns.md +1 -1
- package/package.json +2 -2
- package/template/wrangler.jsonc +7 -6
- package/docs/get-started/01-overview.md +0 -47
|
@@ -1,137 +1,138 @@
|
|
|
1
1
|
// GENERATED by scripts/build-client.mjs from client/ — edit the source there, never this file.
|
|
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={"&":"&","<":"<",">":">",'"':""","'":"'"};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
|
|
4
|
-
`;var
|
|
5
|
-
`;var
|
|
6
|
-
`;var
|
|
7
|
-
`;var
|
|
8
|
-
`;var
|
|
9
|
-
`;var
|
|
10
|
-
`;var
|
|
11
|
-
`;var
|
|
12
|
-
`;var
|
|
13
|
-
`;var
|
|
14
|
-
`;var
|
|
15
|
-
`;var
|
|
16
|
-
`;var
|
|
17
|
-
`;var
|
|
18
|
-
`;var
|
|
19
|
-
`;var
|
|
20
|
-
`;var
|
|
21
|
-
`;var
|
|
22
|
-
`;var
|
|
23
|
-
`;var
|
|
24
|
-
`;var
|
|
25
|
-
`;var
|
|
26
|
-
`;var
|
|
27
|
-
`;var
|
|
28
|
-
`;var
|
|
29
|
-
`;var
|
|
30
|
-
`;var
|
|
31
|
-
`;var
|
|
32
|
-
`;var
|
|
33
|
-
`;var
|
|
34
|
-
`;var
|
|
2
|
+
"use strict";(()=>{function On(e){let t=document.getElementById(e);if(!t)throw new Error(`the admin page has no #${e}`);return t}var $e=On("app"),qn=On("identity"),Un=On("toasts"),Os=document.getElementById("navToggle"),In=document.getElementById("navScrim");function xt(e){document.body.classList.toggle("nav-open",e),Os?.setAttribute("aria-expanded",e?"true":"false"),In&&(In.hidden=!e)}var Et=document.getElementById("navCollapse"),Dn=null;function ka(){return window.innerWidth<540?"full":Dn==="collapsed"?"rail":Dn==="expanded"?"full":window.innerWidth<=1024?"rail":"full"}function Nn(){let e=ka();if(document.documentElement.dataset.nav=e,!Et)return;let t=e!=="rail",n=t?"Collapse sidebar":"Expand sidebar";Et.setAttribute("aria-expanded",t?"true":"false"),Et.setAttribute("aria-label",n),Et.setAttribute("title",n)}var Bn=0,Ds=0;function qs(){Os?.addEventListener("click",()=>xt(!document.body.classList.contains("nav-open"))),In?.addEventListener("click",()=>xt(!1)),document.querySelector(".sidebar")?.addEventListener("click",e=>{e.target instanceof Element&&e.target.closest("a")&&xt(!1)}),Et?.addEventListener("click",()=>{Dn=document.documentElement.dataset.nav!=="rail"?"collapsed":"expanded",Nn()}),window.addEventListener("resize",()=>{document.body.classList.add("nav-resizing"),clearTimeout(Ds),Ds=setTimeout(()=>{document.body.classList.remove("nav-resizing")},200),!Bn&&(Bn=requestAnimationFrame(()=>{Bn=0,Nn()}))}),Nn()}var Ot=null,qt=null;function G(e){Ot?.abort(),qt=null;let t=new AbortController;Ot=t;let n=document.createElement("div");n.className="view",$e.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&&(qt=o)},o=>{if(!t.signal.aborted)throw o})}function Ut(){Ot?.abort(),Ot=null,qt=null,$e.replaceChildren()}function jt(){return qt}function Ge(e,t,n){if(n.aborted)return;let s=setInterval(t,e);n.addEventListener("abort",()=>clearInterval(s),{once:!0})}function Ft(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 Us(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){Ta(r)||reportError(r)}l!==!1&&s()},e);n.addEventListener("abort",o,{once:!0})};s()}function Ye(e,t){if(e.aborted){t();return}e.addEventListener("abort",t,{once:!0})}function Ta(e){return e instanceof Error&&(e.name==="AbortError"||e.name==="ApiError")}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 La={"&":"&","<":"<",">":">",'"':""","'":"'"};function Q(e){return String(e).replace(/[&<>"']/g,t=>La[t]??t)}var Ze=class{markup;constructor(t){this.markup=t,Object.freeze(this)}toString(){return this.markup}};function Fs(e){return e instanceof Ze?e.markup:e==null||e===!1?"":Array.isArray(e)?e.map(Fs).join(""):Q(e)}var Ha=/=\s*$/;function i(e,...t){let n=js(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+=Fs(o)+js(e,s+1)}return new Ze(n)}function js(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 O(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 Vt(e){if(!e||typeof e!="object")return null;let t=e;return t.error!=="invalid_config"||typeof t.variable!="string"?null:{variable:t.variable,message:typeof t.message=="string"?t.message:`${t.variable} is refused`}}var Ca="https://getkestrel.dev/docs/configuration/",Ra={PROVIDER:"provider",APP_ORIGIN:"vars",AWS_REGION:"vars",FROM_ADDRESS:"from_address-and-sending_domain",SENDING_DOMAIN:"from_address-and-sending_domain",ARCHIVE_BASE_PATH:"archive_base_path",SES_MAX_SEND_RATE:"ses_max_send_rate",SUBREQUEST_BUDGET:"subrequest_budget",MIN_LEAD_SECONDS:"min_lead_seconds",ARCHIVE_ORIGIN:"archive_origin-and-media_public_base",MEDIA_PUBLIC_BASE:"archive_origin-and-media_public_base",NOTIFY_FROM:"notify_from",ACCESS_TEAM_DOMAIN:"secrets",ACCESS_AUD:"secrets",ACCESS_ALLOWED_EMAILS:"secrets",RESEND_API_KEY:"secrets",RESEND_WEBHOOK_SECRET:"secrets",AWS_ACCESS_KEY_ID:"secrets",AWS_SECRET_ACCESS_KEY:"secrets",SNS_TOPIC_ARN:"secrets",SES_CONFIGURATION_SET:"secrets",SIMULATE_SENDS:"what-the-app-refuses"},Pa="what-the-app-refuses";function Ma(e){return`${Ca}#${Ra[e]??Pa}`}function Wt(e){$e.querySelector(".config-wall")||(Ut(),document.body.classList.add("signed-out"),g($e,i`<div class="card auth-wall config-wall" role="alert"><h2 tabindex="-1">A setting needs fixing</h2><p>Kestrel won't run until it's fixed, and nothing is sent in the meantime.</p><p class="config-wall-problem">${e.message}</p><p class="hint"><a href="${Ma(e.variable)}" target="_blank" rel="noopener">${e.variable} in the Configuration reference <span aria-hidden="true">↗</span><span class="visually-hidden"> (opens in a new tab)</span></a></p><button id="config-reload" class="primary">Reload</button></div>`),d("#config-reload",$e).onclick=()=>location.reload(),d(".config-wall h2",$e).focus())}var Kt="kestrel_token",W={token:localStorage.getItem(Kt)||"",session:null,appConfig:null};function Ks(e){W.token=(e||"").trim();try{W.token?localStorage.setItem(Kt,W.token):localStorage.removeItem(Kt)}catch{}}function jn(){return W.token?{Authorization:`Bearer ${W.token}`}:{}}function Gs(){let e=W.session;if(e?.auth.mode==="access"){let t=e.principal,n=t.email||(t.kind==="service"?"Service token":"Signed in");g(qn,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(qn,i`<span class="who dev" title="Local dev — auth is bypassed on localhost">Local dev</span>`)}function Fn(){Ut(),document.body.classList.add("signed-out"),g($e,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",$e).onclick=()=>location.reload()}async function Vs(){try{let e=await fetch("/api/dev/token?kind=human");if(e.ok)return Ks((await e.json()).token),!0}catch{}return!1}async function Ws(){try{return await fetch("/api/whoami",{headers:jn(),redirect:"manual"})}catch{return null}}async function Ys(){W.token||await Vs();let e=await Ws();if(!e?.ok&&W.token&&(Ks(""),await Vs()&&(e=await Ws())),e?.ok)return await e.json();let t=e?Vt(await e.json().catch(()=>null)):null;return t?Wt(t):Fn(),null}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}},Aa="Not authorized \u2014 please sign in again.";async function Vn(e,t){let n={...jn(),...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 Fn(),new se(Aa,401);return o}function _a(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 Wn(e){let t=await e.text(),n=null;try{n=t?JSON.parse(t):null}catch{}let s=Vt(n);s&&Wt(s);let o=e.statusText||`Request failed (HTTP ${e.status})`;return new se(_a(n)??o,e.status,n)}async function k(e,t={}){let n=await Vn(e,t);if(!n.ok)throw await Wn(n);let s=await n.text();return s?JSON.parse(s):null}async function et(e,t={}){let n=await Vn(e,t);if(!n.ok)throw await Wn(n);return n.text()}async function Zs(e,t={}){let n=await Vn(e,t);if(!n.ok)throw await Wn(n);let s=n.headers.get("content-disposition")??"",o=/filename="([^"]+)"/.exec(s)?.[1]??null;return{text:await n.text(),filename:o}}function Kn(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 Gt(e,t){return e||Kn(t)}function Gn(e){let t=e?.settings.publication;return{name:Gt(t?.name||"",e?.deployment.fromAddress??"")||"Your publication",tagline:t?.tagline||"",logoUrl:t?.logoUrl||null}}function Yt(){let e=Gn(W.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 zs(e,t,n){return`${e}${t}/${n}`}var Fd=1800*1e3,Zt=300*1e3,Vd=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 Kd=10080*60*1e3;function Js(){let e=W.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 Qs(){return W.appConfig?.deployment.minLeadMs??Zt}function kt(){return zt(Qs())}function Jt(e=Date.now()){return new Date(e+Qs()+6e4)}function Tt(){return W.appConfig?.deployment.provider==="fake"}function de(){return W.appConfig?.deployment.provider==="none"}var Yn="#/docs/resend";function _e(e=""){return i`<a class="${e}" href="${Yn}">Connect an email provider</a>`}function Qt(e,t){return de()?i`<p class="hint not-connected">No email provider connected, so ${e}. ${_e()}${t}.</p>`:null}function Xt(){return Qt("a test can\u2019t be sent yet"," to send one")}function Xs(){return Qt("a confirmation can\u2019t be sent yet"," to add subscribers")}function eo(){return Qt("this can\u2019t be scheduled yet"," to schedule it")}function to(){return Qt("this send is held when it fires, until one is"," to send it")}function tt(e){return Tt()?`${e} (no email provider configured, so nothing is delivered)`:e}function en(e){let t=W.appConfig?.deployment;return t?zs(t.archiveOrigin,t.archiveBasePath,e):null}var no=`<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 so=`<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 oo=`<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 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="M6 3v8a4 4 0 0 0 4 4h8"/><path d="m14 11 4 4-4 4"/></svg>
|
|
6
|
+
`;var ao=`<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 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"><circle cx="12" cy="12" r="9"/><path d="m8 12 3 3 5-6"/></svg>
|
|
8
|
+
`;var lo=`<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 co=`<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 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="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>
|
|
11
|
+
`;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" 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 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="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>
|
|
13
|
+
`;var fo=`<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 ho=`<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 vo=`<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 go=`<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 bo=`<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 yo=`<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 wo=`<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 So=`<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 $o=`<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 Eo=`<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 xo=`<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 ko=`<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 To=`<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 Lo=`<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 Ho=`<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 Co=`<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 Ro=`<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 Po=`<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 Mo=`<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 Ao=`<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 _o=`<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 mr={article:O(no),bold:O(so),check:O(ao),code:O(co),copyout:O(uo),editable:O(mo),heading:O(fo),indent:O(ho),info:O(vo),"info-filled":O(go),italic:O(bo),kestrel:O(yo),lines:O(wo),link:O(So),ol:O(Eo),paperclip:O(xo),plus:O(To),preview:O(Lo),quote:O(Ho),readonly:O(Co),search:O(Ro),send:O(Po),ul:O(Mo),upload:O(Ao),book:O(oo),branch:O(io),"circle-check":O(ro),"circle-minus":O(lo),desktop:O(po),mail:O($o),phone:O(ko),x:O(_o)};function P(e){return mr[e]}function nt(e,t,n){let s=(p,y)=>e===p?i`<a href="#/${p}" data-room="${p}" data-text="${y}" aria-current="page">${y}</a>`:i`<a href="#/${p}" data-room="${p}" data-text="${y}">${y}</a>`,o=Js(),a=W.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,u=r?l?i`<footer class="room-foot" title="${l}">${r}</footer>`:i`<footer class="room-foot">${r}</footer>`:null,m=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">
|
|
35
35
|
<header class="room-bar">
|
|
36
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>
|
|
37
37
|
<div class="room-nav">
|
|
38
|
-
<a class="room-brand" href="#/docs">${
|
|
38
|
+
<a class="room-brand" href="#/docs">${P("kestrel")}<span>Kestrel</span></a>
|
|
39
39
|
<nav class="room-switch" aria-label="Reference">${s("docs","Docs")}${s("reference","API")}</nav>
|
|
40
40
|
<a class="room-close" href="#/dashboard" title="Back to publication" aria-label="Back to publication"><span aria-hidden="true">✕</span></a>
|
|
41
41
|
</div>
|
|
42
42
|
</header>
|
|
43
|
-
${
|
|
44
|
-
${
|
|
45
|
-
</div>`}function
|
|
43
|
+
${m}
|
|
44
|
+
${u}
|
|
45
|
+
</div>`}function No(){$e.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 fr=/^(\/[A-Za-z0-9._~%-]+)+$/,hr=/\/(?:\.|%2e){1,2}(?=\/|$)/i;function Zn(e){return fr.test(e)&&!hr.test(e)}function Bo(e){let t=new URLSearchParams(e).get("to");return t&&Zn(t)?`#${t}`:null}var vr=/^(\d+)\.(\d+)$/;function gr(e){return`${e.seq}.${e.at}`}function Io(e){let t=vr.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 tn(e,t){let n=Io(e),s=Io(t);return!n||!s?n?e:t:gr({seq:Math.min(n.seq,s.seq),at:Math.min(n.at,s.at)})}function qe(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 br=3e3,yr=6e4,wr=3e4,He=new Set,nn=null,ze=null,Ne,Oe=null,Lt=!1,zn=!1,sn=0;function on(e){try{e()}catch(t){reportError(t)}}function Jn(e,t){return e&&t?tn(e,t):e??t}function Do(e){clearTimeout(Ne),Ne=void 0,!(!He.size||document.hidden)&&(Ne=setTimeout(Ht,e))}function Ht(){if(clearTimeout(Ne),Ne=void 0,!He.size)return;if(Oe){Lt=!0;return}let e=Jn(nn,ze);ze=null;let t=[...He],n=new AbortController;Oe=n;let s=setTimeout(()=>n.abort(),wr),o=e?`?since=${encodeURIComponent(e)}`:"";k(`/api/sends/feed${o}`,{signal:n.signal}).then(a=>{if(clearTimeout(s),Oe===n){Oe=null,sn=0,nn=a.cursor;for(let l of t)He.has(l)&&l.take(a);if(Lt){Lt=!1,Ht();return}Do(Math.max(0,a.read_again_at-a.now))}},a=>{if(clearTimeout(s),Oe===n){if(Oe=null,Lt=!1,a instanceof se&&a.code==="cursor_ahead"){let l=[...He];for(let r of l)Uo(r);for(let r of l)on(()=>r.stale());return}a instanceof se&&a.status===400?nn=null:ze=Jn(ze,e),sn+=1,Do(Math.min(br*2**(sn-1),yr))}})}function Sr(){zn||(zn=!0,queueMicrotask(()=>{zn=!1,Ht()}))}function Oo(){document.hidden?(clearTimeout(Ne),Ne=void 0):Ht()}function qo(e,t,n){n.aborted||(He.add(e),ze=Jn(ze,t),He.size===1&&document.addEventListener("visibilitychange",Oo),n.addEventListener("abort",()=>Uo(e),{once:!0}),Sr())}function Uo(e){He.delete(e),!He.size&&(clearTimeout(Ne),Ne=void 0,Oe?.abort(),Oe=null,Lt=!1,sn=0,nn=null,ze=null,document.removeEventListener("visibilitychange",Oo))}function st(e,t,n){let s=new Map(e.sends.map(o=>[o.id,qe(o)]));qo({take(o){if(!o.sends.length&&!o.removed.length)return;let a=[];for(let r of o.sends){let c=qe(r),u=s.get(r.id)??null;u!==c&&a.push({send:r,from:u,to:c}),s.set(r.id,c)}let l=o.removed.map(r=>r.id);for(let r of l)s.delete(r);on(()=>t.update({sends:o.sends,changes:a,removed:l}))},stale:()=>t.stale()},e.cursor,n)}function Ct(e,t,n){let s=e.send.id,o=qe(e.send);qo({take(a){if(a.removed.some(u=>u.id===s)){on(()=>t.removed());return}let l=a.sends.find(u=>u.id===s);if(!l)return;let r=qe(l),c=r!==o?{send:l,from:o,to:r}:null;o=r,on(()=>t.update(l,c))},stale:()=>t.stale()},e.cursor,n)}function ot(){He.size&&Ht()}function it(e){return e.trim().toLowerCase()}function at(e){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}var K=e=>e?new Date(e).toLocaleString(void 0,{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}):"\u2014";function an(e){let t=new Set,n=[];for(let s of String(e||"").split(/[\n,]+/)){let o=it(s);at(o)?t.add(o):o&&n.push(s.trim())}return{valid:[...t],invalid:n}}function rn(e){return e.length?`Not ${e.length===1?"an email address":"email addresses"}: ${e.join(", ")}`:null}function rt(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 Rt(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 ln(e){return`Missed its fire time \xB7 ${Math.max(0,Math.floor((Date.now()-e)/6e4))} min late`}function lt(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 dt(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">
|
|
46
46
|
<input class="lt-search" type="search" placeholder="${e.searchPlaceholder||"Search\u2026"}" aria-label="Search" autocomplete="off">
|
|
47
47
|
<div class="lt-filters">${t}${n}${s}</div>
|
|
48
|
-
</div>`}function
|
|
49
|
-
${
|
|
48
|
+
</div>`}function ct(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 he(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 Ue(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 je(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:jo}>← Prev</button><span class="pager-range muted">${a}–${l} of ${n.total}</span><button type="button" class="pager-next"${c?null:jo}>Next →</button></div>`),r){let u=e.querySelector(".pager-prev");u&&(u.onclick=()=>{t.offset=Math.max(0,n.offset-o),s()})}if(c){let u=e.querySelector(".pager-next");u&&(u.onclick=()=>{t.offset=n.offset+o,s()})}}var jo=i` disabled`;var $r=3600,Er=300,xr=3,Xn=new WeakMap;function $(e){let t=[...Un.querySelectorAll(".toast:not(.leaving)")],n=t.find(o=>o.textContent===e);if(n){Fo(n);return}let s=document.createElement("div");s.className="toast",s.textContent=e,Un.appendChild(s),requestAnimationFrame(()=>s.classList.add("show")),Fo(s);for(let o of t.slice(0,t.length+1-xr))Wo(o)}function Fo(e){clearTimeout(Xn.get(e)),Xn.set(e,window.setTimeout(()=>Wo(e),$r))}function Wo(e){clearTimeout(Xn.get(e)),e.style.height=`${e.offsetHeight}px`,e.offsetHeight,e.classList.add("leaving"),e.style.height="0",setTimeout(()=>e.remove(),Er)}var Je=e=>i`<span class="badge ${e}">${e}</span>`;function ve(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 Vo=8;function kr(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-Vo-n-s,Vo-s));e.style.setProperty("--tip-x",`${Math.round(a)}px`)}function Ko(){for(let e of["pointerover","focusin"])document.addEventListener(e,t=>{let n=t.target instanceof Element?t.target.closest(".info"):null;n&&kr(n)});document.addEventListener("keydown",e=>{let t=document.activeElement;e.key==="Escape"&&t instanceof HTMLElement&&t.classList.contains("info")&&t.blur()})}function dn(e,{below:t=!1}={}){return i`<span class="info${t?" tip-below":""}" role="img" tabindex="0" aria-label="${e}" data-tip="${e}">${P("info-filled")}</span>`}var es=[];function Qn(){for(let e of es)e.remove();es=[]}function cn(e,t){Qn();let n=document.createElement("div");n.className="menu-overlay",n.onclick=Qn;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=()=>{Qn(),a.onClick()},s.appendChild(l)}document.body.appendChild(n),document.body.appendChild(s),es=[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 J(e,t,n){let s=e.textContent;e.disabled=!0,t&&(e.textContent=t);try{return await n()}finally{e.isConnected&&(e.disabled=!1,e.textContent=s)}}function 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 Tr=[{value:"draft",label:"Draft"},{value:"scheduled",label:"Scheduled"}],ts="draft,scheduled";function ns(e){return J(e,"Creating\u2026",async()=>{try{let{post:t}=await k("/api/posts",{method:"POST",json:{}});location.hash=`#/edit/${t.id}`}catch(t){$(t instanceof Error?t.message:String(t))}})}async function ss(e,t){let n={status:ts,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
|
+
${dt({statuses:Tr,allValue:ts,searchPlaceholder:"Search subject\u2026"})}
|
|
50
50
|
<div id="list" class="muted">Loading…</div>
|
|
51
|
-
<div id="postsPager"></div>`);let s=d("#newPost",e);s.onclick=()=>
|
|
51
|
+
<div id="postsPager"></div>`);let s=d("#newPost",e);s.onclick=()=>ns(s);let o=d("#list",e),a=d("#postsPager",e),l=new Map,r=new Set,c=0,u=()=>m(!1);async function m(E){let N=++c;try{let A=await k(`/api/posts?${lt(n)}`,{signal:t});if(N!==c)return null;let H=A.posts;if(l=new Map(H.map(h=>[h.id,h.fire_at])),r=new Set(H.flatMap(h=>h.active_send_id?[h.active_send_id]:[])),!H.length){let h=n.search||n.status&&n.status!==ts;return g(o,i`<p class="muted">${h?"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>${he("Title","title",n)}${he("Status",null,n)}${he("Scheduled","scheduled",n)}${he("Updated","updated",n)}<th></th></tr></thead><tbody>${H.map(h=>{let L=h.active_send_status==="sending",C=L?`#/sent/${h.active_send_id}`:`#/edit/${h.id}`;return i`<tr class="clickable" data-id="${h.id}" data-target="${C}"><td><a href="${C}">${h.subject||i`<em>untitled</em>`}</a></td><td>${L?Je("sending"):Je(h.status)}</td><td class="muted when${h.fire_at?"":" empty"}">${h.fire_at?K(h.fire_at):"\u2014"}</td><td class="muted updated">${K(h.updated_at)}</td><td class="act"><button class="icon" data-menu="${h.id}" data-status="${L?"sending":h.status}" data-target="${C}" aria-label="Post actions">⋯</button></td></tr>`})}</tbody></table></div>`),Ue(o,n,u);for(let h of M("tr[data-id]",o))h.onclick=L=>{let C=L.target;C instanceof Element&&C.tagName!=="A"&&!C.closest("[data-menu]")&&h.dataset.target&&(location.hash=h.dataset.target)};for(let h of M("[data-menu]",o))h.onclick=L=>{L.stopPropagation();let C=h.dataset.status,F=h.dataset.target??"",ee=[{label:C==="sending"?"Watch send":"Open",onClick:()=>{location.hash=F}}];if(C==="draft"&&h.dataset.menu){let R=h.dataset.menu;ee.push({label:"Delete draft",danger:!0,onClick:()=>Lr(R,u)})}cn(h,ee)};return je(a,n,A.page,u),A}catch(A){return!E&&N===c&&X(o,A instanceof Error?A.message:String(A),()=>p?u():void x()),null}}let p=!1,y=E=>{p=!0,st({cursor:E.cursor,sends:E.posts.flatMap(N=>N.active_send_id&&N.active_send_status?[{id:N.active_send_id,status:N.active_send_status,phase:N.active_send_status==="sending"?"progressing":"scheduled"}]:[])},{update({sends:N,changes:A,removed:H}){(A.some(L=>l.has(L.send.post_id))||N.some(L=>L.status==="scheduled"&&l.has(L.post_id)&&l.get(L.post_id)!==L.fire_at)||H.some(L=>r.has(L)))&&m(!0)},stale:()=>G(ss)},t)},x=async()=>{let E=await m(!1);E&&!p&&y(E)};ct(e,n,u),await x()}function Lr(e,t){let n=ve(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=()=>J(s,"Deleting\u2026",async()=>{try{await k(`/api/posts/${e}`,{method:"DELETE"}),n.close(),$("Draft deleted"),t()}catch(o){$(o instanceof Error?o.message:String(o))}})}var Pt=e=>Math.max(0,Math.min(100,Math.round(e||0)));function un(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 pn(e){return i`<span class="countdown" data-fire="${e.fire_at}"${e.phase==="due"&&i` data-due`}${Te(e,"missed")&&i` data-missed`}></span>`}function mn(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 M("[data-closes]",e)){let c=Number(r.dataset.closes);r.hidden=o>=c,l(c)}for(let r of M("[data-fire]",e)){let c=Number(r.dataset.fire),u=r.dataset.missed!==void 0;r.textContent=u?ln(c):Rt(c,r.dataset.due!==void 0),r.classList.toggle("countdown-missed",u),l(c)}a-o<=1e3&&a!==n&&(n=a,Ft(a,s,t))};return Ge(1e3,s,t),s}var Te=(e,t)=>e.conditions.some(n=>n.kind===t);function Ee(e,t){return e.conditions.find(n=>n.kind===t)}var Be=(e,t)=>e.actions.some(n=>n.name===t);function ut(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 Mt(e){return Te(e,"wedged")||Te(e,"refused")}function pt(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 fn(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 ~${un(e.dispatch.eta_ms)} left`:"",r=Ee(e,"stuck");return i`<div class="card spread clickable active-card" data-watch="${e.id}">
|
|
52
52
|
<div class="active-main">
|
|
53
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:${
|
|
54
|
+
<div class="active-bar"><div class="active-fill" style="width:${Pt(a)}%"></div></div>
|
|
55
55
|
<div class="muted active-stat">Sending — ${n.toLocaleString()} of ${o.toLocaleString()} accepted${s?` \xB7 ${s.toLocaleString()} confirmed`:""}${l}</div>
|
|
56
56
|
${r?i`<div class="active-stuck">${r.message}</div>`:null}
|
|
57
57
|
</div>
|
|
58
58
|
<a class="ghost-link" href="#/sent/${e.id}">Watch →</a>
|
|
59
|
-
</div>`}function
|
|
59
|
+
</div>`}function Hr(e){return e instanceof se&&e.status===409&&e.data?.error==="remake_required"?e.data:null}function hn(e){return!(e instanceof se)||e.status!==409?!1:e.data?.error==="remake_too_close"}async function Qe(e,t){let n=null;for(;;)try{return await e(n)}catch(s){let o=Hr(s);if(!o)throw s;let a=o.sends||[];if(!await Cr(a,t))return null;n=a.map(r=>r.id)}}function Cr(e,t){let n=e.length,s=n===1,o=`${n} scheduled email${s?"":"s"}`;return new Promise(a=>{let l=ve(i`<h3>Apply this change to ${o}?</h3>
|
|
60
60
|
<p class="hint">${t.lead(n)} ${t.gerund} applies the change to ${s?"its email":"their emails"} too.</p>
|
|
61
|
-
<ul class="remake-list">${e.map(y=>i`<li><span>${y.subject||i`<em>untitled</em>`}</span><span>sends ${
|
|
61
|
+
<ul class="remake-list">${e.map(y=>i`<li><span>${y.subject||i`<em>untitled</em>`}</span><span>sends ${K(y.fire_at)}</span></li>`)}</ul>
|
|
62
62
|
<dl class="remake-facts">
|
|
63
63
|
<div><dt>Changes</dt><dd>the look of ${s?"its email":"their emails"}</dd></div>
|
|
64
64
|
<div><dt>Stays</dt><dd>${s?"its content and fire time":"their content and fire times"}</dd></div>
|
|
65
65
|
<div><dt>Next</dt><dd>send yourself a fresh test${s?"":" of each"}</dd></div>
|
|
66
66
|
</dl>
|
|
67
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>
|
|
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))},
|
|
69
|
-
<div class="pub-row"><span class="pub-key muted">Publication</span><code class="pub-val">${A}</code>${
|
|
70
|
-
<div class="pub-row"><span class="pub-key muted">Archive</span><code class="pub-val">${H}</code>${
|
|
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))},u=l.el.querySelector("#rmCancel"),m=l.el.querySelector("#rmGo");u&&(u.onclick=()=>{l.close(),c(!1)}),m&&(m.onclick=()=>{l.close(),c(!0)});let p=new MutationObserver(()=>{l.el.isConnected||(p.disconnect(),c(!1))});p.observe(document.body,{childList:!0}),m?.focus()})}var Go={action:"Save",gerund:"Saving",lead:e=>`This template is used by ${e} post${e===1?" that's":"s that are"} already scheduled.`},At=(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 vn(e){let t=e?.sends||[];if(!t.length)return i`<span class="set-chip">${P("info")}No posts scheduled</span>`;let n=t.length,s=e?.retry_after?`One sends at ${K(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 Rr=e=>`${new Date(e).toLocaleString(void 0,{month:"short",day:"numeric"})} at ${new Date(e).toLocaleString(void 0,{hour:"numeric",minute:"2-digit"})}`;function gn(e,t,n){let s=n?"this post":`${t} scheduled post${t===1?"":"s"}`;return i`A template or identity change made <strong>${Rr(e)}</strong> was applied to ${s}. Send a fresh test email to review the changes.`}function mt(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 bn(e){let t=de(),n=ve(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>${Xs()}${t?null:i`<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>`),s=d("#aGo",n.el);if(d("#aCancel",n.el).onclick=n.close,s.disabled=t,t){d("#aCancel",n.el).focus();return}let o=d("#addEmail",n.el);o.focus(),s.onclick=()=>J(s,"Adding\u2026",async()=>{let a=it(o.value);if(!at(a)){$("Enter a valid email");return}try{let l=await k("/api/subscribers",{method:"POST",json:{email:a}});n.close(),$(Pr(a,l.action)),e?.()}catch(l){$(l instanceof Error?l.message:String(l))}})}function Pr(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 Yo(e,t){let n=ve(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=()=>J(s,"Unsubscribing\u2026",async()=>{try{await k(`/api/subscribers/${e.id}/unsubscribe`,{method:"POST"}),n.close(),$(`Unsubscribed ${e.email}`),t()}catch(o){$(o instanceof Error?o.message:String(o))}})}var Mr=1500;function Ie(e=!1,t="Copy"){return i`<span>${t}</span>${P(e?"check":"copyout")}`}function ft(e,t){return i`<button type="button" class="copy-btn" data-copy="${e}" aria-label="${t}">${Ie()}</button>`}function is(e,t,n){return i`<div class="block"><div class="block-head"><span class="block-label">${e}</span>${ft(n,`Copy ${e}`)}</div><pre><code>${t}</code></pre></div>`}var os=new WeakMap;async function as(e,t){try{await navigator.clipboard.writeText(t)}catch{$("Couldn't copy to clipboard");return}let n=os.get(e),s=n?.label??e.getAttribute("aria-label")??"Copy",o=e.dataset.copyWord;window.clearTimeout(n?.timer),g(e,Ie(!0,o)),e.setAttribute("aria-label","Copied"),e.classList.add("copied");let a=window.setTimeout(()=>{g(e,Ie(!1,o)),e.setAttribute("aria-label",s),e.classList.remove("copied"),os.delete(e)},Mr);os.set(e,{label:s,timer:a})}function ht(e){e.addEventListener("click",t=>{let n=t.target instanceof Element?t.target.closest("[data-copy]"):null;n&&e.contains(n)&&as(n,n.dataset.copy??"")})}var zo="kestrel.notices",Zo=100;function Jo(){try{let e=JSON.parse(localStorage.getItem(zo)||"{}");return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}function Ar(e){try{localStorage.setItem(zo,JSON.stringify(e))}catch{}}var Qo=(e,t)=>`${e}|${t}`;function _r(e,t){let n=Jo(),s=Date.now();for(let{subject:a,version:l}of t)n[Qo(e,a)]={v:String(l),t:s};let o=Object.keys(n);if(o.length>Zo){o.sort((a,l)=>(n[a]?.t||0)-(n[l]?.t||0));for(let a of o.slice(0,o.length-Zo))delete n[a]}Ar(n)}function yn(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),u=Jo();if(a.every(y=>u[Qo(n,y.subject)]?.v===String(y.version)))return c?.remove(),null;if(c?.dataset.noticeEvents===r)return c;let m=document.createElement("div");m.className="notice",m.setAttribute("role","status"),m.dataset.notice=l,m.dataset.noticeEvents=r,g(m,i`<span class="notice-text">${o}</span><button type="button" class="icon notice-dismiss" aria-label="Dismiss">${P("x")}</button>`);let p=m.querySelector(".notice-dismiss");if(p&&(p.onclick=()=>{_r(n,a),m.remove()}),c){let y=c.querySelector(".notice-dismiss")===document.activeElement;c.replaceWith(m),y&&p?.focus()}else e.appendChild(m);return m}function Xo(e){let t=[],n=e.filter(p=>Te(p,"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(p=>{let y=Ee(p,"wedged");return y?[{s:p,count:y.count}]:[]}),[o]=s;if(o){let p=s.reduce((x,E)=>x+E.count,0),y=p===1?"delivery":"deliveries";t.push({level:"red",text:s.length===1?i`<a href="#/sent/${o.s.id}">${o.s.subject}</a> has ${p} ambiguous ${y} awaiting a decision; resolve ${p===1?"it":"them"} on its page.`:i`${p} ambiguous ${y} across <a href="#/sent">${s.length} sends</a> await a decision; resolve them on the Sent page.`})}let a=e.flatMap(p=>{let y=Ee(p,"refused");return y?[{s:p,c:y}]:[]}),[l]=a;if(l){let p=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`${p}: ${l.c.message}`})}let r=e.filter(p=>Te(p,"stuck")&&!Mt(p)),[c]=r;c&&t.push({level:"amber",text:r.length===1?i`<a href="#/sent/${c.id}">${c.subject}</a>: ${Ee(c,"stuck")?.message}`:i`<a href="#/sent">${r.length} sends</a> have been sending longer than they should; they may be retrying.`});let u=e.flatMap(p=>{let y=Ee(p,"bounce_spike");return y?[{s:p,c:y}]:[]}),[m]=u;return m&&t.push({level:"amber",text:i`<a href="#/sent/${m.s.id}">${m.s.subject}</a>: ${m.c.message}`}),de()&&t.push({level:"amber",text:i`<span class="dash-not-connected">No email provider connected, so nothing can be sent yet. ${_e()} to start sending.</span>`}),t}function ei(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 ti=e=>e.filter(t=>t.status==="sending"&&!Mt(t));async function rs(e,t){let n=()=>{t.aborted||G(rs)};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,S,U]=await Promise.all([k("/api/posts?limit=200",{signal:t}),k("/api/sends?limit=200",{signal:t}),k("/api/subscribers",{signal:t})]);o=v.posts,a=S,l=U.counts}catch(v){X(s,v instanceof Error?v.message:String(v),n);return}let r=a.sends,c=new Map,u=Gn(W.appConfig),m=W.appConfig?.deployment??null,p=l.confirmed+l.pending+l.unsubscribed+l.suppressed;if(!o.length&&p===0){g(s,i`<div class="dash-head"><div><h1>${u.name}</h1>${u.tagline?i`<p class="muted dash-tagline">${u.tagline}</p>`:null}<p class="muted">Let's get your first post out the door.</p></div></div>${Br(u,m)}<section class="dash-section"><h2>API access</h2>${ci(!1)}</section>`),di(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>`,E=o.filter(v=>v.status==="draft").slice(0,5),N=E.length?i`<div class="table-wrap"><table><tbody>${E.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 ${K(v.updated_at)}</td></tr>`)}</tbody></table></div>`:i`<p class="muted">No drafts in progress.</p>`,A=m?.appOrigin||location.origin,H=(m?.archiveOrigin||location.origin)+(m?.archiveBasePath||""),h=i`<div class="card pub-card">
|
|
69
|
+
<div class="pub-row"><span class="pub-key muted">Publication</span><code class="pub-val">${A}</code>${ft(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>${ft(H,"Copy the archive's address")}</div>
|
|
71
71
|
<div class="pub-foot"><a href="/" target="_blank" rel="noopener">View publication ↗</a></div>
|
|
72
|
-
</div>`,L=o.some(v=>v.author==="service"),
|
|
72
|
+
</div>`,L=o.some(v=>v.author==="service"),C=ci(L),F=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`
|
|
73
73
|
<div class="dash-head">
|
|
74
|
-
<div><h1>${
|
|
74
|
+
<div><h1>${u.name}</h1>${u.tagline?i`<p class="muted dash-tagline">${u.tagline}</p>`:null}</div>
|
|
75
75
|
<button class="primary" data-act="new-post">New post</button>
|
|
76
76
|
</div>
|
|
77
|
-
<div id="dashHealth">${
|
|
78
|
-
<div id="dashActive">${
|
|
77
|
+
<div id="dashHealth">${ei(Xo(ut(r,c)))}</div>
|
|
78
|
+
<div id="dashActive">${ri(ti(ut(r,c)))}</div>
|
|
79
79
|
<section class="dash-section"><h2>Subscribers</h2>${x}</section>
|
|
80
80
|
<div id="dashNotices"></div>
|
|
81
81
|
<div class="dash-cols">
|
|
82
|
-
<section class="dash-section"><h2>Scheduled</h2><div id="dashScheduled">${
|
|
83
|
-
<section class="dash-section"><h2>Drafts</h2>${
|
|
82
|
+
<section class="dash-section"><h2>Scheduled</h2><div id="dashScheduled">${ii(wn(r))}</div></section>
|
|
83
|
+
<section class="dash-section"><h2>Drafts</h2>${N}</section>
|
|
84
84
|
</div>
|
|
85
|
-
<section class="dash-section"><h2>Sent</h2><div id="dashSent">${
|
|
86
|
-
<section class="dash-section"><h2>Quick actions</h2>${
|
|
85
|
+
<section class="dash-section"><h2>Sent</h2><div id="dashSent">${ni(r)}</div></section>
|
|
86
|
+
<section class="dash-section"><h2>Quick actions</h2>${F}</section>
|
|
87
87
|
<div class="dash-cols">
|
|
88
|
-
<section class="dash-section"><h2>Publication</h2>${
|
|
89
|
-
<section class="dash-section"><h2>API access</h2>${
|
|
90
|
-
</div>`),
|
|
88
|
+
<section class="dash-section"><h2>Publication</h2>${h}</section>
|
|
89
|
+
<section class="dash-section"><h2>API access</h2>${C}</section>
|
|
90
|
+
</div>`),di(s,n);for(let v of M("tr[data-id]",s))v.onclick=S=>{let U=S.target;U instanceof Element&&U.tagName!=="A"&&!U.closest("button")&&(location.hash=`#/edit/${v.dataset.id}`)};si(s),li(s),ai(s),oi(s,wn(r));let ee=mn(s,t);ee();let R=0,q=async()=>{let v=++R;try{let S=await k("/api/sends?limit=200",{signal:t});if(v!==R)return;r=S.sends,g(d("#dashScheduled",s),ii(wn(r))),ai(s),oi(s,wn(r)),ee(),g(d("#dashSent",s),ni(r)),si(s),_()}catch{}},_=()=>{let v=ut(r,c);g(d("#dashHealth",s),ei(Xo(v))),g(d("#dashActive",s),ri(ti(v))),li(s),Nr(s,[...c.values()])},b=v=>v==="scheduled"||v==="due";st(a,{update:v=>{for(let S of v.sends)c.set(S.id,S);for(let S of v.removed)c.delete(S);_(),(v.changes.length||v.removed.length||v.sends.some(S=>b(qe(S))))&&q()},stale:n},t)}var wn=e=>e.filter(t=>t.status==="scheduled").sort((t,n)=>t.fire_at-n.fire_at);function ni(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>${Je(n.status)}</td><td class="num">${n.audience.count.toLocaleString()}</td><td class="num delivered">${pt(n.counts)}</td><td class="act">${s?i`<a class="ghost-link" href="${s}" target="_blank" rel="noopener">Archive ↗</a>`:null}</td></tr>`})}</tbody></table></div>`:i`<p class="muted">No sends yet.</p>`}function si(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 Nr(e,t){for(let n of t){let s=M("#dashSent tr[data-send]",e).find(o=>o.dataset.send===n.id)?.querySelector("td.delivered");s&&g(s,pt(n.counts))}}function oi(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;yn(n,{kind:"applied",members:s.map(a=>({subject:a.id,version:a.at})),markup:gn(o,s.length,!1)})}function ii(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">${pn(t)} · ${K(t.fire_at)} · ${t.audience.count} recipients</div></div></div>`)}`:i`<p class="muted">Nothing scheduled.</p>`}function ai(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 ri(e){return e.length?i`<section class="dash-section"><h2>Active send${e.length===1?"":"s"}</h2>${e.map(fn)}</section>`:i``}function li(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 di(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=()=>ns(n);for(let n of M("[data-act='add-sub']",e))n.onclick=()=>bn(t);ht(e)}function ci(e){return e?i`<div class="card pub-card">
|
|
91
91
|
<p class="conn-status"><span class="conn-dot" aria-hidden="true"></span>Claude is connected.</p>
|
|
92
|
-
<div class="pub-foot pub-links"><a href="#/reference">${
|
|
92
|
+
<div class="pub-foot pub-links"><a href="#/reference">${P("code")}API reference →</a><a href="#/docs/connect-claude">${P("article")}Connection guide →</a></div>
|
|
93
93
|
</div>`:i`<div class="card pub-card">
|
|
94
94
|
<p class="pub-note">Let Claude draft, proofread, and schedule your posts.</p>
|
|
95
|
-
<div class="pub-foot pub-links"><a href="#/docs/connect-claude">${
|
|
96
|
-
</div>`}function
|
|
95
|
+
<div class="pub-foot pub-links"><a href="#/docs/connect-claude">${P("article")}Connect Claude →</a><a href="#/reference">${P("code")}API reference →</a></div>
|
|
96
|
+
</div>`}function Br(e,t){let n=`${t?.appOrigin||location.origin}/subscribe`,s=de();return i`<div class="card setup">
|
|
97
97
|
<h2 class="setup-title">Set up your publication</h2>
|
|
98
98
|
<ol class="setup-steps">
|
|
99
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>
|
|
100
|
+
${s?i`<li class="setup-connect"><div class="setup-step-main"><strong>Connect an email provider</strong><span class="muted">Nothing can be sent, and signups stay closed, until one is connected.</span></div><button data-nav="${Yn}">Connect an email provider</button></li>`:i`<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>`}
|
|
100
101
|
<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>
|
|
101
|
-
|
|
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>
|
|
102
|
+
${s?i`<li class="setup-subscribe"><div class="setup-step-main"><strong>Share your subscribe link</strong><span class="muted">Signups open once an email provider is connected; until then the page says they aren’t open yet.</span></div></li>`:i`<li class="setup-subscribe"><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">${Ie()}</button></li>`}
|
|
103
103
|
</ol>
|
|
104
|
-
</div>`}var
|
|
104
|
+
</div>`}var ui=e=>e instanceof Error?e.message:String(e);function Sn(e,t){let n=e.c_in_flight||0,o=ve(i`<h3>Resolve ${n} ambiguous ${n===1?"delivery":"deliveries"}</h3>
|
|
105
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>
|
|
106
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>
|
|
107
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>
|
|
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,
|
|
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,u,m)=>J(c,"Resolving\u2026",async()=>{try{let p=await k(`/api/sends/${e.id}/resolve`,{method:"POST",json:{resolution:u}});o.close(),$(p.completed?"Send completed":`Marked ${m}`),t()}catch(p){$(ui(p))}}),l=d("#rUnsent",o.el),r=d("#rAccepted",o.el);l.onclick=()=>a(l,"unsent","not sent"),r.onclick=()=>a(r,"accepted","sent")}function $n(e,t,n){let s=rt(Jt()),o=rt(new Date(t)),a=ve(i`<h3 id="rsHead">Reschedule this post</h3>
|
|
109
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
|
+
${to()}
|
|
110
111
|
<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=()=>
|
|
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=
|
|
112
|
+
<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=()=>J(r,"Rescheduling\u2026",async()=>{let u=c.value,m=u?new Date(u).getTime():Number.NaN;if(Number.isNaN(m)){$("Pick a valid date & time");return}try{await k(`/api/sends/${e}/reschedule`,{method:"POST",json:{fire_at:new Date(m).toISOString()}}),a.close(),$("Rescheduled"),n()}catch(p){if(p instanceof se&&p.status===404){a.close(),$("This post was deleted elsewhere."),ot();return}$(ui(p))}}),c.focus()}function pi(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 mi=e=>e.preventDefault(),vt="allow-same-origin allow-popups allow-popups-to-escape-sandbox";function gt(e){for(let t of e.querySelectorAll("a[href], area[href]")){if(t.getAttribute("href")?.startsWith("#")){t.addEventListener("click",mi),t.addEventListener("auxclick",mi);continue}t.target="_blank",t.rel="noopener noreferrer"}}function Ir(e,t){let n=/<head(\s[^>]*)?>/i;return n.test(e)?e.replace(n,s=>s+t):t+e}function En(e){return Ir(e,`<meta http-equiv="Content-Security-Policy" content="${pi("'none'",{framed:!0})}">`)}function fi(e){return e.replace(/\{\{\s*(?:if\s+)?[\w.]+\s*\}\}/g,t=>`<span class="cx-var">${t}</span>`)}function Dr(e){let t=Q(e);return t=t.replace(/"[^&]*?"/g,n=>`<span class="cx-str">${n}</span>`),t=t.replace(/(<\/?)([a-zA-Z][\w-]*)/g,(n,s,o)=>`<span class="cx-punct">${s}</span><span class="cx-tag">${o}</span>`),t=t.replace(/(\/?)>/g,(n,s)=>`<span class="cx-punct">${s}></span>`),fi(t)}function Or(e){let t=Q(e);return t=t.replace(/"[^&]*?"/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}`),fi(t)}function hi(e){return O(String(e).split(/(<style>[\s\S]*?<\/style>)/).map(t=>{let n=t.match(/^<style>([\s\S]*?)<\/style>$/);return n?`<span class="cx-punct"><</span><span class="cx-tag">style</span><span class="cx-punct">></span>${Or(n[1]??"")}<span class="cx-punct"></</span><span class="cx-tag">style</span><span class="cx-punct">></span>`:Dr(t)}).join(""))}function ls(e){return Q(e).replace(/(`[^`\n]+`)|(!?)(\[[^\]\n]*\])\(([^)\n]*)\)|(\*\*|__)([^\n]+?)\5|\*([^*\n]+?)\*|\b_([^_\n]+?)_\b/g,(t,n,s,o,a,l,r,c,u)=>{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[m,p]=c!==void 0?["*",c]:["_",u];return`<span class="cx-md-mark">${m}</span><span class="cx-md-em">${p}</span><span class="cx-md-mark">${m}</span>`})}function qr(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>${ls(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]}${ls(s[4]??"")}`:ls(e)}function vi(e){let t=!1;return O(String(e).split(`
|
|
113
|
+
`).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=qr(n);return`<span class="cx-md-ln${o?" cx-md-band":""}">${s}</span>`}).join(""))}var Ur=/("(?:\\.|[^"\\])*")(\s*:)?|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|\b(true|false|null)\b|([{}[\],])/g;function gi(e){let t=JSON.stringify(e,null,2)??"",n="",s=0;for(let o of t.matchAll(Ur)){n+=Q(t.slice(s,o.index));let[a,l,r,c,u]=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>`:u!==void 0?n+=`<span class="cx-tag">${u}</span>`:n+=`<span class="cx-punct">${Q(a)}</span>`,s=o.index+a.length}return O(n+Q(t.slice(s)))}function bi(e){return O(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 jr={idleMs:5e3,maxMs:3e4};function yi(e,t=jr){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 xn=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 wi=Promise.resolve();function ds(e){wi=e}function Si(){return wi.then(()=>{},()=>{})}function $i(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 kn=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 xi(e){return`#/web/post/${encodeURIComponent(e)}`}function ki(e,t){if(e!=="post"||!t)return null;try{return decodeURIComponent(t)}catch{return null}}function Fr(e){return e.replace(/^https?:\/\//,"")}function Vr(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 ${Fr(e.url)}`:null;return i`${t} <span class="web-bar-text">${n}${s}</span>`}function Ei(e){try{return new URL(e).href}catch{return null}}function Ti(e,t,n){let s=t?Ei(t):null;if(s)for(let o of e.querySelectorAll("a[href], area[href]")){let a=o.getAttribute("href");a&&Ei(a)===s&&o.setAttribute("href",n)}}function Wr(e){location.replace(e)}async function Kr(e,t){try{return await k(`/api/posts/${encodeURIComponent(e)}`,{signal:t})}catch{return null}}async function cs(e,t,n){let s=document.title;document.title="Web version",Ye(n,()=>{document.title=s});try{await Si();let[o,a]=await Promise.all([et(`/api/posts/${encodeURIComponent(e)}/web`,{signal:n}),Kr(e,n)]);if(n.aborted)return;let l=a?en(a.post.slug):null;if(a&&(a.sending||a.post.status==="sent")&&l){Wr(l);return}let r=Vr(a?{scheduled:a.scheduled!=null,url:l}:null);g(t,i`<div class="web-page">
|
|
113
114
|
<div class="web-bar" role="note"><div class="web-bar-in">${r}</div></div>
|
|
114
|
-
<iframe class="web-frame" sandbox="${
|
|
115
|
-
</div>`);let c=t.querySelector(".web-frame");if(!c)return;c.onload=()=>{let
|
|
115
|
+
<iframe class="web-frame" sandbox="${vt}" title="Web version" scrolling="no"></iframe>
|
|
116
|
+
</div>`);let c=t.querySelector(".web-frame");if(!c)return;c.onload=()=>{let u=c.contentDocument;if(!u||n.aborted)return;gt(u),u.title&&(document.title=`Web version \xB7 ${u.title}`);let m=()=>{let p=u.body?.scrollHeight;p&&(c.style.height=`${p}px`)};if(m(),u.body&&typeof ResizeObserver<"u"){let p=new ResizeObserver(m);p.observe(u.body),Ye(n,()=>p.disconnect())}},c.srcdoc=En(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,()=>G((c,u)=>cs(e,c,u)))}}}var we=e=>e instanceof Error?e.message:String(e),Gr=[[["heading","Heading"],["bold","Bold (\u2318B)"],["italic","Italic (\u2318I)"]],[["quote","Quote"],["code","Code"],["link","Link (\u2318K)"]],[["ul","Bulleted list"],["ol","Numbered list"],["indent","Indent"]]],Yr=i` readonly`,Zr=i` aria-disabled="true"`;async function us(e,t,n){let s=()=>{n.aborted||G((f,T)=>us(e,f,T))},o=!1,a=(f="This post was deleted elsewhere.")=>{o||(o=!0,$(f),n.aborted||Tn("#/drafts"))};g(t,i`<p class="muted">Loading…</p>`);let l,r=null;try{if(l=await k(`/api/posts/${e}`,{signal:n}),n.aborted)return;let f=Jr(l);if(f){Tn(f);return}}catch(f){if(Ce(f)){a("There's no post here. It may have been deleted.");return}X(t,we(f),s);return}if(l.scheduled){try{r=await k(`/api/sends/${l.scheduled.id}`,{signal:n})}catch(f){if(n.aborted)return;if(Ce(f)){a();return}}if(n.aborted||r&&Li(r.send,s))return}let{post:c,markdown:u,scheduled:m}=l,p=c.status!=="draft",y=new kn(c.current_revision),x=Gr.map((f,T)=>i`${T?i`<span class="sep"></span>`:null}${f.map(([w,j])=>i`<button type="button" class="tb" data-fmt="${w}" title="${j}" aria-label="${j}"${p?Zr:null}>${P(w)}</button>`)}`),E=p?Yr:null,N=`${location.origin}${location.pathname}${xi(e)}`;g(t,i`
|
|
116
117
|
<div class="editor-head">
|
|
117
118
|
<a href="#/drafts" class="back">← Drafts</a>
|
|
118
119
|
<div class="editor-head-right">
|
|
119
|
-
<a class="ghost btn" id="webLink" href="${
|
|
120
|
+
<a class="ghost btn" id="webLink" href="${N}" target="_blank" rel="noopener" aria-label="Web version (opens in a new tab)">Web version ↗</a>
|
|
120
121
|
</div>
|
|
121
122
|
</div>
|
|
122
|
-
${
|
|
123
|
+
${p&&m?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}
|
|
123
124
|
<div id="editorNotices"></div>
|
|
124
125
|
<div id="freshnessBanner" class="banner banner-conflict" role="alert" hidden></div>
|
|
125
126
|
<div class="card">
|
|
126
127
|
<div class="grid2">
|
|
127
|
-
<div><label for="f-subject">Subject</label><input id="f-subject" value="${c.subject}" placeholder="Untitled" aria-describedby="f-subject-error"${
|
|
128
|
+
<div><label for="f-subject">Subject</label><input id="f-subject" value="${c.subject}" placeholder="Untitled" aria-describedby="f-subject-error"${E}><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>
|
|
128
129
|
<div>
|
|
129
130
|
<div class="label-row">
|
|
130
131
|
<label for="f-slug">Slug</label>
|
|
131
|
-
${
|
|
132
|
+
${dn("The web address of this post's archive page.")}
|
|
132
133
|
</div>
|
|
133
|
-
<input id="f-slug" value="${c.slug}"${
|
|
134
|
-
${
|
|
134
|
+
<input id="f-slug" value="${c.slug}"${E}>
|
|
135
|
+
${p?null:i`<label class="slug-auto-toggle"><input type="checkbox" id="f-slug-auto">Auto-generate from subject</label>`}
|
|
135
136
|
</div>
|
|
136
137
|
</div>
|
|
137
138
|
|
|
@@ -139,114 +140,115 @@
|
|
|
139
140
|
<div class="composer">
|
|
140
141
|
<div class="composer-head">
|
|
141
142
|
<div class="ctabs" role="tablist">
|
|
142
|
-
<button type="button" class="ctab active" data-tab="edit" data-text="Edit" role="tab" aria-selected="true"><span class="ctab-label">${
|
|
143
|
-
<button type="button" class="ctab" data-tab="preview" data-text="Preview" role="tab" aria-selected="false"><span class="ctab-label">${
|
|
143
|
+
<button type="button" class="ctab active" data-tab="edit" data-text="Edit" role="tab" aria-selected="true"><span class="ctab-label">${P("editable")}Edit</span></button>
|
|
144
|
+
<button type="button" class="ctab" data-tab="preview" data-text="Preview" role="tab" aria-selected="false"><span class="ctab-label">${P("preview")}Preview</span></button>
|
|
144
145
|
</div>
|
|
145
146
|
<div class="toolbar" role="toolbar" aria-label="Formatting">${x}</div>
|
|
146
147
|
</div>
|
|
147
|
-
<div class="composer-body${
|
|
148
|
+
<div class="composer-body${p?" locked":""}" id="composerBody">
|
|
148
149
|
<pre class="md-hl" id="mdHl" aria-hidden="true"><code></code></pre>
|
|
149
|
-
<textarea id="f-markdown" class="editor"${
|
|
150
|
-
<iframe id="previewFrame" class="preview" sandbox="${
|
|
150
|
+
<textarea id="f-markdown" class="editor"${p?null:i` placeholder="Type your post in Markdown…"`}${E}>${u}</textarea>
|
|
151
|
+
<iframe id="previewFrame" class="preview" sandbox="${vt}" title="Email preview" hidden></iframe>
|
|
151
152
|
</div>
|
|
152
|
-
${
|
|
153
|
+
${p?i`<div class="composer-foot composer-foot-lock" id="lockFoot" role="status">${P("readonly")}<span>Cancel the schedule to edit</span></div>`:i`<div class="composer-foot" id="dropFoot">${P("paperclip")}<span>Paste, drop, or click to add images</span></div>`}
|
|
153
154
|
<input type="file" id="imgInput" accept="image/png,image/jpeg,image/webp,image/gif" multiple hidden>
|
|
154
155
|
</div>
|
|
155
156
|
<div id="warnings"></div>
|
|
156
157
|
|
|
157
158
|
<div class="actions-bar">
|
|
158
|
-
${
|
|
159
|
+
${p?i`<div class="row"><button type="button" class="secondary" id="testBtn">${P("send")}<span>Send test email</span></button></div>`:i`<div class="row">
|
|
159
160
|
<button type="button" class="ghost" id="saveBtn">Save draft</button>
|
|
160
161
|
<span class="save-status" id="saveStatus" aria-live="polite"></span>
|
|
161
162
|
</div>
|
|
162
163
|
<div class="row">
|
|
163
|
-
<button type="button" class="secondary" id="testBtn">${
|
|
164
|
+
<button type="button" class="secondary" id="testBtn">${P("send")}<span>Send test email</span></button>
|
|
164
165
|
<button type="button" class="primary" id="scheduleBtn">Schedule</button>
|
|
165
166
|
</div>`}
|
|
166
167
|
</div>
|
|
167
|
-
</div>`);let A=d("#f-subject"),H=d("#f-slug"),
|
|
168
|
-
`,
|
|
169
|
-
`).map(
|
|
170
|
-
`);
|
|
171
|
-
`)?
|
|
172
|
-
;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
|
|
168
|
+
</div>`);let A=d("#f-subject"),H=d("#f-slug"),h=d("#f-markdown"),L=d(".toolbar",t),C=d("#previewFrame"),F=d("#mdHl"),ee=d("code",F),R=()=>{F.scrollTop=h.scrollTop,F.scrollLeft=h.scrollLeft},q=()=>{g(ee,vi(h.value)),R()};h.addEventListener("scroll",R),q();let _=p?null:d("#f-slug-auto"),b=()=>({subject:A.value,..._?.checked?{slug_auto:!0}:{slug:H.value,slug_auto:!1},markdown:h.value}),v=null,S=()=>{v&&H.value===v.shown&&(H.value=v.slug,wt()),v=null},U=d("#f-subject-error");function I(){A.classList.remove("is-invalid"),U.hidden=!0}function Y(){return A.value.trim()===""?(A.classList.add("is-invalid"),U.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(),St(),_.checked?Le(!0).catch(T=>$(`Couldn't save \u2014 ${we(T)}`)):(H.focus(),H.select())}),H.addEventListener("blur",()=>{S(),H.value.trim()===""&&(_.checked=!0,f(),St(),oe.dirty||(H.value=pe))}),A.addEventListener("input",()=>{A.value.trim()!==""&&I()})}let re=yi(()=>{Le(!0).catch(f=>$(`Couldn't autosave \u2014 ${we(f)}`))});Ye(n,()=>re.cancel());let z=M(".ctab",t);function ie(f){for(let T of z){let w=T.dataset.tab===f;T.classList.toggle("active",w),T.setAttribute("aria-selected",w?"true":"false")}h.hidden=f!=="edit",F.hidden=f!=="edit",C.hidden=f!=="preview",L.classList.toggle("off",f!=="edit"),t.querySelector("#dropFoot")?.classList.toggle("off",f!=="edit"),f==="edit"&&R()}let pe=c.slug;async function bt(){ie("preview");try{p||await Le(!0),C.srcdoc=await et(`/api/posts/${e}/preview`),C.onload=()=>{try{let f=C.contentDocument;f&&(gt(f),Ti(f,en(pe),N),C.style.height=`${f.body.scrollHeight+24}px`)}catch{}}}catch(f){if(n.aborted||$t(f))return;$(we(f))}}for(let f of z)f.onclick=()=>f.dataset.tab==="preview"?bt():ie("edit");p&&bt();let Fe=p?d("#lockFoot"):null,Pe;function Ve(){Fe&&(Fe.classList.remove("nudge"),requestAnimationFrame(()=>Fe.classList.add("nudge")),clearTimeout(Pe),Pe=setTimeout(()=>Fe.classList.remove("nudge"),900))}function B(f,T,w){let j=h.selectionStart,V=h.selectionEnd,D=h.value.slice(j,V)||w;h.value=h.value.slice(0,j)+f+D+T+h.value.slice(V),h.focus(),h.selectionStart=j+f.length,h.selectionEnd=j+f.length+D.length}function Z(f){let T=h.selectionStart,w=h.selectionEnd,j=h.value.lastIndexOf(`
|
|
169
|
+
`,T-1)+1,V=(h.value.slice(j,w)||"").split(`
|
|
170
|
+
`).map(D=>f+D).join(`
|
|
171
|
+
`);h.value=h.value.slice(0,j)+V+h.value.slice(w),h.focus(),h.selectionStart=j,h.selectionEnd=j+V.length}function te(f){if(p){Ve();return}if(ie("edit"),f==="bold")B("**","**","bold text");else if(f==="italic")B("*","*","italic text");else if(f==="heading")Z("## ");else if(f==="quote")Z("> ");else if(f==="ul")Z("- ");else if(f==="ol")Z("1. ");else if(f==="indent")Z(" ");else if(f==="link")B("[","](https://)","link text");else if(f==="code"){let T=h.selectionStart,w=h.selectionEnd;T===w||h.value.slice(T,w).includes(`
|
|
172
|
+
`)?B("```\n","\n```","code"):B("`","`","code")}St()}for(let f of M(".tb[data-fmt]",t))f.onclick=()=>te(f.dataset.fmt??"");h.addEventListener("keydown",f=>{if(!(f.metaKey||f.ctrlKey))return;let T=f.key.toLowerCase();T==="b"?(f.preventDefault(),te("bold")):T==="i"?(f.preventDefault(),te("italic")):T==="k"&&(f.preventDefault(),te("link"))});let ae=!1,De=!1,ge=!1,Se=p?null:d("#saveBtn"),xe=p?null:d("#saveStatus"),oe=new xn(b),yt=!1;function An(){xe&&(p?(xe.className="save-status",xe.textContent=""):yt?(xe.className="save-status is-saving",xe.textContent="Saving\u2026"):ae?(xe.className="save-status is-dirty",xe.textContent="Unsaved changes"):(xe.className="save-status is-saved",xe.textContent="Saved"))}function wt(){ae=oe.dirty,An()}An();function Ms(){re.touch()}function St(){p||(q(),wt(),ge||Ms())}let Xe=Promise.resolve();function Le(f){let T=Xe.catch(()=>{}).then(()=>ya(f));return Xe=T,ds(Xe),T}async function ya(f){if(f&&!oe.dirty)return"unchanged";if(ge)return"refused";re.cancel(),yt=!0,An();let T=b(),w=JSON.stringify(T),j=H.value;try{let V={...T,base_revision:y.base},{post:D}=await k(`/api/posts/${e}`,{method:"PUT",json:V});if(D.slug&&H.value===j){let le="slug"in T;le&&document.activeElement===H?v={shown:j,slug:D.slug}:H.value=D.slug,le&&(w=JSON.stringify({...T,slug:D.slug}))}return D.slug&&(pe=D.slug),y.saved(D.current_revision),oe.markSaved(w),De=!1,f||$("Saved"),"saved"}catch(V){let D=$i(V);if(D)return Ns(D),"refused";if(Ce(V))return a(),"refused";throw De=!0,V}finally{yt=!1,wt()}}Se&&(Se.onclick=()=>{if(ge){Me.scrollIntoView({block:"nearest"});return}J(Se,"Saving\u2026",()=>Le(!1).catch(f=>$(we(f)))).finally(wt)});function As(){Se&&(ge?(Se.setAttribute("aria-disabled","true"),Se.title="Saving is paused: choose Reload or Keep editing above"):(Se.removeAttribute("aria-disabled"),Se.removeAttribute("title")))}let wa=()=>{if(re.cancel(),p||!oe.dirty)return;let f=b(),T=JSON.stringify(f);ae=!1,Xe=Xe.catch(()=>{}).then(async()=>{if(!oe.dirty)return;let w={...f,base_revision:y.base},{post:j}=await k(`/api/posts/${e}`,{method:"PUT",json:w});y.saved(j.current_revision),oe.markSaved(T)}).catch(w=>{if(Ce(w)){$("This post was deleted elsewhere, so your last edits weren't saved.");return}$(w instanceof se&&w.status===409?"Changed elsewhere \u2014 your edits weren't saved":`Couldn't save your changes \u2014 ${we(w)}`)}),ds(Xe)},Sa={dirty:()=>ae,beforeLeave(){return o?"leave":ae&&(De||ge)?"confirm":(wa(),"leave")},manualSave(){Se&&!Se.disabled&&Se.click()}};if(!p){for(let f of[A,H,h])f.addEventListener("input",St);for(let f of[A,H])f.addEventListener("blur",()=>Le(!0).catch(T=>$(`Couldn't save \u2014 ${we(T)}`))),f.addEventListener("keydown",T=>{T.key==="Enter"&&!T.isComposing&&T.keyCode!==229&&(T.preventDefault(),Le(!0).then(S).catch(w=>$(`Couldn't save \u2014 ${we(w)}`)))})}let Me=d("#freshnessBanner"),$a=f=>f==="service"?"Claude":f||null;function _s(){ge=!1,As(),y.clearWarning(),Me.hidden=!0,g(Me,i``)}function Ns(f){if(p)return;if(ge=!0,As(),y.noteWarned(f),f.kind==="locked")g(Me,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 w=$a(f.author);g(Me,i`<span><span aria-hidden="true">⚠️</span> This draft was changed elsewhere${w?i` — last edited by <strong>${w}</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>`)}Me.hidden=!1,d("#freshReload",Me).onclick=()=>{_s(),s()};let T=Me.querySelector("#freshKeep");T&&(T.onclick=()=>{y.adoptWarned(),_s(),wt(),ae&&Ms()})}function $t(f){return Ce(f)?(a(),!0):!1}if(!p){let f=async()=>{if(yt||ge||o||document.hidden)return;let T=y.base;try{let w=await k(`/api/posts/${e}`,{signal:n}),j=y.decide({status:w.post.status,revision:w.post.current_revision,author:w.author},{saving:yt,baseAtRequest:T});j!=="ignore"&&j!=="fresh"&&Ns(j)}catch(w){Ce(w)&&a()}};Ge(1e4,f,n),Qr(f,n)}if(d("#webLink").addEventListener("click",()=>{p||Le(!0).catch(f=>$(`Couldn't save \u2014 ${we(f)}`))}),p&&m){let f=r?.send??{...m,phase:"scheduled",conditions:[]},T=d("#schedWhen"),w=d("#schedControls"),j="",V=0,D=!1,le=d("#rescheduleSchedule"),be=d("#cancelSchedule"),ye=ne=>f.actions===void 0||Be({actions:f.actions},ne),ke=()=>{let ne=Date.now(),fe=f.phase==="due"||ne>=f.fire_at;fe?D||(le.hidden=!0,be.hidden=!0):(D=!0,le.hidden=!ye("reschedule"),be.hidden=!ye("cancel")),le.disabled=fe,be.disabled=fe,w.hidden=le.hidden&&be.hidden;let Ke=zr(f,fe),Is=String(Ke);Is!==j&&(j=Is,g(T,Ke)),!fe&&f.fire_at-ne<=1e3&&f.fire_at!==V&&(V=f.fire_at,Ft(f.fire_at,ke,n))};ke(),Ge(1e3,ke,n);let We=()=>{f.remade_at&&yn(d("#editorNotices"),{kind:"applied",subject:f.id,version:f.remade_at,markup:gn(f.remade_at,1,!0)})};We();let me=ne=>Ct(ne,{update(fe,Ke){if(f=fe,Ke?.to==="sending"||Ke?.to==="sent"||Ke?.to==="complete"){$("Your send started, so this page switched to its live watch."),Tn(`#/sent/${fe.id}`);return}if(Ke?.to==="canceled"){$("The schedule was canceled elsewhere, so this post is a draft again."),s();return}ke(),We()},removed:a,stale:s},n);r?me(r):Us(5e3,async()=>{let ne=await k(`/api/sends/${m.id}`,{signal:n});return Li(ne.send,s)||(f=ne.send,ke(),We(),me(ne)),!1},n),le.onclick=()=>$n(f.id,f.fire_at,s),be.onclick=()=>J(be,"Canceling\u2026",async()=>{try{await k(`/api/sends/${m.id}/cancel`,{method:"POST"}),$("Schedule canceled"),s()}catch(ne){Ce(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."),ot()):$(we(ne))}}).finally(ke)}async function _n(f){if(f.type.startsWith("image/"))try{let T=new FormData;T.append("file",f);let{image:w}=await k(`/api/posts/${e}/images`,{method:"POST",body:T}),j=h.selectionStart,V=`
|
|
173
|
+

|
|
174
|
+
`;h.value=h.value.slice(0,j)+V+h.value.slice(j),h.selectionStart=h.selectionEnd=j+V.length,St(),$("Image added")}catch(T){$t(T)||$(we(T))}}let Ae=d("#composerBody");if(p){let f=w=>w.metaKey||w.ctrlKey?["x","z","y"].includes(w.key.toLowerCase()):w.key.length===1||w.key==="Enter"||w.key==="Backspace"||w.key==="Delete",T=[h,A,H];for(let w of T)w.addEventListener("keydown",j=>{f(j)&&Ve()}),w.addEventListener("paste",Ve);Ae.addEventListener("dragover",w=>w.preventDefault()),Ae.addEventListener("drop",w=>{w.preventDefault(),Ve()})}else{let f=d("#imgInput"),T=d("#dropFoot");Ae.addEventListener("dragover",w=>{w.preventDefault(),Ae.classList.add("dragover")}),Ae.addEventListener("dragleave",w=>{w.target===Ae&&Ae.classList.remove("dragover")}),Ae.addEventListener("drop",w=>{w.preventDefault(),Ae.classList.remove("dragover"),ie("edit");for(let j of w.dataTransfer?.files??[])_n(j)}),h.addEventListener("paste",w=>{for(let j of w.clipboardData?.items||[])if(j.type.startsWith("image/")){let V=j.getAsFile();V&&(w.preventDefault(),_n(V))}}),T.onclick=()=>f.click(),f.onchange=()=>{for(let w of f.files??[])_n(w);f.value=""}}let Ea=d("#warnings");function xa(f){g(Ea,f?.length?i`<div class="warnings"><strong>Warnings:</strong> ${f.join("; ")}</div>`:i``)}d("#testBtn").onclick=()=>{let f=p?"Delivers the frozen copy that will send, exactly as it will fire, so you can check it in a client. One address per line.":"Delivers the rendered email to real inboxes so you can check it in a client. One address per line.",T=de(),w=ve(i`<h3>Send a test</h3><p class="hint">${f}</p>${Xt()}${T?null:i`<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>`),j=d("#tGo",w.el);if(d("#tCancel",w.el).onclick=w.close,j.disabled=T,T){d("#tCancel",w.el).focus();return}let V=d("#testTo",w.el);V.focus(),k("/api/settings").then(D=>{let le=D.settings.testRecipients||[];if(le.length&&!V.value.trim()){V.value=le.join(`
|
|
175
|
+
`);let be=d("#testDefaultsHint",w.el);be.textContent="Pre-filled from your default test recipients (Settings).",be.hidden=!1}}).catch(()=>{}),j.onclick=()=>J(j,"Sending\u2026",async()=>{let{valid:D,invalid:le}=an(V.value),be=rn(le);if(be){$(be);return}if(!D.length){$("Enter at least one email address");return}try{p||await Le(!0);let ye=0,ke=null;for(let We of D){let me=await k(`/api/posts/${e}/test`,{method:"POST",json:{to:We}});me.sent&&ye++,ke=me.warnings}xa(ke),w.close(),$(tt(ye===D.length?`Test sent to ${ye} address${ye===1?"":"es"}`:`Sent ${ye} of ${D.length}; the rest failed`))}catch(ye){if(Ce(ye)){w.close(),$t(ye);return}$(we(ye))}})};let Bs=p?null:d("#scheduleBtn");return Bs&&(Bs.onclick=()=>{if(!Y())return;let f=rt(Jt()),T=rt(new Date(Date.now()+24*3600*1e3)),w=de(),j=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>${eo()}${w?null:i`<label for="schWhen">Send at</label><input type="datetime-local" id="schWhen" min="${f}" value="${T}">`}<div class="actions"><button type="button" id="schCancel">Cancel</button><button type="button" class="primary" id="schGo">Schedule</button></div>${w?null:i`<div class="altrow"><span class="altrow-note">Skip the review window?</span><button type="button" class="linkbtn" id="toSendNow">Send now →</button></div>`}`,V=ve(j),D=d(".modal",V.el),le=()=>{V.close(),Me.scrollIntoView({block:"nearest"})},be=()=>J(d("#schGo",D),"Scheduling\u2026",async()=>{let me=d("#schWhen",D).value,ne=me?new Date(me).getTime():Number.NaN;if(Number.isNaN(ne)){$("Pick a valid date & time");return}try{if(await Le(!0)==="refused"){le();return}await k(`/api/posts/${e}/schedule`,{method:"POST",json:{fire_at:new Date(ne).toISOString()}}),V.close(),$(tt(`Scheduled for ${K(ne)}. Send yourself a test.`)),s()}catch(fe){if(Ce(fe)){V.close(),$t(fe);return}$(we(fe))}}),ye=()=>J(d("#snGo",D),"Queuing\u2026",async()=>{try{if(await Le(!0)==="refused"){le();return}let{send:me}=await k(`/api/posts/${e}/send`,{method:"POST"});V.close(),$(tt(`Sends at ${K(me.fire_at)}, cancelable until then.`)),s()}catch(me){if(Ce(me)){V.close(),$t(me);return}$(we(me))}});function ke(){if(D.setAttribute("aria-labelledby","schHead"),d("#schCancel",D).onclick=V.close,d("#schGo",D).onclick=be,d("#schGo",D).disabled=w,w){d("#schCancel",D).focus();return}d("#toSendNow",D).onclick=We,d("#schWhen",D).focus()}async function We(){g(D,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>`),D.setAttribute("aria-labelledby","snHead"),d("#snCancel",D).onclick=V.close,d("#snGo",D).onclick=ye,d("#snGo",D).disabled=de(),d("#toSchedule",D).onclick=()=>{g(D,j),ke()},d("#snGo",D).focus();try{let ne=(await k("/api/subscribers")).counts.confirmed,fe=D.querySelector("#snWho");fe&&(fe.textContent=`${ne} confirmed subscriber${ne===1?"":"s"}`)}catch{}}ke()}),Sa}function Li(e,t){return e.status==="sending"||e.status==="sent"?(Tn(`#/sent/${e.id}`),!0):e.status==="canceled"?(t(),!0):!1}function zr(e,t){return Te(e,"missed")?i`Scheduled for <strong>${K(e.fire_at)}</strong> · <span class="countdown countdown-missed">${ln(e.fire_at)}</span>`:t?i`Scheduled for <strong>${K(e.fire_at)}</strong> · <span class="countdown">${Rt(e.fire_at,!0)}</span>`:i`Scheduled for <strong>${K(e.fire_at)}</strong> · <span class="countdown">${Rt(e.fire_at)}</span> · cancelable until then.`}function Ce(e){return e instanceof se&&e.status===404}function Jr(e){return e.sending?`#/sent/${e.sending.id}`:e.post.status==="sent"?e.sent?`#/sent/${e.sent.id}`:"#/sent":null}function Tn(e){location.replace(e)}function Qr(e,t){document.addEventListener("visibilitychange",()=>{document.hidden||e()},{signal:t})}var ps=null;function Xr(e){let t=document.createElement("div");g(t,O(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 ms(e,t,n,s){if(g(t,nt("docs",null,i`<div class="docs-index"><p class="muted">Loading…</p></div>`)),!ps)try{ps=await k("/api/docs",{signal:n})}catch(l){n.aborted||X(d(".room-main",t),l instanceof Error?l.message:String(l),()=>G((r,c)=>ms(e,r,c,s)));return}let{landing:o,docs:a}=ps;if(!a.length){g(d(".room-main"),i`<p class="muted">No documentation.</p>`);return}e?sl(t,o,a,e,s?el(s):void 0):Ri(t,o,a)}function el(e){try{return decodeURIComponent(e)}catch{return}}function Ri(e,t,n){let s=(u,m)=>{let p=Xr(u);return i`<li><a class="doc-card" href="#/docs/${u.slug}">${m===null?null:i`<span class="doc-card-n">${String(m).padStart(2,"0")}</span>`}<span class="doc-card-main"><span class="doc-card-t">${u.title} <span class="doc-card-go" aria-hidden="true">→</span></span>${p?i`<span class="doc-card-d">${p}</span>`:null}</span></a></li>`},o=t.sections.map(u=>{let m=n.filter(x=>x.section===u.id);if(!m.length)return null;let p=m.map((x,E)=>s(x,u.numbered?E+1:null)),y=u.blurb?i`<div class="doc-section-blurb">${O(u.blurb)}</div>`:null;return i`<section class="doc-section"><h2 class="doc-section-t">${u.title}</h2>${y}${u.numbered?i`<ol class="doc-cards">${p}</ol>`:i`<ul class="doc-cards">${p}</ul>`}</section>`}),a=i`<div class="docs-index"><p class="eyebrow">Documentation</p><h1>${t.title}</h1><div class="docs-index-intro">${O(t.intro)}</div>${o}</div>`,l=W.appConfig?.deployment.build.repoUrl||"",r=(u,m,p=m)=>i`<a class="rail-link" href="${u}" target="_blank" rel="noopener" aria-label="${m}"><span class="rail-link-full">${m}</span><span class="rail-link-short" aria-hidden="true">${p}</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,nt("docs",c,a)),window.scrollTo(0,0)}var fs=matchMedia("(max-width: 720px)");function Pi(e){if(!e)return;let t=fs.matches;e.open=!t,d("summary",e).tabIndex=t?0:-1}var Mi=()=>document.querySelector("details#tocOnPage"),Hi=!1;function tl(){Hi||(Hi=!0,fs.addEventListener("change",()=>Pi(Mi())))}function nl(e){return e.trim().toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu,"").replace(/\s/g,"-")}var Ci=e=>`doc-h-${e}`;function sl(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"),Ri(e,t,n);return}let r=n.filter(b=>b.section===l.section),c=r.indexOf(l),u=r[c-1],m=r[c+1],p=(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=u||m?i`<nav class="doc-topnav" aria-label="Adjacent docs">${u?p(u,"Previous"):null}${m?p(m,"Next"):null}</nav>`:null;g(e,nt("docs",i`<div id="docToc"></div>`,i`<article class="doc" id="docsMain"></article>`));let x=d("#docToc"),E=d("#docsMain");g(E,i`<section class="doc-part" id="doc-${l.slug}">${O(l.html)}</section>`);let N=d("section.doc-part",E),A=new Map;for(let b of M("h1, h2, h3, h4",N)){let v=nl(b.textContent||""),S=A.get(v)??0;A.set(v,S+1),b.dataset.anchor=S?`${v}-${S}`:v,b.id=Ci(b.dataset.anchor)}let H=N.querySelector("h1"),h=[];if(H&&h.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)):N.prepend(b)}for(let b of M("h2",N))h.push({id:b.id,anchor:b.dataset.anchor||"",title:b.textContent||""});let L=document.createElement("nav");L.className="doc-pager",g(L,i`${u?i`<a class="doc-pager-btn prev" href="#/docs/${u.slug}"><span class="doc-pager-dir">← Previous</span><span class="doc-pager-title">${u.title}</span></a>`:i`<span></span>`}${m?i`<a class="doc-pager-btn next" href="#/docs/${m.slug}"><span class="doc-pager-dir">Next →</span><span class="doc-pager-title">${m.title}</span></a>`:i`<span></span>`}`),E.appendChild(L);let C=h.map(b=>i`<a class="toc-sub" href="#/docs/${l.slug}${b.anchor?`/${b.anchor}`:""}" data-target="${b.id}">${b.title}</a>`);g(x,h.length?i`<details class="toc-onpage" id="tocOnPage"><summary class="toc-label">On this page</summary>${C}</details>`:i``);let F=Mi();Pi(F),tl();let ee=b=>{if(F)for(let v of M(".toc-sub",F))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,S=v?.dataset.target;S&&(b.preventDefault(),ee(S),F&&fs.matches&&(F.open=!1),history.replaceState(history.state,"",v?.getAttribute("href")??location.hash),document.getElementById(S)?.scrollIntoView({block:"start"}))});for(let b of M("a[href^='http']",E))b.target="_blank",b.rel="noopener";for(let b of M("pre",E)){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 S=b.querySelector("code");try{await navigator.clipboard.writeText((S||b).innerText),v.textContent="Copied",setTimeout(()=>{v.textContent="Copy"},1500)}catch{$("Couldn't copy to clipboard")}}),b.appendChild(v)}let R=h.map(b=>b.id),q=R[0];if(F&&q){let b=R[R.length-1]??q,S=(document.querySelector(".room-bar")?.getBoundingClientRect().height||54)+26,U=()=>{if(!document.getElementById(q)){document.onscroll=null;return}let I=q;if(Math.ceil(window.innerHeight+window.scrollY)>=document.documentElement.scrollHeight)I=b;else for(let Y of R)if((document.getElementById(Y)?.getBoundingClientRect().top??Number.POSITIVE_INFINITY)<=S)I=Y;else break;ee(I)};document.onscroll=U,U()}let _=o?document.getElementById(Ci(o)):null;_?_.scrollIntoView({block:"start"}):window.scrollTo(0,0)}function ol(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 Ln(e){return e.replace(/\([^)]*\)/g,"").replace(/\{[^}]*\}\??/g,"")||"/"}function Ai(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}${Ln(e.path)}${o}"`,l=[e.method==="GET"?`curl ${a}`:`curl -X ${e.method} ${a}`];if(e.access==="admin"&&l.push(...ol(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(` \\
|
|
176
|
+
`)}function _i(e,t){return t===void 0?null:is(e,gi(t),JSON.stringify(t,null,2))}function il(e){let t=Ai(e,location.origin,W.session?.auth.mode??"dev");return t===null?null:is("curl",bi(t),t)}function al(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 rl(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 ll(e){return i`${Ln(e).split(/(:[A-Za-z_]+)/).map((t,n)=>n%2?i`<span class="api-param">${t}</span>`:t)}`}function dl(e){let t=`${e.method} ${Ln(e.path)} ${e.summary}`.toLowerCase();return i`<li data-hay="${t}"><details class="api-route">
|
|
176
177
|
<summary class="api-route-sum">
|
|
177
178
|
<span class="api-method m-${e.method}">${e.method}</span>
|
|
178
|
-
<code class="api-path">${
|
|
179
|
+
<code class="api-path">${ll(e.path)}</code>
|
|
179
180
|
<span class="api-route-line">${e.summary}</span>
|
|
180
181
|
<span class="api-chev" aria-hidden="true"></span>
|
|
181
182
|
</summary>
|
|
182
183
|
<div class="api-route-detail">
|
|
183
184
|
<p class="api-summary">${e.summary}</p>
|
|
184
185
|
${e.description?i`<p class="api-desc muted">${e.description}</p>`:null}
|
|
185
|
-
${
|
|
186
|
-
${
|
|
187
|
-
${
|
|
188
|
-
${
|
|
189
|
-
${
|
|
186
|
+
${al(e.query)}
|
|
187
|
+
${rl(e.accepts)}
|
|
188
|
+
${_i("Request",e.example?.request)}
|
|
189
|
+
${_i("Response",e.example?.response)}
|
|
190
|
+
${il(e)}
|
|
190
191
|
</div>
|
|
191
|
-
</details></li>`}var
|
|
192
|
+
</details></li>`}var cl=(e,t)=>e.routes.filter(n=>n.resource===t.key);function ul(e,t){let n=`${e.access}-${t.key}`,s=cl(e,t);return i`<div class="api-res" id="api-${n}" data-sec="${n}">
|
|
192
193
|
<div class="api-res-head"><h3>${t.title}</h3></div>
|
|
193
|
-
<ul class="api-rows">${s.map(
|
|
194
|
-
</div>`}function
|
|
194
|
+
<ul class="api-rows">${s.map(dl)}</ul>
|
|
195
|
+
</div>`}function pl(e){return i`<section class="api-section" id="api-${e.access}" data-tier="${e.access}">
|
|
195
196
|
<h2>${e.title}</h2>
|
|
196
197
|
<p class="api-blurb muted">${e.blurb}</p>
|
|
197
|
-
${e.resources.map(t=>
|
|
198
|
-
</section>`}function
|
|
199
|
-
${
|
|
198
|
+
${e.resources.map(t=>ul(e,t))}
|
|
199
|
+
</section>`}function ml(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 Ni(e,t,n){let s=e.trim().toLowerCase().split(/\s+/).filter(Boolean),o=0;for(let r of M(".api-section",n)){let c=0;for(let m of M(".api-res",r)){let p=0;for(let x of M("li[data-hay]",m)){let E=s.every(N=>(x.dataset.hay??"").includes(N));x.hidden=!E,p+=E?1:0}m.hidden=p===0;let y=t.querySelector(`a[data-sec="${m.dataset.sec}"]`);y&&(y.hidden=p===0),c+=p}r.hidden=c===0;let u=t.querySelector(`a[data-tier="${r.dataset.tier}"]`);u&&(u.hidden=c===0),o+=c}let a=M(".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 fl(e,t){let n=(d(".room-bar").getBoundingClientRect().height||54)+26,s=M(".api-section, .api-res",t).filter(m=>!m.closest("[hidden]")),o=window.scrollY>0&&Math.ceil(window.innerHeight+window.scrollY)>=document.documentElement.scrollHeight,a=s[0];for(let m of s)if(o||m.getBoundingClientRect().top<=n)a=m;else break;let l=a?.dataset.sec??a?.dataset.tier,r=a?.closest(".api-section")?.dataset.tier,c=M("a[data-sec]",e).filter(m=>getComputedStyle(m).display!=="none"),u=c.find(m=>m.dataset.sec===l)??(r===void 0?void 0:c.find(m=>m.dataset.tier===r));for(let m of M("a[data-sec]",e))m.classList.toggle("active",m===u)}async function hs(e,t){g(e,nt("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",u=>{let p=(u.target instanceof Element?u.target.closest("a[data-sec]"):null)?.dataset.sec;p&&(u.preventDefault(),document.getElementById(`api-${p}`)?.scrollIntoView({block:"start"}))});let o;try{({groups:o}=await k("/api/reference",{signal:t}))}catch(u){X(s,u instanceof Error?u.message:String(u),()=>G(hs));return}g(n,ml(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">${P("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>${P("x")}</button></div></header>${o.map(pl)}<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=()=>fl(n,s),c=u=>{a.value=u,l.hidden=u==="",Ni(u,n,s),r()};a.addEventListener("input",()=>c(a.value)),l.addEventListener("click",()=>{c(""),a.focus()}),ht(s),document.addEventListener("keydown",u=>{let m=u.target instanceof HTMLElement&&(u.target.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(u.target.tagName));u.key==="/"&&!m&&!u.metaKey&&!u.ctrlKey&&!u.altKey?(u.preventDefault(),a.focus()):u.key==="Escape"&&u.target===a&&a.value&&c("")},{signal:t}),Ni("",n,s),window.addEventListener("scroll",r,{passive:!0,signal:t}),window.scrollTo(0,0),r()}var Hn=e=>e instanceof Error?e.message:String(e);function hl(e){let t=[Be(e,"reschedule")?i`<button class="ghost" data-reschedule="${e.id}">Reschedule</button>`:null,Be(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 vs(e,t){let n={status:"sent",search:"",failures:"",sort:"fire",dir:"desc",offset:0,limit:50};g(e,i`<h1>Sent</h1>
|
|
200
|
+
${Tt()?i`<p class="muted">No email provider is configured, so these sends are recorded here but nothing is delivered.</p>`:null}
|
|
201
|
+
${de()?i`<div class="health amber sent-not-connected"><span class="health-dot">⚠️</span><div><span>No email provider connected, so nothing can be sent yet. ${_e()} to start sending.</span></div></div>`:null}
|
|
200
202
|
<div id="stuck"></div>
|
|
201
203
|
<h2>Scheduled</h2><div id="scheduled" class="muted">Loading…</div>
|
|
202
204
|
<div id="active"></div>
|
|
203
205
|
<h2>Sent posts</h2>
|
|
204
|
-
${
|
|
206
|
+
${dt({searchPlaceholder:"Search subject\u2026",failures:!0})}
|
|
205
207
|
<div id="sendsList" class="muted">Loading…</div>
|
|
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=
|
|
208
|
+
<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=mn(e,t),u=[],m=!1,p=0,y=0,x=new Map;function E(){_(),b(),ot()}function N(v){let S=v.filter(U=>U.status==="sending"&&!Mt(U));g(a,S.length?i`<h2>In progress</h2>${S.map(fn)}`:i``);for(let U of M("[data-watch]",a))U.onclick=I=>{I.target instanceof Element&&I.target.tagName==="A"||(location.hash=`#/sent/${U.dataset.watch}`)}}function A(v){let S=v.flatMap(I=>{let Y=Ee(I,"refused");return Y?[{s:I,c:Y}]:[]}),U=v.flatMap(I=>{let Y=Ee(I,"wedged");return Y?[{s:I,c:Y}]:[]});g(s,i`${S.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>`)}${U.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>${Be(I,"resolve")?i`<button class="primary" data-resolve="${I.id}">Resolve…</button>`:null}</div>`)}`);for(let I of M("[data-resolve]",s)){let Y=U.find(re=>re.s.id===I.dataset.resolve);Y&&(I.onclick=()=>Sn({id:Y.s.id,c_in_flight:Y.c.count},E))}}function H(){for(let v of x.values()){let S=M("tr[data-id]",l).find(U=>U.dataset.id===v.id)?.querySelector("td.delivered");S&&g(S,pt(v.counts))}}function h(){let v=ut(u,x);A(v),m&&N(v),H()}let L=v=>v==="scheduled"||v==="due",C=v=>v==="sent"||v==="complete";function F(v){for(let S of v.sends)x.set(S.id,S);for(let S of v.removed)x.delete(S);h(),(v.sends.some(S=>L(qe(S)))||v.changes.some(S=>L(S.from)))&&_(),v.removed.length?(_(),b()):v.changes.some(S=>C(S.to)&&!C(S.from))&&b()}let ee=!1;function R(v){let S=v.filter(U=>U!==null);ee||!S.length||(ee=!0,st({cursor:S.map(U=>U.cursor).reduce(tn),sends:S.flatMap(U=>U.sends)},{update:F,stale:()=>{t.aborted||G(vs)}},t))}async function q(){try{let v=await k("/api/sends?status=sending&limit=200",{signal:t});return u=v.sends,m=!0,h(),v}catch(v){return X(a,Hn(v),async()=>R([await q()])),null}}async function _(){let v=++p;try{let S=await k("/api/sends?status=scheduled&sort=fire&dir=asc&limit=200",{signal:t});if(v!==p)return S;let{sends:U}=S,I=z=>i`<div class="card spread clickable sched-card" data-post="${z.post_id}"><div><a class="card-link sched-subj" href="#/edit/${z.post_id}">${z.subject}</a><div class="muted">${pn(z)} · ${K(z.fire_at)} · ${z.audience.count} recipients</div></div>${hl(z)}</div>`,[Y,...re]=U;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 ${U.length} scheduled</button>`:null}`);let z=o.querySelector("#schedToggle");z&&(z.onclick=()=>{let ie=d("#schedMore",o),pe=ie.hidden;ie.hidden=!pe,z.setAttribute("aria-expanded",String(pe)),z.textContent=pe?"Show fewer":`Show all ${U.length} scheduled`})}for(let z of M(".card.clickable",o))z.onclick=ie=>{let pe=ie.target;pe instanceof Element&&pe.tagName!=="A"&&!pe.closest("button")&&(location.hash=`#/edit/${z.dataset.post}`)};for(let z of M("[data-reschedule]",o)){let ie=U.find(pe=>pe.id===z.dataset.reschedule);ie&&(z.onclick=()=>$n(ie.id,ie.fire_at,E))}for(let z of M("[data-cancel]",o))z.onclick=()=>J(z,"Canceling\u2026",async()=>{try{await k(`/api/sends/${z.dataset.cancel}/cancel`,{method:"POST"}),$("Canceled"),E()}catch(ie){$(Hn(ie))}});return c(),S}catch(S){return v===p&&X(o,Hn(S),async()=>R([await _()])),null}}async function b(){let v=++y;try{let S=await k(`/api/sends?${lt(n)}`,{signal:t});if(v!==y)return S;let U=S.sends;if(!U.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``),S;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>${he("Subject","subject",n)}${he("When","fire",n)}${he("Recipients","recipients",n,"num")}<th class="num">Delivered</th></tr></thead><tbody>${U.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">${K(I.completed_at??I.fire_at)}</td><td class="num recipients"><span class="n">${I.audience.count.toLocaleString()}</span></td><td class="num delivered">${pt(I.counts)}</td></tr>`)}</tbody></table></div>`),Ue(l,n,b);for(let I of M("tr[data-id]",l))I.onclick=Y=>{Y.target instanceof Element&&Y.target.tagName==="A"||(location.hash=`#/sent/${I.dataset.id}`)};return je(r,n,S.page,b),H(),S}catch(S){return v===y&&X(l,Hn(S),async()=>R([await b()])),null}}ct(e,n,b),R(await Promise.all([_(),q(),b()]))}var bs=e=>e instanceof Error?e.message:String(e);async function ys(e,t,n){let s=()=>{n.aborted||G((l,r)=>ys(e,l,r))};g(t,i`<p class="muted">Loading…</p>`);let o;try{o=await k(`/api/sends/${e}`,{signal:n})}catch(l){X(t,bs(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"?El(o,t,n,s):Rl(e,o,t,n,s)}var vl={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"}},gl={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 gs(e){if(e===null)return"it is retried on its own";let t=e-Date.now();return t>0?`next retry in ${un(t)}`:"next retry due now"}function bl(e){let t=e.provider.halt;return Te(e,"refused")&&t?.cause==="not_connected"?`No email provider is connected \u2014 nothing goes out until one is; ${gs(t.retry_at)}.`:Te(e,"refused")&&t?`The provider is refusing this account \u2014 nothing more goes out until it is fixed; ${gs(t.retry_at)}.`:e.phase==="backing-off"&&t?.reason==="unavailable"?`The provider is unavailable \u2014 ${gs(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.":gl[e.phase]||""}function _t(e){let t=vl[e]??{label:e,tone:"muted"};return i`<span class="phase-pill tone-${t.tone}">${t.label}</span>`}function yl(e,t,n,s,o){let a=s>0?Pt(100*n/s):0;return i`<div class="wbar-row">
|
|
207
209
|
<div class="wbar-head"><span class="wbar-label">${t}</span><span class="wbar-count">${n.toLocaleString()} / ${s.toLocaleString()}</span></div>
|
|
208
210
|
<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>
|
|
209
211
|
${o?i`<div class="wbar-sub muted">${o}</div>`:null}
|
|
210
|
-
</div>`}function
|
|
212
|
+
</div>`}function wl(e,t,n,s,o){let a=s>0?Pt(100*n/s):0,l=s>0?Pt(100*t/s):0;return i`<div class="wbar-row">
|
|
211
213
|
<div class="wbar-head"><span class="wbar-label">${e}</span><span class="wbar-count">${t.toLocaleString()} of ${n.toLocaleString()} accepted</span></div>
|
|
212
214
|
<div class="wbar dual" role="progressbar" aria-valuenow="${l}" aria-valuemin="0" aria-valuemax="100">
|
|
213
215
|
<div class="wbar-fill tone-accepted" style="width:${a}%"></div>
|
|
214
216
|
<div class="wbar-fill tone-ok" style="width:${l}%"></div>
|
|
215
217
|
</div>
|
|
216
218
|
<div class="wbar-sub muted">${o}</div>
|
|
217
|
-
</div>`}var
|
|
218
|
-
${
|
|
219
|
-
${c?i`<div class="health red" role="alert"><span class="health-dot">⚠️</span><div><strong
|
|
219
|
+
</div>`}var Sl=[{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 $l(e){return i`<div class="wcounts">${Sl.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 Ui(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"?`${bl(e)}${o?`${s?` \xB7 ~${s.toLocaleString()}/min`:""} \xB7 ETA ${un(o)}`:""}`:"Dispatch complete.",l=(t.unsent||0)+(t.bounced||0)+(t.complained||0),r=Tt()?"No email provider configured, so nothing is delivered":de()?"No email provider connected":`Provider: ${e.provider?.name||"\u2014"}`,c=Ee(e,"refused"),u=c?.cause==="not_connected",m=Ee(e,"stuck");return i`
|
|
220
|
+
${m?i`<div class="health amber" role="status"><span class="health-dot">⚠️</span><div>${m.message}</div></div>`:null}
|
|
221
|
+
${c?i`<div class="health red" role="alert"><span class="health-dot">⚠️</span><div><strong>${u?"No email provider is connected":"The provider is refusing this account"}</strong>${u?null:i`<div>${c.error}</div>`}<div>${c.advice}</div><div>${c.since===null?null:`Since ${K(c.since)}. `}No one has been marked unsent, and once ${u?"one is connected":"this is fixed"} the send resumes at its next retry${c.retry_at===null?"":`, ${K(c.retry_at)}`}.</div></div></div>`:null}
|
|
220
222
|
<div class="wbars">
|
|
221
|
-
${
|
|
222
|
-
${
|
|
223
|
+
${yl("sending","Dispatch \u2014 provider-accepted",n,e.audience.count,a)}
|
|
224
|
+
${wl("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.")}
|
|
223
225
|
</div>
|
|
224
|
-
${
|
|
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
|
|
226
|
+
${$l(t)}
|
|
227
|
+
<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 ji(e){let t=e.started_at?`Started ${K(e.started_at)}`:"Sending now";return i`${t} · ${e.audience.count.toLocaleString()} recipients`}function Bi(e){let t=e;return i`
|
|
226
228
|
<div class="editor-head">
|
|
227
229
|
<a href="#/sent" class="back">← Sent</a>
|
|
228
|
-
${
|
|
230
|
+
${Be(t,"resolve")?i`<button type="button" class="primary" id="resolveBtn">Resolve…</button>`:null}
|
|
229
231
|
</div>
|
|
230
232
|
<div class="card rec-card watch-card">
|
|
231
233
|
<div class="rec-head">
|
|
232
|
-
<div class="watch-title"><h1>${e.subject||i`<em>untitled</em>`}</h1><span id="watchPill">${
|
|
233
|
-
<div class="rec-meta" id="watchMeta">${
|
|
234
|
+
<div class="watch-title"><h1>${e.subject||i`<em>untitled</em>`}</h1><span id="watchPill">${_t(t.phase)}</span></div>
|
|
235
|
+
<div class="rec-meta" id="watchMeta">${ji(e)}</div>
|
|
234
236
|
</div>
|
|
235
|
-
<div id="watchBody">${
|
|
237
|
+
<div id="watchBody">${Ui(t)}</div>
|
|
236
238
|
<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>
|
|
237
|
-
</div>`}function
|
|
239
|
+
</div>`}function Ii(e,t,n){let s=e.querySelector("#resolveBtn");if(s){let o=Ee(t,"wedged")?.count??t.counts.in_flight;s.onclick=()=>Sn({id:t.id,c_in_flight:o},n)}}function Di(e,t){let n=e.querySelector("#watchPill");n&&g(n,_t(t.phase));let s=e.querySelector("#watchMeta");s&&g(s,ji(t));let o=e.querySelector("#watchBody");o&&g(o,Ui(t))}function El(e,t,n,s){g(t,Bi(e.send)),Ii(t,e.send,s);let o=e.send;Ge(1e3,()=>Di(t,o),n),Ct(e,{update:a=>{if(a.status!=="sending"){s();return}o=a,Be(a,"resolve")!==!!t.querySelector("#resolveBtn")?(g(t,Bi(a)),Ii(t,a,s)):Di(t,a)},removed:s,stale:s},n)}function Oi(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 qi(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 xl(e,t){return Object.keys(e).every(n=>e[n]===t[n])}function kl(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 Tl=[{v:"failures",label:"Failures"},{v:"delivered",label:"Delivered"},{v:"all",label:"All"}];function Ll(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 Hl(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 Cl(e,t){let n=e.map(s=>{let o=kl(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?K(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>${he("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 Rl(e,t,n,s,o){let{send:a,outcomes:l}=t;if(a.status==="canceled"){Ml(a,n);return}let r=a.links.archive,c=l.recipients,u=a.completed_at??a.fire_at;g(n,i`
|
|
238
240
|
<div class="editor-head">
|
|
239
241
|
<a href="#/sent" class="back">← Sent</a>
|
|
240
242
|
${r?i`<button type="button" class="primary" id="viewPublished">View published post ↗</button>`:null}
|
|
241
243
|
</div>
|
|
242
244
|
<div class="card rec-card">
|
|
243
245
|
<div class="rec-head">
|
|
244
|
-
<div class="watch-title"><h1>${a.subject||i`<em>untitled</em>`}</h1><span id="recPill">${
|
|
245
|
-
<div class="rec-meta">Sent ${
|
|
246
|
+
<div class="watch-title"><h1>${a.subject||i`<em>untitled</em>`}</h1><span id="recPill">${_t(a.phase)}</span></div>
|
|
247
|
+
<div class="rec-meta">Sent ${K(u)} · ${c.toLocaleString()} recipients</div>
|
|
246
248
|
</div>
|
|
247
249
|
<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>
|
|
248
|
-
<div class="rec-tiles">${
|
|
249
|
-
<p class="rec-recon muted">${
|
|
250
|
+
<div class="rec-tiles">${Oi(l)}</div>
|
|
251
|
+
<p class="rec-recon muted">${qi(l)}</p>
|
|
250
252
|
<div class="rec-actions">
|
|
251
253
|
<button type="button" class="ghost" id="csvBtn">Export recipients (CSV)</button>
|
|
252
254
|
</div>
|
|
@@ -254,7 +256,7 @@
|
|
|
254
256
|
<div class="rec-people-head">
|
|
255
257
|
<h2 class="rec-people-title">Recipients</h2>
|
|
256
258
|
<div class="rec-views" role="group" aria-label="Which recipients to show">
|
|
257
|
-
${
|
|
259
|
+
${Tl.map((R,q)=>i`<button type="button" class="rec-view-btn" data-view="${R.v}" aria-pressed="${q===0?"true":"false"}">${R.label}</button>`)}
|
|
258
260
|
</div>
|
|
259
261
|
</div>
|
|
260
262
|
<input class="rec-people-search" type="search" placeholder="Search email…" aria-label="Search recipients by email" autocomplete="off">
|
|
@@ -262,30 +264,30 @@
|
|
|
262
264
|
<div id="recPager"></div>
|
|
263
265
|
</div>
|
|
264
266
|
<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>
|
|
265
|
-
</div>`);let
|
|
267
|
+
</div>`);let m=document.getElementById("viewPublished");m&&r&&(m.onclick=()=>window.open(r,"_blank","noopener"));let p={view:"failures",search:"",sort:"email",dir:"asc",offset:0,limit:50},y=d("#recRows",n),x=d("#recPager",n),E=0,N=!1,A=async R=>{let q=++E;R||(N=!0);try{let _=await k(`/api/sends/${e}/deliveries?${Hl(p)}`,{signal:s});if(q!==E)return;let b=_.page.total>0?Math.floor((_.page.total-1)/_.page.limit)*_.page.limit:0;if(!_.deliveries.length&&p.offset>b)return p.offset=b,A(R);N=!1,h(()=>{if(!_.deliveries.length){g(y,i`<p class="rec-people-empty muted">${Ll(p)}</p>`),g(x,i``);return}g(y,Cl(_.deliveries,p)),Ue(y,p,H),je(x,p,_.page,H)})}catch(_){q===E&&(!R||N)&&(N=!1,X(y,bs(_),H))}},H=()=>A(!1),h=R=>{let q=document.activeElement,_=q instanceof HTMLElement&&(y.contains(q)||x.contains(q)),b=_?q.dataset.sort:void 0,v=_?["pager-prev","pager-next"].find(U=>q.classList.contains(U)):void 0;R();let S=b?y.querySelector(`.th-sort[data-sort="${b}"]`):v?x.querySelector(`.${v}`):null;S&&!S.disabled&&S.focus()},L=M(".rec-view-btn",n);for(let R of L)R.onclick=()=>{let q=R.dataset.view;(q==="failures"||q==="delivered"||q==="all")&&(p.view=q),p.offset=0;for(let _ of L)_.setAttribute("aria-pressed",String(_===R));H()};let C=n.querySelector(".rec-people-search");if(C){let R;C.oninput=()=>{clearTimeout(R),R=setTimeout(()=>{p.search=C.value,p.offset=0,H()},250)}}H();let F=d("#csvBtn");F.onclick=()=>J(F,"Exporting\u2026",async()=>{try{let R=await Zs(a.links.deliveries_csv),q=URL.createObjectURL(new Blob([R.text],{type:"text/csv"})),_=document.createElement("a");_.href=q,_.download=R.filename??"deliveries.csv",_.click(),setTimeout(()=>URL.revokeObjectURL(q),1e4)}catch(R){$(R instanceof se&&R.status===404?"This send no longer exists, so there are no deliveries to export.":bs(R))}});let ee=l;Ct(t,{update(R){if(R.status==="sending"){o();return}g(d("#recPill",n),_t(R.phase));let q=Pl(R);xl(ee,q)||(ee=q,g(d(".rec-tiles",n),Oi(q)),d(".rec-recon",n).textContent=qi(q),A(!0))},removed:o,stale:o},s)}function Pl(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 Ml(e,t){g(t,i`
|
|
266
268
|
<div class="editor-head">
|
|
267
269
|
<a href="#/sent" class="back">← Sent</a>
|
|
268
270
|
</div>
|
|
269
271
|
<div class="card rec-card">
|
|
270
272
|
<div class="rec-head">
|
|
271
|
-
<div class="watch-title"><h1>${e.subject||i`<em>untitled</em>`}</h1><span id="recPill">${
|
|
272
|
-
<div class="rec-meta">Canceled ${
|
|
273
|
+
<div class="watch-title"><h1>${e.subject||i`<em>untitled</em>`}</h1><span id="recPill">${_t(e.phase)}</span></div>
|
|
274
|
+
<div class="rec-meta">Canceled ${K(e.completed_at)} · was scheduled for ${K(e.fire_at)}</div>
|
|
273
275
|
</div>
|
|
274
276
|
<p class="rec-note muted">This send was canceled before it fired, so no one was mailed.</p>
|
|
275
|
-
</div>`)}function
|
|
277
|
+
</div>`)}function ws(e){let t=document.getElementById(e);if(!t)throw new Error(`the admin page has no #${e}`);return t}var Cn=(()=>{let e=ws("savebar"),t=e.querySelector(".savebar-msg"),n=ws("savebarSave"),s=ws("savebarDiscard"),o=0,a=u=>{t&&g(t,i`<span class="savebar-dot"></span><span>${u}</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:u,onDiscard:m,saveLabel:p="Save changes",discardLabel:y="Discard"},x){if(x.aborted)return{setDirty(){},showError(){}};let E=++o;x.addEventListener("abort",()=>{o===E&&r()},{once:!0}),n.textContent=p,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=()=>J(n,"Saving\u2026",()=>Promise.resolve(u())),s.onclick=()=>m();let N=()=>o===E;return{setDirty(A){N()&&(e.classList.remove("is-error"),A?(a("You have unsaved changes."),l()):e.classList.remove("show"))},showError(A){N()&&(e.classList.add("is-error"),a(A),l())}}}return{attach:c}})();var Re=e=>e instanceof Error?e.message:String(e),Nt=i` hidden`;function ce(e,t){return i`<span class="set-chip ${e}">${P(e)}${t}</span>`}function ue(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 Bt(e,t){return(String(e||t).trim()[0]||"K").toUpperCase()}function Fi(e){let{state:t,fromName:n}=e;return i`
|
|
276
278
|
<section class="set-sec">
|
|
277
|
-
${
|
|
279
|
+
${ue("Confirmation email",ce("editable","Editable"))}
|
|
278
280
|
<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
281
|
<div class="set-preview">
|
|
280
282
|
<div class="set-preview-bar set-ce-bar">
|
|
281
283
|
<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">${
|
|
283
|
-
<button type="button" class="set-ce-modebtn" id="ceTabEdit" aria-pressed="false">${
|
|
284
|
+
<button type="button" class="set-ce-modebtn" id="ceTabPreview" aria-pressed="true">${P("preview")}Preview</button>
|
|
285
|
+
<button type="button" class="set-ce-modebtn" id="ceTabEdit" aria-pressed="false">${P("editable")}Edit</button>
|
|
284
286
|
</div>
|
|
285
287
|
</div>
|
|
286
288
|
<div id="cePreviewBody">
|
|
287
289
|
<div class="set-inbox">
|
|
288
|
-
<div class="set-inbox-avatar" id="cePvAvatar">${
|
|
290
|
+
<div class="set-inbox-avatar" id="cePvAvatar">${Bt(t.name,n)}</div>
|
|
289
291
|
<div class="set-inbox-body">
|
|
290
292
|
<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
293
|
<div class="set-inbox-subj" id="cePvSubject"></div>
|
|
@@ -334,45 +336,45 @@
|
|
|
334
336
|
</div>
|
|
335
337
|
</div>
|
|
336
338
|
</div>
|
|
337
|
-
</section>`}function
|
|
339
|
+
</section>`}function Vi(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=Bt(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 E=n.name||t.fromDisplay,N=!!n.logoUrl,A=N||!!E;a.mast.hidden=!A,A&&(a.mastLogo.hidden=!N,N&&g(a.mastLogo,i`<img src="${n.logoUrl}" alt="">`),a.mastName.textContent=E,a.mastName.hidden=!E,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),u=d("#cePreviewBody",e),m=d("#ceTabEdit",e),p=d("#ceTabPreview",e),y=x=>{c.hidden=!x,u.hidden=x,m.setAttribute("aria-pressed",String(x)),p.setAttribute("aria-pressed",String(!x)),x&&o.subject.focus()};return p.onclick=()=>y(!1),m.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 Al={none:"No email provider connected",fake:"Fake (dev, dead-end)",ses:"Amazon SES",resend:"Resend"};function Rn(e){return Al[e]||e}function _l(e){return e.provider==="none"?i`${Rn(e.provider)}. ${_e("set-link")} to start sending.`:i`${Rn(e.provider)}${e.simulation?i`<span class="set-pill">Simulating ${Nl[e.simulation.profile]}${e.simulation.faults==="none"?", no failures":""}</span>`:null}`}var Nl={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 Wi(e){let t=e.data.deployment,{fromName:n}=e;return i`
|
|
338
340
|
<section class="set-sec">
|
|
339
|
-
${
|
|
341
|
+
${ue("Email sender",ce("readonly","Read-only \xB7 set at deploy"))}
|
|
340
342
|
<div class="set-card sunken">
|
|
341
|
-
<div class="set-ro-note">${
|
|
343
|
+
<div class="set-ro-note">${P("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
344
|
<div class="set-preview-bar" style="background:transparent">
|
|
343
345
|
<span class="set-preview-lbl">Inbox preview</span>
|
|
344
346
|
<span class="set-preview-dot">How readers see the sender</span>
|
|
345
347
|
</div>
|
|
346
348
|
<div class="set-inbox">
|
|
347
|
-
<div class="set-inbox-avatar">${
|
|
349
|
+
<div class="set-inbox-avatar">${Bt(n,n)}</div>
|
|
348
350
|
<div class="set-inbox-body">
|
|
349
351
|
<div class="set-inbox-top"><span class="set-inbox-from">${n}</span><span class="set-inbox-time">9:02 AM</span></div>
|
|
350
352
|
<div class="set-inbox-subj">Your latest post: a sample subject line</div>
|
|
351
353
|
<div class="set-inbox-snip">The opening lines of your post show here as the inbox preview…</div>
|
|
352
|
-
<div class="set-inbox-addr">${
|
|
354
|
+
<div class="set-inbox-addr">${Bl(t.fromAddress)}</div>
|
|
353
355
|
</div>
|
|
354
356
|
</div>
|
|
355
357
|
<div class="set-kv" style="border-top:1px solid var(--line)">
|
|
356
358
|
<div class="set-kv-k">From address</div><div class="set-kv-v"><span class="mono">${t.fromAddress}</span></div>
|
|
357
359
|
<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"
|
|
360
|
+
<div class="set-kv-k">Email provider</div><div class="set-kv-v" id="providerValue">${_l(t)}</div>
|
|
359
361
|
</div>
|
|
360
362
|
</div>
|
|
361
|
-
</section>`}function
|
|
363
|
+
</section>`}function Ki(e){let t=e.data.deployment,n=`${t.archiveOrigin||""}${t.archiveBasePath||""}`,s=t.archiveOrigin===t.appOrigin;return i`
|
|
362
364
|
<section class="set-sec">
|
|
363
|
-
${
|
|
365
|
+
${ue("Instance",ce("readonly","Read-only \xB7 set at deploy"))}
|
|
364
366
|
<div class="set-card sunken">
|
|
365
|
-
<div class="set-ro-note">${
|
|
367
|
+
<div class="set-ro-note">${P("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
368
|
<div class="set-kv">
|
|
367
369
|
<div class="set-kv-k">App origin</div><div class="set-kv-v"><span class="mono">${t.appOrigin}</span></div>
|
|
368
370
|
<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
371
|
<div class="set-kv-k">Image URL base</div><div class="set-kv-v"><span class="mono">${t.mediaPublicBase}</span></div>
|
|
370
372
|
<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">${
|
|
372
|
-
<div class="set-kv-k">Minimum lead</div><div class="set-kv-v">${
|
|
373
|
+
<div class="set-kv-k">Cloudflare Access</div><div class="set-kv-v">${t.accessConfigured?i`<span class="set-pill ok">${P("check")}Configured</span>`:i`<span class="set-pill">Not configured</span>`}</div>
|
|
374
|
+
<div class="set-kv-k">Minimum lead</div><div class="set-kv-v">${zt(t.minLeadMs)}${t.minLeadMs===Zt?i`<span class="set-pill">default</span>`:null}</div>
|
|
373
375
|
</div>
|
|
374
376
|
</div>
|
|
375
|
-
</section>`}var
|
|
377
|
+
</section>`}var Gi=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");function Yi(e,t){return e?`<img class="logo" src="${Gi(e)}" alt="${Gi(t)}" width="44" height="44" />`:""}var Pn=e=>e instanceof Error?e.message:String(e),Zi=!1,Il=[{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."}]}],zi={signed:{label:"Signed",html:`<style>
|
|
376
378
|
.email {
|
|
377
379
|
font: 16px/1.6 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
378
380
|
color: #18181b;
|
|
@@ -527,7 +529,7 @@
|
|
|
527
529
|
<a href="{{ .Email.ViewInBrowserURL }}">View in browser</a>
|
|
528
530
|
<div class="address">{{ .Publication.Address }}</div>{{ end }}
|
|
529
531
|
</div>
|
|
530
|
-
</div>`}},
|
|
532
|
+
</div>`}},Dl=e=>Object.hasOwn(zi,e),Ol=`<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>`,ql=new Set([".Post.Body",".Publication.Logo"]);function Ul(e,t){return String(e).replace(/\{\{\s*(?:if\b[^}]*|end)\s*\}\}/g,"").replace(/\{\{\s*([\w.]+)\s*\}\}/g,(s,o)=>ql.has(o)?t[o]||"":Q(t[o]??""))}function jl(e){let t=e.name||"Your publication";return{".Post.Body":Ol,".Post.Subject":"The starlings are back",".Publication.Name":t,".Publication.Tagline":e.tagline||"Your tagline",".Publication.LogoURL":e.logoUrl,".Publication.Logo":Yi(e.logoUrl,t),".Publication.Address":e.address,".Email.SentTo":"you@example.com",".Email.UnsubscribeURL":"#unsubscribe",".Email.ViewInBrowserURL":"#view-in-browser"}}function Fl(e,t){return Ul(e,jl(t))}var Vl='<!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 Ss(e,t,n){e.setAttribute("sandbox",vt);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,O(Fl(t(),n()))),gt(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=Vl,{repaint:a}}function Wl(){return i`${Il.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 $s(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><style></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 k("/api/settings",{signal:t})}catch(B){X(n,Pn(B),()=>G($s));return}let o=s.settings,a=s.deployment,l=o.publication,r={name:Gt(l.name||"",a.fromAddress),tagline:l.tagline||"",logoUrl:l.logoUrl||"",address:l.address||""},c=o.emailTemplate||"",u=Array.isArray(o.testRecipients)?o.testRecipients:[];g(d("#tplInUse"),vn(s.inUse)),g(n,i`
|
|
531
533
|
<div class="set-preview set-tpl-sample">
|
|
532
534
|
<div class="set-preview-bar">
|
|
533
535
|
<span class="set-preview-titles">
|
|
@@ -539,7 +541,7 @@
|
|
|
539
541
|
<button type="button" class="wtog-btn" data-w="640" aria-pressed="true">640</button>
|
|
540
542
|
<button type="button" class="wtog-btn" data-w="375" aria-pressed="false">375</button>
|
|
541
543
|
</span>
|
|
542
|
-
<button type="button" class="secondary" id="tplTest">${
|
|
544
|
+
<button type="button" class="secondary" id="tplTest">${P("send")}<span id="tplTestLbl">Send test email</span></button>
|
|
543
545
|
</span>
|
|
544
546
|
</div>
|
|
545
547
|
<div class="set-email-stage" id="tplStage">
|
|
@@ -560,8 +562,8 @@
|
|
|
560
562
|
<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>
|
|
561
563
|
</div>
|
|
562
564
|
</div>
|
|
563
|
-
<button type="button" class="set-icon-btn" id="tplLineNums" aria-pressed="false" title="Show line numbers" aria-label="Show line numbers">${
|
|
564
|
-
<button type="button" class="ghost" id="tplCopyAll" title="Copy the whole template to the clipboard" aria-label="Copy the whole template">${
|
|
565
|
+
<button type="button" class="set-icon-btn" id="tplLineNums" aria-pressed="false" title="Show line numbers" aria-label="Show line numbers">${P("lines")}</button>
|
|
566
|
+
<button type="button" class="ghost" id="tplCopyAll" title="Copy the whole template to the clipboard" aria-label="Copy the whole template">${Ie()}</button>
|
|
565
567
|
</div>
|
|
566
568
|
<div class="set-tpl-required" aria-label="Required variables">
|
|
567
569
|
<span class="set-req-lbl">Required</span>
|
|
@@ -577,7 +579,7 @@
|
|
|
577
579
|
<div class="set-tpl-msgs" id="tplMsgs" hidden></div>
|
|
578
580
|
</div>
|
|
579
581
|
</div>
|
|
580
|
-
<div class="set-note">${
|
|
582
|
+
<div class="set-note">${P("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>
|
|
581
583
|
</div>
|
|
582
584
|
|
|
583
585
|
<div class="set-card set-tpl-varcard">
|
|
@@ -586,14 +588,14 @@
|
|
|
586
588
|
<h3 class="set-tpl-vartitle">Variables</h3>
|
|
587
589
|
<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>
|
|
588
590
|
</div>
|
|
589
|
-
<div class="set-tpl-vars-body">${
|
|
591
|
+
<div class="set-tpl-vars-body">${Wl()}</div>
|
|
590
592
|
</div>
|
|
591
|
-
</div>`);let
|
|
592
|
-
`).length;for(let
|
|
593
|
-
`;
|
|
594
|
-
`);let
|
|
593
|
+
</div>`);let m=d("#tplEditor"),p=Ss(d("#tplPreview"),()=>m.value,()=>r),y=d("#tplMsgs"),x=d("#tplTest"),E=d("#tplTestLbl"),N=()=>m.value!==c,A=d("#tplHl"),H=d("code",A),h=d("#tplGutter"),L=()=>{let B=m.value;g(H,i`${hi(B)}\n`);let Z="",te=B.split(`
|
|
594
|
+
`).length;for(let ae=1;ae<=te;ae++)Z+=`${ae}
|
|
595
|
+
`;h.textContent=Z},C=()=>{A.scrollTop=m.scrollTop,A.scrollLeft=m.scrollLeft,h.scrollTop=m.scrollTop};m.addEventListener("scroll",C);let F=d("#reqBody"),ee=d("#reqUnsub"),R=()=>{F.className=`set-req-pill ${/\{\{\s*\.Post\.Body\s*\}\}/.test(m.value)?"ok":"bad"}`,ee.className=`set-req-pill ${/\{\{\s*\.Email\.UnsubscribeURL\s*\}\}/.test(m.value)?"ok":"bad"}`},q=Cn.attach({onSave:U,onDiscard:v},t);function _(){L(),R(),C(),q.setDirty(N()),E.textContent=N()?"Save & send test":"Send test email",x.title=N()?"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(B){if(!B.length){y.hidden=!0,g(y,i``);return}y.hidden=!1,y.className="set-tpl-msgs warn",g(y,i`${B.map(Z=>i`<div>${Z}</div>`)}`)}function v(){m.value=c,b([]),p.repaint(),_()}async function S(){let B=m.value,Z=await Qe(te=>k("/api/settings",{method:"PUT",json:te?{emailTemplate:B,remake:te}:{emailTemplate:B}}),Go);return Z?(c=Z.settings.emailTemplate,m.value=c,W.appConfig={...s,settings:Z.settings},p.repaint(),_(),{warnings:Array.isArray(Z.warnings)?Z.warnings:[],remade:Z.remade||[]}):null}async function U(){try{let B=await S();if(!B)return;b(B.warnings),$(mt(B.warnings.length?"Template saved with warnings":"Template saved",B.remade))}catch(B){q.showError(Pn(B))}}let I=B=>{let Z=zi[Dl(B)?B:"signed"];m.value=Z.html,b([]),p.repaint(),_()};m.addEventListener("input",()=>{p.repaint(),_()});let Y=d("#tplExamplesBtn"),re=d("#tplExamplesList"),z=()=>{re.hidden=!0,Y.setAttribute("aria-expanded","false")};Y.onclick=B=>{B.stopPropagation();let Z=re.hidden;re.hidden=!Z,Y.setAttribute("aria-expanded",String(Z))};for(let B of M("[data-example]",re))B.onclick=()=>{I(B.dataset.example??""),z()};Zi||(Zi=!0,document.addEventListener("click",B=>{let Z=document.getElementById("tplExamples"),te=document.getElementById("tplExamplesList"),ae=B.target instanceof Node&&Z?.contains(B.target);Z&&te&&!te.hidden&&!ae&&(te.hidden=!0,document.getElementById("tplExamplesBtn")?.setAttribute("aria-expanded","false"))}));let ie=d("#tplPreview");for(let B of M(".wtog-btn",n))B.onclick=()=>{for(let Z of M(".wtog-btn",n))Z.setAttribute("aria-pressed",String(Z===B));ie.style.maxWidth=`${B.dataset.w}px`,p.repaint()};let pe=d("#tplEditorWrap"),bt=d("#tplLineNums"),Fe=B=>{pe.classList.toggle("show-lines",B),bt.setAttribute("aria-pressed",String(B)),C()},Pe=!1;try{Pe=localStorage.getItem("kestrel.tpl.lineNums")==="1"}catch{}Fe(Pe),bt.onclick=()=>{Pe=!Pe,Fe(Pe);try{localStorage.setItem("kestrel.tpl.lineNums",Pe?"1":"0")}catch{}};let Ve=d("#tplCopyAll");Ve.onclick=()=>as(Ve,m.value),x.onclick=()=>{let B=de(),Z=!B&&N(),te=ve(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>${Xt()}${Z?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}${B?null:i`<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">${Z?"Save & send test":"Send test"}</button></div>`),ae=d("#ttGo",te.el);if(d("#ttCancel",te.el).onclick=te.close,ae.disabled=B,B){d("#ttCancel",te.el).focus();return}let De=d("#tplTestTo",te.el);if(De.focus(),u.length&&!De.value.trim()){De.value=u.join(`
|
|
596
|
+
`);let ge=d("#tplTestHint",te.el);ge.textContent="Pre-filled from your default test recipients (Settings).",ge.hidden=!1}ae.onclick=()=>J(ae,N()?"Saving\u2026":"Sending\u2026",async()=>{let{valid:ge,invalid:Se}=an(De.value),xe=rn(Se);if(xe){$(xe);return}if(!ge.length){$("Enter at least one email address");return}if(N())try{let oe=await S();if(!oe){te.close();return}b(oe.warnings)}catch(oe){q.showError(Pn(oe)),te.close();return}try{let oe=await k("/api/settings/template/test",{method:"POST",json:{to:ge}});te.close(),$(tt(oe.sent===oe.total?`Test sent to ${oe.sent} address${oe.sent===1?"":"es"}`:`Sent ${oe.sent} of ${oe.total}; the rest failed`))}catch(oe){$(Pn(oe))}})},m.value=c,p.repaint(),_()}function Ji(e){return i`
|
|
595
597
|
<section class="set-sec">
|
|
596
|
-
${
|
|
598
|
+
${ue("Email template",ce("editable","Editable"),vn(e.data.inUse))}
|
|
597
599
|
<p class="set-lede">The template controls the look and feel of the emails you send. Edit it on the Template page.</p>
|
|
598
600
|
<div class="set-preview set-tpl-sample">
|
|
599
601
|
<div class="set-preview-bar">
|
|
@@ -603,9 +605,9 @@
|
|
|
603
605
|
<iframe class="set-email-frame" id="tplPreview" title="Sample email preview" scrolling="no"></iframe>
|
|
604
606
|
</div>
|
|
605
607
|
<div class="row" style="margin-top:12px"><button type="button" class="primary" id="tplEditLink">Edit template →</button></div>
|
|
606
|
-
</section>`}function
|
|
608
|
+
</section>`}function Qi(e,t){let n=t.data.settings.emailTemplate||"",s=Ss(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 Xi(){return i`
|
|
607
609
|
<section class="set-sec">
|
|
608
|
-
${
|
|
610
|
+
${ue("Default test recipients",ce("editable","Editable"))}
|
|
609
611
|
<div class="set-card">
|
|
610
612
|
<div class="set-recip">
|
|
611
613
|
<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>
|
|
@@ -616,22 +618,22 @@
|
|
|
616
618
|
</div>
|
|
617
619
|
</div>
|
|
618
620
|
</div>
|
|
619
|
-
</section>`}function
|
|
621
|
+
</section>`}function ea(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}">${P("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=it(o.value);if(r){if(!at(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 Kl(e,t){let n=Math.min(e,t);return{x:Math.floor((e-n)/2),y:Math.floor((t-n)/2),side:n}}var Es=class extends Error{constructor(){super("Couldn\u2019t read that image. Pick a PNG, JPEG, WebP, or GIF.")}};async function ta(e,t){let n=await createImageBitmap(e,{imageOrientation:"from-image"}).catch(()=>{throw new Es});try{let{x:s,y:o,side:a}=Kl(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((u,m)=>l.toBlob(p=>p?u(p):m(new Error("This browser can't prepare the image.")),"image/png")),width:n.width,height:n.height}}finally{n.close()}}function Gl(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 ${K(e.retry_after)}, so saving waits until it has sent.`:"";return`${t} ${s}${o}`}var xs=256;function na(e){let{state:t,fromDisplay:n,fromName:s}=e,o=!!t.address;return i`
|
|
620
622
|
<section class="set-sec">
|
|
621
|
-
${
|
|
623
|
+
${ue("Publication identity",ce("editable","Editable"))}
|
|
622
624
|
<div class="set-card">
|
|
623
625
|
<div class="set-id-grid">
|
|
624
626
|
<div class="set-logo-slot">
|
|
625
627
|
<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?
|
|
628
|
+
<span class="set-logo-ph" id="logoPh"${t.logoUrl?Nt:null}>${P("upload")}Upload</span>
|
|
627
629
|
</div>
|
|
628
630
|
<input type="file" id="logoInput" accept="image/png,image/jpeg,image/webp,image/gif" hidden>
|
|
629
631
|
<div class="set-logo-actions">
|
|
630
632
|
<button type="button" id="logoReplace">${t.logoUrl?"Replace":"Upload"}</button>
|
|
631
|
-
<button type="button" class="danger-subtle" id="logoRemove"${t.logoUrl?null:
|
|
633
|
+
<button type="button" class="danger-subtle" id="logoRemove"${t.logoUrl?null:Nt}>Remove</button>
|
|
632
634
|
</div>
|
|
633
635
|
<div class="set-logo-notes">
|
|
634
|
-
<p class="field-hint">Shown as a square. Pick an image at least ${String(
|
|
636
|
+
<p class="field-hint">Shown as a square. Pick an image at least ${String(xs)} × ${String(xs)} px; a wider or taller one is cropped to its center square.</p>
|
|
635
637
|
<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
638
|
<div class="field-error" id="logoError" role="alert" hidden><span class="field-error-ico" aria-hidden="true">!</span><span></span></div>
|
|
637
639
|
</div>
|
|
@@ -647,11 +649,11 @@
|
|
|
647
649
|
<input id="setTagline" value="${t.tagline}" placeholder="A one-line description" maxlength="200" autocomplete="off">
|
|
648
650
|
<p class="field-hint">A short line under the name on your public pages.</p>
|
|
649
651
|
</div>
|
|
650
|
-
<div class="set-field set-addr-add" id="addrAdd"${o?
|
|
651
|
-
<button type="button" class="ghost" id="addrAddBtn" aria-describedby="addrAddHint">${
|
|
652
|
+
<div class="set-field set-addr-add" id="addrAdd"${o?Nt:null}>
|
|
653
|
+
<button type="button" class="ghost" id="addrAddBtn" aria-describedby="addrAddHint">${P("plus")}Add a mailing address</button>
|
|
652
654
|
<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>
|
|
653
655
|
</div>
|
|
654
|
-
<div class="set-field" id="addrField"${o?null:
|
|
656
|
+
<div class="set-field" id="addrField"${o?null:Nt}>
|
|
655
657
|
<div class="set-field-head">
|
|
656
658
|
<label for="setAddress">Mailing address</label>
|
|
657
659
|
<button type="button" class="danger-subtle set-field-remove" id="addrRemove">Remove</button>
|
|
@@ -661,11 +663,11 @@
|
|
|
661
663
|
</div>
|
|
662
664
|
</div>
|
|
663
665
|
</div>
|
|
664
|
-
<div class="set-note">${
|
|
666
|
+
<div class="set-note">${P("info")}<span>${Gl(e.data.inUse)}</span></div>
|
|
665
667
|
</div>
|
|
666
|
-
</section>`}function
|
|
668
|
+
</section>`}function sa(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"),u=d("#addrAdd",e),m=d("#addrField",e),p=d("#addrWarn",e),y=()=>{u.hidden=r,m.hidden=!r,p.hidden=!(r&&n.address&&!c),p.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),E=d("#logoTile",e),N=d("#logoPh",e),A=d("#logoReplace",e),H=d("#logoRemove",e),h=d("#logoShape",e),L=b=>{if(h.hidden=!0,!b)return;let v=new Image;v.onload=()=>{h.hidden=n.logoUrl!==b||v.naturalWidth===v.naturalHeight},v.src=b},C=()=>{let b=!!n.logoUrl;L(n.logoUrl),E.classList.toggle("has-img",b),E.style.backgroundImage=b?`url('${n.logoUrl}')`:"",N.hidden=b,H.hidden=!b,A.textContent=b?"Replace":"Upload"};A.onclick=()=>x.click(),E.addEventListener("click",()=>x.click()),E.addEventListener("keydown",b=>{(b.key==="Enter"||b.key===" ")&&(b.preventDefault(),x.click())});let F=d("#logoError",e),ee=d("span:last-of-type",F),R=b=>{ee.textContent=b,F.hidden=!b},q=(b,v)=>{n.logoUrl=b.settings.publication.logoUrl||"",t.applySaved(b.settings),C(),t.changed(),$(mt(v,b.remade))},_=b=>{hn(b)?R(Re(b)):$(Re(b))};return x.onchange=async()=>{let b=x.files?.[0];if(x.value="",!!b){R("");try{let v=await ta(b,xs),S=v.width!==v.height,U=await Qe(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 k(`/api/settings/logo${re}`,{method:"POST",body:Y})},At("Upload","Uploading"));U&&q(U,S?"Logo updated, cropped to its center square":"Logo updated")}catch(v){_(v)}}},H.onclick=()=>J(H,"Removing\u2026",async()=>{R("");try{let b=await Qe(v=>{let S=v?`?remake=${encodeURIComponent(v.join(","))}`:"";return k(`/api/settings/logo${S}`,{method:"DELETE"})},At("Remove","Removing"));b&&q(b,"Logo removed")}catch(b){_(b)}}),C(),y(),{reset(){s.value=n.name,o.value=n.tagline,a.value=n.address,r=!!n.address,y()}}}function Yl(e,t){switch(e){case"cloudflare":return"Cloudflare Email, separate from your newsletter's provider";case"provider":return t==="none"?"Your email provider, once one is connected":`Your email provider (${Rn(t)})`;default:return"No email provider configured, so nothing is delivered"}}var Zl={finished:"went out",refused:"provider refusing",stuck:"in flight too long",wedged:"waiting to be resolved",missed:"missed fire time",test:"test"};function oa(e){return e.kind==="test"?"test notification":`${e.subject} (${Zl[e.kind]})`}function ks(e){let{lastSent:t,lastFailure:n}=e;return n?i`<span class="set-pill danger">Not delivered</span> <span class="muted">${K(n.at)}, ${oa(n)}: ${n.error}</span>`:t?i`<span class="set-pill ok">${P("check")}Delivered</span> <span class="muted">${K(t.at)}, ${oa(t)}</span>`:i`<span class="muted">None yet</span>`}function ia(e){let t=e.data.deployment;return i`
|
|
667
669
|
<section class="set-sec">
|
|
668
|
-
${
|
|
670
|
+
${ue("Notifications",ce("editable","Editable"))}
|
|
669
671
|
<p class="set-lede">Kestrel emails you when a send goes out, and right away if a send runs into a problem.</p>
|
|
670
672
|
<div class="set-card">
|
|
671
673
|
<div class="set-recip">
|
|
@@ -677,27 +679,27 @@
|
|
|
677
679
|
<div class="row"><button type="button" class="ghost" id="notifyTest">Send a test notification</button><span class="field-hint" id="notifyTestHint"></span></div>
|
|
678
680
|
</div>
|
|
679
681
|
<div class="set-kv" style="border-top:1px solid var(--line)">
|
|
680
|
-
<div class="set-kv-k">Sent through</div><div class="set-kv-v">${
|
|
682
|
+
<div class="set-kv-k">Sent through</div><div class="set-kv-v">${Yl(t.notifyChannel,t.provider)}</div>
|
|
681
683
|
<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">${
|
|
684
|
+
<div class="set-kv-k">Last notification</div><div class="set-kv-v" id="notifyStatus">${ks(e.data.notificationStatus)}</div>
|
|
683
685
|
</div>
|
|
684
|
-
<div class="set-note">${
|
|
686
|
+
<div class="set-note">${P("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>
|
|
685
687
|
</div>
|
|
686
|
-
</section>`}function
|
|
687
|
-
<h3 class="set-rp-jhead" id="rpJourney-${e.id}">${
|
|
688
|
+
</section>`}function aa(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=()=>J(s,"Sending\u2026",async()=>{try{let r=await k("/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,ks({lastSent:{kind:"test",subject:"",at:Date.now()},lastFailure:null}))}catch(r){$(Re(r)),g(a,ks({lastSent:null,lastFailure:{kind:"test",subject:"",at:Date.now(),error:Re(r)}}))}}),l(),{repaint:l,reset(){n.value=t.state.notifyTo,l()}}}var Ts={base:"app",path:"/subscribe"},Mn={base:"app",path:"/confirm?token=\u2026"},Ls={base:"app",path:"/unsubscribe?token=\u2026"},It=[{id:"subscribe",label:"Subscribe",flow:"steps",states:[{id:"form",label:"Form",at:Ts},{id:"check-inbox",label:"Check your inbox",at:Ts},{id:"not-an-email",label:"Not an email",offPath:!0,at:Ts}]},{id:"confirm",label:"Confirm",flow:"steps",states:[{id:"ready",label:"Ready",at:Mn},{id:"confirmed",label:"Confirmed",at:Mn},{id:"expired",label:"Expired",offPath:!0,at:Mn},{id:"invalid",label:"Invalid link",offPath:!0,at:Mn}]},{id:"unsubscribe",label:"Unsubscribe",flow:"steps",states:[{id:"ask",label:"Ask",at:Ls},{id:"done",label:"Unsubscribed",at:Ls},{id:"invalid",label:"Invalid link",offPath:!0,at:Ls}]},{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 ra(e,t){return`/api/reader-pages/${encodeURIComponent(e)}/${encodeURIComponent(t)}`}var zl={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.`},Jl={subscribe:"mail",confirm:"circle-check",unsubscribe:"circle-minus",reading:"book"},Ql=i`<span class="set-rp-dot"></span>`;function la(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 Xl(){return i`${It.map(e=>{let t=e.states,n=e.flow==="steps";return i`<div class="set-rp-journey" data-journey="${e.id}">
|
|
689
|
+
<h3 class="set-rp-jhead" id="rpJourney-${e.id}">${P(Jl[e.id])}${e.label}</h3>
|
|
688
690
|
<ul class="set-rp-list${n?" steps":""}" aria-labelledby="rpJourney-${e.id}">
|
|
689
|
-
${t.filter(s=>!s.offPath).map((s,o)=>
|
|
691
|
+
${t.filter(s=>!s.offPath).map((s,o)=>la(e,s,n?String(o+1):Ql))}
|
|
690
692
|
</ul>
|
|
691
693
|
<ul class="set-rp-list" aria-label="${e.label}, off the main path">
|
|
692
|
-
${t.filter(s=>s.offPath).map(s=>
|
|
694
|
+
${t.filter(s=>s.offPath).map(s=>la(e,s,P("branch")))}
|
|
693
695
|
</ul>
|
|
694
|
-
</div>`})}`}function
|
|
696
|
+
</div>`})}`}function ed(){return i`<select id="rpSelect" aria-label="Reader page">${It.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 da(e){return i`
|
|
695
697
|
<section class="set-sec set-rp-sec" id="readerPages">
|
|
696
698
|
${e}
|
|
697
699
|
<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
700
|
<div class="set-card set-rp">
|
|
699
|
-
<nav class="set-rp-nav" aria-label="Reader pages">${
|
|
700
|
-
<div class="set-rp-pick">${
|
|
701
|
+
<nav class="set-rp-nav" aria-label="Reader pages">${Xl()}</nav>
|
|
702
|
+
<div class="set-rp-pick">${ed()}</div>
|
|
701
703
|
<div class="set-rp-main">
|
|
702
704
|
<div class="set-rp-top">
|
|
703
705
|
<div>
|
|
@@ -705,8 +707,8 @@
|
|
|
705
707
|
<p class="set-rp-note" id="rpNote"></p>
|
|
706
708
|
</div>
|
|
707
709
|
<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">${
|
|
709
|
-
<button type="button" class="wtog-btn" data-width="phone" aria-pressed="false">${
|
|
710
|
+
<button type="button" class="wtog-btn" data-width="desktop" aria-pressed="true">${P("desktop")}Desktop</button>
|
|
711
|
+
<button type="button" class="wtog-btn" data-width="phone" aria-pressed="false">${P("phone")}Phone</button>
|
|
710
712
|
</span>
|
|
711
713
|
</div>
|
|
712
714
|
<div class="set-rp-at"><span class="mono" id="rpAt"></span><span>Sample · links and forms do nothing</span></div>
|
|
@@ -716,7 +718,7 @@
|
|
|
716
718
|
</div>
|
|
717
719
|
</div>
|
|
718
720
|
</div>
|
|
719
|
-
</section>`}function
|
|
721
|
+
</section>`}function td(e,t){return`${e.base==="app"?t.appOrigin:`${t.archiveOrigin}${t.archiveBasePath}`}${e.path}`.replace(/^https?:\/\//,"")}function ca(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),u=It[0],m=u.states[0],p="desktop",y=0,x=null;Ye(n,()=>x?.disconnect());let E=()=>{for(let h of M(".set-rp-item",s)){let L=h.dataset.journey===u.id&&h.dataset.state===m.id;h.setAttribute("aria-current",L?"true":"false")}for(let h of M(".set-rp-journey",s))h.classList.toggle("current",h.dataset.journey===u.id);o.value=`${u.id}/${m.id}`,d("#rpJourney",e).textContent=u.label,d("#rpState",e).textContent=m.label,g(d("#rpNote",e),zl[u.id]),d("#rpAt",e).textContent=td(m.at,t)},N=()=>{let h=r.contentDocument?.body?.scrollHeight;h&&(r.style.height=`${h}px`)},A=async()=>{let h=++y;try{let L=await et(ra(u.id,m.id),{signal:n});if(h!==y||n.aborted)return;c.hidden=!0,l.hidden=!1,r.srcdoc=En(L)}catch(L){if(h!==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=(h,L)=>{let C=It.find(R=>R.id===h),ee=(C?.states??[]).find(R=>R.id===L);!C||!ee||C===u&&ee===m||(u=C,m=ee,E(),A())};return r.onload=()=>{let h=r.contentDocument;!h||n.aborted||(h.addEventListener("click",L=>{L.target?.closest?.("a[href], area[href]")&&L.preventDefault()}),x?.disconnect(),N(),h.body&&typeof ResizeObserver<"u"&&(x=new ResizeObserver(N),x.observe(h.body)))},s.addEventListener("click",h=>{let L=h.target.closest(".set-rp-item");H(L?.dataset.journey,L?.dataset.state)}),o.addEventListener("change",()=>{let[h,L]=o.value.split("/");H(h,L)}),a.addEventListener("click",h=>{let C=h.target.closest("button[data-width]")?.dataset.width;if(!(C!=="desktop"&&C!=="phone"||C===p)){p=C;for(let F of M("button",a))F.setAttribute("aria-pressed",String(F.dataset.width===C));l.classList.toggle("phone",C==="phone")}}),E(),A(),{refresh:()=>{A()}}}function pa(e){return e?`Subscribe to ${e}`:"Subscribe"}function nd(e,t,n){let s=Q(pa(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
722
|
<div style="font:600 22px/1.2 Georgia,'Times New Roman',serif;letter-spacing:-0.01em;margin-bottom:12px">${s}</div>
|
|
721
723
|
<div style="display:flex;gap:10px;flex-wrap:wrap">
|
|
722
724
|
<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">
|
|
@@ -729,13 +731,14 @@
|
|
|
729
731
|
<input type="email" name="email" placeholder="you@example.com" required>
|
|
730
732
|
</label>
|
|
731
733
|
<button type="submit">Subscribe</button>
|
|
732
|
-
</form>`}function
|
|
734
|
+
</form>`}function sd(e,t){let n=pa(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 ma(e){return e.data.deployment.appOrigin||location.origin}function od(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 ua(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">${P(e)}${t}</h3><p class="set-way-line">${n}</p></div>${o}</div>${s}</div>`}function fa(e){let t=`${ma(e)}/subscribe`;return i`
|
|
733
735
|
<section class="set-sec set-sec-subscribe">
|
|
734
|
-
${
|
|
736
|
+
${ue("Ways to subscribe",ce("copyout","Copy-out"))}
|
|
735
737
|
<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>
|
|
738
|
+
${e.data.deployment.provider==="none"?i`<div class="health amber set-sub-not-connected" role="status"><span class="health-dot">⚠️</span><div><span>No email provider connected, so signups aren’t open yet: the page below says so, and the form takes no address. ${_e()} to open signups.</span></div></div>`:null}
|
|
736
739
|
<div class="set-card">
|
|
737
|
-
${
|
|
738
|
-
${
|
|
740
|
+
${ua("link","Share the page",i`<span id="subscribePageLine"></span>`,i`<div class="set-way-body set-way-addr"><code class="set-addr">${t}</code>${ft(t,"Copy the subscribe page's address")}<a class="ghost-link" href="${t}" target="_blank" rel="noopener">Open ↗</a></div>`)}
|
|
741
|
+
${ua("code","Embed a form",i`Put a sign-up form on another site you own.`,i`<div class="set-embed">
|
|
739
742
|
<div class="set-embed-preview" id="embedPreview"></div>
|
|
740
743
|
<pre class="set-embed-code"><code id="embedCode"></code></pre>
|
|
741
744
|
</div>
|
|
@@ -744,16 +747,16 @@
|
|
|
744
747
|
<button type="button" class="wtog-btn" data-embed="plain" aria-pressed="false">Plain</button>
|
|
745
748
|
</span>`)}
|
|
746
749
|
</div>
|
|
747
|
-
</section>`}function
|
|
748
|
-
${
|
|
749
|
-
${
|
|
750
|
-
${
|
|
751
|
-
${
|
|
752
|
-
${
|
|
753
|
-
`),
|
|
750
|
+
</section>`}function id(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">${Ie(!1,"Copy HTML")}</button>`}function ha(e,t){let n=ma(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,u=()=>{let y=nd(r,c(),n);s.textContent=y,g(o,id(r,y)),g(a,sd(r,c())),l.textContent=od(c())},m=M("[data-embed]",e);for(let y of m)y.onclick=()=>{r=y.dataset.embed==="styled"?"styled":"plain";for(let x of m)x.setAttribute("aria-pressed",String(x.dataset.embed===r));u()};ht(d(".set-sec-subscribe",e)),u();let p=c();return{repaint(){c()!==p&&(p=c(),u())}}}function Dt(e,t){return i`<div class="set-group" data-group="${e}">${t}</div>`}function Hs(e){return{name:e.name,tagline:e.tagline,address:e.address,recipients:[...e.recipients],confirmation:{...e.confirmation},notifyTo:e.notifyTo}}var ad=(e,t)=>e.length===t.length&&e.every((n,s)=>n===t[s]),rd=(e,t)=>e.subject===t.subject&&e.body===t.body&&e.buttonLabel===t.buttonLabel&&e.reassurance===t.reassurance;async function Cs(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 k("/api/settings",{signal:t})}catch(h){X(n,Re(h),()=>G(Cs));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=Hs(r),u=Kn(s.deployment.fromAddress),m=()=>r.name!==c.name||r.tagline!==c.tagline||r.address!==c.address||!ad(r.recipients,c.recipients)||!rd(r.confirmation,c.confirmation)||r.notifyTo!==c.notifyTo,p=[],y=Cn.attach({onSave:A,onDiscard:H},t),x=()=>{},E={data:s,state:r,saved:()=>c,fromDisplay:u,fromName:u||"Your publication",changed(){for(let h of p)h.repaint?.();y.setDirty(m())},applySaved(h){W.appConfig={...s,settings:h},Yt(),x()}};g(n,i`
|
|
751
|
+
${Dt("publication",[na(E)])}
|
|
752
|
+
${Dt("readers",[fa(E),Fi(E),da(ue("Reader pages",ce("preview","Preview")))])}
|
|
753
|
+
${Dt("emails",[Ji(E),Xi()])}
|
|
754
|
+
${Dt("you",[ia(E)])}
|
|
755
|
+
${Dt("deploy",[Wi(E),Ki(E)])}
|
|
756
|
+
`),p.push(sa(n,E),ha(n,E),Vi(n,E),Qi(n,E),ea(n,E),aa(n,E)),x=ca(n,s.deployment,t).refresh;async function A(){try{let h={publication:{name:r.name,tagline:r.tagline,address:r.address},testRecipients:r.recipients,confirmationEmail:r.confirmation,notifications:{to:r.notifyTo}},L=await Qe(F=>k("/api/settings",{method:"PUT",json:F?{...h,remake:F}:h}),At("Save","Saving"));if(!L)return;let C=L.settings;r.name=C.publication.name,r.tagline=C.publication.tagline,r.address=C.publication.address,r.recipients=[...C.testRecipients],r.confirmation={...C.confirmationEmail},r.notifyTo=C.notifications.to,c=Hs(r);for(let F of p)F.reset?.();E.applySaved(C),E.changed(),$(mt("Settings saved",L.remade))}catch(h){hn(h)?y.showError(Re(h)):$(Re(h))}}function H(){Object.assign(r,Hs(c));for(let h of p)h.reset?.();E.changed()}E.changed()}var ld=[{value:"confirmed",label:"Confirmed"},{value:"pending",label:"Pending"},{value:"unsubscribed",label:"Unsubscribed"}];async function va(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`
|
|
754
757
|
<div class="spread page-head"><h1>Subscribers</h1><button class="primary" id="addSub">Add subscriber</button></div>
|
|
755
758
|
<div id="subCounts" class="muted">Loading…</div>
|
|
756
|
-
${
|
|
759
|
+
${dt({statuses:ld,suppressible:!0,searchPlaceholder:"Search email\u2026"})}
|
|
757
760
|
<div id="subList" class="muted">Loading…</div>
|
|
758
|
-
<div id="subPager"></div>`);let o=d("#subList"),a=d("#subPager");async function l(){try{let r=await
|
|
761
|
+
<div id="subPager"></div>`);let o=d("#subList"),a=d("#subPager");async function l(){try{let r=await k(`/api/subscribers?${lt(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>${dn("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>`),pd(o,r.subscribers,s,l),je(a,s,r.page,l)}catch(r){X(o,r instanceof Error?r.message:String(r),l)}}d("#addSub").onclick=()=>bn(l),ct(t,s,l),l()}var dd={bounce:"bounced",complaint:"complaint",manual:"blocked"},cd={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 ud(e){if(!e.suppressed)return null;let t=e.suppression_reason??"",n=dd[t]||"suppressed",s=cd[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 pd(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>${he("Email","email",n)}${he("Status",null,n)}${he("Joined","joined",n)}<th></th></tr></thead><tbody>${t.map(o=>i`<tr data-id="${o.id}"><td>${o.email}${ud(o)}</td><td>${Je(o.status)}</td><td class="muted joined">${K(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>`),Ue(e,n,s);for(let o of M("[data-menu]",e))o.onclick=a=>{a.stopPropagation();let l=t.find(r=>r.id===o.dataset.menu);l&&cn(o,[{label:"Unsubscribe",danger:!0,onClick:()=>Yo(l,s)}])}}var md="You have unsaved changes. Leave without saving?",Rs="";function Ps(){let e=location.hash||"#/dashboard";Rs=e;let[,t,n,s]=(Zn(e.slice(1))?e:"#/dashboard").split("/");document.body.classList.toggle("editor-mode",t==="edit");let o=t==="web"?ki(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 xt(!1),t==="edit"&&n?G((l,r)=>us(n,l,r)):o!==null?G((l,r)=>cs(o,l,r)):t==="drafts"?G(ss):t==="subscribers"?G((l,r)=>va(n,l,r)):t==="sent"?n?G((l,r)=>ys(n,l,r)):G(vs):t==="template"?G($s):t==="settings"?G(Cs):t==="reference"?G(hs):t==="docs"?G((l,r)=>ms(n,l,r,s)):G(rs)}function ga(){let e=Bo(location.search);if(e){let t=new URL(location.href);t.search="",t.hash=e,history.replaceState(null,"",t)}}function ba(){window.addEventListener("hashchange",()=>{if(location.hash!==Rs){if(jt()?.beforeLeave?.()==="confirm"&&!confirm(md)){location.hash=Rs;return}Ps()}}),window.addEventListener("beforeunload",e=>{jt()?.dirty?.()&&(e.preventDefault(),e.returnValue="")}),window.addEventListener("keydown",e=>{let t=jt()?.manualSave;(e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==="s"&&t&&(e.preventDefault(),t())})}async function fd(){qs(),Ko(),No(),ba();let e=await Ys();if(e){W.session=e,document.body.classList.remove("signed-out"),Gs();try{W.appConfig=await k("/api/settings")}catch{}return Yt(),ga(),Ps()}}fd();})();
|
|
759
762
|
//# sourceMappingURL=app.js.map
|