@reddoorla/maintenance 0.52.0 → 0.54.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.map +1 -1
- package/dist/index.js +97 -92
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4775,6 +4775,90 @@ function missingOnboarding(row) {
|
|
|
4775
4775
|
return Object.keys(ONBOARDING_LABELS).filter((key) => !checks[key]).map((key) => ONBOARDING_LABELS[key]);
|
|
4776
4776
|
}
|
|
4777
4777
|
|
|
4778
|
+
// src/dashboard/submission-view.ts
|
|
4779
|
+
function extraFieldsList(raw) {
|
|
4780
|
+
if (!raw || raw.trim() === "") return "";
|
|
4781
|
+
let parsed;
|
|
4782
|
+
try {
|
|
4783
|
+
parsed = JSON.parse(raw);
|
|
4784
|
+
} catch {
|
|
4785
|
+
return `<div class="subm-kv"><span class="k">Extra fields</span> <code>${escapeHtml(raw)}</code></div>`;
|
|
4786
|
+
}
|
|
4787
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4788
|
+
return `<div class="subm-kv"><span class="k">Extra fields</span> <code>${escapeHtml(raw)}</code></div>`;
|
|
4789
|
+
}
|
|
4790
|
+
const rows = Object.entries(parsed).map(
|
|
4791
|
+
([k, v]) => `<div class="subm-kv"><span class="k">${escapeHtml(k)}</span> ${escapeHtml(String(v))}</div>`
|
|
4792
|
+
).join("");
|
|
4793
|
+
return rows;
|
|
4794
|
+
}
|
|
4795
|
+
function renderSubmissionRow(s) {
|
|
4796
|
+
const when = s.submittedAt ? escapeHtml(relativeTimeFromNow(s.submittedAt)) : "\u2014";
|
|
4797
|
+
const type = escapeHtml(s.formType);
|
|
4798
|
+
const who = escapeHtml(s.name || "(no name)");
|
|
4799
|
+
const email = escapeHtml(s.email || "");
|
|
4800
|
+
const status = escapeHtml(s.status);
|
|
4801
|
+
const id = escapeHtml(s.id);
|
|
4802
|
+
const url = `/api/submissions/${encodeURIComponent(s.id)}/status`;
|
|
4803
|
+
const btn = (label, action) => `<button class="subm-status" data-id="${id}" data-status="${action}" data-url="${url}">${label}</button>`;
|
|
4804
|
+
const kv = (label, value) => value === null || value === "" ? "" : `<div class="subm-kv"><span class="k">${label}</span> ${escapeHtml(String(value))}</div>`;
|
|
4805
|
+
const sourceLink = s.sourceUrl ? `<div class="subm-kv"><span class="k">Source</span> <a href="${escapeHtml(safeUrl(s.sourceUrl))}" rel="noopener noreferrer">${escapeHtml(s.sourceUrl)}</a></div>` : "";
|
|
4806
|
+
const messageBlock = s.message ? `<div class="subm-kv"><span class="k">Message</span></div><div class="subm-msg">${escapeHtml(s.message)}</div>` : "";
|
|
4807
|
+
const details = [
|
|
4808
|
+
kv("Phone", s.phone),
|
|
4809
|
+
messageBlock,
|
|
4810
|
+
sourceLink,
|
|
4811
|
+
kv("UTM", s.utm),
|
|
4812
|
+
extraFieldsList(s.extraFields),
|
|
4813
|
+
kv("Notify", s.notifyStatus),
|
|
4814
|
+
kv("Resend ID", s.resendMessageId),
|
|
4815
|
+
kv("Submission #", s.submissionId)
|
|
4816
|
+
].join("");
|
|
4817
|
+
return `<li class="subm-item">
|
|
4818
|
+
<details>
|
|
4819
|
+
<summary class="subm-head"><strong>${type}</strong> \xB7 ${who} <span class="muted">${email}</span> <span class="pill subm-${status}">${status}</span> <span class="muted">${when}</span></summary>
|
|
4820
|
+
<div class="subm-detail">${details}</div>
|
|
4821
|
+
</details>
|
|
4822
|
+
<div class="subm-actions">${btn("Read", "read")}${btn("Archive", "archived")}${btn("Spam", "spam")}</div>
|
|
4823
|
+
</li>`;
|
|
4824
|
+
}
|
|
4825
|
+
var SUBMISSION_STYLES = `.subm-list { list-style: none; padding: 0; margin: 0; }
|
|
4826
|
+
.subm-item { padding: 0.6rem 0; border-bottom: 1px solid #eee; }
|
|
4827
|
+
@media (prefers-color-scheme: dark) { .subm-item { border-color: #2a2a2a; } }
|
|
4828
|
+
.subm-head { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
|
4829
|
+
.subm-msg { margin: 0.35rem 0; white-space: pre-wrap; }
|
|
4830
|
+
.subm-detail { padding: 0.35rem 0 0.2rem; }
|
|
4831
|
+
.subm-kv { font-size: 0.9rem; margin: 0.15rem 0; }
|
|
4832
|
+
.subm-kv .k { color: #888; margin-right: 0.4rem; }
|
|
4833
|
+
summary.subm-head { cursor: pointer; }
|
|
4834
|
+
.subm-actions { display: flex; gap: 0.4rem; }
|
|
4835
|
+
button.subm-status { font: inherit; padding: 0.25rem 0.7rem; border: 1px solid #888; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; }
|
|
4836
|
+
button.subm-status:disabled { opacity: 0.6; cursor: default; }
|
|
4837
|
+
.spam-screen .spam-kv { font-size: 0.95rem; margin: 0.2rem 0; }
|
|
4838
|
+
.spam-screen .spam-kv .k { color: #888; display: inline-block; min-width: 11rem; }
|
|
4839
|
+
.pill.subm-new { background: #e8f0fe; color: #1a56db; }
|
|
4840
|
+
.pill.subm-read { background: #f0f0f0; color: #555; }
|
|
4841
|
+
.pill.subm-archived { background: #eee; color: #888; }
|
|
4842
|
+
.pill.subm-spam { background: #fdecea; color: #b00; }
|
|
4843
|
+
.subm-viewall { font-size: 0.8rem; font-weight: normal; margin-left: 0.4rem; white-space: nowrap; }`;
|
|
4844
|
+
var SUBMISSION_STATUS_SCRIPT = `document.querySelectorAll("button.subm-status").forEach((b) => {
|
|
4845
|
+
b.addEventListener("click", async () => {
|
|
4846
|
+
b.disabled = true;
|
|
4847
|
+
try {
|
|
4848
|
+
const res = await fetch(b.dataset.url, {
|
|
4849
|
+
method: "POST",
|
|
4850
|
+
headers: { "content-type": "application/json" },
|
|
4851
|
+
body: JSON.stringify({ status: b.dataset.status }),
|
|
4852
|
+
});
|
|
4853
|
+
b.textContent = res.ok ? "\u2713" : "Failed";
|
|
4854
|
+
if (!res.ok) b.disabled = false;
|
|
4855
|
+
} catch {
|
|
4856
|
+
b.textContent = "Failed";
|
|
4857
|
+
b.disabled = false;
|
|
4858
|
+
}
|
|
4859
|
+
});
|
|
4860
|
+
});`;
|
|
4861
|
+
|
|
4778
4862
|
// src/dashboard/render.ts
|
|
4779
4863
|
var DASH = "\u2014";
|
|
4780
4864
|
function scoreTile(label, value) {
|
|
@@ -4885,60 +4969,15 @@ function reportRow(r) {
|
|
|
4885
4969
|
const action = isPendingApproval(r) ? approveButton(r) : "";
|
|
4886
4970
|
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>`;
|
|
4887
4971
|
}
|
|
4888
|
-
function extraFieldsList(raw) {
|
|
4889
|
-
if (!raw || raw.trim() === "") return "";
|
|
4890
|
-
let parsed;
|
|
4891
|
-
try {
|
|
4892
|
-
parsed = JSON.parse(raw);
|
|
4893
|
-
} catch {
|
|
4894
|
-
return `<div class="subm-kv"><span class="k">Extra fields</span> <code>${escapeHtml(raw)}</code></div>`;
|
|
4895
|
-
}
|
|
4896
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4897
|
-
return `<div class="subm-kv"><span class="k">Extra fields</span> <code>${escapeHtml(raw)}</code></div>`;
|
|
4898
|
-
}
|
|
4899
|
-
const rows = Object.entries(parsed).map(
|
|
4900
|
-
([k, v]) => `<div class="subm-kv"><span class="k">${escapeHtml(k)}</span> ${escapeHtml(String(v))}</div>`
|
|
4901
|
-
).join("");
|
|
4902
|
-
return rows;
|
|
4903
|
-
}
|
|
4904
|
-
function submissionRow(s) {
|
|
4905
|
-
const when = s.submittedAt ? escapeHtml(relativeTimeFromNow(s.submittedAt)) : "\u2014";
|
|
4906
|
-
const type = escapeHtml(s.formType);
|
|
4907
|
-
const who = escapeHtml(s.name || "(no name)");
|
|
4908
|
-
const email = escapeHtml(s.email || "");
|
|
4909
|
-
const status = escapeHtml(s.status);
|
|
4910
|
-
const id = escapeHtml(s.id);
|
|
4911
|
-
const url = `/api/submissions/${encodeURIComponent(s.id)}/status`;
|
|
4912
|
-
const btn = (label, action) => `<button class="subm-status" data-id="${id}" data-status="${action}" data-url="${url}">${label}</button>`;
|
|
4913
|
-
const kv = (label, value) => value === null || value === "" ? "" : `<div class="subm-kv"><span class="k">${label}</span> ${escapeHtml(String(value))}</div>`;
|
|
4914
|
-
const sourceLink = s.sourceUrl ? `<div class="subm-kv"><span class="k">Source</span> <a href="${escapeHtml(safeUrl(s.sourceUrl))}" rel="noopener noreferrer">${escapeHtml(s.sourceUrl)}</a></div>` : "";
|
|
4915
|
-
const messageBlock = s.message ? `<div class="subm-kv"><span class="k">Message</span></div><div class="subm-msg">${escapeHtml(s.message)}</div>` : "";
|
|
4916
|
-
const details = [
|
|
4917
|
-
kv("Phone", s.phone),
|
|
4918
|
-
messageBlock,
|
|
4919
|
-
sourceLink,
|
|
4920
|
-
kv("UTM", s.utm),
|
|
4921
|
-
extraFieldsList(s.extraFields),
|
|
4922
|
-
kv("Notify", s.notifyStatus),
|
|
4923
|
-
kv("Resend ID", s.resendMessageId),
|
|
4924
|
-
kv("Submission #", s.submissionId)
|
|
4925
|
-
].join("");
|
|
4926
|
-
return `<li class="subm-item">
|
|
4927
|
-
<details>
|
|
4928
|
-
<summary class="subm-head"><strong>${type}</strong> \xB7 ${who} <span class="muted">${email}</span> <span class="pill subm-${status}">${status}</span> <span class="muted">${when}</span></summary>
|
|
4929
|
-
<div class="subm-detail">${details}</div>
|
|
4930
|
-
</details>
|
|
4931
|
-
<div class="subm-actions">${btn("Read", "read")}${btn("Archive", "archived")}${btn("Spam", "spam")}</div>
|
|
4932
|
-
</li>`;
|
|
4933
|
-
}
|
|
4934
4972
|
var SUBMISSIONS_PER_SITE_CAP = 25;
|
|
4935
|
-
function submissionsSection(submissions) {
|
|
4973
|
+
function submissionsSection(submissions, site) {
|
|
4936
4974
|
if (submissions.length === 0) return "";
|
|
4937
4975
|
const recent = [...submissions].sort((a, b) => (b.submittedAt ?? "").localeCompare(a.submittedAt ?? "")).slice(0, SUBMISSIONS_PER_SITE_CAP);
|
|
4938
4976
|
const note = submissions.length > recent.length ? `<span class="muted"> \u2014 showing ${recent.length} of ${submissions.length}</span>` : "";
|
|
4977
|
+
const viewAll = `<a class="subm-viewall" href="/submissions?site=${escapeHtml(siteSlug(site.name))}">View all for this site \u2192</a>`;
|
|
4939
4978
|
return `<div class="section submissions">
|
|
4940
|
-
<h2>Form submissions (${submissions.length})${note}</h2>
|
|
4941
|
-
<ul class="subm-list">${recent.map(
|
|
4979
|
+
<h2>Form submissions (${submissions.length})${note} ${viewAll}</h2>
|
|
4980
|
+
<ul class="subm-list">${recent.map(renderSubmissionRow).join("")}</ul>
|
|
4942
4981
|
</div>`;
|
|
4943
4982
|
}
|
|
4944
4983
|
var SPAM_WINDOW_DAYS = 30;
|
|
@@ -5020,24 +5059,6 @@ button.approve:disabled { opacity: 0.6; cursor: default; }
|
|
|
5020
5059
|
.auto-pass { background: #e6f4ea; color: #137333; }
|
|
5021
5060
|
.auto-amber { background: #fef7e0; color: #b06000; }
|
|
5022
5061
|
.pill { font-size: 0.75rem; padding: 0.1rem 0.5rem; border-radius: 999px; font-weight: 700; }
|
|
5023
|
-
.subm-list { list-style: none; padding: 0; margin: 0; }
|
|
5024
|
-
.subm-item { padding: 0.6rem 0; border-bottom: 1px solid #eee; }
|
|
5025
|
-
@media (prefers-color-scheme: dark) { .subm-item { border-color: #2a2a2a; } }
|
|
5026
|
-
.subm-head { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
|
5027
|
-
.subm-msg { margin: 0.35rem 0; white-space: pre-wrap; }
|
|
5028
|
-
.subm-detail { padding: 0.35rem 0 0.2rem; }
|
|
5029
|
-
.subm-kv { font-size: 0.9rem; margin: 0.15rem 0; }
|
|
5030
|
-
.subm-kv .k { color: #888; margin-right: 0.4rem; }
|
|
5031
|
-
summary.subm-head { cursor: pointer; }
|
|
5032
|
-
.subm-actions { display: flex; gap: 0.4rem; }
|
|
5033
|
-
button.subm-status { font: inherit; padding: 0.25rem 0.7rem; border: 1px solid #888; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; }
|
|
5034
|
-
button.subm-status:disabled { opacity: 0.6; cursor: default; }
|
|
5035
|
-
.spam-screen .spam-kv { font-size: 0.95rem; margin: 0.2rem 0; }
|
|
5036
|
-
.spam-screen .spam-kv .k { color: #888; display: inline-block; min-width: 11rem; }
|
|
5037
|
-
.pill.subm-new { background: #e8f0fe; color: #1a56db; }
|
|
5038
|
-
.pill.subm-read { background: #f0f0f0; color: #555; }
|
|
5039
|
-
.pill.subm-archived { background: #eee; color: #888; }
|
|
5040
|
-
.pill.subm-spam { background: #fdecea; color: #b00; }
|
|
5041
5062
|
.vuln-list { list-style: none; padding: 0; margin: 0; }
|
|
5042
5063
|
.vuln-item { padding: 0.45rem 0; border-bottom: 1px solid #eee; display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: baseline; }
|
|
5043
5064
|
@media (prefers-color-scheme: dark) { .vuln-item { border-color: #2a2a2a; } }
|
|
@@ -5084,7 +5105,7 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
|
|
|
5084
5105
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
5085
5106
|
${FAVICON_LINK}
|
|
5086
5107
|
<title>${name} \u2014 Reddoor maintenance</title>
|
|
5087
|
-
<style>${STYLES}</style>
|
|
5108
|
+
<style>${STYLES}${SUBMISSION_STYLES}</style>
|
|
5088
5109
|
</head>
|
|
5089
5110
|
<body>
|
|
5090
5111
|
<a class="home" href="/">\u2190 Fleet home</a>
|
|
@@ -5093,7 +5114,6 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
|
|
|
5093
5114
|
${auditedLine}
|
|
5094
5115
|
${setupSection(site)}
|
|
5095
5116
|
${pendingSection(reports)}
|
|
5096
|
-
${submissionsSection(submissions)}
|
|
5097
5117
|
|
|
5098
5118
|
<div class="section">
|
|
5099
5119
|
<h2>Lighthouse</h2>
|
|
@@ -5107,14 +5127,14 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
|
|
|
5107
5127
|
|
|
5108
5128
|
${securitySection(site)}
|
|
5109
5129
|
|
|
5110
|
-
${spamScreenSection(spamTotals, submissions, now)}
|
|
5111
|
-
|
|
5112
5130
|
<div class="section">
|
|
5113
5131
|
<h2>Reports</h2>
|
|
5114
5132
|
${reportsSection}
|
|
5115
5133
|
</div>
|
|
5116
5134
|
|
|
5117
5135
|
${siteDetailsSection(site)}
|
|
5136
|
+
${spamScreenSection(spamTotals, submissions, now)}
|
|
5137
|
+
${submissionsSection(submissions, site)}
|
|
5118
5138
|
<script>
|
|
5119
5139
|
document.querySelectorAll("button.approve").forEach((b) => {
|
|
5120
5140
|
b.addEventListener("click", async () => {
|
|
@@ -5131,23 +5151,7 @@ function renderSiteDashboardHtml(site, reports, submissions = [], spamTotals = n
|
|
|
5131
5151
|
}
|
|
5132
5152
|
});
|
|
5133
5153
|
});
|
|
5134
|
-
|
|
5135
|
-
b.addEventListener("click", async () => {
|
|
5136
|
-
b.disabled = true;
|
|
5137
|
-
try {
|
|
5138
|
-
const res = await fetch(b.dataset.url, {
|
|
5139
|
-
method: "POST",
|
|
5140
|
-
headers: { "content-type": "application/json" },
|
|
5141
|
-
body: JSON.stringify({ status: b.dataset.status }),
|
|
5142
|
-
});
|
|
5143
|
-
b.textContent = res.ok ? "\u2713" : "Failed";
|
|
5144
|
-
if (!res.ok) b.disabled = false;
|
|
5145
|
-
} catch {
|
|
5146
|
-
b.textContent = "Failed";
|
|
5147
|
-
b.disabled = false;
|
|
5148
|
-
}
|
|
5149
|
-
});
|
|
5150
|
-
});
|
|
5154
|
+
${SUBMISSION_STATUS_SCRIPT}
|
|
5151
5155
|
// Checklist gate: ticking a box POSTs the one field; the response { complete }
|
|
5152
5156
|
// decides whether THIS report's Approve button is enabled. Scoped per report by
|
|
5153
5157
|
// matching the checkbox's report id to the Approve button's id, so multiple
|
|
@@ -5270,6 +5274,7 @@ h1 { margin: 0 0 0.25rem; font-size: 1.75rem; }
|
|
|
5270
5274
|
.summary .heads { color:#666; font-size:0.9rem; }
|
|
5271
5275
|
.spam-rollup { font-size:0.9rem; margin-bottom:1rem; }
|
|
5272
5276
|
.muted { color:#999; }
|
|
5277
|
+
.subm-viewall { font-size:0.8rem; font-weight:normal; margin-left:0.4rem; white-space:nowrap; }
|
|
5273
5278
|
.filters { display:flex; flex-wrap:wrap; gap:0.4rem; margin-bottom:1.25rem; }
|
|
5274
5279
|
.filters button { font:inherit; font-size:0.85rem; padding:0.25rem 0.7rem; border:1px solid #ccc; border-radius:999px; background:transparent; color:inherit; cursor:pointer; }
|
|
5275
5280
|
.filters button[aria-pressed="true"] { background:#1a1a1a; color:#fff; border-color:#1a1a1a; }
|
|
@@ -5375,9 +5380,9 @@ function submissionsStrip(model) {
|
|
|
5375
5380
|
</div>`;
|
|
5376
5381
|
}).join("");
|
|
5377
5382
|
const overflow = subs.length - shown.length;
|
|
5378
|
-
const more = overflow > 0 ? `<div class="approve-row subm-more muted">+${overflow} more \u2014
|
|
5383
|
+
const more = overflow > 0 ? `<div class="approve-row subm-more muted"><a href="/submissions">+${overflow} more \u2014 view all submissions</a></div>` : "";
|
|
5379
5384
|
return `<section class="approve-strip subm-strip" data-tier="submissions">
|
|
5380
|
-
<h2>\u{1F4E5} New submissions (${subs.length})</h2>
|
|
5385
|
+
<h2>\u{1F4E5} New submissions (${subs.length}) <a class="subm-viewall" href="/submissions">View all \u2192</a></h2>
|
|
5381
5386
|
${rows}${more}
|
|
5382
5387
|
</section>`;
|
|
5383
5388
|
}
|
|
@@ -5471,11 +5476,11 @@ function renderCockpitHtml(model) {
|
|
|
5471
5476
|
<h1>Reddoor fleet cockpit</h1>
|
|
5472
5477
|
<div class="meta">${total} site${total === 1 ? "" : "s"} on the Reddoor stack.</div>
|
|
5473
5478
|
${summaryBar(model)}
|
|
5474
|
-
${spamRollup(model)}
|
|
5475
5479
|
${allClearBanner(model)}
|
|
5476
5480
|
${approveStrip(model)}
|
|
5477
|
-
${submissionsStrip(model)}
|
|
5478
5481
|
${sections}
|
|
5482
|
+
${spamRollup(model)}
|
|
5483
|
+
${submissionsStrip(model)}
|
|
5479
5484
|
${FILTER_SCRIPT}
|
|
5480
5485
|
</body>
|
|
5481
5486
|
</html>`;
|