@reddoorla/maintenance 0.53.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/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(submissionRow).join("")}</ul>
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
- document.querySelectorAll("button.subm-status").forEach((b) => {
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 triage on each site page</div>` : "";
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>`;