@reddoorla/maintenance 0.41.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/bin.js +331 -17
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/forms/index.d.ts +11 -2
- package/dist/forms/index.js +1 -1
- package/dist/forms/index.js.map +1 -1
- package/dist/index.d.ts +33 -2
- package/dist/index.js +321 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2781,7 +2781,18 @@ var DEFAULT_COPY = {
|
|
|
2781
2781
|
"Hosting, DNS, and SSL configured",
|
|
2782
2782
|
"Continuous integration + automatic dependency updates",
|
|
2783
2783
|
"Analytics and uptime monitoring"
|
|
2784
|
-
]
|
|
2784
|
+
],
|
|
2785
|
+
announceHeading: "YOUR ONGOING SITE CARE",
|
|
2786
|
+
announceBody: "We've completed a full test of your site and set it up for ongoing care to keep it fast, secure, and healthy. Here's what you can expect from us going forward:",
|
|
2787
|
+
announceCadenceHeading: "WHAT TO EXPECT",
|
|
2788
|
+
announceTestingLabel: "Full site testing",
|
|
2789
|
+
announceMaintenanceLabel: "Routine maintenance",
|
|
2790
|
+
announceMonitorItems: ["Performance", "Accessibility", "Security", "Uptime"],
|
|
2791
|
+
announcePreviewLabel: "From your latest full site test:",
|
|
2792
|
+
announceImprovementResend: "Your contact forms now deliver straight to your inbox through reliable infrastructure, so no inquiry slips through the cracks.",
|
|
2793
|
+
announceImprovementSvelte5: "We've modernized your site to the latest framework \u2014 it's faster, more secure, and built to last.",
|
|
2794
|
+
announceCadence: "After each one we'll send you a short report like this \u2014 there's nothing you need to do.",
|
|
2795
|
+
announceOpenDoor: "And if you'd ever like to expand the scope, add features, or freshen anything up, just reply \u2014 we'd love to help."
|
|
2785
2796
|
};
|
|
2786
2797
|
function override(v) {
|
|
2787
2798
|
if (typeof v !== "string") return null;
|
|
@@ -3116,16 +3127,162 @@ function buildLaunchMjml(data) {
|
|
|
3116
3127
|
</mjml>`;
|
|
3117
3128
|
}
|
|
3118
3129
|
|
|
3130
|
+
// src/reports/announcement-email/template.ts
|
|
3131
|
+
var FREQ_PHRASE = {
|
|
3132
|
+
Monthly: "every month",
|
|
3133
|
+
Quarterly: "every quarter",
|
|
3134
|
+
Yearly: "every year"
|
|
3135
|
+
};
|
|
3136
|
+
var RED2 = "#C00";
|
|
3137
|
+
var GREY2 = "#757575";
|
|
3138
|
+
var SCORE_PREVIEW = [
|
|
3139
|
+
{ label: "Performance", key: "performance" },
|
|
3140
|
+
{ label: "Readability", key: "accessibility" },
|
|
3141
|
+
{ label: "Best Practices", key: "bestPractices" },
|
|
3142
|
+
{ label: "Site Structure", key: "seo" }
|
|
3143
|
+
];
|
|
3144
|
+
function buildAnnouncementMjml(data) {
|
|
3145
|
+
const copy = data.copy ?? DEFAULT_COPY;
|
|
3146
|
+
const previewText = "Your monthly report from Reddoor";
|
|
3147
|
+
const improvementItems = [];
|
|
3148
|
+
if (data.improvements?.resendForms) improvementItems.push(copy.announceImprovementResend);
|
|
3149
|
+
if (data.improvements?.svelte5) improvementItems.push(copy.announceImprovementSvelte5);
|
|
3150
|
+
const improvementsSection = improvementItems.length > 0 ? `
|
|
3151
|
+
<mj-section background-color="white">
|
|
3152
|
+
<mj-column>
|
|
3153
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">RECENT IMPROVEMENTS</mj-text>
|
|
3154
|
+
${improvementItems.map(
|
|
3155
|
+
(item) => `
|
|
3156
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="4px" padding-bottom="4px">\u2022 ${escapeXml(item)}</mj-text>`
|
|
3157
|
+
).join("")}
|
|
3158
|
+
</mj-column>
|
|
3159
|
+
</mj-section>` : "";
|
|
3160
|
+
const cad = data.cadence;
|
|
3161
|
+
const cadenceLines = [];
|
|
3162
|
+
if (cad && cad.testing !== "None")
|
|
3163
|
+
cadenceLines.push(`${copy.announceTestingLabel} \u2014 ${FREQ_PHRASE[cad.testing]}`);
|
|
3164
|
+
if (cad && cad.maintenance !== "None")
|
|
3165
|
+
cadenceLines.push(`${copy.announceMaintenanceLabel} \u2014 ${FREQ_PHRASE[cad.maintenance]}`);
|
|
3166
|
+
const cadenceSection = cadenceLines.length > 0 ? `
|
|
3167
|
+
<mj-section background-color="white">
|
|
3168
|
+
<mj-column>
|
|
3169
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">${escapeXml(copy.announceCadenceHeading)}</mj-text>
|
|
3170
|
+
${cadenceLines.map(
|
|
3171
|
+
(line) => `
|
|
3172
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="4px" padding-bottom="4px">\u2022 ${escapeXml(line)}</mj-text>`
|
|
3173
|
+
).join("")}
|
|
3174
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="12px">${escapeXml(copy.announceCadence)}</mj-text>
|
|
3175
|
+
</mj-column>
|
|
3176
|
+
</mj-section>` : "";
|
|
3177
|
+
const monitorRows = copy.announceMonitorItems.map(
|
|
3178
|
+
(item) => `
|
|
3179
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="4px" padding-bottom="4px">\u2022 ${escapeXml(item)}</mj-text>`
|
|
3180
|
+
).join("");
|
|
3181
|
+
const scoreRows = SCORE_PREVIEW.map(
|
|
3182
|
+
({ label, key }) => `
|
|
3183
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="300" padding-top="25px">${label}</mj-text>
|
|
3184
|
+
<mj-text color="${RED2}" font-size="44px" font-weight="400" padding-top="0px">${data.lighthouse[key]}</mj-text>`
|
|
3185
|
+
).join("");
|
|
3186
|
+
const contactRows = copy.contact.map(
|
|
3187
|
+
(line) => `
|
|
3188
|
+
<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml(line)}</mj-text>`
|
|
3189
|
+
).join("");
|
|
3190
|
+
const footerAddressRows = copy.footerAddress.map(
|
|
3191
|
+
(line) => `
|
|
3192
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">${escapeXml(line)}</mj-text>`
|
|
3193
|
+
).join("");
|
|
3194
|
+
return `<mjml>
|
|
3195
|
+
<mj-head>
|
|
3196
|
+
<mj-attributes>
|
|
3197
|
+
<mj-text font-family="helvetica, sans-serif" padding-left="5px" padding-right="5px" />
|
|
3198
|
+
<mj-section padding-left="11%" padding-right="11%"/>
|
|
3199
|
+
<mj-image padding="0px" />
|
|
3200
|
+
</mj-attributes>
|
|
3201
|
+
<mj-preview>${escapeXml(previewText)}</mj-preview>
|
|
3202
|
+
${headerStyleBlock(data)}
|
|
3203
|
+
</mj-head>
|
|
3204
|
+
<mj-body background-color="white">
|
|
3205
|
+
<mj-section background-color="#F4F4F4" padding-top="0px" padding-bottom="0px" padding-left="0px" padding-right="0px">
|
|
3206
|
+
<mj-column>${headerImageTag(data)}</mj-column>
|
|
3207
|
+
</mj-section>
|
|
3208
|
+
<mj-section background-color="white">
|
|
3209
|
+
<mj-column>
|
|
3210
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="75px">${escapeXml(copy.announceHeading)}</mj-text>
|
|
3211
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="20px">Prepared for ${escapeXml(data.siteName)}</mj-text>
|
|
3212
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="8px">${escapeXml(copy.announceBody)}</mj-text>
|
|
3213
|
+
</mj-column>
|
|
3214
|
+
</mj-section>
|
|
3215
|
+
${cadenceSection}
|
|
3216
|
+
${improvementsSection}
|
|
3217
|
+
<mj-section background-color="white">
|
|
3218
|
+
<mj-column>
|
|
3219
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="36px">WHAT WE MONITOR</mj-text>
|
|
3220
|
+
${monitorRows}
|
|
3221
|
+
</mj-column>
|
|
3222
|
+
</mj-section>
|
|
3223
|
+
<mj-section background-color="#F4F4F4">
|
|
3224
|
+
<mj-column>
|
|
3225
|
+
<mj-text color="${RED2}" font-size="20px" font-weight="700" padding-top="55px">${escapeXml(copy.announcePreviewLabel)}</mj-text>
|
|
3226
|
+
${scoreRows}
|
|
3227
|
+
</mj-column>
|
|
3228
|
+
</mj-section>
|
|
3229
|
+
<mj-section background-color="white">
|
|
3230
|
+
<mj-column>
|
|
3231
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="36px">${escapeXml(copy.announceOpenDoor)}</mj-text>
|
|
3232
|
+
</mj-column>
|
|
3233
|
+
</mj-section>
|
|
3234
|
+
<mj-section background-color="white">
|
|
3235
|
+
<mj-column padding-top="36px">
|
|
3236
|
+
<mj-text color="${RED2}" font-family="helvetica, sans-serif" font-size="24px" font-weight="700" padding-top="36px" line-height="36px">Any questions, concerns or requests?</mj-text>
|
|
3237
|
+
${contactRows}
|
|
3238
|
+
<mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
|
|
3239
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" line-height="20px" font-style="italic">Copyright ${(/* @__PURE__ */ new Date()).getUTCFullYear()} ${escapeXml(copy.footerOrg)}. All rights reserved.</mj-text>
|
|
3240
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="700" line-height="16px" padding-top="0" padding-bottom="0px">Our mailing address is:</mj-text>
|
|
3241
|
+
<mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">${escapeXml(copy.footerOrg)}</mj-text>
|
|
3242
|
+
${footerAddressRows}
|
|
3243
|
+
</mj-column>
|
|
3244
|
+
</mj-section>
|
|
3245
|
+
</mj-body>
|
|
3246
|
+
</mjml>`;
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3119
3249
|
// src/reports/render.ts
|
|
3120
3250
|
async function renderReportHtml(data) {
|
|
3121
|
-
const mjml = data.reportType === "Launch" ? buildLaunchMjml(data) : buildMjml(data);
|
|
3251
|
+
const mjml = data.reportType === "Launch" ? buildLaunchMjml(data) : data.reportType === "Announcement" ? buildAnnouncementMjml(data) : buildMjml(data);
|
|
3122
3252
|
const out = await mjml2html(mjml, { validationLevel: "strict" });
|
|
3123
3253
|
return { html: out.html, warnings: out.errors ?? [] };
|
|
3124
3254
|
}
|
|
3125
3255
|
|
|
3256
|
+
// src/reports/checklist.ts
|
|
3257
|
+
var MAINTENANCE_CHECKLIST = [
|
|
3258
|
+
{ key: "logs", label: "Reviewed Logs", field: "Maint: Reviewed Logs" },
|
|
3259
|
+
{ key: "cms", label: "CMS Checked", field: "Maint: CMS Checked" },
|
|
3260
|
+
{ key: "dns", label: "DNS Checked", field: "Maint: DNS Checked" },
|
|
3261
|
+
{ key: "google", label: "Google Indexed", field: "Maint: Google Indexed" },
|
|
3262
|
+
{ key: "cert", label: "Reviewed Certificate", field: "Maint: Reviewed Certificate" },
|
|
3263
|
+
{ key: "security", label: "Security Updates", field: "Maint: Security Updates" }
|
|
3264
|
+
];
|
|
3265
|
+
var TESTING_CHECKLIST = [
|
|
3266
|
+
{ key: "desktop", label: "Desktop Browsers", field: "Test: Desktop Browsers" },
|
|
3267
|
+
{ key: "mobile", label: "Mobile Browsers", field: "Test: Mobile Browsers" },
|
|
3268
|
+
{ key: "packages", label: "Package Updates", field: "Test: Package Updates" },
|
|
3269
|
+
{ key: "bottle", label: "Bottlenecks", field: "Test: Bottlenecks" },
|
|
3270
|
+
{ key: "forms", label: "Form Functionality", field: "Test: Form Functionality" },
|
|
3271
|
+
{ key: "animation", label: "Animation Functionality", field: "Test: Animation Functionality" }
|
|
3272
|
+
];
|
|
3273
|
+
var ALL_CHECKLIST_FIELDS = [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map(
|
|
3274
|
+
(i) => i.field
|
|
3275
|
+
);
|
|
3276
|
+
function checklistFor(type) {
|
|
3277
|
+
return type === "Maintenance" ? MAINTENANCE_CHECKLIST : type === "Testing" ? TESTING_CHECKLIST : [];
|
|
3278
|
+
}
|
|
3279
|
+
function isChecklistComplete(report) {
|
|
3280
|
+
return checklistFor(report.reportType).every((i) => report.checklist[i.field] === true);
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3126
3283
|
// src/reports/airtable/reports.ts
|
|
3127
3284
|
var REPORTS_TABLE = "Reports";
|
|
3128
|
-
var REPORT_TYPES = ["Maintenance", "Testing", "Launch"];
|
|
3285
|
+
var REPORT_TYPES = ["Maintenance", "Testing", "Launch", "Announcement"];
|
|
3129
3286
|
function toReportType(raw) {
|
|
3130
3287
|
if (raw && REPORT_TYPES.includes(raw)) return raw;
|
|
3131
3288
|
if (raw)
|
|
@@ -3163,7 +3320,8 @@ function mapRow2(rec) {
|
|
|
3163
3320
|
approvedBy: f["Approved By"] ?? null,
|
|
3164
3321
|
deliveryStatus: f["Delivery status"] ?? "pending",
|
|
3165
3322
|
renderedHtmlAttachment: html,
|
|
3166
|
-
resendMessageId: f["Resend message ID"] ?? null
|
|
3323
|
+
resendMessageId: f["Resend message ID"] ?? null,
|
|
3324
|
+
checklist: Object.fromEntries(ALL_CHECKLIST_FIELDS.map((name) => [name, Boolean(f[name])]))
|
|
3167
3325
|
};
|
|
3168
3326
|
}
|
|
3169
3327
|
function lighthouseFromFields(f) {
|
|
@@ -3198,6 +3356,7 @@ async function createDraft(base, input) {
|
|
|
3198
3356
|
if (input.searchFoundPage1 !== void 0) fields["Search found page 1"] = input.searchFoundPage1;
|
|
3199
3357
|
if (input.searchPosition !== void 0) fields["Search position"] = input.searchPosition;
|
|
3200
3358
|
if (input.period !== void 0) fields["Period"] = input.period;
|
|
3359
|
+
if (input.subjectOverride !== void 0) fields["Subject override"] = input.subjectOverride;
|
|
3201
3360
|
const created = await base(REPORTS_TABLE).create([{ fields }]);
|
|
3202
3361
|
const rec = created[0];
|
|
3203
3362
|
if (!rec) throw new Error("Airtable create returned no records");
|
|
@@ -3687,6 +3846,13 @@ async function sendApprovedReports(options = {}) {
|
|
|
3687
3846
|
return { output: lines.join("\n"), code: anyFailed ? 1 : 0 };
|
|
3688
3847
|
}
|
|
3689
3848
|
async function sendOne(client, base, site, report) {
|
|
3849
|
+
if (!isChecklistComplete(report)) {
|
|
3850
|
+
const items = checklistFor(report.reportType);
|
|
3851
|
+
const done = items.filter((i) => report.checklist[i.field] === true).length;
|
|
3852
|
+
throw new Error(
|
|
3853
|
+
`Report ${report.reportId} checklist incomplete \u2014 ${done}/${items.length} items checked`
|
|
3854
|
+
);
|
|
3855
|
+
}
|
|
3690
3856
|
if (!site.headerImage) {
|
|
3691
3857
|
throw new Error(`Site '${site.name}' has no Header image set on the Websites row`);
|
|
3692
3858
|
}
|
|
@@ -3903,7 +4069,33 @@ function relativeTimeFromNow(iso, now = /* @__PURE__ */ new Date()) {
|
|
|
3903
4069
|
var REDDOOR_FAVICON_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAACXBIWXMAAAAnAAAAJwEqCZFPAAAAk1BMVEVHcEzkGTfjGDbjGTfjGTfjGTfjGTfjGDbjGDbjGTfjGTfjGTfkGTfjGTfjGDbjGDbkGTfjGDbjGDbjGTfjGTfjGDbjGTfjGDbjGTfjGDbjGTfjGTfjGDbjGDbjGDbjGDbjGTfjGTbjGDbjGDbjGDbjGDbjGTfjGTfjGTfjGDbjGDbjGTfjGDbjGDbjGTfjGTbkGTfxxbwzAAAALXRSTlMA4DABMOD8cOBwDltwi+bFLMlk54/BDVPk/I61/u2cFaBw9nqO1izBIQE+Becdo6eEAAABBElEQVQ4y91SCVYCMQwt4kxbcEHFfUFFcMGf9P6nM0kXEPEAmtdJs+cnU+f+He2fIIEAYuVgkmvDfTG9Rh+6LoRBUBqE7gi09l9eAeeTrZJoFd5uQWfj4VbPvRqwmj9zfzP6CYpyi48F6HW5A3WuMHsEPsfvO8cSkMPTe+pfRr/MTckdg+4enqL3PkYf/YFIqiiP8VAw6EZkH6QEO0xJLUhmdbY5+TjHkCVohprlcpzlnJw9GlNkrZC16uCmWIaAZItNrT4xV0T2yxrIWoNKy4rdVXOq0MycWp4r43FOMQWciibMYaOHCXKSPhapqTupGFAnwHoVuUUZq7jaSkqDb0/uD9MXqvJMDtU7lL0AAAAASUVORK5CYII=";
|
|
3904
4070
|
var FAVICON_LINK = `<link rel="icon" type="image/png" href="data:image/png;base64,${REDDOOR_FAVICON_PNG_BASE64}" />`;
|
|
3905
4071
|
|
|
4072
|
+
// src/dashboard/onboarding.ts
|
|
4073
|
+
function isNonEmpty(s) {
|
|
4074
|
+
return typeof s === "string" && s.trim().length > 0;
|
|
4075
|
+
}
|
|
4076
|
+
function onboardingStatus(row) {
|
|
4077
|
+
const checks = {
|
|
4078
|
+
firstAudit: isNonEmpty(row.lastLighthouseAuditAt),
|
|
4079
|
+
recipients: isNonEmpty(row.reportRecipientsTo),
|
|
4080
|
+
schedule: row.maintenanceFreq !== "None",
|
|
4081
|
+
poc: isNonEmpty(row.pointOfContact)
|
|
4082
|
+
};
|
|
4083
|
+
const score = Object.values(checks).filter(Boolean).length;
|
|
4084
|
+
return { score, total: 4, checks };
|
|
4085
|
+
}
|
|
4086
|
+
var ONBOARDING_LABELS = {
|
|
4087
|
+
firstAudit: "First audit",
|
|
4088
|
+
recipients: "Report recipients",
|
|
4089
|
+
schedule: "Maintenance schedule",
|
|
4090
|
+
poc: "Point of contact"
|
|
4091
|
+
};
|
|
4092
|
+
function missingOnboarding(row) {
|
|
4093
|
+
const { checks } = onboardingStatus(row);
|
|
4094
|
+
return Object.keys(ONBOARDING_LABELS).filter((key) => !checks[key]).map((key) => ONBOARDING_LABELS[key]);
|
|
4095
|
+
}
|
|
4096
|
+
|
|
3906
4097
|
// src/dashboard/render.ts
|
|
4098
|
+
var DASH = "\u2014";
|
|
3907
4099
|
function scoreTile(label, value) {
|
|
3908
4100
|
const display = value === null ? "\u2014" : String(value);
|
|
3909
4101
|
return `<div class="tile"><div class="tile-value">${escapeHtml(display)}</div><div class="tile-label">${escapeHtml(label)}</div></div>`;
|
|
@@ -3936,10 +4128,25 @@ function securitySub(site) {
|
|
|
3936
4128
|
const l = site.securityVulnsLow ?? 0;
|
|
3937
4129
|
return `${c}C / ${h}H / ${m}M / ${l}L`;
|
|
3938
4130
|
}
|
|
4131
|
+
function checklistBlock(r) {
|
|
4132
|
+
const items = checklistFor(r.reportType);
|
|
4133
|
+
if (items.length === 0) return "";
|
|
4134
|
+
const rid = escapeHtml(r.id);
|
|
4135
|
+
const url = `/api/reports/${encodeURIComponent(r.id)}/checklist`;
|
|
4136
|
+
const boxes = items.map((item) => {
|
|
4137
|
+
const checked = r.checklist[item.field] === true ? " checked" : "";
|
|
4138
|
+
return `<label class="check-item"><input type="checkbox" class="checklist-checkbox" data-checklist-report-id="${rid}" data-field="${escapeHtml(item.field)}" data-checklist-url="${escapeHtml(url)}"${checked} /> ${escapeHtml(item.label)}</label>`;
|
|
4139
|
+
}).join("");
|
|
4140
|
+
return `<div class="checklist" data-checklist-for="${rid}">${boxes}</div>`;
|
|
4141
|
+
}
|
|
4142
|
+
function approveButton(r) {
|
|
4143
|
+
const disabled = isChecklistComplete(r) ? "" : " disabled";
|
|
4144
|
+
return `<button class="approve" data-report-id="${escapeHtml(r.id)}" data-approve-url="/api/reports/${encodeURIComponent(r.id)}/approve"${disabled}>Approve</button>`;
|
|
4145
|
+
}
|
|
3939
4146
|
function pendingRow(r) {
|
|
3940
4147
|
const type = escapeHtml(r.reportType);
|
|
3941
4148
|
const period = r.period ? escapeHtml(r.period) : "\u2014";
|
|
3942
|
-
return `<li><strong>${type}</strong> <span class="muted">${period}</span>
|
|
4149
|
+
return `<li><div class="pending-head"><strong>${type}</strong> <span class="muted">${period}</span> ${approveButton(r)}</div>${checklistBlock(r)}</li>`;
|
|
3943
4150
|
}
|
|
3944
4151
|
function pendingSection(reports) {
|
|
3945
4152
|
const pending = reports.filter(isPendingApproval);
|
|
@@ -3949,13 +4156,29 @@ function pendingSection(reports) {
|
|
|
3949
4156
|
<ul class="pending-list">${pending.map(pendingRow).join("")}</ul>
|
|
3950
4157
|
</div>`;
|
|
3951
4158
|
}
|
|
4159
|
+
function gaUsersCell(r) {
|
|
4160
|
+
if (r.gaUsersCurrent === null) return DASH;
|
|
4161
|
+
const current = String(r.gaUsersCurrent);
|
|
4162
|
+
if (r.gaUsersPrevious === null) return escapeHtml(current);
|
|
4163
|
+
const delta = r.gaUsersCurrent - r.gaUsersPrevious;
|
|
4164
|
+
const sign = delta > 0 ? "+" : "";
|
|
4165
|
+
return `${escapeHtml(current)} <span class="muted">(${escapeHtml(`${sign}${delta}`)})</span>`;
|
|
4166
|
+
}
|
|
4167
|
+
function searchCell(r) {
|
|
4168
|
+
if (r.searchFoundPage1 && r.searchPosition !== null) {
|
|
4169
|
+
return escapeHtml(`#${r.searchPosition}`);
|
|
4170
|
+
}
|
|
4171
|
+
return DASH;
|
|
4172
|
+
}
|
|
3952
4173
|
function reportRow(r) {
|
|
3953
|
-
const date = r.completedOn ? escapeHtml(r.completedOn) :
|
|
4174
|
+
const date = r.completedOn ? escapeHtml(r.completedOn) : DASH;
|
|
3954
4175
|
const type = escapeHtml(r.reportType);
|
|
3955
4176
|
const id = escapeHtml(r.reportId);
|
|
4177
|
+
const ga = gaUsersCell(r);
|
|
4178
|
+
const search = searchCell(r);
|
|
3956
4179
|
const link = r.renderedHtmlAttachment ? `<a href="${escapeHtml(safeUrl(r.renderedHtmlAttachment.url))}">view</a>` : `<span class="muted">no attachment</span>`;
|
|
3957
|
-
const action = isPendingApproval(r) ?
|
|
3958
|
-
return `<tr><td>${date}</td><td>${type}</td><td><code>${id}</code></td><td>${link}</td><td>${action}</td></tr>`;
|
|
4180
|
+
const action = isPendingApproval(r) ? approveButton(r) : "";
|
|
4181
|
+
return `<tr><td>${date}</td><td>${type}</td><td><code>${id}</code></td><td>${ga}</td><td>${search}</td><td>${link}</td><td>${action}</td></tr>`;
|
|
3959
4182
|
}
|
|
3960
4183
|
function submissionRow(s) {
|
|
3961
4184
|
const when = s.submittedAt ? escapeHtml(relativeTimeFromNow(s.submittedAt)) : "\u2014";
|
|
@@ -3981,6 +4204,34 @@ function submissionsSection(submissions) {
|
|
|
3981
4204
|
<ul class="subm-list">${recent.map(submissionRow).join("")}</ul>
|
|
3982
4205
|
</div>`;
|
|
3983
4206
|
}
|
|
4207
|
+
function setupSection(site) {
|
|
4208
|
+
const { score, total } = onboardingStatus(site);
|
|
4209
|
+
const missing2 = missingOnboarding(site);
|
|
4210
|
+
const detail = missing2.length === 0 ? `<span class="setup-ok">complete</span>` : `<span class="setup-missing">Missing: ${escapeHtml(missing2.join(", "))}</span>`;
|
|
4211
|
+
return `<div class="setup-line">Setup ${score}/${total} \u2014 ${detail}</div>`;
|
|
4212
|
+
}
|
|
4213
|
+
function detailRow(label, value) {
|
|
4214
|
+
const display = typeof value === "string" && value.trim().length > 0 ? escapeHtml(value.trim()) : DASH;
|
|
4215
|
+
return `<div class="detail"><dt>${escapeHtml(label)}</dt><dd>${display}</dd></div>`;
|
|
4216
|
+
}
|
|
4217
|
+
function siteDetailsSection(site) {
|
|
4218
|
+
const lastCommit = site.lastCommitAt ? `${relativeTimeFromNow(site.lastCommitAt)}` : null;
|
|
4219
|
+
const rows = [
|
|
4220
|
+
detailRow("Maintenance cadence", site.maintenanceFreq),
|
|
4221
|
+
detailRow("Testing cadence", site.testingFreq),
|
|
4222
|
+
detailRow("Report recipients (To)", site.reportRecipientsTo),
|
|
4223
|
+
detailRow("Report recipients (CC)", site.reportRecipientsCc),
|
|
4224
|
+
detailRow("Point of contact", site.pointOfContact),
|
|
4225
|
+
detailRow("GA4 property", site.ga4PropertyId),
|
|
4226
|
+
detailRow("Search query", site.searchQuery),
|
|
4227
|
+
detailRow("Git repo", site.gitRepo),
|
|
4228
|
+
detailRow("Last commit", lastCommit)
|
|
4229
|
+
].join("");
|
|
4230
|
+
return `<div class="section site-details">
|
|
4231
|
+
<h2>Site details</h2>
|
|
4232
|
+
<dl class="details">${rows}</dl>
|
|
4233
|
+
</div>`;
|
|
4234
|
+
}
|
|
3984
4235
|
var STYLES = `
|
|
3985
4236
|
:root { color-scheme: light dark; }
|
|
3986
4237
|
body { font: 16px/1.5 system-ui, -apple-system, sans-serif; max-width: 860px; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; }
|
|
@@ -4005,7 +4256,12 @@ th, td { text-align: left; padding: 0.5rem; border-bottom: 1px solid #eee; }
|
|
|
4005
4256
|
button.approve { font: inherit; padding: 0.35rem 0.85rem; border: 1px solid #2c7; border-radius: 6px; background: #2c7; color: #fff; cursor: pointer; }
|
|
4006
4257
|
button.approve:disabled { opacity: 0.6; cursor: default; }
|
|
4007
4258
|
.pending-list { list-style: none; padding: 0; margin: 0; }
|
|
4008
|
-
.pending-list li {
|
|
4259
|
+
.pending-list li { padding: 0.5rem; border-bottom: 1px solid #eee; }
|
|
4260
|
+
@media (prefers-color-scheme: dark) { .pending-list li { border-color: #2a2a2a; } }
|
|
4261
|
+
.pending-head { display: flex; align-items: center; gap: 0.5rem; }
|
|
4262
|
+
.checklist { display: flex; flex-wrap: wrap; gap: 0.25rem 1.25rem; margin: 0.5rem 0 0.25rem 0.25rem; }
|
|
4263
|
+
.check-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.9rem; }
|
|
4264
|
+
.check-item input { margin: 0; }
|
|
4009
4265
|
.pill { font-size: 0.75rem; padding: 0.1rem 0.5rem; border-radius: 999px; font-weight: 700; }
|
|
4010
4266
|
.subm-list { list-style: none; padding: 0; margin: 0; }
|
|
4011
4267
|
.subm-item { padding: 0.6rem 0; border-bottom: 1px solid #eee; }
|
|
@@ -4019,6 +4275,14 @@ button.subm-status:disabled { opacity: 0.6; cursor: default; }
|
|
|
4019
4275
|
.pill.subm-read { background: #f0f0f0; color: #555; }
|
|
4020
4276
|
.pill.subm-archived { background: #eee; color: #888; }
|
|
4021
4277
|
.pill.subm-spam { background: #fdecea; color: #b00; }
|
|
4278
|
+
.home { display: inline-block; font-size: 0.9rem; margin-bottom: 0.75rem; text-decoration: none; }
|
|
4279
|
+
.setup-line { font-size: 0.9rem; color: #666; margin-bottom: 1rem; }
|
|
4280
|
+
.setup-ok { color: #1b7a2f; font-weight: 600; }
|
|
4281
|
+
.setup-missing { color: #a65a00; }
|
|
4282
|
+
.details { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0.5rem 1.5rem; margin: 0; }
|
|
4283
|
+
.detail { display: flex; flex-direction: column; }
|
|
4284
|
+
.detail dt { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; color: #999; }
|
|
4285
|
+
.detail dd { margin: 0; }
|
|
4022
4286
|
`;
|
|
4023
4287
|
function renderSiteDashboardHtml(site, reports, submissions = []) {
|
|
4024
4288
|
const name = escapeHtml(site.name);
|
|
@@ -4040,7 +4304,7 @@ function renderSiteDashboardHtml(site, reports, submissions = []) {
|
|
|
4040
4304
|
const auditedLine = site.lastLighthouseAuditAt ? `<div class="audited">Last audited ${escapeHtml(relativeTimeFromNow(site.lastLighthouseAuditAt))}</div>` : "";
|
|
4041
4305
|
const recentReports = [...reports].sort((a, b) => (b.completedOn ?? "").localeCompare(a.completedOn ?? "")).slice(0, 6);
|
|
4042
4306
|
const reportsSection = recentReports.length === 0 ? `<div class="empty">No reports yet.</div>` : `<table>
|
|
4043
|
-
<thead><tr><th>Completed</th><th>Type</th><th>ID</th><th>Report</th><th></th></tr></thead>
|
|
4307
|
+
<thead><tr><th>Completed</th><th>Type</th><th>ID</th><th>GA users</th><th>Search</th><th>Report</th><th></th></tr></thead>
|
|
4044
4308
|
<tbody>${recentReports.map(reportRow).join("")}</tbody>
|
|
4045
4309
|
</table>`;
|
|
4046
4310
|
return `<!doctype html>
|
|
@@ -4053,9 +4317,11 @@ function renderSiteDashboardHtml(site, reports, submissions = []) {
|
|
|
4053
4317
|
<style>${STYLES}</style>
|
|
4054
4318
|
</head>
|
|
4055
4319
|
<body>
|
|
4320
|
+
<a class="home" href="/">\u2190 Fleet home</a>
|
|
4056
4321
|
<h1>${name}</h1>
|
|
4057
4322
|
<div class="meta"><a href="${escapeHtml(urlSafe)}">${escapeHtml(site.url)}</a></div>
|
|
4058
4323
|
${auditedLine}
|
|
4324
|
+
${setupSection(site)}
|
|
4059
4325
|
${pendingSection(reports)}
|
|
4060
4326
|
${submissionsSection(submissions)}
|
|
4061
4327
|
|
|
@@ -4073,6 +4339,8 @@ function renderSiteDashboardHtml(site, reports, submissions = []) {
|
|
|
4073
4339
|
<h2>Reports</h2>
|
|
4074
4340
|
${reportsSection}
|
|
4075
4341
|
</div>
|
|
4342
|
+
|
|
4343
|
+
${siteDetailsSection(site)}
|
|
4076
4344
|
<script>
|
|
4077
4345
|
document.querySelectorAll("button.approve").forEach((b) => {
|
|
4078
4346
|
b.addEventListener("click", async () => {
|
|
@@ -4106,39 +4374,52 @@ function renderSiteDashboardHtml(site, reports, submissions = []) {
|
|
|
4106
4374
|
}
|
|
4107
4375
|
});
|
|
4108
4376
|
});
|
|
4377
|
+
// Checklist gate: ticking a box POSTs the one field; the response { complete }
|
|
4378
|
+
// decides whether THIS report's Approve button is enabled. Scoped per report by
|
|
4379
|
+
// matching the checkbox's report id to the Approve button's id, so multiple
|
|
4380
|
+
// pending reports on one page never cross-toggle. On failure the checkbox reverts.
|
|
4381
|
+
document.querySelectorAll("input.checklist-checkbox").forEach((cb) => {
|
|
4382
|
+
cb.addEventListener("change", async () => {
|
|
4383
|
+
const reportId = cb.dataset.checklistReportId;
|
|
4384
|
+
const approveBtn = document.querySelector(
|
|
4385
|
+
'button.approve[data-report-id="' + (window.CSS && CSS.escape ? CSS.escape(reportId) : reportId) + '"]',
|
|
4386
|
+
);
|
|
4387
|
+
cb.disabled = true;
|
|
4388
|
+
try {
|
|
4389
|
+
const res = await fetch(cb.dataset.checklistUrl, {
|
|
4390
|
+
method: "POST",
|
|
4391
|
+
headers: { "content-type": "application/json" },
|
|
4392
|
+
body: JSON.stringify({ reportId, field: cb.dataset.field, value: cb.checked }),
|
|
4393
|
+
});
|
|
4394
|
+
if (!res.ok) throw new Error("bad status");
|
|
4395
|
+
const data = await res.json();
|
|
4396
|
+
if (approveBtn) approveBtn.disabled = !data.complete;
|
|
4397
|
+
} catch {
|
|
4398
|
+
// Revert the optimistic flip so the box reflects the (unchanged) server state.
|
|
4399
|
+
cb.checked = !cb.checked;
|
|
4400
|
+
} finally {
|
|
4401
|
+
cb.disabled = false;
|
|
4402
|
+
}
|
|
4403
|
+
});
|
|
4404
|
+
});
|
|
4109
4405
|
</script>
|
|
4110
4406
|
</body>
|
|
4111
4407
|
</html>`;
|
|
4112
4408
|
}
|
|
4113
4409
|
|
|
4114
|
-
// src/dashboard/onboarding.ts
|
|
4115
|
-
function isNonEmpty(s) {
|
|
4116
|
-
return typeof s === "string" && s.trim().length > 0;
|
|
4117
|
-
}
|
|
4118
|
-
function onboardingStatus(row) {
|
|
4119
|
-
const checks = {
|
|
4120
|
-
firstAudit: isNonEmpty(row.lastLighthouseAuditAt),
|
|
4121
|
-
recipients: isNonEmpty(row.reportRecipientsTo),
|
|
4122
|
-
schedule: row.maintenanceFreq !== "None",
|
|
4123
|
-
poc: isNonEmpty(row.pointOfContact)
|
|
4124
|
-
};
|
|
4125
|
-
const score = Object.values(checks).filter(Boolean).length;
|
|
4126
|
-
return { score, total: 4, checks };
|
|
4127
|
-
}
|
|
4128
|
-
|
|
4129
4410
|
// src/dashboard/fleet-render.ts
|
|
4130
|
-
var
|
|
4411
|
+
var DASH2 = "\u2014";
|
|
4131
4412
|
function scoreSpan(category, value) {
|
|
4132
|
-
const display = value === null ?
|
|
4413
|
+
const display = value === null ? DASH2 : String(value);
|
|
4133
4414
|
return `<span class="score ${category}">${escapeHtml(display)}</span>`;
|
|
4134
4415
|
}
|
|
4135
4416
|
function a11ySpan(value) {
|
|
4136
|
-
const display = value === null ?
|
|
4417
|
+
const display = value === null ? DASH2 : String(value);
|
|
4137
4418
|
return `<span class="metric a11y">${escapeHtml(display)}</span>`;
|
|
4138
4419
|
}
|
|
4139
4420
|
function depsSpan(drifted, majorBehind, outdated) {
|
|
4140
4421
|
if (drifted === null || majorBehind === null) {
|
|
4141
|
-
return `<span class="metric deps">${
|
|
4422
|
+
return `<span class="metric deps">${DASH2}</span>`;
|
|
4142
4423
|
}
|
|
4143
4424
|
const driftPart = drifted === 0 ? "0" : `${drifted} drifted (${majorBehind} major)`;
|
|
4144
4425
|
const display = outdated === null ? driftPart : `${driftPart} \xB7 ${outdated} outdated`;
|
|
@@ -4146,7 +4427,7 @@ function depsSpan(drifted, majorBehind, outdated) {
|
|
|
4146
4427
|
}
|
|
4147
4428
|
function securitySpan(critical, high, moderate, low) {
|
|
4148
4429
|
if (critical === null || high === null || moderate === null || low === null) {
|
|
4149
|
-
return `<span class="metric sec">${
|
|
4430
|
+
return `<span class="metric sec">${DASH2}</span>`;
|
|
4150
4431
|
}
|
|
4151
4432
|
const total = critical + high + moderate + low;
|
|
4152
4433
|
const display = total === 0 ? "0" : `${critical}C/${high}H/${moderate}M/${low}L`;
|
|
@@ -4156,6 +4437,10 @@ function card(site) {
|
|
|
4156
4437
|
const name = escapeHtml(site.name);
|
|
4157
4438
|
const href = `/s/${escapeHtml(siteSlug(site.name))}`;
|
|
4158
4439
|
const onboarding = onboardingStatus(site);
|
|
4440
|
+
const missing2 = missingOnboarding(site);
|
|
4441
|
+
const setupTitle = escapeHtml(
|
|
4442
|
+
missing2.length === 0 ? "Setup complete" : `Missing: ${missing2.join(", ")}`
|
|
4443
|
+
);
|
|
4159
4444
|
const audited = relativeTimeFromNow(site.lastLighthouseAuditAt);
|
|
4160
4445
|
const safeSiteUrl = escapeHtml(safeUrl(site.url));
|
|
4161
4446
|
const visibleUrl = escapeHtml(site.url);
|
|
@@ -4163,15 +4448,15 @@ function card(site) {
|
|
|
4163
4448
|
<header class="card-head">
|
|
4164
4449
|
<a class="site" href="${href}">${name}</a>
|
|
4165
4450
|
<a class="url" href="${safeSiteUrl}" target="_blank" rel="noopener">${visibleUrl}</a>
|
|
4166
|
-
<span class="setup">Setup: <strong>${onboarding.score}/${onboarding.total}</strong></span>
|
|
4451
|
+
<span class="setup" title="${setupTitle}">Setup: <strong>${onboarding.score}/${onboarding.total}</strong></span>
|
|
4167
4452
|
<span class="audited">Audited: <strong>${escapeHtml(audited)}</strong></span>
|
|
4168
4453
|
</header>
|
|
4169
4454
|
<div class="card-metrics">
|
|
4170
4455
|
<span class="cluster lighthouse">
|
|
4171
|
-
${scoreSpan("perf", site.pScore)}
|
|
4172
|
-
${scoreSpan("a11y-lh", site.rScore)}
|
|
4173
|
-
${scoreSpan("bp", site.bpScore)}
|
|
4174
|
-
${scoreSpan("seo", site.seoScore)}
|
|
4456
|
+
<span class="metric-label">Perf</span> ${scoreSpan("perf", site.pScore)}
|
|
4457
|
+
<span class="metric-label">Access</span> ${scoreSpan("a11y-lh", site.rScore)}
|
|
4458
|
+
<span class="metric-label">BP</span> ${scoreSpan("bp", site.bpScore)}
|
|
4459
|
+
<span class="metric-label">SEO</span> ${scoreSpan("seo", site.seoScore)}
|
|
4175
4460
|
</span>
|
|
4176
4461
|
<span class="cluster health">
|
|
4177
4462
|
<span class="metric-label">a11y</span> ${a11ySpan(site.a11yViolations)}
|
|
@@ -4244,6 +4529,7 @@ var FILTERS = [
|
|
|
4244
4529
|
"prs",
|
|
4245
4530
|
"ci",
|
|
4246
4531
|
"stale",
|
|
4532
|
+
"no-domain",
|
|
4247
4533
|
"pending",
|
|
4248
4534
|
"submissions"
|
|
4249
4535
|
];
|