@moorage/plugin-site-health 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @moorage/plugin-site-health
2
2
 
3
+ ## 0.1.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 8d9ae31: Site Health now stays within the sandbox's limit of 10 calls to the site per run, with one to spare, so no step can fail halfway on Cloudflare. The last step of a full check is split in two. Settings saved by 0.1.2 are copied to their new place only when an Admin opens the Site Health page; when that happens, the check moves on at the next page load. Links to other sites that turn the first kind of request away are asked again on their next turn when a run has no calls left. Switching the plugin on schedules its background task once instead of twice.
8
+
9
+ ## 0.1.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 961b4b9: Who gets emails, and whether links to other websites are checked, now stay put when your site moves the plugin between running in-process (`plugins: []`) and sandboxed (`sandboxed: []`). EmDash keeps most plugin data in a different place in each mode, so before this a switch emptied the email list (the first Admin to open the page then became the only person on it) and turned the outside-link check back on. These options are now kept where EmDash finds them in both modes.
14
+
15
+ Nothing to do on your side: the next check or visit to the Site Health page after updating copies them to the new place. Update first, open the page once, and only then switch modes. Each save also keeps a copy where earlier versions look, so going back to 0.1.2 still uses your latest options. The check results themselves are rebuilt by the next full check after a switch.
16
+
3
17
  ## 0.1.2
4
18
 
5
19
  ### Patch Changes
package/README.md CHANGED
@@ -62,7 +62,7 @@ import siteHealth from "@moorage/plugin-site-health";
62
62
  emdash({ sandboxed: [siteHealth] });
63
63
  ```
64
64
 
65
- On a site without a sandbox runner (the Workers free plan), list it under `plugins: [siteHealth]` instead. It works the same.
65
+ On a site without a sandbox runner (the Workers free plan), list it under `plugins: [siteHealth]` instead. It works the same. Since 0.1.3 the email list and the outside-link choice survive moving the plugin from one list to the other (the check results are rebuilt by the next check); when updating from an earlier version, update first, open the Site Health page once, then move it.
66
66
 
67
67
  **On Cloudflare's Workers free plan** each request gets about 10 ms of CPU. EmDash's own saving and publishing already comes close to that, and every plugin run in the site's process adds a little, so a save or publish can now and then fail with "Worker exceeded resource limits"; trying again works. Workers Paid (30 seconds per request) does not have this problem.
68
68
 
@@ -90,7 +90,7 @@ Then open **Tools > Site Health** in the admin sidebar.
90
90
 
91
91
  The numbers on the page are always a finished check. A check that is still running never changes them.
92
92
 
93
- Each run stays within the EmDash sandbox's limits of about 10 calls and 50 ms of CPU. The scheduled task runs every minute.
93
+ Each run stays within the EmDash sandbox's limits of 10 calls (it makes at most 9) and about 50 ms of CPU. The scheduled task runs every minute.
94
94
 
95
95
  **If the site cannot open its own pages** (not public yet, bot protection, or a host that blocks a site from fetching itself), it says so in one banner. If your site is in the middle of an update and shows every visitor an error page, it says that instead, and emails at once. Links inside your content are still checked, and anything that needed the live page is listed as "could not check", never as broken.
96
96
 
package/dist/index.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  /** @type {import("emdash").PluginDescriptor} */
8
8
  const descriptor = Object.freeze({
9
9
  "id": "site-health",
10
- "version": "0.1.2",
10
+ "version": "0.1.4",
11
11
  "format": "standard",
12
12
  "entrypoint": "@moorage/plugin-site-health/sandbox",
13
13
  "capabilities": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "site-health",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "declaredAccess": {
5
5
  "content": {
6
6
  "read": {}
package/dist/plugin.mjs CHANGED
@@ -1,11 +1,11 @@
1
- const e=`9999`;function t(e){return e.menu?`site`:e.kind===`page`?e.url===`/`?`site`:`page`:e.kind===`insecure`?`page`:`link`}function n(e,t){switch(e.reason){case`missing`:return`${t} goes to a page that does not exist.`;case`draft`:return`${t} goes to a page that is not published yet.`;case`gone`:return`${t}: that other website has removed this page.`;case`no-site`:return`${t}: that website no longer exists.`;case`server`:return`${t}: that website has shown an error for a week.`;case`page-error`:return`${t}: this page is broken; visitors see an error.`;case`insecure-active`:return`${t} is loaded over an insecure connection. Browsers block it, so visitors see an empty box.`;case`redirect`:case`moved`:return`${t} now goes on to ${e.detail??`another address`}.${e.kind===`internal`&&e.detail?` Linking to ${e.detail} directly saves a step.`:``}`;case`insecure-image`:return`${t} is a picture shown over an insecure connection. Browsers fix this themselves for now.`;case`refuses`:return`${t}: that website turns away automatic checkers.`;case`slow`:return`${t}: that website asked us to slow down. Trying again tomorrow.`;case`timeout`:return`${t} did not answer within 8 seconds.`;case`trouble`:return`${t}: that website had a problem when we checked. Trying again.`;case`no-live`:return`${t}: we could not open your site to check this address.`;default:return`${t}: we could not open it.`}}function r(e){if(e.kind===`page`)return``;let t=[];e.menu&&t.push(`Site menu (every page)`);for(let n of e.sources)if(!n.menu){if(t.length>=4)break;t.push(n.t)}let n=t.length-(e.menu?1:0),r=e.count-(e.menu?e.sources.filter(e=>e.menu).length:0)-n;return t.length===0?``:` Found on: ${t.join(`, `)}${r>0?` and ${r} more`:``}.`}const i={scan:`scan`,summary:`summary`,queue:`queue`,options:`options`,alerts:`alerts`,checklist:`checklist`};async function a(e){let t=await e.kv.list(),n=new Map(t.map(e=>[e.key,e.value])),r=n.get(i.options)??{},a=n.get(i.alerts)??{},o=n.get(i.queue)??{},s=n.get(i.checklist)??{};return{scan:n.get(i.scan)??null,summary:n.get(i.summary)??null,queue:{hosts:{},...o},options:{outbound:!0,...r},alerts:{pending:[],sent:[],...a},checklist:{rows:{},...s}}}function o(e,t){return{id:e,kind:t.kind,url:t.url,...t.reason?{reason:t.reason}:{},...t.detail?{detail:t.detail}:{},sources:t.sources.slice(0,5),count:t.count,menu:t.menu,editor:t.editor,...t.since?{since:t.since}:{}}}function s(e,t){return t.count-e.count}function c(e,t){return e.filter(e=>e.id!==t)}function l(e,t,n,r){let i=n?.state,a=r?.state,l=n?.group;if(i===a&&l===r?.group&&n?.reason===r?.reason)return null;let u={...e,fix:{...e.fix},rows:{site:c(e.rows.site,t),page:c(e.rows.page,t),link:c(e.rows.link,t)},lookRows:c(e.lookRows,t),unknownRows:c(e.unknownRows,t)};return i===`broken`&&l&&(u.fix[l]=Math.max(0,u.fix[l]-1),n?.editor&&(u.editorFix=Math.max(0,u.editorFix-1))),i===`look`&&(u.look=Math.max(0,u.look-1)),i===`unknown`&&(u.unknown=Math.max(0,u.unknown-1)),r&&a===`broken`&&r.group&&(u.fix[r.group]+=1,r.editor&&(u.editorFix+=1),u.rows[r.group]=[...u.rows[r.group],o(t,r)].sort(s).slice(0,10)),r&&a===`look`&&(u.look+=1,u.lookRows=[...u.lookRows,o(t,r)].slice(0,10)),r&&a===`unknown`&&(u.unknown+=1,u.unknownRows=[...u.unknownRows,o(t,r)].slice(0,10)),u}function u(e){return e.fix.site+e.fix.page+e.fix.link}function d(e,t){try{return new URL(e,t)}catch{return null}}const f=/^(mailto|tel|sms|javascript|data|blob|about|ftp):/i;function p(e){return e.startsWith(`/_emdash/`)||e.startsWith(`/_astro/`)||e===`/_emdash`}function m(e){let t=e;try{t=decodeURI(e)}catch{}let n=t.replace(/\/{2,}/g,`/`);return n.length>1&&n.endsWith(`/`)?n.slice(0,-1):n||`/`}function h(e,t){let n=e.trim();if(n===``||n.startsWith(`#`)||n.startsWith(`?`)||f.test(n)||/\s/.test(n)&&!/^https?:\/\//i.test(n))return null;let r=d(t),i=/^https?:\/\//i.test(n)||n.startsWith(`//`);if(!i&&!n.startsWith(`/`))return null;let a=d(n.startsWith(`//`)?`https:${n}`:n,`https://site.invalid`);if(!a||a.protocol!==`https:`&&a.protocol!==`http:`)return null;let o=a.hostname.toLowerCase();if(!i||r!==null&&o.replace(/^www\./,``)===r.hostname.replace(/^www\./,``)){let e=m(a.pathname);return p(e)?null:{url:e,internal:!0,host:``,insecure:i&&a.protocol===`http:`}}return a.hash=``,{url:a.href,internal:!1,host:o,insecure:a.protocol===`http:`}}function g(e,t){let n=2166136261,r=16777619,i=`${e}|${t}`;for(let e=0;e<i.length;e+=1){let t=i.charCodeAt(e);n=Math.imul(n^t,16777619)>>>0,r=Math.imul(r^t,1540483477)>>>0}return`${e[0]}${n.toString(36)}${(r&1048575).toString(36)}`}function _(e,t=30){let n=e.replace(/^https?:\/\//i,``).replace(/^www\./i,``).replace(/\/$/,``);return n.length>t?`${n.slice(0,t-1)}…`:n}function v(e){let t=d(e);if(!t||t.protocol!==`https:`&&t.protocol!==`http:`)return null;let n=t.hostname.toLowerCase().replace(/\.$/,``);return n===`localhost`||n.endsWith(`.localhost`)||n.endsWith(`.local`)||/^(127|10|0)\.|^192\.168\.|^169\.254\.|^172\.(1[6-9]|2\d|3[01])\./.test(n)||n===`[::1]`||n.startsWith(`[fc`)||n.startsWith(`[fd`)?null:t.origin}const y=864e5;7*y;function b(e){return`${n(e,_(e.url))}${r(e)}`}function x(e){return e.menu||e.reason===`page-error`}function ee(e){let t=v(e.site.url);return t?`${t}/_emdash/admin/plugins/${e.plugin.id}/health`:null}function te(e){let t=[];for(let n of e.split(/[\s,;]+/)){let e=n.trim();if(e!==``){if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e))return{wrong:e};t.includes(e.toLowerCase())||t.push(e.toLowerCase())}}return{list:t.slice(0,20)}}const S=/no email provider|not configured/i;function C(e,t,n,r){return e.ok?{state:`working`,at:n,to:t.length,list:t,kind:r}:{state:S.test(e.error)?`not-set-up`:`failed`,at:n,to:0,list:t,kind:r,error:e.error.slice(0,200)}}const ne=`updating|`;async function w(e,t,n,r){let[i,...a]=t;if(!i)return{ok:!1,error:`no addresses`};if(!e.email)return{ok:!1,error:`Email is not configured`};try{return await e.email.send({to:i,...a.length>0?{cc:a}:{},subject:n,text:r}),{ok:!0}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}function re(e){return e.site.name.trim()||v(e.site.url)?.replace(/^https?:\/\//,``)||`Your site`}async function ie(e,t,n,r){let a=[...n.rows.site,...n.rows.page].filter(x).map(e=>({id:e.id,text:b(e)}));n.live===`updating`&&a.unshift({id:`${ne}${n.pass}`,text:`Your site showed visitors an error page instead of your pages: an update to its database has not finished. Whoever looks after your site should finish the update.`});let o=a.filter(e=>!t.alerts.sent.includes(e.id));if(o.length===0)return;let s={...t.alerts,pending:[...t.alerts.pending,...o.map(e=>({id:e.id,text:e.text}))].slice(-20),sent:[...t.alerts.sent,...o.map(e=>e.id)].slice(-200)};t.alerts=s,await e.kv.set(i.alerts,s)}function ae(e,t){if((e.options.recipients??[]).length===0||!e.summary)return!1;let{alerts:n}=e;return n.pending.length>0&&(!n.urgentAt||t-Date.parse(n.urgentAt)>=y)||!n.weeklyAt||t-Date.parse(n.weeklyAt)>=6048e5}async function oe(e,t,n){let r=t.options.recipients??[],a={...t.alerts},o=re(e),s=ee(e),c=new Date(n).toISOString();if(a.pending.length>0&&(!a.urgentAt||n-Date.parse(a.urgentAt)>=y)){let t=a.pending.some(e=>e.id.startsWith(ne)),n=a.pending.some(e=>/visitors see an error/.test(e.text)),i=await w(e,r,`${o}: ${t?`your site is showing visitors an error`:n?`a page on your site shows an error`:`a link in your site menu is broken`}`,[`Site Health found something visitors will hit:`,``,...a.pending.map(e=>`- ${e.text}`),``,s?`See them and fix them: ${s}`:`See them in Site Health in your site's admin.`,``,`You get this because your address is on Site Health's list. At most one of these a day. Change the list in Site Health, Checklist, Options.`].join(`
2
- `));a.urgentAt=c,a.pending=[],a.email=C(i,r,c,`alert`)}else if(!a.weeklyAt)a.weeklyAt=c;else if(t.summary){let n=a.weeklyAt,i=[...t.summary.rows.site,...t.summary.rows.page,...t.summary.rows.link].filter(e=>e.since&&e.since>n);if(a.weeklyAt=c,i.length>0){let n=t.summary.fix.site+t.summary.fix.page+t.summary.fix.link;a.email=C(await w(e,r,`${o}: ${i.length} new ${i.length===1?`problem`:`problems`} this week`,[`New since last week:`,``,...i.map(e=>`- ${b(e)}`),``,`${n} ${n===1?`thing`:`things`} to fix in all. ${s?`See them: ${s}`:`See them in Site Health in your site's admin.`}`,``,`Sent once a week, only when something is new.`].join(`
3
- `)),r,c,`alert`)}}t.alerts=a,await e.kv.set(i.alerts,a)}async function se(e,t,n){let r=t.options.recipients??[],a=await w(e,r,`${re(e)}: test email from Site Health`,`This is a test from Site Health. If you can read it, alerts about broken links and pages that show errors will reach you.`),o=new Date(n).toISOString();return t.alerts={...t.alerts,email:C(a,r,o,`test`)},await e.kv.set(i.alerts,t.alerts),a}const ce=/^(href|url|link|src)$|(_url|Url|_href|Href|_link|Link)$/,le=/iframe|embed|video|map|script|audio|form/i,ue=/image|picture|photo/i;function T(e,t){let n=new Map,r=0,i=(e,r)=>{let i=h(e,t);i&&n.set(`${r}|${i.url}`,{...i,use:r})},a=(e,t,n)=>{if(r+=1,r>5e3||n>30||typeof e!=`object`||!e)return;if(Array.isArray(e)){for(let r of e)a(r,t,n+1);return}let o=e,s=typeof o._type==`string`?o._type:t;for(let[e,t]of Object.entries(o))if(typeof t==`string`){if(!ce.test(e)||t.length>2e3)continue;i(t,e===`src`||e.endsWith(`Src`)||e.endsWith(`_src`)?ue.test(s)?`image`:le.test(s)?`embed`:`link`:`link`)}else t&&typeof t==`object`&&a(t,s,n+1)};return a(e,``,0),[...n.values()]}const E=2e5,de=/<a\b[^>]*?\bhref\s*=\s*("([^"]*)"|'([^']*)')/gi,fe=/<(header|nav|footer)\b[\s\S]*?<\/\1\s*>/gi,pe=/<(iframe|script|embed|object|video|audio|source)\b[^>]*?\bsrc\s*=\s*("http:[^"]*"|'http:[^']*')|<link\b(?=[^>]*\brel\s*=\s*["']?stylesheet)[^>]*?\bhref\s*=\s*("http:[^"]*"|'http:[^']*')|<form\b[^>]*?\baction\s*=\s*("http:[^"]*"|'http:[^']*')/gi,me=/<img\b[^>]*?\bsrc\s*=\s*("http:[^"]*"|'http:[^']*')/gi;function he(e){return e.slice(1,-1).replaceAll(`&amp;`,`&`)}function D(e,t){let n=e.length>E?e.slice(0,E):e,r=new Map;for(let e of n.matchAll(fe))for(let n of e[0].matchAll(de)){let e=h((n[2]??n[3]??``).replaceAll(`&amp;`,`&`),t);e&&r.set(e.url,e)}let i=new Set;for(let e of n.matchAll(pe)){let t=e[2]??e[3]??e[4];t&&i.add(he(t))}let a=new Set;for(let e of n.matchAll(me))e[1]&&a.add(he(e[1]));return{menu:[...r.values()],active:[...i],passive:[...a]}}function ge(e,t,n){if(!n.slug)return null;let r=e??`/${encodeURIComponent(t)}/{slug}`,i=n.publishedAt?new Date(n.publishedAt):null,a=e=>String(e).padStart(2,`0`),o=i&&!Number.isNaN(i.getTime())?{year:String(i.getUTCFullYear()),month:a(i.getUTCMonth()+1),day:a(i.getUTCDate()),hour:a(i.getUTCHours()),minute:a(i.getUTCMinutes()),second:a(i.getUTCSeconds())}:{},s=r.replaceAll(`{slug}`,encodeURIComponent(n.slug)).replaceAll(`{id}`,encodeURIComponent(n.id)).replace(/\{(year|month|day|hour|minute|second)\}/g,(e,t)=>o[t]??e);if(s.includes(`{`))return null;s=s.replace(/\/{2,}/g,`/`),s.length>1&&s.endsWith(`/`)&&(s=s.slice(0,-1)),s.startsWith(`/`)||(s=`/${s}`);try{return decodeURI(s)}catch{return s}}const O=36e5,k=24*O;function _e(e){return`EmDash-SiteHealth/0.1 (+${e||`unknown site`})`}async function A(e,t,n={}){let r=e.http;if(!r)return{error:`no network`};let i,a=new Promise(e=>{i=setTimeout(()=>e({timedOut:!0}),8e3)}),o=(async()=>{try{let i=await r.fetch(t,{method:n.method??`GET`,redirect:n.follow===!1?`manual`:`follow`,headers:{"User-Agent":_e(e.site.url),Accept:`text/html,*/*;q=0.5`}}),a=i.headers.get(`location`),o={status:i.status,headers:i.headers,...a?{location:d(a,t)?.href??a}:{},...i.redirected&&i.url?{finalUrl:i.url}:{}};if(n.body){let e=await i.text();o.body=e.length>E?e.slice(0,E):e}return o}catch(e){return{error:e instanceof Error?e.message:String(e)}}})();try{return await Promise.race([o,a])}finally{clearTimeout(i)}}function ve(e){return/internal host|not allowed to fetch|private|localhost/i.test(e.error??``)}const ye=/ENOTFOUND|EAI_AGAIN|getaddrinfo|DNS|name not resolved|could not resolve/i;function be(e,t){let n=e=>e.replace(/^https?:\/\//i,``).replace(/^www\./i,``).replace(/\/$/,``);return n(e)===n(t)}function j(e,t,n){if(t.timedOut)return{kind:`unknown`,reason:`timeout`};if(t.error!==void 0)return ye.test(t.error)?{kind:`dead`}:{kind:`unknown`,reason:`error`};let r=t.status??0;return r>=300&&r<400&&t.location?be(e,t.location)?{kind:`ok`}:{kind:`moved`,to:t.location}:r>=200&&r<300?t.finalUrl&&!be(e,t.finalUrl)?{kind:`moved`,to:t.finalUrl}:{kind:`ok`}:n===`HEAD`&&[400,403,404,405,501].includes(r)?{kind:`retry`}:r===404||r===410?{kind:`gone`}:r===429?{kind:`unknown`,reason:`slow`}:r>=500&&r<600?{kind:`server`}:[401,403,407,451,999].includes(r)||r>=400?{kind:`unknown`,reason:`refuses`}:{kind:`unknown`,reason:`error`}}function M(e,t){return new Date(e+t).toISOString()}function N(e,n,r){let i={...e,state:`broken`,reason:n,since:e.state===`broken`&&e.since?e.since:new Date(r).toISOString()};return i.group=t(i),i}function P(e){let{fails:t,firstFailAt:n,since:r,group:i,detail:a,reason:o,...s}=e;return s}function xe(e,t,n){let r=new Date(n).toISOString(),i={...e,checkedAt:r};switch(t.kind){case`ok`:return{...P(i),state:`ok`,due:M(n,7*k)};case`moved`:return{...P(i),state:`look`,reason:`moved`,detail:_(t.to),due:M(n,7*k)};case`gone`:case`dead`:{let a=t.kind===`gone`?`gone`:`no-site`,o=e.firstFailAt?Date.parse(e.firstFailAt):NaN;return(e.fails??0)>=1&&n-o>=k-O?{...N(i,a,n),fails:(e.fails??0)+1,due:M(n,7*k)}:{...i,state:e.state===`broken`?`broken`:`pending`,fails:(e.fails??0)+1,firstFailAt:e.firstFailAt??r,due:M(n,k)}}case`server`:{let t=e.firstFailAt?Date.parse(e.firstFailAt):NaN;return(e.fails??0)>=2&&n-t>=7*k-O?{...N(i,`server`,n),fails:(e.fails??0)+1,due:M(n,7*k)}:{...i,state:e.state===`broken`?`broken`:`unknown`,reason:e.state===`broken`?e.reason:`trouble`,fails:(e.fails??0)+1,firstFailAt:e.firstFailAt??r,due:M(n,3*k)}}case`unknown`:case`retry`:{let r=t.kind===`unknown`?t.reason:`refuses`;return e.state===`broken`?{...i,due:M(n,k)}:{...i,state:`unknown`,reason:r,due:M(n,r===`slow`||r===`timeout`?k:7*k)}}}}function Se(e,t,n){let r=new Date(n).toISOString(),i={...e,checkedAt:r,due:`9999`};switch(t.kind){case`ok`:return{...P(i),state:`ok`};case`moved`:{let e=d(t.to);return{...P(i),state:`look`,reason:`redirect`,detail:_(e?e.pathname:t.to)}}case`gone`:case`retry`:return N(i,`missing`,n);default:return{...P(i),state:`unknown`,reason:`no-live`}}}function F(e,t,n){let r=new Date(n).toISOString(),i=t.status??0;if(i>=500){let t=e.firstFailAt?Date.parse(e.firstFailAt):NaN;return(e.fails??0)>=1&&n-t>=5e4?{...N({...e,checkedAt:r},`page-error`,n),fails:(e.fails??0)+1,due:M(n,k)}:{...e,checkedAt:r,state:e.state===`broken`?`broken`:`pending`,fails:(e.fails??0)+1,firstFailAt:e.firstFailAt??r,due:M(n,6e4)}}return t.status!==void 0&&i<400?{...P({...e,checkedAt:r}),state:`ok`,due:`9999`}:{...P({...e,checkedAt:r}),state:`unknown`,reason:`no-live`,due:`9999`}}async function Ce(e,t,n,r){let a=n.checklist.checkedAt?Date.parse(n.checklist.checkedAt):0;if(t.live===`ok`&&r-a<6048e5)return;let o={},s=d(e.site.url),c=v(e.site.url),l={status:`unknown`,text:t.live===`updating`?`Your site was showing visitors an error while an update finished, so we could not find out.`:`We could not open your site to find out.`};if(o.caching=t.caching===`ok`?{status:`working`,text:`Pages can be kept ready for visitors instead of built on every visit.`}:t.caching===`no`?{status:`attention`,text:`Every visit builds the page again, which makes it slower. Not urgent; your web person can let pages be kept ready.`}:l,!c||!s||t.live!==`ok`)Object.assign(o,{secure:l,www:l,missing:l,search:l});else{let t=s.hostname,n=t.startsWith(`www.`)?t.slice(4):`www.${t}`,[r,i,a,u,f]=await Promise.all([s.protocol===`https:`?A(e,`http://${t}/`,{follow:!1}):Promise.resolve(null),A(e,`${s.protocol}//${n}/`,{follow:!1}),A(e,`${c}/site-health-check-a-page-that-does-not-exist`),A(e,`${c}/robots.txt`,{body:!0}),A(e,`${c}/sitemap.xml`,{method:`HEAD`})]);r?(r.status??0)>=300&&(r.status??0)<400&&r.location?.startsWith(`https:`)?o.secure={status:`working`,text:`The insecure address sends visitors to the secure one.`}:r.status===void 0?o.secure=l:o.secure={status:`attention`,text:`The insecure address shows your site without sending visitors to the secure one.`}:o.secure={status:`attention`,text:`Your site address is not secure. Ask your web person to switch it to https.`};let p=i.location?d(i.location)?.hostname:void 0;i.status!==void 0&&i.status>=300&&i.status<400&&p===t?o.www={status:`working`,text:`${n} sends visitors on to your address.`}:i.status!==void 0&&i.status<300?o.www={status:`attention`,text:`${n} shows your site too. Visitors and search engines should be sent to one address; your web person can set this.`}:o.www={status:`attention`,text:`${t.startsWith(`www.`)?`The address without www`:`The address with www`} does not answer. Point it at your site so it sends visitors on.`},a.status===404||a.status===410?o.missing={status:`working`,text:`An address that does not exist shows your "page not found" page and tells search engines it is missing.`}:a.status!==void 0&&a.status<300?o.missing={status:`attention`,text:`An address that does not exist shows a normal page, so search engines may list pages that are not there.`}:o.missing=l;let m=/user-agent:\s*\*\s*(?:\r?\n(?!user-agent)[^\n]*)*?\r?\ndisallow:\s*\/\s*(\r?\n|$)/i.test(u.status===200?u.body??``:``),h=f.status!==void 0&&f.status<300;o.search=m?{status:`attention`,text:`Your site tells search engines to stay away, so it will not appear in search results.`}:{status:`working`,text:h?`They are allowed in, and the list of your pages for them answers.`:`They are allowed in. There is no list of your pages for them yet.`}}let u={...t.live===`ok`?{checkedAt:new Date(r).toISOString()}:{},rows:o};n.checklist=u,await e.kv.set(i.checklist,u)}const we=`site-health`,Te=864e5;function I(e){return e.storage.links}function L(e){return e.storage.pages}function R(e){return new Date(e).toISOString()}async function z(e,t,n=Date.now()){let r=await e.schema?.listCollections()??[],a={pass:(t.scan?.pass??0)+1,status:`running`,phase:`redirects`,collections:r.filter(e=>!e.hidden).map(e=>({slug:e.slug,label:e.label,pattern:e.urlPattern,routable:e.routable,titleField:e.titleField})),ci:0,walked:0,liveDone:0,startedAt:R(n),lastStepAt:R(n)};return await Promise.all([e.kv.set(i.scan,a),Ee(e)]),a}async function Ee(e){try{await e.cron?.schedule(we,{schedule:`* * * * *`})}catch(t){e.log.warn(`could not schedule the background check`,{error:String(t)})}}function De(e,t){let n=e.data[t??`title`]??e.data.title??e.data.name;return typeof n==`string`&&n.trim()!==``?n.trim():e.slug??`(no title)`}function Oe(e){if(e.use===`link`){let t=e.internal?`internal`:`external`;return{id:g(t,e.url),kind:t,url:e.url,host:e.host}}return!e.insecure||e.internal?null:{id:g(`insecure`,e.url),kind:`insecure`,url:e.url,host:e.host}}function B(n,r,i,a,o,s={}){let c=!i.menu,l=!n||n.pass!==a,u=l?[]:[...n.sources],d=u.some(e=>e.p===i.p&&e.menu===i.menu&&e.id===i.id);!d&&u.length<10&&u.push(i);let f=n?{...n,sources:u,count:l?1:n.count+(d?0:1),menu:(l?!1:n.menu)||i.menu===!0,content:(l?!1:n.content)||c,pass:a}:{kind:r.kind,url:r.url,host:r.host,sources:u,count:1,menu:i.menu===!0,content:c,editor:!1,pass:a,state:`pending`,due:r.kind===`external`?R(o):e};return Object.assign(f,s),f.editor=f.kind===`internal`||f.kind===`external`||f.kind===`insecure`&&f.content,f.state===`broken`&&(f.group=t(f)),f}function V(e){return e===`image`?{state:`look`,reason:`insecure-image`}:{state:`broken`,reason:`insecure-active`}}async function ke(e,t){let n=await e.redirects?.list({limit:100,...t.cursor?{cursor:t.cursor}:{}}),r=(n?.items??[]).filter(e=>e.enabled!==!1&&e.source.startsWith(`/`)&&!/[*[(:]/.test(e.source)).map(e=>{let n=m(e.source);return{id:n,data:{status:`redirect`,path:n,title:n,dest:e.destination||``,pass:t.pass}}});r.length>0&&await L(e).putMany(r),n?.hasMore&&n.cursor?t.cursor=n.cursor:(t.cursor=void 0,t.phase=`walk`)}async function Ae(e,t,n){let r=t.collections[t.ci];if(!r){t.phase=`resolve`,t.cursor=void 0;return}let i=await e.content?.list(r.slug,{limit:50,...t.cursor?{cursor:t.cursor}:{}}),a=i?.items??[],o=[],s=new Map;for(let n of a){let i=r.routable?ge(r.pattern,r.slug,n):null,a=De(n,r.titleField),c=[];if(n.status===`published`){let t={c:r.slug,id:n.id,t:a,p:i??``};for(let r of T(n.data,e.site.url)){let e=Oe(r);if(!e||e.kind===`internal`&&e.url===i)continue;c.push(e.id);let n=s.get(e.id);n?n.sources.push(t):s.set(e.id,{base:e,use:r.use,sources:[t]})}}i&&o.push({id:i,data:{status:n.status,path:i,title:a,collection:r.slug,id:n.id,pass:t.pass,links:c.slice(0,200)}})}if(t.walked+=a.length,o.length>0&&await L(e).putMany(o),s.size>0){let r=[...s.keys()],i=await I(e).getMany(r),a=r.map(e=>{let r=s.get(e),a=i.get(e);for(let e of r.sources)a=B(a,r.base,e,t.pass,n,r.base.kind===`insecure`?V(r.use):{});return a?.kind===`insecure`&&a.state===`broken`&&!a.since&&(a.since=R(n)),{id:e,data:a}});await I(e).putMany(a)}i?.hasMore&&i.cursor?t.cursor=i.cursor:(t.cursor=void 0,t.ci+=1)}function H(n,r,i){let a=e=>{let{reason:t,detail:n,since:r,group:i,fails:a,firstFailAt:o,...s}=e;return s};if(n.state===`fine`)return n;if(n.url===`/`)return{...a(n),state:`ok`};if(!r)return{...a(n),state:`pending`,due:e};if(r.status===`redirect`)return{...a(n),state:`look`,reason:`redirect`,detail:r.dest?_(r.dest):`another page`};if(r.status===`published`)return{...a(n),state:`ok`};let o={...a(n),state:`broken`,reason:`draft`,since:n.state===`broken`&&n.reason===`draft`&&n.since?n.since:R(i)};return o.group=t(o),o}async function je(e,t,n){let r=await I(e).query({where:{pass:t.pass},limit:100,...t.cursor?{cursor:t.cursor}:{}}),i=r.items.filter(e=>e.data.kind===`internal`);if(i.length>0){let t=await L(e).getMany([...new Set(i.map(e=>e.data.url))]);await I(e).putMany(i.map(e=>({id:e.id,data:H(e.data,t.get(e.data.url),n)})))}r.hasMore&&r.cursor?t.cursor=r.cursor:(t.cursor=void 0,t.phase=`live-home`,t.liveTotal=await L(e).count({status:`published`,pass:t.pass}))}async function Me(e,n,r,i,a,o){let s=[];for(let e of a.menu){let t=Oe(e);t&&s.push({id:t.id,base:t,source:{t:`Site menu`,p:``,menu:!0},extra:{}})}for(let[e,t]of[[a.active,`embed`],[a.passive,`image`]])for(let n of e)s.push({id:g(`insecure`,n),base:{kind:`insecure`,url:n,host:``},source:{t:i.title,p:i.path},extra:V(t)});for(let e of o)s.push({id:g(`page`,e.path),base:{kind:`page`,url:e.path,host:``},source:{t:e.title,p:e.path},extra:{}});if(s.length===0)return;let c=[...new Set(s.map(e=>e.id))],l=await I(e).getMany(c),u=s.filter(e=>e.base.kind===`internal`).map(e=>e.base.url),d=u.length>0?await L(e).getMany([...new Set(u)]):new Map,f=new Map;for(let e of s){let i=B(f.get(e.id)??l.get(e.id),e.base,e.source,n.pass,r,e.extra);if(i.kind===`internal`&&(i=H(i,d.get(i.url),r),i.state===`broken`&&(i.group=t(i))),i.kind===`page`){let t=o.find(e=>e.path===i.url);t&&(i=F(l.get(e.id)??i,{status:t.status},r)),i.pass=n.pass}i.state===`broken`&&!i.since&&(i.since=R(r)),f.set(e.id,i)}await I(e).putMany([...f].map(([e,t])=>({id:e,data:t})))}function Ne(e){let t=e.status??0;if(t<200||t>=300)return!1;let n=e.headers?.get(`content-type`)??``;if(n!==``&&!/html/i.test(n))return!1;let r=(e.body??``).slice(0,2e3);return!/error code: ?\d{3,4}|cf-error-details|Worker threw exception|cloudflare-nginx/i.test(r)}function Pe(e){return e.status===503?/Database migrations are required|database has not been migrated/i.test((e.body??``).slice(0,2e3)):!1}async function Fe(e,t,n){let r=v(e.site.url);if(!r){t.live=`not-public`,t.phase=`live-paths`;return}let i=await A(e,`${r}/`,{body:!0}),a=i.status??0;if(i.status===void 0){t.live=ve(i)?`not-public`:`failed`,t.phase=`live-paths`;return}if(Pe(i)){t.live=`updating`,t.phase=`live-paths`;return}if(a===403||a===429||a===503||a===401){t.live=`blocked`,t.phase=`live-paths`;return}if(!Ne(i)){t.live=`failed`,t.phase=`live-paths`;return}t.live=`ok`;let o=(i.headers?.get(`cache-control`)??``).toLowerCase();t.caching=/no-store|private/.test(o)||i.headers?.has(`set-cookie`)?`no`:`ok`;let s=D(i.body??``,e.site.url);await Me(e,t,n,{path:`/`,title:`Home`},{menu:s.menu.map(e=>({...e,use:`link`})),active:s.active,passive:s.passive},a>=500?[{path:`/`,title:`Home`,status:a}]:[]),t.phase=`live-pages`,t.cursor=void 0}async function Ie(e,t,n){let r=v(e.site.url),i=await L(e).query({where:{status:`published`},limit:2,...t.cursor?{cursor:t.cursor}:{}});if(r&&i.items.length>0){let a=await Promise.all(i.items.map(async t=>({page:t.data,outcome:await A(e,`${r}${t.data.path}`,{body:!0})}))),o=[],s=[],c=[],l=null;for(let{page:t,outcome:n}of a)if((n.status??0)>=500&&c.push({path:t.path,title:t.title,status:n.status??500}),n.body){let r=D(n.body,e.site.url);o.push(...r.active),s.push(...r.passive),(r.active.length||r.passive.length)&&!l&&(l=t)}await Me(e,t,n,l??{path:``,title:``},{menu:[],active:o,passive:s},c),t.liveDone+=i.items.length}i.hasMore&&i.cursor?t.cursor=i.cursor:(t.cursor=void 0,t.phase=`live-paths`)}async function Le(t,n,r){let i=v(t.site.url),a=n.live===`ok`&&i!==null,o=await I(t).query({where:{kind:`internal`,state:`pending`},limit:a?2:100});if(o.items.length===0){n.phase=`checklist`;return}let s=await Promise.all(o.items.map(async n=>{if(!a)return{id:n.id,data:{...n.data,state:`unknown`,reason:`no-live`,due:e}};let o=await A(t,`${i}${n.data.url}`);return{id:n.id,data:Se(n.data,j(n.data.url,o,`GET`),r)}}));await I(t).putMany(s)}async function Re(e,t){let[n,r]=await Promise.all([I(e).query({where:{pass:{lt:t.pass}},limit:100}),L(e).query({where:{pass:{lt:t.pass}},limit:100})]);await Promise.all([n.items.length>0?I(e).deleteMany(n.items.map(e=>e.id)):0,r.items.length>0?L(e).deleteMany(r.items.map(e=>e.id)):0]),!n.hasMore&&!r.hasMore&&(t.phase=`count`)}async function ze(e,t){let n=I(e),[r,i,a,o,s,c]=await Promise.all([L(e).count({status:`published`}),n.count({kind:{in:[`internal`,`external`]}}),n.count({state:`broken`,group:`site`}),n.count({state:`broken`,group:`page`}),n.count({state:`broken`,group:`link`}),n.count({state:`broken`,editor:!0})]);t.counts={pages:r,links:i,fix:{site:a,page:o,link:s},editorFix:c,look:0,unknown:0},t.phase=`freeze`}async function Be(e,n,r,i){let a=I(e),[s,c,l,u,d]=await Promise.all([a.query({where:{state:`broken`},orderBy:{count:`desc`},limit:100}),a.query({where:{state:`look`},limit:10}),a.query({where:{state:`unknown`},limit:10}),a.count({state:`look`}),a.count({state:`unknown`})]),f={site:[],page:[],link:[]};for(let{id:e,data:n}of s.items){let r=n.group??t(n);f[r].length<10&&f[r].push(o(e,{...n,group:r}))}let p=n.counts??{pages:0,links:0,fix:{site:0,page:0,link:0},editorFix:0,look:0,unknown:0},m={...p,look:u,unknown:d,pass:n.pass,finishedAt:R(i),...r.summary?{prevFinishedAt:r.summary.finishedAt}:{},rows:f,lookRows:c.items.map(({id:e,data:t})=>o(e,t)),unknownRows:l.items.map(({id:e,data:t})=>o(e,t)),live:n.live??`failed`};n.status=`done`,n.finishedAt=R(i);let h=p.pages===0&&!r.summary;return n.nextAt=R(i+(h?6e5:Te)),n.counts=void 0,n.phase=`redirects`,h?null:m}async function U(e,t,n,r=Date.now()){let a=t.scan;if(!a||a.status!==`running`)return a;let o={...a,lastStepAt:R(r)},s=null;switch(o.phase){case`redirects`:await ke(e,o);break;case`walk`:await Ae(e,o,r);break;case`resolve`:await je(e,o,r);break;case`live-home`:await Fe(e,o,r);break;case`live-pages`:await Ie(e,o,r);break;case`live-paths`:await Le(e,o,r);break;case`checklist`:await Ce(e,o,t,r),o.phase=`sweep`;break;case`sweep`:await Re(e,o);break;case`count`:await ze(e,o);break;case`freeze`:s=await Be(e,o,t,r);break}return s&&(await e.kv.set(i.summary,s),t.summary=s,await ie(e,t,s,r)),o.ops=n.ops+1,await e.kv.set(i.scan,o),t.scan=o,o}const Ve=6e4;async function He(e,t,n,r=Date.now()){let a=await I(e).query({where:{due:{lte:R(r)}},orderBy:{due:`asc`},limit:20}),o=v(e.site.url),s={...t.queue.hosts},c=[],u=new Set;for(let e of a.items){if(c.length>=3)break;let n=e.data;if(n.state!==`fine`){if(n.kind===`external`){if(!t.options.outbound)continue;let e=s[n.host];if(u.has(n.host)||e&&r-e.at<Ve*(e.slow&&e.slow>=2?1440:1))continue;u.add(n.host)}else if(n.kind!==`page`||!o||t.summary?.live!==`ok`)continue;c.push({id:e.id,row:n})}}let d=0,f=await Promise.all(c.map(async({id:t,row:n})=>{if(n.kind===`page`)return d+=1,{id:t,before:n,after:F(n,await A(e,`${o}${n.url}`),r)};d+=1;let i=n.get?`GET`:`HEAD`,a=await A(e,n.url,{method:i}),c=j(n.url,a,i);return s[n.host]={at:r,...s[n.host]?.slow?{slow:s[n.host].slow}:{}},{id:t,before:n,after:null,result:c,outcome:a}}));for(let t of f){if(t.after||!(`result`in t)||!t.result)continue;let n=t.result;if(n.kind===`retry`&&d<4&&(d+=1,n=j(t.before.url,await A(e,t.before.url,{method:`GET`}),`GET`)),n.kind===`retry`&&d>=4){t.after={...t.before,due:R(r+Ve),get:!0};continue}if(n.kind===`unknown`&&n.reason===`slow`){let e=s[t.before.host]??{at:r};s[t.before.host]={...e,slow:(e.slow??0)+1}}t.after=xe(t.before,n,r),t.result.kind===`retry`&&(t.after.get=!0)}for(let[e,t]of Object.entries(s))r-t.at>Te&&!t.slow&&delete s[e];f.length>0&&await I(e).putMany(f.map(e=>({id:e.id,data:e.after})));let p=t.summary;if(p){for(let e of f){let t=l(p,e.id,e.before,e.after);t&&(p=t)}p!==t.summary&&(await e.kv.set(i.summary,p),t.summary=p,await ie(e,t,p,r))}return t.queue={hosts:s,cronAt:R(r),ops:n.ops+1},await e.kv.set(i.queue,t.queue),f.length}function Ue(e,t){let n=Math.ceil(e/50)+(t?Math.ceil(e/2):0)+3;if(n<60)return`about ${Math.max(5,Math.round(n/5)*5)} minutes`;let r=Math.round(n/60);return r===1?`about an hour`:`about ${r} hours`}async function We(e,n,r,a,o=Date.now()){if(!a.scan)return;let s=a.scan.collections.find(e=>e.slug===n);if(!s)return;let c=s.routable?ge(s.pattern,n,r):null,u=De(r,s.titleField),d=r.status===`published`?T(r.data,e.site.url):[],f={c:n,id:r.id,t:u,p:c??``},p=new Map;for(let e of d){let t=Oe(e);t&&!(t.kind===`internal`&&t.url===c)&&p.set(t.id,{base:t,use:e.use})}let m=c?await L(e).get(c):null,h=[...new Set([...p.keys(),...m?.links??[]])].slice(0,400),g=h.length>0?await I(e).getMany(h):new Map,_=[...p.values()].filter(e=>e.base.kind===`internal`).map(e=>e.base.url),v=_.length>0?await L(e).getMany([...new Set(_)]):new Map,y=[],b=a.summary;for(let e of h){let n=g.get(e),i=p.get(e),s=null;if(i){let e=n?n.sources.filter(e=>e.id!==r.id):[],c=n?{...n,sources:e,count:Math.max(e.length,n.count-(n.sources.length-e.length))}:void 0;s=B(c,i.base,f,n?.pass??a.scan.pass,o,i.base.kind===`insecure`?V(i.use):{}),c&&(s={...s,sources:[...e,f].slice(0,10),count:c.count+1,pass:n.pass}),s.kind===`internal`&&(s=H(s,v.get(s.url),o)),s.kind===`internal`&&s.state===`pending`&&n?.state!==`pending`&&(s={...s,state:n?.state===`broken`&&n.reason===`missing`?`broken`:`unknown`,reason:n?.reason??`no-live`},s.state===`broken`&&(s.group=t(s)))}else if(n){let e=n.sources.filter(e=>e.id!==r.id);s=e.length===0&&!n.menu?null:{...n,sources:e,count:Math.max(e.length,n.count-1)}}s&&s.state===`broken`&&!s.since&&(s.since=R(o)),s&&y.push({id:e,data:s}),b&&=l(b,e,n,s)??b}let x=h.filter(e=>!p.has(e)&&!y.some(t=>t.id===e));await Promise.all([y.length>0?I(e).putMany(y):void 0,x.length>0?I(e).deleteMany(x):void 0,c?L(e).put(c,{status:r.status,path:c,title:u,collection:n,id:r.id,pass:m?.pass??a.scan.pass,links:[...p.keys()].slice(0,200)}):void 0]),b&&b!==a.summary&&await e.kv.set(i.summary,b)}const Ge=10*6e4,Ke=[{group:`site`,title:`Every page`,text:`In the site menu or footer, so every visitor can hit it. Fix these first.`},{group:`page`,title:`One page`,text:`Visitors who open these pages see an error or an empty box.`},{group:`link`,title:`One link`,text:`A link inside a page that goes nowhere.`}];function W(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function G(e,t=Date.now()){if(!e)return``;let n=Math.round((t-Date.parse(e))/6e4);if(Number.isNaN(n))return``;if(n<2)return`just now`;if(n<60)return`${n} minutes ago`;let r=Math.round(n/60);if(r<24)return r===1?`an hour ago`:`${r} hours ago`;let i=Math.round(r/24);return i===1?`yesterday`:`${i} days ago`}function K(e,t,n=`${t}s`){return`${e} ${e===1?t:n}`}function q(e,t){let i=t?.prevFinishedAt!==void 0&&e.since!==void 0&&e.since>t.prevFinishedAt,a=(e.reason===`gone`||e.reason===`no-site`)&&e.since?` Checked twice since ${new Date(e.since).toLocaleDateString(`en-GB`,{day:`numeric`,month:`short`,timeZone:`UTC`})}.`:``;return`${i?`New since the last check. `:``}${n(e,_(e.url))}${a}${r(e)}`}function J(e,t){let n=[],r=e.kind===`internal`||e.kind===`page`;return t&&r&&e.url!==`/`&&e.reason!==`draft`&&e.reason!==`page-error`&&n.push({label:`Send visitors to another page`,value:`redirect|${e.id}`}),e.menu&&n.push({label:`Change the link in Menus`,value:`menus|${e.id}`}),e.sources.filter(e=>e.c&&e.id).slice(0,3).forEach((t,r)=>n.push({label:`Edit ${t.t}`,value:`edit${r}|${e.id}`})),n.push({label:`Check it again now`,value:`check|${e.id}`}),n.push({label:`This is fine, stop checking it`,value:`fine|${e.id}`}),{type:`menu`,action_id:`fix`,label:`Fix`,items:n}}function qe(e,t){let n=v(t)??d(t)?.origin,r=e.kind===`internal`?n?`${n}${e.url}`:null:e.url;if(!(!r||!/^https?:\/\//.test(r)))return{type:`link`,label:`Open`,target:{kind:`external`,url:r}}}function Y(e,t,n,r){return{type:`table`,page_action_id:e,columns:[{key:`text`,label:t},{key:`fix`,label:``,format:`element`}],rows:n.map(e=>({text:e.text,...e.action?{fix:e.action}:{}})),...r?{next_cursor:r}:{}}}function Je(e){let t=e.scan,n=!t||[`redirects`,`walk`,`resolve`].includes(t.phase),r=t?.liveTotal??t?.walked??0,i=[{type:`banner`,title:`Checking your site for the first time`,description:`${n?`This usually takes 5 to 30 minutes, and a few hours for a very large site.`:`This takes ${Ue(r,t?.live===`ok`)} for a site with ${K(r,`page`)}.`} Links to other sites follow a day later: each one is checked twice, a day apart, before we call it broken. You can leave this page.`}];return t&&!n&&t.live===`ok`&&(t.liveTotal??0)>0?i.push({type:`meter`,label:`Pages checked`,value:Math.min(t.liveDone,t.liveTotal??0),max:t.liveTotal??0,custom_value:`${Math.min(t.liveDone,t.liveTotal??0)} of ${K(t.liveTotal??0,`page`)}`}):t&&!n&&i.push({type:`context`,text:`${K(t.walked,`page`)} read. Finishing off.`}),i.push({type:`context`,text:`Started ${G(t?.startedAt)||`just now`}. Each page is also checked again when it is published.`},{type:`actions`,elements:[{type:`button`,action_id:`refresh`,label:`Refresh`}]}),i}function Ye(e,t){return e.live===`not-public`?[{type:`banner`,title:`This site is not public yet`,description:`${t?`Its address is ${t}, which only this computer or network can open.`:`It has no address set yet.`} Links inside your pages are checked now; the rest starts once the site has a public address.`}]:e.live===`updating`?[{type:`banner`,variant:`alert`,title:`Your site is in the middle of an update`,description:`During the check that finished ${G(e.finishedAt)}, your site showed visitors an error page instead of your pages: an update to its database had not finished. Whoever looks after your site should finish the update. Links inside your pages are still checked; menus and page errors are checked again once your site is back.`}]:e.live===`blocked`||e.live===`failed`?[{type:`banner`,variant:`alert`,title:`We cannot open your pages to check them`,description:`Links inside your pages are still checked. Menus, page errors and insecure items are not. If your site has bot protection, ask whoever set it up to let "EmDash Site Health" in.`}]:[]}function Xe(e){let t=e.panel;if(!t)return[];if(t.kind===`redirect`){let n=e.pickable??[];return[{type:`header`,text:`Send visitors to another page`},{type:`context`,text:`Anyone who opens ${t.url}, from your menu, a bookmark or Google, lands on the page you pick. This fixes it everywhere at once.`},...e.problem?[{type:`banner`,variant:`alert`,title:`Not added`,description:e.problem}]:[],{type:`form`,block_id:`redirect`,fields:[{type:`text_input`,action_id:`source`,label:`Old address`,initial_value:t.url},{type:`combobox`,action_id:`destination`,label:`Send visitors to`,placeholder:`Pick a page, or type an address`,options:n}],submit:{label:`Send visitors there`,action_id:`add_redirect|${t.id}`}},{type:`actions`,elements:[{type:`button`,action_id:`close`,label:`Close`}]}]}return t.kind===`edit`?[{type:`banner`,title:`Change the link to ${_(t.url)} on ${t.title}`,description:`Open the page, find the words that link to ${_(t.url)} and point them at the right page: select the words, then use the link button.`},{type:`actions`,elements:[{type:`link`,label:`Open ${t.title}`,appearance:`primary`,target:{kind:`content`,collection:t.collection,id:t.entry}},{type:`button`,action_id:`close`,label:`Close`}]}]:[{type:`banner`,title:`Change the link to ${_(t.url)} in your menus`,description:`Open Menus in the sidebar, under Manage. Find the item that goes to this address and point it at the right page. If the address is on this site, sending visitors to another page also fixes it.`},{type:`actions`,elements:[{type:`button`,action_id:`close`,label:`Close`}]}]}function Ze(e,t,n,r){let{summary:i,scan:a}=t;if(!i)return Je(t);let o=[...n?Ye(i,e.site.url):[],...Xe(r)],s=e=>n||e.editor,c=n?u(i):i.editorFix;o.push({type:`stats`,items:[{label:`Pages checked`,value:i.pages},{label:`Links checked`,value:i.links},{label:`To fix`,value:c}]});let l=!1;for(let{group:e,title:t,text:a}of Ke){let c=r.more?.group===e?r.more:void 0,u=(c?c.rows:i.rows[e]).filter(s);if(u.length===0)continue;l=!0;let d=n?i.fix[e]:u.length,f=c?c.cursor:d>u.length?`start`:void 0;o.push({type:`header`,text:t},{type:`context`,text:a},Y(`more_${e}`,`Problem`,u.map(e=>({text:q(e,i),action:J(e,n)})),f))}l||o.push({type:`empty`,size:`sm`,title:`Nothing broken. Nice work.`,description:`We checked ${K(i.pages,`page`)} and ${K(i.links,`link`)} ${G(i.finishedAt)}. Next check: tomorrow.`});let d=i.lookRows.filter(s);d.length>0&&o.push({type:`accordion`,label:`Worth a look (${n?i.look:d.length})`,blocks:[{type:`context`,text:`These work, but take a detour. Tidier to fix, not urgent.`},Y(`look_table`,`What we noticed`,d.map(e=>({text:q(e,null),action:J(e,n)})))]});let f=i.unknownRows.filter(s);f.length>0&&o.push({type:`accordion`,label:`Links we could not check (${n?i.unknown:f.length})`,blocks:[{type:`context`,text:`Some sites, such as LinkedIn and Instagram, turn away automatic checkers. These links are probably fine. Open one to see for yourself; we try again next week.`},Y(`unknown_table`,`What happened`,f.map(t=>{let r=t.kind===`internal`?J(t,n):qe(t,e.site.url);return{text:q(t,null),...r?{action:r}:{}}}))]});let p=a?.status===`running`;return o.push({type:`context`,text:p?`Checking again now. These are the results of the last full check, ${G(i.finishedAt)}.`:`Last full check: ${G(i.finishedAt)}. Pages on this site are checked every day, links to other sites every week.`}),n||o.push({type:`context`,text:`Pages that show errors and the site's setup are looked after by your site admin.`}),o.push({type:`actions`,elements:[{type:`button`,action_id:p?`refresh`:`check_all`,label:p?`Refresh`:`Check everything again`}]}),o}const Qe={working:`Working`,attention:`Needs attention`,untested:`Not tested yet`,unknown:`Could not tell`};function $e(e,t){let n=[e.queue.cronAt,e.scan?.cronAt].filter(e=>!!e).map(Date.parse),r=n.length>0?Math.max(...n):0,i=e.scan?Date.parse(e.scan.startedAt):t;return r>0&&t-r<Ge?{status:`working`,text:`Last ran ${K(Math.max(1,Math.round((t-r)/6e4)),`minute`)} ago. Scheduled posts and these checks depend on it.`}:r===0&&t-i<Ge?{status:`untested`,text:`Waiting for the first run. Scheduled posts and these checks depend on it.`}:{status:`attention`,text:`Not running. Scheduled posts will not publish, and these checks only move while this page is open.`}}function et(e){return e.length<=2?`Goes to ${e.join(` and `)}.`:`Goes to ${e.slice(0,2).join(`, `)} and ${e.length-2} more.`}function tt(e,t,n=Date.now()){let r=e.options.recipients??[],i=e.alerts.email;if(!t)return{status:`attention`,text:`Not set up. Your site has no email service chosen, so alerts, form messages and password resets are not sent. Whoever set up your site can choose one in Settings, Email.`};if(r.length===0)return{status:`attention`,text:`Nobody is on the list, so no alerts are sent. Add an address under Options below.`};let a=et(r);if(!i)return{status:`untested`,text:`Not checked yet. ${a} To find out whether email works, press Send a test email to check now.`};let o=G(i.at,n),s=i.kind===`alert`?`an alert`:`a test email`;return i.state===`working`?(i.list?i.list.length===r.length&&i.list.every(e=>r.includes(e)):i.to===r.length)?{status:`working`,text:`Last checked ${o}: ${s} was sent. ${a} To check again, press Send a test email.`}:{status:`untested`,text:`The list has changed since the last email, ${o}. ${a} To find out whether it reaches them, press Send a test email to check now.`}:S.test(i.error??``)?{status:`attention`,text:`Last checked ${o}: your site had no email service chosen, so nothing was sent. If one has been chosen in Settings, Email since, press Send a test email to check now.`}:{status:`attention`,text:`Last checked ${o}: ${s} could not be sent; the email service turned it down. ${a} Press Send a test email to try again; if it fails, whoever set up your site can look at Settings, Email.`}}function nt(e,t,n,r=Date.now()){let i=t?d(t):null,a=i?i.protocol===`https:`?{status:`working`,text:`Set, secure, and the address visitors use.`}:{status:v(i.href)?`attention`:`unknown`,text:v(i.href)?`Set, but not secure. Ask your web person to switch it to https.`:`Set to an address only this computer or network can open.`}:{status:`attention`,text:`Not set. Set it in Settings so links and emails point to your site.`},o=e.checklist.rows;return[...e.summary?.live===`updating`?[{name:`Site up`,status:`attention`,text:`During the check that finished ${G(e.summary.finishedAt,r)}, your site showed visitors an error page: an update to its database had not finished. Whoever looks after your site should finish the update.`}]:[],{name:`Site address`,...a},{name:`Scheduled tasks`,...$e(e,r)},{name:`Email`,...tt(e,n,r)},{name:`Secure connection`,...o.secure??{status:`untested`,text:`Checked with the next full check.`}},{name:`With and without www`,...o.www??{status:`untested`,text:`Checked with the next full check.`}},{name:`Missing pages`,...o.missing??{status:`untested`,text:`Checked with the next full check.`}},{name:`Search engines`,...o.search??{status:`untested`,text:`Checked with the next full check.`}},{name:`Quick repeat visits`,...o.caching??{status:`untested`,text:`Checked with the next full check.`}}]}function rt(e,t,n,r,i){let a=nt(e,t,n,i),o=a.filter(e=>e.status===`attention`&&e.name!==`Quick repeat visits`),s=[...a].sort((e,t)=>Number(t.status===`attention`)-Number(e.status===`attention`)),c=[];a.find(e=>e.name===`Scheduled tasks`)?.status===`attention`?c.push({type:`banner`,variant:`alert`,title:`Scheduled tasks are not running`,description:`Scheduled posts will not publish, and these checks only move while this page is open. Whoever set up your site can switch them on in a few minutes.`},{type:`accordion`,label:`Copy the instructions for your web person`,blocks:[{type:`context`,text:`Send the steps below to whoever set up your site: they switch on the scheduled tasks your site needs.`},{type:`code`,code:`This EmDash site's scheduled tasks are not running (the Site Health task has not run for 10 minutes).
1
+ const e=`9999`;function t(e){return e.menu?`site`:e.kind===`page`?e.url===`/`?`site`:`page`:e.kind===`insecure`?`page`:`link`}function n(e,t){switch(e.reason){case`missing`:return`${t} goes to a page that does not exist.`;case`draft`:return`${t} goes to a page that is not published yet.`;case`gone`:return`${t}: that other website has removed this page.`;case`no-site`:return`${t}: that website no longer exists.`;case`server`:return`${t}: that website has shown an error for a week.`;case`page-error`:return`${t}: this page is broken; visitors see an error.`;case`insecure-active`:return`${t} is loaded over an insecure connection. Browsers block it, so visitors see an empty box.`;case`redirect`:case`moved`:return`${t} now goes on to ${e.detail??`another address`}.${e.kind===`internal`&&e.detail?` Linking to ${e.detail} directly saves a step.`:``}`;case`insecure-image`:return`${t} is a picture shown over an insecure connection. Browsers fix this themselves for now.`;case`refuses`:return`${t}: that website turns away automatic checkers.`;case`slow`:return`${t}: that website asked us to slow down. Trying again tomorrow.`;case`timeout`:return`${t} did not answer within 8 seconds.`;case`trouble`:return`${t}: that website had a problem when we checked. Trying again.`;case`no-live`:return`${t}: we could not open your site to check this address.`;default:return`${t}: we could not open it.`}}function r(e){if(e.kind===`page`)return``;let t=[];e.menu&&t.push(`Site menu (every page)`);for(let n of e.sources)if(!n.menu){if(t.length>=4)break;t.push(n.t)}let n=t.length-(e.menu?1:0),r=e.count-(e.menu?e.sources.filter(e=>e.menu).length:0)-n;return t.length===0?``:` Found on: ${t.join(`, `)}${r>0?` and ${r} more`:``}.`}function i(e){return e!=null}async function a(e,t,n){try{await e.settings.set(t.name,n)}catch(n){e.log.warn(`could not copy the settings to their new place`,{key:t.name,error:String(n)})}}async function o(e,t,n,{copy:r=!0}={}){let o=t.get(`settings:${n.name}`);if(i(o))return o;let s=t.get(n.legacy);return i(s)?(r&&await a(e,n,s),s):null}async function s(e,t,n){await e.settings.set(t.name,n);try{await e.kv.set(t.legacy,n)}catch(n){e.log.warn(`could not keep a copy of the settings for the earlier release`,{key:t.legacy,error:String(n)})}}const c={scan:`scan`,summary:`summary`,queue:`queue`,options:`options`,alerts:`alerts`,checklist:`checklist`},l={name:`options`,legacy:c.options};async function u(e,{copy:t=!1}={}){let n=await e.kv.list(),r=new Map(n.map(e=>[e.key,e.value])),i=(t?await o(e,r,l):r.get(`settings:${l.name}`)??r.get(l.legacy)??null)??{},a=r.get(c.alerts)??{},s=r.get(c.queue)??{},u=r.get(c.checklist)??{};return{scan:r.get(c.scan)??null,summary:r.get(c.summary)??null,queue:{hosts:{},...s},options:{outbound:!0,...i},alerts:{pending:[],sent:[],...a},checklist:{rows:{},...u}}}function d(e,t){return{id:e,kind:t.kind,url:t.url,...t.reason?{reason:t.reason}:{},...t.detail?{detail:t.detail}:{},sources:t.sources.slice(0,5),count:t.count,menu:t.menu,editor:t.editor,...t.since?{since:t.since}:{}}}function f(e,t){return t.count-e.count}function p(e,t){return e.filter(e=>e.id!==t)}function m(e,t,n,r){let i=n?.state,a=r?.state,o=n?.group;if(i===a&&o===r?.group&&n?.reason===r?.reason)return null;let s={...e,fix:{...e.fix},rows:{site:p(e.rows.site,t),page:p(e.rows.page,t),link:p(e.rows.link,t)},lookRows:p(e.lookRows,t),unknownRows:p(e.unknownRows,t)};return i===`broken`&&o&&(s.fix[o]=Math.max(0,s.fix[o]-1),n?.editor&&(s.editorFix=Math.max(0,s.editorFix-1))),i===`look`&&(s.look=Math.max(0,s.look-1)),i===`unknown`&&(s.unknown=Math.max(0,s.unknown-1)),r&&a===`broken`&&r.group&&(s.fix[r.group]+=1,r.editor&&(s.editorFix+=1),s.rows[r.group]=[...s.rows[r.group],d(t,r)].sort(f).slice(0,10)),r&&a===`look`&&(s.look+=1,s.lookRows=[...s.lookRows,d(t,r)].slice(0,10)),r&&a===`unknown`&&(s.unknown+=1,s.unknownRows=[...s.unknownRows,d(t,r)].slice(0,10)),s}function h(e){return e.fix.site+e.fix.page+e.fix.link}function g(e,t){try{return new URL(e,t)}catch{return null}}const _=/^(mailto|tel|sms|javascript|data|blob|about|ftp):/i;function v(e){return e.startsWith(`/_emdash/`)||e.startsWith(`/_astro/`)||e===`/_emdash`}function y(e){let t=e;try{t=decodeURI(e)}catch{}let n=t.replace(/\/{2,}/g,`/`);return n.length>1&&n.endsWith(`/`)?n.slice(0,-1):n||`/`}function b(e,t){let n=e.trim();if(n===``||n.startsWith(`#`)||n.startsWith(`?`)||_.test(n)||/\s/.test(n)&&!/^https?:\/\//i.test(n))return null;let r=g(t),i=/^https?:\/\//i.test(n)||n.startsWith(`//`);if(!i&&!n.startsWith(`/`))return null;let a=g(n.startsWith(`//`)?`https:${n}`:n,`https://site.invalid`);if(!a||a.protocol!==`https:`&&a.protocol!==`http:`)return null;let o=a.hostname.toLowerCase();if(!i||r!==null&&o.replace(/^www\./,``)===r.hostname.replace(/^www\./,``)){let e=y(a.pathname);return v(e)?null:{url:e,internal:!0,host:``,insecure:i&&a.protocol===`http:`}}return a.hash=``,{url:a.href,internal:!1,host:o,insecure:a.protocol===`http:`}}function x(e,t){let n=2166136261,r=16777619,i=`${e}|${t}`;for(let e=0;e<i.length;e+=1){let t=i.charCodeAt(e);n=Math.imul(n^t,16777619)>>>0,r=Math.imul(r^t,1540483477)>>>0}return`${e[0]}${n.toString(36)}${(r&1048575).toString(36)}`}function S(e,t=30){let n=e.replace(/^https?:\/\//i,``).replace(/^www\./i,``).replace(/\/$/,``);return n.length>t?`${n.slice(0,t-1)}…`:n}function C(e){let t=g(e);if(!t||t.protocol!==`https:`&&t.protocol!==`http:`)return null;let n=t.hostname.toLowerCase().replace(/\.$/,``);return n===`localhost`||n.endsWith(`.localhost`)||n.endsWith(`.local`)||/^(127|10|0)\.|^192\.168\.|^169\.254\.|^172\.(1[6-9]|2\d|3[01])\./.test(n)||n===`[::1]`||n.startsWith(`[fc`)||n.startsWith(`[fd`)?null:t.origin}const w=864e5;7*w;function ee(e){return`${n(e,S(e.url))}${r(e)}`}function te(e){return e.menu||e.reason===`page-error`}function ne(e){let t=C(e.site.url);return t?`${t}/_emdash/admin/plugins/${e.plugin.id}/health`:null}function re(e){let t=[];for(let n of e.split(/[\s,;]+/)){let e=n.trim();if(e!==``){if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e))return{wrong:e};t.includes(e.toLowerCase())||t.push(e.toLowerCase())}}return{list:t.slice(0,20)}}const T=/no email provider|not configured/i;function E(e,t,n,r){return e.ok?{state:`working`,at:n,to:t.length,list:t,kind:r}:{state:T.test(e.error)?`not-set-up`:`failed`,at:n,to:0,list:t,kind:r,error:e.error.slice(0,200)}}const ie=`updating|`;async function D(e,t,n,r){let[i,...a]=t;if(!i)return{ok:!1,error:`no addresses`};if(!e.email)return{ok:!1,error:`Email is not configured`};try{return await e.email.send({to:i,...a.length>0?{cc:a}:{},subject:n,text:r}),{ok:!0}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}function ae(e){return e.site.name.trim()||C(e.site.url)?.replace(/^https?:\/\//,``)||`Your site`}async function oe(e,t,n,r){let i=[...n.rows.site,...n.rows.page].filter(te).map(e=>({id:e.id,text:ee(e)}));n.live===`updating`&&i.unshift({id:`${ie}${n.pass}`,text:`Your site showed visitors an error page instead of your pages: an update to its database has not finished. Whoever looks after your site should finish the update.`});let a=i.filter(e=>!t.alerts.sent.includes(e.id));if(a.length===0)return;let o={...t.alerts,pending:[...t.alerts.pending,...a.map(e=>({id:e.id,text:e.text}))].slice(-20),sent:[...t.alerts.sent,...a.map(e=>e.id)].slice(-200)};t.alerts=o,await e.kv.set(c.alerts,o)}function se(e,t){if((e.options.recipients??[]).length===0||!e.summary)return!1;let{alerts:n}=e;return n.pending.length>0&&(!n.urgentAt||t-Date.parse(n.urgentAt)>=w)||!n.weeklyAt||t-Date.parse(n.weeklyAt)>=6048e5}async function ce(e,t,n){let r=t.options.recipients??[],i={...t.alerts},a=ae(e),o=ne(e),s=new Date(n).toISOString();if(i.pending.length>0&&(!i.urgentAt||n-Date.parse(i.urgentAt)>=w)){let t=i.pending.some(e=>e.id.startsWith(ie)),n=i.pending.some(e=>/visitors see an error/.test(e.text)),c=await D(e,r,`${a}: ${t?`your site is showing visitors an error`:n?`a page on your site shows an error`:`a link in your site menu is broken`}`,[`Site Health found something visitors will hit:`,``,...i.pending.map(e=>`- ${e.text}`),``,o?`See them and fix them: ${o}`:`See them in Site Health in your site's admin.`,``,`You get this because your address is on Site Health's list. At most one of these a day. Change the list in Site Health, Checklist, Options.`].join(`
2
+ `));i.urgentAt=s,i.pending=[],i.email=E(c,r,s,`alert`)}else if(!i.weeklyAt)i.weeklyAt=s;else if(t.summary){let n=i.weeklyAt,c=[...t.summary.rows.site,...t.summary.rows.page,...t.summary.rows.link].filter(e=>e.since&&e.since>n);if(i.weeklyAt=s,c.length>0){let n=t.summary.fix.site+t.summary.fix.page+t.summary.fix.link;i.email=E(await D(e,r,`${a}: ${c.length} new ${c.length===1?`problem`:`problems`} this week`,[`New since last week:`,``,...c.map(e=>`- ${ee(e)}`),``,`${n} ${n===1?`thing`:`things`} to fix in all. ${o?`See them: ${o}`:`See them in Site Health in your site's admin.`}`,``,`Sent once a week, only when something is new.`].join(`
3
+ `)),r,s,`alert`)}}t.alerts=i,await e.kv.set(c.alerts,i)}async function le(e,t,n){let r=t.options.recipients??[],i=await D(e,r,`${ae(e)}: test email from Site Health`,`This is a test from Site Health. If you can read it, alerts about broken links and pages that show errors will reach you.`),a=new Date(n).toISOString();return t.alerts={...t.alerts,email:E(i,r,a,`test`)},await e.kv.set(c.alerts,t.alerts),i}function ue(e,t){return e.ops+t<=9}const de=[`kv`,`settings`,`content`,`schema`,`redirects`,`http`,`email`,`cron`,`log`];function fe(e,t){return new Proxy(e,{get(e,n,r){let i=Reflect.get(e,n,r);return typeof i==`function`?(...n)=>(t.ops+=1,i.apply(e,n)):i}})}function O(e,t){let n={};for(let r of de){let i=e[r];i&&(n[r]=fe(i,t))}let r=new Map,i=new Proxy(e.storage,{get(e,n,i){let a=Reflect.get(e,n,i);if(typeof n!=`string`||!a||typeof a!=`object`)return a;let o=r.get(n);return o||(o=fe(a,t),r.set(n,o)),o}});return new Proxy(e,{get(e,t,r){return t===`storage`?i:typeof t==`string`&&t in n?n[t]:Reflect.get(e,t,r)}})}const pe=/^(href|url|link|src)$|(_url|Url|_href|Href|_link|Link)$/,me=/iframe|embed|video|map|script|audio|form/i,he=/image|picture|photo/i;function k(e,t){let n=new Map,r=0,i=(e,r)=>{let i=b(e,t);i&&n.set(`${r}|${i.url}`,{...i,use:r})},a=(e,t,n)=>{if(r+=1,r>5e3||n>30||typeof e!=`object`||!e)return;if(Array.isArray(e)){for(let r of e)a(r,t,n+1);return}let o=e,s=typeof o._type==`string`?o._type:t;for(let[e,t]of Object.entries(o))if(typeof t==`string`){if(!pe.test(e)||t.length>2e3)continue;i(t,e===`src`||e.endsWith(`Src`)||e.endsWith(`_src`)?he.test(s)?`image`:me.test(s)?`embed`:`link`:`link`)}else t&&typeof t==`object`&&a(t,s,n+1)};return a(e,``,0),[...n.values()]}const A=2e5,ge=/<a\b[^>]*?\bhref\s*=\s*("([^"]*)"|'([^']*)')/gi,_e=/<(header|nav|footer)\b[\s\S]*?<\/\1\s*>/gi,ve=/<(iframe|script|embed|object|video|audio|source)\b[^>]*?\bsrc\s*=\s*("http:[^"]*"|'http:[^']*')|<link\b(?=[^>]*\brel\s*=\s*["']?stylesheet)[^>]*?\bhref\s*=\s*("http:[^"]*"|'http:[^']*')|<form\b[^>]*?\baction\s*=\s*("http:[^"]*"|'http:[^']*')/gi,ye=/<img\b[^>]*?\bsrc\s*=\s*("http:[^"]*"|'http:[^']*')/gi;function be(e){return e.slice(1,-1).replaceAll(`&amp;`,`&`)}function j(e,t){let n=e.length>A?e.slice(0,A):e,r=new Map;for(let e of n.matchAll(_e))for(let n of e[0].matchAll(ge)){let e=b((n[2]??n[3]??``).replaceAll(`&amp;`,`&`),t);e&&r.set(e.url,e)}let i=new Set;for(let e of n.matchAll(ve)){let t=e[2]??e[3]??e[4];t&&i.add(be(t))}let a=new Set;for(let e of n.matchAll(ye))e[1]&&a.add(be(e[1]));return{menu:[...r.values()],active:[...i],passive:[...a]}}function xe(e,t,n){if(!n.slug)return null;let r=e??`/${encodeURIComponent(t)}/{slug}`,i=n.publishedAt?new Date(n.publishedAt):null,a=e=>String(e).padStart(2,`0`),o=i&&!Number.isNaN(i.getTime())?{year:String(i.getUTCFullYear()),month:a(i.getUTCMonth()+1),day:a(i.getUTCDate()),hour:a(i.getUTCHours()),minute:a(i.getUTCMinutes()),second:a(i.getUTCSeconds())}:{},s=r.replaceAll(`{slug}`,encodeURIComponent(n.slug)).replaceAll(`{id}`,encodeURIComponent(n.id)).replace(/\{(year|month|day|hour|minute|second)\}/g,(e,t)=>o[t]??e);if(s.includes(`{`))return null;s=s.replace(/\/{2,}/g,`/`),s.length>1&&s.endsWith(`/`)&&(s=s.slice(0,-1)),s.startsWith(`/`)||(s=`/${s}`);try{return decodeURI(s)}catch{return s}}const M=36e5,N=24*M;function Se(e){return`EmDash-SiteHealth/0.1 (+${e||`unknown site`})`}async function P(e,t,n={}){let r=e.http;if(!r)return{error:`no network`};let i,a=new Promise(e=>{i=setTimeout(()=>e({timedOut:!0}),8e3)}),o=(async()=>{try{let i=await r.fetch(t,{method:n.method??`GET`,redirect:n.follow===!1?`manual`:`follow`,headers:{"User-Agent":Se(e.site.url),Accept:`text/html,*/*;q=0.5`}}),a=i.headers.get(`location`),o={status:i.status,headers:i.headers,...a?{location:g(a,t)?.href??a}:{},...i.redirected&&i.url?{finalUrl:i.url}:{}};if(n.body){let e=await i.text();o.body=e.length>A?e.slice(0,A):e}return o}catch(e){return{error:e instanceof Error?e.message:String(e)}}})();try{return await Promise.race([o,a])}finally{clearTimeout(i)}}function Ce(e){return/internal host|not allowed to fetch|private|localhost/i.test(e.error??``)}const we=/ENOTFOUND|EAI_AGAIN|getaddrinfo|DNS|name not resolved|could not resolve/i;function Te(e,t){let n=e=>e.replace(/^https?:\/\//i,``).replace(/^www\./i,``).replace(/\/$/,``);return n(e)===n(t)}function F(e,t,n){if(t.timedOut)return{kind:`unknown`,reason:`timeout`};if(t.error!==void 0)return we.test(t.error)?{kind:`dead`}:{kind:`unknown`,reason:`error`};let r=t.status??0;return r>=300&&r<400&&t.location?Te(e,t.location)?{kind:`ok`}:{kind:`moved`,to:t.location}:r>=200&&r<300?t.finalUrl&&!Te(e,t.finalUrl)?{kind:`moved`,to:t.finalUrl}:{kind:`ok`}:n===`HEAD`&&[400,403,404,405,501].includes(r)?{kind:`retry`}:r===404||r===410?{kind:`gone`}:r===429?{kind:`unknown`,reason:`slow`}:r>=500&&r<600?{kind:`server`}:[401,403,407,451,999].includes(r)||r>=400?{kind:`unknown`,reason:`refuses`}:{kind:`unknown`,reason:`error`}}function I(e,t){return new Date(e+t).toISOString()}function L(e,n,r){let i={...e,state:`broken`,reason:n,since:e.state===`broken`&&e.since?e.since:new Date(r).toISOString()};return i.group=t(i),i}function R(e){let{fails:t,firstFailAt:n,since:r,group:i,detail:a,reason:o,...s}=e;return s}function Ee(e,t,n){let r=new Date(n).toISOString(),i={...e,checkedAt:r};switch(t.kind){case`ok`:return{...R(i),state:`ok`,due:I(n,7*N)};case`moved`:return{...R(i),state:`look`,reason:`moved`,detail:S(t.to),due:I(n,7*N)};case`gone`:case`dead`:{let a=t.kind===`gone`?`gone`:`no-site`,o=e.firstFailAt?Date.parse(e.firstFailAt):NaN;return(e.fails??0)>=1&&n-o>=N-M?{...L(i,a,n),fails:(e.fails??0)+1,due:I(n,7*N)}:{...i,state:e.state===`broken`?`broken`:`pending`,fails:(e.fails??0)+1,firstFailAt:e.firstFailAt??r,due:I(n,N)}}case`server`:{let t=e.firstFailAt?Date.parse(e.firstFailAt):NaN;return(e.fails??0)>=2&&n-t>=7*N-M?{...L(i,`server`,n),fails:(e.fails??0)+1,due:I(n,7*N)}:{...i,state:e.state===`broken`?`broken`:`unknown`,reason:e.state===`broken`?e.reason:`trouble`,fails:(e.fails??0)+1,firstFailAt:e.firstFailAt??r,due:I(n,3*N)}}case`unknown`:case`retry`:{let r=t.kind===`unknown`?t.reason:`refuses`;return e.state===`broken`?{...i,due:I(n,N)}:{...i,state:`unknown`,reason:r,due:I(n,r===`slow`||r===`timeout`?N:7*N)}}}}function De(e,t,n){let r=new Date(n).toISOString(),i={...e,checkedAt:r,due:`9999`};switch(t.kind){case`ok`:return{...R(i),state:`ok`};case`moved`:{let e=g(t.to);return{...R(i),state:`look`,reason:`redirect`,detail:S(e?e.pathname:t.to)}}case`gone`:case`retry`:return L(i,`missing`,n);default:return{...R(i),state:`unknown`,reason:`no-live`}}}function z(e,t,n){let r=new Date(n).toISOString(),i=t.status??0;if(i>=500){let t=e.firstFailAt?Date.parse(e.firstFailAt):NaN;return(e.fails??0)>=1&&n-t>=5e4?{...L({...e,checkedAt:r},`page-error`,n),fails:(e.fails??0)+1,due:I(n,N)}:{...e,checkedAt:r,state:e.state===`broken`?`broken`:`pending`,fails:(e.fails??0)+1,firstFailAt:e.firstFailAt??r,due:I(n,6e4)}}return t.status!==void 0&&i<400?{...R({...e,checkedAt:r}),state:`ok`,due:`9999`}:{...R({...e,checkedAt:r}),state:`unknown`,reason:`no-live`,due:`9999`}}async function Oe(e,t,n,r){let i=n.checklist.checkedAt?Date.parse(n.checklist.checkedAt):0;if(t.live===`ok`&&r-i<6048e5)return;let a={},o=g(e.site.url),s=C(e.site.url),l={status:`unknown`,text:t.live===`updating`?`Your site was showing visitors an error while an update finished, so we could not find out.`:`We could not open your site to find out.`};if(a.caching=t.caching===`ok`?{status:`working`,text:`Pages can be kept ready for visitors instead of built on every visit.`}:t.caching===`no`?{status:`attention`,text:`Every visit builds the page again, which makes it slower. Not urgent; your web person can let pages be kept ready.`}:l,!s||!o||t.live!==`ok`)Object.assign(a,{secure:l,www:l,missing:l,search:l});else{let t=o.hostname,n=t.startsWith(`www.`)?t.slice(4):`www.${t}`,[r,i,c,u,d]=await Promise.all([o.protocol===`https:`?P(e,`http://${t}/`,{follow:!1}):Promise.resolve(null),P(e,`${o.protocol}//${n}/`,{follow:!1}),P(e,`${s}/site-health-check-a-page-that-does-not-exist`),P(e,`${s}/robots.txt`,{body:!0}),P(e,`${s}/sitemap.xml`,{method:`HEAD`})]);r?(r.status??0)>=300&&(r.status??0)<400&&r.location?.startsWith(`https:`)?a.secure={status:`working`,text:`The insecure address sends visitors to the secure one.`}:r.status===void 0?a.secure=l:a.secure={status:`attention`,text:`The insecure address shows your site without sending visitors to the secure one.`}:a.secure={status:`attention`,text:`Your site address is not secure. Ask your web person to switch it to https.`};let f=i.location?g(i.location)?.hostname:void 0;i.status!==void 0&&i.status>=300&&i.status<400&&f===t?a.www={status:`working`,text:`${n} sends visitors on to your address.`}:i.status!==void 0&&i.status<300?a.www={status:`attention`,text:`${n} shows your site too. Visitors and search engines should be sent to one address; your web person can set this.`}:a.www={status:`attention`,text:`${t.startsWith(`www.`)?`The address without www`:`The address with www`} does not answer. Point it at your site so it sends visitors on.`},c.status===404||c.status===410?a.missing={status:`working`,text:`An address that does not exist shows your "page not found" page and tells search engines it is missing.`}:c.status!==void 0&&c.status<300?a.missing={status:`attention`,text:`An address that does not exist shows a normal page, so search engines may list pages that are not there.`}:a.missing=l;let p=/user-agent:\s*\*\s*(?:\r?\n(?!user-agent)[^\n]*)*?\r?\ndisallow:\s*\/\s*(\r?\n|$)/i.test(u.status===200?u.body??``:``),m=d.status!==void 0&&d.status<300;a.search=p?{status:`attention`,text:`Your site tells search engines to stay away, so it will not appear in search results.`}:{status:`working`,text:m?`They are allowed in, and the list of your pages for them answers.`:`They are allowed in. There is no list of your pages for them yet.`}}let u={...t.live===`ok`?{checkedAt:new Date(r).toISOString()}:{},rows:a};n.checklist=u,await e.kv.set(c.checklist,u)}const ke=`site-health`,Ae=864e5;function B(e){return e.storage.links}function V(e){return e.storage.pages}function H(e){return new Date(e).toISOString()}async function U(e,t,n=Date.now()){let r=await e.schema?.listCollections()??[],i={pass:(t.scan?.pass??0)+1,status:`running`,phase:`redirects`,collections:r.filter(e=>!e.hidden).map(e=>({slug:e.slug,label:e.label,pattern:e.urlPattern,routable:e.routable,titleField:e.titleField})),ci:0,walked:0,liveDone:0,startedAt:H(n),lastStepAt:H(n)};return await Promise.all([e.kv.set(c.scan,i),W(e)]),i}async function W(e){try{await e.cron?.schedule(ke,{schedule:`* * * * *`})}catch(t){e.log.warn(`could not schedule the background check`,{error:String(t)})}}function je(e,t){let n=e.data[t??`title`]??e.data.title??e.data.name;return typeof n==`string`&&n.trim()!==``?n.trim():e.slug??`(no title)`}function G(e){if(e.use===`link`){let t=e.internal?`internal`:`external`;return{id:x(t,e.url),kind:t,url:e.url,host:e.host}}return!e.insecure||e.internal?null:{id:x(`insecure`,e.url),kind:`insecure`,url:e.url,host:e.host}}function K(n,r,i,a,o,s={}){let c=!i.menu,l=!n||n.pass!==a,u=l?[]:[...n.sources],d=u.some(e=>e.p===i.p&&e.menu===i.menu&&e.id===i.id);!d&&u.length<10&&u.push(i);let f=n?{...n,sources:u,count:l?1:n.count+(d?0:1),menu:(l?!1:n.menu)||i.menu===!0,content:(l?!1:n.content)||c,pass:a}:{kind:r.kind,url:r.url,host:r.host,sources:u,count:1,menu:i.menu===!0,content:c,editor:!1,pass:a,state:`pending`,due:r.kind===`external`?H(o):e};return Object.assign(f,s),f.editor=f.kind===`internal`||f.kind===`external`||f.kind===`insecure`&&f.content,f.state===`broken`&&(f.group=t(f)),f}function q(e){return e===`image`?{state:`look`,reason:`insecure-image`}:{state:`broken`,reason:`insecure-active`}}async function Me(e,t){let n=await e.redirects?.list({limit:100,...t.cursor?{cursor:t.cursor}:{}}),r=(n?.items??[]).filter(e=>e.enabled!==!1&&e.source.startsWith(`/`)&&!/[*[(:]/.test(e.source)).map(e=>{let n=y(e.source);return{id:n,data:{status:`redirect`,path:n,title:n,dest:e.destination||``,pass:t.pass}}});r.length>0&&await V(e).putMany(r),n?.hasMore&&n.cursor?t.cursor=n.cursor:(t.cursor=void 0,t.phase=`walk`)}async function Ne(e,t,n){let r=t.collections[t.ci];if(!r){t.phase=`resolve`,t.cursor=void 0;return}let i=await e.content?.list(r.slug,{limit:50,...t.cursor?{cursor:t.cursor}:{}}),a=i?.items??[],o=[],s=new Map;for(let n of a){let i=r.routable?xe(r.pattern,r.slug,n):null,a=je(n,r.titleField),c=[];if(n.status===`published`){let t={c:r.slug,id:n.id,t:a,p:i??``};for(let r of k(n.data,e.site.url)){let e=G(r);if(!e||e.kind===`internal`&&e.url===i)continue;c.push(e.id);let n=s.get(e.id);n?n.sources.push(t):s.set(e.id,{base:e,use:r.use,sources:[t]})}}i&&o.push({id:i,data:{status:n.status,path:i,title:a,collection:r.slug,id:n.id,pass:t.pass,links:c.slice(0,200)}})}if(t.walked+=a.length,o.length>0&&await V(e).putMany(o),s.size>0){let r=[...s.keys()],i=await B(e).getMany(r),a=r.map(e=>{let r=s.get(e),a=i.get(e);for(let e of r.sources)a=K(a,r.base,e,t.pass,n,r.base.kind===`insecure`?q(r.use):{});return a?.kind===`insecure`&&a.state===`broken`&&!a.since&&(a.since=H(n)),{id:e,data:a}});await B(e).putMany(a)}i?.hasMore&&i.cursor?t.cursor=i.cursor:(t.cursor=void 0,t.ci+=1)}function J(n,r,i){let a=e=>{let{reason:t,detail:n,since:r,group:i,fails:a,firstFailAt:o,...s}=e;return s};if(n.state===`fine`)return n;if(n.url===`/`)return{...a(n),state:`ok`};if(!r)return{...a(n),state:`pending`,due:e};if(r.status===`redirect`)return{...a(n),state:`look`,reason:`redirect`,detail:r.dest?S(r.dest):`another page`};if(r.status===`published`)return{...a(n),state:`ok`};let o={...a(n),state:`broken`,reason:`draft`,since:n.state===`broken`&&n.reason===`draft`&&n.since?n.since:H(i)};return o.group=t(o),o}async function Pe(e,t,n){let r=await B(e).query({where:{pass:t.pass},limit:100,...t.cursor?{cursor:t.cursor}:{}}),i=r.items.filter(e=>e.data.kind===`internal`);if(i.length>0){let t=await V(e).getMany([...new Set(i.map(e=>e.data.url))]);await B(e).putMany(i.map(e=>({id:e.id,data:J(e.data,t.get(e.data.url),n)})))}r.hasMore&&r.cursor?t.cursor=r.cursor:(t.cursor=void 0,t.phase=`live-home`,t.liveTotal=await V(e).count({status:`published`,pass:t.pass}))}async function Fe(e,n,r,i,a,o){let s=[];for(let e of a.menu){let t=G(e);t&&s.push({id:t.id,base:t,source:{t:`Site menu`,p:``,menu:!0},extra:{}})}for(let[e,t]of[[a.active,`embed`],[a.passive,`image`]])for(let n of e)s.push({id:x(`insecure`,n),base:{kind:`insecure`,url:n,host:``},source:{t:i.title,p:i.path},extra:q(t)});for(let e of o)s.push({id:x(`page`,e.path),base:{kind:`page`,url:e.path,host:``},source:{t:e.title,p:e.path},extra:{}});if(s.length===0)return;let c=[...new Set(s.map(e=>e.id))],l=await B(e).getMany(c),u=s.filter(e=>e.base.kind===`internal`).map(e=>e.base.url),d=u.length>0?await V(e).getMany([...new Set(u)]):new Map,f=new Map;for(let e of s){let i=K(f.get(e.id)??l.get(e.id),e.base,e.source,n.pass,r,e.extra);if(i.kind===`internal`&&(i=J(i,d.get(i.url),r),i.state===`broken`&&(i.group=t(i))),i.kind===`page`){let t=o.find(e=>e.path===i.url);t&&(i=z(l.get(e.id)??i,{status:t.status},r)),i.pass=n.pass}i.state===`broken`&&!i.since&&(i.since=H(r)),f.set(e.id,i)}await B(e).putMany([...f].map(([e,t])=>({id:e,data:t})))}function Ie(e){let t=e.status??0;if(t<200||t>=300)return!1;let n=e.headers?.get(`content-type`)??``;if(n!==``&&!/html/i.test(n))return!1;let r=(e.body??``).slice(0,2e3);return!/error code: ?\d{3,4}|cf-error-details|Worker threw exception|cloudflare-nginx/i.test(r)}function Le(e){return e.status===503?/Database migrations are required|database has not been migrated/i.test((e.body??``).slice(0,2e3)):!1}async function Re(e,t,n){let r=C(e.site.url);if(!r){t.live=`not-public`,t.phase=`live-paths`;return}let i=await P(e,`${r}/`,{body:!0}),a=i.status??0;if(i.status===void 0){t.live=Ce(i)?`not-public`:`failed`,t.phase=`live-paths`;return}if(Le(i)){t.live=`updating`,t.phase=`live-paths`;return}if(a===403||a===429||a===503||a===401){t.live=`blocked`,t.phase=`live-paths`;return}if(!Ie(i)){t.live=`failed`,t.phase=`live-paths`;return}t.live=`ok`;let o=(i.headers?.get(`cache-control`)??``).toLowerCase();t.caching=/no-store|private/.test(o)||i.headers?.has(`set-cookie`)?`no`:`ok`;let s=j(i.body??``,e.site.url);await Fe(e,t,n,{path:`/`,title:`Home`},{menu:s.menu.map(e=>({...e,use:`link`})),active:s.active,passive:s.passive},a>=500?[{path:`/`,title:`Home`,status:a}]:[]),t.phase=`live-pages`,t.cursor=void 0}async function ze(e,t,n){let r=C(e.site.url),i=await V(e).query({where:{status:`published`},limit:2,...t.cursor?{cursor:t.cursor}:{}});if(r&&i.items.length>0){let a=await Promise.all(i.items.map(async t=>({page:t.data,outcome:await P(e,`${r}${t.data.path}`,{body:!0})}))),o=[],s=[],c=[],l=null;for(let{page:t,outcome:n}of a)if((n.status??0)>=500&&c.push({path:t.path,title:t.title,status:n.status??500}),n.body){let r=j(n.body,e.site.url);o.push(...r.active),s.push(...r.passive),(r.active.length||r.passive.length)&&!l&&(l=t)}await Fe(e,t,n,l??{path:``,title:``},{menu:[],active:o,passive:s},c),t.liveDone+=i.items.length}i.hasMore&&i.cursor?t.cursor=i.cursor:(t.cursor=void 0,t.phase=`live-paths`)}async function Be(t,n,r){let i=C(t.site.url),a=n.live===`ok`&&i!==null,o=await B(t).query({where:{kind:`internal`,state:`pending`},limit:a?2:100});if(o.items.length===0){n.phase=`checklist`;return}let s=await Promise.all(o.items.map(async n=>{if(!a)return{id:n.id,data:{...n.data,state:`unknown`,reason:`no-live`,due:e}};let o=await P(t,`${i}${n.data.url}`);return{id:n.id,data:De(n.data,F(n.data.url,o,`GET`),r)}}));await B(t).putMany(s)}async function Ve(e,t){let[n,r]=await Promise.all([B(e).query({where:{pass:{lt:t.pass}},limit:100}),V(e).query({where:{pass:{lt:t.pass}},limit:100})]);await Promise.all([n.items.length>0?B(e).deleteMany(n.items.map(e=>e.id)):0,r.items.length>0?V(e).deleteMany(r.items.map(e=>e.id)):0]),!n.hasMore&&!r.hasMore&&(t.phase=`count`)}async function He(e,t){let n=B(e),[r,i,a,o,s,c]=await Promise.all([V(e).count({status:`published`}),n.count({kind:{in:[`internal`,`external`]}}),n.count({state:`broken`,group:`site`}),n.count({state:`broken`,group:`page`}),n.count({state:`broken`,group:`link`}),n.count({state:`broken`,editor:!0})]);t.counts={pages:r,links:i,fix:{site:a,page:o,link:s},editorFix:c,look:0,unknown:0},t.phase=`freeze`}async function Ue(e,t){let n=B(e),[r,i,a,o]=await Promise.all([n.query({where:{state:`look`},limit:10}),n.query({where:{state:`unknown`},limit:10}),n.count({state:`look`}),n.count({state:`unknown`})]);t.counts={...t.counts??We,look:a,unknown:o},t.lists={pass:t.pass,lookRows:r.items.map(({id:e,data:t})=>d(e,t)),unknownRows:i.items.map(({id:e,data:t})=>d(e,t))}}const We={pages:0,links:0,fix:{site:0,page:0,link:0},editorFix:0,look:0,unknown:0};async function Ge(e,n,r,i){let a=await B(e).query({where:{state:`broken`},orderBy:{count:`desc`},limit:100}),o={site:[],page:[],link:[]};for(let{id:e,data:n}of a.items){let r=n.group??t(n);o[r].length<10&&o[r].push(d(e,{...n,group:r}))}let s=n.counts??We,c={...s,pass:n.pass,finishedAt:H(i),...r.summary?{prevFinishedAt:r.summary.finishedAt}:{},rows:o,lookRows:n.lists?.lookRows??[],unknownRows:n.lists?.unknownRows??[],live:n.live??`failed`};n.status=`done`,n.finishedAt=H(i);let l=s.pages===0&&!r.summary;return n.nextAt=H(i+(l?6e5:Ae)),n.counts=void 0,n.lists=void 0,n.phase=`redirects`,l?null:c}async function Y(e,t,n,r=Date.now()){let i=t.scan;if(!i||i.status!==`running`)return i;let a={...i,lastStepAt:H(r)},o=null;switch(a.phase){case`redirects`:await Me(e,a);break;case`walk`:await Ne(e,a,r);break;case`resolve`:await Pe(e,a,r);break;case`live-home`:await Re(e,a,r);break;case`live-pages`:await ze(e,a,r);break;case`live-paths`:await Be(e,a,r);break;case`checklist`:await Oe(e,a,t,r),a.phase=`sweep`;break;case`sweep`:await Ve(e,a);break;case`count`:await He(e,a);break;case`freeze`:a.lists?.pass===a.pass?o=await Ge(e,a,t,r):await Ue(e,a);break}return o&&(await e.kv.set(c.summary,o),t.summary=o,await oe(e,t,o,r)),a.ops=n.ops+1,await e.kv.set(c.scan,a),t.scan=a,a}const Ke=6e4;async function qe(e,t,n,r=Date.now()){let i=await B(e).query({where:{due:{lte:H(r)}},orderBy:{due:`asc`},limit:20}),a=C(e.site.url),o={...t.queue.hosts},s=[],l=new Set;for(let e of i.items){if(s.length>=3)break;let n=e.data;if(n.state!==`fine`){if(n.kind===`external`){if(!t.options.outbound)continue;let e=o[n.host];if(l.has(n.host)||e&&r-e.at<Ke*(e.slow&&e.slow>=2?1440:1))continue;l.add(n.host)}else if(n.kind!==`page`||!a||t.summary?.live!==`ok`)continue;s.push({id:e.id,row:n})}}let u=await Promise.all(s.map(async({id:t,row:n})=>{if(n.kind===`page`)return{id:t,before:n,after:z(n,await P(e,`${a}${n.url}`),r)};let i=n.get?`GET`:`HEAD`,s=await P(e,n.url,{method:i}),c=F(n.url,s,i);return o[n.host]={at:r,...o[n.host]?.slow?{slow:o[n.host].slow}:{}},{id:t,before:n,after:null,result:c,outcome:s}}));for(let t of u){if(t.after||!(`result`in t)||!t.result)continue;let i=t.result;if(i.kind===`retry`&&ue(n,5)&&(i=F(t.before.url,await P(e,t.before.url,{method:`GET`}),`GET`)),i.kind===`retry`){t.after={...t.before,due:H(r+Ke),get:!0};continue}if(i.kind===`unknown`&&i.reason===`slow`){let e=o[t.before.host]??{at:r};o[t.before.host]={...e,slow:(e.slow??0)+1}}t.after=Ee(t.before,i,r),t.result.kind===`retry`&&(t.after.get=!0)}for(let[e,t]of Object.entries(o))r-t.at>Ae&&!t.slow&&delete o[e];u.length>0&&await B(e).putMany(u.map(e=>({id:e.id,data:e.after})));let d=t.summary;if(d){for(let e of u){let t=m(d,e.id,e.before,e.after);t&&(d=t)}d!==t.summary&&(await e.kv.set(c.summary,d),t.summary=d,await oe(e,t,d,r))}return t.queue={hosts:o,cronAt:H(r),ops:n.ops+1},await e.kv.set(c.queue,t.queue),u.length}function Je(e,t){let n=Math.ceil(e/50)+(t?Math.ceil(e/2):0)+3;if(n<60)return`about ${Math.max(5,Math.round(n/5)*5)} minutes`;let r=Math.round(n/60);return r===1?`about an hour`:`about ${r} hours`}async function Ye(e,n,r,i,a=Date.now()){if(!i.scan)return;let o=i.scan.collections.find(e=>e.slug===n);if(!o)return;let s=o.routable?xe(o.pattern,n,r):null,l=je(r,o.titleField),u=r.status===`published`?k(r.data,e.site.url):[],d={c:n,id:r.id,t:l,p:s??``},f=new Map;for(let e of u){let t=G(e);t&&!(t.kind===`internal`&&t.url===s)&&f.set(t.id,{base:t,use:e.use})}let p=[...f.values()].filter(e=>e.base.kind===`internal`).map(e=>e.base.url),h=[...new Set([...s?[s]:[],...p])],g=h.length>0?await V(e).getMany(h):new Map,_=s?g.get(s)??null:null,v=[...new Set([...f.keys(),..._?.links??[]])].slice(0,400),y=v.length>0?await B(e).getMany(v):new Map,b=[],x=i.summary;for(let e of v){let n=y.get(e),o=f.get(e),s=null;if(o){let e=n?n.sources.filter(e=>e.id!==r.id):[],c=n?{...n,sources:e,count:Math.max(e.length,n.count-(n.sources.length-e.length))}:void 0;s=K(c,o.base,d,n?.pass??i.scan.pass,a,o.base.kind===`insecure`?q(o.use):{}),c&&(s={...s,sources:[...e,d].slice(0,10),count:c.count+1,pass:n.pass}),s.kind===`internal`&&(s=J(s,g.get(s.url),a)),s.kind===`internal`&&s.state===`pending`&&n?.state!==`pending`&&(s={...s,state:n?.state===`broken`&&n.reason===`missing`?`broken`:`unknown`,reason:n?.reason??`no-live`},s.state===`broken`&&(s.group=t(s)))}else if(n){let e=n.sources.filter(e=>e.id!==r.id);s=e.length===0&&!n.menu?null:{...n,sources:e,count:Math.max(e.length,n.count-1)}}s&&s.state===`broken`&&!s.since&&(s.since=H(a)),s&&b.push({id:e,data:s}),x&&=m(x,e,n,s)??x}let S=v.filter(e=>!f.has(e)&&!b.some(t=>t.id===e));await Promise.all([b.length>0?B(e).putMany(b):void 0,S.length>0?B(e).deleteMany(S):void 0,s?V(e).put(s,{status:r.status,path:s,title:l,collection:n,id:r.id,pass:_?.pass??i.scan.pass,links:[...f.keys()].slice(0,200)}):void 0]),x&&x!==i.summary&&await e.kv.set(c.summary,x)}const Xe=10*6e4,Ze=[{group:`site`,title:`Every page`,text:`In the site menu or footer, so every visitor can hit it. Fix these first.`},{group:`page`,title:`One page`,text:`Visitors who open these pages see an error or an empty box.`},{group:`link`,title:`One link`,text:`A link inside a page that goes nowhere.`}];function Qe(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function X(e,t=Date.now()){if(!e)return``;let n=Math.round((t-Date.parse(e))/6e4);if(Number.isNaN(n))return``;if(n<2)return`just now`;if(n<60)return`${n} minutes ago`;let r=Math.round(n/60);if(r<24)return r===1?`an hour ago`:`${r} hours ago`;let i=Math.round(r/24);return i===1?`yesterday`:`${i} days ago`}function Z(e,t,n=`${t}s`){return`${e} ${e===1?t:n}`}function $e(e,t){let i=t?.prevFinishedAt!==void 0&&e.since!==void 0&&e.since>t.prevFinishedAt,a=(e.reason===`gone`||e.reason===`no-site`)&&e.since?` Checked twice since ${new Date(e.since).toLocaleDateString(`en-GB`,{day:`numeric`,month:`short`,timeZone:`UTC`})}.`:``;return`${i?`New since the last check. `:``}${n(e,S(e.url))}${a}${r(e)}`}function et(e,t){let n=[],r=e.kind===`internal`||e.kind===`page`;return t&&r&&e.url!==`/`&&e.reason!==`draft`&&e.reason!==`page-error`&&n.push({label:`Send visitors to another page`,value:`redirect|${e.id}`}),e.menu&&n.push({label:`Change the link in Menus`,value:`menus|${e.id}`}),e.sources.filter(e=>e.c&&e.id).slice(0,3).forEach((t,r)=>n.push({label:`Edit ${t.t}`,value:`edit${r}|${e.id}`})),n.push({label:`Check it again now`,value:`check|${e.id}`}),n.push({label:`This is fine, stop checking it`,value:`fine|${e.id}`}),{type:`menu`,action_id:`fix`,label:`Fix`,items:n}}function tt(e,t){let n=C(t)??g(t)?.origin,r=e.kind===`internal`?n?`${n}${e.url}`:null:e.url;if(!(!r||!/^https?:\/\//.test(r)))return{type:`link`,label:`Open`,target:{kind:`external`,url:r}}}function nt(e,t,n,r){return{type:`table`,page_action_id:e,columns:[{key:`text`,label:t},{key:`fix`,label:``,format:`element`}],rows:n.map(e=>({text:e.text,...e.action?{fix:e.action}:{}})),...r?{next_cursor:r}:{}}}function rt(e){let t=e.scan,n=!t||[`redirects`,`walk`,`resolve`].includes(t.phase),r=t?.liveTotal??t?.walked??0,i=[{type:`banner`,title:`Checking your site for the first time`,description:`${n?`This usually takes 5 to 30 minutes, and a few hours for a very large site.`:`This takes ${Je(r,t?.live===`ok`)} for a site with ${Z(r,`page`)}.`} Links to other sites follow a day later: each one is checked twice, a day apart, before we call it broken. You can leave this page.`}];return t&&!n&&t.live===`ok`&&(t.liveTotal??0)>0?i.push({type:`meter`,label:`Pages checked`,value:Math.min(t.liveDone,t.liveTotal??0),max:t.liveTotal??0,custom_value:`${Math.min(t.liveDone,t.liveTotal??0)} of ${Z(t.liveTotal??0,`page`)}`}):t&&!n&&i.push({type:`context`,text:`${Z(t.walked,`page`)} read. Finishing off.`}),i.push({type:`context`,text:`Started ${X(t?.startedAt)||`just now`}. Each page is also checked again when it is published.`},{type:`actions`,elements:[{type:`button`,action_id:`refresh`,label:`Refresh`}]}),i}function it(e,t){return e.live===`not-public`?[{type:`banner`,title:`This site is not public yet`,description:`${t?`Its address is ${t}, which only this computer or network can open.`:`It has no address set yet.`} Links inside your pages are checked now; the rest starts once the site has a public address.`}]:e.live===`updating`?[{type:`banner`,variant:`alert`,title:`Your site is in the middle of an update`,description:`During the check that finished ${X(e.finishedAt)}, your site showed visitors an error page instead of your pages: an update to its database had not finished. Whoever looks after your site should finish the update. Links inside your pages are still checked; menus and page errors are checked again once your site is back.`}]:e.live===`blocked`||e.live===`failed`?[{type:`banner`,variant:`alert`,title:`We cannot open your pages to check them`,description:`Links inside your pages are still checked. Menus, page errors and insecure items are not. If your site has bot protection, ask whoever set it up to let "EmDash Site Health" in.`}]:[]}function at(e){let t=e.panel;if(!t)return[];if(t.kind===`redirect`){let n=e.pickable??[];return[{type:`header`,text:`Send visitors to another page`},{type:`context`,text:`Anyone who opens ${t.url}, from your menu, a bookmark or Google, lands on the page you pick. This fixes it everywhere at once.`},...e.problem?[{type:`banner`,variant:`alert`,title:`Not added`,description:e.problem}]:[],{type:`form`,block_id:`redirect`,fields:[{type:`text_input`,action_id:`source`,label:`Old address`,initial_value:t.url},{type:`combobox`,action_id:`destination`,label:`Send visitors to`,placeholder:`Pick a page, or type an address`,options:n}],submit:{label:`Send visitors there`,action_id:`add_redirect|${t.id}`}},{type:`actions`,elements:[{type:`button`,action_id:`close`,label:`Close`}]}]}return t.kind===`edit`?[{type:`banner`,title:`Change the link to ${S(t.url)} on ${t.title}`,description:`Open the page, find the words that link to ${S(t.url)} and point them at the right page: select the words, then use the link button.`},{type:`actions`,elements:[{type:`link`,label:`Open ${t.title}`,appearance:`primary`,target:{kind:`content`,collection:t.collection,id:t.entry}},{type:`button`,action_id:`close`,label:`Close`}]}]:[{type:`banner`,title:`Change the link to ${S(t.url)} in your menus`,description:`Open Menus in the sidebar, under Manage. Find the item that goes to this address and point it at the right page. If the address is on this site, sending visitors to another page also fixes it.`},{type:`actions`,elements:[{type:`button`,action_id:`close`,label:`Close`}]}]}function ot(e,t,n,r){let{summary:i,scan:a}=t;if(!i)return rt(t);let o=[...n?it(i,e.site.url):[],...at(r)],s=e=>n||e.editor,c=n?h(i):i.editorFix;o.push({type:`stats`,items:[{label:`Pages checked`,value:i.pages},{label:`Links checked`,value:i.links},{label:`To fix`,value:c}]});let l=!1;for(let{group:e,title:t,text:a}of Ze){let c=r.more?.group===e?r.more:void 0,u=(c?c.rows:i.rows[e]).filter(s);if(u.length===0)continue;l=!0;let d=n?i.fix[e]:u.length,f=c?c.cursor:d>u.length?`start`:void 0;o.push({type:`header`,text:t},{type:`context`,text:a},nt(`more_${e}`,`Problem`,u.map(e=>({text:$e(e,i),action:et(e,n)})),f))}l||o.push({type:`empty`,size:`sm`,title:`Nothing broken. Nice work.`,description:`We checked ${Z(i.pages,`page`)} and ${Z(i.links,`link`)} ${X(i.finishedAt)}. Next check: tomorrow.`});let u=i.lookRows.filter(s);u.length>0&&o.push({type:`accordion`,label:`Worth a look (${n?i.look:u.length})`,blocks:[{type:`context`,text:`These work, but take a detour. Tidier to fix, not urgent.`},nt(`look_table`,`What we noticed`,u.map(e=>({text:$e(e,null),action:et(e,n)})))]});let d=i.unknownRows.filter(s);d.length>0&&o.push({type:`accordion`,label:`Links we could not check (${n?i.unknown:d.length})`,blocks:[{type:`context`,text:`Some sites, such as LinkedIn and Instagram, turn away automatic checkers. These links are probably fine. Open one to see for yourself; we try again next week.`},nt(`unknown_table`,`What happened`,d.map(t=>{let r=t.kind===`internal`?et(t,n):tt(t,e.site.url);return{text:$e(t,null),...r?{action:r}:{}}}))]});let f=a?.status===`running`;return o.push({type:`context`,text:f?`Checking again now. These are the results of the last full check, ${X(i.finishedAt)}.`:`Last full check: ${X(i.finishedAt)}. Pages on this site are checked every day, links to other sites every week.`}),n||o.push({type:`context`,text:`Pages that show errors and the site's setup are looked after by your site admin.`}),o.push({type:`actions`,elements:[{type:`button`,action_id:f?`refresh`:`check_all`,label:f?`Refresh`:`Check everything again`}]}),o}const st={working:`Working`,attention:`Needs attention`,untested:`Not tested yet`,unknown:`Could not tell`};function ct(e,t){let n=[e.queue.cronAt,e.scan?.cronAt].filter(e=>!!e).map(Date.parse),r=n.length>0?Math.max(...n):0,i=e.scan?Date.parse(e.scan.startedAt):t;return r>0&&t-r<Xe?{status:`working`,text:`Last ran ${Z(Math.max(1,Math.round((t-r)/6e4)),`minute`)} ago. Scheduled posts and these checks depend on it.`}:r===0&&t-i<Xe?{status:`untested`,text:`Waiting for the first run. Scheduled posts and these checks depend on it.`}:{status:`attention`,text:`Not running. Scheduled posts will not publish, and these checks only move while this page is open.`}}function lt(e){return e.length<=2?`Goes to ${e.join(` and `)}.`:`Goes to ${e.slice(0,2).join(`, `)} and ${e.length-2} more.`}function ut(e,t,n=Date.now()){let r=e.options.recipients??[],i=e.alerts.email;if(!t)return{status:`attention`,text:`Not set up. Your site has no email service chosen, so alerts, form messages and password resets are not sent. Whoever set up your site can choose one in Settings, Email.`};if(r.length===0)return{status:`attention`,text:`Nobody is on the list, so no alerts are sent. Add an address under Options below.`};let a=lt(r);if(!i)return{status:`untested`,text:`Not checked yet. ${a} To find out whether email works, press Send a test email to check now.`};let o=X(i.at,n),s=i.kind===`alert`?`an alert`:`a test email`;return i.state===`working`?(i.list?i.list.length===r.length&&i.list.every(e=>r.includes(e)):i.to===r.length)?{status:`working`,text:`Last checked ${o}: ${s} was sent. ${a} To check again, press Send a test email.`}:{status:`untested`,text:`The list has changed since the last email, ${o}. ${a} To find out whether it reaches them, press Send a test email to check now.`}:T.test(i.error??``)?{status:`attention`,text:`Last checked ${o}: your site had no email service chosen, so nothing was sent. If one has been chosen in Settings, Email since, press Send a test email to check now.`}:{status:`attention`,text:`Last checked ${o}: ${s} could not be sent; the email service turned it down. ${a} Press Send a test email to try again; if it fails, whoever set up your site can look at Settings, Email.`}}function dt(e,t,n,r=Date.now()){let i=t?g(t):null,a=i?i.protocol===`https:`?{status:`working`,text:`Set, secure, and the address visitors use.`}:{status:C(i.href)?`attention`:`unknown`,text:C(i.href)?`Set, but not secure. Ask your web person to switch it to https.`:`Set to an address only this computer or network can open.`}:{status:`attention`,text:`Not set. Set it in Settings so links and emails point to your site.`},o=e.checklist.rows;return[...e.summary?.live===`updating`?[{name:`Site up`,status:`attention`,text:`During the check that finished ${X(e.summary.finishedAt,r)}, your site showed visitors an error page: an update to its database had not finished. Whoever looks after your site should finish the update.`}]:[],{name:`Site address`,...a},{name:`Scheduled tasks`,...ct(e,r)},{name:`Email`,...ut(e,n,r)},{name:`Secure connection`,...o.secure??{status:`untested`,text:`Checked with the next full check.`}},{name:`With and without www`,...o.www??{status:`untested`,text:`Checked with the next full check.`}},{name:`Missing pages`,...o.missing??{status:`untested`,text:`Checked with the next full check.`}},{name:`Search engines`,...o.search??{status:`untested`,text:`Checked with the next full check.`}},{name:`Quick repeat visits`,...o.caching??{status:`untested`,text:`Checked with the next full check.`}}]}function ft(e,t,n,r,i){let a=dt(e,t,n,i),o=a.filter(e=>e.status===`attention`&&e.name!==`Quick repeat visits`),s=[...a].sort((e,t)=>Number(t.status===`attention`)-Number(e.status===`attention`)),c=[];a.find(e=>e.name===`Scheduled tasks`)?.status===`attention`?c.push({type:`banner`,variant:`alert`,title:`Scheduled tasks are not running`,description:`Scheduled posts will not publish, and these checks only move while this page is open. Whoever set up your site can switch them on in a few minutes.`},{type:`accordion`,label:`Copy the instructions for your web person`,blocks:[{type:`context`,text:`Send the steps below to whoever set up your site: they switch on the scheduled tasks your site needs.`},{type:`code`,code:`This EmDash site's scheduled tasks are not running (the Site Health task has not run for 10 minutes).
4
4
  On Cloudflare: add a Cron Trigger to the Worker, for example in wrangler.jsonc:
5
5
  "triggers": { "crons": ["* * * * *"] }
6
6
  and make sure the Worker's entry exports EmDash's scheduled() handler.
7
7
  On Node: EmDash runs scheduled tasks inside the server process; check the server stays running.
8
8
  Then run: npx emdash doctor
9
- See EmDash's deployment guide, "Scheduled tasks".`,language:`bash`}]}):o.length>0&&c.push({type:`banner`,variant:`alert`,title:`${K(o.length,`thing needs`,`things need`)} attention`,description:o[0]?.text??``});let l=a.find(e=>e.name===`Email`);c.push({type:`table`,page_action_id:`checklist_table`,columns:[{key:`check`,label:`Check`},{key:`status`,label:`Status`,format:`badge`}],rows:s.filter(e=>e!==l).map(e=>({check:`${e.name}. ${e.text}`,status:Qe[e.status]}))},...l?[{type:`section`,text:`Email: ${Qe[l.status]}. ${l.text}`,accessory:{type:`button`,action_id:`send_test`,label:`Send a test email`}}]:[],{type:`context`,text:e.checklist.checkedAt?`Checked ${G(e.checklist.checkedAt)}. Changes you make elsewhere show here after the next check.`:`The rest is checked with the next full check.`});let u=e.options.recipients??[],d=r.fine??[];return c.push({type:`accordion`,label:`Options`,default_open:u.length===0||(r.toast?.message?.startsWith(`Saved`)??!1),blocks:[{type:`form`,block_id:`options`,fields:[{type:`text_input`,action_id:`recipients`,label:`Who gets emails (one address per line)`,multiline:!0,initial_value:u.join(`
10
- `)},{type:`toggle`,action_id:`outbound`,label:`Check links to other sites (recommended; a few each minute, never one site more than once a minute)`,initial_value:e.options.outbound}],submit:{label:`Save`,action_id:`save_options`}},{type:`context`,text:`Emails go out at once when a page shows an error or a menu link breaks, and once a week with anything new. Never more than one email a day of each kind.`},{type:`header`,text:`Links you marked as fine (${d.length})`},d.length===0?{type:`context`,text:`None yet. Pick "This is fine, stop checking it" in a Fix menu to add one.`}:{type:`table`,page_action_id:`fine_table`,columns:[{key:`text`,label:`Address`},{key:`again`,label:``,format:`element`}],rows:d.map(({id:e,row:t})=>({text:`${_(t.url,24)} Marked by ${t.fine?.by??`someone`}, ${new Date(t.fine?.at??Date.now()).toLocaleDateString(`en-GB`,{day:`numeric`,month:`short`,timeZone:`UTC`})}.`,again:{type:`button`,action_id:`check_fine`,label:`Check it again`,value:e}}))}]}),c}function X(e,t,n,r){let i={type:`context`,text:`Finds broken links and setup problems before your visitors do.`},a=Ze(e,t,n,r);return{blocks:n?[i,{type:`tab`,default_tab:r.tab,panels:[{label:`Problems`,blocks:a},{label:`Checklist`,blocks:rt(t,e.site.url,!!e.email,r,Date.now())}]}]:[i,...a],...r.toast?{toast:r.toast}:{}}}function it(e,t,r,i){let a={type:`actions`,elements:[{type:`link`,label:`Open Site Health`,target:{kind:`plugin-page`,path:`/health`}}]},{summary:o,scan:s}=e;if(!o){let e=s?.liveTotal;return{blocks:[{type:`section`,text:s&&e&&s.live===`ok`?`First check in progress: ${Math.min(s.liveDone,e)} of ${K(e,`page`)}. Results in ${Ue(Math.max(0,e-s.liveDone),!0)}.`:`First check in progress. Results soon.`},a]}}let c=t?u(o):o.editorFix;if(!t)return{blocks:[{type:`section`,text:c===0?`Nothing broken. Checked ${G(o.finishedAt)}.`:`${K(c,`broken link`)} on your pages.`},a]};let l=nt(e,r,i).filter(e=>e.status===`attention`&&e.name!==`Quick repeat visits`).length;if(c===0&&l===0)return{blocks:[{type:`section`,text:`Nothing broken. Checked ${G(o.finishedAt)}.`},a]};let d=o.rows.site[0]??o.rows.page[0]??o.rows.link[0];return{blocks:[{type:`stats`,items:[{label:`To fix`,value:c},{label:`Setup`,value:l}]},...d?[{type:`context`,text:`Worst: ${_(d.url)}${d.menu||d.url===`/`?` is broken on every page.`:` ${n(d,``).replace(/^[: ]+/,``)}`}`}]:[],a]}}async function at(e){let t=await L(e).query({where:{status:`published`},limit:100}),n=t.hasMore&&t.cursor?await L(e).query({where:{status:`published`},limit:100,cursor:t.cursor}):{items:[]};return[...t.items,...n.items].map(({data:e})=>({label:`${e.title} (${e.path})`,value:e.path})).sort((e,t)=>e.value===`/`?-1:t.value===`/`?1:e.label.localeCompare(t.label))}async function ot(e){return(await I(e).query({where:{state:`fine`},limit:20})).items.map(({id:e,data:t})=>({id:e,row:t}))}function st(e,t){if(e)return[...e.rows.site,...e.rows.page,...e.rows.link,...e.lookRows,...e.unknownRows].find(e=>e.id===t)}async function Z(e,t,n,r,a){if(await I(e).put(n,a),t.summary){let o=l(t.summary,n,r,a);o&&(t.summary=o,await e.kv.set(i.summary,o))}}async function ct(n,r,i,a){let o=await I(n).get(i);if(!o)return{type:`info`,message:`That address is no longer on your pages.`};let s=r.summary?.live===`ok`?v(n.site.url):null,c=o,l=o.state===`fine`?{...o,state:`pending`,fine:void 0}:o;if(o.kind===`external`){let e=j(o.url,await A(n,o.url,{method:`HEAD`}),`HEAD`);e.kind===`retry`&&(e=j(o.url,await A(n,o.url),`GET`)),c=xe(l,e,a)}else if(o.kind===`internal`)c=H(l,await L(n).get(o.url)??void 0,a),c.state===`pending`&&(c=s?Se(c,j(o.url,await A(n,`${s}${o.url}`),`GET`),a):{...c,state:`unknown`,reason:`no-live`,due:e});else if(o.kind===`page`)c=s?F({...l,fails:o.state===`broken`?1:0,firstFailAt:new Date(a-6e4).toISOString()},await A(n,`${s}${o.url}`),a):o;else{let e=o.sources[0],t=!0;if(e?.c&&e.id){let r=await n.content?.get(e.c,e.id);t=!!r&&T(r.data,n.site.url).some(e=>e.url===o.url)}else if(s&&e){let r=await A(n,`${s}${e.p||`/`}`,{body:!0}),i=r.body?D(r.body,n.site.url):null;t=!i||[...i.active,...i.passive].includes(o.url)}c=t?l:{...l,state:`ok`,reason:void 0,group:void 0,since:void 0}}c.state===`broken`&&(c.group=t(c)),await Z(n,r,i,o,c);let u=_(o.url);return c.state===`ok`||c.state===`look`?{type:`success`,message:`Works now. ${u} is off the list.`}:c.state===`broken`?{type:`info`,message:`Still broken. ${u} was checked just now.`}:{type:`info`,message:`We could not check ${u} just now. It is not counted as broken.`}}async function lt(e,t,n,r,i=!1){try{if(!t.scan||!t.summary&&t.scan.status===`done`){t.scan=await z(e,t,r),await U(e,t,n,r);return}let a=Date.parse(t.scan.lastStepAt??t.scan.startedAt);t.scan.status===`running`&&(i||r-a>12e4)&&await U(e,t,n,r)}catch(t){e.log.warn(`could not move the check along`,{error:String(t)})}}async function ut(t,n,r,s){let c=Date.now(),l=await a(r),u=(n?.role??0)>=50,d=W(t)?t:{},f=typeof d.action_id==`string`?d.action_id:``,p={tab:0};if(d.type===`page_load`&&typeof d.page==`string`&&d.page.startsWith(`widget:`))return await lt(r,l,s,c),it(l,u,r.site.url,!!r.email);u&&l.options.recipients===void 0&&n?.email?(l.options={...l.options,recipients:[n.email.toLowerCase()]},await r.kv.set(i.options,l.options)):(d.type===`page_load`||f===`refresh`)&&await lt(r,l,s,c,f===`refresh`);let h=async e=>u?{...e,fine:await ot(r)}:e;if(d.type===`block_action`){if(f===`fix`&&typeof d.value==`string`){let[t=``,i=``]=d.value.split(`|`),a=st(l.summary,i);if(!a)return X(r,l,u,{tab:0,toast:{type:`info`,message:`That problem is no longer listed.`}});if(t===`redirect`&&u)return X(r,l,u,{tab:0,panel:{kind:`redirect`,id:i,url:a.url},pickable:await at(r)});if(t===`menus`)return X(r,l,u,await h({tab:0,panel:{kind:`menus`,url:a.url}}));if(t.startsWith(`edit`)){let e=a.sources.filter(e=>e.c&&e.id)[Number(t.slice(4))];if(e?.c&&e.id)return X(r,l,u,await h({tab:0,panel:{kind:`edit`,url:a.url,title:e.t,collection:e.c,entry:e.id}}))}if(t===`check`)return X(r,l,u,await h({tab:0,toast:await ct(r,l,i,c)}));if(t===`fine`){let t=await I(r).get(i);if(t){let a=n?.name||n?.email||`someone`;await Z(r,l,i,t,{...t,state:`fine`,due:e,fine:{by:a,at:new Date(c).toISOString()}})}return X(r,l,u,await h({tab:0,toast:{type:`success`,message:`Marked as fine. ${_(a.url)} is not checked any more.`}}))}}if(f===`check_fine`&&typeof d.value==`string`&&u)return X(r,l,u,await h({tab:1,toast:await ct(r,l,d.value,c)}));if(f===`check_all`)return l.scan?.status!==`running`&&(l.scan=await z(r,l,c),await U(r,l,s,c)),X(r,l,u,await h({tab:0,toast:{type:`info`,message:`Checking everything again. The last results stay here until it is done.`}}));if(f===`send_test`&&u){if((l.options.recipients??[]).length===0)return X(r,l,u,await h({tab:1,toast:{type:`error`,message:`Add an address under Options first.`}}));let e=await se(r,l,c);return X(r,l,u,await h({tab:1,toast:e.ok?{type:`success`,message:`Test sent to ${K(l.options.recipients?.length??0,`address`,`addresses`)}.`}:{type:`info`,message:S.test(e.error)?`Your site has no email service chosen. Whoever set it up can choose one in Settings, Email.`:`The test email could not be sent. The Email row says what to do.`}}))}let t=Ke.find(e=>f===`more_${e.group}`)?.group;if(t&&l.summary){let e=W(d.value)?d.value:{},n=typeof e.cursor==`string`&&e.cursor!==`start`?e.cursor:void 0,i=await I(r).query({where:{state:`broken`,group:t},orderBy:{count:`desc`},limit:50,...n?{cursor:n}:{}});return X(r,l,u,await h({tab:0,more:{group:t,rows:i.items.map(({id:e,data:t})=>o(e,t)),...i.hasMore&&i.cursor?{cursor:i.cursor}:{}}}))}return X(r,l,u,await h({tab:f===`close`?0:p.tab}))}if(d.type===`form_submit`&&u){let e=W(d.values)?d.values:{};if(f===`save_options`){let t=te(typeof e.recipients==`string`?e.recipients:(l.options.recipients??[]).join(`
11
- `));return`wrong`in t?X(r,l,u,await h({tab:1,toast:{type:`error`,message:`"${t.wrong}" is not an email address. Nothing was saved.`}})):(l.options={recipients:t.list,outbound:typeof e.outbound==`boolean`?e.outbound:l.options.outbound},await r.kv.set(i.options,l.options),X(r,l,u,await h({tab:1,toast:{type:`success`,message:t.list.length===0?`Saved. Nobody gets emails now.`:`Saved. Emails go to ${K(t.list.length,`address`,`addresses`)}.`}})))}if(f.startsWith(`add_redirect|`)){let t=f.slice(13),n=st(l.summary,t),i=m(typeof e.source==`string`?e.source.trim():n?.url??``),a=typeof e.destination==`string`?e.destination.trim():``,o=a.startsWith(`/`)?m(a):a,s=async e=>X(r,l,u,{tab:0,panel:{kind:`redirect`,id:t,url:i||n?.url||``},pickable:await at(r),problem:e});if(!i.startsWith(`/`))return s(`The old address must be a page on this site, starting with /.`);if(!o.startsWith(`/`)&&!/^https?:\/\//.test(o))return s(`Pick a page to send visitors to.`);if(o===i)return s(`The old address and the new one are the same.`);if((await L(r).get(i))?.status===`published`)return s(`${i} is a live page. Change its address in the page editor instead.`);try{await r.redirects?.create?.({source:i,destination:o,type:301})}catch(e){return s(`EmDash did not add it: ${e instanceof Error?e.message:String(e)}`)}let d={status:`redirect`,path:i,title:i,dest:o,pass:l.scan?.pass??0};await L(r).put(i,d);let p=await I(r).get(t);return p&&await Z(r,l,t,p,H(p,d,c)),X(r,l,u,await h({tab:0,toast:{type:`success`,message:`Done. ${i} now goes to ${o}.`}}))}}return X(r,l,u,await h(p))}const dt=[`kv`,`content`,`schema`,`redirects`,`http`,`email`,`cron`];function ft(e,t){return new Proxy(e,{get(e,n,r){let i=Reflect.get(e,n,r);return typeof i==`function`?(...n)=>(t.ops+=1,i.apply(e,n)):i}})}function Q(e,t){let n={};for(let r of dt){let i=e[r];i&&(n[r]=ft(i,t))}let r=new Map,i=new Proxy(e.storage,{get(e,n,i){let a=Reflect.get(e,n,i);if(typeof n!=`string`||!a||typeof a!=`object`)return a;let o=r.get(n);return o||(o=ft(a,t),r.set(n,o)),o}});return new Proxy(e,{get(e,t,r){return t===`storage`?i:typeof t==`string`&&t in n?n[t]:Reflect.get(e,t,r)}})}async function pt(e,t=Date.now()){let n={ops:0},r=Q(e,n),o=await a(r),s=new Date(t).toISOString();if(ae(o,t)){await oe(r,o,t),o.queue={...o.queue,cronAt:s,ops:n.ops+1},await r.kv.set(i.queue,o.queue);return}let c=o.scan;if(!c||c.status===`done`&&c.nextAt!==void 0&&c.nextAt<=s){o.scan={...await z(r,o,t),cronAt:s},await U(r,o,n,t);return}let l=!o.summary||(o.queue.cronAt??``)>=(c?.cronAt??``);if(c?.status===`running`&&l){o.scan={...c,cronAt:s},await U(r,o,n,t);return}await He(r,o,n,t)}async function $(e,t,n,r=!1){if(typeof n==`string`)try{let i=Q(e,{ops:0}),o=await a(i);await We(i,t,(r?null:await i.content?.get(t,n))??{id:n,slug:null,status:`deleted`,data:{}},o)}catch(r){e.log.warn(`could not re-read the page's links`,{collection:t,id:n,error:String(r)})}}const mt=e=>e?.id,ht={hooks:{"plugin:activate":async(e,t)=>{try{await Ee(t);let e={ops:0},n=Q(t,e),r=await a(n);if(r.scan)return;r.scan=await z(n,r),await U(n,r,e)}catch(e){t.log.warn(`could not start the first check`,{error:String(e)})}},cron:async(e,t)=>{e.name===we&&await pt(t)},"content:afterSave":async(e,t)=>{await $(t,e.collection,mt(e.content))},"content:afterPublish":async(e,t)=>{await $(t,e.collection,mt(e.content))},"content:afterUnpublish":async(e,t)=>{await $(t,e.collection,mt(e.content))},"content:afterDelete":async(e,t)=>{await $(t,e.collection,e.id,!0)}},routes:{admin:{permission:`plugins:read`,handler:async(e,t)=>{let n={ops:0};return ut(e.input,e.user,Q(t,n),n)}}}};export{pt as cronRun,ht as default};
9
+ See EmDash's deployment guide, "Scheduled tasks".`,language:`bash`}]}):o.length>0&&c.push({type:`banner`,variant:`alert`,title:`${Z(o.length,`thing needs`,`things need`)} attention`,description:o[0]?.text??``});let l=a.find(e=>e.name===`Email`);c.push({type:`table`,page_action_id:`checklist_table`,columns:[{key:`check`,label:`Check`},{key:`status`,label:`Status`,format:`badge`}],rows:s.filter(e=>e!==l).map(e=>({check:`${e.name}. ${e.text}`,status:st[e.status]}))},...l?[{type:`section`,text:`Email: ${st[l.status]}. ${l.text}`,accessory:{type:`button`,action_id:`send_test`,label:`Send a test email`}}]:[],{type:`context`,text:e.checklist.checkedAt?`Checked ${X(e.checklist.checkedAt)}. Changes you make elsewhere show here after the next check.`:`The rest is checked with the next full check.`});let u=e.options.recipients??[],d=r.fine??[];return c.push({type:`accordion`,label:`Options`,default_open:u.length===0||(r.toast?.message?.startsWith(`Saved`)??!1),blocks:[{type:`form`,block_id:`options`,fields:[{type:`text_input`,action_id:`recipients`,label:`Who gets emails (one address per line)`,multiline:!0,initial_value:u.join(`
10
+ `)},{type:`toggle`,action_id:`outbound`,label:`Check links to other sites (recommended; a few each minute, never one site more than once a minute)`,initial_value:e.options.outbound}],submit:{label:`Save`,action_id:`save_options`}},{type:`context`,text:`Emails go out at once when a page shows an error or a menu link breaks, and once a week with anything new. Never more than one email a day of each kind.`},{type:`header`,text:`Links you marked as fine (${d.length})`},d.length===0?{type:`context`,text:`None yet. Pick "This is fine, stop checking it" in a Fix menu to add one.`}:{type:`table`,page_action_id:`fine_table`,columns:[{key:`text`,label:`Address`},{key:`again`,label:``,format:`element`}],rows:d.map(({id:e,row:t})=>({text:`${S(t.url,24)} Marked by ${t.fine?.by??`someone`}, ${new Date(t.fine?.at??Date.now()).toLocaleDateString(`en-GB`,{day:`numeric`,month:`short`,timeZone:`UTC`})}.`,again:{type:`button`,action_id:`check_fine`,label:`Check it again`,value:e}}))}]}),c}function Q(e,t,n,r){let i={type:`context`,text:`Finds broken links and setup problems before your visitors do.`},a=ot(e,t,n,r);return{blocks:n?[i,{type:`tab`,default_tab:r.tab,panels:[{label:`Problems`,blocks:a},{label:`Checklist`,blocks:ft(t,e.site.url,!!e.email,r,Date.now())}]}]:[i,...a],...r.toast?{toast:r.toast}:{}}}function pt(e,t,r,i){let a={type:`actions`,elements:[{type:`link`,label:`Open Site Health`,target:{kind:`plugin-page`,path:`/health`}}]},{summary:o,scan:s}=e;if(!o){let e=s?.liveTotal;return{blocks:[{type:`section`,text:s&&e&&s.live===`ok`?`First check in progress: ${Math.min(s.liveDone,e)} of ${Z(e,`page`)}. Results in ${Je(Math.max(0,e-s.liveDone),!0)}.`:`First check in progress. Results soon.`},a]}}let c=t?h(o):o.editorFix;if(!t)return{blocks:[{type:`section`,text:c===0?`Nothing broken. Checked ${X(o.finishedAt)}.`:`${Z(c,`broken link`)} on your pages.`},a]};let l=dt(e,r,i).filter(e=>e.status===`attention`&&e.name!==`Quick repeat visits`).length;if(c===0&&l===0)return{blocks:[{type:`section`,text:`Nothing broken. Checked ${X(o.finishedAt)}.`},a]};let u=o.rows.site[0]??o.rows.page[0]??o.rows.link[0];return{blocks:[{type:`stats`,items:[{label:`To fix`,value:c},{label:`Setup`,value:l}]},...u?[{type:`context`,text:`Worst: ${S(u.url)}${u.menu||u.url===`/`?` is broken on every page.`:` ${n(u,``).replace(/^[: ]+/,``)}`}`}]:[],a]}}async function mt(e){let t=await V(e).query({where:{status:`published`},limit:100}),n=t.hasMore&&t.cursor?await V(e).query({where:{status:`published`},limit:100,cursor:t.cursor}):{items:[]};return[...t.items,...n.items].map(({data:e})=>({label:`${e.title} (${e.path})`,value:e.path})).sort((e,t)=>e.value===`/`?-1:t.value===`/`?1:e.label.localeCompare(t.label))}async function ht(e){return(await B(e).query({where:{state:`fine`},limit:20})).items.map(({id:e,data:t})=>({id:e,row:t}))}function gt(e,t){if(e)return[...e.rows.site,...e.rows.page,...e.rows.link,...e.lookRows,...e.unknownRows].find(e=>e.id===t)}async function _t(e,t,n,r,i){if(await B(e).put(n,i),t.summary){let a=m(t.summary,n,r,i);a&&(t.summary=a,await e.kv.set(c.summary,a))}}async function vt(n,r,i,a){let o=await B(n).get(i);if(!o)return{type:`info`,message:`That address is no longer on your pages.`};let s=r.summary?.live===`ok`?C(n.site.url):null,c=o,l=o.state===`fine`?{...o,state:`pending`,fine:void 0}:o;if(o.kind===`external`){let e=F(o.url,await P(n,o.url,{method:`HEAD`}),`HEAD`);e.kind===`retry`&&(e=F(o.url,await P(n,o.url),`GET`)),c=Ee(l,e,a)}else if(o.kind===`internal`)c=J(l,await V(n).get(o.url)??void 0,a),c.state===`pending`&&(c=s?De(c,F(o.url,await P(n,`${s}${o.url}`),`GET`),a):{...c,state:`unknown`,reason:`no-live`,due:e});else if(o.kind===`page`)c=s?z({...l,fails:o.state===`broken`?1:0,firstFailAt:new Date(a-6e4).toISOString()},await P(n,`${s}${o.url}`),a):o;else{let e=o.sources[0],t=!0;if(e?.c&&e.id){let r=await n.content?.get(e.c,e.id);t=!!r&&k(r.data,n.site.url).some(e=>e.url===o.url)}else if(s&&e){let r=await P(n,`${s}${e.p||`/`}`,{body:!0}),i=r.body?j(r.body,n.site.url):null;t=!i||[...i.active,...i.passive].includes(o.url)}c=t?l:{...l,state:`ok`,reason:void 0,group:void 0,since:void 0}}c.state===`broken`&&(c.group=t(c)),await _t(n,r,i,o,c);let u=S(o.url);return c.state===`ok`||c.state===`look`?{type:`success`,message:`Works now. ${u} is off the list.`}:c.state===`broken`?{type:`info`,message:`Still broken. ${u} was checked just now.`}:{type:`info`,message:`We could not check ${u} just now. It is not counted as broken.`}}async function yt(e,t,n,r,i=!1){if(ue(n,8))try{if(!t.scan||!t.summary&&t.scan.status===`done`){t.scan=await U(e,t,r),await Y(e,t,n,r);return}let a=Date.parse(t.scan.lastStepAt??t.scan.startedAt);t.scan.status===`running`&&(i||r-a>12e4)&&await Y(e,t,n,r)}catch(t){e.log.warn(`could not move the check along`,{error:String(t)})}}async function bt(t,n,r,i){let a=Date.now(),o=(n?.role??0)>=50,c=Qe(t)?t:{},f=typeof c.action_id==`string`?c.action_id:``,p={tab:0},m=c.type===`page_load`&&typeof c.page==`string`&&c.page.startsWith(`widget:`),h=await u(r,{copy:o&&!m});if(m)return await yt(r,h,i,a),pt(h,o,r.site.url,!!r.email);o&&h.options.recipients===void 0&&n?.email?(h.options={...h.options,recipients:[n.email.toLowerCase()]},await s(r,l,h.options)):(c.type===`page_load`||f===`refresh`)&&await yt(r,h,i,a,f===`refresh`);let g=async e=>o?{...e,fine:await ht(r)}:e;if(c.type===`block_action`){if(f===`fix`&&typeof c.value==`string`){let[t=``,i=``]=c.value.split(`|`),s=gt(h.summary,i);if(!s)return Q(r,h,o,{tab:0,toast:{type:`info`,message:`That problem is no longer listed.`}});if(t===`redirect`&&o)return Q(r,h,o,{tab:0,panel:{kind:`redirect`,id:i,url:s.url},pickable:await mt(r)});if(t===`menus`)return Q(r,h,o,await g({tab:0,panel:{kind:`menus`,url:s.url}}));if(t.startsWith(`edit`)){let e=s.sources.filter(e=>e.c&&e.id)[Number(t.slice(4))];if(e?.c&&e.id)return Q(r,h,o,await g({tab:0,panel:{kind:`edit`,url:s.url,title:e.t,collection:e.c,entry:e.id}}))}if(t===`check`)return Q(r,h,o,await g({tab:0,toast:await vt(r,h,i,a)}));if(t===`fine`){let t=await B(r).get(i);if(t){let o=n?.name||n?.email||`someone`;await _t(r,h,i,t,{...t,state:`fine`,due:e,fine:{by:o,at:new Date(a).toISOString()}})}return Q(r,h,o,await g({tab:0,toast:{type:`success`,message:`Marked as fine. ${S(s.url)} is not checked any more.`}}))}}if(f===`check_fine`&&typeof c.value==`string`&&o)return Q(r,h,o,await g({tab:1,toast:await vt(r,h,c.value,a)}));if(f===`check_all`)return h.scan?.status!==`running`&&(h.scan=await U(r,h,a),await Y(r,h,i,a)),Q(r,h,o,await g({tab:0,toast:{type:`info`,message:`Checking everything again. The last results stay here until it is done.`}}));if(f===`send_test`&&o){if((h.options.recipients??[]).length===0)return Q(r,h,o,await g({tab:1,toast:{type:`error`,message:`Add an address under Options first.`}}));let e=await le(r,h,a);return Q(r,h,o,await g({tab:1,toast:e.ok?{type:`success`,message:`Test sent to ${Z(h.options.recipients?.length??0,`address`,`addresses`)}.`}:{type:`info`,message:T.test(e.error)?`Your site has no email service chosen. Whoever set it up can choose one in Settings, Email.`:`The test email could not be sent. The Email row says what to do.`}}))}let t=Ze.find(e=>f===`more_${e.group}`)?.group;if(t&&h.summary){let e=Qe(c.value)?c.value:{},n=typeof e.cursor==`string`&&e.cursor!==`start`?e.cursor:void 0,i=await B(r).query({where:{state:`broken`,group:t},orderBy:{count:`desc`},limit:50,...n?{cursor:n}:{}});return Q(r,h,o,await g({tab:0,more:{group:t,rows:i.items.map(({id:e,data:t})=>d(e,t)),...i.hasMore&&i.cursor?{cursor:i.cursor}:{}}}))}return Q(r,h,o,await g({tab:f===`close`?0:p.tab}))}if(c.type===`form_submit`&&o){let e=Qe(c.values)?c.values:{};if(f===`save_options`){let t=re(typeof e.recipients==`string`?e.recipients:(h.options.recipients??[]).join(`
11
+ `));return`wrong`in t?Q(r,h,o,await g({tab:1,toast:{type:`error`,message:`"${t.wrong}" is not an email address. Nothing was saved.`}})):(h.options={recipients:t.list,outbound:typeof e.outbound==`boolean`?e.outbound:h.options.outbound},await s(r,l,h.options),Q(r,h,o,await g({tab:1,toast:{type:`success`,message:t.list.length===0?`Saved. Nobody gets emails now.`:`Saved. Emails go to ${Z(t.list.length,`address`,`addresses`)}.`}})))}if(f.startsWith(`add_redirect|`)){let t=f.slice(13),n=gt(h.summary,t),i=y(typeof e.source==`string`?e.source.trim():n?.url??``),s=typeof e.destination==`string`?e.destination.trim():``,c=s.startsWith(`/`)?y(s):s,l=async e=>Q(r,h,o,{tab:0,panel:{kind:`redirect`,id:t,url:i||n?.url||``},pickable:await mt(r),problem:e});if(!i.startsWith(`/`))return l(`The old address must be a page on this site, starting with /.`);if(!c.startsWith(`/`)&&!/^https?:\/\//.test(c))return l(`Pick a page to send visitors to.`);if(c===i)return l(`The old address and the new one are the same.`);if((await V(r).get(i))?.status===`published`)return l(`${i} is a live page. Change its address in the page editor instead.`);try{await r.redirects?.create?.({source:i,destination:c,type:301})}catch(e){return l(`EmDash did not add it: ${e instanceof Error?e.message:String(e)}`)}let u={status:`redirect`,path:i,title:i,dest:c,pass:h.scan?.pass??0};await V(r).put(i,u);let d=await B(r).get(t);return d&&await _t(r,h,t,d,J(d,u,a)),Q(r,h,o,await g({tab:0,toast:{type:`success`,message:`Done. ${i} now goes to ${c}.`}}))}}return Q(r,h,o,await g(p))}async function xt(e,t=Date.now()){let n={ops:0},r=O(e,n),i=await u(r),a=new Date(t).toISOString();if(se(i,t)){await ce(r,i,t),i.queue={...i.queue,cronAt:a,ops:n.ops+1},await r.kv.set(c.queue,i.queue);return}let o=i.scan;if(!o||o.status===`done`&&o.nextAt!==void 0&&o.nextAt<=a){i.scan={...await U(r,i,t),cronAt:a},await Y(r,i,n,t);return}let s=!i.summary||(i.queue.cronAt??``)>=(o?.cronAt??``);if(o?.status===`running`&&s){i.scan={...o,cronAt:a},await Y(r,i,n,t);return}await qe(r,i,n,t)}async function $(e,t,n,r=!1){if(typeof n!=`string`)return;let i=O(e,{ops:0});try{let e=await u(i);await Ye(i,t,(r?null:await i.content?.get(t,n))??{id:n,slug:null,status:`deleted`,data:{}},e)}catch(e){i.log.warn(`could not re-read the page's links`,{collection:t,id:n,error:String(e)})}}const St=e=>e?.id,Ct={hooks:{"plugin:activate":async(e,t)=>{let n={ops:0},r=O(t,n);try{let e=await u(r);if(e.scan){await W(r);return}e.scan=await U(r,e),await Y(r,e,n)}catch(e){r.log.warn(`could not start the first check`,{error:String(e)}),await W(r)}},cron:async(e,t)=>{e.name===ke&&await xt(t)},"content:afterSave":async(e,t)=>{await $(t,e.collection,St(e.content))},"content:afterPublish":async(e,t)=>{await $(t,e.collection,St(e.content))},"content:afterUnpublish":async(e,t)=>{await $(t,e.collection,St(e.content))},"content:afterDelete":async(e,t)=>{await $(t,e.collection,e.id,!0)}},routes:{admin:{permission:`plugins:read`,handler:async(e,t)=>{let n={ops:0};return bt(e.input,e.user,O(t,n),n)}}}};export{xt as cronRun,Ct as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moorage/plugin-site-health",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Site Health for EmDash: a broken link checker for your pages, menus and links to other sites, with email alerts and a setup checklist.",
5
5
  "keywords": [
6
6
  "emdash",