From bc5f9f88d199b3d00bdba8e54e431be75902413a Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 17 Mar 2026 00:30:07 -0700 Subject: [PATCH 01/56] IO-3616 Production Phone# Deeplinking Signed-off-by: Allan Carr --- .../production-list-columns.data.jsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/client/src/components/production-list-columns/production-list-columns.data.jsx b/client/src/components/production-list-columns/production-list-columns.data.jsx index 44fb6bafa..f44bbb3cc 100644 --- a/client/src/components/production-list-columns/production-list-columns.data.jsx +++ b/client/src/components/production-list-columns/production-list-columns.data.jsx @@ -6,7 +6,7 @@ import { setModalContext } from "../../redux/modals/modals.actions"; import { store } from "../../redux/store"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { TimeFormatter } from "../../utils/DateFormatter"; -import PhoneFormatter from "../../utils/PhoneFormatter"; +import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import { onlyUnique } from "../../utils/arrayHelper"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; import { alphaSort, dateSort, statusSort } from "../../utils/sorters"; @@ -28,6 +28,7 @@ import ProductionListColumnCategory from "./production-list-columns.status.categ import ProductionListColumnStatus from "./production-list-columns.status.component"; import ProductionListColumnTouchTime from "./prodution-list-columns.touchtime.component"; import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx"; +import ChatOpenButton from "../chat-open-button/chat-open-button.component.jsx"; const getEmployeeName = (employeeId, employees) => { const employee = employees.find((e) => e.id === employeeId); @@ -271,14 +272,24 @@ const productionListColumnsData = ({ technician, state, activeStatuses, data, bo dataIndex: "ownr_ph1", key: "ownr_ph1", ellipsis: true, - render: (text, record) => {record.ownr_ph1} + render: (text, record) => + technician ? ( + {record.ownr_ph1} + ) : ( + + ) }, { title: i18n.t("jobs.fields.ownr_ph2"), dataIndex: "ownr_ph2", key: "ownr_ph2", ellipsis: true, - render: (text, record) => {record.ownr_ph2} + render: (text, record) => + technician ? ( + {record.ownr_ph2} + ) : ( + + ) }, { title: i18n.t("jobs.fields.specialcoveragepolicy"), From 73ba95a240c05f00219fe725cbcdf4cc662d9211 Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 17 Mar 2026 10:50:22 -0400 Subject: [PATCH 02/56] feature/IO-3587-Commision-Cut - rebuild from master-AIO --- .../read-only-form-item.component.jsx | 18 +- .../form-list-move-arrows.component.jsx | 4 +- ...or-allocations-table.payroll.component.jsx | 54 +- .../shop-info.task-presets.component.jsx | 55 +- .../shop-employee-teams.form.component.jsx | 513 +++++++++--------- .../time-ticket-task-modal.component.jsx | 88 ++- .../time-ticket-task-modal.container.jsx | 27 +- .../tt-approvals-list.component.jsx | 18 +- .../tt-approve-button.component.jsx | 18 +- client/src/graphql/bodyshop.queries.js | 4 + client/src/graphql/employee_teams.queries.js | 12 + client/src/graphql/timetickets.queries.js | 4 + client/src/graphql/tt-approvals.queries.js | 19 + client/src/translations/en_us/common.json | 27 +- client/src/translations/es/common.json | 27 +- client/src/translations/fr/common.json | 27 +- server/graphql-client/queries.js | 6 + server/payroll/calculate-totals.js | 66 +-- server/payroll/claim-task.js | 99 +++- server/payroll/pay-all.js | 501 ++++++++++------- server/payroll/payroll.test.js | 465 ++++++++++++++++ 21 files changed, 1473 insertions(+), 579 deletions(-) create mode 100644 server/payroll/payroll.test.js diff --git a/client/src/components/form-items-formatted/read-only-form-item.component.jsx b/client/src/components/form-items-formatted/read-only-form-item.component.jsx index 786d6a1a0..fc4490528 100644 --- a/client/src/components/form-items-formatted/read-only-form-item.component.jsx +++ b/client/src/components/form-items-formatted/read-only-form-item.component.jsx @@ -10,8 +10,13 @@ const mapDispatchToProps = () => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); +const toFiniteNumber = (value) => { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; +}; + const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => { - if (!value) return null; + if (value === null || value === undefined || value === "") return null; switch (type) { case "employee": { const emp = bodyshop.employees.find((e) => e.id === value); @@ -20,8 +25,15 @@ const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => { case "text": return
{value}
; - case "currency": - return
{Dinero({ amount: Math.round(value * 100) }).toFormat()}
; + case "currency": { + const numericValue = toFiniteNumber(value); + + if (numericValue === null) { + return null; + } + + return
{Dinero({ amount: Math.round(numericValue * 100) }).toFormat()}
; + } default: return
{value}
; } diff --git a/client/src/components/form-list-move-arrows/form-list-move-arrows.component.jsx b/client/src/components/form-list-move-arrows/form-list-move-arrows.component.jsx index 75047277d..3c2e6a239 100644 --- a/client/src/components/form-list-move-arrows/form-list-move-arrows.component.jsx +++ b/client/src/components/form-list-move-arrows/form-list-move-arrows.component.jsx @@ -1,7 +1,7 @@ import { DownOutlined, UpOutlined } from "@ant-design/icons"; import { Space } from "antd"; -export default function FormListMoveArrows({ move, index, total }) { +export default function FormListMoveArrows({ move, index, total, orientation = "vertical" }) { const upDisabled = index === 0; const downDisabled = index === total - 1; @@ -14,7 +14,7 @@ export default function FormListMoveArrows({ move, index, total }) { }; return ( - + diff --git a/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx b/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx index 3bcc32f14..02a9366f6 100644 --- a/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx +++ b/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx @@ -21,6 +21,8 @@ const mapStateToProps = createStructuredSelector({ technician: selectTechnician }); +const getRequestErrorMessage = (error) => error?.response?.data?.error || error?.message || ""; + export function PayrollLaborAllocationsTable({ jobId, joblines, @@ -43,16 +45,23 @@ export function PayrollLaborAllocationsTable({ }); const notification = useNotification(); - useEffect(() => { - async function CalculateTotals() { + const loadTotals = async () => { + try { const { data } = await axios.post("/payroll/calculatelabor", { jobid: jobId }); setTotals(data); + } catch (error) { + setTotals([]); + notification.error({ + title: getRequestErrorMessage(error) + }); } + }; + useEffect(() => { if (!!joblines && !!timetickets && !!bodyshop) { - CalculateTotals(); + loadTotals(); } if (!jobId) setTotals([]); }, [joblines, timetickets, bodyshop, adjustments, jobId]); @@ -210,28 +219,36 @@ export function PayrollLaborAllocationsTable({ + + + +
+ + +
+
+ + + Waiting for first refresh... +
+ + + +
+
Total00 visible
+
New0New since last refresh
+
NewestNo messagesNot refreshed yet
+
FetchIdleEndpoint: ${escapeHtml(SES_ENDPOINT)}
+
+ + + +
+ + + + +`; +} + +function renderStyles() { + return ` + :root{--panel:rgba(255,255,255,.86);--panel-strong:#fff;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--accent:#cf6d3c;--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 18px 48px rgba(35,43,53,.12);} + *{box-sizing:border-box} + html,body{margin:0;min-height:100%} + body{background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.14),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:16px/1.5 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif} + button,input,select,textarea{font:inherit} + button{cursor:pointer} + .page{max-width:1440px;margin:0 auto;padding:24px} + .hero{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(320px,.9fr);gap:24px;margin-bottom:24px} + .hero-copy,.hero-controls,.stat,.card{background:var(--panel);backdrop-filter:blur(16px);border:1px solid var(--line);box-shadow:var(--shadow)} + .hero-copy,.hero-controls{border-radius:24px;padding:24px} + .eyebrow{margin:0 0 8px;color:var(--accent);font-size:.8rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase} + h1{margin:0;font-size:clamp(2.2rem,5vw,4rem);line-height:.95;letter-spacing:-.05em} + .lede{margin:16px 0 0;max-width:60ch;color:var(--muted)} + .hero-controls{display:grid;gap:12px} + .row{display:flex;flex-wrap:wrap;gap:12px;align-items:center} + .primary,.ghost,.mini,.tab{border-radius:999px;border:1px solid transparent;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} + .primary,.ghost{min-height:44px;padding:0 18px;font-weight:700} + .mini,.tab{min-height:34px;padding:0 12px;font-weight:600} + .primary{background:var(--accent);color:#fff7f2} + .ghost,.mini{background:rgba(255,255,255,.76);border-color:var(--line);color:var(--ink)} + .tab{background:transparent;color:var(--muted)} + .tab.active{background:#fff;border-color:var(--line);color:var(--ink)} + .primary:hover,.ghost:hover,.mini:hover,.tab:hover{transform:translateY(-1px)} + .chip{display:inline-flex;align-items:center;gap:10px;min-height:44px;padding:0 16px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);font-weight:600} + .chip input{margin:0;accent-color:var(--accent)} + .chip select{border:none;background:transparent;outline:none;color:var(--ink)} + .search{flex:1 1 320px;min-height:48px;padding:0 16px;border-radius:16px;border:1px solid var(--line);background:rgba(255,255,255,.82);color:var(--ink);outline:none} + .status{display:inline-flex;align-items:center;min-height:40px;padding:0 14px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);color:var(--muted);font-weight:600} + .status.ok{color:var(--ok);border-color:rgba(31,143,101,.22);background:rgba(31,143,101,.08)} + .status.warn{color:var(--warn);border-color:rgba(157,95,0,.22);background:rgba(157,95,0,.08)} + .status.bad{color:var(--bad);border-color:rgba(179,58,58,.22);background:rgba(179,58,58,.08)} + .stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;margin-bottom:18px} + .stat{border-radius:18px;padding:18px} + .stat span{display:block;margin-bottom:10px;color:var(--muted);font-size:.82rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} + .stat strong{display:block;font-size:clamp(2rem,4vw,2.6rem);line-height:1;letter-spacing:-.05em} + .stat strong.small{font-size:1.1rem;line-height:1.3;letter-spacing:-.02em} + .stat small{display:block;margin-top:10px;color:var(--muted);font-size:.95rem} + .banner,.empty{margin:0 0 18px;padding:16px 18px;border-radius:16px;border:1px solid var(--line);background:rgba(255,255,255,.82)} + .banner{color:var(--bad);border-color:rgba(179,58,58,.24);background:rgba(179,58,58,.08)} + .list{display:grid;gap:18px} + .card{overflow:hidden;border-radius:18px} + .card.new{border-color:rgba(31,143,101,.28);box-shadow:var(--shadow),0 0 0 1px rgba(31,143,101,.12)} + .summary{list-style:none;display:grid;gap:12px;padding:20px;cursor:pointer} + .summary::-webkit-details-marker{display:none} + .top,.tags,.toolbar,.actions,.attachments{display:flex;flex-wrap:wrap;gap:10px;align-items:center} + .top{justify-content:space-between} + .head{min-width:0;flex:1 1 320px} + .head h2{margin:0;font-size:clamp(1.1rem,2vw,1.45rem);line-height:1.2;letter-spacing:-.03em;word-break:break-word} + .meta{margin:6px 0 0;color:var(--muted);font-size:.95rem;word-break:break-word} + .time,.tag{display:inline-flex;align-items:center;min-height:28px;padding:0 12px;border-radius:999px;font-size:.84rem;font-weight:700} + .time{background:rgba(31,41,51,.06)} + .tag{background:rgba(31,41,51,.06);color:var(--muted)} + .tag.new{background:rgba(31,143,101,.1);color:var(--ok)} + .tag.bad{background:rgba(179,58,58,.1);color:var(--bad)} + .preview{margin:0} + .body{display:grid;gap:16px;padding:18px 20px 20px;border-top:1px solid var(--line);background:var(--panel-strong)} + .toolbar{justify-content:space-between;align-items:flex-start} + .tabs{display:inline-flex;gap:6px;padding:6px;border-radius:999px;background:rgba(31,41,51,.05)} + .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px} + .metaCard{padding:14px 16px;border-radius:14px;background:rgba(31,41,51,.035);border:1px solid rgba(31,41,51,.08)} + .metaCard dt{margin:0 0 6px;color:var(--muted);font-size:.8rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} + .metaCard dd{margin:0;word-break:break-word} + .attachments{gap:8px} + .attachment{padding:9px 12px;border-radius:12px;background:rgba(255,255,255,.72);border:1px solid var(--line);font-size:.92rem} + .panel{overflow:hidden;border-radius:14px;border:1px solid var(--line);background:#fff} + iframe{width:100%;min-height:720px;border:none;background:#fff} + pre{margin:0;padding:16px;white-space:pre-wrap;word-break:break-word;overflow:auto;font:13px/1.55 "Cascadia Code","Consolas",monospace;color:#102030;background:linear-gradient(180deg,rgba(207,109,60,.04),transparent 140px),#fff} + .placeholder,.inlineError{padding:16px} + .inlineError{color:var(--bad)} + @media (max-width:1080px){.hero{grid-template-columns:1fr}.stats{grid-template-columns:repeat(2,minmax(0,1fr))}} + @media (max-width:720px){.page{padding:16px}.stats{grid-template-columns:1fr}.primary,.ghost,.chip{width:100%;justify-content:center}.toolbar,.row{align-items:stretch}iframe{min-height:560px}} `; } +function escapeHtml(value) { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function clientApp(config) { + const state = { + messages: [], + filtered: [], + search: "", + auto: true, + interval: config.defaultRefreshMs, + loading: false, + error: "", + updatedAt: 0, + source: "initial", + duration: 0, + parseErrors: 0, + newest: "", + newIds: new Set(), + knownIds: new Set(), + openIds: new Set(), + views: {}, + raw: {} + }; + + const el = { + refreshButton: document.getElementById("refreshButton"), + autoToggle: document.getElementById("autoToggle"), + intervalSelect: document.getElementById("intervalSelect"), + searchInput: document.getElementById("searchInput"), + clearSearchButton: document.getElementById("clearSearchButton"), + expandAllButton: document.getElementById("expandAllButton"), + collapseAllButton: document.getElementById("collapseAllButton"), + statusChip: document.getElementById("statusChip"), + totalStat: document.getElementById("totalStat"), + visibleStat: document.getElementById("visibleStat"), + newStat: document.getElementById("newStat"), + newestStat: document.getElementById("newestStat"), + updatedStat: document.getElementById("updatedStat"), + fetchStat: document.getElementById("fetchStat"), + fetchDetail: document.getElementById("fetchDetail"), + banner: document.getElementById("banner"), + empty: document.getElementById("empty"), + list: document.getElementById("list") + }; + + el.intervalSelect.value = String(config.defaultRefreshMs); + wire(); + renderAll(); + refreshMessages("initial"); + window.setInterval(renderLiveClock, 1000); + + function wire() { + el.refreshButton.addEventListener("click", () => refreshMessages("manual")); + + el.autoToggle.addEventListener("change", () => { + state.auto = el.autoToggle.checked; + scheduleRefresh(); + renderStatus(); + }); + + el.intervalSelect.addEventListener("change", () => { + state.interval = Number(el.intervalSelect.value) || config.defaultRefreshMs; + scheduleRefresh(); + renderStatus(); + }); + + el.searchInput.addEventListener("input", (event) => { + state.search = event.target.value; + applyFilter(); + }); + + el.clearSearchButton.addEventListener("click", () => { + state.search = ""; + el.searchInput.value = ""; + applyFilter(); + }); + + el.expandAllButton.addEventListener("click", () => { + state.filtered.forEach((message) => state.openIds.add(message.id)); + renderList(); + }); + + el.collapseAllButton.addEventListener("click", () => { + state.openIds.clear(); + renderList(); + }); + + el.list.addEventListener( + "toggle", + (event) => { + const details = event.target; + + if (!(details instanceof HTMLDetailsElement) || !details.matches(".card")) { + return; + } + + const id = details.dataset.id; + + if (!id) { + return; + } + + if (details.open) { + state.openIds.add(id); + } else { + state.openIds.delete(id); + } + + hydrate(details, getMessage(id)); + }, + true + ); + + el.list.addEventListener("click", async (event) => { + const button = event.target.closest("button[data-action]"); + + if (!button) { + return; + } + + const id = button.dataset.id; + const message = getMessage(id); + + if (!message) { + return; + } + + if (button.dataset.action === "view") { + state.views[id] = button.dataset.view; + renderList(); + return; + } + + if (button.dataset.action === "load-raw") { + await loadRaw(id); + renderList(); + return; + } + + if (button.dataset.action === "copy-raw") { + const raw = await loadRaw(id); + + if (raw) { + await copyText(raw); + setStatus("Raw message copied to the clipboard.", "ok"); + } + } + }); + + document.addEventListener("visibilitychange", () => { + window.clearTimeout(state.timer); + + if (!document.hidden && state.auto) { + refreshMessages("visibility"); + } else { + renderStatus(); + } + }); + + window.addEventListener("keydown", (event) => { + const isField = + event.target instanceof HTMLElement && + (event.target.matches("input,textarea,select") || event.target.isContentEditable); + + if (!isField && event.key.toLowerCase() === "r") { + event.preventDefault(); + refreshMessages("keyboard"); + } + }); + } + + async function refreshMessages(source) { + if (state.loading) { + return; + } + + state.loading = true; + state.source = source; + state.error = ""; + renderStatus(); + renderFetch(); + + try { + const response = await fetch("/api/messages", { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + const messages = Array.isArray(payload.messages) ? payload.messages : []; + const nextIds = new Set(messages.map((message) => message.id)); + + state.newIds = state.updatedAt + ? new Set(messages.filter((message) => !state.knownIds.has(message.id)).map((message) => message.id)) + : new Set(); + state.knownIds = nextIds; + state.messages = messages; + state.duration = payload.fetchDurationMs || 0; + state.parseErrors = payload.parseErrors || 0; + state.newest = payload.latestMessageTimestamp || ""; + state.updatedAt = Date.now(); + + pruneState(); + applyFilter(); + setStatus(`Updated ${messages.length} message${messages.length === 1 ? "" : "s"}.`, "ok"); + } catch (error) { + state.error = error.message || "Unknown refresh error"; + setStatus(`Refresh failed: ${state.error}`, "bad"); + } finally { + state.loading = false; + scheduleRefresh(); + renderAll(); + } + } + + function pruneState() { + const ids = new Set(state.messages.map((message) => message.id)); + state.openIds = new Set([...state.openIds].filter((id) => ids.has(id))); + state.newIds = new Set([...state.newIds].filter((id) => ids.has(id))); + + Object.keys(state.views).forEach((id) => { + if (!ids.has(id)) { + delete state.views[id]; + } + }); + + Object.keys(state.raw).forEach((id) => { + if (!ids.has(id)) { + delete state.raw[id]; + } + }); + } + + function applyFilter() { + const search = state.search.trim().toLowerCase(); + state.filtered = !search + ? [...state.messages] + : state.messages.filter((message) => haystack(message).includes(search)); + renderAll(); + } + + function haystack(message) { + return [ + message.subject, + message.from, + message.to, + message.replyTo, + message.preview, + message.textContent, + message.region, + ...(message.attachments || []).flatMap((attachment) => [attachment.filename, attachment.contentType]) + ] + .filter(Boolean) + .join(" ") + .toLowerCase(); + } + + function scheduleRefresh() { + window.clearTimeout(state.timer); + + if (!state.auto || document.hidden || state.loading) { + return; + } + + state.timer = window.setTimeout(() => refreshMessages("auto"), state.interval); + } + + function renderAll() { + renderStats(); + renderFetch(); + renderStatus(); + renderList(); + renderLiveClock(); + } + + function renderStats() { + el.totalStat.textContent = String(state.messages.length); + el.visibleStat.textContent = `${state.filtered.length} visible${state.search ? " after search" : ""}`; + el.newStat.textContent = String(state.newIds.size); + el.newestStat.textContent = state.newest ? formatDateTime(state.newest) : "No messages"; + } + + function renderFetch() { + if (state.loading) { + el.fetchStat.textContent = "Refreshing..."; + el.fetchDetail.textContent = `Endpoint: ${config.endpoint}`; + return; + } + + if (state.error) { + el.fetchStat.textContent = "Needs attention"; + el.fetchDetail.textContent = state.error; + return; + } + + if (!state.updatedAt) { + el.fetchStat.textContent = "Idle"; + el.fetchDetail.textContent = `Endpoint: ${config.endpoint}`; + return; + } + + el.fetchStat.textContent = `${state.duration}ms`; + el.fetchDetail.textContent = `${state.parseErrors} parse error${state.parseErrors === 1 ? "" : "s"}. Endpoint: ${config.endpoint}`; + } + + function renderStatus() { + el.statusChip.className = "status"; + + if (state.loading) { + el.statusChip.classList.add("warn"); + el.statusChip.textContent = "Refreshing messages..."; + return; + } + + if (state.error) { + el.statusChip.classList.add("bad"); + el.statusChip.textContent = `Refresh failed: ${state.error}`; + return; + } + + if (!state.auto) { + el.statusChip.textContent = "Live refresh paused"; + return; + } + + if (document.hidden) { + el.statusChip.classList.add("warn"); + el.statusChip.textContent = "Tab hidden, live refresh paused"; + return; + } + + if (!state.updatedAt) { + el.statusChip.textContent = "Waiting for first refresh..."; + return; + } + + const seconds = Math.max(0, Math.ceil((state.updatedAt + state.interval - Date.now()) / 1000)); + el.statusChip.classList.add("ok"); + el.statusChip.textContent = `Live refresh on, next check in ${seconds}s`; + } + + function renderLiveClock() { + if (!state.updatedAt) { + el.updatedStat.textContent = "Not refreshed yet"; + return; + } + + el.updatedStat.textContent = `Updated ${formatRelative(state.updatedAt)} via ${state.source}`; + renderStatus(); + } + + function renderList() { + el.banner.hidden = !state.error; + el.banner.textContent = state.error ? `Refresh failed: ${state.error}` : ""; + + if (!state.filtered.length) { + el.list.innerHTML = ""; + el.empty.hidden = false; + el.empty.textContent = state.messages.length + ? "No messages match the current search." + : "No emails yet. Send one through LocalStack SES and refresh."; + return; + } + + el.empty.hidden = true; + el.list.innerHTML = state.filtered.map(renderCard).join(""); + el.list.querySelectorAll(".card[open]").forEach((details) => hydrate(details, getMessage(details.dataset.id))); + } + + function renderCard(message) { + const open = state.openIds.has(message.id); + const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text"); + const tags = []; + + if (state.newIds.has(message.id)) { + tags.push('New'); + } + + if (message.attachmentCount) { + tags.push( + `${message.attachmentCount} attachment${message.attachmentCount === 1 ? "" : "s"}` + ); + } + + tags.push(`${message.hasHtml ? "HTML" : "Text only"}`); + + if (message.parseError) { + tags.push('Parse issue'); + } + + return ` +
+ +
+
+

${escapeHtml(message.subject)}

+

${escapeHtml(message.from)} to ${escapeHtml(message.to)}

+
+ ${escapeHtml(formatDateTime(message.timestamp))} +
+
${tags.join("")}
+

${escapeHtml(message.preview)}

+
+
+
+
+ ${ + message.hasHtml + ? `` + : "" + } + + +
+
+ +
+
+ +
+ ${metaCard("From", message.from)} + ${metaCard("To", message.to)} + ${metaCard("Reply-To", message.replyTo || "None")} + ${metaCard("Sent", formatDateTime(message.timestamp))} + ${metaCard("Region", message.region || "Unknown region")} + ${metaCard("LocalStack Id", message.id)} + ${metaCard("Message-Id", message.messageId || "Not available")} + ${metaCard("Raw size", formatBytes(message.rawSizeBytes))} + ${message.parseError ? metaCard("Parse error", message.parseError) : ""} +
+ + ${ + message.attachments?.length + ? `
${message.attachments + .map((attachment) => { + const size = attachment.size ? `, ${formatBytes(attachment.size)}` : ""; + return `${escapeHtml(attachment.filename)} (${escapeHtml(attachment.contentType)}${escapeHtml(size)})`; + }) + .join("")}
` + : "" + } + +
${renderPanel(message, view)}
+
+
+ `; + } + + function renderPanel(message, view) { + if (view === "rendered" && message.hasHtml) { + return ``; + } + + if (view === "raw") { + const raw = state.raw[message.id]; + + if (!raw) { + return `
Raw MIME source is loaded on demand.
`; + } + + if (raw.status === "loading") { + return '
Loading raw source...
'; + } + + if (raw.status === "error") { + return `
Unable to load raw source: ${escapeHtml(raw.error)}
`; + } + + return `
${escapeHtml(raw.value)}
`; + } + + return `
${escapeHtml(message.textContent || "No plain-text content available for this message.")}
`; + } + + function metaCard(label, value) { + return `
${escapeHtml(label)}
${escapeHtml(value)}
`; + } + + function hydrate(details, message) { + if (!details || !details.open || !message) { + return; + } + + const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text"); + + if (view !== "rendered" || !message.hasHtml) { + return; + } + + const iframe = details.querySelector("[data-frame]"); + + if (iframe) { + iframe.referrerPolicy = "no-referrer"; + iframe.sandbox = ""; + iframe.srcdoc = message.renderedHtml || ""; + } + } + + function getMessage(id) { + return state.messages.find((message) => message.id === id); + } + + async function loadRaw(id) { + if (state.raw[id]?.status === "loaded") { + return state.raw[id].value; + } + + if (state.raw[id]?.status === "loading") { + return null; + } + + state.raw[id] = { status: "loading" }; + renderList(); + + try { + const response = await fetch(`/api/messages/${encodeURIComponent(id)}/raw`, { cache: "no-store" }); + + if (!response.ok) { + throw new Error((await response.text()) || `Request failed with ${response.status}`); + } + + const value = await response.text(); + state.raw[id] = { status: "loaded", value }; + return value; + } catch (error) { + state.raw[id] = { status: "error", error: error.message || "Unknown raw load error" }; + setStatus("Could not load the raw message source.", "bad"); + return null; + } finally { + renderList(); + } + } + + async function copyText(value) { + try { + await navigator.clipboard.writeText(value); + } catch { + const input = document.createElement("textarea"); + input.value = value; + input.setAttribute("readonly", ""); + input.style.position = "fixed"; + input.style.opacity = "0"; + document.body.appendChild(input); + input.select(); + document.execCommand("copy"); + document.body.removeChild(input); + } + } + + function setStatus(message, tone) { + el.statusChip.className = "status"; + + if (tone) { + el.statusChip.classList.add(tone); + } + + el.statusChip.textContent = message; + } + + function formatDateTime(value) { + if (!value) { + return "Unknown time"; + } + + const date = new Date(value); + return Number.isNaN(date.getTime()) + ? "Unknown time" + : new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date); + } + + function formatRelative(timestampMs) { + const seconds = Math.max(1, Math.round((Date.now() - timestampMs) / 1000)); + + if (seconds < 60) { + return `${seconds}s ago`; + } + + const minutes = Math.round(seconds / 60); + + if (minutes < 60) { + return `${minutes}m ago`; + } + + const hours = Math.round(minutes / 60); + + if (hours < 24) { + return `${hours}h ago`; + } + + return `${Math.round(hours / 24)}d ago`; + } + + function formatBytes(value) { + if (!value) { + return "0 B"; + } + + const units = ["B", "KB", "MB", "GB"]; + let size = value; + let index = 0; + + while (size >= 1024 && index < units.length - 1) { + size /= 1024; + index += 1; + } + + return `${index === 0 ? size : size.toFixed(1)} ${units[index]}`; + } + + function escapeHtml(value) { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + async function safeJson(response) { + try { + return await response.json(); + } catch { + return null; + } + } +} + app.listen(PORT, () => { - console.log(`Server is running on http://localhost:${PORT}`); + console.log(`Local email viewer is running on http://localhost:${PORT}`); + console.log(`Watching LocalStack SES endpoint at ${SES_ENDPOINT}`); }); diff --git a/_reference/localEmailViewer/package.json b/_reference/localEmailViewer/package.json index 5f553cf17..a6b522e0e 100644 --- a/_reference/localEmailViewer/package.json +++ b/_reference/localEmailViewer/package.json @@ -4,12 +4,13 @@ "main": "index.js", "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node index.js", + "check": "node --check index.js" }, "keywords": [], "author": "", "license": "ISC", - "description": "", + "description": "LocalStack SES email viewer for inspecting local outbound mail", "dependencies": { "express": "^5.1.0", "mailparser": "^3.7.4", From 98781a76e6854d6149390809aa3f56ee445bfffb Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 19 Mar 2026 14:39:30 -0400 Subject: [PATCH 09/56] Localstack Email Viewer --- _reference/localEmailViewer/index.js | 47 +++++++++++++--------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/_reference/localEmailViewer/index.js b/_reference/localEmailViewer/index.js index 4106d2903..15b7736a9 100644 --- a/_reference/localEmailViewer/index.js +++ b/_reference/localEmailViewer/index.js @@ -468,32 +468,6 @@ function clientApp(config) { renderList(); }); - el.list.addEventListener( - "toggle", - (event) => { - const details = event.target; - - if (!(details instanceof HTMLDetailsElement) || !details.matches(".card")) { - return; - } - - const id = details.dataset.id; - - if (!id) { - return; - } - - if (details.open) { - state.openIds.add(id); - } else { - state.openIds.delete(id); - } - - hydrate(details, getMessage(id)); - }, - true - ); - el.list.addEventListener("click", async (event) => { const button = event.target.closest("button[data-action]"); @@ -749,9 +723,30 @@ function clientApp(config) { el.empty.hidden = true; el.list.innerHTML = state.filtered.map(renderCard).join(""); + bindCardToggles(); el.list.querySelectorAll(".card[open]").forEach((details) => hydrate(details, getMessage(details.dataset.id))); } + function bindCardToggles() { + el.list.querySelectorAll(".card").forEach((details) => { + details.addEventListener("toggle", () => { + const id = details.dataset.id; + + if (!id) { + return; + } + + if (details.open) { + state.openIds.add(id); + } else { + state.openIds.delete(id); + } + + hydrate(details, getMessage(id)); + }); + }); + } + function renderCard(message) { const open = state.openIds.has(message.id); const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text"); From 34c9a3854cbbec9c98ae9be2ce4a08380f727cb6 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 19 Mar 2026 15:51:55 -0400 Subject: [PATCH 10/56] feature/IO-3587-Commision-Cut - Improved local email / Test Plans --- .gitignore | 3 - _reference/localEmailViewer/index.js | 251 +++++-- .../commission-based-cut-manual-test-plan.md | 0 .../testPlans/select-component-test-plan.md | 647 ++++++++++++++++++ 4 files changed, 832 insertions(+), 69 deletions(-) rename _reference/{ => testPlans}/commission-based-cut-manual-test-plan.md (100%) create mode 100644 _reference/testPlans/select-component-test-plan.md diff --git a/.gitignore b/.gitignore index 4b0d51183..59c48f34d 100644 --- a/.gitignore +++ b/.gitignore @@ -142,8 +142,6 @@ docker_data /CLAUDE.md /COPILOT.md /GEMINI.md -/_reference/select-component-test-plan.md - /.cursorrules /AGENTS.md /AI_CONTEXT.md @@ -151,4 +149,3 @@ docker_data /COPILOT.md /.github/copilot-instructions.md /GEMINI.md -/_reference/select-component-test-plan.md diff --git a/_reference/localEmailViewer/index.js b/_reference/localEmailViewer/index.js index 15b7736a9..7f35fa393 100644 --- a/_reference/localEmailViewer/index.js +++ b/_reference/localEmailViewer/index.js @@ -46,8 +46,7 @@ app.get("/api/messages", async (req, res) => { app.get("/api/messages/:id/raw", async (req, res) => { try { - const messages = await fetchSesMessages(); - const message = messages.find((candidate) => resolveMessageId(candidate) === req.params.id); + const message = await findSesMessageById(req.params.id); if (!message) { res.status(404).type("text/plain").send("Message not found"); @@ -61,6 +60,42 @@ app.get("/api/messages/:id/raw", async (req, res) => { } }); +app.get("/api/messages/:id/attachments/:index", async (req, res) => { + try { + const attachmentIndex = Number.parseInt(req.params.index, 10); + + if (!Number.isInteger(attachmentIndex) || attachmentIndex < 0) { + res.status(400).type("text/plain").send("Attachment index must be a non-negative integer"); + return; + } + + const parsed = await parseSesMessageById(req.params.id); + + if (!parsed) { + res.status(404).type("text/plain").send("Message not found"); + return; + } + + const attachment = parsed.attachments?.[attachmentIndex]; + + if (!attachment) { + res.status(404).type("text/plain").send("Attachment not found"); + return; + } + + const filename = resolveAttachmentFilename(attachment, attachmentIndex); + const content = Buffer.isBuffer(attachment.content) ? attachment.content : Buffer.from(attachment.content || ""); + + res.setHeader("Content-Type", attachment.contentType || "application/octet-stream"); + res.setHeader("Content-Disposition", buildAttachmentDisposition(filename)); + res.setHeader("Content-Length", String(content.length)); + res.send(content); + } catch (error) { + console.error("Error downloading attachment:", error); + res.status(502).type("text/plain").send(`Unable to download attachment: ${error.message}`); + } +}); + async function loadMessages() { const startedAt = Date.now(); const sesMessages = await fetchSesMessages(); @@ -98,6 +133,21 @@ async function fetchSesMessages() { return Array.isArray(data.messages) ? data.messages : []; } +async function findSesMessageById(id) { + const messages = await fetchSesMessages(); + return messages.find((message, index) => resolveMessageId(message, index) === id) || null; +} + +async function parseSesMessageById(id) { + const message = await findSesMessageById(id); + + if (!message) { + return null; + } + + return simpleParser(message.RawData || ""); +} + async function toMessageViewModel(message, index) { const id = resolveMessageId(message, index); @@ -120,8 +170,9 @@ async function toMessageViewModel(message, index) { messageId: parsed.messageId || "", rawSizeBytes: Buffer.byteLength(message.RawData || "", "utf8"), attachmentCount: parsed.attachments.length, - attachments: parsed.attachments.map((attachment) => ({ - filename: attachment.filename || "Unnamed attachment", + attachments: parsed.attachments.map((attachment, attachmentIndex) => ({ + index: attachmentIndex, + filename: resolveAttachmentFilename(attachment, attachmentIndex), contentType: attachment.contentType || "application/octet-stream", size: attachment.size || 0 })), @@ -159,6 +210,45 @@ function resolveMessageId(message, index = 0) { return message.Id || `${message.Timestamp || "unknown"}-${message.Source || "unknown"}-${index}`; } +function resolveAttachmentFilename(attachment, index = 0) { + if (attachment?.filename) { + return attachment.filename; + } + + return `attachment-${index + 1}${attachmentExtension(attachment?.contentType)}`; +} + +function attachmentExtension(contentType) { + const normalized = String(contentType || "") + .split(";")[0] + .trim() + .toLowerCase(); + + return ( + { + "application/json": ".json", + "application/pdf": ".pdf", + "application/zip": ".zip", + "image/gif": ".gif", + "image/jpeg": ".jpg", + "image/png": ".png", + "image/webp": ".webp", + "text/calendar": ".ics", + "text/csv": ".csv", + "text/html": ".html", + "text/plain": ".txt" + }[normalized] || "" + ); +} + +function buildAttachmentDisposition(filename) { + const fallback = String(filename || "attachment") + .replace(/[^\x20-\x7e]/g, "_") + .replace(/["\\]/g, "_"); + + return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename || "attachment")}`; +} + function normalizeTimestamp(value) { if (!value) { return ""; @@ -254,11 +344,11 @@ function renderHtml() {

LocalStack SES

Email Viewer

-

A faster local inbox for generated emails with live refresh, manual refresh, search, and raw MIME inspection.

+

Compact inbox for generated emails with live polling, search, and raw MIME inspection.

- +
- - + + Waiting for first refresh...
@@ -301,77 +391,80 @@ function renderHtml() { function renderStyles() { return ` - :root{--panel:rgba(255,255,255,.86);--panel-strong:#fff;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--accent:#cf6d3c;--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 18px 48px rgba(35,43,53,.12);} + :root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);} *{box-sizing:border-box} html,body{margin:0;min-height:100%} - body{background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.14),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:16px/1.5 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif} + body{background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.12),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:15px/1.45 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif} button,input,select,textarea{font:inherit} button{cursor:pointer} - .page{max-width:1440px;margin:0 auto;padding:24px} - .hero{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(320px,.9fr);gap:24px;margin-bottom:24px} - .hero-copy,.hero-controls,.stat,.card{background:var(--panel);backdrop-filter:blur(16px);border:1px solid var(--line);box-shadow:var(--shadow)} - .hero-copy,.hero-controls{border-radius:24px;padding:24px} - .eyebrow{margin:0 0 8px;color:var(--accent);font-size:.8rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase} - h1{margin:0;font-size:clamp(2.2rem,5vw,4rem);line-height:.95;letter-spacing:-.05em} - .lede{margin:16px 0 0;max-width:60ch;color:var(--muted)} - .hero-controls{display:grid;gap:12px} - .row{display:flex;flex-wrap:wrap;gap:12px;align-items:center} + .page{max-width:1360px;margin:0 auto;padding:18px} + .hero{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(320px,.95fr);gap:14px;margin-bottom:14px} + .hero-copy,.hero-controls,.stat{background:var(--panel);backdrop-filter:blur(14px);border:1px solid var(--line);box-shadow:var(--shadow)} + .card{background:var(--card-shell);border:1px solid var(--card-line);box-shadow:var(--card-shadow)} + .hero-copy,.hero-controls{border-radius:18px;padding:18px} + .eyebrow{margin:0 0 6px;color:var(--accent);font-size:.76rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase} + h1{margin:0;font-size:clamp(1.9rem,4vw,3.1rem);line-height:.98;letter-spacing:-.05em} + .lede{margin:10px 0 0;max-width:54ch;color:var(--muted);font-size:.96rem} + .hero-controls{display:grid;gap:10px} + .row{display:flex;flex-wrap:wrap;gap:8px;align-items:center} .primary,.ghost,.mini,.tab{border-radius:999px;border:1px solid transparent;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} - .primary,.ghost{min-height:44px;padding:0 18px;font-weight:700} - .mini,.tab{min-height:34px;padding:0 12px;font-weight:600} + .primary,.ghost{min-height:38px;padding:0 14px;font-weight:700} + .mini,.tab{min-height:30px;padding:0 10px;font-weight:600} .primary{background:var(--accent);color:#fff7f2} .ghost,.mini{background:rgba(255,255,255,.76);border-color:var(--line);color:var(--ink)} .tab{background:transparent;color:var(--muted)} - .tab.active{background:#fff;border-color:var(--line);color:var(--ink)} + .tab.active{background:#fff;border-color:rgba(207,109,60,.18);color:var(--ink)} .primary:hover,.ghost:hover,.mini:hover,.tab:hover{transform:translateY(-1px)} - .chip{display:inline-flex;align-items:center;gap:10px;min-height:44px;padding:0 16px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);font-weight:600} + .chip{display:inline-flex;align-items:center;gap:8px;min-height:38px;padding:0 12px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);font-weight:600;font-size:.92rem} .chip input{margin:0;accent-color:var(--accent)} .chip select{border:none;background:transparent;outline:none;color:var(--ink)} - .search{flex:1 1 320px;min-height:48px;padding:0 16px;border-radius:16px;border:1px solid var(--line);background:rgba(255,255,255,.82);color:var(--ink);outline:none} - .status{display:inline-flex;align-items:center;min-height:40px;padding:0 14px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);color:var(--muted);font-weight:600} + .search{flex:1 1 260px;min-height:40px;padding:0 14px;border-radius:14px;border:1px solid var(--line);background:rgba(255,255,255,.82);color:var(--ink);outline:none} + .status{display:inline-flex;align-items:center;min-height:34px;padding:0 12px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);color:var(--muted);font-size:.9rem;font-weight:600} .status.ok{color:var(--ok);border-color:rgba(31,143,101,.22);background:rgba(31,143,101,.08)} .status.warn{color:var(--warn);border-color:rgba(157,95,0,.22);background:rgba(157,95,0,.08)} .status.bad{color:var(--bad);border-color:rgba(179,58,58,.22);background:rgba(179,58,58,.08)} - .stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;margin-bottom:18px} - .stat{border-radius:18px;padding:18px} - .stat span{display:block;margin-bottom:10px;color:var(--muted);font-size:.82rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} - .stat strong{display:block;font-size:clamp(2rem,4vw,2.6rem);line-height:1;letter-spacing:-.05em} + .stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:12px} + .stat{border-radius:16px;padding:14px} + .stat span{display:block;margin-bottom:8px;color:var(--muted);font-size:.74rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} + .stat strong{display:block;font-size:clamp(1.6rem,3vw,2rem);line-height:1;letter-spacing:-.05em} .stat strong.small{font-size:1.1rem;line-height:1.3;letter-spacing:-.02em} - .stat small{display:block;margin-top:10px;color:var(--muted);font-size:.95rem} - .banner,.empty{margin:0 0 18px;padding:16px 18px;border-radius:16px;border:1px solid var(--line);background:rgba(255,255,255,.82)} + .stat small{display:block;margin-top:8px;color:var(--muted);font-size:.85rem} + .banner,.empty{margin:0 0 12px;padding:12px 14px;border-radius:14px;border:1px solid var(--line);background:rgba(255,255,255,.82)} .banner{color:var(--bad);border-color:rgba(179,58,58,.24);background:rgba(179,58,58,.08)} - .list{display:grid;gap:18px} - .card{overflow:hidden;border-radius:18px} - .card.new{border-color:rgba(31,143,101,.28);box-shadow:var(--shadow),0 0 0 1px rgba(31,143,101,.12)} - .summary{list-style:none;display:grid;gap:12px;padding:20px;cursor:pointer} + .list{display:grid;gap:12px} + .card{overflow:hidden;border-radius:16px} + .card.new{border-color:rgba(31,143,101,.3);box-shadow:var(--card-shadow),0 0 0 1px rgba(31,143,101,.12)} + .summary{list-style:none;display:grid;gap:8px;padding:14px 16px;cursor:pointer;background:linear-gradient(180deg,rgba(255,250,244,.88),rgba(255,246,238,.96))} .summary::-webkit-details-marker{display:none} - .top,.tags,.toolbar,.actions,.attachments{display:flex;flex-wrap:wrap;gap:10px;align-items:center} + .top,.tags,.toolbar,.actions,.attachments{display:flex;flex-wrap:wrap;gap:8px;align-items:center} .top{justify-content:space-between} .head{min-width:0;flex:1 1 320px} - .head h2{margin:0;font-size:clamp(1.1rem,2vw,1.45rem);line-height:1.2;letter-spacing:-.03em;word-break:break-word} - .meta{margin:6px 0 0;color:var(--muted);font-size:.95rem;word-break:break-word} - .time,.tag{display:inline-flex;align-items:center;min-height:28px;padding:0 12px;border-radius:999px;font-size:.84rem;font-weight:700} + .head h2{margin:0;font-size:clamp(1rem,1.6vw,1.22rem);line-height:1.18;letter-spacing:-.03em;word-break:break-word} + .meta{margin:4px 0 0;color:var(--muted);font-size:.88rem;word-break:break-word} + .time,.tag{display:inline-flex;align-items:center;min-height:24px;padding:0 10px;border-radius:999px;font-size:.76rem;font-weight:700} .time{background:rgba(31,41,51,.06)} - .tag{background:rgba(31,41,51,.06);color:var(--muted)} + .tag{background:var(--accent-soft);color:#8d5632} .tag.new{background:rgba(31,143,101,.1);color:var(--ok)} .tag.bad{background:rgba(179,58,58,.1);color:var(--bad)} - .preview{margin:0} - .body{display:grid;gap:16px;padding:18px 20px 20px;border-top:1px solid var(--line);background:var(--panel-strong)} - .toolbar{justify-content:space-between;align-items:flex-start} - .tabs{display:inline-flex;gap:6px;padding:6px;border-radius:999px;background:rgba(31,41,51,.05)} - .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px} - .metaCard{padding:14px 16px;border-radius:14px;background:rgba(31,41,51,.035);border:1px solid rgba(31,41,51,.08)} - .metaCard dt{margin:0 0 6px;color:var(--muted);font-size:.8rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} + .preview{margin:0;color:#324150;font-size:.94rem} + .body{display:grid;gap:12px;padding:12px 16px 16px;border-top:1px solid rgba(207,109,60,.14);background:var(--card-body)} + .toolbar{justify-content:space-between;align-items:center} + .tabs{display:inline-flex;gap:4px;padding:4px;border-radius:999px;background:rgba(207,109,60,.08)} + .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px} + .metaCard{padding:10px 12px;border-radius:12px;background:rgba(255,255,255,.78);border:1px solid rgba(207,109,60,.12)} + .metaCard dt{margin:0 0 4px;color:var(--muted);font-size:.72rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} .metaCard dd{margin:0;word-break:break-word} - .attachments{gap:8px} - .attachment{padding:9px 12px;border-radius:12px;background:rgba(255,255,255,.72);border:1px solid var(--line);font-size:.92rem} - .panel{overflow:hidden;border-radius:14px;border:1px solid var(--line);background:#fff} - iframe{width:100%;min-height:720px;border:none;background:#fff} - pre{margin:0;padding:16px;white-space:pre-wrap;word-break:break-word;overflow:auto;font:13px/1.55 "Cascadia Code","Consolas",monospace;color:#102030;background:linear-gradient(180deg,rgba(207,109,60,.04),transparent 140px),#fff} - .placeholder,.inlineError{padding:16px} + .attachments{gap:6px} + .attachment{display:inline-flex;align-items:center;gap:8px;padding:7px 10px;border-radius:10px;background:rgba(255,248,240,.96);border:1px solid rgba(207,109,60,.12);font-size:.84rem} + .attachmentLink{color:#8d5632;text-decoration:none;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} + .attachmentLink:hover{transform:translateY(-1px);background:#fff;border-color:rgba(207,109,60,.28)} + .panel{overflow:hidden;border-radius:12px;border:1px solid rgba(207,109,60,.14);background:#fff} + iframe{width:100%;min-height:560px;border:none;background:#fff} + pre{margin:0;padding:12px;white-space:pre-wrap;word-break:break-word;overflow:auto;font:12.5px/1.45 "Cascadia Code","Consolas",monospace;color:#102030;background:linear-gradient(180deg,rgba(207,109,60,.04),transparent 140px),#fff} + .placeholder,.inlineError{padding:12px} .inlineError{color:var(--bad)} @media (max-width:1080px){.hero{grid-template-columns:1fr}.stats{grid-template-columns:repeat(2,minmax(0,1fr))}} - @media (max-width:720px){.page{padding:16px}.stats{grid-template-columns:1fr}.primary,.ghost,.chip{width:100%;justify-content:center}.toolbar,.row{align-items:stretch}iframe{min-height:560px}} + @media (max-width:720px){.page{padding:12px}.stats{grid-template-columns:1fr}.primary,.ghost,.chip{width:100%;justify-content:center}.toolbar,.row{align-items:stretch}iframe{min-height:420px}} `; } @@ -402,7 +495,8 @@ function clientApp(config) { knownIds: new Set(), openIds: new Set(), views: {}, - raw: {} + raw: {}, + listSignature: "" }; const el = { @@ -531,6 +625,8 @@ function clientApp(config) { return; } + let shouldRenderList = false; + state.loading = true; state.source = source; state.error = ""; @@ -548,19 +644,23 @@ function clientApp(config) { const payload = await response.json(); const messages = Array.isArray(payload.messages) ? payload.messages : []; const nextIds = new Set(messages.map((message) => message.id)); + const nextSignature = computeListSignature(messages); - state.newIds = state.updatedAt - ? new Set(messages.filter((message) => !state.knownIds.has(message.id)).map((message) => message.id)) - : new Set(); + shouldRenderList = nextSignature !== state.listSignature; + state.newIds = + state.updatedAt && shouldRenderList + ? new Set(messages.filter((message) => !state.knownIds.has(message.id)).map((message) => message.id)) + : state.newIds; state.knownIds = nextIds; state.messages = messages; state.duration = payload.fetchDurationMs || 0; state.parseErrors = payload.parseErrors || 0; state.newest = payload.latestMessageTimestamp || ""; state.updatedAt = Date.now(); + state.listSignature = nextSignature; pruneState(); - applyFilter(); + applyFilter(shouldRenderList); setStatus(`Updated ${messages.length} message${messages.length === 1 ? "" : "s"}.`, "ok"); } catch (error) { state.error = error.message || "Unknown refresh error"; @@ -568,7 +668,7 @@ function clientApp(config) { } finally { state.loading = false; scheduleRefresh(); - renderAll(); + renderAll({ renderList: shouldRenderList }); } } @@ -590,12 +690,28 @@ function clientApp(config) { }); } - function applyFilter() { + function applyFilter(shouldRenderList = true) { const search = state.search.trim().toLowerCase(); state.filtered = !search ? [...state.messages] : state.messages.filter((message) => haystack(message).includes(search)); - renderAll(); + renderAll({ renderList: shouldRenderList }); + } + + function computeListSignature(messages) { + return messages + .map((message) => + [ + message.id, + message.timestampMs || 0, + message.rawSizeBytes || 0, + message.attachmentCount || 0, + message.hasHtml ? 1 : 0, + message.preview || "", + message.parseError || "" + ].join("::") + ) + .join("|"); } function haystack(message) { @@ -624,11 +740,14 @@ function clientApp(config) { state.timer = window.setTimeout(() => refreshMessages("auto"), state.interval); } - function renderAll() { + function renderAll(options = {}) { + const { renderList: shouldRenderList = true } = options; renderStats(); renderFetch(); renderStatus(); - renderList(); + if (shouldRenderList) { + renderList(); + } renderLiveClock(); } @@ -814,7 +933,7 @@ function clientApp(config) { ? `
${message.attachments .map((attachment) => { const size = attachment.size ? `, ${formatBytes(attachment.size)}` : ""; - return `${escapeHtml(attachment.filename)} (${escapeHtml(attachment.contentType)}${escapeHtml(size)})`; + return `${escapeHtml(attachment.filename)} (${escapeHtml(attachment.contentType)}${escapeHtml(size)})`; }) .join("")}
` : "" diff --git a/_reference/commission-based-cut-manual-test-plan.md b/_reference/testPlans/commission-based-cut-manual-test-plan.md similarity index 100% rename from _reference/commission-based-cut-manual-test-plan.md rename to _reference/testPlans/commission-based-cut-manual-test-plan.md diff --git a/_reference/testPlans/select-component-test-plan.md b/_reference/testPlans/select-component-test-plan.md new file mode 100644 index 000000000..ae5eff2c2 --- /dev/null +++ b/_reference/testPlans/select-component-test-plan.md @@ -0,0 +1,647 @@ +# Ant Design Select.Option Deprecation - Manual Testing Plan +**Branch:** `feature/IO-3544-Ant-Select-Deprecation` +**Base Branch:** `master-AIO` +**Jira:** IO-3544 + +## Overview +This branch migrates all Ant Design `` components to the new `options` prop pattern (required for Ant Design v5+). The deprecated `Select.Option` child component pattern has been replaced with the `options` array prop. + +## What Changed +- **Old Pattern:** `` +- **New Pattern:** ` Live refresh @@ -359,29 +615,94 @@ function renderHtml() { + Waiting for first refresh...
-
-
- Waiting for first refresh... +
+ + +
+
Total00 visible
+
New0New since last refresh
+
NewestNo messagesNot refreshed yet
+
FetchIdleEndpoint: ${escapeHtml(SES_ENDPOINT)}
+
+ +
+
+ + +
- - -
-
Total00 visible
-
New0New since last refresh
-
NewestNo messagesNot refreshed yet
-
FetchIdleEndpoint: ${escapeHtml(SES_ENDPOINT)}
- - -
+ @@ -391,23 +712,32 @@ function renderHtml() { function renderStyles() { return ` - :root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);} + :root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--log-shell:linear-gradient(180deg,rgba(239,246,255,.98),rgba(248,251,255,.99));--log-body:#f8fbff;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--log-line:rgba(48,113,169,.22);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--info:#3071a9;--info-soft:rgba(48,113,169,.1);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);--log-shadow:0 16px 32px rgba(48,113,169,.12);} *{box-sizing:border-box} html,body{margin:0;min-height:100%} body{background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.12),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:15px/1.45 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif} button,input,select,textarea{font:inherit} button{cursor:pointer} - .page{max-width:1360px;margin:0 auto;padding:18px} - .hero{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(320px,.95fr);gap:14px;margin-bottom:14px} - .hero-copy,.hero-controls,.stat{background:var(--panel);backdrop-filter:blur(14px);border:1px solid var(--line);box-shadow:var(--shadow)} + .page{display:grid;gap:14px;max-width:1360px;min-height:100vh;margin:0 auto;padding:18px} + .hero{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(320px,.95fr);gap:14px;margin-bottom:0} + .hero-copy,.hero-controls,.toolControls,.stat{background:var(--panel);backdrop-filter:blur(14px);border:1px solid var(--line);box-shadow:var(--shadow)} .card{background:var(--card-shell);border:1px solid var(--card-line);box-shadow:var(--card-shadow)} - .hero-copy,.hero-controls{border-radius:18px;padding:18px} + .hero-copy,.hero-controls,.toolControls{border-radius:18px;padding:18px} .eyebrow{margin:0 0 6px;color:var(--accent);font-size:.76rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase} h1{margin:0;font-size:clamp(1.9rem,4vw,3.1rem);line-height:.98;letter-spacing:-.05em} .lede{margin:10px 0 0;max-width:54ch;color:var(--muted);font-size:.96rem} .hero-controls{display:grid;gap:10px} + .helper{margin:0;color:var(--muted);font-size:.95rem} + .workspaceTabs{display:flex;flex-wrap:wrap;gap:8px;padding:6px;border-radius:999px;background:rgba(31,41,51,.05)} + .workspaceTab,.primary,.ghost,.mini,.tab{border-radius:999px;border:1px solid transparent;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} + .workspaceTab{min-height:34px;padding:0 14px;background:transparent;color:var(--muted);font-weight:700} + .workspaceTab.active{background:#fff;border-color:rgba(31,41,51,.08);color:var(--ink)} + .workspacePanel{display:grid;gap:12px} + .workspacePanel[hidden]{display:none} + .toolControls{display:grid;gap:10px} + .contentPane{height:clamp(360px,50vh,720px);overflow:auto;padding-right:4px} + .contentStack{display:grid;gap:12px;min-width:min-content} .row{display:flex;flex-wrap:wrap;gap:8px;align-items:center} - .primary,.ghost,.mini,.tab{border-radius:999px;border:1px solid transparent;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} .primary,.ghost{min-height:38px;padding:0 14px;font-weight:700} .mini,.tab{min-height:30px;padding:0 10px;font-weight:600} .primary{background:var(--accent);color:#fff7f2} @@ -429,9 +759,10 @@ function renderStyles() { .stat strong{display:block;font-size:clamp(1.6rem,3vw,2rem);line-height:1;letter-spacing:-.05em} .stat strong.small{font-size:1.1rem;line-height:1.3;letter-spacing:-.02em} .stat small{display:block;margin-top:8px;color:var(--muted);font-size:.85rem} - .banner,.empty{margin:0 0 12px;padding:12px 14px;border-radius:14px;border:1px solid var(--line);background:rgba(255,255,255,.82)} + .banner,.empty{margin:0;padding:12px 14px;border-radius:14px;border:1px solid var(--line);background:rgba(255,255,255,.82)} .banner{color:var(--bad);border-color:rgba(179,58,58,.24);background:rgba(179,58,58,.08)} - .list{display:grid;gap:12px} + .list{display:grid;gap:12px;align-content:start} + .logList{display:grid;gap:10px;align-content:start} .card{overflow:hidden;border-radius:16px} .card.new{border-color:rgba(31,143,101,.3);box-shadow:var(--card-shadow),0 0 0 1px rgba(31,143,101,.12)} .summary{list-style:none;display:grid;gap:8px;padding:14px 16px;cursor:pointer;background:linear-gradient(180deg,rgba(255,250,244,.88),rgba(255,246,238,.96))} @@ -459,12 +790,22 @@ function renderStyles() { .attachmentLink{color:#8d5632;text-decoration:none;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} .attachmentLink:hover{transform:translateY(-1px);background:#fff;border-color:rgba(207,109,60,.28)} .panel{overflow:hidden;border-radius:12px;border:1px solid rgba(207,109,60,.14);background:#fff} + .logEvent{overflow:hidden;border-radius:16px;border:1px solid var(--log-line);background:var(--log-shell);box-shadow:var(--log-shadow)} + .logSummary{list-style:none;display:grid;gap:8px;padding:12px 14px;cursor:pointer} + .logSummary::-webkit-details-marker{display:none} + .logSummaryTop{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:center} + .logMeta{display:flex;flex-wrap:wrap;gap:8px;align-items:center} + .logTag{background:var(--info-soft);color:var(--info);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .logPreview{margin:0;color:#324150;font-size:.9rem} + .logBody{padding:0 14px 14px;border-top:1px solid rgba(48,113,169,.14);background:var(--log-body)} + .logActions{display:flex;justify-content:flex-end;padding:12px 0 0} + .logBody pre{background:linear-gradient(180deg,rgba(48,113,169,.04),transparent 140px),#fff} iframe{width:100%;min-height:560px;border:none;background:#fff} pre{margin:0;padding:12px;white-space:pre-wrap;word-break:break-word;overflow:auto;font:12.5px/1.45 "Cascadia Code","Consolas",monospace;color:#102030;background:linear-gradient(180deg,rgba(207,109,60,.04),transparent 140px),#fff} .placeholder,.inlineError{padding:12px} .inlineError{color:var(--bad)} @media (max-width:1080px){.hero{grid-template-columns:1fr}.stats{grid-template-columns:repeat(2,minmax(0,1fr))}} - @media (max-width:720px){.page{padding:12px}.stats{grid-template-columns:1fr}.primary,.ghost,.chip{width:100%;justify-content:center}.toolbar,.row{align-items:stretch}iframe{min-height:420px}} + @media (max-width:720px){.page{padding:12px}.stats{grid-template-columns:1fr}.workspaceTab,.primary,.ghost,.chip{width:100%;justify-content:center}.toolbar,.row{align-items:stretch}.logSummaryTop{align-items:flex-start}.contentPane{height:clamp(300px,48vh,560px)}iframe{min-height:420px}} `; } @@ -478,6 +819,11 @@ function escapeHtml(value) { } function clientApp(config) { + const appState = { + panel: "emails", + logsReady: false + }; + const state = { messages: [], filtered: [], @@ -499,7 +845,34 @@ function clientApp(config) { listSignature: "" }; + const logsState = { + groups: [], + streams: [], + events: [], + filtered: [], + group: config.defaultLogGroup || "", + stream: "", + search: "", + auto: true, + interval: config.defaultRefreshMs, + windowMs: config.defaultLogWindowMs, + limit: config.defaultLogLimit, + loading: false, + error: "", + updatedAt: 0, + source: "initial", + duration: 0, + newest: 0, + searchedLogStreams: 0, + openIds: new Set(), + hasInteracted: false, + listSignature: "" + }; + const el = { + workspaceTabs: document.getElementById("workspaceTabs"), + emailsPanel: document.getElementById("emailsPanel"), + logsPanel: document.getElementById("logsPanel"), refreshButton: document.getElementById("refreshButton"), autoToggle: document.getElementById("autoToggle"), intervalSelect: document.getElementById("intervalSelect"), @@ -517,16 +890,54 @@ function clientApp(config) { fetchDetail: document.getElementById("fetchDetail"), banner: document.getElementById("banner"), empty: document.getElementById("empty"), - list: document.getElementById("list") + list: document.getElementById("list"), + logsRefreshButton: document.getElementById("logsRefreshButton"), + logsAutoToggle: document.getElementById("logsAutoToggle"), + logsIntervalSelect: document.getElementById("logsIntervalSelect"), + logsGroupSelect: document.getElementById("logsGroupSelect"), + logsStreamSelect: document.getElementById("logsStreamSelect"), + logsWindowSelect: document.getElementById("logsWindowSelect"), + logsLimitSelect: document.getElementById("logsLimitSelect"), + logsSearchInput: document.getElementById("logsSearchInput"), + logsClearSearchButton: document.getElementById("logsClearSearchButton"), + logsStatusChip: document.getElementById("logsStatusChip"), + logsTotalStat: document.getElementById("logsTotalStat"), + logsVisibleStat: document.getElementById("logsVisibleStat"), + logsStreamsStat: document.getElementById("logsStreamsStat"), + logsNewestStat: document.getElementById("logsNewestStat"), + logsUpdatedStat: document.getElementById("logsUpdatedStat"), + logsFetchStat: document.getElementById("logsFetchStat"), + logsFetchDetail: document.getElementById("logsFetchDetail"), + logsBanner: document.getElementById("logsBanner"), + logsEmpty: document.getElementById("logsEmpty"), + logsList: document.getElementById("logsList") }; el.intervalSelect.value = String(config.defaultRefreshMs); + el.logsIntervalSelect.value = String(config.defaultRefreshMs); + el.logsWindowSelect.value = String(config.defaultLogWindowMs); + el.logsLimitSelect.value = String(config.defaultLogLimit); wire(); + renderWorkspace(); renderAll(); + renderLogsAll(); refreshMessages("initial"); - window.setInterval(renderLiveClock, 1000); + window.setInterval(() => { + renderLiveClock(); + renderLogsLiveClock(); + }, 1000); function wire() { + el.workspaceTabs.addEventListener("click", async (event) => { + const button = event.target.closest("button[data-panel]"); + + if (!button) { + return; + } + + await setPanel(button.dataset.panel); + }); + el.refreshButton.addEventListener("click", () => refreshMessages("manual")); el.autoToggle.addEventListener("change", () => { @@ -598,10 +1009,88 @@ function clientApp(config) { } }); + el.logsRefreshButton.addEventListener("click", () => refreshLogs("manual")); + + el.logsAutoToggle.addEventListener("change", () => { + logsState.auto = el.logsAutoToggle.checked; + scheduleLogsRefresh(); + renderLogsStatus(); + }); + + el.logsIntervalSelect.addEventListener("change", () => { + logsState.interval = Number(el.logsIntervalSelect.value) || config.defaultRefreshMs; + scheduleLogsRefresh(); + renderLogsStatus(); + }); + + el.logsWindowSelect.addEventListener("change", () => { + logsState.windowMs = Number(el.logsWindowSelect.value) || config.defaultLogWindowMs; + refreshLogs("window"); + }); + + el.logsLimitSelect.addEventListener("change", () => { + logsState.limit = Number(el.logsLimitSelect.value) || config.defaultLogLimit; + refreshLogs("limit"); + }); + + el.logsSearchInput.addEventListener("input", (event) => { + logsState.search = event.target.value; + applyLogsFilter(); + }); + + el.logsClearSearchButton.addEventListener("click", () => { + logsState.search = ""; + el.logsSearchInput.value = ""; + applyLogsFilter(); + }); + + el.logsGroupSelect.addEventListener("change", async () => { + logsState.group = el.logsGroupSelect.value; + logsState.stream = ""; + await refreshLogStreams(); + await refreshLogs("group"); + }); + + el.logsStreamSelect.addEventListener("change", () => { + logsState.stream = el.logsStreamSelect.value; + refreshLogs("stream"); + }); + + el.logsList.addEventListener("click", async (event) => { + const button = event.target.closest("button[data-log-action]"); + + if (!button) { + return; + } + + const logEvent = getLogEvent(button.dataset.id); + + if (!logEvent) { + return; + } + + if (button.dataset.logAction === "copy") { + await copyText(formatLogMessage(logEvent.message)); + setLogsStatus("Log payload copied to the clipboard.", "ok"); + } + }); + document.addEventListener("visibilitychange", () => { window.clearTimeout(state.timer); + window.clearTimeout(logsState.timer); - if (!document.hidden && state.auto) { + if (document.hidden) { + renderStatus(); + renderLogsStatus(); + return; + } + + if (appState.panel === "logs" && logsState.auto) { + refreshLogs("visibility"); + return; + } + + if (state.auto) { refreshMessages("visibility"); } else { renderStatus(); @@ -615,11 +1104,64 @@ function clientApp(config) { if (!isField && event.key.toLowerCase() === "r") { event.preventDefault(); + + if (appState.panel === "logs") { + refreshLogs("keyboard"); + return; + } + refreshMessages("keyboard"); } }); } + async function setPanel(panel) { + if (!panel || panel === appState.panel) { + if (panel === "logs") { + await ensureLogsReady(); + } + + renderWorkspace(); + return; + } + + appState.panel = panel; + renderWorkspace(); + + if (panel === "logs") { + await ensureLogsReady(); + } + + scheduleRefresh(); + scheduleLogsRefresh(); + renderStatus(); + renderLogsStatus(); + } + + function renderWorkspace() { + el.emailsPanel.hidden = appState.panel !== "emails"; + el.logsPanel.hidden = appState.panel !== "logs"; + + el.workspaceTabs.querySelectorAll("button[data-panel]").forEach((button) => { + const active = button.dataset.panel === appState.panel; + button.classList.toggle("active", active); + button.setAttribute("aria-pressed", active ? "true" : "false"); + }); + } + + async function ensureLogsReady() { + if (appState.logsReady) { + return; + } + + await refreshLogGroups(); + appState.logsReady = !logsState.error; + + if (logsState.group) { + await refreshLogs("initial"); + } + } + async function refreshMessages(source) { if (state.loading) { return; @@ -672,6 +1214,144 @@ function clientApp(config) { } } + async function refreshLogGroups() { + try { + const response = await fetch("/api/logs/groups", { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + logsState.groups = Array.isArray(payload.groups) ? payload.groups : []; + + const availableGroups = logsState.groups.map((group) => group.name).filter(Boolean); + + if (!availableGroups.includes(logsState.group)) { + logsState.group = availableGroups.includes(config.defaultLogGroup) + ? config.defaultLogGroup + : availableGroups[0] || ""; + } + + logsState.error = ""; + await refreshLogStreams(); + } catch (error) { + logsState.error = error.message || "Unknown log group refresh error"; + } finally { + renderLogsAll(); + } + } + + async function refreshLogStreams() { + if (!logsState.group) { + logsState.streams = []; + logsState.stream = ""; + renderLogsAll(); + return; + } + + try { + const response = await fetch(`/api/logs/streams?group=${encodeURIComponent(logsState.group)}`, { + cache: "no-store" + }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + logsState.streams = Array.isArray(payload.streams) ? payload.streams : []; + + if (!logsState.streams.some((stream) => stream.name === logsState.stream)) { + logsState.stream = ""; + } + + logsState.error = ""; + } catch (error) { + logsState.streams = []; + logsState.stream = ""; + logsState.error = error.message || "Unknown log stream refresh error"; + } finally { + renderLogsAll(); + } + } + + async function refreshLogs(source) { + if (logsState.loading) { + return; + } + + if (!appState.logsReady) { + await ensureLogsReady(); + return; + } + + if (!logsState.group) { + logsState.error = logsState.groups.length ? "Choose a log group to load events." : "No log groups found."; + renderLogsAll(); + return; + } + + let shouldRenderList = false; + + logsState.loading = true; + logsState.source = source; + logsState.error = ""; + renderLogsStatus(); + renderLogsFetch(); + + try { + const params = new URLSearchParams({ + group: logsState.group, + windowMs: String(logsState.windowMs), + limit: String(logsState.limit) + }); + + if (logsState.stream) { + params.set("stream", logsState.stream); + } + + const response = await fetch(`/api/logs/events?${params.toString()}`, { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + const events = Array.isArray(payload.events) ? payload.events : []; + const nextSignature = computeLogListSignature(events); + shouldRenderList = nextSignature !== logsState.listSignature; + logsState.events = events; + logsState.duration = payload.fetchDurationMs || 0; + logsState.newest = payload.latestTimestamp || 0; + logsState.updatedAt = Date.now(); + logsState.searchedLogStreams = payload.searchedLogStreams || (logsState.stream ? 1 : logsState.streams.length); + logsState.listSignature = nextSignature; + + pruneLogsState(); + applyLogsFilter(shouldRenderList); + setLogsStatus(`Updated ${logsState.events.length} log event${logsState.events.length === 1 ? "" : "s"}.`, "ok"); + } catch (error) { + logsState.error = error.message || "Unknown log refresh error"; + setLogsStatus(`Refresh failed: ${logsState.error}`, "bad"); + } finally { + logsState.loading = false; + scheduleLogsRefresh(); + renderLogsAll({ renderList: shouldRenderList }); + } + } + + function applyLogsFilter(shouldRenderList = true) { + const search = logsState.search.trim().toLowerCase(); + logsState.filtered = !search + ? [...logsState.events] + : logsState.events.filter((event) => logHaystack(event).includes(search)); + renderLogsAll({ renderList: shouldRenderList }); + } + function pruneState() { const ids = new Set(state.messages.map((message) => message.id)); state.openIds = new Set([...state.openIds].filter((id) => ids.has(id))); @@ -690,6 +1370,11 @@ function clientApp(config) { }); } + function pruneLogsState() { + const ids = new Set(logsState.events.map((event) => event.id)); + logsState.openIds = new Set([...logsState.openIds].filter((id) => ids.has(id))); + } + function applyFilter(shouldRenderList = true) { const search = state.search.trim().toLowerCase(); state.filtered = !search @@ -714,6 +1399,16 @@ function clientApp(config) { .join("|"); } + function computeLogListSignature(events) { + return events + .map((event) => + [event.id, event.timestamp || 0, event.ingestionTime || 0, event.logStreamName || "", event.message || ""].join( + "::" + ) + ) + .join("|"); + } + function haystack(message) { return [ message.subject, @@ -730,16 +1425,30 @@ function clientApp(config) { .toLowerCase(); } + function logHaystack(event) { + return [event.logStreamName, event.message, event.preview].filter(Boolean).join(" ").toLowerCase(); + } + function scheduleRefresh() { window.clearTimeout(state.timer); - if (!state.auto || document.hidden || state.loading) { + if (appState.panel !== "emails" || !state.auto || document.hidden || state.loading) { return; } state.timer = window.setTimeout(() => refreshMessages("auto"), state.interval); } + function scheduleLogsRefresh() { + window.clearTimeout(logsState.timer); + + if (appState.panel !== "logs" || !logsState.auto || document.hidden || logsState.loading) { + return; + } + + logsState.timer = window.setTimeout(() => refreshLogs("auto"), logsState.interval); + } + function renderAll(options = {}) { const { renderList: shouldRenderList = true } = options; renderStats(); @@ -751,6 +1460,18 @@ function clientApp(config) { renderLiveClock(); } + function renderLogsAll(options = {}) { + const { renderList: shouldRenderList = true } = options; + renderLogsFilters(); + renderLogsStats(); + renderLogsFetch(); + renderLogsStatus(); + if (shouldRenderList) { + renderLogsList(); + } + renderLogsLiveClock(); + } + function renderStats() { el.totalStat.textContent = String(state.messages.length); el.visibleStat.textContent = `${state.filtered.length} visible${state.search ? " after search" : ""}`; @@ -758,6 +1479,34 @@ function clientApp(config) { el.newestStat.textContent = state.newest ? formatDateTime(state.newest) : "No messages"; } + function renderLogsFilters() { + const groups = logsState.groups.length + ? logsState.groups.map((group) => ``) + : ['']; + const streams = [ + '', + ...logsState.streams.map( + (stream) => `` + ) + ]; + + el.logsGroupSelect.innerHTML = groups.join(""); + el.logsStreamSelect.innerHTML = streams.join(""); + + if (logsState.group) { + el.logsGroupSelect.value = logsState.group; + } + + el.logsStreamSelect.value = logsState.stream; + } + + function renderLogsStats() { + el.logsTotalStat.textContent = String(logsState.events.length); + el.logsVisibleStat.textContent = `${logsState.filtered.length} visible${logsState.search ? " after search" : ""}`; + el.logsStreamsStat.textContent = String(logsState.streams.length || logsState.searchedLogStreams || 0); + el.logsNewestStat.textContent = logsState.newest ? formatDateTime(logsState.newest) : "No events"; + } + function renderFetch() { if (state.loading) { el.fetchStat.textContent = "Refreshing..."; @@ -781,6 +1530,29 @@ function clientApp(config) { el.fetchDetail.textContent = `${state.parseErrors} parse error${state.parseErrors === 1 ? "" : "s"}. Endpoint: ${config.endpoint}`; } + function renderLogsFetch() { + if (logsState.loading) { + el.logsFetchStat.textContent = "Refreshing..."; + el.logsFetchDetail.textContent = `Endpoint: ${config.cloudWatchEndpoint} (${config.cloudWatchRegion})`; + return; + } + + if (logsState.error) { + el.logsFetchStat.textContent = "Needs attention"; + el.logsFetchDetail.textContent = logsState.error; + return; + } + + if (!logsState.updatedAt) { + el.logsFetchStat.textContent = "Idle"; + el.logsFetchDetail.textContent = `Endpoint: ${config.cloudWatchEndpoint} (${config.cloudWatchRegion})`; + return; + } + + el.logsFetchStat.textContent = `${logsState.duration}ms`; + el.logsFetchDetail.textContent = `${logsState.group || "No group selected"}. Endpoint: ${config.cloudWatchEndpoint}`; + } + function renderStatus() { el.statusChip.className = "status"; @@ -817,6 +1589,42 @@ function clientApp(config) { el.statusChip.textContent = `Live refresh on, next check in ${seconds}s`; } + function renderLogsStatus() { + el.logsStatusChip.className = "status"; + + if (logsState.loading) { + el.logsStatusChip.classList.add("warn"); + el.logsStatusChip.textContent = "Refreshing logs..."; + return; + } + + if (logsState.error) { + el.logsStatusChip.classList.add("bad"); + el.logsStatusChip.textContent = `Refresh failed: ${logsState.error}`; + return; + } + + if (!logsState.auto) { + el.logsStatusChip.textContent = "Live refresh paused"; + return; + } + + if (document.hidden) { + el.logsStatusChip.classList.add("warn"); + el.logsStatusChip.textContent = "Tab hidden, live refresh paused"; + return; + } + + if (!logsState.updatedAt) { + el.logsStatusChip.textContent = "Waiting for first refresh..."; + return; + } + + const seconds = Math.max(0, Math.ceil((logsState.updatedAt + logsState.interval - Date.now()) / 1000)); + el.logsStatusChip.classList.add("ok"); + el.logsStatusChip.textContent = `Live refresh on, next check in ${seconds}s`; + } + function renderLiveClock() { if (!state.updatedAt) { el.updatedStat.textContent = "Not refreshed yet"; @@ -827,6 +1635,16 @@ function clientApp(config) { renderStatus(); } + function renderLogsLiveClock() { + if (!logsState.updatedAt) { + el.logsUpdatedStat.textContent = "Not refreshed yet"; + return; + } + + el.logsUpdatedStat.textContent = `Updated ${formatRelative(logsState.updatedAt)} via ${logsState.source}`; + renderLogsStatus(); + } + function renderList() { el.banner.hidden = !state.error; el.banner.textContent = state.error ? `Refresh failed: ${state.error}` : ""; @@ -846,6 +1664,76 @@ function clientApp(config) { el.list.querySelectorAll(".card[open]").forEach((details) => hydrate(details, getMessage(details.dataset.id))); } + function renderLogsList() { + el.logsBanner.hidden = !logsState.error; + el.logsBanner.textContent = logsState.error ? `Refresh failed: ${logsState.error}` : ""; + + if (!logsState.group && !logsState.groups.length) { + el.logsList.innerHTML = ""; + el.logsEmpty.hidden = false; + el.logsEmpty.textContent = "No CloudWatch log groups found in LocalStack yet."; + return; + } + + if (!logsState.filtered.length) { + el.logsList.innerHTML = ""; + el.logsEmpty.hidden = false; + el.logsEmpty.textContent = logsState.events.length + ? "No log events match the current search." + : "No log events found for the selected group, stream, and time window."; + return; + } + + el.logsEmpty.hidden = true; + el.logsList.innerHTML = logsState.filtered.map((event, index) => renderLogEvent(event, index)).join(""); + bindLogToggles(); + } + + function renderLogEvent(event, index) { + const open = logsState.openIds.has(event.id) || (!logsState.hasInteracted && index === 0) ? "open" : ""; + + return ` +
+ +
+
+ ${escapeHtml(formatDateTime(event.timestamp))} + ${escapeHtml(event.logStreamName || "Unknown stream")} +
+ ${escapeHtml(formatRelative(event.timestamp || Date.now()))} +
+

${escapeHtml(event.preview || "No preview available.")}

+
+
+
+ +
+
${escapeHtml(formatLogMessage(event.message))}
+
+
+ `; + } + + function bindLogToggles() { + el.logsList.querySelectorAll(".logEvent").forEach((details) => { + details.addEventListener("toggle", () => { + const id = details.dataset.id; + + if (!id) { + return; + } + + logsState.hasInteracted = true; + + if (details.open) { + logsState.openIds.add(id); + } else { + logsState.openIds.delete(id); + } + }); + }); + } + function bindCardToggles() { el.list.querySelectorAll(".card").forEach((details) => { details.addEventListener("toggle", () => { @@ -999,6 +1887,10 @@ function clientApp(config) { return state.messages.find((message) => message.id === id); } + function getLogEvent(id) { + return logsState.events.find((event) => event.id === id); + } + async function loadRaw(id) { if (state.raw[id]?.status === "loaded") { return state.raw[id].value; @@ -1056,6 +1948,16 @@ function clientApp(config) { el.statusChip.textContent = message; } + function setLogsStatus(message, tone) { + el.logsStatusChip.className = "status"; + + if (tone) { + el.logsStatusChip.classList.add(tone); + } + + el.logsStatusChip.textContent = message; + } + function formatDateTime(value) { if (!value) { return "Unknown time"; @@ -1106,6 +2008,20 @@ function clientApp(config) { return `${index === 0 ? size : size.toFixed(1)} ${units[index]}`; } + function formatLogMessage(message) { + const value = String(message || "").trim(); + + if (!value) { + return "No log payload."; + } + + try { + return JSON.stringify(JSON.parse(value), null, 2); + } catch { + return value; + } + } + function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") @@ -1125,6 +2041,7 @@ function clientApp(config) { } app.listen(PORT, () => { - console.log(`Local email viewer is running on http://localhost:${PORT}`); + console.log(`LocalStack inspector is running on http://localhost:${PORT}`); console.log(`Watching LocalStack SES endpoint at ${SES_ENDPOINT}`); + console.log(`Watching LocalStack CloudWatch Logs endpoint at ${CLOUDWATCH_ENDPOINT} (${CLOUDWATCH_REGION})`); }); diff --git a/_reference/localEmailViewer/package-lock.json b/_reference/localEmailViewer/package-lock.json index 2c7ecad93..2dec2be60 100644 --- a/_reference/localEmailViewer/package-lock.json +++ b/_reference/localEmailViewer/package-lock.json @@ -9,11 +9,634 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@aws-sdk/client-cloudwatch-logs": "^3.1012.0", "express": "^5.1.0", "mailparser": "^3.7.4", "node-fetch": "^3.3.2" } }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch-logs": { + "version": "3.1012.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1012.0.tgz", + "integrity": "sha512-nkMczmeCG9a1YD48zDKZLX7wLGb5ycXMJXVFnXF8xVxQ2v2uo2xFz1ZZganWHSQxR6ZfBPG7R9OTkDrVx79ZPA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/credential-provider-node": "^3.972.22", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.22", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.8", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.12", + "@smithy/eventstream-serde-browser": "^4.2.12", + "@smithy/eventstream-serde-config-resolver": "^4.3.12", + "@smithy/eventstream-serde-node": "^4.2.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-retry": "^4.4.43", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.42", + "@smithy/util-defaults-mode-node": "^4.2.45", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.973.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.21.tgz", + "integrity": "sha512-OTUcDX9Yfz/FLKbHjiMaP9D4Hs44lYJzN7zBcrK2nDmBt0Wr8D6nYt12QoBkZsW0nVMFsTIGaZCrsU9zCcIMXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/xml-builder": "^3.972.12", + "@smithy/core": "^3.23.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.19.tgz", + "integrity": "sha512-33NpkQtmnsjLr9QdZvL3w8bjy+WoBJ+jY8JwuzxIq38rDNi1kwpBWW7Yjh+8bMlksd+ZAWW0fH4S/6OeoAdU5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.21.tgz", + "integrity": "sha512-xFke7yjbON4unNOG0TApQwz+o1LH5VhVLgWlUuiLRWNDyBfeHIFje2ck8qHybvJ8Fkm5m3SsN+pvHtVo6PGWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.21.tgz", + "integrity": "sha512-fmJN7KhB7CoG65w9fC2LVOd2wZbR2d1yJIpZNe2J5CeDPu7nUHSmavuJAeGEoE3OL5UIBVPNhmK/fV/NQrs3Hw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/credential-provider-env": "^3.972.19", + "@aws-sdk/credential-provider-http": "^3.972.21", + "@aws-sdk/credential-provider-login": "^3.972.21", + "@aws-sdk/credential-provider-process": "^3.972.19", + "@aws-sdk/credential-provider-sso": "^3.972.21", + "@aws-sdk/credential-provider-web-identity": "^3.972.21", + "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.21.tgz", + "integrity": "sha512-ENU+YCiuQocQjfIf9bPxZ+ZY0wIBkl3SMH22optBQwy8UFpSfonHynXzGT27xQxer4cYTNOpwDqbfo57BusbpQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.22.tgz", + "integrity": "sha512-VE6i8nkmrRyhKut7nnfCWRbdDf+CfyRr8ixSwdaPDguYlgvkAO2pHu9oK11XzbSuatB0io1ozI/vpYhelXn8Pg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.19", + "@aws-sdk/credential-provider-http": "^3.972.21", + "@aws-sdk/credential-provider-ini": "^3.972.21", + "@aws-sdk/credential-provider-process": "^3.972.19", + "@aws-sdk/credential-provider-sso": "^3.972.21", + "@aws-sdk/credential-provider-web-identity": "^3.972.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.19.tgz", + "integrity": "sha512-hjj5bFo4kf5/WzAMjDEFByVOMbq5gZiagIpJexf7Kp9nIDaGzhCphMsx03NCA8s9zUJzHlD1lXazd7MS+e03Lg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.21.tgz", + "integrity": "sha512-9jWRCuMZpZKlqCZ46bvievqdfswsyB2yPAr9rOiN+FxaGgf8jrR5iYDqJgscvk1jrbAxiK4cIjHv3XjIAWAhzQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/token-providers": "3.1012.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.21.tgz", + "integrity": "sha512-ShWQO/cQVZ+j3zUDK7Kj+m7grPzQCVA2iaZdJ+hJTGvVH5lR32Ip/rgZZ+zBdH6D6wczP9Upa4NMXoqJdGpK1g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.22.tgz", + "integrity": "sha512-pZPNGWZVQvgUIO/P9PXZNz7ciq9mLYb/wQEurg3phKTa3DiBIunIRcgA0eBNwmog6S3oy0KR1bv4EJ4ld9A5sQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.996.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.11.tgz", + "integrity": "sha512-i7SwoSR4JB/79JoGDUACnFUQOZwXGLWNX35lIb1Pq72nUGlVV+RFZp+BLa8S+mog2pbXU9+6Kc5YwGiMi5bKhQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.22", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.8", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-retry": "^4.4.43", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.42", + "@smithy/util-defaults-mode-node": "^4.2.45", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", + "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1012.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1012.0.tgz", + "integrity": "sha512-vzKwy020zjuiF4WTJzejx5nYcXJnRhHpb6i3lyZHIwfFwXG1yX4bzBVNMWYWF+bz1i2Pp2VhJbPyzpqj4VuJXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.21", + "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.8.tgz", + "integrity": "sha512-Kvb96TafGPLYo4Z2GRCzQTne77epXgiZEo0DDXwavzkWmgDV/1XD1tMA766gzRcHHFUraWsE+4T8DKtPTZUxgQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.22", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.13.tgz", + "integrity": "sha512-I/+BMxM4WE/6xL0tyV7tAUDOAXmyw/va1oGr/eSly43HmLUcD1G+v96vEKAA8VoLcZ03ZQo/PWzjmN9zQErqPQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "fast-xml-parser": "5.5.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@selderee/plugin-htmlparser2": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", @@ -27,6 +650,657 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", + "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz", + "integrity": "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.12", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", + "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", + "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz", + "integrity": "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz", + "integrity": "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz", + "integrity": "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz", + "integrity": "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz", + "integrity": "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", + "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", + "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", + "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", + "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.27", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.27.tgz", + "integrity": "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-middleware": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.44", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.44.tgz", + "integrity": "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", + "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", + "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", + "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", + "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", + "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", + "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", + "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", + "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", + "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", + "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", + "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.7.tgz", + "integrity": "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", + "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", + "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.43", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.43.tgz", + "integrity": "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.47", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.47.tgz", + "integrity": "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.13", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", + "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", + "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", + "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.20", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", + "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -60,6 +1334,12 @@ "node": ">=18" } }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -373,6 +1653,41 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.6.tgz", + "integrity": "sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.1.3", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -856,6 +2171,21 @@ "node": ">= 0.8" } }, + "node_modules/path-expression-matcher": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", + "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -1113,6 +2443,18 @@ "node": ">= 0.8" } }, + "node_modules/strnum": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.1.tgz", + "integrity": "sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/tlds": { "version": "1.259.0", "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.259.0.tgz", @@ -1131,6 +2473,12 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", diff --git a/_reference/localEmailViewer/package.json b/_reference/localEmailViewer/package.json index a6b522e0e..377708878 100644 --- a/_reference/localEmailViewer/package.json +++ b/_reference/localEmailViewer/package.json @@ -10,8 +10,9 @@ "keywords": [], "author": "", "license": "ISC", - "description": "LocalStack SES email viewer for inspecting local outbound mail", + "description": "LocalStack inspector for SES emails and CloudWatch logs", "dependencies": { + "@aws-sdk/client-cloudwatch-logs": "^3.1012.0", "express": "^5.1.0", "mailparser": "^3.7.4", "node-fetch": "^3.3.2" From debc67cc49a96adca2071c5745a156a4097caeb2 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 19 Mar 2026 16:46:25 -0400 Subject: [PATCH 12/56] feature/IO-3587-Commision-Cut - Improved localstack client --- _reference/localEmailViewer/index.js | 273 +++++++++++++++++++++------ 1 file changed, 216 insertions(+), 57 deletions(-) diff --git a/_reference/localEmailViewer/index.js b/_reference/localEmailViewer/index.js index 4d935135a..2022198c7 100644 --- a/_reference/localEmailViewer/index.js +++ b/_reference/localEmailViewer/index.js @@ -587,17 +587,18 @@ function renderHtml() {
-
-

LocalStack Toolbox

-

Inspector

-

Inspect generated SES mail and CloudWatch logs from the same local stack console.

-
-
-
- - +
+
+

LocalStack Toolbox

+

Inspector

+
+
+
+ + +
+
-

Keep outbound email inspection and CloudWatch tailing in one local viewer without leaving the browser.

@@ -632,11 +633,14 @@ function renderHtml() {
FetchIdleEndpoint: ${escapeHtml(SES_ENDPOINT)}
-
+
+
+ +
@@ -695,11 +699,14 @@ function renderHtml() {
FetchIdleEndpoint: ${escapeHtml(CLOUDWATCH_ENDPOINT)} (${escapeHtml(CLOUDWATCH_REGION)})
-
+
+
+ +
@@ -715,57 +722,65 @@ function renderStyles() { :root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--log-shell:linear-gradient(180deg,rgba(239,246,255,.98),rgba(248,251,255,.99));--log-body:#f8fbff;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--log-line:rgba(48,113,169,.22);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--info:#3071a9;--info-soft:rgba(48,113,169,.1);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);--log-shadow:0 16px 32px rgba(48,113,169,.12);} *{box-sizing:border-box} html,body{margin:0;min-height:100%} - body{background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.12),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:15px/1.45 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif} + body{color-scheme:light;background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.12),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:15px/1.45 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif;transition:background-color .18s ease,color .18s ease} button,input,select,textarea{font:inherit} button{cursor:pointer} - .page{display:grid;gap:14px;max-width:1360px;min-height:100vh;margin:0 auto;padding:18px} - .hero{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(320px,.95fr);gap:14px;margin-bottom:0} - .hero-copy,.hero-controls,.toolControls,.stat{background:var(--panel);backdrop-filter:blur(14px);border:1px solid var(--line);box-shadow:var(--shadow)} + .page{display:grid;gap:10px;max-width:1360px;align-content:start;margin:0 auto;padding:14px} + .hero{display:block;margin-bottom:0} + .heroShell,.toolControls,.stat{background:var(--panel);backdrop-filter:blur(14px);border:1px solid var(--line);box-shadow:var(--shadow)} .card{background:var(--card-shell);border:1px solid var(--card-line);box-shadow:var(--card-shadow)} - .hero-copy,.hero-controls,.toolControls{border-radius:18px;padding:18px} - .eyebrow{margin:0 0 6px;color:var(--accent);font-size:.76rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase} - h1{margin:0;font-size:clamp(1.9rem,4vw,3.1rem);line-height:.98;letter-spacing:-.05em} - .lede{margin:10px 0 0;max-width:54ch;color:var(--muted);font-size:.96rem} - .hero-controls{display:grid;gap:10px} - .helper{margin:0;color:var(--muted);font-size:.95rem} - .workspaceTabs{display:flex;flex-wrap:wrap;gap:8px;padding:6px;border-radius:999px;background:rgba(31,41,51,.05)} + .heroShell,.toolControls{border-radius:18px} + .heroShell{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px} + .toolControls{padding:12px} + .heroIdentity{display:grid;gap:3px;min-width:0} + .eyebrow{margin:0 0 4px;color:var(--accent);font-size:.72rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase} + h1{margin:0;font-size:clamp(1.8rem,3.6vw,2.85rem);line-height:.96;letter-spacing:-.05em} + .lede{margin:8px 0 0;max-width:54ch;color:var(--muted);font-size:.92rem} + .heroTopRow{display:flex;flex:1 1 360px;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end} + .helper{margin:0;color:var(--muted);font-size:.89rem} + .workspaceTabs{display:flex;flex-wrap:wrap;gap:6px;padding:4px;border-radius:999px;background:rgba(31,41,51,.05)} .workspaceTab,.primary,.ghost,.mini,.tab{border-radius:999px;border:1px solid transparent;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} - .workspaceTab{min-height:34px;padding:0 14px;background:transparent;color:var(--muted);font-weight:700} + .workspaceTab{min-height:32px;padding:0 12px;background:transparent;color:var(--muted);font-weight:700} .workspaceTab.active{background:#fff;border-color:rgba(31,41,51,.08);color:var(--ink)} - .workspacePanel{display:grid;gap:12px} + .themeToggle{white-space:nowrap} + .workspacePanel{display:grid;gap:6px} .workspacePanel[hidden]{display:none} - .toolControls{display:grid;gap:10px} - .contentPane{height:clamp(360px,50vh,720px);overflow:auto;padding-right:4px} - .contentStack{display:grid;gap:12px;min-width:min-content} - .row{display:flex;flex-wrap:wrap;gap:8px;align-items:center} - .primary,.ghost{min-height:38px;padding:0 14px;font-weight:700} - .mini,.tab{min-height:30px;padding:0 10px;font-weight:600} + .toolControls{display:grid;gap:8px} + .contentPane{height:clamp(360px,50vh,720px);overflow:auto;scroll-behavior:smooth;padding-right:4px} + .contentStack{display:grid;gap:8px;min-width:100%;padding-bottom:18px} + .paneTopWrap{display:flex;justify-content:flex-end;position:sticky;bottom:14px;pointer-events:none;padding-right:10px} + .paneTopButton{display:inline-flex;align-items:center;justify-content:center;width:42px;height:42px;border-radius:999px;border:1px solid rgba(255,255,255,.32);background:rgba(31,41,51,.42);color:#fff;font-size:1.1rem;line-height:1;backdrop-filter:blur(12px);box-shadow:0 10px 24px rgba(31,41,51,.18);opacity:0;transform:translateY(8px);visibility:hidden;pointer-events:none;transition:opacity .16s ease,transform .16s ease,background-color .12s ease;z-index:6} + .paneTopButton.visible{opacity:.78;transform:translateY(0);visibility:visible;pointer-events:auto} + .paneTopButton.visible:hover{opacity:1;background:rgba(31,41,51,.62);transform:translateY(-1px)} + .row{display:flex;flex-wrap:wrap;gap:6px;align-items:center} + .primary,.ghost{min-height:34px;padding:0 12px;font-weight:700} + .mini,.tab{min-height:28px;padding:0 10px;font-weight:600} .primary{background:var(--accent);color:#fff7f2} .ghost,.mini{background:rgba(255,255,255,.76);border-color:var(--line);color:var(--ink)} .tab{background:transparent;color:var(--muted)} .tab.active{background:#fff;border-color:rgba(207,109,60,.18);color:var(--ink)} .primary:hover,.ghost:hover,.mini:hover,.tab:hover{transform:translateY(-1px)} - .chip{display:inline-flex;align-items:center;gap:8px;min-height:38px;padding:0 12px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);font-weight:600;font-size:.92rem} + .chip{display:inline-flex;align-items:center;gap:7px;min-height:34px;padding:0 10px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);font-weight:600;font-size:.88rem} .chip input{margin:0;accent-color:var(--accent)} .chip select{border:none;background:transparent;outline:none;color:var(--ink)} - .search{flex:1 1 260px;min-height:40px;padding:0 14px;border-radius:14px;border:1px solid var(--line);background:rgba(255,255,255,.82);color:var(--ink);outline:none} - .status{display:inline-flex;align-items:center;min-height:34px;padding:0 12px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);color:var(--muted);font-size:.9rem;font-weight:600} + .search{flex:1 1 260px;min-height:36px;padding:0 12px;border-radius:12px;border:1px solid var(--line);background:rgba(255,255,255,.82);color:var(--ink);outline:none} + .status{display:inline-flex;align-items:center;min-height:32px;padding:0 11px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);color:var(--muted);font-size:.86rem;font-weight:600} .status.ok{color:var(--ok);border-color:rgba(31,143,101,.22);background:rgba(31,143,101,.08)} .status.warn{color:var(--warn);border-color:rgba(157,95,0,.22);background:rgba(157,95,0,.08)} .status.bad{color:var(--bad);border-color:rgba(179,58,58,.22);background:rgba(179,58,58,.08)} - .stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:12px} - .stat{border-radius:16px;padding:14px} - .stat span{display:block;margin-bottom:8px;color:var(--muted);font-size:.74rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} + .stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:0} + .stat{border-radius:16px;padding:10px 12px} + .stat span{display:block;margin-bottom:4px;color:var(--muted);font-size:.72rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} .stat strong{display:block;font-size:clamp(1.6rem,3vw,2rem);line-height:1;letter-spacing:-.05em} .stat strong.small{font-size:1.1rem;line-height:1.3;letter-spacing:-.02em} - .stat small{display:block;margin-top:8px;color:var(--muted);font-size:.85rem} + .stat small{display:block;margin-top:4px;color:var(--muted);font-size:.82rem} .banner,.empty{margin:0;padding:12px 14px;border-radius:14px;border:1px solid var(--line);background:rgba(255,255,255,.82)} .banner{color:var(--bad);border-color:rgba(179,58,58,.24);background:rgba(179,58,58,.08)} .list{display:grid;gap:12px;align-content:start} - .logList{display:grid;gap:10px;align-content:start} + .logList{display:grid;gap:10px;align-content:start;width:100%} .card{overflow:hidden;border-radius:16px} .card.new{border-color:rgba(31,143,101,.3);box-shadow:var(--card-shadow),0 0 0 1px rgba(31,143,101,.12)} - .summary{list-style:none;display:grid;gap:8px;padding:14px 16px;cursor:pointer;background:linear-gradient(180deg,rgba(255,250,244,.88),rgba(255,246,238,.96))} + .summary{list-style:none;display:grid;gap:7px;padding:12px 14px;cursor:pointer;background:linear-gradient(180deg,rgba(255,250,244,.88),rgba(255,246,238,.96))} .summary::-webkit-details-marker{display:none} .top,.tags,.toolbar,.actions,.attachments{display:flex;flex-wrap:wrap;gap:8px;align-items:center} .top{justify-content:space-between} @@ -777,12 +792,12 @@ function renderStyles() { .tag{background:var(--accent-soft);color:#8d5632} .tag.new{background:rgba(31,143,101,.1);color:var(--ok)} .tag.bad{background:rgba(179,58,58,.1);color:var(--bad)} - .preview{margin:0;color:#324150;font-size:.94rem} - .body{display:grid;gap:12px;padding:12px 16px 16px;border-top:1px solid rgba(207,109,60,.14);background:var(--card-body)} + .preview{margin:0;color:#324150;font-size:.9rem} + .body{display:grid;gap:10px;padding:10px 14px 14px;border-top:1px solid rgba(207,109,60,.14);background:var(--card-body)} .toolbar{justify-content:space-between;align-items:center} - .tabs{display:inline-flex;gap:4px;padding:4px;border-radius:999px;background:rgba(207,109,60,.08)} + .tabs{display:inline-flex;gap:4px;padding:3px;border-radius:999px;background:rgba(207,109,60,.08)} .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px} - .metaCard{padding:10px 12px;border-radius:12px;background:rgba(255,255,255,.78);border:1px solid rgba(207,109,60,.12)} + .metaCard{padding:9px 11px;border-radius:12px;background:rgba(255,255,255,.78);border:1px solid rgba(207,109,60,.12)} .metaCard dt{margin:0 0 4px;color:var(--muted);font-size:.72rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} .metaCard dd{margin:0;word-break:break-word} .attachments{gap:6px} @@ -790,22 +805,78 @@ function renderStyles() { .attachmentLink{color:#8d5632;text-decoration:none;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} .attachmentLink:hover{transform:translateY(-1px);background:#fff;border-color:rgba(207,109,60,.28)} .panel{overflow:hidden;border-radius:12px;border:1px solid rgba(207,109,60,.14);background:#fff} - .logEvent{overflow:hidden;border-radius:16px;border:1px solid var(--log-line);background:var(--log-shell);box-shadow:var(--log-shadow)} - .logSummary{list-style:none;display:grid;gap:8px;padding:12px 14px;cursor:pointer} + .logEvent{width:100%;overflow:hidden;border-radius:16px;border:1px solid var(--log-line);background:var(--log-shell);box-shadow:var(--log-shadow)} + .logSummary{list-style:none;display:grid;gap:7px;padding:10px 12px;cursor:pointer} .logSummary::-webkit-details-marker{display:none} .logSummaryTop{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:center} .logMeta{display:flex;flex-wrap:wrap;gap:8px;align-items:center} .logTag{background:var(--info-soft);color:var(--info);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} - .logPreview{margin:0;color:#324150;font-size:.9rem} - .logBody{padding:0 14px 14px;border-top:1px solid rgba(48,113,169,.14);background:var(--log-body)} - .logActions{display:flex;justify-content:flex-end;padding:12px 0 0} - .logBody pre{background:linear-gradient(180deg,rgba(48,113,169,.04),transparent 140px),#fff} + .logPreview{margin:0;color:#324150;font:600 .88rem/1.45 "Cascadia Code","Consolas",monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + .logBody{padding:8px 12px 12px;border-top:1px solid rgba(48,113,169,.14);background:var(--log-body)} + .logCodeWrap{position:relative} + .logCopyButton{position:absolute;top:10px;right:10px;z-index:1;backdrop-filter:blur(12px);box-shadow:0 8px 18px rgba(31,41,51,.16)} + .logBody pre{border-radius:12px;border:1px solid rgba(48,113,169,.14);padding:42px 12px 12px;background:linear-gradient(180deg,rgba(48,113,169,.04),transparent 140px),#fff} iframe{width:100%;min-height:560px;border:none;background:#fff} pre{margin:0;padding:12px;white-space:pre-wrap;word-break:break-word;overflow:auto;font:12.5px/1.45 "Cascadia Code","Consolas",monospace;color:#102030;background:linear-gradient(180deg,rgba(207,109,60,.04),transparent 140px),#fff} .placeholder,.inlineError{padding:12px} .inlineError{color:var(--bad)} - @media (max-width:1080px){.hero{grid-template-columns:1fr}.stats{grid-template-columns:repeat(2,minmax(0,1fr))}} - @media (max-width:720px){.page{padding:12px}.stats{grid-template-columns:1fr}.workspaceTab,.primary,.ghost,.chip{width:100%;justify-content:center}.toolbar,.row{align-items:stretch}.logSummaryTop{align-items:flex-start}.contentPane{height:clamp(300px,48vh,560px)}iframe{min-height:420px}} + body[data-theme="dark"]{color-scheme:dark;background:radial-gradient(circle at top left,rgba(207,109,60,.12),transparent 28%),radial-gradient(circle at top right,rgba(48,113,169,.12),transparent 26%),linear-gradient(180deg,#10161d,#17202a)} + body[data-theme="dark"] .heroShell, + body[data-theme="dark"] .toolControls, + body[data-theme="dark"] .stat{background:rgba(15,21,30,.84);border-color:rgba(148,163,184,.16);box-shadow:0 14px 30px rgba(0,0,0,.32)} + body[data-theme="dark"] .card{background:linear-gradient(180deg,rgba(50,35,28,.96),rgba(31,25,22,.98));border-color:rgba(207,109,60,.24);box-shadow:0 18px 34px rgba(0,0,0,.34)} + body[data-theme="dark"] .logEvent{background:linear-gradient(180deg,rgba(18,31,45,.96),rgba(14,24,36,.98));border-color:rgba(73,144,204,.22);box-shadow:0 16px 32px rgba(0,0,0,.34)} + body[data-theme="dark"] .workspaceTabs{background:rgba(148,163,184,.12)} + body[data-theme="dark"] .workspaceTab, + body[data-theme="dark"] .tab{color:#aab8c8} + body[data-theme="dark"] .workspaceTab.active, + body[data-theme="dark"] .tab.active, + body[data-theme="dark"] .ghost, + body[data-theme="dark"] .mini, + body[data-theme="dark"] .chip, + body[data-theme="dark"] .status, + body[data-theme="dark"] .search{background:rgba(18,25,35,.88);border-color:rgba(148,163,184,.18);color:#edf2f7} + body[data-theme="dark"] .chip select, + body[data-theme="dark"] .search::placeholder{color:#9fb0c2} + body[data-theme="dark"] .ghost, + body[data-theme="dark"] .mini, + body[data-theme="dark"] .workspaceTab.active, + body[data-theme="dark"] .tab.active{border-color:rgba(148,163,184,.18)} + body[data-theme="dark"] .summary{background:linear-gradient(180deg,rgba(58,40,31,.88),rgba(45,33,28,.96))} + body[data-theme="dark"] .body{background:#211a17;border-top-color:rgba(207,109,60,.18)} + body[data-theme="dark"] .logSummary{background:linear-gradient(180deg,rgba(21,34,47,.94),rgba(16,27,39,.98))} + body[data-theme="dark"] .logBody{background:#13212d;border-top-color:rgba(73,144,204,.18)} + body[data-theme="dark"] .metaCard{background:rgba(17,25,35,.64);border-color:rgba(148,163,184,.14)} + body[data-theme="dark"] .attachment{background:rgba(50,35,28,.9);border-color:rgba(207,109,60,.18)} + body[data-theme="dark"] .attachmentLink{color:#f6c4a9} + body[data-theme="dark"] .attachmentLink:hover{background:rgba(75,52,39,.96);border-color:rgba(246,196,169,.26)} + body[data-theme="dark"] .panel, + body[data-theme="dark"] pre, + body[data-theme="dark"] .logBody pre{background:linear-gradient(180deg,rgba(73,144,204,.06),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} + body[data-theme="dark"] .panel{border-color:rgba(148,163,184,.14)} + body[data-theme="dark"] .banner, + body[data-theme="dark"] .empty{background:rgba(15,21,30,.82);border-color:rgba(148,163,184,.16)} + body[data-theme="dark"] .time{background:rgba(148,163,184,.12);color:#e8edf3} + body[data-theme="dark"] .tag{background:rgba(207,109,60,.14);color:#f0c2aa} + body[data-theme="dark"] .logTag{background:rgba(73,144,204,.16);color:#93cfff} + body[data-theme="dark"] .preview, + body[data-theme="dark"] .logPreview, + body[data-theme="dark"] .metaCard dd, + body[data-theme="dark"] .head h2, + body[data-theme="dark"] .stat strong, + body[data-theme="dark"] h1{color:#edf2f7} + body[data-theme="dark"] .meta, + body[data-theme="dark"] .helper, + body[data-theme="dark"] .lede, + body[data-theme="dark"] .stat small, + body[data-theme="dark"] .stat span, + body[data-theme="dark"] .chip, + body[data-theme="dark"] .workspaceTab, + body[data-theme="dark"] .tab{color:#aab8c8} + body[data-theme="dark"] .paneTopButton{border-color:rgba(255,255,255,.18);background:rgba(8,12,18,.58);color:#edf2f7} + body[data-theme="dark"] .paneTopButton.visible:hover{background:rgba(8,12,18,.8)} + @media (max-width:1080px){.stats{grid-template-columns:repeat(2,minmax(0,1fr))}} + @media (max-width:720px){.page{padding:12px}.heroShell,.heroTopRow,.toolbar,.row{align-items:stretch}.stats{grid-template-columns:1fr}.heroTopRow{justify-content:stretch;flex-basis:100%}.workspaceTab,.primary,.ghost,.chip,.themeToggle{width:100%;justify-content:center}.logSummaryTop{align-items:flex-start}.logCopyButton{top:8px;right:8px}.contentPane{height:clamp(300px,48vh,560px)}iframe{min-height:420px}} `; } @@ -821,8 +892,10 @@ function escapeHtml(value) { function clientApp(config) { const appState = { panel: "emails", - logsReady: false + logsReady: false, + theme: getInitialTheme() }; + const THEME_STORAGE_KEY = "localstack-inspector-theme"; const state = { messages: [], @@ -871,6 +944,7 @@ function clientApp(config) { const el = { workspaceTabs: document.getElementById("workspaceTabs"), + themeToggle: document.getElementById("themeToggle"), emailsPanel: document.getElementById("emailsPanel"), logsPanel: document.getElementById("logsPanel"), refreshButton: document.getElementById("refreshButton"), @@ -880,6 +954,7 @@ function clientApp(config) { clearSearchButton: document.getElementById("clearSearchButton"), expandAllButton: document.getElementById("expandAllButton"), collapseAllButton: document.getElementById("collapseAllButton"), + scrollToTopButton: document.getElementById("scrollToTopButton"), statusChip: document.getElementById("statusChip"), totalStat: document.getElementById("totalStat"), visibleStat: document.getElementById("visibleStat"), @@ -891,6 +966,7 @@ function clientApp(config) { banner: document.getElementById("banner"), empty: document.getElementById("empty"), list: document.getElementById("list"), + emailsContentPane: document.getElementById("emailsContentPane"), logsRefreshButton: document.getElementById("logsRefreshButton"), logsAutoToggle: document.getElementById("logsAutoToggle"), logsIntervalSelect: document.getElementById("logsIntervalSelect"), @@ -900,6 +976,7 @@ function clientApp(config) { logsLimitSelect: document.getElementById("logsLimitSelect"), logsSearchInput: document.getElementById("logsSearchInput"), logsClearSearchButton: document.getElementById("logsClearSearchButton"), + logsScrollToTopButton: document.getElementById("logsScrollToTopButton"), logsStatusChip: document.getElementById("logsStatusChip"), logsTotalStat: document.getElementById("logsTotalStat"), logsVisibleStat: document.getElementById("logsVisibleStat"), @@ -910,13 +987,15 @@ function clientApp(config) { logsFetchDetail: document.getElementById("logsFetchDetail"), logsBanner: document.getElementById("logsBanner"), logsEmpty: document.getElementById("logsEmpty"), - logsList: document.getElementById("logsList") + logsList: document.getElementById("logsList"), + logsContentPane: document.getElementById("logsContentPane") }; el.intervalSelect.value = String(config.defaultRefreshMs); el.logsIntervalSelect.value = String(config.defaultRefreshMs); el.logsWindowSelect.value = String(config.defaultLogWindowMs); el.logsLimitSelect.value = String(config.defaultLogLimit); + applyTheme(appState.theme); wire(); renderWorkspace(); renderAll(); @@ -938,6 +1017,10 @@ function clientApp(config) { await setPanel(button.dataset.panel); }); + el.themeToggle.addEventListener("click", () => { + applyTheme(appState.theme === "dark" ? "light" : "dark"); + }); + el.refreshButton.addEventListener("click", () => refreshMessages("manual")); el.autoToggle.addEventListener("change", () => { @@ -973,6 +1056,14 @@ function clientApp(config) { renderList(); }); + el.scrollToTopButton.addEventListener("click", () => { + scrollPaneToTop(el.emailsContentPane); + }); + + el.emailsContentPane.addEventListener("scroll", () => { + updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); + }); + el.list.addEventListener("click", async (event) => { const button = event.target.closest("button[data-action]"); @@ -1044,6 +1135,14 @@ function clientApp(config) { applyLogsFilter(); }); + el.logsScrollToTopButton.addEventListener("click", () => { + scrollPaneToTop(el.logsContentPane); + }); + + el.logsContentPane.addEventListener("scroll", () => { + updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); + }); + el.logsGroupSelect.addEventListener("change", async () => { logsState.group = el.logsGroupSelect.value; logsState.stream = ""; @@ -1113,6 +1212,9 @@ function clientApp(config) { refreshMessages("keyboard"); } }); + + updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); + updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); } async function setPanel(panel) { @@ -1655,6 +1757,7 @@ function clientApp(config) { el.empty.textContent = state.messages.length ? "No messages match the current search." : "No emails yet. Send one through LocalStack SES and refresh."; + updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); return; } @@ -1662,6 +1765,7 @@ function clientApp(config) { el.list.innerHTML = state.filtered.map(renderCard).join(""); bindCardToggles(); el.list.querySelectorAll(".card[open]").forEach((details) => hydrate(details, getMessage(details.dataset.id))); + updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); } function renderLogsList() { @@ -1672,6 +1776,7 @@ function clientApp(config) { el.logsList.innerHTML = ""; el.logsEmpty.hidden = false; el.logsEmpty.textContent = "No CloudWatch log groups found in LocalStack yet."; + updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); return; } @@ -1681,12 +1786,14 @@ function clientApp(config) { el.logsEmpty.textContent = logsState.events.length ? "No log events match the current search." : "No log events found for the selected group, stream, and time window."; + updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); return; } el.logsEmpty.hidden = true; el.logsList.innerHTML = logsState.filtered.map((event, index) => renderLogEvent(event, index)).join(""); bindLogToggles(); + updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); } function renderLogEvent(event, index) { @@ -1705,10 +1812,10 @@ function clientApp(config) {

${escapeHtml(event.preview || "No preview available.")}

-
- +
+ +
${escapeHtml(formatLogMessage(event.message))}
-
${escapeHtml(formatLogMessage(event.message))}
`; @@ -1958,6 +2065,58 @@ function clientApp(config) { el.logsStatusChip.textContent = message; } + function getInitialTheme() { + try { + const storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY); + + if (storedTheme === "dark" || storedTheme === "light") { + return storedTheme; + } + } catch {} + + return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light"; + } + + function applyTheme(theme) { + const nextTheme = theme === "dark" ? "dark" : "light"; + appState.theme = nextTheme; + document.body.dataset.theme = nextTheme; + + try { + window.localStorage.setItem(THEME_STORAGE_KEY, nextTheme); + } catch {} + + renderThemeToggle(); + } + + function renderThemeToggle() { + if (!el.themeToggle) { + return; + } + + const isDark = appState.theme === "dark"; + el.themeToggle.textContent = isDark ? "Theme: Dark" : "Theme: Light"; + el.themeToggle.setAttribute("aria-pressed", isDark ? "true" : "false"); + el.themeToggle.setAttribute("aria-label", isDark ? "Switch to light theme" : "Switch to dark theme"); + el.themeToggle.title = isDark ? "Switch to light theme" : "Switch to dark theme"; + } + + function scrollPaneToTop(element) { + if (!element) { + return; + } + + element.scrollTo({ top: 0, behavior: "smooth" }); + } + + function updatePaneTopButtonVisibility(pane, button) { + if (!pane || !button) { + return; + } + + button.classList.toggle("visible", pane.scrollTop > 140); + } + function formatDateTime(value) { if (!value) { return "Unknown time"; From f0f5c09fd70d1b409fa22922f23433768c347c7c Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 19 Mar 2026 17:03:17 -0400 Subject: [PATCH 13/56] feature/IO-3587-Commision-Cut - Improved localstack client --- _reference/localEmailViewer/index.js | 292 ++++++++++++++++++++++----- 1 file changed, 246 insertions(+), 46 deletions(-) diff --git a/_reference/localEmailViewer/index.js b/_reference/localEmailViewer/index.js index 2022198c7..88e28f8b4 100644 --- a/_reference/localEmailViewer/index.js +++ b/_reference/localEmailViewer/index.js @@ -689,6 +689,8 @@ function renderHtml() {
+ +
@@ -721,11 +723,11 @@ function renderStyles() { return ` :root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--log-shell:linear-gradient(180deg,rgba(239,246,255,.98),rgba(248,251,255,.99));--log-body:#f8fbff;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--log-line:rgba(48,113,169,.22);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--info:#3071a9;--info-soft:rgba(48,113,169,.1);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);--log-shadow:0 16px 32px rgba(48,113,169,.12);} *{box-sizing:border-box} - html,body{margin:0;min-height:100%} + html,body{margin:0;height:100%;overflow:hidden} body{color-scheme:light;background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.12),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:15px/1.45 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif;transition:background-color .18s ease,color .18s ease} button,input,select,textarea{font:inherit} button{cursor:pointer} - .page{display:grid;gap:10px;max-width:1360px;align-content:start;margin:0 auto;padding:14px} + .page{display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;max-width:1360px;height:100vh;height:100dvh;margin:0 auto;padding:14px;overflow:hidden} .hero{display:block;margin-bottom:0} .heroShell,.toolControls,.stat{background:var(--panel);backdrop-filter:blur(14px);border:1px solid var(--line);box-shadow:var(--shadow)} .card{background:var(--card-shell);border:1px solid var(--card-line);box-shadow:var(--card-shadow)} @@ -743,10 +745,10 @@ function renderStyles() { .workspaceTab{min-height:32px;padding:0 12px;background:transparent;color:var(--muted);font-weight:700} .workspaceTab.active{background:#fff;border-color:rgba(31,41,51,.08);color:var(--ink)} .themeToggle{white-space:nowrap} - .workspacePanel{display:grid;gap:6px} + .workspacePanel{display:grid;grid-template-rows:auto auto minmax(0,1fr);gap:6px;min-height:0} .workspacePanel[hidden]{display:none} .toolControls{display:grid;gap:8px} - .contentPane{height:clamp(360px,50vh,720px);overflow:auto;scroll-behavior:smooth;padding-right:4px} + .contentPane{height:100%;min-height:0;overflow:auto;scroll-behavior:smooth;padding-right:4px} .contentStack{display:grid;gap:8px;min-width:100%;padding-bottom:18px} .paneTopWrap{display:flex;justify-content:flex-end;position:sticky;bottom:14px;pointer-events:none;padding-right:10px} .paneTopButton{display:inline-flex;align-items:center;justify-content:center;width:42px;height:42px;border-radius:999px;border:1px solid rgba(255,255,255,.32);background:rgba(31,41,51,.42);color:#fff;font-size:1.1rem;line-height:1;backdrop-filter:blur(12px);box-shadow:0 10px 24px rgba(31,41,51,.18);opacity:0;transform:translateY(8px);visibility:hidden;pointer-events:none;transition:opacity .16s ease,transform .16s ease,background-color .12s ease;z-index:6} @@ -810,12 +812,17 @@ function renderStyles() { .logSummary::-webkit-details-marker{display:none} .logSummaryTop{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:center} .logMeta{display:flex;flex-wrap:wrap;gap:8px;align-items:center} + .logSummaryActions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end} .logTag{background:var(--info-soft);color:var(--info);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .logPreview{margin:0;color:#324150;font:600 .88rem/1.45 "Cascadia Code","Consolas",monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .logBody{padding:8px 12px 12px;border-top:1px solid rgba(48,113,169,.14);background:var(--log-body)} - .logCodeWrap{position:relative} - .logCopyButton{position:absolute;top:10px;right:10px;z-index:1;backdrop-filter:blur(12px);box-shadow:0 8px 18px rgba(31,41,51,.16)} - .logBody pre{border-radius:12px;border:1px solid rgba(48,113,169,.14);padding:42px 12px 12px;background:linear-gradient(180deg,rgba(48,113,169,.04),transparent 140px),#fff} + .logCopyButton{box-shadow:none} + .logBody pre{border-radius:12px;border:1px solid rgba(48,113,169,.14);padding:12px;background:linear-gradient(180deg,rgba(48,113,169,.04),transparent 140px),#fff} + .jsonSyntax .jsonKey{color:#b55f2d} + .jsonSyntax .jsonString{color:#1f8f65} + .jsonSyntax .jsonNumber{color:#2f6ea9} + .jsonSyntax .jsonBoolean{color:#9d5f00} + .jsonSyntax .jsonNull{color:#b33a3a} iframe{width:100%;min-height:560px;border:none;background:#fff} pre{margin:0;padding:12px;white-space:pre-wrap;word-break:break-word;overflow:auto;font:12.5px/1.45 "Cascadia Code","Consolas",monospace;color:#102030;background:linear-gradient(180deg,rgba(207,109,60,.04),transparent 140px),#fff} .placeholder,.inlineError{padding:12px} @@ -865,6 +872,11 @@ function renderStyles() { body[data-theme="dark"] .head h2, body[data-theme="dark"] .stat strong, body[data-theme="dark"] h1{color:#edf2f7} + body[data-theme="dark"] .jsonSyntax .jsonKey{color:#f0b08a} + body[data-theme="dark"] .jsonSyntax .jsonString{color:#80d5b0} + body[data-theme="dark"] .jsonSyntax .jsonNumber{color:#94c9ff} + body[data-theme="dark"] .jsonSyntax .jsonBoolean{color:#f0c274} + body[data-theme="dark"] .jsonSyntax .jsonNull{color:#ff9c9c} body[data-theme="dark"] .meta, body[data-theme="dark"] .helper, body[data-theme="dark"] .lede, @@ -876,7 +888,7 @@ function renderStyles() { body[data-theme="dark"] .paneTopButton{border-color:rgba(255,255,255,.18);background:rgba(8,12,18,.58);color:#edf2f7} body[data-theme="dark"] .paneTopButton.visible:hover{background:rgba(8,12,18,.8)} @media (max-width:1080px){.stats{grid-template-columns:repeat(2,minmax(0,1fr))}} - @media (max-width:720px){.page{padding:12px}.heroShell,.heroTopRow,.toolbar,.row{align-items:stretch}.stats{grid-template-columns:1fr}.heroTopRow{justify-content:stretch;flex-basis:100%}.workspaceTab,.primary,.ghost,.chip,.themeToggle{width:100%;justify-content:center}.logSummaryTop{align-items:flex-start}.logCopyButton{top:8px;right:8px}.contentPane{height:clamp(300px,48vh,560px)}iframe{min-height:420px}} + @media (max-width:720px){.page{padding:12px}.heroShell,.heroTopRow,.toolbar,.row{align-items:stretch}.stats{grid-template-columns:1fr}.heroTopRow{justify-content:stretch;flex-basis:100%}.workspaceTab,.primary,.ghost,.chip,.themeToggle{width:100%;justify-content:center}.logSummaryTop,.logSummaryActions{align-items:flex-start}.contentPane{min-height:300px}iframe{min-height:420px}} `; } @@ -890,19 +902,28 @@ function escapeHtml(value) { } function clientApp(config) { + const THEME_STORAGE_KEY = "localstack-inspector-theme"; + const PANEL_STORAGE_KEY = "localstack-inspector-panel"; + const EMAIL_PREFERENCES_STORAGE_KEY = "localstack-inspector-email-preferences"; + const LOG_PREFERENCES_STORAGE_KEY = "localstack-inspector-log-preferences"; + const REFRESH_INTERVALS = [5000, 10000, 15000, 30000, 60000]; + const LOG_WINDOWS = [300000, 900000, 3600000, 21600000, 86400000]; + const LOG_LIMITS = [100, 200, 300, 500]; + const storedEmailPreferences = getStoredPreferences(EMAIL_PREFERENCES_STORAGE_KEY); + const storedLogPreferences = getStoredPreferences(LOG_PREFERENCES_STORAGE_KEY); + const appState = { - panel: "emails", + panel: getInitialPanel(), logsReady: false, theme: getInitialTheme() }; - const THEME_STORAGE_KEY = "localstack-inspector-theme"; const state = { messages: [], filtered: [], - search: "", - auto: true, - interval: config.defaultRefreshMs, + search: getStoredText(storedEmailPreferences?.search), + auto: getStoredBoolean(storedEmailPreferences?.auto, true), + interval: getStoredNumber(storedEmailPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs), loading: false, error: "", updatedAt: 0, @@ -923,13 +944,13 @@ function clientApp(config) { streams: [], events: [], filtered: [], - group: config.defaultLogGroup || "", - stream: "", - search: "", - auto: true, - interval: config.defaultRefreshMs, - windowMs: config.defaultLogWindowMs, - limit: config.defaultLogLimit, + group: getStoredText(storedLogPreferences?.group, config.defaultLogGroup || ""), + stream: getStoredText(storedLogPreferences?.stream), + search: getStoredText(storedLogPreferences?.search), + auto: getStoredBoolean(storedLogPreferences?.auto, true), + interval: getStoredNumber(storedLogPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs), + windowMs: getStoredNumber(storedLogPreferences?.windowMs, LOG_WINDOWS, config.defaultLogWindowMs), + limit: getStoredNumber(storedLogPreferences?.limit, LOG_LIMITS, config.defaultLogLimit), loading: false, error: "", updatedAt: 0, @@ -938,7 +959,6 @@ function clientApp(config) { newest: 0, searchedLogStreams: 0, openIds: new Set(), - hasInteracted: false, listSignature: "" }; @@ -976,6 +996,8 @@ function clientApp(config) { logsLimitSelect: document.getElementById("logsLimitSelect"), logsSearchInput: document.getElementById("logsSearchInput"), logsClearSearchButton: document.getElementById("logsClearSearchButton"), + logsExpandAllButton: document.getElementById("logsExpandAllButton"), + logsCollapseAllButton: document.getElementById("logsCollapseAllButton"), logsScrollToTopButton: document.getElementById("logsScrollToTopButton"), logsStatusChip: document.getElementById("logsStatusChip"), logsTotalStat: document.getElementById("logsTotalStat"), @@ -991,16 +1013,27 @@ function clientApp(config) { logsContentPane: document.getElementById("logsContentPane") }; - el.intervalSelect.value = String(config.defaultRefreshMs); - el.logsIntervalSelect.value = String(config.defaultRefreshMs); - el.logsWindowSelect.value = String(config.defaultLogWindowMs); - el.logsLimitSelect.value = String(config.defaultLogLimit); + el.autoToggle.checked = state.auto; + el.intervalSelect.value = String(state.interval); + el.searchInput.value = state.search; + el.logsAutoToggle.checked = logsState.auto; + el.logsIntervalSelect.value = String(logsState.interval); + el.logsWindowSelect.value = String(logsState.windowMs); + el.logsLimitSelect.value = String(logsState.limit); + el.logsSearchInput.value = logsState.search; applyTheme(appState.theme); + persistPanel(); + persistEmailPreferences(); + persistLogPreferences(); wire(); renderWorkspace(); renderAll(); renderLogsAll(); - refreshMessages("initial"); + if (appState.panel === "logs") { + ensureLogsReady(); + } else { + refreshMessages("initial"); + } window.setInterval(() => { renderLiveClock(); renderLogsLiveClock(); @@ -1025,12 +1058,14 @@ function clientApp(config) { el.autoToggle.addEventListener("change", () => { state.auto = el.autoToggle.checked; + persistEmailPreferences(); scheduleRefresh(); renderStatus(); }); el.intervalSelect.addEventListener("change", () => { state.interval = Number(el.intervalSelect.value) || config.defaultRefreshMs; + persistEmailPreferences(); scheduleRefresh(); renderStatus(); }); @@ -1104,23 +1139,27 @@ function clientApp(config) { el.logsAutoToggle.addEventListener("change", () => { logsState.auto = el.logsAutoToggle.checked; + persistLogPreferences(); scheduleLogsRefresh(); renderLogsStatus(); }); el.logsIntervalSelect.addEventListener("change", () => { logsState.interval = Number(el.logsIntervalSelect.value) || config.defaultRefreshMs; + persistLogPreferences(); scheduleLogsRefresh(); renderLogsStatus(); }); el.logsWindowSelect.addEventListener("change", () => { logsState.windowMs = Number(el.logsWindowSelect.value) || config.defaultLogWindowMs; + persistLogPreferences(); refreshLogs("window"); }); el.logsLimitSelect.addEventListener("change", () => { logsState.limit = Number(el.logsLimitSelect.value) || config.defaultLogLimit; + persistLogPreferences(); refreshLogs("limit"); }); @@ -1135,6 +1174,16 @@ function clientApp(config) { applyLogsFilter(); }); + el.logsExpandAllButton.addEventListener("click", () => { + logsState.filtered.forEach((event) => logsState.openIds.add(event.id)); + renderLogsList(); + }); + + el.logsCollapseAllButton.addEventListener("click", () => { + logsState.openIds.clear(); + renderLogsList(); + }); + el.logsScrollToTopButton.addEventListener("click", () => { scrollPaneToTop(el.logsContentPane); }); @@ -1146,12 +1195,14 @@ function clientApp(config) { el.logsGroupSelect.addEventListener("change", async () => { logsState.group = el.logsGroupSelect.value; logsState.stream = ""; + persistLogPreferences(); await refreshLogStreams(); await refreshLogs("group"); }); el.logsStreamSelect.addEventListener("change", () => { logsState.stream = el.logsStreamSelect.value; + persistLogPreferences(); refreshLogs("stream"); }); @@ -1162,6 +1213,9 @@ function clientApp(config) { return; } + event.preventDefault(); + event.stopPropagation(); + const logEvent = getLogEvent(button.dataset.id); if (!logEvent) { @@ -1228,10 +1282,13 @@ function clientApp(config) { } appState.panel = panel; + persistPanel(); renderWorkspace(); if (panel === "logs") { await ensureLogsReady(); + } else if (!state.updatedAt && !state.loading) { + await refreshMessages("panel"); } scheduleRefresh(); @@ -1341,6 +1398,7 @@ function clientApp(config) { } catch (error) { logsState.error = error.message || "Unknown log group refresh error"; } finally { + persistLogPreferences(); renderLogsAll(); } } @@ -1376,6 +1434,7 @@ function clientApp(config) { logsState.stream = ""; logsState.error = error.message || "Unknown log stream refresh error"; } finally { + persistLogPreferences(); renderLogsAll(); } } @@ -1451,6 +1510,7 @@ function clientApp(config) { logsState.filtered = !search ? [...logsState.events] : logsState.events.filter((event) => logHaystack(event).includes(search)); + persistLogPreferences(); renderLogsAll({ renderList: shouldRenderList }); } @@ -1482,6 +1542,7 @@ function clientApp(config) { state.filtered = !search ? [...state.messages] : state.messages.filter((message) => haystack(message).includes(search)); + persistEmailPreferences(); renderAll({ renderList: shouldRenderList }); } @@ -1791,31 +1852,29 @@ function clientApp(config) { } el.logsEmpty.hidden = true; - el.logsList.innerHTML = logsState.filtered.map((event, index) => renderLogEvent(event, index)).join(""); + el.logsList.innerHTML = logsState.filtered.map((event) => renderLogEvent(event)).join(""); bindLogToggles(); updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); } - function renderLogEvent(event, index) { - const open = logsState.openIds.has(event.id) || (!logsState.hasInteracted && index === 0) ? "open" : ""; - + function renderLogEvent(event) { return ` -
+
${escapeHtml(formatDateTime(event.timestamp))} ${escapeHtml(event.logStreamName || "Unknown stream")}
- ${escapeHtml(formatRelative(event.timestamp || Date.now()))} +
+ + ${escapeHtml(formatRelative(event.timestamp || Date.now()))} +
-

${escapeHtml(event.preview || "No preview available.")}

+

${renderLogPreviewContent(event)}

-
- -
${escapeHtml(formatLogMessage(event.message))}
-
+
${renderLogBodyContent(event.message)}
`; @@ -1830,8 +1889,6 @@ function clientApp(config) { return; } - logsState.hasInteracted = true; - if (details.open) { logsState.openIds.add(id); } else { @@ -2065,14 +2122,17 @@ function clientApp(config) { el.logsStatusChip.textContent = message; } - function getInitialTheme() { - try { - const storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY); + function getInitialPanel() { + const storedPanel = readStoredValue(PANEL_STORAGE_KEY); + return storedPanel === "logs" ? "logs" : "emails"; + } - if (storedTheme === "dark" || storedTheme === "light") { - return storedTheme; - } - } catch {} + function getInitialTheme() { + const storedTheme = readStoredValue(THEME_STORAGE_KEY); + + if (storedTheme === "dark" || storedTheme === "light") { + return storedTheme; + } return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light"; } @@ -2101,6 +2161,78 @@ function clientApp(config) { el.themeToggle.title = isDark ? "Switch to light theme" : "Switch to dark theme"; } + function persistPanel() { + writeStoredValue(PANEL_STORAGE_KEY, appState.panel); + } + + function persistEmailPreferences() { + writeStoredJson(EMAIL_PREFERENCES_STORAGE_KEY, { + search: state.search, + auto: state.auto, + interval: state.interval + }); + } + + function persistLogPreferences() { + writeStoredJson(LOG_PREFERENCES_STORAGE_KEY, { + group: logsState.group, + stream: logsState.stream, + search: logsState.search, + auto: logsState.auto, + interval: logsState.interval, + windowMs: logsState.windowMs, + limit: logsState.limit + }); + } + + function getStoredPreferences(key) { + try { + const rawValue = window.localStorage.getItem(key); + + if (!rawValue) { + return null; + } + + const parsedValue = JSON.parse(rawValue); + return parsedValue && typeof parsedValue === "object" && !Array.isArray(parsedValue) ? parsedValue : null; + } catch { + return null; + } + } + + function readStoredValue(key) { + try { + return window.localStorage.getItem(key); + } catch { + return null; + } + } + + function writeStoredValue(key, value) { + try { + window.localStorage.setItem(key, String(value)); + } catch {} + } + + function writeStoredJson(key, value) { + try { + window.localStorage.setItem(key, JSON.stringify(value)); + } catch {} + } + + function getStoredText(value, fallback = "") { + return typeof value === "string" ? value : fallback; + } + + function getStoredBoolean(value, fallback) { + return typeof value === "boolean" ? value : fallback; + } + + function getStoredNumber(value, allowedValues, fallback) { + const normalizedValue = Number(value); + return Number.isFinite(normalizedValue) && allowedValues.includes(normalizedValue) ? normalizedValue : fallback; + } + function scrollPaneToTop(element) { if (!element) { return; @@ -2181,6 +2313,74 @@ function clientApp(config) { } } + function renderLogPreviewContent(event) { + const parsedLog = tryParseLogJson(event?.message); + + if (!parsedLog.ok) { + return escapeHtml(event?.preview || "No preview available."); + } + + const compactJson = JSON.stringify(parsedLog.value); + const previewText = compactJson.length > 220 ? `${compactJson.slice(0, 217)}...` : compactJson; + return highlightJsonText(previewText); + } + + function renderLogBodyContent(message) { + const parsedLog = tryParseLogJson(message); + + if (!parsedLog.ok) { + return escapeHtml(formatLogMessage(message)); + } + + return highlightJsonText(JSON.stringify(parsedLog.value, null, 2)); + } + + function tryParseLogJson(message) { + const value = String(message || "").trim(); + + if (!value) { + return { ok: false, value: null }; + } + + try { + return { ok: true, value: JSON.parse(value) }; + } catch { + return { ok: false, value: null }; + } + } + + function highlightJsonText(value) { + const source = String(value ?? ""); + const tokenRegex = + /("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?)/g; + let html = ""; + let lastIndex = 0; + + for (const match of source.matchAll(tokenRegex)) { + const [token] = match; + const index = match.index ?? 0; + let className = "jsonNumber"; + + html += escapeHtml(source.slice(lastIndex, index)); + + if (token.endsWith(":")) { + className = "jsonKey"; + } else if (token === "true" || token === "false") { + className = "jsonBoolean"; + } else if (token === "null") { + className = "jsonNull"; + } else if (token.startsWith('"')) { + className = "jsonString"; + } + + html += `${escapeHtml(token)}`; + lastIndex = index + token.length; + } + + html += escapeHtml(source.slice(lastIndex)); + return html; + } + function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") From 31c6e7c0e5b4cc093e631642ddf6d9517c5fc46e Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 19 Mar 2026 17:14:32 -0400 Subject: [PATCH 14/56] feature/IO-3587-Commision-Cut - Improved localstack client --- _reference/localEmailViewer/index.js | 114 +++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 16 deletions(-) diff --git a/_reference/localEmailViewer/index.js b/_reference/localEmailViewer/index.js index 88e28f8b4..162f9504b 100644 --- a/_reference/localEmailViewer/index.js +++ b/_reference/localEmailViewer/index.js @@ -594,10 +594,10 @@ function renderHtml() {
- - + +
- +
@@ -605,7 +605,7 @@ function renderHtml() {
- + + + Waiting for first refresh... +
+
+ + + + +
+
+ +
+
Secrets00 visible
+
Loaded0Values loaded this session
+
LatestNo secretsNot refreshed yet
+
FetchIdleEndpoint: ${escapeHtml(SECRETS_ENDPOINT)} (${escapeHtml(SECRETS_REGION)})
+
+ +
+
+ + +
+
+ +
+
+
+
@@ -721,7 +911,7 @@ function renderHtml() { function renderStyles() { return ` - :root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--log-shell:linear-gradient(180deg,rgba(239,246,255,.98),rgba(248,251,255,.99));--log-body:#f8fbff;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--log-line:rgba(48,113,169,.22);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--info:#3071a9;--info-soft:rgba(48,113,169,.1);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);--log-shadow:0 16px 32px rgba(48,113,169,.12);} + :root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--log-shell:linear-gradient(180deg,rgba(239,246,255,.98),rgba(248,251,255,.99));--log-body:#f8fbff;--secret-shell:linear-gradient(180deg,rgba(239,251,246,.98),rgba(247,253,249,.99));--secret-body:#f6fcf8;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--log-line:rgba(48,113,169,.22);--secret-line:rgba(31,143,101,.2);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--info:#3071a9;--info-soft:rgba(48,113,169,.1);--secret:#1f8f65;--secret-soft:rgba(31,143,101,.1);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);--log-shadow:0 16px 32px rgba(48,113,169,.12);--secret-shadow:0 16px 32px rgba(31,143,101,.12);} *{box-sizing:border-box} html,body{margin:0;height:100%;overflow:hidden} body{color-scheme:light;background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.12),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:15px/1.45 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif;transition:background-color .18s ease,color .18s ease} @@ -808,16 +998,22 @@ function renderStyles() { .attachmentLink:hover{transform:translateY(-1px);background:#fff;border-color:rgba(207,109,60,.28)} .panel{overflow:hidden;border-radius:12px;border:1px solid rgba(207,109,60,.14);background:#fff} .logEvent{width:100%;overflow:hidden;border-radius:16px;border:1px solid var(--log-line);background:var(--log-shell);box-shadow:var(--log-shadow)} + .secretCard{background:var(--secret-shell);border:1px solid var(--secret-line);box-shadow:var(--secret-shadow)} .logSummary{list-style:none;display:grid;gap:7px;padding:10px 12px;cursor:pointer} .logSummary::-webkit-details-marker{display:none} + .secretSummary{background:linear-gradient(180deg,rgba(244,253,248,.9),rgba(236,249,242,.96))} .logSummaryTop{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:center} .logMeta{display:flex;flex-wrap:wrap;gap:8px;align-items:center} .logSummaryActions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end} .logTag{background:var(--info-soft);color:var(--info);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .secretTag{background:var(--secret-soft);color:var(--secret);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .logPreview{margin:0;color:#324150;font:600 .88rem/1.45 "Cascadia Code","Consolas",monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .logBody{padding:8px 12px 12px;border-top:1px solid rgba(48,113,169,.14);background:var(--log-body)} + .secretBody{border-top-color:rgba(31,143,101,.14);background:var(--secret-body)} .logCopyButton{box-shadow:none} .logBody pre{border-radius:12px;border:1px solid rgba(48,113,169,.14);padding:12px;background:linear-gradient(180deg,rgba(48,113,169,.04),transparent 140px),#fff} + .secretValuePanel{display:grid;gap:10px} + .secretValuePanel pre{border-radius:12px;border:1px solid rgba(31,143,101,.14);padding:12px;background:linear-gradient(180deg,rgba(31,143,101,.04),transparent 140px),#fff} .jsonSyntax .jsonKey{color:#b55f2d} .jsonSyntax .jsonString{color:#1f8f65} .jsonSyntax .jsonNumber{color:#2f6ea9} @@ -833,6 +1029,7 @@ function renderStyles() { body[data-theme="dark"] .stat{background:rgba(15,21,30,.84);border-color:rgba(148,163,184,.16);box-shadow:0 14px 30px rgba(0,0,0,.32)} body[data-theme="dark"] .card{background:linear-gradient(180deg,rgba(50,35,28,.96),rgba(31,25,22,.98));border-color:rgba(207,109,60,.24);box-shadow:0 18px 34px rgba(0,0,0,.34)} body[data-theme="dark"] .logEvent{background:linear-gradient(180deg,rgba(18,31,45,.96),rgba(14,24,36,.98));border-color:rgba(73,144,204,.22);box-shadow:0 16px 32px rgba(0,0,0,.34)} + body[data-theme="dark"] .secretCard{background:linear-gradient(180deg,rgba(19,39,31,.96),rgba(14,30,24,.98));border-color:rgba(64,170,126,.22);box-shadow:0 16px 32px rgba(0,0,0,.34)} body[data-theme="dark"] .workspaceTabs{background:rgba(148,163,184,.12)} body[data-theme="dark"] .workspaceTab, body[data-theme="dark"] .tab{color:#aab8c8} @@ -853,6 +1050,8 @@ function renderStyles() { body[data-theme="dark"] .body{background:#211a17;border-top-color:rgba(207,109,60,.18)} body[data-theme="dark"] .logSummary{background:linear-gradient(180deg,rgba(21,34,47,.94),rgba(16,27,39,.98))} body[data-theme="dark"] .logBody{background:#13212d;border-top-color:rgba(73,144,204,.18)} + body[data-theme="dark"] .secretSummary{background:linear-gradient(180deg,rgba(21,43,34,.94),rgba(16,34,27,.98))} + body[data-theme="dark"] .secretBody{background:#12241c;border-top-color:rgba(64,170,126,.18)} body[data-theme="dark"] .metaCard{background:rgba(17,25,35,.64);border-color:rgba(148,163,184,.14)} body[data-theme="dark"] .attachment{background:rgba(50,35,28,.9);border-color:rgba(207,109,60,.18)} body[data-theme="dark"] .attachmentLink{color:#f6c4a9} @@ -860,12 +1059,14 @@ function renderStyles() { body[data-theme="dark"] .panel, body[data-theme="dark"] pre, body[data-theme="dark"] .logBody pre{background:linear-gradient(180deg,rgba(73,144,204,.06),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} + body[data-theme="dark"] .secretValuePanel pre{background:linear-gradient(180deg,rgba(64,170,126,.08),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} body[data-theme="dark"] .panel{border-color:rgba(148,163,184,.14)} body[data-theme="dark"] .banner, body[data-theme="dark"] .empty{background:rgba(15,21,30,.82);border-color:rgba(148,163,184,.16)} body[data-theme="dark"] .time{background:rgba(148,163,184,.12);color:#e8edf3} body[data-theme="dark"] .tag{background:rgba(207,109,60,.14);color:#f0c2aa} body[data-theme="dark"] .logTag{background:rgba(73,144,204,.16);color:#93cfff} + body[data-theme="dark"] .secretTag{background:rgba(64,170,126,.16);color:#9fe0be} body[data-theme="dark"] .preview, body[data-theme="dark"] .logPreview, body[data-theme="dark"] .metaCard dd, @@ -906,15 +1107,18 @@ function clientApp(config) { const PANEL_STORAGE_KEY = "localstack-inspector-panel"; const EMAIL_PREFERENCES_STORAGE_KEY = "localstack-inspector-email-preferences"; const LOG_PREFERENCES_STORAGE_KEY = "localstack-inspector-log-preferences"; + const SECRET_PREFERENCES_STORAGE_KEY = "localstack-inspector-secret-preferences"; const REFRESH_INTERVALS = [5000, 10000, 15000, 30000, 60000]; const LOG_WINDOWS = [300000, 900000, 3600000, 21600000, 86400000]; const LOG_LIMITS = [100, 200, 300, 500]; const storedEmailPreferences = getStoredPreferences(EMAIL_PREFERENCES_STORAGE_KEY); const storedLogPreferences = getStoredPreferences(LOG_PREFERENCES_STORAGE_KEY); + const storedSecretPreferences = getStoredPreferences(SECRET_PREFERENCES_STORAGE_KEY); const appState = { panel: getInitialPanel(), logsReady: false, + secretsReady: false, theme: getInitialTheme() }; @@ -962,11 +1166,29 @@ function clientApp(config) { listSignature: "" }; + const secretsState = { + items: [], + filtered: [], + search: getStoredText(storedSecretPreferences?.search), + auto: getStoredBoolean(storedSecretPreferences?.auto, true), + interval: getStoredNumber(storedSecretPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs), + loading: false, + error: "", + updatedAt: 0, + source: "initial", + duration: 0, + newest: "", + openIds: new Set(), + values: {}, + listSignature: "" + }; + const el = { workspaceTabs: document.getElementById("workspaceTabs"), themeToggle: document.getElementById("themeToggle"), emailsPanel: document.getElementById("emailsPanel"), logsPanel: document.getElementById("logsPanel"), + secretsPanel: document.getElementById("secretsPanel"), refreshButton: document.getElementById("refreshButton"), autoToggle: document.getElementById("autoToggle"), intervalSelect: document.getElementById("intervalSelect"), @@ -1010,7 +1232,27 @@ function clientApp(config) { logsBanner: document.getElementById("logsBanner"), logsEmpty: document.getElementById("logsEmpty"), logsList: document.getElementById("logsList"), - logsContentPane: document.getElementById("logsContentPane") + logsContentPane: document.getElementById("logsContentPane"), + secretsRefreshButton: document.getElementById("secretsRefreshButton"), + secretsAutoToggle: document.getElementById("secretsAutoToggle"), + secretsIntervalSelect: document.getElementById("secretsIntervalSelect"), + secretsSearchInput: document.getElementById("secretsSearchInput"), + secretsClearSearchButton: document.getElementById("secretsClearSearchButton"), + secretsExpandAllButton: document.getElementById("secretsExpandAllButton"), + secretsCollapseAllButton: document.getElementById("secretsCollapseAllButton"), + secretsScrollToTopButton: document.getElementById("secretsScrollToTopButton"), + secretsStatusChip: document.getElementById("secretsStatusChip"), + secretsTotalStat: document.getElementById("secretsTotalStat"), + secretsVisibleStat: document.getElementById("secretsVisibleStat"), + secretsLoadedStat: document.getElementById("secretsLoadedStat"), + secretsNewestStat: document.getElementById("secretsNewestStat"), + secretsUpdatedStat: document.getElementById("secretsUpdatedStat"), + secretsFetchStat: document.getElementById("secretsFetchStat"), + secretsFetchDetail: document.getElementById("secretsFetchDetail"), + secretsBanner: document.getElementById("secretsBanner"), + secretsEmpty: document.getElementById("secretsEmpty"), + secretsList: document.getElementById("secretsList"), + secretsContentPane: document.getElementById("secretsContentPane") }; el.autoToggle.checked = state.auto; @@ -1021,22 +1263,30 @@ function clientApp(config) { el.logsWindowSelect.value = String(logsState.windowMs); el.logsLimitSelect.value = String(logsState.limit); el.logsSearchInput.value = logsState.search; + el.secretsAutoToggle.checked = secretsState.auto; + el.secretsIntervalSelect.value = String(secretsState.interval); + el.secretsSearchInput.value = secretsState.search; applyTheme(appState.theme); persistPanel(); persistEmailPreferences(); persistLogPreferences(); + persistSecretPreferences(); wire(); renderWorkspace(); renderAll(); renderLogsAll(); + renderSecretsAll(); if (appState.panel === "logs") { ensureLogsReady(); + } else if (appState.panel === "secrets") { + ensureSecretsReady(); } else { refreshMessages("initial"); } window.setInterval(() => { renderLiveClock(); renderLogsLiveClock(); + renderSecretsLiveClock(); }, 1000); function wire() { @@ -1228,18 +1478,112 @@ function clientApp(config) { } }); + el.secretsRefreshButton.addEventListener("click", () => refreshSecrets("manual")); + + el.secretsAutoToggle.addEventListener("change", () => { + secretsState.auto = el.secretsAutoToggle.checked; + persistSecretPreferences(); + scheduleSecretsRefresh(); + renderSecretsStatus(); + }); + + el.secretsIntervalSelect.addEventListener("change", () => { + secretsState.interval = Number(el.secretsIntervalSelect.value) || config.defaultRefreshMs; + persistSecretPreferences(); + scheduleSecretsRefresh(); + renderSecretsStatus(); + }); + + el.secretsSearchInput.addEventListener("input", (event) => { + secretsState.search = event.target.value; + applySecretsFilter(); + }); + + el.secretsClearSearchButton.addEventListener("click", () => { + secretsState.search = ""; + el.secretsSearchInput.value = ""; + applySecretsFilter(); + }); + + el.secretsExpandAllButton.addEventListener("click", () => { + secretsState.filtered.forEach((secret) => secretsState.openIds.add(secret.id)); + syncSecretExpansion(); + }); + + el.secretsCollapseAllButton.addEventListener("click", () => { + secretsState.openIds.clear(); + syncSecretExpansion(); + }); + + el.secretsScrollToTopButton.addEventListener("click", () => { + scrollPaneToTop(el.secretsContentPane); + }); + + el.secretsContentPane.addEventListener("scroll", () => { + updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); + }); + + el.secretsList.addEventListener("click", async (event) => { + const button = event.target.closest("button[data-secret-action]"); + + if (!button) { + return; + } + + const id = button.dataset.id; + const secret = getSecret(id); + + if (!secret) { + return; + } + + if (button.dataset.secretAction === "load-value") { + await ensureSecretValue(id, { force: false }); + return; + } + + if (button.dataset.secretAction === "reload-value") { + await ensureSecretValue(id, { force: true }); + return; + } + + if (button.dataset.secretAction === "copy-value") { + const entry = await ensureSecretValue(id, { force: false }); + + if (entry?.status === "loaded") { + await copyText(entry.copyValue); + setSecretsStatus("Secret value copied to the clipboard.", "ok"); + } + } + }); + document.addEventListener("visibilitychange", () => { window.clearTimeout(state.timer); window.clearTimeout(logsState.timer); + window.clearTimeout(secretsState.timer); if (document.hidden) { renderStatus(); renderLogsStatus(); + renderSecretsStatus(); return; } - if (appState.panel === "logs" && logsState.auto) { - refreshLogs("visibility"); + if (appState.panel === "secrets") { + if (secretsState.auto) { + refreshSecrets("visibility"); + } else { + renderSecretsStatus(); + } + return; + } + + if (appState.panel === "logs") { + if (logsState.auto) { + refreshLogs("visibility"); + } else { + renderLogsStatus(); + } return; } @@ -1263,18 +1607,26 @@ function clientApp(config) { return; } + if (appState.panel === "secrets") { + refreshSecrets("keyboard"); + return; + } + refreshMessages("keyboard"); } }); updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); + updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); } async function setPanel(panel) { if (!panel || panel === appState.panel) { if (panel === "logs") { await ensureLogsReady(); + } else if (panel === "secrets") { + await ensureSecretsReady(); } renderWorkspace(); @@ -1287,19 +1639,24 @@ function clientApp(config) { if (panel === "logs") { await ensureLogsReady(); + } else if (panel === "secrets") { + await ensureSecretsReady(); } else if (!state.updatedAt && !state.loading) { await refreshMessages("panel"); } scheduleRefresh(); scheduleLogsRefresh(); + scheduleSecretsRefresh(); renderStatus(); renderLogsStatus(); + renderSecretsStatus(); } function renderWorkspace() { el.emailsPanel.hidden = appState.panel !== "emails"; el.logsPanel.hidden = appState.panel !== "logs"; + el.secretsPanel.hidden = appState.panel !== "secrets"; el.workspaceTabs.querySelectorAll("button[data-panel]").forEach((button) => { const active = button.dataset.panel === appState.panel; @@ -1321,6 +1678,15 @@ function clientApp(config) { } } + async function ensureSecretsReady() { + if (appState.secretsReady) { + return; + } + + await refreshSecrets("initial"); + appState.secretsReady = !secretsState.error; + } + async function refreshMessages(source) { if (state.loading) { return; @@ -1505,6 +1871,52 @@ function clientApp(config) { } } + async function refreshSecrets(source) { + if (secretsState.loading) { + return; + } + + let shouldRenderList = false; + + secretsState.loading = true; + secretsState.source = source; + secretsState.error = ""; + renderSecretsStatus(); + renderSecretsFetch(); + + try { + const response = await fetch("/api/secrets", { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + const items = Array.isArray(payload.secrets) ? payload.secrets : []; + const nextSignature = computeSecretsListSignature(items); + shouldRenderList = nextSignature !== secretsState.listSignature; + secretsState.items = items; + secretsState.duration = payload.fetchDurationMs || 0; + secretsState.newest = payload.latestTimestamp || ""; + secretsState.updatedAt = Date.now(); + secretsState.listSignature = nextSignature; + + pruneSecretsState(); + applySecretsFilter(shouldRenderList); + appState.secretsReady = true; + setSecretsStatus(`Updated ${items.length} secret${items.length === 1 ? "" : "s"}.`, "ok"); + } catch (error) { + secretsState.error = error.message || "Unknown secrets refresh error"; + appState.secretsReady = false; + setSecretsStatus(`Refresh failed: ${secretsState.error}`, "bad"); + } finally { + secretsState.loading = false; + scheduleSecretsRefresh(); + renderSecretsAll({ renderList: shouldRenderList }); + } + } + function applyLogsFilter(shouldRenderList = true) { const search = logsState.search.trim().toLowerCase(); logsState.filtered = !search @@ -1514,6 +1926,15 @@ function clientApp(config) { renderLogsAll({ renderList: shouldRenderList }); } + function applySecretsFilter(shouldRenderList = true) { + const search = secretsState.search.trim().toLowerCase(); + secretsState.filtered = !search + ? [...secretsState.items] + : secretsState.items.filter((secret) => secretHaystack(secret).includes(search)); + persistSecretPreferences(); + renderSecretsAll({ renderList: shouldRenderList }); + } + function pruneState() { const ids = new Set(state.messages.map((message) => message.id)); state.openIds = new Set([...state.openIds].filter((id) => ids.has(id))); @@ -1537,6 +1958,17 @@ function clientApp(config) { logsState.openIds = new Set([...logsState.openIds].filter((id) => ids.has(id))); } + function pruneSecretsState() { + const ids = new Set(secretsState.items.map((secret) => secret.id)); + secretsState.openIds = new Set([...secretsState.openIds].filter((id) => ids.has(id))); + + Object.keys(secretsState.values).forEach((id) => { + if (!ids.has(id)) { + delete secretsState.values[id]; + } + }); + } + function applyFilter(shouldRenderList = true) { const search = state.search.trim().toLowerCase(); state.filtered = !search @@ -1572,6 +2004,26 @@ function clientApp(config) { .join("|"); } + function computeSecretsListSignature(items) { + return items + .map((secret) => + [ + secret.id, + secret.name || "", + secret.arn || "", + secret.description || "", + secret.lastChangedDate || "", + secret.createdDate || "", + secret.rotationEnabled ? 1 : 0, + secret.owningService || "", + secret.primaryRegion || "", + secret.versionCount || 0, + secret.tagCount || 0 + ].join("::") + ) + .join("|"); + } + function haystack(message) { return [ message.subject, @@ -1592,6 +2044,20 @@ function clientApp(config) { return [event.logStreamName, event.message, event.preview].filter(Boolean).join(" ").toLowerCase(); } + function secretHaystack(secret) { + return [ + secret.name, + secret.arn, + secret.description, + secret.primaryRegion, + secret.owningService, + ...(secret.tags || []).flatMap((tag) => [tag.key, tag.value]) + ] + .filter(Boolean) + .join(" ") + .toLowerCase(); + } + function scheduleRefresh() { window.clearTimeout(state.timer); @@ -1612,6 +2078,16 @@ function clientApp(config) { logsState.timer = window.setTimeout(() => refreshLogs("auto"), logsState.interval); } + function scheduleSecretsRefresh() { + window.clearTimeout(secretsState.timer); + + if (appState.panel !== "secrets" || !secretsState.auto || document.hidden || secretsState.loading) { + return; + } + + secretsState.timer = window.setTimeout(() => refreshSecrets("auto"), secretsState.interval); + } + function renderAll(options = {}) { const { renderList: shouldRenderList = true } = options; renderStats(); @@ -1635,6 +2111,17 @@ function clientApp(config) { renderLogsLiveClock(); } + function renderSecretsAll(options = {}) { + const { renderList: shouldRenderList = true } = options; + renderSecretsStats(); + renderSecretsFetch(); + renderSecretsStatus(); + if (shouldRenderList) { + renderSecretsList(); + } + renderSecretsLiveClock(); + } + function renderStats() { el.totalStat.textContent = String(state.messages.length); el.visibleStat.textContent = `${state.filtered.length} visible${state.search ? " after search" : ""}`; @@ -1670,6 +2157,15 @@ function clientApp(config) { el.logsNewestStat.textContent = logsState.newest ? formatDateTime(logsState.newest) : "No events"; } + function renderSecretsStats() { + el.secretsTotalStat.textContent = String(secretsState.items.length); + el.secretsVisibleStat.textContent = `${secretsState.filtered.length} visible${secretsState.search ? " after search" : ""}`; + el.secretsLoadedStat.textContent = String( + Object.values(secretsState.values).filter((entry) => entry?.status === "loaded").length + ); + el.secretsNewestStat.textContent = secretsState.newest ? formatDateTime(secretsState.newest) : "No secrets"; + } + function renderFetch() { if (state.loading) { el.fetchStat.textContent = "Refreshing..."; @@ -1716,6 +2212,29 @@ function clientApp(config) { el.logsFetchDetail.textContent = `${logsState.group || "No group selected"}. Endpoint: ${config.cloudWatchEndpoint}`; } + function renderSecretsFetch() { + if (secretsState.loading) { + el.secretsFetchStat.textContent = "Refreshing..."; + el.secretsFetchDetail.textContent = `Endpoint: ${config.secretsEndpoint} (${config.secretsRegion})`; + return; + } + + if (secretsState.error) { + el.secretsFetchStat.textContent = "Needs attention"; + el.secretsFetchDetail.textContent = secretsState.error; + return; + } + + if (!secretsState.updatedAt) { + el.secretsFetchStat.textContent = "Idle"; + el.secretsFetchDetail.textContent = `Endpoint: ${config.secretsEndpoint} (${config.secretsRegion})`; + return; + } + + el.secretsFetchStat.textContent = `${secretsState.duration}ms`; + el.secretsFetchDetail.textContent = `Endpoint: ${config.secretsEndpoint} (${config.secretsRegion})`; + } + function renderStatus() { el.statusChip.className = "status"; @@ -1788,6 +2307,42 @@ function clientApp(config) { el.logsStatusChip.textContent = `Live refresh on, next check in ${seconds}s`; } + function renderSecretsStatus() { + el.secretsStatusChip.className = "status"; + + if (secretsState.loading) { + el.secretsStatusChip.classList.add("warn"); + el.secretsStatusChip.textContent = "Refreshing secrets..."; + return; + } + + if (secretsState.error) { + el.secretsStatusChip.classList.add("bad"); + el.secretsStatusChip.textContent = `Refresh failed: ${secretsState.error}`; + return; + } + + if (!secretsState.auto) { + el.secretsStatusChip.textContent = "Live refresh paused"; + return; + } + + if (document.hidden) { + el.secretsStatusChip.classList.add("warn"); + el.secretsStatusChip.textContent = "Tab hidden, live refresh paused"; + return; + } + + if (!secretsState.updatedAt) { + el.secretsStatusChip.textContent = "Waiting for first refresh..."; + return; + } + + const seconds = Math.max(0, Math.ceil((secretsState.updatedAt + secretsState.interval - Date.now()) / 1000)); + el.secretsStatusChip.classList.add("ok"); + el.secretsStatusChip.textContent = `Live refresh on, next check in ${seconds}s`; + } + function renderLiveClock() { if (!state.updatedAt) { el.updatedStat.textContent = "Not refreshed yet"; @@ -1808,6 +2363,16 @@ function clientApp(config) { renderLogsStatus(); } + function renderSecretsLiveClock() { + if (!secretsState.updatedAt) { + el.secretsUpdatedStat.textContent = "Not refreshed yet"; + return; + } + + el.secretsUpdatedStat.textContent = `Updated ${formatRelative(secretsState.updatedAt)} via ${secretsState.source}`; + renderSecretsStatus(); + } + function renderList() { el.banner.hidden = !state.error; el.banner.textContent = state.error ? `Refresh failed: ${state.error}` : ""; @@ -1857,6 +2422,27 @@ function clientApp(config) { updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); } + function renderSecretsList() { + el.secretsBanner.hidden = !secretsState.error; + el.secretsBanner.textContent = secretsState.error ? `Refresh failed: ${secretsState.error}` : ""; + + if (!secretsState.filtered.length) { + el.secretsList.innerHTML = ""; + el.secretsEmpty.hidden = false; + el.secretsEmpty.textContent = secretsState.items.length + ? "No secrets match the current search." + : "No Secrets Manager entries found in LocalStack yet."; + updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); + return; + } + + el.secretsEmpty.hidden = true; + el.secretsList.innerHTML = secretsState.filtered.map((secret) => renderSecretCard(secret)).join(""); + bindSecretToggles(); + syncSecretExpansion(); + updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); + } + function renderLogEvent(event) { return `
@@ -1880,6 +2466,69 @@ function clientApp(config) { `; } + function renderSecretCard(secret) { + const valueState = secretsState.values[secret.id]; + const tags = []; + + if (secret.owningService) { + tags.push(`${escapeHtml(secret.owningService)}`); + } + + if (secret.primaryRegion) { + tags.push(`${escapeHtml(secret.primaryRegion)}`); + } + + if (secret.rotationEnabled) { + tags.push('Rotation on'); + } else { + tags.push('Rotation off'); + } + + if (secret.tagCount) { + tags.push(`${secret.tagCount} tag${secret.tagCount === 1 ? "" : "s"}`); + } + + if (valueState?.status === "loaded") { + tags.push(`${escapeHtml(valueState.label)}`); + } + + if (valueState?.status === "error") { + tags.push('Value load failed'); + } + + return ` +
+ +
+
+

🔐 ${escapeHtml(secret.name)}

+

${escapeHtml(secret.description || secret.arn || "No description")}

+
+ ${escapeHtml(formatDateTime(secret.lastChangedDate || secret.createdDate))} +
+
${tags.join("")}
+

${escapeHtml(buildSecretPreview(secret))}

+
+
+
+ ${metaCard("Name", secret.name)} + ${metaCard("ARN", secret.arn || "Not available")} + ${metaCard("Description", secret.description || "None")} + ${metaCard("Last changed", formatDateTime(secret.lastChangedDate || secret.createdDate))} + ${metaCard("Created", formatDateTime(secret.createdDate))} + ${metaCard("Last accessed", formatDateTime(secret.lastAccessedDate))} + ${metaCard("Primary region", secret.primaryRegion || "Not set")} + ${metaCard("Owning service", secret.owningService || "Not set")} +
+ +
+ ${renderSecretValuePanel(secret, valueState)} +
+
+
+ `; + } + function bindLogToggles() { el.logsList.querySelectorAll(".logEvent").forEach((details) => { details.addEventListener("toggle", () => { @@ -1898,6 +2547,25 @@ function clientApp(config) { }); } + function bindSecretToggles() { + el.secretsList.querySelectorAll(".secretCard").forEach((details) => { + details.addEventListener("toggle", () => { + const id = details.dataset.id; + + if (!id) { + return; + } + + if (details.open) { + secretsState.openIds.add(id); + ensureSecretValue(id, { force: false }); + } else { + secretsState.openIds.delete(id); + } + }); + }); + } + function bindCardToggles() { el.list.querySelectorAll(".card").forEach((details) => { details.addEventListener("toggle", () => { @@ -2023,6 +2691,43 @@ function clientApp(config) { return `
${escapeHtml(message.textContent || "No plain-text content available for this message.")}
`; } + function renderSecretValuePanel(secret, valueState) { + if (!valueState) { + return `
Secret values are loaded on demand.
`; + } + + if (valueState.status === "loading") { + return '
Loading secret value...
'; + } + + if (valueState.status === "error") { + return `
Unable to load secret value: ${escapeHtml(valueState.error)}
`; + } + + return ` +
+
+ ${escapeHtml(valueState.label)} + ${ + valueState.versionId + ? `Version ${escapeHtml(valueState.versionId.slice(0, 8))}` + : "" + } + ${ + valueState.versionStages.length + ? `${escapeHtml(valueState.versionStages.join(", "))}` + : "" + } +
+
+ + +
+
+
${valueState.displayHtml}
+ `; + } + function metaCard(label, value) { return `
${escapeHtml(label)}
${escapeHtml(value)}
`; } @@ -2035,7 +2740,6 @@ function clientApp(config) { if (shouldOpen && !details.open) { details.open = true; - return; } if (!shouldOpen && details.open) { @@ -2053,6 +2757,31 @@ function clientApp(config) { window.requestAnimationFrame(applyCardState); } + function syncSecretExpansion() { + const applySecretState = () => { + el.secretsList.querySelectorAll(".secretCard").forEach((details) => { + const id = details.dataset.id; + const shouldOpen = Boolean(id && secretsState.openIds.has(id)); + + if (shouldOpen && !details.open) { + details.open = true; + } + + if (!shouldOpen && details.open) { + details.open = false; + return; + } + + if (shouldOpen) { + ensureSecretValue(id, { force: false }); + } + }); + }; + + applySecretState(); + window.requestAnimationFrame(applySecretState); + } + function resolveAttachmentIcon(attachment) { const filename = String(attachment?.filename || "").toLowerCase(); const contentType = String(attachment?.contentType || "").toLowerCase(); @@ -2137,6 +2866,10 @@ function clientApp(config) { return logsState.events.find((event) => event.id === id); } + function getSecret(id) { + return secretsState.items.find((secret) => secret.id === id); + } + async function loadRaw(id) { if (state.raw[id]?.status === "loaded") { return state.raw[id].value; @@ -2168,6 +2901,72 @@ function clientApp(config) { } } + async function ensureSecretValue(id, options = {}) { + const { force = false } = options; + + if (!id) { + return null; + } + + if (!force && secretsState.values[id]?.status === "loaded") { + return secretsState.values[id]; + } + + if (secretsState.values[id]?.status === "loading") { + return null; + } + + secretsState.values[id] = { status: "loading" }; + renderSecretsAll(); + + try { + const response = await fetch(`/api/secrets/value?id=${encodeURIComponent(id)}`, { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + const secretString = typeof payload.secretString === "string" ? payload.secretString : ""; + const secretBinary = typeof payload.secretBinary === "string" ? payload.secretBinary : ""; + const parsedString = secretString ? tryParseJsonText(secretString) : { ok: false, value: null }; + const entry = { + status: "loaded", + label: secretBinary ? "Binary" : parsedString.ok ? "JSON" : secretString ? "Text" : "Empty", + copyValue: secretBinary + ? secretBinary + : parsedString.ok + ? JSON.stringify(parsedString.value, null, 2) + : secretString || "No secret value.", + displayHtml: secretBinary + ? escapeHtml(secretBinary) + : parsedString.ok + ? highlightJsonText(JSON.stringify(parsedString.value, null, 2)) + : escapeHtml(secretString || "No secret value."), + isJson: parsedString.ok, + isBinary: Boolean(secretBinary), + versionId: payload.versionId || "", + versionStages: Array.isArray(payload.versionStages) ? payload.versionStages : [], + createdDate: payload.createdDate || "", + arn: payload.arn || "", + name: payload.name || "" + }; + + secretsState.values[id] = entry; + return entry; + } catch (error) { + secretsState.values[id] = { + status: "error", + error: error.message || "Unknown secret value error" + }; + setSecretsStatus("Could not load the secret value.", "bad"); + return null; + } finally { + renderSecretsAll(); + } + } + async function copyText(value) { try { await navigator.clipboard.writeText(value); @@ -2204,9 +3003,19 @@ function clientApp(config) { el.logsStatusChip.textContent = message; } + function setSecretsStatus(message, tone) { + el.secretsStatusChip.className = "status"; + + if (tone) { + el.secretsStatusChip.classList.add(tone); + } + + el.secretsStatusChip.textContent = message; + } + function getInitialPanel() { const storedPanel = readStoredValue(PANEL_STORAGE_KEY); - return storedPanel === "logs" ? "logs" : "emails"; + return ["emails", "logs", "secrets"].includes(storedPanel) ? storedPanel : "emails"; } function getInitialTheme() { @@ -2267,6 +3076,14 @@ function clientApp(config) { }); } + function persistSecretPreferences() { + writeStoredJson(SECRET_PREFERENCES_STORAGE_KEY, { + search: secretsState.search, + auto: secretsState.auto, + interval: secretsState.interval + }); + } + function getStoredPreferences(key) { try { const rawValue = window.localStorage.getItem(key); @@ -2395,8 +3212,34 @@ function clientApp(config) { } } + function buildSecretPreview(secret) { + const fragments = []; + + if (secret.description) { + fragments.push(secret.description); + } + + if (secret.owningService) { + fragments.push(`Service: ${secret.owningService}`); + } + + if (secret.primaryRegion) { + fragments.push(`Region: ${secret.primaryRegion}`); + } + + if (secret.tagCount) { + fragments.push(`${secret.tagCount} tag${secret.tagCount === 1 ? "" : "s"}`); + } + + if (!fragments.length) { + fragments.push("No description or tags yet."); + } + + return fragments.join(" • "); + } + function renderLogPreviewContent(event) { - const parsedLog = tryParseLogJson(event?.message); + const parsedLog = tryParseJsonText(event?.message); if (!parsedLog.ok) { return escapeHtml(event?.preview || "No preview available."); @@ -2408,7 +3251,7 @@ function clientApp(config) { } function renderLogBodyContent(message) { - const parsedLog = tryParseLogJson(message); + const parsedLog = tryParseJsonText(message); if (!parsedLog.ok) { return escapeHtml(formatLogMessage(message)); @@ -2417,7 +3260,7 @@ function clientApp(config) { return highlightJsonText(JSON.stringify(parsedLog.value, null, 2)); } - function tryParseLogJson(message) { + function tryParseJsonText(message) { const value = String(message || "").trim(); if (!value) { @@ -2485,4 +3328,5 @@ app.listen(PORT, () => { console.log(`LocalStack inspector is running on http://localhost:${PORT}`); console.log(`Watching LocalStack SES endpoint at ${SES_ENDPOINT}`); console.log(`Watching LocalStack CloudWatch Logs endpoint at ${CLOUDWATCH_ENDPOINT} (${CLOUDWATCH_REGION})`); + console.log(`Watching LocalStack Secrets Manager endpoint at ${SECRETS_ENDPOINT} (${SECRETS_REGION})`); }); diff --git a/_reference/localEmailViewer/package-lock.json b/_reference/localEmailViewer/package-lock.json index 2dec2be60..2f00ef450 100644 --- a/_reference/localEmailViewer/package-lock.json +++ b/_reference/localEmailViewer/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.1012.0", + "@aws-sdk/client-secrets-manager": "^3.1013.0", "express": "^5.1.0", "mailparser": "^3.7.4", "node-fetch": "^3.3.2" @@ -207,14 +208,64 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-secrets-manager": { + "version": "3.1013.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1013.0.tgz", + "integrity": "sha512-jVleaMhnwz7JsYZ8vo3otRdhNl1vmJPfwYQtAfb0T29TwfI9pvfRuWD4CXbm0DPmCJzhN7YT+tqn1dLhk83Y3g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.22", + "@aws-sdk/credential-provider-node": "^3.972.23", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.23", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.9", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-retry": "^4.4.43", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.42", + "@smithy/util-defaults-mode-node": "^4.2.45", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/core": { - "version": "3.973.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.21.tgz", - "integrity": "sha512-OTUcDX9Yfz/FLKbHjiMaP9D4Hs44lYJzN7zBcrK2nDmBt0Wr8D6nYt12QoBkZsW0nVMFsTIGaZCrsU9zCcIMXQ==", + "version": "3.973.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.22.tgz", + "integrity": "sha512-lY6g5L95jBNgOUitUhfV2N/W+i08jHEl3xuLODYSQH5Sf50V+LkVYBSyZRLtv2RyuXZXiV7yQ+acpswK1tlrOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", - "@aws-sdk/xml-builder": "^3.972.12", + "@aws-sdk/xml-builder": "^3.972.14", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", @@ -232,12 +283,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.19.tgz", - "integrity": "sha512-33NpkQtmnsjLr9QdZvL3w8bjy+WoBJ+jY8JwuzxIq38rDNi1kwpBWW7Yjh+8bMlksd+ZAWW0fH4S/6OeoAdU5A==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.20.tgz", + "integrity": "sha512-vI0QN96DFx3g9AunfOWF3CS4cMkqFiR/WM/FyP9QHr5rZ2dKPkYwP3tCgAOvGuu9CXI7dC1vU2FVUuZ+tfpNvQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", + "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", @@ -248,12 +299,12 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.21.tgz", - "integrity": "sha512-xFke7yjbON4unNOG0TApQwz+o1LH5VhVLgWlUuiLRWNDyBfeHIFje2ck8qHybvJ8Fkm5m3SsN+pvHtVo6PGWlQ==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.22.tgz", + "integrity": "sha512-aS/81smalpe7XDnuQfOq4LIPuaV2PRKU2aMTrHcqO5BD4HwO5kESOHNcec2AYfBtLtIDqgF6RXisgBnfK/jt0w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", + "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", @@ -269,19 +320,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.21.tgz", - "integrity": "sha512-fmJN7KhB7CoG65w9fC2LVOd2wZbR2d1yJIpZNe2J5CeDPu7nUHSmavuJAeGEoE3OL5UIBVPNhmK/fV/NQrs3Hw==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.22.tgz", + "integrity": "sha512-rpF8fBT0LllMDp78s62aL2A/8MaccjyJ0ORzqu+ZADeECLSrrCWIeeXsuRam+pxiAMkI1uIyDZJmgLGdadkPXw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/credential-provider-env": "^3.972.19", - "@aws-sdk/credential-provider-http": "^3.972.21", - "@aws-sdk/credential-provider-login": "^3.972.21", - "@aws-sdk/credential-provider-process": "^3.972.19", - "@aws-sdk/credential-provider-sso": "^3.972.21", - "@aws-sdk/credential-provider-web-identity": "^3.972.21", - "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/core": "^3.973.22", + "@aws-sdk/credential-provider-env": "^3.972.20", + "@aws-sdk/credential-provider-http": "^3.972.22", + "@aws-sdk/credential-provider-login": "^3.972.22", + "@aws-sdk/credential-provider-process": "^3.972.20", + "@aws-sdk/credential-provider-sso": "^3.972.22", + "@aws-sdk/credential-provider-web-identity": "^3.972.22", + "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", @@ -294,13 +345,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.21.tgz", - "integrity": "sha512-ENU+YCiuQocQjfIf9bPxZ+ZY0wIBkl3SMH22optBQwy8UFpSfonHynXzGT27xQxer4cYTNOpwDqbfo57BusbpQ==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.22.tgz", + "integrity": "sha512-u33CO9zeNznlVSg9tWTCRYxaGkqr1ufU6qeClpmzAabXZa8RZxQoVXxL5T53oZJFzQYj+FImORCSsi7H7B77gQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/core": "^3.973.22", + "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", @@ -313,17 +364,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.22.tgz", - "integrity": "sha512-VE6i8nkmrRyhKut7nnfCWRbdDf+CfyRr8ixSwdaPDguYlgvkAO2pHu9oK11XzbSuatB0io1ozI/vpYhelXn8Pg==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.23.tgz", + "integrity": "sha512-U8tyLbLOZItuVWTH0ay9gWo4xMqZwqQbg1oMzdU4FQSkTpqXemm4X0uoKBR6llqAStgBp30ziKFJHTA43l4qMw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.19", - "@aws-sdk/credential-provider-http": "^3.972.21", - "@aws-sdk/credential-provider-ini": "^3.972.21", - "@aws-sdk/credential-provider-process": "^3.972.19", - "@aws-sdk/credential-provider-sso": "^3.972.21", - "@aws-sdk/credential-provider-web-identity": "^3.972.21", + "@aws-sdk/credential-provider-env": "^3.972.20", + "@aws-sdk/credential-provider-http": "^3.972.22", + "@aws-sdk/credential-provider-ini": "^3.972.22", + "@aws-sdk/credential-provider-process": "^3.972.20", + "@aws-sdk/credential-provider-sso": "^3.972.22", + "@aws-sdk/credential-provider-web-identity": "^3.972.22", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", @@ -336,12 +387,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.19.tgz", - "integrity": "sha512-hjj5bFo4kf5/WzAMjDEFByVOMbq5gZiagIpJexf7Kp9nIDaGzhCphMsx03NCA8s9zUJzHlD1lXazd7MS+e03Lg==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.20.tgz", + "integrity": "sha512-QRfk7GbA4/HDRjhP3QYR6QBr/QKreVoOzvvlRHnOuGgYJkeoPgPY3LAI1kK1ZMgZ4hH9KiGp757/ntol+INAig==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", + "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -353,14 +404,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.21.tgz", - "integrity": "sha512-9jWRCuMZpZKlqCZ46bvievqdfswsyB2yPAr9rOiN+FxaGgf8jrR5iYDqJgscvk1jrbAxiK4cIjHv3XjIAWAhzQ==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.22.tgz", + "integrity": "sha512-4vqlSaUbBj4aNPVKfB6yXuIQ2Z2mvLfIGba2OzzF6zUkN437/PGWsxBU2F8QPSFHti6seckvyCXidU3H+R8NvQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/nested-clients": "^3.996.11", - "@aws-sdk/token-providers": "3.1012.0", + "@aws-sdk/core": "^3.973.22", + "@aws-sdk/nested-clients": "^3.996.12", + "@aws-sdk/token-providers": "3.1013.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -372,13 +423,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.21.tgz", - "integrity": "sha512-ShWQO/cQVZ+j3zUDK7Kj+m7grPzQCVA2iaZdJ+hJTGvVH5lR32Ip/rgZZ+zBdH6D6wczP9Upa4NMXoqJdGpK1g==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.22.tgz", + "integrity": "sha512-/wN1CYg2rVLhW8/jLxMWacQrkpaynnL+4j/Z+e6X1PfoE6NiC0BeOw3i0JmtZrKun85wNV5GmspvuWJihfeeUw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/core": "^3.973.22", + "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -435,12 +486,12 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.22.tgz", - "integrity": "sha512-pZPNGWZVQvgUIO/P9PXZNz7ciq9mLYb/wQEurg3phKTa3DiBIunIRcgA0eBNwmog6S3oy0KR1bv4EJ4ld9A5sQ==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.23.tgz", + "integrity": "sha512-HQu8QoqGZZTvg0Spl9H39QTsSMFwgu+8yz/QGKndXFLk9FZMiCiIgBCVlTVKMDvVbgqIzD9ig+/HmXsIL2Rb+g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", + "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.12", @@ -454,23 +505,23 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.11.tgz", - "integrity": "sha512-i7SwoSR4JB/79JoGDUACnFUQOZwXGLWNX35lIb1Pq72nUGlVV+RFZp+BLa8S+mog2pbXU9+6Kc5YwGiMi5bKhQ==", + "version": "3.996.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.12.tgz", + "integrity": "sha512-KLdQGJPSm98uLINolQ0Tol8OAbk7g0Y7zplHJ1K83vbMIH13aoCvR6Tho66xueW4l4aZlEgVGLWBnD8ifUMsGQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.21", + "@aws-sdk/core": "^3.973.22", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.22", + "@aws-sdk/middleware-user-agent": "^3.972.23", "@aws-sdk/region-config-resolver": "^3.972.8", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.8", + "@aws-sdk/util-user-agent-node": "^3.973.9", "@smithy/config-resolver": "^4.4.11", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", @@ -519,13 +570,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1012.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1012.0.tgz", - "integrity": "sha512-vzKwy020zjuiF4WTJzejx5nYcXJnRhHpb6i3lyZHIwfFwXG1yX4bzBVNMWYWF+bz1i2Pp2VhJbPyzpqj4VuJXQ==", + "version": "3.1013.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1013.0.tgz", + "integrity": "sha512-IL1c54UvbuERrs9oLm5rvkzMciwhhpn1FL0SlC3XUMoLlFhdBsWJgQKK8O5fsQLxbFVqjbjFx9OBkrn44X9PHw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.21", - "@aws-sdk/nested-clients": "^3.996.11", + "@aws-sdk/core": "^3.973.22", + "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -590,12 +641,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.8.tgz", - "integrity": "sha512-Kvb96TafGPLYo4Z2GRCzQTne77epXgiZEo0DDXwavzkWmgDV/1XD1tMA766gzRcHHFUraWsE+4T8DKtPTZUxgQ==", + "version": "3.973.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.9.tgz", + "integrity": "sha512-jeFqqp8KD/P5O+qeKxyGeu7WEVIZFNprnkaDjGmBOjwxYwafCBhpxTgV1TlW6L8e76Vh/siNylNmN/OmSIFBUQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.22", + "@aws-sdk/middleware-user-agent": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", @@ -615,9 +666,9 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.13.tgz", - "integrity": "sha512-I/+BMxM4WE/6xL0tyV7tAUDOAXmyw/va1oGr/eSly43HmLUcD1G+v96vEKAA8VoLcZ03ZQo/PWzjmN9zQErqPQ==", + "version": "3.972.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.14.tgz", + "integrity": "sha512-G/Yd8Bnnyh8QrqLf8jWJbixEnScUFW24e/wOBGYdw1Cl4r80KX/DvHyM2GVZ2vTp7J4gTEr8IXJlTadA8+UfuQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", diff --git a/_reference/localEmailViewer/package.json b/_reference/localEmailViewer/package.json index 377708878..67ea3fc6b 100644 --- a/_reference/localEmailViewer/package.json +++ b/_reference/localEmailViewer/package.json @@ -10,9 +10,10 @@ "keywords": [], "author": "", "license": "ISC", - "description": "LocalStack inspector for SES emails and CloudWatch logs", + "description": "LocalStack inspector for SES emails, CloudWatch logs, and Secrets Manager", "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.1012.0", + "@aws-sdk/client-secrets-manager": "^3.1013.0", "express": "^5.1.0", "mailparser": "^3.7.4", "node-fetch": "^3.3.2" From 285043a6ba1abea755114fce7ae286a2e0cb29fc Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 19 Mar 2026 18:23:58 -0400 Subject: [PATCH 16/56] feature/IO-3587-Commision-Cut - Improved localstack client --- _reference/localEmailViewer/README.md | 15 +- _reference/localEmailViewer/index.js | 1519 ++++++++++++++++- _reference/localEmailViewer/package-lock.json | 365 ++++ _reference/localEmailViewer/package.json | 3 +- 4 files changed, 1863 insertions(+), 39 deletions(-) diff --git a/_reference/localEmailViewer/README.md b/_reference/localEmailViewer/README.md index 6492c1176..019d6f946 100644 --- a/_reference/localEmailViewer/README.md +++ b/_reference/localEmailViewer/README.md @@ -3,6 +3,7 @@ This app connects to your Docker LocalStack endpoints and gives you a compact in - SES generated emails - CloudWatch log groups, streams, and recent events - Secrets Manager secrets and values +- S3 buckets and object previews ```shell npm start @@ -21,9 +22,14 @@ Features: - SES email workspace with manual refresh, live refresh, search, HTML/text/raw views, attachment downloads, and new-message highlighting - CloudWatch Logs workspace with log group selection, stream filtering, adjustable time window, - adjustable event limit, live refresh, and in-browser log search + adjustable event limit, live refresh, in-browser log search, log-level highlighting, wrap toggle, + and optional tail-to-newest mode - Secrets Manager workspace with live refresh, search, expandable secret metadata, lazy-loaded - secret values, and copyable JSON/text values + secret values, masked-by-default secret viewing, and quick copy actions +- S3 Explorer workspace with bucket selection, prefix filtering, object search, lazy object + previews, + object key/URI copy actions, and downloads +- Shared LocalStack service health strip plus a reset action for clearing saved viewer state - Compact single-page UI for switching between the local stack tools you use most Optional environment variables: @@ -40,4 +46,9 @@ CLOUDWATCH_VIEWER_WINDOW_MS=900000 CLOUDWATCH_VIEWER_LIMIT=200 SECRETS_VIEWER_ENDPOINT=http://localhost:4566 SECRETS_VIEWER_REGION=ca-central-1 +S3_VIEWER_ENDPOINT=http://localhost:4566 +S3_VIEWER_REGION=ca-central-1 +S3_VIEWER_BUCKET= +S3_VIEWER_PREVIEW_BYTES=262144 +S3_VIEWER_IMAGE_PREVIEW_BYTES=1048576 ``` diff --git a/_reference/localEmailViewer/index.js b/_reference/localEmailViewer/index.js index f45eab624..64b72fbd7 100644 --- a/_reference/localEmailViewer/index.js +++ b/_reference/localEmailViewer/index.js @@ -7,6 +7,13 @@ import { FilterLogEventsCommand } from "@aws-sdk/client-cloudwatch-logs"; import { GetSecretValueCommand, ListSecretsCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; +import { + GetObjectCommand, + HeadObjectCommand, + ListBucketsCommand, + ListObjectsV2Command, + S3Client +} from "@aws-sdk/client-s3"; import { simpleParser } from "mailparser"; const app = express(); @@ -22,6 +29,11 @@ const CLOUDWATCH_DEFAULT_WINDOW_MS = Number(process.env.CLOUDWATCH_VIEWER_WINDOW const CLOUDWATCH_DEFAULT_LIMIT = Number(process.env.CLOUDWATCH_VIEWER_LIMIT || 200); const SECRETS_ENDPOINT = process.env.SECRETS_VIEWER_ENDPOINT || CLOUDWATCH_ENDPOINT; const SECRETS_REGION = process.env.SECRETS_VIEWER_REGION || CLOUDWATCH_REGION; +const S3_ENDPOINT = process.env.S3_VIEWER_ENDPOINT || CLOUDWATCH_ENDPOINT; +const S3_REGION = process.env.S3_VIEWER_REGION || CLOUDWATCH_REGION; +const S3_DEFAULT_BUCKET = process.env.S3_VIEWER_BUCKET || ""; +const S3_PREVIEW_MAX_BYTES = Number(process.env.S3_VIEWER_PREVIEW_BYTES || 256 * 1024); +const S3_IMAGE_PREVIEW_MAX_BYTES = Number(process.env.S3_VIEWER_IMAGE_PREVIEW_BYTES || 1024 * 1024); const LOCALSTACK_CREDENTIALS = { accessKeyId: process.env.AWS_ACCESS_KEY_ID || "test", secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "test" @@ -36,6 +48,12 @@ const secretsManagerClient = new SecretsManagerClient({ endpoint: SECRETS_ENDPOINT, credentials: LOCALSTACK_CREDENTIALS }); +const s3Client = new S3Client({ + region: S3_REGION, + endpoint: S3_ENDPOINT, + credentials: LOCALSTACK_CREDENTIALS, + forcePathStyle: true +}); app.use((req, res, next) => { res.set("Cache-Control", "no-store"); @@ -57,13 +75,26 @@ app.get("/health", (req, res) => { endpoints: { ses: SES_ENDPOINT, cloudWatchLogs: CLOUDWATCH_ENDPOINT, - secretsManager: SECRETS_ENDPOINT + secretsManager: SECRETS_ENDPOINT, + s3: S3_ENDPOINT }, port: PORT, defaultRefreshMs: DEFAULT_REFRESH_MS }); }); +app.get("/api/service-health", async (req, res) => { + try { + res.json(await loadServiceHealthSummary()); + } catch (error) { + console.error("Error fetching service health:", error); + res.status(502).json({ + error: "Unable to fetch LocalStack service health", + details: error.message + }); + } +}); + app.get("/api/messages", async (req, res) => { try { res.json(await loadMessages()); @@ -236,6 +267,108 @@ app.get("/api/secrets/value", async (req, res) => { } }); +app.get("/api/s3/buckets", async (req, res) => { + try { + res.json(await loadS3Buckets()); + } catch (error) { + console.error("Error fetching S3 buckets:", error); + res.status(502).json({ + error: "Unable to fetch S3 buckets from LocalStack", + details: error.message, + endpoint: S3_ENDPOINT + }); + } +}); + +app.get("/api/s3/objects", async (req, res) => { + try { + const bucket = String(req.query.bucket || ""); + const prefix = String(req.query.prefix || ""); + + if (!bucket) { + res.status(400).json({ error: "Query parameter 'bucket' is required" }); + return; + } + + res.json(await loadS3Objects({ bucket, prefix })); + } catch (error) { + console.error("Error fetching S3 objects:", error); + res.status(502).json({ + error: "Unable to fetch S3 objects from LocalStack", + details: error.message, + endpoint: S3_ENDPOINT + }); + } +}); + +app.get("/api/s3/object", async (req, res) => { + try { + const bucket = String(req.query.bucket || ""); + const key = String(req.query.key || ""); + + if (!bucket || !key) { + res.status(400).json({ error: "Query parameters 'bucket' and 'key' are required" }); + return; + } + + res.json(await loadS3ObjectPreview({ bucket, key })); + } catch (error) { + if (error?.name === "NoSuchKey" || error?.name === "NotFound") { + res.status(404).json({ + error: "Object not found", + details: error.message, + endpoint: S3_ENDPOINT + }); + return; + } + + console.error("Error fetching S3 object preview:", error); + res.status(502).json({ + error: "Unable to fetch S3 object preview from LocalStack", + details: error.message, + endpoint: S3_ENDPOINT + }); + } +}); + +app.get("/api/s3/download", async (req, res) => { + try { + const bucket = String(req.query.bucket || ""); + const key = String(req.query.key || ""); + const inline = String(req.query.inline || "") === "1"; + + if (!bucket || !key) { + res.status(400).type("text/plain").send("Query parameters 'bucket' and 'key' are required"); + return; + } + + const response = await s3Client.send( + new GetObjectCommand({ + Bucket: bucket, + Key: key + }) + ); + const content = Buffer.from(await response.Body.transformToByteArray()); + const filename = basenameFromKey(key); + + res.setHeader("Content-Type", response.ContentType || guessObjectContentType(key)); + res.setHeader( + "Content-Disposition", + inline ? buildInlineDisposition(filename) : buildAttachmentDisposition(filename) + ); + res.setHeader("Content-Length", String(content.length)); + res.send(content); + } catch (error) { + if (error?.name === "NoSuchKey" || error?.name === "NotFound") { + res.status(404).type("text/plain").send("Object not found"); + return; + } + + console.error("Error downloading S3 object:", error); + res.status(502).type("text/plain").send(`Unable to download S3 object: ${error.message}`); + } +}); + async function loadMessages() { const startedAt = Date.now(); const sesMessages = await fetchSesMessages(); @@ -496,6 +629,192 @@ async function loadSecretValue(secretId) { }; } +async function loadS3Buckets() { + const startedAt = Date.now(); + const response = await s3Client.send(new ListBucketsCommand({})); + const buckets = (response.Buckets || []) + .map((bucket) => ({ + name: bucket.Name || "", + creationDate: normalizeTimestamp(bucket.CreationDate) + })) + .filter((bucket) => bucket.name) + .sort((left, right) => left.name.localeCompare(right.name)); + + return { + endpoint: S3_ENDPOINT, + region: S3_REGION, + fetchedAt: new Date().toISOString(), + fetchDurationMs: Date.now() - startedAt, + totalBuckets: buckets.length, + buckets + }; +} + +async function loadS3Objects({ bucket, prefix }) { + const startedAt = Date.now(); + const objects = []; + let continuationToken; + let pageCount = 0; + + do { + const response = await s3Client.send( + new ListObjectsV2Command({ + Bucket: bucket, + Prefix: prefix || undefined, + ContinuationToken: continuationToken, + MaxKeys: 200 + }) + ); + + objects.push( + ...(response.Contents || []).map((object, index) => ({ + id: `${bucket}::${object.Key || index}`, + bucket, + key: object.Key || "", + size: object.Size || 0, + lastModified: normalizeTimestamp(object.LastModified), + etag: String(object.ETag || "").replace(/^"|"$/g, ""), + storageClass: object.StorageClass || "STANDARD" + })) + ); + + continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; + pageCount += 1; + } while (continuationToken && pageCount < 10 && objects.length < 1000); + + objects.sort((left, right) => { + const leftTime = Date.parse(left.lastModified || 0) || 0; + const rightTime = Date.parse(right.lastModified || 0) || 0; + + if (rightTime !== leftTime) { + return rightTime - leftTime; + } + + return left.key.localeCompare(right.key); + }); + + return { + endpoint: S3_ENDPOINT, + region: S3_REGION, + bucket, + prefix, + fetchedAt: new Date().toISOString(), + fetchDurationMs: Date.now() - startedAt, + totalObjects: objects.length, + latestTimestamp: objects[0]?.lastModified || "", + objects + }; +} + +async function loadS3ObjectPreview({ bucket, key }) { + const startedAt = Date.now(); + const head = await s3Client.send( + new HeadObjectCommand({ + Bucket: bucket, + Key: key + }) + ); + + const contentType = head.ContentType || guessObjectContentType(key); + const contentLength = Number(head.ContentLength || 0); + const previewType = resolveS3PreviewType(contentType, key); + const result = { + endpoint: S3_ENDPOINT, + region: S3_REGION, + bucket, + key, + fetchDurationMs: 0, + contentType, + contentLength, + etag: String(head.ETag || "").replace(/^"|"$/g, ""), + lastModified: normalizeTimestamp(head.LastModified), + metadata: head.Metadata || {}, + previewType, + previewText: "", + imageDataUrl: "", + truncated: false + }; + + const shouldLoadTextPreview = previewType === "json" || previewType === "text" || previewType === "html"; + const shouldLoadImagePreview = + previewType === "image" && contentLength > 0 && contentLength <= S3_IMAGE_PREVIEW_MAX_BYTES; + + if ((shouldLoadTextPreview || shouldLoadImagePreview) && contentLength > 0) { + const previewBytes = Math.max(1, Math.min(contentLength || S3_PREVIEW_MAX_BYTES, S3_PREVIEW_MAX_BYTES)); + const response = await s3Client.send( + new GetObjectCommand({ + Bucket: bucket, + Key: key, + Range: `bytes=0-${previewBytes - 1}` + }) + ); + const content = Buffer.from(await response.Body.transformToByteArray()); + result.truncated = contentLength > content.length; + + if (shouldLoadImagePreview) { + result.imageDataUrl = `data:${contentType};base64,${content.toString("base64")}`; + } else { + result.previewText = content.toString("utf8"); + } + } + + result.fetchDurationMs = Date.now() - startedAt; + return result; +} + +async function loadServiceHealthSummary() { + const startedAt = Date.now(); + const [sesResult, logsResult, secretsResult, s3Result] = await Promise.allSettled([ + fetchSesMessages(), + loadLogGroups(), + loadSecrets(), + loadS3Buckets() + ]); + + return { + fetchedAt: new Date().toISOString(), + fetchDurationMs: Date.now() - startedAt, + services: { + emails: summarizeHealthResult({ + icon: "✉️", + panel: "emails", + label: "SES Emails", + result: sesResult, + count: sesResult.status === "fulfilled" ? sesResult.value.length : 0, + detail: SES_ENDPOINT, + noun: "email" + }), + logs: summarizeHealthResult({ + icon: "📜", + panel: "logs", + label: "CloudWatch Logs", + result: logsResult, + count: logsResult.status === "fulfilled" ? logsResult.value.length : 0, + detail: `${CLOUDWATCH_ENDPOINT} (${CLOUDWATCH_REGION})`, + noun: "group" + }), + secrets: summarizeHealthResult({ + icon: "🔐", + panel: "secrets", + label: "Secrets Manager", + result: secretsResult, + count: secretsResult.status === "fulfilled" ? secretsResult.value.totalSecrets : 0, + detail: `${SECRETS_ENDPOINT} (${SECRETS_REGION})`, + noun: "secret" + }), + s3: summarizeHealthResult({ + icon: "🪣", + panel: "s3", + label: "S3 Explorer", + result: s3Result, + count: s3Result.status === "fulfilled" ? s3Result.value.totalBuckets : 0, + detail: `${S3_ENDPOINT} (${S3_REGION})`, + noun: "bucket" + }) + } + }; +} + async function findSesMessageById(id) { const messages = await fetchSesMessages(); return messages.find((message, index) => resolveMessageId(message, index) === id) || null; @@ -612,6 +931,116 @@ function buildAttachmentDisposition(filename) { return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename || "attachment")}`; } +function buildInlineDisposition(filename) { + const fallback = String(filename || "file") + .replace(/[^\x20-\x7e]/g, "_") + .replace(/["\\]/g, "_"); + + return `inline; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename || "file")}`; +} + +function basenameFromKey(key) { + const value = String(key || ""); + const parts = value.split("/").filter(Boolean); + return parts[parts.length - 1] || "file"; +} + +function guessObjectContentType(key) { + const normalizedKey = String(key || "").toLowerCase(); + + if (normalizedKey.endsWith(".json")) { + return "application/json"; + } + + if (normalizedKey.endsWith(".csv")) { + return "text/csv"; + } + + if (normalizedKey.endsWith(".html") || normalizedKey.endsWith(".htm")) { + return "text/html"; + } + + if (normalizedKey.endsWith(".txt") || normalizedKey.endsWith(".log") || normalizedKey.endsWith(".md")) { + return "text/plain"; + } + + if (normalizedKey.endsWith(".png")) { + return "image/png"; + } + + if (normalizedKey.endsWith(".jpg") || normalizedKey.endsWith(".jpeg")) { + return "image/jpeg"; + } + + if (normalizedKey.endsWith(".gif")) { + return "image/gif"; + } + + if (normalizedKey.endsWith(".webp")) { + return "image/webp"; + } + + if (normalizedKey.endsWith(".svg")) { + return "image/svg+xml"; + } + + if (normalizedKey.endsWith(".pdf")) { + return "application/pdf"; + } + + return "application/octet-stream"; +} + +function resolveS3PreviewType(contentType, key) { + const normalizedType = String(contentType || "").toLowerCase(); + const normalizedKey = String(key || "").toLowerCase(); + + if (normalizedType.includes("json") || normalizedKey.endsWith(".json")) { + return "json"; + } + + if (normalizedType.startsWith("image/")) { + return "image"; + } + + if (normalizedType.includes("html") || normalizedKey.endsWith(".html") || normalizedKey.endsWith(".htm")) { + return "html"; + } + + if ( + normalizedType.startsWith("text/") || + [".txt", ".log", ".csv", ".xml", ".yml", ".yaml", ".md"].some((extension) => normalizedKey.endsWith(extension)) + ) { + return "text"; + } + + return "binary"; +} + +function summarizeHealthResult({ icon, panel, label, result, count, detail, noun }) { + if (result.status === "fulfilled") { + return { + ok: true, + icon, + panel, + label, + count, + summary: `${count} ${noun}${count === 1 ? "" : "s"}`, + detail + }; + } + + return { + ok: false, + icon, + panel, + label, + count: 0, + summary: "Needs attention", + detail: result.reason?.message || detail + }; +} + function normalizeTimestamp(value) { if (!value) { return ""; @@ -715,6 +1144,9 @@ function getClientConfig() { cloudWatchRegion: CLOUDWATCH_REGION, secretsEndpoint: SECRETS_ENDPOINT, secretsRegion: SECRETS_REGION, + s3Endpoint: S3_ENDPOINT, + s3Region: S3_REGION, + defaultS3Bucket: S3_DEFAULT_BUCKET, defaultLogGroup: CLOUDWATCH_DEFAULT_GROUP, defaultLogWindowMs: CLOUDWATCH_DEFAULT_WINDOW_MS, defaultLogLimit: CLOUDWATCH_DEFAULT_LIMIT @@ -739,12 +1171,15 @@ function renderHtml() {

Inspector

-
- - - +
+ +
- +
+
+ Stack +
+
@@ -836,6 +1271,8 @@ function renderHtml() {
+ +
@@ -902,6 +1339,56 @@ function renderHtml() { + + @@ -911,7 +1398,7 @@ function renderHtml() { function renderStyles() { return ` - :root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--log-shell:linear-gradient(180deg,rgba(239,246,255,.98),rgba(248,251,255,.99));--log-body:#f8fbff;--secret-shell:linear-gradient(180deg,rgba(239,251,246,.98),rgba(247,253,249,.99));--secret-body:#f6fcf8;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--log-line:rgba(48,113,169,.22);--secret-line:rgba(31,143,101,.2);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--info:#3071a9;--info-soft:rgba(48,113,169,.1);--secret:#1f8f65;--secret-soft:rgba(31,143,101,.1);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);--log-shadow:0 16px 32px rgba(48,113,169,.12);--secret-shadow:0 16px 32px rgba(31,143,101,.12);} + :root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--log-shell:linear-gradient(180deg,rgba(239,246,255,.98),rgba(248,251,255,.99));--log-body:#f8fbff;--secret-shell:linear-gradient(180deg,rgba(239,251,246,.98),rgba(247,253,249,.99));--secret-body:#f6fcf8;--bucket-shell:linear-gradient(180deg,rgba(255,249,232,.98),rgba(255,252,243,.99));--bucket-body:#fffcf2;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--log-line:rgba(48,113,169,.22);--secret-line:rgba(31,143,101,.2);--bucket-line:rgba(181,137,37,.22);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--info:#3071a9;--info-soft:rgba(48,113,169,.1);--secret:#1f8f65;--secret-soft:rgba(31,143,101,.1);--bucket:#9d6b00;--bucket-soft:rgba(181,137,37,.12);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);--log-shadow:0 16px 32px rgba(48,113,169,.12);--secret-shadow:0 16px 32px rgba(31,143,101,.12);--bucket-shadow:0 16px 32px rgba(181,137,37,.12);} *{box-sizing:border-box} html,body{margin:0;height:100%;overflow:hidden} body{color-scheme:light;background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.12),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:15px/1.45 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif;transition:background-color .18s ease,color .18s ease} @@ -929,11 +1416,21 @@ function renderStyles() { h1{margin:0;font-size:clamp(1.8rem,3.6vw,2.85rem);line-height:.96;letter-spacing:-.05em} .lede{margin:8px 0 0;max-width:54ch;color:var(--muted);font-size:.92rem} .heroTopRow{display:flex;flex:1 1 360px;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end} + .heroActions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end} + .heroStatusRow{display:flex;flex:1 1 100%;flex-wrap:wrap;gap:8px;align-items:center} + .heroStatusLabel{color:var(--muted);font-size:.72rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase} .helper{margin:0;color:var(--muted);font-size:.89rem} - .workspaceTabs{display:flex;flex-wrap:wrap;gap:6px;padding:4px;border-radius:999px;background:rgba(31,41,51,.05)} - .workspaceTab,.primary,.ghost,.mini,.tab{display:inline-flex;align-items:center;justify-content:center;gap:6px;border-radius:999px;border:1px solid transparent;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} - .workspaceTab{min-height:32px;padding:0 12px;background:transparent;color:var(--muted);font-weight:700} - .workspaceTab.active{background:#fff;border-color:rgba(31,41,51,.08);color:var(--ink)} + .healthStrip{display:flex;flex:1 1 520px;flex-wrap:wrap;gap:6px;align-items:center;min-width:0} + .healthBadge{display:inline-flex;align-items:center;gap:8px;min-height:30px;max-width:100%;padding:0 10px;border-radius:999px;border:1px solid rgba(31,41,51,.1);background:rgba(255,255,255,.78);box-shadow:0 8px 18px rgba(15,23,42,.06);text-align:left;transition:transform .12s ease,background-color .12s ease,border-color .12s ease,box-shadow .12s ease} + .healthBadgeName{display:inline-flex;align-items:center;gap:6px;font-size:.8rem;font-weight:800;white-space:nowrap} + .healthBadgeSummary{min-width:0;overflow:hidden;color:var(--muted);font-size:.78rem;font-weight:700;text-overflow:ellipsis;white-space:nowrap} + .healthBadge.ok{border-color:rgba(31,143,101,.22);background:rgba(31,143,101,.1)} + .healthBadge.bad{border-color:rgba(179,58,58,.22);background:rgba(179,58,58,.1)} + .healthBadge.warn{border-color:rgba(157,95,0,.22);background:rgba(157,95,0,.1)} + .healthBadge.active{border-color:rgba(207,109,60,.28);background:rgba(207,109,60,.16);box-shadow:0 10px 24px rgba(207,109,60,.12)} + .healthBadge.active .healthBadgeName,.healthBadge.active .healthBadgeSummary{color:var(--ink)} + .healthRefreshButton{flex:0 0 auto;padding:0 10px} + .primary,.ghost,.mini,.tab{display:inline-flex;align-items:center;justify-content:center;gap:6px;border-radius:999px;border:1px solid transparent;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} .themeToggle{white-space:nowrap} .workspacePanel{display:grid;grid-template-rows:auto auto minmax(0,1fr);gap:6px;min-height:0} .workspacePanel[hidden]{display:none} @@ -956,6 +1453,7 @@ function renderStyles() { .chip input{margin:0;accent-color:var(--accent)} .chip select{border:none;background:transparent;outline:none;color:var(--ink)} .search{flex:1 1 260px;min-height:36px;padding:0 12px;border-radius:12px;border:1px solid var(--line);background:rgba(255,255,255,.82);color:var(--ink);outline:none} + .searchCompact{flex:1 1 220px} .status{display:inline-flex;align-items:center;min-height:32px;padding:0 11px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);color:var(--muted);font-size:.86rem;font-weight:600} .status.ok{color:var(--ok);border-color:rgba(31,143,101,.22);background:rgba(31,143,101,.08)} .status.warn{color:var(--warn);border-color:rgba(157,95,0,.22);background:rgba(157,95,0,.08)} @@ -999,21 +1497,32 @@ function renderStyles() { .panel{overflow:hidden;border-radius:12px;border:1px solid rgba(207,109,60,.14);background:#fff} .logEvent{width:100%;overflow:hidden;border-radius:16px;border:1px solid var(--log-line);background:var(--log-shell);box-shadow:var(--log-shadow)} .secretCard{background:var(--secret-shell);border:1px solid var(--secret-line);box-shadow:var(--secret-shadow)} + .s3Card{background:var(--bucket-shell);border:1px solid var(--bucket-line);box-shadow:var(--bucket-shadow)} .logSummary{list-style:none;display:grid;gap:7px;padding:10px 12px;cursor:pointer} .logSummary::-webkit-details-marker{display:none} .secretSummary{background:linear-gradient(180deg,rgba(244,253,248,.9),rgba(236,249,242,.96))} + .s3Summary{background:linear-gradient(180deg,rgba(255,251,238,.92),rgba(255,246,223,.98))} .logSummaryTop{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:center} .logMeta{display:flex;flex-wrap:wrap;gap:8px;align-items:center} .logSummaryActions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end} .logTag{background:var(--info-soft);color:var(--info);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .secretTag{background:var(--secret-soft);color:var(--secret);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .bucketTag{background:var(--bucket-soft);color:var(--bucket);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .logPreview{margin:0;color:#324150;font:600 .88rem/1.45 "Cascadia Code","Consolas",monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .logBody{padding:8px 12px 12px;border-top:1px solid rgba(48,113,169,.14);background:var(--log-body)} .secretBody{border-top-color:rgba(31,143,101,.14);background:var(--secret-body)} + .s3Body{border-top-color:rgba(181,137,37,.18);background:var(--bucket-body)} .logCopyButton{box-shadow:none} .logBody pre{border-radius:12px;border:1px solid rgba(48,113,169,.14);padding:12px;background:linear-gradient(180deg,rgba(48,113,169,.04),transparent 140px),#fff} .secretValuePanel{display:grid;gap:10px} .secretValuePanel pre{border-radius:12px;border:1px solid rgba(31,143,101,.14);padding:12px;background:linear-gradient(180deg,rgba(31,143,101,.04),transparent 140px),#fff} + .s3PreviewPanel{display:grid;gap:10px} + .s3PreviewImage{max-width:min(100%,640px);border-radius:12px;border:1px solid rgba(181,137,37,.16);background:#fff} + .logBody.wrapOff pre{white-space:pre;word-break:normal} + .tag.levelError{background:rgba(179,58,58,.12);color:var(--bad)} + .tag.levelWarn{background:rgba(157,95,0,.12);color:var(--warn)} + .tag.levelInfo{background:rgba(48,113,169,.12);color:var(--info)} + .tag.levelDebug{background:rgba(96,112,128,.12);color:var(--muted)} .jsonSyntax .jsonKey{color:#b55f2d} .jsonSyntax .jsonString{color:#1f8f65} .jsonSyntax .jsonNumber{color:#2f6ea9} @@ -1030,10 +1539,12 @@ function renderStyles() { body[data-theme="dark"] .card{background:linear-gradient(180deg,rgba(50,35,28,.96),rgba(31,25,22,.98));border-color:rgba(207,109,60,.24);box-shadow:0 18px 34px rgba(0,0,0,.34)} body[data-theme="dark"] .logEvent{background:linear-gradient(180deg,rgba(18,31,45,.96),rgba(14,24,36,.98));border-color:rgba(73,144,204,.22);box-shadow:0 16px 32px rgba(0,0,0,.34)} body[data-theme="dark"] .secretCard{background:linear-gradient(180deg,rgba(19,39,31,.96),rgba(14,30,24,.98));border-color:rgba(64,170,126,.22);box-shadow:0 16px 32px rgba(0,0,0,.34)} - body[data-theme="dark"] .workspaceTabs{background:rgba(148,163,184,.12)} - body[data-theme="dark"] .workspaceTab, + body[data-theme="dark"] .s3Card{background:linear-gradient(180deg,rgba(52,42,17,.96),rgba(37,30,13,.98));border-color:rgba(181,137,37,.24);box-shadow:0 16px 32px rgba(0,0,0,.34)} + body[data-theme="dark"] .healthBadge{background:rgba(15,21,30,.84);border-color:rgba(148,163,184,.18);box-shadow:0 10px 22px rgba(0,0,0,.28)} + body[data-theme="dark"] .healthBadge.active{border-color:rgba(207,109,60,.32);background:rgba(207,109,60,.18);box-shadow:0 12px 26px rgba(0,0,0,.3)} + body[data-theme="dark"] .healthBadge.active .healthBadgeName, + body[data-theme="dark"] .healthBadge.active .healthBadgeSummary{color:#f8ede6} body[data-theme="dark"] .tab{color:#aab8c8} - body[data-theme="dark"] .workspaceTab.active, body[data-theme="dark"] .tab.active, body[data-theme="dark"] .ghost, body[data-theme="dark"] .mini, @@ -1044,7 +1555,6 @@ function renderStyles() { body[data-theme="dark"] .search::placeholder{color:#9fb0c2} body[data-theme="dark"] .ghost, body[data-theme="dark"] .mini, - body[data-theme="dark"] .workspaceTab.active, body[data-theme="dark"] .tab.active{border-color:rgba(148,163,184,.18)} body[data-theme="dark"] .summary{background:linear-gradient(180deg,rgba(58,40,31,.88),rgba(45,33,28,.96))} body[data-theme="dark"] .body{background:#211a17;border-top-color:rgba(207,109,60,.18)} @@ -1052,6 +1562,8 @@ function renderStyles() { body[data-theme="dark"] .logBody{background:#13212d;border-top-color:rgba(73,144,204,.18)} body[data-theme="dark"] .secretSummary{background:linear-gradient(180deg,rgba(21,43,34,.94),rgba(16,34,27,.98))} body[data-theme="dark"] .secretBody{background:#12241c;border-top-color:rgba(64,170,126,.18)} + body[data-theme="dark"] .s3Summary{background:linear-gradient(180deg,rgba(53,41,19,.94),rgba(39,31,15,.98))} + body[data-theme="dark"] .s3Body{background:#241d10;border-top-color:rgba(181,137,37,.18)} body[data-theme="dark"] .metaCard{background:rgba(17,25,35,.64);border-color:rgba(148,163,184,.14)} body[data-theme="dark"] .attachment{background:rgba(50,35,28,.9);border-color:rgba(207,109,60,.18)} body[data-theme="dark"] .attachmentLink{color:#f6c4a9} @@ -1060,6 +1572,7 @@ function renderStyles() { body[data-theme="dark"] pre, body[data-theme="dark"] .logBody pre{background:linear-gradient(180deg,rgba(73,144,204,.06),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} body[data-theme="dark"] .secretValuePanel pre{background:linear-gradient(180deg,rgba(64,170,126,.08),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} + body[data-theme="dark"] .s3PreviewPanel pre{background:linear-gradient(180deg,rgba(181,137,37,.08),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} body[data-theme="dark"] .panel{border-color:rgba(148,163,184,.14)} body[data-theme="dark"] .banner, body[data-theme="dark"] .empty{background:rgba(15,21,30,.82);border-color:rgba(148,163,184,.16)} @@ -1067,6 +1580,7 @@ function renderStyles() { body[data-theme="dark"] .tag{background:rgba(207,109,60,.14);color:#f0c2aa} body[data-theme="dark"] .logTag{background:rgba(73,144,204,.16);color:#93cfff} body[data-theme="dark"] .secretTag{background:rgba(64,170,126,.16);color:#9fe0be} + body[data-theme="dark"] .bucketTag{background:rgba(181,137,37,.16);color:#f1d38c} body[data-theme="dark"] .preview, body[data-theme="dark"] .logPreview, body[data-theme="dark"] .metaCard dd, @@ -1084,12 +1598,11 @@ function renderStyles() { body[data-theme="dark"] .stat small, body[data-theme="dark"] .stat span, body[data-theme="dark"] .chip, - body[data-theme="dark"] .workspaceTab, body[data-theme="dark"] .tab{color:#aab8c8} body[data-theme="dark"] .paneTopButton{border-color:rgba(255,255,255,.18);background:rgba(8,12,18,.58);color:#edf2f7} body[data-theme="dark"] .paneTopButton.visible:hover{background:rgba(8,12,18,.8)} @media (max-width:1080px){.stats{grid-template-columns:repeat(2,minmax(0,1fr))}} - @media (max-width:720px){.page{padding:12px}.heroShell,.heroTopRow,.toolbar,.row{align-items:stretch}.stats{grid-template-columns:1fr}.heroTopRow{justify-content:stretch;flex-basis:100%}.workspaceTab,.primary,.ghost,.chip,.themeToggle{width:100%;justify-content:center}.logSummaryTop,.logSummaryActions{align-items:flex-start}.contentPane{min-height:300px}iframe{min-height:420px}} + @media (max-width:720px){.page{padding:12px}.heroShell,.heroTopRow,.toolbar,.row,.heroActions{align-items:stretch}.heroTopRow{justify-content:stretch;flex-basis:100%}.heroStatusRow{align-items:flex-start}.heroStatusLabel,.healthStrip{flex-basis:100%}.primary,.ghost,.chip,.themeToggle{width:100%;justify-content:center}.healthBadge{justify-content:flex-start}.logSummaryTop,.logSummaryActions{align-items:flex-start}.contentPane{min-height:300px}iframe{min-height:420px}} `; } @@ -1108,17 +1621,21 @@ function clientApp(config) { const EMAIL_PREFERENCES_STORAGE_KEY = "localstack-inspector-email-preferences"; const LOG_PREFERENCES_STORAGE_KEY = "localstack-inspector-log-preferences"; const SECRET_PREFERENCES_STORAGE_KEY = "localstack-inspector-secret-preferences"; + const S3_PREFERENCES_STORAGE_KEY = "localstack-inspector-s3-preferences"; + const HEALTH_REFRESH_MS = 30000; const REFRESH_INTERVALS = [5000, 10000, 15000, 30000, 60000]; const LOG_WINDOWS = [300000, 900000, 3600000, 21600000, 86400000]; const LOG_LIMITS = [100, 200, 300, 500]; const storedEmailPreferences = getStoredPreferences(EMAIL_PREFERENCES_STORAGE_KEY); const storedLogPreferences = getStoredPreferences(LOG_PREFERENCES_STORAGE_KEY); const storedSecretPreferences = getStoredPreferences(SECRET_PREFERENCES_STORAGE_KEY); + const storedS3Preferences = getStoredPreferences(S3_PREFERENCES_STORAGE_KEY); const appState = { panel: getInitialPanel(), logsReady: false, secretsReady: false, + s3Ready: false, theme: getInitialTheme() }; @@ -1155,6 +1672,8 @@ function clientApp(config) { interval: getStoredNumber(storedLogPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs), windowMs: getStoredNumber(storedLogPreferences?.windowMs, LOG_WINDOWS, config.defaultLogWindowMs), limit: getStoredNumber(storedLogPreferences?.limit, LOG_LIMITS, config.defaultLogLimit), + wrapLines: getStoredBoolean(storedLogPreferences?.wrapLines, true), + tailNewest: getStoredBoolean(storedLogPreferences?.tailNewest, false), loading: false, error: "", updatedAt: 0, @@ -1180,15 +1699,47 @@ function clientApp(config) { newest: "", openIds: new Set(), values: {}, + revealedIds: new Set(), listSignature: "" }; + const s3State = { + buckets: [], + objects: [], + filtered: [], + bucket: getStoredText(storedS3Preferences?.bucket, config.defaultS3Bucket || ""), + prefix: getStoredText(storedS3Preferences?.prefix), + search: getStoredText(storedS3Preferences?.search), + auto: getStoredBoolean(storedS3Preferences?.auto, true), + interval: getStoredNumber(storedS3Preferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs), + loading: false, + error: "", + updatedAt: 0, + source: "initial", + duration: 0, + newest: "", + openIds: new Set(), + previews: {}, + listSignature: "" + }; + + const healthState = { + services: {}, + loading: false, + error: "", + updatedAt: 0, + source: "initial" + }; + const el = { - workspaceTabs: document.getElementById("workspaceTabs"), themeToggle: document.getElementById("themeToggle"), + resetStateButton: document.getElementById("resetStateButton"), + healthRefreshButton: document.getElementById("healthRefreshButton"), + healthStrip: document.getElementById("healthStrip"), emailsPanel: document.getElementById("emailsPanel"), logsPanel: document.getElementById("logsPanel"), secretsPanel: document.getElementById("secretsPanel"), + s3Panel: document.getElementById("s3Panel"), refreshButton: document.getElementById("refreshButton"), autoToggle: document.getElementById("autoToggle"), intervalSelect: document.getElementById("intervalSelect"), @@ -1218,6 +1769,8 @@ function clientApp(config) { logsLimitSelect: document.getElementById("logsLimitSelect"), logsSearchInput: document.getElementById("logsSearchInput"), logsClearSearchButton: document.getElementById("logsClearSearchButton"), + logsWrapToggle: document.getElementById("logsWrapToggle"), + logsTailToggle: document.getElementById("logsTailToggle"), logsExpandAllButton: document.getElementById("logsExpandAllButton"), logsCollapseAllButton: document.getElementById("logsCollapseAllButton"), logsScrollToTopButton: document.getElementById("logsScrollToTopButton"), @@ -1252,7 +1805,30 @@ function clientApp(config) { secretsBanner: document.getElementById("secretsBanner"), secretsEmpty: document.getElementById("secretsEmpty"), secretsList: document.getElementById("secretsList"), - secretsContentPane: document.getElementById("secretsContentPane") + secretsContentPane: document.getElementById("secretsContentPane"), + s3RefreshButton: document.getElementById("s3RefreshButton"), + s3AutoToggle: document.getElementById("s3AutoToggle"), + s3IntervalSelect: document.getElementById("s3IntervalSelect"), + s3BucketSelect: document.getElementById("s3BucketSelect"), + s3PrefixInput: document.getElementById("s3PrefixInput"), + s3ApplyPrefixButton: document.getElementById("s3ApplyPrefixButton"), + s3SearchInput: document.getElementById("s3SearchInput"), + s3ClearSearchButton: document.getElementById("s3ClearSearchButton"), + s3ExpandAllButton: document.getElementById("s3ExpandAllButton"), + s3CollapseAllButton: document.getElementById("s3CollapseAllButton"), + s3ScrollToTopButton: document.getElementById("s3ScrollToTopButton"), + s3StatusChip: document.getElementById("s3StatusChip"), + s3TotalStat: document.getElementById("s3TotalStat"), + s3VisibleStat: document.getElementById("s3VisibleStat"), + s3BucketsStat: document.getElementById("s3BucketsStat"), + s3NewestStat: document.getElementById("s3NewestStat"), + s3UpdatedStat: document.getElementById("s3UpdatedStat"), + s3FetchStat: document.getElementById("s3FetchStat"), + s3FetchDetail: document.getElementById("s3FetchDetail"), + s3Banner: document.getElementById("s3Banner"), + s3Empty: document.getElementById("s3Empty"), + s3List: document.getElementById("s3List"), + s3ContentPane: document.getElementById("s3ContentPane") }; el.autoToggle.checked = state.auto; @@ -1263,45 +1839,72 @@ function clientApp(config) { el.logsWindowSelect.value = String(logsState.windowMs); el.logsLimitSelect.value = String(logsState.limit); el.logsSearchInput.value = logsState.search; + el.logsWrapToggle.checked = logsState.wrapLines; + el.logsTailToggle.checked = logsState.tailNewest; el.secretsAutoToggle.checked = secretsState.auto; el.secretsIntervalSelect.value = String(secretsState.interval); el.secretsSearchInput.value = secretsState.search; + el.s3AutoToggle.checked = s3State.auto; + el.s3IntervalSelect.value = String(s3State.interval); + el.s3PrefixInput.value = s3State.prefix; + el.s3SearchInput.value = s3State.search; applyTheme(appState.theme); persistPanel(); persistEmailPreferences(); persistLogPreferences(); persistSecretPreferences(); + persistS3Preferences(); wire(); renderWorkspace(); renderAll(); renderLogsAll(); renderSecretsAll(); + renderS3All(); + renderHealthStrip(); if (appState.panel === "logs") { ensureLogsReady(); } else if (appState.panel === "secrets") { ensureSecretsReady(); + } else if (appState.panel === "s3") { + ensureS3Ready(); } else { refreshMessages("initial"); } + refreshHealthSummary("initial"); + window.setInterval(() => { + if (!document.hidden) { + refreshHealthSummary("auto"); + } + }, HEALTH_REFRESH_MS); window.setInterval(() => { renderLiveClock(); renderLogsLiveClock(); renderSecretsLiveClock(); + renderS3LiveClock(); + renderHealthStrip(); }, 1000); function wire() { - el.workspaceTabs.addEventListener("click", async (event) => { - const button = event.target.closest("button[data-panel]"); + el.themeToggle.addEventListener("click", () => { + applyTheme(appState.theme === "dark" ? "light" : "dark"); + }); + + el.resetStateButton.addEventListener("click", () => { + resetSavedState(); + }); + + el.healthRefreshButton.addEventListener("click", () => { + refreshHealthSummary("manual"); + }); + + el.healthStrip.addEventListener("click", async (event) => { + const button = event.target.closest("button[data-health-panel]"); if (!button) { return; } - await setPanel(button.dataset.panel); - }); - - el.themeToggle.addEventListener("click", () => { - applyTheme(appState.theme === "dark" ? "light" : "dark"); + await setPanel(button.dataset.healthPanel); }); el.refreshButton.addEventListener("click", () => refreshMessages("manual")); @@ -1424,6 +2027,17 @@ function clientApp(config) { applyLogsFilter(); }); + el.logsWrapToggle.addEventListener("change", () => { + logsState.wrapLines = el.logsWrapToggle.checked; + persistLogPreferences(); + renderLogsList(); + }); + + el.logsTailToggle.addEventListener("change", () => { + logsState.tailNewest = el.logsTailToggle.checked; + persistLogPreferences(); + }); + el.logsExpandAllButton.addEventListener("click", () => { logsState.filtered.forEach((event) => logsState.openIds.add(event.id)); renderLogsList(); @@ -1554,6 +2168,139 @@ function clientApp(config) { await copyText(entry.copyValue); setSecretsStatus("Secret value copied to the clipboard.", "ok"); } + + return; + } + + if (button.dataset.secretAction === "toggle-reveal") { + toggleSecretReveal(id); + return; + } + + if (button.dataset.secretAction === "copy-name") { + await copyText(secret.name || ""); + setSecretsStatus("Secret name copied to the clipboard.", "ok"); + return; + } + + if (button.dataset.secretAction === "copy-arn") { + await copyText(secret.arn || ""); + setSecretsStatus("Secret ARN copied to the clipboard.", "ok"); + return; + } + + if (button.dataset.secretAction === "copy-env") { + const entry = await ensureSecretValue(id, { force: false }); + + if (entry?.status === "loaded") { + await copyText(`${secret.name}=${entry.copyValue}`); + setSecretsStatus("Secret env line copied to the clipboard.", "ok"); + } + } + }); + + el.s3RefreshButton.addEventListener("click", async () => { + await refreshS3Buckets(); + await refreshS3("manual"); + }); + + el.s3AutoToggle.addEventListener("change", () => { + s3State.auto = el.s3AutoToggle.checked; + persistS3Preferences(); + scheduleS3Refresh(); + renderS3Status(); + }); + + el.s3IntervalSelect.addEventListener("change", () => { + s3State.interval = Number(el.s3IntervalSelect.value) || config.defaultRefreshMs; + persistS3Preferences(); + scheduleS3Refresh(); + renderS3Status(); + }); + + el.s3BucketSelect.addEventListener("change", () => { + s3State.bucket = el.s3BucketSelect.value; + persistS3Preferences(); + refreshS3("bucket"); + }); + + el.s3PrefixInput.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + s3State.prefix = el.s3PrefixInput.value.trim(); + persistS3Preferences(); + refreshS3("prefix"); + } + }); + + el.s3ApplyPrefixButton.addEventListener("click", () => { + s3State.prefix = el.s3PrefixInput.value.trim(); + persistS3Preferences(); + refreshS3("prefix"); + }); + + el.s3SearchInput.addEventListener("input", (event) => { + s3State.search = event.target.value; + applyS3Filter(); + }); + + el.s3ClearSearchButton.addEventListener("click", () => { + s3State.search = ""; + el.s3SearchInput.value = ""; + applyS3Filter(); + }); + + el.s3ExpandAllButton.addEventListener("click", () => { + s3State.filtered.forEach((object) => s3State.openIds.add(object.id)); + syncS3Expansion(); + }); + + el.s3CollapseAllButton.addEventListener("click", () => { + s3State.openIds.clear(); + syncS3Expansion(); + }); + + el.s3ScrollToTopButton.addEventListener("click", () => { + scrollPaneToTop(el.s3ContentPane); + }); + + el.s3ContentPane.addEventListener("scroll", () => { + updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); + }); + + el.s3List.addEventListener("click", async (event) => { + const button = event.target.closest("button[data-s3-action]"); + + if (!button) { + return; + } + + const id = button.dataset.id; + const object = getS3Object(id); + + if (!object) { + return; + } + + if (button.dataset.s3Action === "load-preview") { + await ensureS3Preview(id, { force: false }); + return; + } + + if (button.dataset.s3Action === "reload-preview") { + await ensureS3Preview(id, { force: true }); + return; + } + + if (button.dataset.s3Action === "copy-key") { + await copyText(object.key); + setS3Status("Object key copied to the clipboard.", "ok"); + return; + } + + if (button.dataset.s3Action === "copy-uri") { + await copyText(`s3://${object.bucket}/${object.key}`); + setS3Status("S3 URI copied to the clipboard.", "ok"); } }); @@ -1561,11 +2308,24 @@ function clientApp(config) { window.clearTimeout(state.timer); window.clearTimeout(logsState.timer); window.clearTimeout(secretsState.timer); + window.clearTimeout(s3State.timer); if (document.hidden) { renderStatus(); renderLogsStatus(); renderSecretsStatus(); + renderS3Status(); + return; + } + + refreshHealthSummary("visibility"); + + if (appState.panel === "s3") { + if (s3State.auto) { + refreshS3("visibility"); + } else { + renderS3Status(); + } return; } @@ -1612,6 +2372,11 @@ function clientApp(config) { return; } + if (appState.panel === "s3") { + refreshS3("keyboard"); + return; + } + refreshMessages("keyboard"); } }); @@ -1619,6 +2384,7 @@ function clientApp(config) { updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); + updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); } async function setPanel(panel) { @@ -1627,6 +2393,8 @@ function clientApp(config) { await ensureLogsReady(); } else if (panel === "secrets") { await ensureSecretsReady(); + } else if (panel === "s3") { + await ensureS3Ready(); } renderWorkspace(); @@ -1641,6 +2409,8 @@ function clientApp(config) { await ensureLogsReady(); } else if (panel === "secrets") { await ensureSecretsReady(); + } else if (panel === "s3") { + await ensureS3Ready(); } else if (!state.updatedAt && !state.loading) { await refreshMessages("panel"); } @@ -1648,21 +2418,19 @@ function clientApp(config) { scheduleRefresh(); scheduleLogsRefresh(); scheduleSecretsRefresh(); + scheduleS3Refresh(); renderStatus(); renderLogsStatus(); renderSecretsStatus(); + renderS3Status(); } function renderWorkspace() { el.emailsPanel.hidden = appState.panel !== "emails"; el.logsPanel.hidden = appState.panel !== "logs"; el.secretsPanel.hidden = appState.panel !== "secrets"; - - el.workspaceTabs.querySelectorAll("button[data-panel]").forEach((button) => { - const active = button.dataset.panel === appState.panel; - button.classList.toggle("active", active); - button.setAttribute("aria-pressed", active ? "true" : "false"); - }); + el.s3Panel.hidden = appState.panel !== "s3"; + renderHealthStrip(); } async function ensureLogsReady() { @@ -1687,6 +2455,19 @@ function clientApp(config) { appState.secretsReady = !secretsState.error; } + async function ensureS3Ready() { + if (appState.s3Ready) { + return; + } + + await refreshS3Buckets(); + appState.s3Ready = !s3State.error; + + if (s3State.bucket) { + await refreshS3("initial"); + } + } + async function refreshMessages(source) { if (state.loading) { return; @@ -1868,6 +2649,10 @@ function clientApp(config) { logsState.loading = false; scheduleLogsRefresh(); renderLogsAll({ renderList: shouldRenderList }); + + if (shouldRenderList && logsState.tailNewest) { + scrollPaneToTop(el.logsContentPane); + } } } @@ -1917,6 +2702,132 @@ function clientApp(config) { } } + async function refreshHealthSummary(source) { + if (healthState.loading && source === "auto") { + return; + } + + healthState.loading = true; + healthState.source = source; + renderHealthStrip(); + + try { + const response = await fetch("/api/service-health", { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + healthState.services = payload.services || {}; + healthState.updatedAt = Date.now(); + healthState.error = ""; + } catch (error) { + healthState.error = error.message || "Unknown health refresh error"; + } finally { + healthState.loading = false; + renderHealthStrip(); + } + } + + async function refreshS3Buckets() { + try { + const response = await fetch("/api/s3/buckets", { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + s3State.buckets = Array.isArray(payload.buckets) ? payload.buckets : []; + + const availableBuckets = s3State.buckets.map((bucket) => bucket.name).filter(Boolean); + + if (!availableBuckets.includes(s3State.bucket)) { + s3State.bucket = availableBuckets.includes(config.defaultS3Bucket) + ? config.defaultS3Bucket + : availableBuckets[0] || ""; + } + + s3State.error = ""; + } catch (error) { + s3State.buckets = []; + s3State.bucket = ""; + s3State.error = error.message || "Unknown S3 bucket refresh error"; + } finally { + appState.s3Ready = !s3State.error; + persistS3Preferences(); + renderS3All(); + } + } + + async function refreshS3(source) { + if (s3State.loading) { + return; + } + + if (!appState.s3Ready) { + await ensureS3Ready(); + return; + } + + if (!s3State.bucket) { + s3State.error = s3State.buckets.length ? "Choose a bucket to load objects." : "No S3 buckets found."; + renderS3All(); + return; + } + + let shouldRenderList = false; + + s3State.loading = true; + s3State.source = source; + s3State.error = ""; + renderS3Status(); + renderS3Fetch(); + + try { + const params = new URLSearchParams({ + bucket: s3State.bucket + }); + + if (s3State.prefix) { + params.set("prefix", s3State.prefix); + } + + const response = await fetch(`/api/s3/objects?${params.toString()}`, { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + const objects = Array.isArray(payload.objects) ? payload.objects : []; + const nextSignature = computeS3ListSignature(objects); + shouldRenderList = nextSignature !== s3State.listSignature; + s3State.objects = objects; + s3State.duration = payload.fetchDurationMs || 0; + s3State.newest = payload.latestTimestamp || ""; + s3State.updatedAt = Date.now(); + s3State.listSignature = nextSignature; + + pruneS3State(); + applyS3Filter(shouldRenderList); + appState.s3Ready = true; + setS3Status(`Updated ${objects.length} object${objects.length === 1 ? "" : "s"}.`, "ok"); + } catch (error) { + s3State.error = error.message || "Unknown S3 refresh error"; + appState.s3Ready = false; + setS3Status(`Refresh failed: ${s3State.error}`, "bad"); + } finally { + s3State.loading = false; + scheduleS3Refresh(); + renderS3All({ renderList: shouldRenderList }); + } + } + function applyLogsFilter(shouldRenderList = true) { const search = logsState.search.trim().toLowerCase(); logsState.filtered = !search @@ -1935,6 +2846,15 @@ function clientApp(config) { renderSecretsAll({ renderList: shouldRenderList }); } + function applyS3Filter(shouldRenderList = true) { + const search = s3State.search.trim().toLowerCase(); + s3State.filtered = !search + ? [...s3State.objects] + : s3State.objects.filter((object) => s3Haystack(object).includes(search)); + persistS3Preferences(); + renderS3All({ renderList: shouldRenderList }); + } + function pruneState() { const ids = new Set(state.messages.map((message) => message.id)); state.openIds = new Set([...state.openIds].filter((id) => ids.has(id))); @@ -1967,6 +2887,19 @@ function clientApp(config) { delete secretsState.values[id]; } }); + + secretsState.revealedIds = new Set([...secretsState.revealedIds].filter((id) => ids.has(id))); + } + + function pruneS3State() { + const ids = new Set(s3State.objects.map((object) => object.id)); + s3State.openIds = new Set([...s3State.openIds].filter((id) => ids.has(id))); + + Object.keys(s3State.previews).forEach((id) => { + if (!ids.has(id)) { + delete s3State.previews[id]; + } + }); } function applyFilter(shouldRenderList = true) { @@ -2024,6 +2957,14 @@ function clientApp(config) { .join("|"); } + function computeS3ListSignature(objects) { + return objects + .map((object) => + [object.id, object.key || "", object.size || 0, object.lastModified || "", object.etag || ""].join("::") + ) + .join("|"); + } + function haystack(message) { return [ message.subject, @@ -2058,6 +2999,10 @@ function clientApp(config) { .toLowerCase(); } + function s3Haystack(object) { + return [object.bucket, object.key, object.storageClass, object.etag].filter(Boolean).join(" ").toLowerCase(); + } + function scheduleRefresh() { window.clearTimeout(state.timer); @@ -2088,6 +3033,16 @@ function clientApp(config) { secretsState.timer = window.setTimeout(() => refreshSecrets("auto"), secretsState.interval); } + function scheduleS3Refresh() { + window.clearTimeout(s3State.timer); + + if (appState.panel !== "s3" || !s3State.auto || document.hidden || s3State.loading) { + return; + } + + s3State.timer = window.setTimeout(() => refreshS3("auto"), s3State.interval); + } + function renderAll(options = {}) { const { renderList: shouldRenderList = true } = options; renderStats(); @@ -2122,6 +3077,50 @@ function clientApp(config) { renderSecretsLiveClock(); } + function renderS3All(options = {}) { + const { renderList: shouldRenderList = true } = options; + renderS3Filters(); + renderS3Stats(); + renderS3Fetch(); + renderS3Status(); + if (shouldRenderList) { + renderS3List(); + } + renderS3LiveClock(); + } + + function renderHealthStrip() { + const serviceOrder = [ + { key: "emails", panel: "emails", icon: "✉️", label: "SES Emails", shortLabel: "SES" }, + { key: "logs", panel: "logs", icon: "📜", label: "CloudWatch Logs", shortLabel: "Logs" }, + { key: "secrets", panel: "secrets", icon: "🔐", label: "Secrets Manager", shortLabel: "Secrets" }, + { key: "s3", panel: "s3", icon: "🪣", label: "S3 Explorer", shortLabel: "S3" } + ]; + + el.healthStrip.innerHTML = serviceOrder + .map((service) => { + const entry = healthState.services?.[service.key]; + const toneClass = healthState.loading && !entry ? "warn" : entry?.ok ? "ok" : entry ? "bad" : ""; + const activeClass = service.panel === appState.panel ? "active" : ""; + const className = ["healthBadge", toneClass, activeClass].filter(Boolean).join(" "); + const summary = entry?.summary || (healthState.loading ? "Checking..." : "Waiting"); + const detail = entry?.detail || (healthState.error ? healthState.error : "Not checked yet"); + const updatedMeta = healthState.updatedAt + ? `Updated ${formatRelative(healthState.updatedAt)} via ${healthState.source}` + : ""; + const titleParts = [`${service.label}: ${detail}`]; + + if (updatedMeta) { + titleParts.push(updatedMeta); + } + + return ``; + }) + .join(""); + } + function renderStats() { el.totalStat.textContent = String(state.messages.length); el.visibleStat.textContent = `${state.filtered.length} visible${state.search ? " after search" : ""}`; @@ -2148,6 +3147,25 @@ function clientApp(config) { } el.logsStreamSelect.value = logsState.stream; + el.logsWrapToggle.checked = logsState.wrapLines; + el.logsTailToggle.checked = logsState.tailNewest; + } + + function renderS3Filters() { + const bucketOptions = s3State.buckets.length + ? s3State.buckets.map( + (bucket) => `` + ) + : ['']; + + el.s3BucketSelect.innerHTML = bucketOptions.join(""); + + if (s3State.bucket) { + el.s3BucketSelect.value = s3State.bucket; + } + + el.s3PrefixInput.value = s3State.prefix; + el.s3SearchInput.value = s3State.search; } function renderLogsStats() { @@ -2166,6 +3184,13 @@ function clientApp(config) { el.secretsNewestStat.textContent = secretsState.newest ? formatDateTime(secretsState.newest) : "No secrets"; } + function renderS3Stats() { + el.s3TotalStat.textContent = String(s3State.objects.length); + el.s3VisibleStat.textContent = `${s3State.filtered.length} visible${s3State.search ? " after search" : ""}`; + el.s3BucketsStat.textContent = String(s3State.buckets.length); + el.s3NewestStat.textContent = s3State.newest ? formatDateTime(s3State.newest) : "No objects"; + } + function renderFetch() { if (state.loading) { el.fetchStat.textContent = "Refreshing..."; @@ -2235,6 +3260,29 @@ function clientApp(config) { el.secretsFetchDetail.textContent = `Endpoint: ${config.secretsEndpoint} (${config.secretsRegion})`; } + function renderS3Fetch() { + if (s3State.loading) { + el.s3FetchStat.textContent = "Refreshing..."; + el.s3FetchDetail.textContent = `Endpoint: ${config.s3Endpoint} (${config.s3Region})`; + return; + } + + if (s3State.error) { + el.s3FetchStat.textContent = "Needs attention"; + el.s3FetchDetail.textContent = s3State.error; + return; + } + + if (!s3State.updatedAt) { + el.s3FetchStat.textContent = "Idle"; + el.s3FetchDetail.textContent = `Endpoint: ${config.s3Endpoint} (${config.s3Region})`; + return; + } + + el.s3FetchStat.textContent = `${s3State.duration}ms`; + el.s3FetchDetail.textContent = `${s3State.bucket || "No bucket selected"}. Endpoint: ${config.s3Endpoint}`; + } + function renderStatus() { el.statusChip.className = "status"; @@ -2343,6 +3391,42 @@ function clientApp(config) { el.secretsStatusChip.textContent = `Live refresh on, next check in ${seconds}s`; } + function renderS3Status() { + el.s3StatusChip.className = "status"; + + if (s3State.loading) { + el.s3StatusChip.classList.add("warn"); + el.s3StatusChip.textContent = "Refreshing objects..."; + return; + } + + if (s3State.error) { + el.s3StatusChip.classList.add("bad"); + el.s3StatusChip.textContent = `Refresh failed: ${s3State.error}`; + return; + } + + if (!s3State.auto) { + el.s3StatusChip.textContent = "Live refresh paused"; + return; + } + + if (document.hidden) { + el.s3StatusChip.classList.add("warn"); + el.s3StatusChip.textContent = "Tab hidden, live refresh paused"; + return; + } + + if (!s3State.updatedAt) { + el.s3StatusChip.textContent = "Waiting for first refresh..."; + return; + } + + const seconds = Math.max(0, Math.ceil((s3State.updatedAt + s3State.interval - Date.now()) / 1000)); + el.s3StatusChip.classList.add("ok"); + el.s3StatusChip.textContent = `Live refresh on, next check in ${seconds}s`; + } + function renderLiveClock() { if (!state.updatedAt) { el.updatedStat.textContent = "Not refreshed yet"; @@ -2373,6 +3457,16 @@ function clientApp(config) { renderSecretsStatus(); } + function renderS3LiveClock() { + if (!s3State.updatedAt) { + el.s3UpdatedStat.textContent = "Not refreshed yet"; + return; + } + + el.s3UpdatedStat.textContent = `Updated ${formatRelative(s3State.updatedAt)} via ${s3State.source}`; + renderS3Status(); + } + function renderList() { el.banner.hidden = !state.error; el.banner.textContent = state.error ? `Refresh failed: ${state.error}` : ""; @@ -2443,7 +3537,39 @@ function clientApp(config) { updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); } + function renderS3List() { + el.s3Banner.hidden = !s3State.error; + el.s3Banner.textContent = s3State.error ? `Refresh failed: ${s3State.error}` : ""; + + if (!s3State.bucket && !s3State.buckets.length) { + el.s3List.innerHTML = ""; + el.s3Empty.hidden = false; + el.s3Empty.textContent = "No S3 buckets found in LocalStack yet."; + updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); + return; + } + + if (!s3State.filtered.length) { + el.s3List.innerHTML = ""; + el.s3Empty.hidden = false; + el.s3Empty.textContent = s3State.objects.length + ? "No S3 objects match the current search." + : "No S3 objects found for the selected bucket and prefix."; + updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); + return; + } + + el.s3Empty.hidden = true; + el.s3List.innerHTML = s3State.filtered.map((object) => renderS3ObjectCard(object)).join(""); + bindS3Toggles(); + syncS3Expansion(); + updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); + } + function renderLogEvent(event) { + const level = detectLogLevel(event); + const levelTag = level ? `${escapeHtml(level.label)}` : ""; + return `
@@ -2451,6 +3577,7 @@ function clientApp(config) {
${escapeHtml(formatDateTime(event.timestamp))} ${escapeHtml(event.logStreamName || "Unknown stream")} + ${levelTag}
@@ -2459,7 +3586,7 @@ function clientApp(config) {

${renderLogPreviewContent(event)}

-
+
${renderLogBodyContent(event.message)}
@@ -2529,6 +3656,62 @@ function clientApp(config) { `; } + function renderS3ObjectCard(object) { + const previewState = s3State.previews[object.id]; + const tags = [ + `${escapeHtml(object.bucket)}`, + `${escapeHtml(formatBytes(object.size))}` + ]; + + if (object.storageClass) { + tags.push(`${escapeHtml(object.storageClass)}`); + } + + if (previewState?.status === "loaded") { + tags.push(`${escapeHtml(previewState.previewType)}`); + } + + if (previewState?.status === "error") { + tags.push('Preview failed'); + } + + return ` +
+ +
+
+

🪣 ${escapeHtml(object.key)}

+

${escapeHtml(object.bucket)} • ${escapeHtml(formatBytes(object.size))}

+
+ ${escapeHtml(formatDateTime(object.lastModified))} +
+
${tags.join("")}
+

${escapeHtml(buildS3Preview(object))}

+
+
+
+
+ + + ⬇️ Download +
+
+ +
+ ${metaCard("Bucket", object.bucket)} + ${metaCard("Key", object.key)} + ${metaCard("Size", formatBytes(object.size))} + ${metaCard("Modified", formatDateTime(object.lastModified))} + ${metaCard("Storage class", object.storageClass || "STANDARD")} + ${metaCard("ETag", object.etag || "Not available")} +
+ +
${renderS3PreviewPanel(object, previewState)}
+
+
+ `; + } + function bindLogToggles() { el.logsList.querySelectorAll(".logEvent").forEach((details) => { details.addEventListener("toggle", () => { @@ -2566,6 +3749,25 @@ function clientApp(config) { }); } + function bindS3Toggles() { + el.s3List.querySelectorAll(".s3Card").forEach((details) => { + details.addEventListener("toggle", () => { + const id = details.dataset.id; + + if (!id) { + return; + } + + if (details.open) { + s3State.openIds.add(id); + ensureS3Preview(id, { force: false }); + } else { + s3State.openIds.delete(id); + } + }); + }); + } + function bindCardToggles() { el.list.querySelectorAll(".card").forEach((details) => { details.addEventListener("toggle", () => { @@ -2704,6 +3906,9 @@ function clientApp(config) { return `
Unable to load secret value: ${escapeHtml(valueState.error)}
`; } + const revealed = secretsState.revealedIds.has(secret.id); + const maskedHtml = escapeHtml(maskSecretValue(valueState.copyValue)); + return `
@@ -2720,11 +3925,55 @@ function clientApp(config) { }
+ + + +
-
${valueState.displayHtml}
+
${revealed ? valueState.displayHtml : maskedHtml}
+ `; + } + + function renderS3PreviewPanel(object, previewState) { + if (!previewState) { + return `
Object previews load on demand.
`; + } + + if (previewState.status === "loading") { + return '
Loading object preview...
'; + } + + if (previewState.status === "error") { + return `
Unable to load object preview: ${escapeHtml(previewState.error)}
`; + } + + const truncatedTag = previewState.truncated ? 'Preview truncated' : ""; + let previewContent = `
No inline preview available for this object type.
`; + + if (previewState.previewType === "image" && previewState.imageDataUrl) { + previewContent = `${escapeHtml(object.key)}`; + } else if (previewState.previewType === "json") { + previewContent = `
${highlightJsonText(prettyJsonOrText(previewState.previewText))}
`; + } else if (previewState.previewType === "text" || previewState.previewType === "html") { + previewContent = `
${escapeHtml(previewState.previewText || "No preview text available.")}
`; + } + + return ` +
+
+ ${escapeHtml(previewState.previewType)} + ${truncatedTag} + ${previewState.contentType ? `${escapeHtml(previewState.contentType)}` : ""} +
+
+ + ⬇️ Download +
+
+ ${previewContent} `; } @@ -2782,6 +4031,31 @@ function clientApp(config) { window.requestAnimationFrame(applySecretState); } + function syncS3Expansion() { + const applyS3State = () => { + el.s3List.querySelectorAll(".s3Card").forEach((details) => { + const id = details.dataset.id; + const shouldOpen = Boolean(id && s3State.openIds.has(id)); + + if (shouldOpen && !details.open) { + details.open = true; + } + + if (!shouldOpen && details.open) { + details.open = false; + return; + } + + if (shouldOpen) { + ensureS3Preview(id, { force: false }); + } + }); + }; + + applyS3State(); + window.requestAnimationFrame(applyS3State); + } + function resolveAttachmentIcon(attachment) { const filename = String(attachment?.filename || "").toLowerCase(); const contentType = String(attachment?.contentType || "").toLowerCase(); @@ -2870,6 +4144,10 @@ function clientApp(config) { return secretsState.items.find((secret) => secret.id === id); } + function getS3Object(id) { + return s3State.objects.find((object) => object.id === id); + } + async function loadRaw(id) { if (state.raw[id]?.status === "loaded") { return state.raw[id].value; @@ -2967,6 +4245,67 @@ function clientApp(config) { } } + async function ensureS3Preview(id, options = {}) { + const { force = false } = options; + + if (!id) { + return null; + } + + if (!force && s3State.previews[id]?.status === "loaded") { + return s3State.previews[id]; + } + + if (s3State.previews[id]?.status === "loading") { + return null; + } + + const object = getS3Object(id); + + if (!object) { + return null; + } + + s3State.previews[id] = { status: "loading" }; + renderS3All(); + + try { + const response = await fetch( + `/api/s3/object?bucket=${encodeURIComponent(object.bucket)}&key=${encodeURIComponent(object.key)}`, + { cache: "no-store" } + ); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + const entry = { + status: "loaded", + previewType: payload.previewType || "binary", + previewText: payload.previewText || "", + imageDataUrl: payload.imageDataUrl || "", + contentType: payload.contentType || "", + contentLength: payload.contentLength || 0, + truncated: Boolean(payload.truncated), + metadata: payload.metadata || {} + }; + + s3State.previews[id] = entry; + return entry; + } catch (error) { + s3State.previews[id] = { + status: "error", + error: error.message || "Unknown S3 preview error" + }; + setS3Status("Could not load the S3 object preview.", "bad"); + return null; + } finally { + renderS3All(); + } + } + async function copyText(value) { try { await navigator.clipboard.writeText(value); @@ -3013,9 +4352,19 @@ function clientApp(config) { el.secretsStatusChip.textContent = message; } + function setS3Status(message, tone) { + el.s3StatusChip.className = "status"; + + if (tone) { + el.s3StatusChip.classList.add(tone); + } + + el.s3StatusChip.textContent = message; + } + function getInitialPanel() { const storedPanel = readStoredValue(PANEL_STORAGE_KEY); - return ["emails", "logs", "secrets"].includes(storedPanel) ? storedPanel : "emails"; + return ["emails", "logs", "secrets", "s3"].includes(storedPanel) ? storedPanel : "emails"; } function getInitialTheme() { @@ -3072,7 +4421,9 @@ function clientApp(config) { auto: logsState.auto, interval: logsState.interval, windowMs: logsState.windowMs, - limit: logsState.limit + limit: logsState.limit, + wrapLines: logsState.wrapLines, + tailNewest: logsState.tailNewest }); } @@ -3084,6 +4435,33 @@ function clientApp(config) { }); } + function persistS3Preferences() { + writeStoredJson(S3_PREFERENCES_STORAGE_KEY, { + bucket: s3State.bucket, + prefix: s3State.prefix, + search: s3State.search, + auto: s3State.auto, + interval: s3State.interval + }); + } + + function resetSavedState() { + [ + THEME_STORAGE_KEY, + PANEL_STORAGE_KEY, + EMAIL_PREFERENCES_STORAGE_KEY, + LOG_PREFERENCES_STORAGE_KEY, + SECRET_PREFERENCES_STORAGE_KEY, + S3_PREFERENCES_STORAGE_KEY + ].forEach((key) => { + try { + window.localStorage.removeItem(key); + } catch {} + }); + + window.location.reload(); + } + function getStoredPreferences(key) { try { const rawValue = window.localStorage.getItem(key); @@ -3238,6 +4616,74 @@ function clientApp(config) { return fragments.join(" • "); } + function toggleSecretReveal(id) { + if (secretsState.revealedIds.has(id)) { + secretsState.revealedIds.delete(id); + } else { + secretsState.revealedIds.add(id); + } + + renderSecretsAll(); + } + + function maskSecretValue(value) { + const source = String(value || ""); + + if (!source) { + return "Secret is loaded but empty."; + } + + const lines = source.split("\n"); + return lines.map((line) => "•".repeat(Math.max(12, Math.min(line.length || 0, 48)))).join("\n"); + } + + function buildS3Preview(object) { + const fragments = []; + + if (object.storageClass) { + fragments.push(object.storageClass); + } + + fragments.push(formatBytes(object.size)); + + if (object.etag) { + fragments.push(`ETag ${object.etag.slice(0, 12)}`); + } + + return fragments.join(" • "); + } + + function prettyJsonOrText(value) { + const parsed = tryParseJsonText(value); + return parsed.ok ? JSON.stringify(parsed.value, null, 2) : String(value || ""); + } + + function detectLogLevel(event) { + const parsed = tryParseJsonText(event?.message); + const candidates = parsed.ok + ? [parsed.value?.level, parsed.value?.severity, parsed.value?.logLevel, parsed.value?.status, parsed.value?.lvl] + : [String(event?.message || "").match(/\b(error|warn|warning|info|debug|trace|fatal)\b/i)?.[0] || ""]; + const normalized = String(candidates.find(Boolean) || "").toLowerCase(); + + if (["fatal", "error", "critical"].includes(normalized)) { + return { label: normalized.toUpperCase(), className: "levelError" }; + } + + if (["warn", "warning"].includes(normalized)) { + return { label: "WARN", className: "levelWarn" }; + } + + if (["info", "notice"].includes(normalized)) { + return { label: normalized.toUpperCase(), className: "levelInfo" }; + } + + if (["debug", "trace"].includes(normalized)) { + return { label: normalized.toUpperCase(), className: "levelDebug" }; + } + + return null; + } + function renderLogPreviewContent(event) { const parsedLog = tryParseJsonText(event?.message); @@ -3329,4 +4775,5 @@ app.listen(PORT, () => { console.log(`Watching LocalStack SES endpoint at ${SES_ENDPOINT}`); console.log(`Watching LocalStack CloudWatch Logs endpoint at ${CLOUDWATCH_ENDPOINT} (${CLOUDWATCH_REGION})`); console.log(`Watching LocalStack Secrets Manager endpoint at ${SECRETS_ENDPOINT} (${SECRETS_REGION})`); + console.log(`Watching LocalStack S3 endpoint at ${S3_ENDPOINT} (${S3_REGION})`); }); diff --git a/_reference/localEmailViewer/package-lock.json b/_reference/localEmailViewer/package-lock.json index 2f00ef450..b01591261 100644 --- a/_reference/localEmailViewer/package-lock.json +++ b/_reference/localEmailViewer/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.1012.0", + "@aws-sdk/client-s3": "^3.1013.0", "@aws-sdk/client-secrets-manager": "^3.1013.0", "express": "^5.1.0", "mailparser": "^3.7.4", @@ -30,6 +31,69 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", @@ -208,6 +272,72 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1013.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1013.0.tgz", + "integrity": "sha512-vFdyRyRatF+xP9Fi+4alZkmzZadqOAM34Pm6SUZsYtumNrWkgMc/pFWITnsq6eltM8qcV/vcinQ1ZBXWm/PlKg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.22", + "@aws-sdk/credential-provider-node": "^3.972.23", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", + "@aws-sdk/middleware-expect-continue": "^3.972.8", + "@aws-sdk/middleware-flexible-checksums": "^3.974.2", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-location-constraint": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-sdk-s3": "^3.972.22", + "@aws-sdk/middleware-ssec": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.23", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/signature-v4-multi-region": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.9", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.12", + "@smithy/eventstream-serde-browser": "^4.2.12", + "@smithy/eventstream-serde-config-resolver": "^4.3.12", + "@smithy/eventstream-serde-node": "^4.2.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-blob-browser": "^4.2.13", + "@smithy/hash-node": "^4.2.12", + "@smithy/hash-stream-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/md5-js": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-retry": "^4.4.43", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.42", + "@smithy/util-defaults-mode-node": "^4.2.45", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-secrets-manager": { "version": "3.1013.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1013.0.tgz", @@ -282,6 +412,19 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.5.tgz", + "integrity": "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/credential-provider-env": { "version": "3.972.20", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.20.tgz", @@ -440,6 +583,64 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.8.tgz", + "integrity": "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.8.tgz", + "integrity": "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.974.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.2.tgz", + "integrity": "sha512-4soN/N4R6ptdnHw7hXPVDZMIIL+vhN8rwtLdDyS0uD7ExhadtJzolTBIM5eKSkbw5uBEbIwtJc8HCG2NM6tN/g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.973.22", + "@aws-sdk/crc64-nvme": "^3.972.5", + "@aws-sdk/types": "^3.973.6", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.972.8", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", @@ -455,6 +656,20 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.8.tgz", + "integrity": "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-logger": { "version": "3.972.8", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", @@ -485,6 +700,45 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.22.tgz", + "integrity": "sha512-dkUcRxF4rVpPbyHpxjCApGK6b7JpnSeo7tDoNakpRKmiLMCqgy4tlGBgeEYJnZgLrA4xc5jVKuXgvgqKqU18Kw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.22", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.8.tgz", + "integrity": "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-user-agent": { "version": "3.972.23", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.23.tgz", @@ -569,6 +823,23 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.10.tgz", + "integrity": "sha512-yJSbFTedh1McfqXa9wZzjchqQ2puq5PI/qRz5kUjg2UXS5mO4MBYBbeXaZ2rp/h+ZbkcYEdo4Qsiah9psyoxrA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.22", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/token-providers": { "version": "3.1013.0", "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1013.0.tgz", @@ -600,6 +871,18 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/util-endpoints": { "version": "3.996.5", "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", @@ -714,6 +997,31 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", + "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", + "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/config-resolver": { "version": "4.4.13", "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz", @@ -854,6 +1162,21 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.13.tgz", + "integrity": "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.2", + "@smithy/chunked-blob-reader-native": "^4.2.3", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/hash-node": { "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", @@ -869,6 +1192,20 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.12.tgz", + "integrity": "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/invalid-dependency": { "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", @@ -894,6 +1231,20 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/md5-js": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.12.tgz", + "integrity": "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/middleware-content-length": { "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", @@ -1340,6 +1691,20 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.13.tgz", + "integrity": "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/uuid": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", diff --git a/_reference/localEmailViewer/package.json b/_reference/localEmailViewer/package.json index 67ea3fc6b..956794f4c 100644 --- a/_reference/localEmailViewer/package.json +++ b/_reference/localEmailViewer/package.json @@ -10,9 +10,10 @@ "keywords": [], "author": "", "license": "ISC", - "description": "LocalStack inspector for SES emails, CloudWatch logs, and Secrets Manager", + "description": "LocalStack inspector for SES emails, CloudWatch logs, Secrets Manager, and S3", "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.1012.0", + "@aws-sdk/client-s3": "^3.1013.0", "@aws-sdk/client-secrets-manager": "^3.1013.0", "express": "^5.1.0", "mailparser": "^3.7.4", From 9d9e626cfed3570ed3e05fbf98472d5355852ef8 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 19 Mar 2026 18:41:43 -0400 Subject: [PATCH 17/56] feature/IO-3587-Commision-Cut-Clean - Split localStack client into smaller files / seperated by concerns --- _reference/localEmailViewer/README.md | 8 + _reference/localEmailViewer/index.js | 4525 +---------------- _reference/localEmailViewer/package.json | 2 +- .../localEmailViewer/public/client-app.js | 3154 ++++++++++++ _reference/localEmailViewer/server/config.js | 45 + .../server/localstack-service.js | 845 +++ _reference/localEmailViewer/server/page.js | 495 ++ 7 files changed, 4592 insertions(+), 4482 deletions(-) create mode 100644 _reference/localEmailViewer/public/client-app.js create mode 100644 _reference/localEmailViewer/server/config.js create mode 100644 _reference/localEmailViewer/server/localstack-service.js create mode 100644 _reference/localEmailViewer/server/page.js diff --git a/_reference/localEmailViewer/README.md b/_reference/localEmailViewer/README.md index 019d6f946..cbfe9e555 100644 --- a/_reference/localEmailViewer/README.md +++ b/_reference/localEmailViewer/README.md @@ -32,6 +32,14 @@ Features: - Shared LocalStack service health strip plus a reset action for clearing saved viewer state - Compact single-page UI for switching between the local stack tools you use most +Code layout: + +- `index.js`: small Express bootstrap and route registration +- `server/config.js`: LocalStack endpoints, defaults, and AWS client setup +- `server/localstack-service.js`: SES, Logs, Secrets, and S3 data loading helpers +- `server/page.js`: server-rendered HTML shell, CSS, and client config payload +- `public/client-app.js`: browser-side UI state, rendering, refresh logic, and interactions + Optional environment variables: ```shell diff --git a/_reference/localEmailViewer/index.js b/_reference/localEmailViewer/index.js index 64b72fbd7..67df19055 100644 --- a/_reference/localEmailViewer/index.js +++ b/_reference/localEmailViewer/index.js @@ -1,59 +1,41 @@ import express from "express"; -import fetch from "node-fetch"; +import { readFileSync } from "node:fs"; import { - CloudWatchLogsClient, - DescribeLogGroupsCommand, - DescribeLogStreamsCommand, - FilterLogEventsCommand -} from "@aws-sdk/client-cloudwatch-logs"; -import { GetSecretValueCommand, ListSecretsCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; + CLOUDWATCH_DEFAULT_LIMIT, + CLOUDWATCH_DEFAULT_WINDOW_MS, + CLOUDWATCH_ENDPOINT, + CLOUDWATCH_REGION, + DEFAULT_REFRESH_MS, + PORT, + S3_ENDPOINT, + S3_REGION, + SES_ENDPOINT, + SECRETS_ENDPOINT, + SECRETS_REGION +} from "./server/config.js"; +import { getClientConfig, renderHtml } from "./server/page.js"; import { - GetObjectCommand, - HeadObjectCommand, - ListBucketsCommand, - ListObjectsV2Command, - S3Client -} from "@aws-sdk/client-s3"; -import { simpleParser } from "mailparser"; + buildAttachmentDisposition, + buildInlineDisposition, + clampNumber, + findSesMessageById, + loadLogEvents, + loadLogGroups, + loadLogStreams, + loadMessageAttachment, + loadMessages, + loadS3Buckets, + loadS3ObjectDownload, + loadS3ObjectPreview, + loadS3Objects, + loadSecretValue, + loadSecrets, + loadServiceHealthSummary +} from "./server/localstack-service.js"; const app = express(); - -const PORT = Number(process.env.PORT || 3334); -const SES_ENDPOINT = process.env.SES_VIEWER_ENDPOINT || "http://localhost:4566/_aws/ses"; -const FETCH_TIMEOUT_MS = Number(process.env.SES_VIEWER_FETCH_TIMEOUT_MS || 5000); -const DEFAULT_REFRESH_MS = Number(process.env.SES_VIEWER_REFRESH_MS || 10000); -const CLOUDWATCH_ENDPOINT = process.env.CLOUDWATCH_VIEWER_ENDPOINT || "http://localhost:4566"; -const CLOUDWATCH_REGION = process.env.CLOUDWATCH_VIEWER_REGION || process.env.AWS_DEFAULT_REGION || "ca-central-1"; -const CLOUDWATCH_DEFAULT_GROUP = process.env.CLOUDWATCH_VIEWER_LOG_GROUP || "development"; -const CLOUDWATCH_DEFAULT_WINDOW_MS = Number(process.env.CLOUDWATCH_VIEWER_WINDOW_MS || 15 * 60 * 1000); -const CLOUDWATCH_DEFAULT_LIMIT = Number(process.env.CLOUDWATCH_VIEWER_LIMIT || 200); -const SECRETS_ENDPOINT = process.env.SECRETS_VIEWER_ENDPOINT || CLOUDWATCH_ENDPOINT; -const SECRETS_REGION = process.env.SECRETS_VIEWER_REGION || CLOUDWATCH_REGION; -const S3_ENDPOINT = process.env.S3_VIEWER_ENDPOINT || CLOUDWATCH_ENDPOINT; -const S3_REGION = process.env.S3_VIEWER_REGION || CLOUDWATCH_REGION; -const S3_DEFAULT_BUCKET = process.env.S3_VIEWER_BUCKET || ""; -const S3_PREVIEW_MAX_BYTES = Number(process.env.S3_VIEWER_PREVIEW_BYTES || 256 * 1024); -const S3_IMAGE_PREVIEW_MAX_BYTES = Number(process.env.S3_VIEWER_IMAGE_PREVIEW_BYTES || 1024 * 1024); -const LOCALSTACK_CREDENTIALS = { - accessKeyId: process.env.AWS_ACCESS_KEY_ID || "test", - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "test" -}; -const cloudWatchLogsClient = new CloudWatchLogsClient({ - region: CLOUDWATCH_REGION, - endpoint: CLOUDWATCH_ENDPOINT, - credentials: LOCALSTACK_CREDENTIALS -}); -const secretsManagerClient = new SecretsManagerClient({ - region: SECRETS_REGION, - endpoint: SECRETS_ENDPOINT, - credentials: LOCALSTACK_CREDENTIALS -}); -const s3Client = new S3Client({ - region: S3_REGION, - endpoint: S3_ENDPOINT, - credentials: LOCALSTACK_CREDENTIALS, - forcePathStyle: true -}); +const CLIENT_APP_PATH = new URL("./public/client-app.js", import.meta.url); +const CLIENT_APP_SOURCE = readFileSync(CLIENT_APP_PATH, "utf8"); app.use((req, res, next) => { res.set("Cache-Control", "no-store"); @@ -65,7 +47,7 @@ app.get("/", (req, res) => { }); app.get("/app.js", (req, res) => { - res.type("application/javascript").send(`(${clientApp.toString()})(${JSON.stringify(getClientConfig())});`); + res.type("application/javascript").send(`${CLIENT_APP_SOURCE}\n\nclientApp(${JSON.stringify(getClientConfig())});\n`); }); app.get("/health", (req, res) => { @@ -133,27 +115,17 @@ app.get("/api/messages/:id/attachments/:index", async (req, res) => { return; } - const parsed = await parseSesMessageById(req.params.id); - - if (!parsed) { - res.status(404).type("text/plain").send("Message not found"); - return; - } - - const attachment = parsed.attachments?.[attachmentIndex]; + const attachment = await loadMessageAttachment(req.params.id, attachmentIndex); if (!attachment) { res.status(404).type("text/plain").send("Attachment not found"); return; } - const filename = resolveAttachmentFilename(attachment, attachmentIndex); - const content = Buffer.isBuffer(attachment.content) ? attachment.content : Buffer.from(attachment.content || ""); - - res.setHeader("Content-Type", attachment.contentType || "application/octet-stream"); - res.setHeader("Content-Disposition", buildAttachmentDisposition(filename)); - res.setHeader("Content-Length", String(content.length)); - res.send(content); + res.setHeader("Content-Type", attachment.contentType); + res.setHeader("Content-Disposition", buildAttachmentDisposition(attachment.filename)); + res.setHeader("Content-Length", String(attachment.content.length)); + res.send(attachment.content); } catch (error) { console.error("Error downloading attachment:", error); res.status(502).type("text/plain").send(`Unable to download attachment: ${error.message}`); @@ -166,7 +138,6 @@ app.get("/api/logs/groups", async (req, res) => { res.json({ endpoint: CLOUDWATCH_ENDPOINT, region: CLOUDWATCH_REGION, - defaultGroup: CLOUDWATCH_DEFAULT_GROUP, groups }); } catch (error) { @@ -342,22 +313,15 @@ app.get("/api/s3/download", async (req, res) => { return; } - const response = await s3Client.send( - new GetObjectCommand({ - Bucket: bucket, - Key: key - }) - ); - const content = Buffer.from(await response.Body.transformToByteArray()); - const filename = basenameFromKey(key); + const object = await loadS3ObjectDownload({ bucket, key }); - res.setHeader("Content-Type", response.ContentType || guessObjectContentType(key)); + res.setHeader("Content-Type", object.contentType); res.setHeader( "Content-Disposition", - inline ? buildInlineDisposition(filename) : buildAttachmentDisposition(filename) + inline ? buildInlineDisposition(object.filename) : buildAttachmentDisposition(object.filename) ); - res.setHeader("Content-Length", String(content.length)); - res.send(content); + res.setHeader("Content-Length", String(object.content.length)); + res.send(object.content); } catch (error) { if (error?.name === "NoSuchKey" || error?.name === "NotFound") { res.status(404).type("text/plain").send("Object not found"); @@ -369,4407 +333,6 @@ app.get("/api/s3/download", async (req, res) => { } }); -async function loadMessages() { - const startedAt = Date.now(); - const sesMessages = await fetchSesMessages(); - const messages = await Promise.all(sesMessages.map((message, index) => toMessageViewModel(message, index))); - - messages.sort((left, right) => { - if ((right.timestampMs || 0) !== (left.timestampMs || 0)) { - return (right.timestampMs || 0) - (left.timestampMs || 0); - } - - return right.index - left.index; - }); - - return { - endpoint: SES_ENDPOINT, - fetchedAt: new Date().toISOString(), - fetchDurationMs: Date.now() - startedAt, - totalMessages: messages.length, - parseErrors: messages.filter((message) => Boolean(message.parseError)).length, - latestMessageTimestamp: messages[0]?.timestamp || "", - messages - }; -} - -async function fetchSesMessages() { - const response = await fetch(SES_ENDPOINT, { - signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) - }); - - if (!response.ok) { - throw new Error(`SES endpoint responded with ${response.status}`); - } - - const data = await response.json(); - return Array.isArray(data.messages) ? data.messages : []; -} - -async function loadLogGroups() { - const groups = []; - let nextToken; - let pageCount = 0; - - do { - const response = await cloudWatchLogsClient.send( - new DescribeLogGroupsCommand({ - nextToken, - limit: 50 - }) - ); - - groups.push( - ...(response.logGroups || []).map((group) => ({ - name: group.logGroupName || "", - arn: group.arn || "", - storedBytes: group.storedBytes || 0, - retentionInDays: group.retentionInDays || 0, - creationTime: group.creationTime || 0 - })) - ); - - nextToken = response.nextToken; - pageCount += 1; - } while (nextToken && pageCount < 10); - - return groups.sort((left, right) => left.name.localeCompare(right.name)); -} - -async function loadLogStreams(logGroupName) { - const streams = []; - let nextToken; - let pageCount = 0; - - do { - const response = await cloudWatchLogsClient.send( - new DescribeLogStreamsCommand({ - logGroupName, - descending: true, - orderBy: "LastEventTime", - nextToken, - limit: 50 - }) - ); - - streams.push( - ...(response.logStreams || []).map((stream) => ({ - name: stream.logStreamName || "", - arn: stream.arn || "", - lastEventTimestamp: stream.lastEventTimestamp || 0, - lastIngestionTime: stream.lastIngestionTime || 0, - storedBytes: stream.storedBytes || 0 - })) - ); - - nextToken = response.nextToken; - pageCount += 1; - } while (nextToken && pageCount < 6 && streams.length < 250); - - return streams; -} - -async function loadLogEvents({ logGroupName, logStreamName, windowMs, limit }) { - const startedAt = Date.now(); - const eventMap = new Map(); - const startTime = Date.now() - windowMs; - let nextToken; - let previousToken = ""; - let pageCount = 0; - let searchedLogStreams = 0; - - do { - const response = await cloudWatchLogsClient.send( - new FilterLogEventsCommand({ - logGroupName, - logStreamNames: logStreamName ? [logStreamName] : undefined, - startTime, - endTime: Date.now(), - limit, - nextToken - }) - ); - - for (const event of response.events || []) { - const id = - event.eventId || `${event.logStreamName || "stream"}-${event.timestamp || 0}-${event.ingestionTime || 0}`; - - if (!eventMap.has(id)) { - const message = String(event.message || "").trim(); - eventMap.set(id, { - id, - timestamp: event.timestamp || 0, - ingestionTime: event.ingestionTime || 0, - logStreamName: event.logStreamName || "", - message, - preview: buildLogPreview(message) - }); - } - } - - searchedLogStreams = Math.max(searchedLogStreams, (response.searchedLogStreams || []).length); - previousToken = nextToken || ""; - nextToken = response.nextToken; - pageCount += 1; - } while (nextToken && nextToken !== previousToken && pageCount < 10 && eventMap.size < limit); - - const events = [...eventMap.values()] - .sort((left, right) => { - if ((right.timestamp || 0) !== (left.timestamp || 0)) { - return (right.timestamp || 0) - (left.timestamp || 0); - } - - return left.logStreamName.localeCompare(right.logStreamName); - }) - .slice(0, limit); - - return { - endpoint: CLOUDWATCH_ENDPOINT, - region: CLOUDWATCH_REGION, - logGroupName, - logStreamName, - fetchDurationMs: Date.now() - startedAt, - latestTimestamp: events[0]?.timestamp || 0, - searchedLogStreams, - totalEvents: events.length, - events - }; -} - -async function loadSecrets() { - const startedAt = Date.now(); - const secrets = []; - let nextToken; - let pageCount = 0; - - do { - const response = await secretsManagerClient.send( - new ListSecretsCommand({ - NextToken: nextToken, - MaxResults: 50 - }) - ); - - secrets.push( - ...(response.SecretList || []).map((secret, index) => ({ - id: secret.ARN || secret.Name || `secret-${index}`, - name: secret.Name || "Unnamed secret", - arn: secret.ARN || "", - description: secret.Description || "", - createdDate: normalizeTimestamp(secret.CreatedDate), - lastChangedDate: normalizeTimestamp(secret.LastChangedDate), - lastAccessedDate: normalizeTimestamp(secret.LastAccessedDate), - deletedDate: normalizeTimestamp(secret.DeletedDate), - primaryRegion: secret.PrimaryRegion || "", - owningService: secret.OwningService || "", - rotationEnabled: Boolean(secret.RotationEnabled), - versionCount: Object.keys(secret.SecretVersionsToStages || {}).length, - tagCount: Array.isArray(secret.Tags) ? secret.Tags.length : 0, - tags: (secret.Tags || []) - .map((tag) => ({ - key: tag.Key || "", - value: tag.Value || "" - })) - .filter((tag) => tag.key || tag.value) - })) - ); - - nextToken = response.NextToken; - pageCount += 1; - } while (nextToken && pageCount < 10 && secrets.length < 500); - - secrets.sort((left, right) => { - const leftTime = Date.parse(left.lastChangedDate || left.createdDate || 0) || 0; - const rightTime = Date.parse(right.lastChangedDate || right.createdDate || 0) || 0; - - if (rightTime !== leftTime) { - return rightTime - leftTime; - } - - return left.name.localeCompare(right.name); - }); - - return { - endpoint: SECRETS_ENDPOINT, - region: SECRETS_REGION, - fetchedAt: new Date().toISOString(), - fetchDurationMs: Date.now() - startedAt, - totalSecrets: secrets.length, - latestTimestamp: secrets[0]?.lastChangedDate || secrets[0]?.createdDate || "", - secrets - }; -} - -async function loadSecretValue(secretId) { - const startedAt = Date.now(); - const response = await secretsManagerClient.send( - new GetSecretValueCommand({ - SecretId: secretId - }) - ); - - const secretBinary = response.SecretBinary - ? typeof response.SecretBinary === "string" - ? response.SecretBinary - : Buffer.from(response.SecretBinary).toString("base64") - : ""; - - return { - endpoint: SECRETS_ENDPOINT, - region: SECRETS_REGION, - fetchDurationMs: Date.now() - startedAt, - id: secretId, - name: response.Name || "", - arn: response.ARN || "", - versionId: response.VersionId || "", - versionStages: Array.isArray(response.VersionStages) ? response.VersionStages : [], - createdDate: normalizeTimestamp(response.CreatedDate), - secretString: typeof response.SecretString === "string" ? response.SecretString : "", - secretBinary - }; -} - -async function loadS3Buckets() { - const startedAt = Date.now(); - const response = await s3Client.send(new ListBucketsCommand({})); - const buckets = (response.Buckets || []) - .map((bucket) => ({ - name: bucket.Name || "", - creationDate: normalizeTimestamp(bucket.CreationDate) - })) - .filter((bucket) => bucket.name) - .sort((left, right) => left.name.localeCompare(right.name)); - - return { - endpoint: S3_ENDPOINT, - region: S3_REGION, - fetchedAt: new Date().toISOString(), - fetchDurationMs: Date.now() - startedAt, - totalBuckets: buckets.length, - buckets - }; -} - -async function loadS3Objects({ bucket, prefix }) { - const startedAt = Date.now(); - const objects = []; - let continuationToken; - let pageCount = 0; - - do { - const response = await s3Client.send( - new ListObjectsV2Command({ - Bucket: bucket, - Prefix: prefix || undefined, - ContinuationToken: continuationToken, - MaxKeys: 200 - }) - ); - - objects.push( - ...(response.Contents || []).map((object, index) => ({ - id: `${bucket}::${object.Key || index}`, - bucket, - key: object.Key || "", - size: object.Size || 0, - lastModified: normalizeTimestamp(object.LastModified), - etag: String(object.ETag || "").replace(/^"|"$/g, ""), - storageClass: object.StorageClass || "STANDARD" - })) - ); - - continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; - pageCount += 1; - } while (continuationToken && pageCount < 10 && objects.length < 1000); - - objects.sort((left, right) => { - const leftTime = Date.parse(left.lastModified || 0) || 0; - const rightTime = Date.parse(right.lastModified || 0) || 0; - - if (rightTime !== leftTime) { - return rightTime - leftTime; - } - - return left.key.localeCompare(right.key); - }); - - return { - endpoint: S3_ENDPOINT, - region: S3_REGION, - bucket, - prefix, - fetchedAt: new Date().toISOString(), - fetchDurationMs: Date.now() - startedAt, - totalObjects: objects.length, - latestTimestamp: objects[0]?.lastModified || "", - objects - }; -} - -async function loadS3ObjectPreview({ bucket, key }) { - const startedAt = Date.now(); - const head = await s3Client.send( - new HeadObjectCommand({ - Bucket: bucket, - Key: key - }) - ); - - const contentType = head.ContentType || guessObjectContentType(key); - const contentLength = Number(head.ContentLength || 0); - const previewType = resolveS3PreviewType(contentType, key); - const result = { - endpoint: S3_ENDPOINT, - region: S3_REGION, - bucket, - key, - fetchDurationMs: 0, - contentType, - contentLength, - etag: String(head.ETag || "").replace(/^"|"$/g, ""), - lastModified: normalizeTimestamp(head.LastModified), - metadata: head.Metadata || {}, - previewType, - previewText: "", - imageDataUrl: "", - truncated: false - }; - - const shouldLoadTextPreview = previewType === "json" || previewType === "text" || previewType === "html"; - const shouldLoadImagePreview = - previewType === "image" && contentLength > 0 && contentLength <= S3_IMAGE_PREVIEW_MAX_BYTES; - - if ((shouldLoadTextPreview || shouldLoadImagePreview) && contentLength > 0) { - const previewBytes = Math.max(1, Math.min(contentLength || S3_PREVIEW_MAX_BYTES, S3_PREVIEW_MAX_BYTES)); - const response = await s3Client.send( - new GetObjectCommand({ - Bucket: bucket, - Key: key, - Range: `bytes=0-${previewBytes - 1}` - }) - ); - const content = Buffer.from(await response.Body.transformToByteArray()); - result.truncated = contentLength > content.length; - - if (shouldLoadImagePreview) { - result.imageDataUrl = `data:${contentType};base64,${content.toString("base64")}`; - } else { - result.previewText = content.toString("utf8"); - } - } - - result.fetchDurationMs = Date.now() - startedAt; - return result; -} - -async function loadServiceHealthSummary() { - const startedAt = Date.now(); - const [sesResult, logsResult, secretsResult, s3Result] = await Promise.allSettled([ - fetchSesMessages(), - loadLogGroups(), - loadSecrets(), - loadS3Buckets() - ]); - - return { - fetchedAt: new Date().toISOString(), - fetchDurationMs: Date.now() - startedAt, - services: { - emails: summarizeHealthResult({ - icon: "✉️", - panel: "emails", - label: "SES Emails", - result: sesResult, - count: sesResult.status === "fulfilled" ? sesResult.value.length : 0, - detail: SES_ENDPOINT, - noun: "email" - }), - logs: summarizeHealthResult({ - icon: "📜", - panel: "logs", - label: "CloudWatch Logs", - result: logsResult, - count: logsResult.status === "fulfilled" ? logsResult.value.length : 0, - detail: `${CLOUDWATCH_ENDPOINT} (${CLOUDWATCH_REGION})`, - noun: "group" - }), - secrets: summarizeHealthResult({ - icon: "🔐", - panel: "secrets", - label: "Secrets Manager", - result: secretsResult, - count: secretsResult.status === "fulfilled" ? secretsResult.value.totalSecrets : 0, - detail: `${SECRETS_ENDPOINT} (${SECRETS_REGION})`, - noun: "secret" - }), - s3: summarizeHealthResult({ - icon: "🪣", - panel: "s3", - label: "S3 Explorer", - result: s3Result, - count: s3Result.status === "fulfilled" ? s3Result.value.totalBuckets : 0, - detail: `${S3_ENDPOINT} (${S3_REGION})`, - noun: "bucket" - }) - } - }; -} - -async function findSesMessageById(id) { - const messages = await fetchSesMessages(); - return messages.find((message, index) => resolveMessageId(message, index) === id) || null; -} - -async function parseSesMessageById(id) { - const message = await findSesMessageById(id); - - if (!message) { - return null; - } - - return simpleParser(message.RawData || ""); -} - -async function toMessageViewModel(message, index) { - const id = resolveMessageId(message, index); - - try { - const parsed = await simpleParser(message.RawData || ""); - const textContent = normalizeText(parsed.text || ""); - const renderedHtml = buildRenderedHtml(parsed.html || parsed.textAsHtml || ""); - const timestamp = normalizeTimestamp(message.Timestamp || parsed.date); - - return { - id, - index, - from: formatAddressList(parsed.from) || message.Source || "Unknown sender", - to: formatAddressList(parsed.to) || "No To Address", - replyTo: formatAddressList(parsed.replyTo), - subject: parsed.subject || "No Subject", - region: message.Region || "", - timestamp, - timestampMs: timestamp ? Date.parse(timestamp) : 0, - messageId: parsed.messageId || "", - rawSizeBytes: Buffer.byteLength(message.RawData || "", "utf8"), - attachmentCount: parsed.attachments.length, - attachments: parsed.attachments.map((attachment, attachmentIndex) => ({ - index: attachmentIndex, - filename: resolveAttachmentFilename(attachment, attachmentIndex), - contentType: attachment.contentType || "application/octet-stream", - size: attachment.size || 0 - })), - preview: buildPreview(textContent, renderedHtml), - textContent, - renderedHtml, - hasHtml: Boolean(renderedHtml), - parseError: "" - }; - } catch (error) { - return { - id, - index, - from: message.Source || "Unknown sender", - to: "Unknown recipient", - replyTo: "", - subject: "Unable to parse message", - region: message.Region || "", - timestamp: normalizeTimestamp(message.Timestamp), - timestampMs: message.Timestamp ? Date.parse(message.Timestamp) : 0, - messageId: "", - rawSizeBytes: Buffer.byteLength(message.RawData || "", "utf8"), - attachmentCount: 0, - attachments: [], - preview: "This message could not be parsed. Open the raw view to inspect the MIME source.", - textContent: "", - renderedHtml: "", - hasHtml: false, - parseError: error.message - }; - } -} - -function resolveMessageId(message, index = 0) { - return message.Id || `${message.Timestamp || "unknown"}-${message.Source || "unknown"}-${index}`; -} - -function resolveAttachmentFilename(attachment, index = 0) { - if (attachment?.filename) { - return attachment.filename; - } - - return `attachment-${index + 1}${attachmentExtension(attachment?.contentType)}`; -} - -function attachmentExtension(contentType) { - const normalized = String(contentType || "") - .split(";")[0] - .trim() - .toLowerCase(); - - return ( - { - "application/json": ".json", - "application/pdf": ".pdf", - "application/zip": ".zip", - "image/gif": ".gif", - "image/jpeg": ".jpg", - "image/png": ".png", - "image/webp": ".webp", - "text/calendar": ".ics", - "text/csv": ".csv", - "text/html": ".html", - "text/plain": ".txt" - }[normalized] || "" - ); -} - -function buildAttachmentDisposition(filename) { - const fallback = String(filename || "attachment") - .replace(/[^\x20-\x7e]/g, "_") - .replace(/["\\]/g, "_"); - - return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename || "attachment")}`; -} - -function buildInlineDisposition(filename) { - const fallback = String(filename || "file") - .replace(/[^\x20-\x7e]/g, "_") - .replace(/["\\]/g, "_"); - - return `inline; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename || "file")}`; -} - -function basenameFromKey(key) { - const value = String(key || ""); - const parts = value.split("/").filter(Boolean); - return parts[parts.length - 1] || "file"; -} - -function guessObjectContentType(key) { - const normalizedKey = String(key || "").toLowerCase(); - - if (normalizedKey.endsWith(".json")) { - return "application/json"; - } - - if (normalizedKey.endsWith(".csv")) { - return "text/csv"; - } - - if (normalizedKey.endsWith(".html") || normalizedKey.endsWith(".htm")) { - return "text/html"; - } - - if (normalizedKey.endsWith(".txt") || normalizedKey.endsWith(".log") || normalizedKey.endsWith(".md")) { - return "text/plain"; - } - - if (normalizedKey.endsWith(".png")) { - return "image/png"; - } - - if (normalizedKey.endsWith(".jpg") || normalizedKey.endsWith(".jpeg")) { - return "image/jpeg"; - } - - if (normalizedKey.endsWith(".gif")) { - return "image/gif"; - } - - if (normalizedKey.endsWith(".webp")) { - return "image/webp"; - } - - if (normalizedKey.endsWith(".svg")) { - return "image/svg+xml"; - } - - if (normalizedKey.endsWith(".pdf")) { - return "application/pdf"; - } - - return "application/octet-stream"; -} - -function resolveS3PreviewType(contentType, key) { - const normalizedType = String(contentType || "").toLowerCase(); - const normalizedKey = String(key || "").toLowerCase(); - - if (normalizedType.includes("json") || normalizedKey.endsWith(".json")) { - return "json"; - } - - if (normalizedType.startsWith("image/")) { - return "image"; - } - - if (normalizedType.includes("html") || normalizedKey.endsWith(".html") || normalizedKey.endsWith(".htm")) { - return "html"; - } - - if ( - normalizedType.startsWith("text/") || - [".txt", ".log", ".csv", ".xml", ".yml", ".yaml", ".md"].some((extension) => normalizedKey.endsWith(extension)) - ) { - return "text"; - } - - return "binary"; -} - -function summarizeHealthResult({ icon, panel, label, result, count, detail, noun }) { - if (result.status === "fulfilled") { - return { - ok: true, - icon, - panel, - label, - count, - summary: `${count} ${noun}${count === 1 ? "" : "s"}`, - detail - }; - } - - return { - ok: false, - icon, - panel, - label, - count: 0, - summary: "Needs attention", - detail: result.reason?.message || detail - }; -} - -function normalizeTimestamp(value) { - if (!value) { - return ""; - } - - const date = value instanceof Date ? value : new Date(value); - return Number.isNaN(date.getTime()) ? "" : date.toISOString(); -} - -function normalizeText(value) { - return String(value || "") - .replace(/\r\n/g, "\n") - .trim(); -} - -function buildPreview(textContent, renderedHtml) { - const source = (textContent || stripTags(renderedHtml)).replace(/\s+/g, " ").trim(); - - if (!source) { - return "No message preview available."; - } - - return source.length > 220 ? `${source.slice(0, 217)}...` : source; -} - -function buildLogPreview(message) { - const source = String(message || "") - .replace(/\s+/g, " ") - .trim(); - - if (!source) { - return "No log preview available."; - } - - return source.length > 220 ? `${source.slice(0, 217)}...` : source; -} - -function clampNumber(value, fallback, min, max) { - const parsed = Number(value); - - if (!Number.isFinite(parsed)) { - return fallback; - } - - return Math.min(Math.max(parsed, min), max); -} - -function buildRenderedHtml(html) { - if (!html) { - return ""; - } - - const value = String(html); - const hasDocument = /]/i.test(value) || / - - - - - - - - ${value} -`; -} - -function stripTags(value) { - return String(value || "") - .replace(//gi, " ") - .replace(//gi, " ") - .replace(/<[^>]+>/g, " "); -} - -function formatAddressList(addresses) { - if (!addresses?.value?.length) { - return ""; - } - - return addresses.value - .map(({ name, address }) => { - if (name && address) { - return `${name} <${address}>`; - } - - return address || name || ""; - }) - .filter(Boolean) - .join(", "); -} - -function getClientConfig() { - return { - defaultRefreshMs: DEFAULT_REFRESH_MS, - endpoint: SES_ENDPOINT, - cloudWatchEndpoint: CLOUDWATCH_ENDPOINT, - cloudWatchRegion: CLOUDWATCH_REGION, - secretsEndpoint: SECRETS_ENDPOINT, - secretsRegion: SECRETS_REGION, - s3Endpoint: S3_ENDPOINT, - s3Region: S3_REGION, - defaultS3Bucket: S3_DEFAULT_BUCKET, - defaultLogGroup: CLOUDWATCH_DEFAULT_GROUP, - defaultLogWindowMs: CLOUDWATCH_DEFAULT_WINDOW_MS, - defaultLogLimit: CLOUDWATCH_DEFAULT_LIMIT - }; -} - -function renderHtml() { - return ` - - - - - LocalStack Inspector - - - -
-
-
-
-

LocalStack Toolbox

-

Inspector

-
-
-
- - -
-
-
- Stack -
- -
-
-
- -
-
-
- - - - Waiting for first refresh... -
-
- - - - -
-
- -
-
Total00 visible
-
New0New since last refresh
-
NewestNo messagesNot refreshed yet
-
FetchIdleEndpoint: ${escapeHtml(SES_ENDPOINT)}
-
- -
-
- - -
-
- -
-
-
-
- - - - - - -
- - - -`; -} - -function renderStyles() { - return ` - :root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--log-shell:linear-gradient(180deg,rgba(239,246,255,.98),rgba(248,251,255,.99));--log-body:#f8fbff;--secret-shell:linear-gradient(180deg,rgba(239,251,246,.98),rgba(247,253,249,.99));--secret-body:#f6fcf8;--bucket-shell:linear-gradient(180deg,rgba(255,249,232,.98),rgba(255,252,243,.99));--bucket-body:#fffcf2;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--log-line:rgba(48,113,169,.22);--secret-line:rgba(31,143,101,.2);--bucket-line:rgba(181,137,37,.22);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--info:#3071a9;--info-soft:rgba(48,113,169,.1);--secret:#1f8f65;--secret-soft:rgba(31,143,101,.1);--bucket:#9d6b00;--bucket-soft:rgba(181,137,37,.12);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);--log-shadow:0 16px 32px rgba(48,113,169,.12);--secret-shadow:0 16px 32px rgba(31,143,101,.12);--bucket-shadow:0 16px 32px rgba(181,137,37,.12);} - *{box-sizing:border-box} - html,body{margin:0;height:100%;overflow:hidden} - body{color-scheme:light;background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.12),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:15px/1.45 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif;transition:background-color .18s ease,color .18s ease} - button,input,select,textarea{font:inherit} - button{cursor:pointer} - .page{display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;max-width:1360px;height:100vh;height:100dvh;margin:0 auto;padding:14px;overflow:hidden} - .hero{display:block;margin-bottom:0} - .heroShell,.toolControls,.stat{background:var(--panel);backdrop-filter:blur(14px);border:1px solid var(--line);box-shadow:var(--shadow)} - .card{background:var(--card-shell);border:1px solid var(--card-line);box-shadow:var(--card-shadow)} - .heroShell,.toolControls{border-radius:18px} - .heroShell{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px} - .toolControls{padding:12px} - .heroIdentity{display:grid;gap:3px;min-width:0} - .eyebrow{margin:0 0 4px;color:var(--accent);font-size:.72rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase} - h1{margin:0;font-size:clamp(1.8rem,3.6vw,2.85rem);line-height:.96;letter-spacing:-.05em} - .lede{margin:8px 0 0;max-width:54ch;color:var(--muted);font-size:.92rem} - .heroTopRow{display:flex;flex:1 1 360px;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end} - .heroActions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end} - .heroStatusRow{display:flex;flex:1 1 100%;flex-wrap:wrap;gap:8px;align-items:center} - .heroStatusLabel{color:var(--muted);font-size:.72rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase} - .helper{margin:0;color:var(--muted);font-size:.89rem} - .healthStrip{display:flex;flex:1 1 520px;flex-wrap:wrap;gap:6px;align-items:center;min-width:0} - .healthBadge{display:inline-flex;align-items:center;gap:8px;min-height:30px;max-width:100%;padding:0 10px;border-radius:999px;border:1px solid rgba(31,41,51,.1);background:rgba(255,255,255,.78);box-shadow:0 8px 18px rgba(15,23,42,.06);text-align:left;transition:transform .12s ease,background-color .12s ease,border-color .12s ease,box-shadow .12s ease} - .healthBadgeName{display:inline-flex;align-items:center;gap:6px;font-size:.8rem;font-weight:800;white-space:nowrap} - .healthBadgeSummary{min-width:0;overflow:hidden;color:var(--muted);font-size:.78rem;font-weight:700;text-overflow:ellipsis;white-space:nowrap} - .healthBadge.ok{border-color:rgba(31,143,101,.22);background:rgba(31,143,101,.1)} - .healthBadge.bad{border-color:rgba(179,58,58,.22);background:rgba(179,58,58,.1)} - .healthBadge.warn{border-color:rgba(157,95,0,.22);background:rgba(157,95,0,.1)} - .healthBadge.active{border-color:rgba(207,109,60,.28);background:rgba(207,109,60,.16);box-shadow:0 10px 24px rgba(207,109,60,.12)} - .healthBadge.active .healthBadgeName,.healthBadge.active .healthBadgeSummary{color:var(--ink)} - .healthRefreshButton{flex:0 0 auto;padding:0 10px} - .primary,.ghost,.mini,.tab{display:inline-flex;align-items:center;justify-content:center;gap:6px;border-radius:999px;border:1px solid transparent;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} - .themeToggle{white-space:nowrap} - .workspacePanel{display:grid;grid-template-rows:auto auto minmax(0,1fr);gap:6px;min-height:0} - .workspacePanel[hidden]{display:none} - .toolControls{display:grid;gap:8px} - .contentPane{height:100%;min-height:0;overflow:auto;scroll-behavior:smooth;padding-right:4px} - .contentStack{display:grid;gap:8px;min-width:100%;padding-bottom:18px} - .paneTopWrap{display:flex;justify-content:flex-end;position:sticky;bottom:14px;pointer-events:none;padding-right:10px} - .paneTopButton{display:inline-flex;align-items:center;justify-content:center;width:42px;height:42px;border-radius:999px;border:1px solid rgba(255,255,255,.32);background:rgba(31,41,51,.42);color:#fff;font-size:1.1rem;line-height:1;backdrop-filter:blur(12px);box-shadow:0 10px 24px rgba(31,41,51,.18);opacity:0;transform:translateY(8px);visibility:hidden;pointer-events:none;transition:opacity .16s ease,transform .16s ease,background-color .12s ease;z-index:6} - .paneTopButton.visible{opacity:.78;transform:translateY(0);visibility:visible;pointer-events:auto} - .paneTopButton.visible:hover{opacity:1;background:rgba(31,41,51,.62);transform:translateY(-1px)} - .row{display:flex;flex-wrap:wrap;gap:6px;align-items:center} - .primary,.ghost{min-height:34px;padding:0 12px;font-weight:700} - .mini,.tab{min-height:28px;padding:0 10px;font-weight:600} - .primary{background:var(--accent);color:#fff7f2} - .ghost,.mini{background:rgba(255,255,255,.76);border-color:var(--line);color:var(--ink)} - .tab{background:transparent;color:var(--muted)} - .tab.active{background:#fff;border-color:rgba(207,109,60,.18);color:var(--ink)} - .primary:hover,.ghost:hover,.mini:hover,.tab:hover{transform:translateY(-1px)} - .chip{display:inline-flex;align-items:center;gap:7px;min-height:34px;padding:0 10px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);font-weight:600;font-size:.88rem} - .chip input{margin:0;accent-color:var(--accent)} - .chip select{border:none;background:transparent;outline:none;color:var(--ink)} - .search{flex:1 1 260px;min-height:36px;padding:0 12px;border-radius:12px;border:1px solid var(--line);background:rgba(255,255,255,.82);color:var(--ink);outline:none} - .searchCompact{flex:1 1 220px} - .status{display:inline-flex;align-items:center;min-height:32px;padding:0 11px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);color:var(--muted);font-size:.86rem;font-weight:600} - .status.ok{color:var(--ok);border-color:rgba(31,143,101,.22);background:rgba(31,143,101,.08)} - .status.warn{color:var(--warn);border-color:rgba(157,95,0,.22);background:rgba(157,95,0,.08)} - .status.bad{color:var(--bad);border-color:rgba(179,58,58,.22);background:rgba(179,58,58,.08)} - .stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:0} - .stat{border-radius:16px;padding:10px 12px} - .stat span{display:block;margin-bottom:4px;color:var(--muted);font-size:.72rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} - .stat strong{display:block;font-size:clamp(1.6rem,3vw,2rem);line-height:1;letter-spacing:-.05em} - .stat strong.small{font-size:1.1rem;line-height:1.3;letter-spacing:-.02em} - .stat small{display:block;margin-top:4px;color:var(--muted);font-size:.82rem} - .banner,.empty{margin:0;padding:12px 14px;border-radius:14px;border:1px solid var(--line);background:rgba(255,255,255,.82)} - .banner{color:var(--bad);border-color:rgba(179,58,58,.24);background:rgba(179,58,58,.08)} - .list{display:grid;gap:12px;align-content:start} - .logList{display:grid;gap:10px;align-content:start;width:100%} - .card{overflow:hidden;border-radius:16px} - .card.new{border-color:rgba(31,143,101,.3);box-shadow:var(--card-shadow),0 0 0 1px rgba(31,143,101,.12)} - .summary{list-style:none;display:grid;gap:7px;padding:12px 14px;cursor:pointer;background:linear-gradient(180deg,rgba(255,250,244,.88),rgba(255,246,238,.96))} - .summary::-webkit-details-marker{display:none} - .top,.tags,.toolbar,.actions,.attachments{display:flex;flex-wrap:wrap;gap:8px;align-items:center} - .top{justify-content:space-between} - .head{min-width:0;flex:1 1 320px} - .head h2{margin:0;font-size:clamp(1rem,1.6vw,1.22rem);line-height:1.18;letter-spacing:-.03em;word-break:break-word} - .meta{margin:4px 0 0;color:var(--muted);font-size:.88rem;word-break:break-word} - .time,.tag{display:inline-flex;align-items:center;min-height:24px;padding:0 10px;border-radius:999px;font-size:.76rem;font-weight:700} - .time{background:rgba(31,41,51,.06)} - .tag{background:var(--accent-soft);color:#8d5632} - .tag.new{background:rgba(31,143,101,.1);color:var(--ok)} - .tag.bad{background:rgba(179,58,58,.1);color:var(--bad)} - .preview{margin:0;color:#324150;font-size:.9rem} - .body{display:grid;gap:10px;padding:10px 14px 14px;border-top:1px solid rgba(207,109,60,.14);background:var(--card-body)} - .toolbar{justify-content:space-between;align-items:center} - .tabs{display:inline-flex;gap:4px;padding:3px;border-radius:999px;background:rgba(207,109,60,.08)} - .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px} - .metaCard{padding:9px 11px;border-radius:12px;background:rgba(255,255,255,.78);border:1px solid rgba(207,109,60,.12)} - .metaCard dt{margin:0 0 4px;color:var(--muted);font-size:.72rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} - .metaCard dd{margin:0;word-break:break-word} - .attachments{gap:6px} - .attachment{display:inline-flex;align-items:center;gap:8px;padding:7px 10px;border-radius:10px;background:rgba(255,248,240,.96);border:1px solid rgba(207,109,60,.12);font-size:.84rem} - .attachmentLink{color:#8d5632;text-decoration:none;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} - .attachmentLink:hover{transform:translateY(-1px);background:#fff;border-color:rgba(207,109,60,.28)} - .panel{overflow:hidden;border-radius:12px;border:1px solid rgba(207,109,60,.14);background:#fff} - .logEvent{width:100%;overflow:hidden;border-radius:16px;border:1px solid var(--log-line);background:var(--log-shell);box-shadow:var(--log-shadow)} - .secretCard{background:var(--secret-shell);border:1px solid var(--secret-line);box-shadow:var(--secret-shadow)} - .s3Card{background:var(--bucket-shell);border:1px solid var(--bucket-line);box-shadow:var(--bucket-shadow)} - .logSummary{list-style:none;display:grid;gap:7px;padding:10px 12px;cursor:pointer} - .logSummary::-webkit-details-marker{display:none} - .secretSummary{background:linear-gradient(180deg,rgba(244,253,248,.9),rgba(236,249,242,.96))} - .s3Summary{background:linear-gradient(180deg,rgba(255,251,238,.92),rgba(255,246,223,.98))} - .logSummaryTop{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:center} - .logMeta{display:flex;flex-wrap:wrap;gap:8px;align-items:center} - .logSummaryActions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end} - .logTag{background:var(--info-soft);color:var(--info);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} - .secretTag{background:var(--secret-soft);color:var(--secret);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} - .bucketTag{background:var(--bucket-soft);color:var(--bucket);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} - .logPreview{margin:0;color:#324150;font:600 .88rem/1.45 "Cascadia Code","Consolas",monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} - .logBody{padding:8px 12px 12px;border-top:1px solid rgba(48,113,169,.14);background:var(--log-body)} - .secretBody{border-top-color:rgba(31,143,101,.14);background:var(--secret-body)} - .s3Body{border-top-color:rgba(181,137,37,.18);background:var(--bucket-body)} - .logCopyButton{box-shadow:none} - .logBody pre{border-radius:12px;border:1px solid rgba(48,113,169,.14);padding:12px;background:linear-gradient(180deg,rgba(48,113,169,.04),transparent 140px),#fff} - .secretValuePanel{display:grid;gap:10px} - .secretValuePanel pre{border-radius:12px;border:1px solid rgba(31,143,101,.14);padding:12px;background:linear-gradient(180deg,rgba(31,143,101,.04),transparent 140px),#fff} - .s3PreviewPanel{display:grid;gap:10px} - .s3PreviewImage{max-width:min(100%,640px);border-radius:12px;border:1px solid rgba(181,137,37,.16);background:#fff} - .logBody.wrapOff pre{white-space:pre;word-break:normal} - .tag.levelError{background:rgba(179,58,58,.12);color:var(--bad)} - .tag.levelWarn{background:rgba(157,95,0,.12);color:var(--warn)} - .tag.levelInfo{background:rgba(48,113,169,.12);color:var(--info)} - .tag.levelDebug{background:rgba(96,112,128,.12);color:var(--muted)} - .jsonSyntax .jsonKey{color:#b55f2d} - .jsonSyntax .jsonString{color:#1f8f65} - .jsonSyntax .jsonNumber{color:#2f6ea9} - .jsonSyntax .jsonBoolean{color:#9d5f00} - .jsonSyntax .jsonNull{color:#b33a3a} - iframe{width:100%;min-height:560px;border:none;background:#fff} - pre{margin:0;padding:12px;white-space:pre-wrap;word-break:break-word;overflow:auto;font:12.5px/1.45 "Cascadia Code","Consolas",monospace;color:#102030;background:linear-gradient(180deg,rgba(207,109,60,.04),transparent 140px),#fff} - .placeholder,.inlineError{padding:12px} - .inlineError{color:var(--bad)} - body[data-theme="dark"]{color-scheme:dark;background:radial-gradient(circle at top left,rgba(207,109,60,.12),transparent 28%),radial-gradient(circle at top right,rgba(48,113,169,.12),transparent 26%),linear-gradient(180deg,#10161d,#17202a)} - body[data-theme="dark"] .heroShell, - body[data-theme="dark"] .toolControls, - body[data-theme="dark"] .stat{background:rgba(15,21,30,.84);border-color:rgba(148,163,184,.16);box-shadow:0 14px 30px rgba(0,0,0,.32)} - body[data-theme="dark"] .card{background:linear-gradient(180deg,rgba(50,35,28,.96),rgba(31,25,22,.98));border-color:rgba(207,109,60,.24);box-shadow:0 18px 34px rgba(0,0,0,.34)} - body[data-theme="dark"] .logEvent{background:linear-gradient(180deg,rgba(18,31,45,.96),rgba(14,24,36,.98));border-color:rgba(73,144,204,.22);box-shadow:0 16px 32px rgba(0,0,0,.34)} - body[data-theme="dark"] .secretCard{background:linear-gradient(180deg,rgba(19,39,31,.96),rgba(14,30,24,.98));border-color:rgba(64,170,126,.22);box-shadow:0 16px 32px rgba(0,0,0,.34)} - body[data-theme="dark"] .s3Card{background:linear-gradient(180deg,rgba(52,42,17,.96),rgba(37,30,13,.98));border-color:rgba(181,137,37,.24);box-shadow:0 16px 32px rgba(0,0,0,.34)} - body[data-theme="dark"] .healthBadge{background:rgba(15,21,30,.84);border-color:rgba(148,163,184,.18);box-shadow:0 10px 22px rgba(0,0,0,.28)} - body[data-theme="dark"] .healthBadge.active{border-color:rgba(207,109,60,.32);background:rgba(207,109,60,.18);box-shadow:0 12px 26px rgba(0,0,0,.3)} - body[data-theme="dark"] .healthBadge.active .healthBadgeName, - body[data-theme="dark"] .healthBadge.active .healthBadgeSummary{color:#f8ede6} - body[data-theme="dark"] .tab{color:#aab8c8} - body[data-theme="dark"] .tab.active, - body[data-theme="dark"] .ghost, - body[data-theme="dark"] .mini, - body[data-theme="dark"] .chip, - body[data-theme="dark"] .status, - body[data-theme="dark"] .search{background:rgba(18,25,35,.88);border-color:rgba(148,163,184,.18);color:#edf2f7} - body[data-theme="dark"] .chip select, - body[data-theme="dark"] .search::placeholder{color:#9fb0c2} - body[data-theme="dark"] .ghost, - body[data-theme="dark"] .mini, - body[data-theme="dark"] .tab.active{border-color:rgba(148,163,184,.18)} - body[data-theme="dark"] .summary{background:linear-gradient(180deg,rgba(58,40,31,.88),rgba(45,33,28,.96))} - body[data-theme="dark"] .body{background:#211a17;border-top-color:rgba(207,109,60,.18)} - body[data-theme="dark"] .logSummary{background:linear-gradient(180deg,rgba(21,34,47,.94),rgba(16,27,39,.98))} - body[data-theme="dark"] .logBody{background:#13212d;border-top-color:rgba(73,144,204,.18)} - body[data-theme="dark"] .secretSummary{background:linear-gradient(180deg,rgba(21,43,34,.94),rgba(16,34,27,.98))} - body[data-theme="dark"] .secretBody{background:#12241c;border-top-color:rgba(64,170,126,.18)} - body[data-theme="dark"] .s3Summary{background:linear-gradient(180deg,rgba(53,41,19,.94),rgba(39,31,15,.98))} - body[data-theme="dark"] .s3Body{background:#241d10;border-top-color:rgba(181,137,37,.18)} - body[data-theme="dark"] .metaCard{background:rgba(17,25,35,.64);border-color:rgba(148,163,184,.14)} - body[data-theme="dark"] .attachment{background:rgba(50,35,28,.9);border-color:rgba(207,109,60,.18)} - body[data-theme="dark"] .attachmentLink{color:#f6c4a9} - body[data-theme="dark"] .attachmentLink:hover{background:rgba(75,52,39,.96);border-color:rgba(246,196,169,.26)} - body[data-theme="dark"] .panel, - body[data-theme="dark"] pre, - body[data-theme="dark"] .logBody pre{background:linear-gradient(180deg,rgba(73,144,204,.06),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} - body[data-theme="dark"] .secretValuePanel pre{background:linear-gradient(180deg,rgba(64,170,126,.08),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} - body[data-theme="dark"] .s3PreviewPanel pre{background:linear-gradient(180deg,rgba(181,137,37,.08),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} - body[data-theme="dark"] .panel{border-color:rgba(148,163,184,.14)} - body[data-theme="dark"] .banner, - body[data-theme="dark"] .empty{background:rgba(15,21,30,.82);border-color:rgba(148,163,184,.16)} - body[data-theme="dark"] .time{background:rgba(148,163,184,.12);color:#e8edf3} - body[data-theme="dark"] .tag{background:rgba(207,109,60,.14);color:#f0c2aa} - body[data-theme="dark"] .logTag{background:rgba(73,144,204,.16);color:#93cfff} - body[data-theme="dark"] .secretTag{background:rgba(64,170,126,.16);color:#9fe0be} - body[data-theme="dark"] .bucketTag{background:rgba(181,137,37,.16);color:#f1d38c} - body[data-theme="dark"] .preview, - body[data-theme="dark"] .logPreview, - body[data-theme="dark"] .metaCard dd, - body[data-theme="dark"] .head h2, - body[data-theme="dark"] .stat strong, - body[data-theme="dark"] h1{color:#edf2f7} - body[data-theme="dark"] .jsonSyntax .jsonKey{color:#f0b08a} - body[data-theme="dark"] .jsonSyntax .jsonString{color:#80d5b0} - body[data-theme="dark"] .jsonSyntax .jsonNumber{color:#94c9ff} - body[data-theme="dark"] .jsonSyntax .jsonBoolean{color:#f0c274} - body[data-theme="dark"] .jsonSyntax .jsonNull{color:#ff9c9c} - body[data-theme="dark"] .meta, - body[data-theme="dark"] .helper, - body[data-theme="dark"] .lede, - body[data-theme="dark"] .stat small, - body[data-theme="dark"] .stat span, - body[data-theme="dark"] .chip, - body[data-theme="dark"] .tab{color:#aab8c8} - body[data-theme="dark"] .paneTopButton{border-color:rgba(255,255,255,.18);background:rgba(8,12,18,.58);color:#edf2f7} - body[data-theme="dark"] .paneTopButton.visible:hover{background:rgba(8,12,18,.8)} - @media (max-width:1080px){.stats{grid-template-columns:repeat(2,minmax(0,1fr))}} - @media (max-width:720px){.page{padding:12px}.heroShell,.heroTopRow,.toolbar,.row,.heroActions{align-items:stretch}.heroTopRow{justify-content:stretch;flex-basis:100%}.heroStatusRow{align-items:flex-start}.heroStatusLabel,.healthStrip{flex-basis:100%}.primary,.ghost,.chip,.themeToggle{width:100%;justify-content:center}.healthBadge{justify-content:flex-start}.logSummaryTop,.logSummaryActions{align-items:flex-start}.contentPane{min-height:300px}iframe{min-height:420px}} - `; -} - -function escapeHtml(value) { - return String(value ?? "") - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -function clientApp(config) { - const THEME_STORAGE_KEY = "localstack-inspector-theme"; - const PANEL_STORAGE_KEY = "localstack-inspector-panel"; - const EMAIL_PREFERENCES_STORAGE_KEY = "localstack-inspector-email-preferences"; - const LOG_PREFERENCES_STORAGE_KEY = "localstack-inspector-log-preferences"; - const SECRET_PREFERENCES_STORAGE_KEY = "localstack-inspector-secret-preferences"; - const S3_PREFERENCES_STORAGE_KEY = "localstack-inspector-s3-preferences"; - const HEALTH_REFRESH_MS = 30000; - const REFRESH_INTERVALS = [5000, 10000, 15000, 30000, 60000]; - const LOG_WINDOWS = [300000, 900000, 3600000, 21600000, 86400000]; - const LOG_LIMITS = [100, 200, 300, 500]; - const storedEmailPreferences = getStoredPreferences(EMAIL_PREFERENCES_STORAGE_KEY); - const storedLogPreferences = getStoredPreferences(LOG_PREFERENCES_STORAGE_KEY); - const storedSecretPreferences = getStoredPreferences(SECRET_PREFERENCES_STORAGE_KEY); - const storedS3Preferences = getStoredPreferences(S3_PREFERENCES_STORAGE_KEY); - - const appState = { - panel: getInitialPanel(), - logsReady: false, - secretsReady: false, - s3Ready: false, - theme: getInitialTheme() - }; - - const state = { - messages: [], - filtered: [], - search: getStoredText(storedEmailPreferences?.search), - auto: getStoredBoolean(storedEmailPreferences?.auto, true), - interval: getStoredNumber(storedEmailPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs), - loading: false, - error: "", - updatedAt: 0, - source: "initial", - duration: 0, - parseErrors: 0, - newest: "", - newIds: new Set(), - knownIds: new Set(), - openIds: new Set(), - views: {}, - raw: {}, - listSignature: "" - }; - - const logsState = { - groups: [], - streams: [], - events: [], - filtered: [], - group: getStoredText(storedLogPreferences?.group, config.defaultLogGroup || ""), - stream: getStoredText(storedLogPreferences?.stream), - search: getStoredText(storedLogPreferences?.search), - auto: getStoredBoolean(storedLogPreferences?.auto, true), - interval: getStoredNumber(storedLogPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs), - windowMs: getStoredNumber(storedLogPreferences?.windowMs, LOG_WINDOWS, config.defaultLogWindowMs), - limit: getStoredNumber(storedLogPreferences?.limit, LOG_LIMITS, config.defaultLogLimit), - wrapLines: getStoredBoolean(storedLogPreferences?.wrapLines, true), - tailNewest: getStoredBoolean(storedLogPreferences?.tailNewest, false), - loading: false, - error: "", - updatedAt: 0, - source: "initial", - duration: 0, - newest: 0, - searchedLogStreams: 0, - openIds: new Set(), - listSignature: "" - }; - - const secretsState = { - items: [], - filtered: [], - search: getStoredText(storedSecretPreferences?.search), - auto: getStoredBoolean(storedSecretPreferences?.auto, true), - interval: getStoredNumber(storedSecretPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs), - loading: false, - error: "", - updatedAt: 0, - source: "initial", - duration: 0, - newest: "", - openIds: new Set(), - values: {}, - revealedIds: new Set(), - listSignature: "" - }; - - const s3State = { - buckets: [], - objects: [], - filtered: [], - bucket: getStoredText(storedS3Preferences?.bucket, config.defaultS3Bucket || ""), - prefix: getStoredText(storedS3Preferences?.prefix), - search: getStoredText(storedS3Preferences?.search), - auto: getStoredBoolean(storedS3Preferences?.auto, true), - interval: getStoredNumber(storedS3Preferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs), - loading: false, - error: "", - updatedAt: 0, - source: "initial", - duration: 0, - newest: "", - openIds: new Set(), - previews: {}, - listSignature: "" - }; - - const healthState = { - services: {}, - loading: false, - error: "", - updatedAt: 0, - source: "initial" - }; - - const el = { - themeToggle: document.getElementById("themeToggle"), - resetStateButton: document.getElementById("resetStateButton"), - healthRefreshButton: document.getElementById("healthRefreshButton"), - healthStrip: document.getElementById("healthStrip"), - emailsPanel: document.getElementById("emailsPanel"), - logsPanel: document.getElementById("logsPanel"), - secretsPanel: document.getElementById("secretsPanel"), - s3Panel: document.getElementById("s3Panel"), - refreshButton: document.getElementById("refreshButton"), - autoToggle: document.getElementById("autoToggle"), - intervalSelect: document.getElementById("intervalSelect"), - searchInput: document.getElementById("searchInput"), - clearSearchButton: document.getElementById("clearSearchButton"), - expandAllButton: document.getElementById("expandAllButton"), - collapseAllButton: document.getElementById("collapseAllButton"), - scrollToTopButton: document.getElementById("scrollToTopButton"), - statusChip: document.getElementById("statusChip"), - totalStat: document.getElementById("totalStat"), - visibleStat: document.getElementById("visibleStat"), - newStat: document.getElementById("newStat"), - newestStat: document.getElementById("newestStat"), - updatedStat: document.getElementById("updatedStat"), - fetchStat: document.getElementById("fetchStat"), - fetchDetail: document.getElementById("fetchDetail"), - banner: document.getElementById("banner"), - empty: document.getElementById("empty"), - list: document.getElementById("list"), - emailsContentPane: document.getElementById("emailsContentPane"), - logsRefreshButton: document.getElementById("logsRefreshButton"), - logsAutoToggle: document.getElementById("logsAutoToggle"), - logsIntervalSelect: document.getElementById("logsIntervalSelect"), - logsGroupSelect: document.getElementById("logsGroupSelect"), - logsStreamSelect: document.getElementById("logsStreamSelect"), - logsWindowSelect: document.getElementById("logsWindowSelect"), - logsLimitSelect: document.getElementById("logsLimitSelect"), - logsSearchInput: document.getElementById("logsSearchInput"), - logsClearSearchButton: document.getElementById("logsClearSearchButton"), - logsWrapToggle: document.getElementById("logsWrapToggle"), - logsTailToggle: document.getElementById("logsTailToggle"), - logsExpandAllButton: document.getElementById("logsExpandAllButton"), - logsCollapseAllButton: document.getElementById("logsCollapseAllButton"), - logsScrollToTopButton: document.getElementById("logsScrollToTopButton"), - logsStatusChip: document.getElementById("logsStatusChip"), - logsTotalStat: document.getElementById("logsTotalStat"), - logsVisibleStat: document.getElementById("logsVisibleStat"), - logsStreamsStat: document.getElementById("logsStreamsStat"), - logsNewestStat: document.getElementById("logsNewestStat"), - logsUpdatedStat: document.getElementById("logsUpdatedStat"), - logsFetchStat: document.getElementById("logsFetchStat"), - logsFetchDetail: document.getElementById("logsFetchDetail"), - logsBanner: document.getElementById("logsBanner"), - logsEmpty: document.getElementById("logsEmpty"), - logsList: document.getElementById("logsList"), - logsContentPane: document.getElementById("logsContentPane"), - secretsRefreshButton: document.getElementById("secretsRefreshButton"), - secretsAutoToggle: document.getElementById("secretsAutoToggle"), - secretsIntervalSelect: document.getElementById("secretsIntervalSelect"), - secretsSearchInput: document.getElementById("secretsSearchInput"), - secretsClearSearchButton: document.getElementById("secretsClearSearchButton"), - secretsExpandAllButton: document.getElementById("secretsExpandAllButton"), - secretsCollapseAllButton: document.getElementById("secretsCollapseAllButton"), - secretsScrollToTopButton: document.getElementById("secretsScrollToTopButton"), - secretsStatusChip: document.getElementById("secretsStatusChip"), - secretsTotalStat: document.getElementById("secretsTotalStat"), - secretsVisibleStat: document.getElementById("secretsVisibleStat"), - secretsLoadedStat: document.getElementById("secretsLoadedStat"), - secretsNewestStat: document.getElementById("secretsNewestStat"), - secretsUpdatedStat: document.getElementById("secretsUpdatedStat"), - secretsFetchStat: document.getElementById("secretsFetchStat"), - secretsFetchDetail: document.getElementById("secretsFetchDetail"), - secretsBanner: document.getElementById("secretsBanner"), - secretsEmpty: document.getElementById("secretsEmpty"), - secretsList: document.getElementById("secretsList"), - secretsContentPane: document.getElementById("secretsContentPane"), - s3RefreshButton: document.getElementById("s3RefreshButton"), - s3AutoToggle: document.getElementById("s3AutoToggle"), - s3IntervalSelect: document.getElementById("s3IntervalSelect"), - s3BucketSelect: document.getElementById("s3BucketSelect"), - s3PrefixInput: document.getElementById("s3PrefixInput"), - s3ApplyPrefixButton: document.getElementById("s3ApplyPrefixButton"), - s3SearchInput: document.getElementById("s3SearchInput"), - s3ClearSearchButton: document.getElementById("s3ClearSearchButton"), - s3ExpandAllButton: document.getElementById("s3ExpandAllButton"), - s3CollapseAllButton: document.getElementById("s3CollapseAllButton"), - s3ScrollToTopButton: document.getElementById("s3ScrollToTopButton"), - s3StatusChip: document.getElementById("s3StatusChip"), - s3TotalStat: document.getElementById("s3TotalStat"), - s3VisibleStat: document.getElementById("s3VisibleStat"), - s3BucketsStat: document.getElementById("s3BucketsStat"), - s3NewestStat: document.getElementById("s3NewestStat"), - s3UpdatedStat: document.getElementById("s3UpdatedStat"), - s3FetchStat: document.getElementById("s3FetchStat"), - s3FetchDetail: document.getElementById("s3FetchDetail"), - s3Banner: document.getElementById("s3Banner"), - s3Empty: document.getElementById("s3Empty"), - s3List: document.getElementById("s3List"), - s3ContentPane: document.getElementById("s3ContentPane") - }; - - el.autoToggle.checked = state.auto; - el.intervalSelect.value = String(state.interval); - el.searchInput.value = state.search; - el.logsAutoToggle.checked = logsState.auto; - el.logsIntervalSelect.value = String(logsState.interval); - el.logsWindowSelect.value = String(logsState.windowMs); - el.logsLimitSelect.value = String(logsState.limit); - el.logsSearchInput.value = logsState.search; - el.logsWrapToggle.checked = logsState.wrapLines; - el.logsTailToggle.checked = logsState.tailNewest; - el.secretsAutoToggle.checked = secretsState.auto; - el.secretsIntervalSelect.value = String(secretsState.interval); - el.secretsSearchInput.value = secretsState.search; - el.s3AutoToggle.checked = s3State.auto; - el.s3IntervalSelect.value = String(s3State.interval); - el.s3PrefixInput.value = s3State.prefix; - el.s3SearchInput.value = s3State.search; - applyTheme(appState.theme); - persistPanel(); - persistEmailPreferences(); - persistLogPreferences(); - persistSecretPreferences(); - persistS3Preferences(); - wire(); - renderWorkspace(); - renderAll(); - renderLogsAll(); - renderSecretsAll(); - renderS3All(); - renderHealthStrip(); - if (appState.panel === "logs") { - ensureLogsReady(); - } else if (appState.panel === "secrets") { - ensureSecretsReady(); - } else if (appState.panel === "s3") { - ensureS3Ready(); - } else { - refreshMessages("initial"); - } - refreshHealthSummary("initial"); - window.setInterval(() => { - if (!document.hidden) { - refreshHealthSummary("auto"); - } - }, HEALTH_REFRESH_MS); - window.setInterval(() => { - renderLiveClock(); - renderLogsLiveClock(); - renderSecretsLiveClock(); - renderS3LiveClock(); - renderHealthStrip(); - }, 1000); - - function wire() { - el.themeToggle.addEventListener("click", () => { - applyTheme(appState.theme === "dark" ? "light" : "dark"); - }); - - el.resetStateButton.addEventListener("click", () => { - resetSavedState(); - }); - - el.healthRefreshButton.addEventListener("click", () => { - refreshHealthSummary("manual"); - }); - - el.healthStrip.addEventListener("click", async (event) => { - const button = event.target.closest("button[data-health-panel]"); - - if (!button) { - return; - } - - await setPanel(button.dataset.healthPanel); - }); - - el.refreshButton.addEventListener("click", () => refreshMessages("manual")); - - el.autoToggle.addEventListener("change", () => { - state.auto = el.autoToggle.checked; - persistEmailPreferences(); - scheduleRefresh(); - renderStatus(); - }); - - el.intervalSelect.addEventListener("change", () => { - state.interval = Number(el.intervalSelect.value) || config.defaultRefreshMs; - persistEmailPreferences(); - scheduleRefresh(); - renderStatus(); - }); - - el.searchInput.addEventListener("input", (event) => { - state.search = event.target.value; - applyFilter(); - }); - - el.clearSearchButton.addEventListener("click", () => { - state.search = ""; - el.searchInput.value = ""; - applyFilter(); - }); - - el.expandAllButton.addEventListener("click", () => { - state.filtered.forEach((message) => state.openIds.add(message.id)); - syncCardExpansion(); - }); - - el.collapseAllButton.addEventListener("click", () => { - state.openIds.clear(); - syncCardExpansion(); - }); - - el.scrollToTopButton.addEventListener("click", () => { - scrollPaneToTop(el.emailsContentPane); - }); - - el.emailsContentPane.addEventListener("scroll", () => { - updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); - }); - - el.list.addEventListener("click", async (event) => { - const button = event.target.closest("button[data-action]"); - - if (!button) { - return; - } - - const id = button.dataset.id; - const message = getMessage(id); - - if (!message) { - return; - } - - if (button.dataset.action === "view") { - state.views[id] = button.dataset.view; - renderList(); - return; - } - - if (button.dataset.action === "load-raw") { - await loadRaw(id); - renderList(); - return; - } - - if (button.dataset.action === "copy-raw") { - const raw = await loadRaw(id); - - if (raw) { - await copyText(raw); - setStatus("Raw message copied to the clipboard.", "ok"); - } - } - }); - - el.logsRefreshButton.addEventListener("click", () => refreshLogs("manual")); - - el.logsAutoToggle.addEventListener("change", () => { - logsState.auto = el.logsAutoToggle.checked; - persistLogPreferences(); - scheduleLogsRefresh(); - renderLogsStatus(); - }); - - el.logsIntervalSelect.addEventListener("change", () => { - logsState.interval = Number(el.logsIntervalSelect.value) || config.defaultRefreshMs; - persistLogPreferences(); - scheduleLogsRefresh(); - renderLogsStatus(); - }); - - el.logsWindowSelect.addEventListener("change", () => { - logsState.windowMs = Number(el.logsWindowSelect.value) || config.defaultLogWindowMs; - persistLogPreferences(); - refreshLogs("window"); - }); - - el.logsLimitSelect.addEventListener("change", () => { - logsState.limit = Number(el.logsLimitSelect.value) || config.defaultLogLimit; - persistLogPreferences(); - refreshLogs("limit"); - }); - - el.logsSearchInput.addEventListener("input", (event) => { - logsState.search = event.target.value; - applyLogsFilter(); - }); - - el.logsClearSearchButton.addEventListener("click", () => { - logsState.search = ""; - el.logsSearchInput.value = ""; - applyLogsFilter(); - }); - - el.logsWrapToggle.addEventListener("change", () => { - logsState.wrapLines = el.logsWrapToggle.checked; - persistLogPreferences(); - renderLogsList(); - }); - - el.logsTailToggle.addEventListener("change", () => { - logsState.tailNewest = el.logsTailToggle.checked; - persistLogPreferences(); - }); - - el.logsExpandAllButton.addEventListener("click", () => { - logsState.filtered.forEach((event) => logsState.openIds.add(event.id)); - renderLogsList(); - }); - - el.logsCollapseAllButton.addEventListener("click", () => { - logsState.openIds.clear(); - renderLogsList(); - }); - - el.logsScrollToTopButton.addEventListener("click", () => { - scrollPaneToTop(el.logsContentPane); - }); - - el.logsContentPane.addEventListener("scroll", () => { - updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); - }); - - el.logsGroupSelect.addEventListener("change", async () => { - logsState.group = el.logsGroupSelect.value; - logsState.stream = ""; - persistLogPreferences(); - await refreshLogStreams(); - await refreshLogs("group"); - }); - - el.logsStreamSelect.addEventListener("change", () => { - logsState.stream = el.logsStreamSelect.value; - persistLogPreferences(); - refreshLogs("stream"); - }); - - el.logsList.addEventListener("click", async (event) => { - const button = event.target.closest("button[data-log-action]"); - - if (!button) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - - const logEvent = getLogEvent(button.dataset.id); - - if (!logEvent) { - return; - } - - if (button.dataset.logAction === "copy") { - await copyText(formatLogMessage(logEvent.message)); - setLogsStatus("Log payload copied to the clipboard.", "ok"); - } - }); - - el.secretsRefreshButton.addEventListener("click", () => refreshSecrets("manual")); - - el.secretsAutoToggle.addEventListener("change", () => { - secretsState.auto = el.secretsAutoToggle.checked; - persistSecretPreferences(); - scheduleSecretsRefresh(); - renderSecretsStatus(); - }); - - el.secretsIntervalSelect.addEventListener("change", () => { - secretsState.interval = Number(el.secretsIntervalSelect.value) || config.defaultRefreshMs; - persistSecretPreferences(); - scheduleSecretsRefresh(); - renderSecretsStatus(); - }); - - el.secretsSearchInput.addEventListener("input", (event) => { - secretsState.search = event.target.value; - applySecretsFilter(); - }); - - el.secretsClearSearchButton.addEventListener("click", () => { - secretsState.search = ""; - el.secretsSearchInput.value = ""; - applySecretsFilter(); - }); - - el.secretsExpandAllButton.addEventListener("click", () => { - secretsState.filtered.forEach((secret) => secretsState.openIds.add(secret.id)); - syncSecretExpansion(); - }); - - el.secretsCollapseAllButton.addEventListener("click", () => { - secretsState.openIds.clear(); - syncSecretExpansion(); - }); - - el.secretsScrollToTopButton.addEventListener("click", () => { - scrollPaneToTop(el.secretsContentPane); - }); - - el.secretsContentPane.addEventListener("scroll", () => { - updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); - }); - - el.secretsList.addEventListener("click", async (event) => { - const button = event.target.closest("button[data-secret-action]"); - - if (!button) { - return; - } - - const id = button.dataset.id; - const secret = getSecret(id); - - if (!secret) { - return; - } - - if (button.dataset.secretAction === "load-value") { - await ensureSecretValue(id, { force: false }); - return; - } - - if (button.dataset.secretAction === "reload-value") { - await ensureSecretValue(id, { force: true }); - return; - } - - if (button.dataset.secretAction === "copy-value") { - const entry = await ensureSecretValue(id, { force: false }); - - if (entry?.status === "loaded") { - await copyText(entry.copyValue); - setSecretsStatus("Secret value copied to the clipboard.", "ok"); - } - - return; - } - - if (button.dataset.secretAction === "toggle-reveal") { - toggleSecretReveal(id); - return; - } - - if (button.dataset.secretAction === "copy-name") { - await copyText(secret.name || ""); - setSecretsStatus("Secret name copied to the clipboard.", "ok"); - return; - } - - if (button.dataset.secretAction === "copy-arn") { - await copyText(secret.arn || ""); - setSecretsStatus("Secret ARN copied to the clipboard.", "ok"); - return; - } - - if (button.dataset.secretAction === "copy-env") { - const entry = await ensureSecretValue(id, { force: false }); - - if (entry?.status === "loaded") { - await copyText(`${secret.name}=${entry.copyValue}`); - setSecretsStatus("Secret env line copied to the clipboard.", "ok"); - } - } - }); - - el.s3RefreshButton.addEventListener("click", async () => { - await refreshS3Buckets(); - await refreshS3("manual"); - }); - - el.s3AutoToggle.addEventListener("change", () => { - s3State.auto = el.s3AutoToggle.checked; - persistS3Preferences(); - scheduleS3Refresh(); - renderS3Status(); - }); - - el.s3IntervalSelect.addEventListener("change", () => { - s3State.interval = Number(el.s3IntervalSelect.value) || config.defaultRefreshMs; - persistS3Preferences(); - scheduleS3Refresh(); - renderS3Status(); - }); - - el.s3BucketSelect.addEventListener("change", () => { - s3State.bucket = el.s3BucketSelect.value; - persistS3Preferences(); - refreshS3("bucket"); - }); - - el.s3PrefixInput.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - event.preventDefault(); - s3State.prefix = el.s3PrefixInput.value.trim(); - persistS3Preferences(); - refreshS3("prefix"); - } - }); - - el.s3ApplyPrefixButton.addEventListener("click", () => { - s3State.prefix = el.s3PrefixInput.value.trim(); - persistS3Preferences(); - refreshS3("prefix"); - }); - - el.s3SearchInput.addEventListener("input", (event) => { - s3State.search = event.target.value; - applyS3Filter(); - }); - - el.s3ClearSearchButton.addEventListener("click", () => { - s3State.search = ""; - el.s3SearchInput.value = ""; - applyS3Filter(); - }); - - el.s3ExpandAllButton.addEventListener("click", () => { - s3State.filtered.forEach((object) => s3State.openIds.add(object.id)); - syncS3Expansion(); - }); - - el.s3CollapseAllButton.addEventListener("click", () => { - s3State.openIds.clear(); - syncS3Expansion(); - }); - - el.s3ScrollToTopButton.addEventListener("click", () => { - scrollPaneToTop(el.s3ContentPane); - }); - - el.s3ContentPane.addEventListener("scroll", () => { - updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); - }); - - el.s3List.addEventListener("click", async (event) => { - const button = event.target.closest("button[data-s3-action]"); - - if (!button) { - return; - } - - const id = button.dataset.id; - const object = getS3Object(id); - - if (!object) { - return; - } - - if (button.dataset.s3Action === "load-preview") { - await ensureS3Preview(id, { force: false }); - return; - } - - if (button.dataset.s3Action === "reload-preview") { - await ensureS3Preview(id, { force: true }); - return; - } - - if (button.dataset.s3Action === "copy-key") { - await copyText(object.key); - setS3Status("Object key copied to the clipboard.", "ok"); - return; - } - - if (button.dataset.s3Action === "copy-uri") { - await copyText(`s3://${object.bucket}/${object.key}`); - setS3Status("S3 URI copied to the clipboard.", "ok"); - } - }); - - document.addEventListener("visibilitychange", () => { - window.clearTimeout(state.timer); - window.clearTimeout(logsState.timer); - window.clearTimeout(secretsState.timer); - window.clearTimeout(s3State.timer); - - if (document.hidden) { - renderStatus(); - renderLogsStatus(); - renderSecretsStatus(); - renderS3Status(); - return; - } - - refreshHealthSummary("visibility"); - - if (appState.panel === "s3") { - if (s3State.auto) { - refreshS3("visibility"); - } else { - renderS3Status(); - } - return; - } - - if (appState.panel === "secrets") { - if (secretsState.auto) { - refreshSecrets("visibility"); - } else { - renderSecretsStatus(); - } - return; - } - - if (appState.panel === "logs") { - if (logsState.auto) { - refreshLogs("visibility"); - } else { - renderLogsStatus(); - } - return; - } - - if (state.auto) { - refreshMessages("visibility"); - } else { - renderStatus(); - } - }); - - window.addEventListener("keydown", (event) => { - const isField = - event.target instanceof HTMLElement && - (event.target.matches("input,textarea,select") || event.target.isContentEditable); - - if (!isField && event.key.toLowerCase() === "r") { - event.preventDefault(); - - if (appState.panel === "logs") { - refreshLogs("keyboard"); - return; - } - - if (appState.panel === "secrets") { - refreshSecrets("keyboard"); - return; - } - - if (appState.panel === "s3") { - refreshS3("keyboard"); - return; - } - - refreshMessages("keyboard"); - } - }); - - updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); - updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); - updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); - updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); - } - - async function setPanel(panel) { - if (!panel || panel === appState.panel) { - if (panel === "logs") { - await ensureLogsReady(); - } else if (panel === "secrets") { - await ensureSecretsReady(); - } else if (panel === "s3") { - await ensureS3Ready(); - } - - renderWorkspace(); - return; - } - - appState.panel = panel; - persistPanel(); - renderWorkspace(); - - if (panel === "logs") { - await ensureLogsReady(); - } else if (panel === "secrets") { - await ensureSecretsReady(); - } else if (panel === "s3") { - await ensureS3Ready(); - } else if (!state.updatedAt && !state.loading) { - await refreshMessages("panel"); - } - - scheduleRefresh(); - scheduleLogsRefresh(); - scheduleSecretsRefresh(); - scheduleS3Refresh(); - renderStatus(); - renderLogsStatus(); - renderSecretsStatus(); - renderS3Status(); - } - - function renderWorkspace() { - el.emailsPanel.hidden = appState.panel !== "emails"; - el.logsPanel.hidden = appState.panel !== "logs"; - el.secretsPanel.hidden = appState.panel !== "secrets"; - el.s3Panel.hidden = appState.panel !== "s3"; - renderHealthStrip(); - } - - async function ensureLogsReady() { - if (appState.logsReady) { - return; - } - - await refreshLogGroups(); - appState.logsReady = !logsState.error; - - if (logsState.group) { - await refreshLogs("initial"); - } - } - - async function ensureSecretsReady() { - if (appState.secretsReady) { - return; - } - - await refreshSecrets("initial"); - appState.secretsReady = !secretsState.error; - } - - async function ensureS3Ready() { - if (appState.s3Ready) { - return; - } - - await refreshS3Buckets(); - appState.s3Ready = !s3State.error; - - if (s3State.bucket) { - await refreshS3("initial"); - } - } - - async function refreshMessages(source) { - if (state.loading) { - return; - } - - let shouldRenderList = false; - - state.loading = true; - state.source = source; - state.error = ""; - renderStatus(); - renderFetch(); - - try { - const response = await fetch("/api/messages", { cache: "no-store" }); - - if (!response.ok) { - const payload = await safeJson(response); - throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); - } - - const payload = await response.json(); - const messages = Array.isArray(payload.messages) ? payload.messages : []; - const nextIds = new Set(messages.map((message) => message.id)); - const nextSignature = computeListSignature(messages); - - shouldRenderList = nextSignature !== state.listSignature; - state.newIds = - state.updatedAt && shouldRenderList - ? new Set(messages.filter((message) => !state.knownIds.has(message.id)).map((message) => message.id)) - : state.newIds; - state.knownIds = nextIds; - state.messages = messages; - state.duration = payload.fetchDurationMs || 0; - state.parseErrors = payload.parseErrors || 0; - state.newest = payload.latestMessageTimestamp || ""; - state.updatedAt = Date.now(); - state.listSignature = nextSignature; - - pruneState(); - applyFilter(shouldRenderList); - setStatus(`Updated ${messages.length} message${messages.length === 1 ? "" : "s"}.`, "ok"); - } catch (error) { - state.error = error.message || "Unknown refresh error"; - setStatus(`Refresh failed: ${state.error}`, "bad"); - } finally { - state.loading = false; - scheduleRefresh(); - renderAll({ renderList: shouldRenderList }); - } - } - - async function refreshLogGroups() { - try { - const response = await fetch("/api/logs/groups", { cache: "no-store" }); - - if (!response.ok) { - const payload = await safeJson(response); - throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); - } - - const payload = await response.json(); - logsState.groups = Array.isArray(payload.groups) ? payload.groups : []; - - const availableGroups = logsState.groups.map((group) => group.name).filter(Boolean); - - if (!availableGroups.includes(logsState.group)) { - logsState.group = availableGroups.includes(config.defaultLogGroup) - ? config.defaultLogGroup - : availableGroups[0] || ""; - } - - logsState.error = ""; - await refreshLogStreams(); - } catch (error) { - logsState.error = error.message || "Unknown log group refresh error"; - } finally { - persistLogPreferences(); - renderLogsAll(); - } - } - - async function refreshLogStreams() { - if (!logsState.group) { - logsState.streams = []; - logsState.stream = ""; - renderLogsAll(); - return; - } - - try { - const response = await fetch(`/api/logs/streams?group=${encodeURIComponent(logsState.group)}`, { - cache: "no-store" - }); - - if (!response.ok) { - const payload = await safeJson(response); - throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); - } - - const payload = await response.json(); - logsState.streams = Array.isArray(payload.streams) ? payload.streams : []; - - if (!logsState.streams.some((stream) => stream.name === logsState.stream)) { - logsState.stream = ""; - } - - logsState.error = ""; - } catch (error) { - logsState.streams = []; - logsState.stream = ""; - logsState.error = error.message || "Unknown log stream refresh error"; - } finally { - persistLogPreferences(); - renderLogsAll(); - } - } - - async function refreshLogs(source) { - if (logsState.loading) { - return; - } - - if (!appState.logsReady) { - await ensureLogsReady(); - return; - } - - if (!logsState.group) { - logsState.error = logsState.groups.length ? "Choose a log group to load events." : "No log groups found."; - renderLogsAll(); - return; - } - - let shouldRenderList = false; - - logsState.loading = true; - logsState.source = source; - logsState.error = ""; - renderLogsStatus(); - renderLogsFetch(); - - try { - const params = new URLSearchParams({ - group: logsState.group, - windowMs: String(logsState.windowMs), - limit: String(logsState.limit) - }); - - if (logsState.stream) { - params.set("stream", logsState.stream); - } - - const response = await fetch(`/api/logs/events?${params.toString()}`, { cache: "no-store" }); - - if (!response.ok) { - const payload = await safeJson(response); - throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); - } - - const payload = await response.json(); - const events = Array.isArray(payload.events) ? payload.events : []; - const nextSignature = computeLogListSignature(events); - shouldRenderList = nextSignature !== logsState.listSignature; - logsState.events = events; - logsState.duration = payload.fetchDurationMs || 0; - logsState.newest = payload.latestTimestamp || 0; - logsState.updatedAt = Date.now(); - logsState.searchedLogStreams = payload.searchedLogStreams || (logsState.stream ? 1 : logsState.streams.length); - logsState.listSignature = nextSignature; - - pruneLogsState(); - applyLogsFilter(shouldRenderList); - setLogsStatus(`Updated ${logsState.events.length} log event${logsState.events.length === 1 ? "" : "s"}.`, "ok"); - } catch (error) { - logsState.error = error.message || "Unknown log refresh error"; - setLogsStatus(`Refresh failed: ${logsState.error}`, "bad"); - } finally { - logsState.loading = false; - scheduleLogsRefresh(); - renderLogsAll({ renderList: shouldRenderList }); - - if (shouldRenderList && logsState.tailNewest) { - scrollPaneToTop(el.logsContentPane); - } - } - } - - async function refreshSecrets(source) { - if (secretsState.loading) { - return; - } - - let shouldRenderList = false; - - secretsState.loading = true; - secretsState.source = source; - secretsState.error = ""; - renderSecretsStatus(); - renderSecretsFetch(); - - try { - const response = await fetch("/api/secrets", { cache: "no-store" }); - - if (!response.ok) { - const payload = await safeJson(response); - throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); - } - - const payload = await response.json(); - const items = Array.isArray(payload.secrets) ? payload.secrets : []; - const nextSignature = computeSecretsListSignature(items); - shouldRenderList = nextSignature !== secretsState.listSignature; - secretsState.items = items; - secretsState.duration = payload.fetchDurationMs || 0; - secretsState.newest = payload.latestTimestamp || ""; - secretsState.updatedAt = Date.now(); - secretsState.listSignature = nextSignature; - - pruneSecretsState(); - applySecretsFilter(shouldRenderList); - appState.secretsReady = true; - setSecretsStatus(`Updated ${items.length} secret${items.length === 1 ? "" : "s"}.`, "ok"); - } catch (error) { - secretsState.error = error.message || "Unknown secrets refresh error"; - appState.secretsReady = false; - setSecretsStatus(`Refresh failed: ${secretsState.error}`, "bad"); - } finally { - secretsState.loading = false; - scheduleSecretsRefresh(); - renderSecretsAll({ renderList: shouldRenderList }); - } - } - - async function refreshHealthSummary(source) { - if (healthState.loading && source === "auto") { - return; - } - - healthState.loading = true; - healthState.source = source; - renderHealthStrip(); - - try { - const response = await fetch("/api/service-health", { cache: "no-store" }); - - if (!response.ok) { - const payload = await safeJson(response); - throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); - } - - const payload = await response.json(); - healthState.services = payload.services || {}; - healthState.updatedAt = Date.now(); - healthState.error = ""; - } catch (error) { - healthState.error = error.message || "Unknown health refresh error"; - } finally { - healthState.loading = false; - renderHealthStrip(); - } - } - - async function refreshS3Buckets() { - try { - const response = await fetch("/api/s3/buckets", { cache: "no-store" }); - - if (!response.ok) { - const payload = await safeJson(response); - throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); - } - - const payload = await response.json(); - s3State.buckets = Array.isArray(payload.buckets) ? payload.buckets : []; - - const availableBuckets = s3State.buckets.map((bucket) => bucket.name).filter(Boolean); - - if (!availableBuckets.includes(s3State.bucket)) { - s3State.bucket = availableBuckets.includes(config.defaultS3Bucket) - ? config.defaultS3Bucket - : availableBuckets[0] || ""; - } - - s3State.error = ""; - } catch (error) { - s3State.buckets = []; - s3State.bucket = ""; - s3State.error = error.message || "Unknown S3 bucket refresh error"; - } finally { - appState.s3Ready = !s3State.error; - persistS3Preferences(); - renderS3All(); - } - } - - async function refreshS3(source) { - if (s3State.loading) { - return; - } - - if (!appState.s3Ready) { - await ensureS3Ready(); - return; - } - - if (!s3State.bucket) { - s3State.error = s3State.buckets.length ? "Choose a bucket to load objects." : "No S3 buckets found."; - renderS3All(); - return; - } - - let shouldRenderList = false; - - s3State.loading = true; - s3State.source = source; - s3State.error = ""; - renderS3Status(); - renderS3Fetch(); - - try { - const params = new URLSearchParams({ - bucket: s3State.bucket - }); - - if (s3State.prefix) { - params.set("prefix", s3State.prefix); - } - - const response = await fetch(`/api/s3/objects?${params.toString()}`, { cache: "no-store" }); - - if (!response.ok) { - const payload = await safeJson(response); - throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); - } - - const payload = await response.json(); - const objects = Array.isArray(payload.objects) ? payload.objects : []; - const nextSignature = computeS3ListSignature(objects); - shouldRenderList = nextSignature !== s3State.listSignature; - s3State.objects = objects; - s3State.duration = payload.fetchDurationMs || 0; - s3State.newest = payload.latestTimestamp || ""; - s3State.updatedAt = Date.now(); - s3State.listSignature = nextSignature; - - pruneS3State(); - applyS3Filter(shouldRenderList); - appState.s3Ready = true; - setS3Status(`Updated ${objects.length} object${objects.length === 1 ? "" : "s"}.`, "ok"); - } catch (error) { - s3State.error = error.message || "Unknown S3 refresh error"; - appState.s3Ready = false; - setS3Status(`Refresh failed: ${s3State.error}`, "bad"); - } finally { - s3State.loading = false; - scheduleS3Refresh(); - renderS3All({ renderList: shouldRenderList }); - } - } - - function applyLogsFilter(shouldRenderList = true) { - const search = logsState.search.trim().toLowerCase(); - logsState.filtered = !search - ? [...logsState.events] - : logsState.events.filter((event) => logHaystack(event).includes(search)); - persistLogPreferences(); - renderLogsAll({ renderList: shouldRenderList }); - } - - function applySecretsFilter(shouldRenderList = true) { - const search = secretsState.search.trim().toLowerCase(); - secretsState.filtered = !search - ? [...secretsState.items] - : secretsState.items.filter((secret) => secretHaystack(secret).includes(search)); - persistSecretPreferences(); - renderSecretsAll({ renderList: shouldRenderList }); - } - - function applyS3Filter(shouldRenderList = true) { - const search = s3State.search.trim().toLowerCase(); - s3State.filtered = !search - ? [...s3State.objects] - : s3State.objects.filter((object) => s3Haystack(object).includes(search)); - persistS3Preferences(); - renderS3All({ renderList: shouldRenderList }); - } - - function pruneState() { - const ids = new Set(state.messages.map((message) => message.id)); - state.openIds = new Set([...state.openIds].filter((id) => ids.has(id))); - state.newIds = new Set([...state.newIds].filter((id) => ids.has(id))); - - Object.keys(state.views).forEach((id) => { - if (!ids.has(id)) { - delete state.views[id]; - } - }); - - Object.keys(state.raw).forEach((id) => { - if (!ids.has(id)) { - delete state.raw[id]; - } - }); - } - - function pruneLogsState() { - const ids = new Set(logsState.events.map((event) => event.id)); - logsState.openIds = new Set([...logsState.openIds].filter((id) => ids.has(id))); - } - - function pruneSecretsState() { - const ids = new Set(secretsState.items.map((secret) => secret.id)); - secretsState.openIds = new Set([...secretsState.openIds].filter((id) => ids.has(id))); - - Object.keys(secretsState.values).forEach((id) => { - if (!ids.has(id)) { - delete secretsState.values[id]; - } - }); - - secretsState.revealedIds = new Set([...secretsState.revealedIds].filter((id) => ids.has(id))); - } - - function pruneS3State() { - const ids = new Set(s3State.objects.map((object) => object.id)); - s3State.openIds = new Set([...s3State.openIds].filter((id) => ids.has(id))); - - Object.keys(s3State.previews).forEach((id) => { - if (!ids.has(id)) { - delete s3State.previews[id]; - } - }); - } - - function applyFilter(shouldRenderList = true) { - const search = state.search.trim().toLowerCase(); - state.filtered = !search - ? [...state.messages] - : state.messages.filter((message) => haystack(message).includes(search)); - persistEmailPreferences(); - renderAll({ renderList: shouldRenderList }); - } - - function computeListSignature(messages) { - return messages - .map((message) => - [ - message.id, - message.timestampMs || 0, - message.rawSizeBytes || 0, - message.attachmentCount || 0, - message.hasHtml ? 1 : 0, - message.preview || "", - message.parseError || "" - ].join("::") - ) - .join("|"); - } - - function computeLogListSignature(events) { - return events - .map((event) => - [event.id, event.timestamp || 0, event.ingestionTime || 0, event.logStreamName || "", event.message || ""].join( - "::" - ) - ) - .join("|"); - } - - function computeSecretsListSignature(items) { - return items - .map((secret) => - [ - secret.id, - secret.name || "", - secret.arn || "", - secret.description || "", - secret.lastChangedDate || "", - secret.createdDate || "", - secret.rotationEnabled ? 1 : 0, - secret.owningService || "", - secret.primaryRegion || "", - secret.versionCount || 0, - secret.tagCount || 0 - ].join("::") - ) - .join("|"); - } - - function computeS3ListSignature(objects) { - return objects - .map((object) => - [object.id, object.key || "", object.size || 0, object.lastModified || "", object.etag || ""].join("::") - ) - .join("|"); - } - - function haystack(message) { - return [ - message.subject, - message.from, - message.to, - message.replyTo, - message.preview, - message.textContent, - message.region, - ...(message.attachments || []).flatMap((attachment) => [attachment.filename, attachment.contentType]) - ] - .filter(Boolean) - .join(" ") - .toLowerCase(); - } - - function logHaystack(event) { - return [event.logStreamName, event.message, event.preview].filter(Boolean).join(" ").toLowerCase(); - } - - function secretHaystack(secret) { - return [ - secret.name, - secret.arn, - secret.description, - secret.primaryRegion, - secret.owningService, - ...(secret.tags || []).flatMap((tag) => [tag.key, tag.value]) - ] - .filter(Boolean) - .join(" ") - .toLowerCase(); - } - - function s3Haystack(object) { - return [object.bucket, object.key, object.storageClass, object.etag].filter(Boolean).join(" ").toLowerCase(); - } - - function scheduleRefresh() { - window.clearTimeout(state.timer); - - if (appState.panel !== "emails" || !state.auto || document.hidden || state.loading) { - return; - } - - state.timer = window.setTimeout(() => refreshMessages("auto"), state.interval); - } - - function scheduleLogsRefresh() { - window.clearTimeout(logsState.timer); - - if (appState.panel !== "logs" || !logsState.auto || document.hidden || logsState.loading) { - return; - } - - logsState.timer = window.setTimeout(() => refreshLogs("auto"), logsState.interval); - } - - function scheduleSecretsRefresh() { - window.clearTimeout(secretsState.timer); - - if (appState.panel !== "secrets" || !secretsState.auto || document.hidden || secretsState.loading) { - return; - } - - secretsState.timer = window.setTimeout(() => refreshSecrets("auto"), secretsState.interval); - } - - function scheduleS3Refresh() { - window.clearTimeout(s3State.timer); - - if (appState.panel !== "s3" || !s3State.auto || document.hidden || s3State.loading) { - return; - } - - s3State.timer = window.setTimeout(() => refreshS3("auto"), s3State.interval); - } - - function renderAll(options = {}) { - const { renderList: shouldRenderList = true } = options; - renderStats(); - renderFetch(); - renderStatus(); - if (shouldRenderList) { - renderList(); - } - renderLiveClock(); - } - - function renderLogsAll(options = {}) { - const { renderList: shouldRenderList = true } = options; - renderLogsFilters(); - renderLogsStats(); - renderLogsFetch(); - renderLogsStatus(); - if (shouldRenderList) { - renderLogsList(); - } - renderLogsLiveClock(); - } - - function renderSecretsAll(options = {}) { - const { renderList: shouldRenderList = true } = options; - renderSecretsStats(); - renderSecretsFetch(); - renderSecretsStatus(); - if (shouldRenderList) { - renderSecretsList(); - } - renderSecretsLiveClock(); - } - - function renderS3All(options = {}) { - const { renderList: shouldRenderList = true } = options; - renderS3Filters(); - renderS3Stats(); - renderS3Fetch(); - renderS3Status(); - if (shouldRenderList) { - renderS3List(); - } - renderS3LiveClock(); - } - - function renderHealthStrip() { - const serviceOrder = [ - { key: "emails", panel: "emails", icon: "✉️", label: "SES Emails", shortLabel: "SES" }, - { key: "logs", panel: "logs", icon: "📜", label: "CloudWatch Logs", shortLabel: "Logs" }, - { key: "secrets", panel: "secrets", icon: "🔐", label: "Secrets Manager", shortLabel: "Secrets" }, - { key: "s3", panel: "s3", icon: "🪣", label: "S3 Explorer", shortLabel: "S3" } - ]; - - el.healthStrip.innerHTML = serviceOrder - .map((service) => { - const entry = healthState.services?.[service.key]; - const toneClass = healthState.loading && !entry ? "warn" : entry?.ok ? "ok" : entry ? "bad" : ""; - const activeClass = service.panel === appState.panel ? "active" : ""; - const className = ["healthBadge", toneClass, activeClass].filter(Boolean).join(" "); - const summary = entry?.summary || (healthState.loading ? "Checking..." : "Waiting"); - const detail = entry?.detail || (healthState.error ? healthState.error : "Not checked yet"); - const updatedMeta = healthState.updatedAt - ? `Updated ${formatRelative(healthState.updatedAt)} via ${healthState.source}` - : ""; - const titleParts = [`${service.label}: ${detail}`]; - - if (updatedMeta) { - titleParts.push(updatedMeta); - } - - return ``; - }) - .join(""); - } - - function renderStats() { - el.totalStat.textContent = String(state.messages.length); - el.visibleStat.textContent = `${state.filtered.length} visible${state.search ? " after search" : ""}`; - el.newStat.textContent = String(state.newIds.size); - el.newestStat.textContent = state.newest ? formatDateTime(state.newest) : "No messages"; - } - - function renderLogsFilters() { - const groups = logsState.groups.length - ? logsState.groups.map((group) => ``) - : ['']; - const streams = [ - '', - ...logsState.streams.map( - (stream) => `` - ) - ]; - - el.logsGroupSelect.innerHTML = groups.join(""); - el.logsStreamSelect.innerHTML = streams.join(""); - - if (logsState.group) { - el.logsGroupSelect.value = logsState.group; - } - - el.logsStreamSelect.value = logsState.stream; - el.logsWrapToggle.checked = logsState.wrapLines; - el.logsTailToggle.checked = logsState.tailNewest; - } - - function renderS3Filters() { - const bucketOptions = s3State.buckets.length - ? s3State.buckets.map( - (bucket) => `` - ) - : ['']; - - el.s3BucketSelect.innerHTML = bucketOptions.join(""); - - if (s3State.bucket) { - el.s3BucketSelect.value = s3State.bucket; - } - - el.s3PrefixInput.value = s3State.prefix; - el.s3SearchInput.value = s3State.search; - } - - function renderLogsStats() { - el.logsTotalStat.textContent = String(logsState.events.length); - el.logsVisibleStat.textContent = `${logsState.filtered.length} visible${logsState.search ? " after search" : ""}`; - el.logsStreamsStat.textContent = String(logsState.streams.length || logsState.searchedLogStreams || 0); - el.logsNewestStat.textContent = logsState.newest ? formatDateTime(logsState.newest) : "No events"; - } - - function renderSecretsStats() { - el.secretsTotalStat.textContent = String(secretsState.items.length); - el.secretsVisibleStat.textContent = `${secretsState.filtered.length} visible${secretsState.search ? " after search" : ""}`; - el.secretsLoadedStat.textContent = String( - Object.values(secretsState.values).filter((entry) => entry?.status === "loaded").length - ); - el.secretsNewestStat.textContent = secretsState.newest ? formatDateTime(secretsState.newest) : "No secrets"; - } - - function renderS3Stats() { - el.s3TotalStat.textContent = String(s3State.objects.length); - el.s3VisibleStat.textContent = `${s3State.filtered.length} visible${s3State.search ? " after search" : ""}`; - el.s3BucketsStat.textContent = String(s3State.buckets.length); - el.s3NewestStat.textContent = s3State.newest ? formatDateTime(s3State.newest) : "No objects"; - } - - function renderFetch() { - if (state.loading) { - el.fetchStat.textContent = "Refreshing..."; - el.fetchDetail.textContent = `Endpoint: ${config.endpoint}`; - return; - } - - if (state.error) { - el.fetchStat.textContent = "Needs attention"; - el.fetchDetail.textContent = state.error; - return; - } - - if (!state.updatedAt) { - el.fetchStat.textContent = "Idle"; - el.fetchDetail.textContent = `Endpoint: ${config.endpoint}`; - return; - } - - el.fetchStat.textContent = `${state.duration}ms`; - el.fetchDetail.textContent = `${state.parseErrors} parse error${state.parseErrors === 1 ? "" : "s"}. Endpoint: ${config.endpoint}`; - } - - function renderLogsFetch() { - if (logsState.loading) { - el.logsFetchStat.textContent = "Refreshing..."; - el.logsFetchDetail.textContent = `Endpoint: ${config.cloudWatchEndpoint} (${config.cloudWatchRegion})`; - return; - } - - if (logsState.error) { - el.logsFetchStat.textContent = "Needs attention"; - el.logsFetchDetail.textContent = logsState.error; - return; - } - - if (!logsState.updatedAt) { - el.logsFetchStat.textContent = "Idle"; - el.logsFetchDetail.textContent = `Endpoint: ${config.cloudWatchEndpoint} (${config.cloudWatchRegion})`; - return; - } - - el.logsFetchStat.textContent = `${logsState.duration}ms`; - el.logsFetchDetail.textContent = `${logsState.group || "No group selected"}. Endpoint: ${config.cloudWatchEndpoint}`; - } - - function renderSecretsFetch() { - if (secretsState.loading) { - el.secretsFetchStat.textContent = "Refreshing..."; - el.secretsFetchDetail.textContent = `Endpoint: ${config.secretsEndpoint} (${config.secretsRegion})`; - return; - } - - if (secretsState.error) { - el.secretsFetchStat.textContent = "Needs attention"; - el.secretsFetchDetail.textContent = secretsState.error; - return; - } - - if (!secretsState.updatedAt) { - el.secretsFetchStat.textContent = "Idle"; - el.secretsFetchDetail.textContent = `Endpoint: ${config.secretsEndpoint} (${config.secretsRegion})`; - return; - } - - el.secretsFetchStat.textContent = `${secretsState.duration}ms`; - el.secretsFetchDetail.textContent = `Endpoint: ${config.secretsEndpoint} (${config.secretsRegion})`; - } - - function renderS3Fetch() { - if (s3State.loading) { - el.s3FetchStat.textContent = "Refreshing..."; - el.s3FetchDetail.textContent = `Endpoint: ${config.s3Endpoint} (${config.s3Region})`; - return; - } - - if (s3State.error) { - el.s3FetchStat.textContent = "Needs attention"; - el.s3FetchDetail.textContent = s3State.error; - return; - } - - if (!s3State.updatedAt) { - el.s3FetchStat.textContent = "Idle"; - el.s3FetchDetail.textContent = `Endpoint: ${config.s3Endpoint} (${config.s3Region})`; - return; - } - - el.s3FetchStat.textContent = `${s3State.duration}ms`; - el.s3FetchDetail.textContent = `${s3State.bucket || "No bucket selected"}. Endpoint: ${config.s3Endpoint}`; - } - - function renderStatus() { - el.statusChip.className = "status"; - - if (state.loading) { - el.statusChip.classList.add("warn"); - el.statusChip.textContent = "Refreshing messages..."; - return; - } - - if (state.error) { - el.statusChip.classList.add("bad"); - el.statusChip.textContent = `Refresh failed: ${state.error}`; - return; - } - - if (!state.auto) { - el.statusChip.textContent = "Live refresh paused"; - return; - } - - if (document.hidden) { - el.statusChip.classList.add("warn"); - el.statusChip.textContent = "Tab hidden, live refresh paused"; - return; - } - - if (!state.updatedAt) { - el.statusChip.textContent = "Waiting for first refresh..."; - return; - } - - const seconds = Math.max(0, Math.ceil((state.updatedAt + state.interval - Date.now()) / 1000)); - el.statusChip.classList.add("ok"); - el.statusChip.textContent = `Live refresh on, next check in ${seconds}s`; - } - - function renderLogsStatus() { - el.logsStatusChip.className = "status"; - - if (logsState.loading) { - el.logsStatusChip.classList.add("warn"); - el.logsStatusChip.textContent = "Refreshing logs..."; - return; - } - - if (logsState.error) { - el.logsStatusChip.classList.add("bad"); - el.logsStatusChip.textContent = `Refresh failed: ${logsState.error}`; - return; - } - - if (!logsState.auto) { - el.logsStatusChip.textContent = "Live refresh paused"; - return; - } - - if (document.hidden) { - el.logsStatusChip.classList.add("warn"); - el.logsStatusChip.textContent = "Tab hidden, live refresh paused"; - return; - } - - if (!logsState.updatedAt) { - el.logsStatusChip.textContent = "Waiting for first refresh..."; - return; - } - - const seconds = Math.max(0, Math.ceil((logsState.updatedAt + logsState.interval - Date.now()) / 1000)); - el.logsStatusChip.classList.add("ok"); - el.logsStatusChip.textContent = `Live refresh on, next check in ${seconds}s`; - } - - function renderSecretsStatus() { - el.secretsStatusChip.className = "status"; - - if (secretsState.loading) { - el.secretsStatusChip.classList.add("warn"); - el.secretsStatusChip.textContent = "Refreshing secrets..."; - return; - } - - if (secretsState.error) { - el.secretsStatusChip.classList.add("bad"); - el.secretsStatusChip.textContent = `Refresh failed: ${secretsState.error}`; - return; - } - - if (!secretsState.auto) { - el.secretsStatusChip.textContent = "Live refresh paused"; - return; - } - - if (document.hidden) { - el.secretsStatusChip.classList.add("warn"); - el.secretsStatusChip.textContent = "Tab hidden, live refresh paused"; - return; - } - - if (!secretsState.updatedAt) { - el.secretsStatusChip.textContent = "Waiting for first refresh..."; - return; - } - - const seconds = Math.max(0, Math.ceil((secretsState.updatedAt + secretsState.interval - Date.now()) / 1000)); - el.secretsStatusChip.classList.add("ok"); - el.secretsStatusChip.textContent = `Live refresh on, next check in ${seconds}s`; - } - - function renderS3Status() { - el.s3StatusChip.className = "status"; - - if (s3State.loading) { - el.s3StatusChip.classList.add("warn"); - el.s3StatusChip.textContent = "Refreshing objects..."; - return; - } - - if (s3State.error) { - el.s3StatusChip.classList.add("bad"); - el.s3StatusChip.textContent = `Refresh failed: ${s3State.error}`; - return; - } - - if (!s3State.auto) { - el.s3StatusChip.textContent = "Live refresh paused"; - return; - } - - if (document.hidden) { - el.s3StatusChip.classList.add("warn"); - el.s3StatusChip.textContent = "Tab hidden, live refresh paused"; - return; - } - - if (!s3State.updatedAt) { - el.s3StatusChip.textContent = "Waiting for first refresh..."; - return; - } - - const seconds = Math.max(0, Math.ceil((s3State.updatedAt + s3State.interval - Date.now()) / 1000)); - el.s3StatusChip.classList.add("ok"); - el.s3StatusChip.textContent = `Live refresh on, next check in ${seconds}s`; - } - - function renderLiveClock() { - if (!state.updatedAt) { - el.updatedStat.textContent = "Not refreshed yet"; - return; - } - - el.updatedStat.textContent = `Updated ${formatRelative(state.updatedAt)} via ${state.source}`; - renderStatus(); - } - - function renderLogsLiveClock() { - if (!logsState.updatedAt) { - el.logsUpdatedStat.textContent = "Not refreshed yet"; - return; - } - - el.logsUpdatedStat.textContent = `Updated ${formatRelative(logsState.updatedAt)} via ${logsState.source}`; - renderLogsStatus(); - } - - function renderSecretsLiveClock() { - if (!secretsState.updatedAt) { - el.secretsUpdatedStat.textContent = "Not refreshed yet"; - return; - } - - el.secretsUpdatedStat.textContent = `Updated ${formatRelative(secretsState.updatedAt)} via ${secretsState.source}`; - renderSecretsStatus(); - } - - function renderS3LiveClock() { - if (!s3State.updatedAt) { - el.s3UpdatedStat.textContent = "Not refreshed yet"; - return; - } - - el.s3UpdatedStat.textContent = `Updated ${formatRelative(s3State.updatedAt)} via ${s3State.source}`; - renderS3Status(); - } - - function renderList() { - el.banner.hidden = !state.error; - el.banner.textContent = state.error ? `Refresh failed: ${state.error}` : ""; - - if (!state.filtered.length) { - el.list.innerHTML = ""; - el.empty.hidden = false; - el.empty.textContent = state.messages.length - ? "No messages match the current search." - : "No emails yet. Send one through LocalStack SES and refresh."; - updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); - return; - } - - el.empty.hidden = true; - el.list.innerHTML = state.filtered.map(renderCard).join(""); - bindCardToggles(); - syncCardExpansion(); - updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); - } - - function renderLogsList() { - el.logsBanner.hidden = !logsState.error; - el.logsBanner.textContent = logsState.error ? `Refresh failed: ${logsState.error}` : ""; - - if (!logsState.group && !logsState.groups.length) { - el.logsList.innerHTML = ""; - el.logsEmpty.hidden = false; - el.logsEmpty.textContent = "No CloudWatch log groups found in LocalStack yet."; - updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); - return; - } - - if (!logsState.filtered.length) { - el.logsList.innerHTML = ""; - el.logsEmpty.hidden = false; - el.logsEmpty.textContent = logsState.events.length - ? "No log events match the current search." - : "No log events found for the selected group, stream, and time window."; - updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); - return; - } - - el.logsEmpty.hidden = true; - el.logsList.innerHTML = logsState.filtered.map((event) => renderLogEvent(event)).join(""); - bindLogToggles(); - updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); - } - - function renderSecretsList() { - el.secretsBanner.hidden = !secretsState.error; - el.secretsBanner.textContent = secretsState.error ? `Refresh failed: ${secretsState.error}` : ""; - - if (!secretsState.filtered.length) { - el.secretsList.innerHTML = ""; - el.secretsEmpty.hidden = false; - el.secretsEmpty.textContent = secretsState.items.length - ? "No secrets match the current search." - : "No Secrets Manager entries found in LocalStack yet."; - updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); - return; - } - - el.secretsEmpty.hidden = true; - el.secretsList.innerHTML = secretsState.filtered.map((secret) => renderSecretCard(secret)).join(""); - bindSecretToggles(); - syncSecretExpansion(); - updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); - } - - function renderS3List() { - el.s3Banner.hidden = !s3State.error; - el.s3Banner.textContent = s3State.error ? `Refresh failed: ${s3State.error}` : ""; - - if (!s3State.bucket && !s3State.buckets.length) { - el.s3List.innerHTML = ""; - el.s3Empty.hidden = false; - el.s3Empty.textContent = "No S3 buckets found in LocalStack yet."; - updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); - return; - } - - if (!s3State.filtered.length) { - el.s3List.innerHTML = ""; - el.s3Empty.hidden = false; - el.s3Empty.textContent = s3State.objects.length - ? "No S3 objects match the current search." - : "No S3 objects found for the selected bucket and prefix."; - updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); - return; - } - - el.s3Empty.hidden = true; - el.s3List.innerHTML = s3State.filtered.map((object) => renderS3ObjectCard(object)).join(""); - bindS3Toggles(); - syncS3Expansion(); - updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); - } - - function renderLogEvent(event) { - const level = detectLogLevel(event); - const levelTag = level ? `${escapeHtml(level.label)}` : ""; - - return ` -
- -
-
- ${escapeHtml(formatDateTime(event.timestamp))} - ${escapeHtml(event.logStreamName || "Unknown stream")} - ${levelTag} -
-
- - ${escapeHtml(formatRelative(event.timestamp || Date.now()))} -
-
-

${renderLogPreviewContent(event)}

-
-
-
${renderLogBodyContent(event.message)}
-
-
- `; - } - - function renderSecretCard(secret) { - const valueState = secretsState.values[secret.id]; - const tags = []; - - if (secret.owningService) { - tags.push(`${escapeHtml(secret.owningService)}`); - } - - if (secret.primaryRegion) { - tags.push(`${escapeHtml(secret.primaryRegion)}`); - } - - if (secret.rotationEnabled) { - tags.push('Rotation on'); - } else { - tags.push('Rotation off'); - } - - if (secret.tagCount) { - tags.push(`${secret.tagCount} tag${secret.tagCount === 1 ? "" : "s"}`); - } - - if (valueState?.status === "loaded") { - tags.push(`${escapeHtml(valueState.label)}`); - } - - if (valueState?.status === "error") { - tags.push('Value load failed'); - } - - return ` -
- -
-
-

🔐 ${escapeHtml(secret.name)}

-

${escapeHtml(secret.description || secret.arn || "No description")}

-
- ${escapeHtml(formatDateTime(secret.lastChangedDate || secret.createdDate))} -
-
${tags.join("")}
-

${escapeHtml(buildSecretPreview(secret))}

-
-
-
- ${metaCard("Name", secret.name)} - ${metaCard("ARN", secret.arn || "Not available")} - ${metaCard("Description", secret.description || "None")} - ${metaCard("Last changed", formatDateTime(secret.lastChangedDate || secret.createdDate))} - ${metaCard("Created", formatDateTime(secret.createdDate))} - ${metaCard("Last accessed", formatDateTime(secret.lastAccessedDate))} - ${metaCard("Primary region", secret.primaryRegion || "Not set")} - ${metaCard("Owning service", secret.owningService || "Not set")} -
- -
- ${renderSecretValuePanel(secret, valueState)} -
-
-
- `; - } - - function renderS3ObjectCard(object) { - const previewState = s3State.previews[object.id]; - const tags = [ - `${escapeHtml(object.bucket)}`, - `${escapeHtml(formatBytes(object.size))}` - ]; - - if (object.storageClass) { - tags.push(`${escapeHtml(object.storageClass)}`); - } - - if (previewState?.status === "loaded") { - tags.push(`${escapeHtml(previewState.previewType)}`); - } - - if (previewState?.status === "error") { - tags.push('Preview failed'); - } - - return ` -
- -
-
-

🪣 ${escapeHtml(object.key)}

-

${escapeHtml(object.bucket)} • ${escapeHtml(formatBytes(object.size))}

-
- ${escapeHtml(formatDateTime(object.lastModified))} -
-
${tags.join("")}
-

${escapeHtml(buildS3Preview(object))}

-
-
-
-
- - - ⬇️ Download -
-
- -
- ${metaCard("Bucket", object.bucket)} - ${metaCard("Key", object.key)} - ${metaCard("Size", formatBytes(object.size))} - ${metaCard("Modified", formatDateTime(object.lastModified))} - ${metaCard("Storage class", object.storageClass || "STANDARD")} - ${metaCard("ETag", object.etag || "Not available")} -
- -
${renderS3PreviewPanel(object, previewState)}
-
-
- `; - } - - function bindLogToggles() { - el.logsList.querySelectorAll(".logEvent").forEach((details) => { - details.addEventListener("toggle", () => { - const id = details.dataset.id; - - if (!id) { - return; - } - - if (details.open) { - logsState.openIds.add(id); - } else { - logsState.openIds.delete(id); - } - }); - }); - } - - function bindSecretToggles() { - el.secretsList.querySelectorAll(".secretCard").forEach((details) => { - details.addEventListener("toggle", () => { - const id = details.dataset.id; - - if (!id) { - return; - } - - if (details.open) { - secretsState.openIds.add(id); - ensureSecretValue(id, { force: false }); - } else { - secretsState.openIds.delete(id); - } - }); - }); - } - - function bindS3Toggles() { - el.s3List.querySelectorAll(".s3Card").forEach((details) => { - details.addEventListener("toggle", () => { - const id = details.dataset.id; - - if (!id) { - return; - } - - if (details.open) { - s3State.openIds.add(id); - ensureS3Preview(id, { force: false }); - } else { - s3State.openIds.delete(id); - } - }); - }); - } - - function bindCardToggles() { - el.list.querySelectorAll(".card").forEach((details) => { - details.addEventListener("toggle", () => { - const id = details.dataset.id; - - if (!id) { - return; - } - - if (details.open) { - state.openIds.add(id); - } else { - state.openIds.delete(id); - } - - hydrate(details, getMessage(id)); - }); - }); - } - - function renderCard(message) { - const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text"); - const tags = []; - - if (state.newIds.has(message.id)) { - tags.push('New'); - } - - if (message.attachmentCount) { - tags.push( - `${message.attachmentCount} attachment${message.attachmentCount === 1 ? "" : "s"}` - ); - } - - tags.push(`${message.hasHtml ? "HTML" : "Text only"}`); - - if (message.parseError) { - tags.push('Parse issue'); - } - - return ` -
- -
-
-

${escapeHtml(message.subject)}

-

${escapeHtml(message.from)} to ${escapeHtml(message.to)}

-
- ${escapeHtml(formatDateTime(message.timestamp))} -
-
${tags.join("")}
-

${escapeHtml(message.preview)}

-
-
-
-
- ${ - message.hasHtml - ? `` - : "" - } - - -
-
- -
-
- -
- ${metaCard("From", message.from)} - ${metaCard("To", message.to)} - ${metaCard("Reply-To", message.replyTo || "None")} - ${metaCard("Sent", formatDateTime(message.timestamp))} - ${metaCard("Region", message.region || "Unknown region")} - ${metaCard("LocalStack Id", message.id)} - ${metaCard("Message-Id", message.messageId || "Not available")} - ${metaCard("Raw size", formatBytes(message.rawSizeBytes))} - ${message.parseError ? metaCard("Parse error", message.parseError) : ""} -
- - ${ - message.attachments?.length - ? `
${message.attachments - .map((attachment) => { - const size = attachment.size ? `, ${formatBytes(attachment.size)}` : ""; - const icon = resolveAttachmentIcon(attachment); - return `${icon} ${escapeHtml(attachment.filename)} (${escapeHtml(attachment.contentType)}${escapeHtml(size)})`; - }) - .join("")}
` - : "" - } - -
${renderPanel(message, view)}
-
-
- `; - } - - function renderPanel(message, view) { - if (view === "rendered" && message.hasHtml) { - return ``; - } - - if (view === "raw") { - const raw = state.raw[message.id]; - - if (!raw) { - return `
Raw MIME source is loaded on demand.
`; - } - - if (raw.status === "loading") { - return '
Loading raw source...
'; - } - - if (raw.status === "error") { - return `
Unable to load raw source: ${escapeHtml(raw.error)}
`; - } - - return `
${escapeHtml(raw.value)}
`; - } - - return `
${escapeHtml(message.textContent || "No plain-text content available for this message.")}
`; - } - - function renderSecretValuePanel(secret, valueState) { - if (!valueState) { - return `
Secret values are loaded on demand.
`; - } - - if (valueState.status === "loading") { - return '
Loading secret value...
'; - } - - if (valueState.status === "error") { - return `
Unable to load secret value: ${escapeHtml(valueState.error)}
`; - } - - const revealed = secretsState.revealedIds.has(secret.id); - const maskedHtml = escapeHtml(maskSecretValue(valueState.copyValue)); - - return ` -
-
- ${escapeHtml(valueState.label)} - ${ - valueState.versionId - ? `Version ${escapeHtml(valueState.versionId.slice(0, 8))}` - : "" - } - ${ - valueState.versionStages.length - ? `${escapeHtml(valueState.versionStages.join(", "))}` - : "" - } -
-
- - - - - - -
-
-
${revealed ? valueState.displayHtml : maskedHtml}
- `; - } - - function renderS3PreviewPanel(object, previewState) { - if (!previewState) { - return `
Object previews load on demand.
`; - } - - if (previewState.status === "loading") { - return '
Loading object preview...
'; - } - - if (previewState.status === "error") { - return `
Unable to load object preview: ${escapeHtml(previewState.error)}
`; - } - - const truncatedTag = previewState.truncated ? 'Preview truncated' : ""; - let previewContent = `
No inline preview available for this object type.
`; - - if (previewState.previewType === "image" && previewState.imageDataUrl) { - previewContent = `${escapeHtml(object.key)}`; - } else if (previewState.previewType === "json") { - previewContent = `
${highlightJsonText(prettyJsonOrText(previewState.previewText))}
`; - } else if (previewState.previewType === "text" || previewState.previewType === "html") { - previewContent = `
${escapeHtml(previewState.previewText || "No preview text available.")}
`; - } - - return ` -
-
- ${escapeHtml(previewState.previewType)} - ${truncatedTag} - ${previewState.contentType ? `${escapeHtml(previewState.contentType)}` : ""} -
-
- - ⬇️ Download -
-
- ${previewContent} - `; - } - - function metaCard(label, value) { - return `
${escapeHtml(label)}
${escapeHtml(value)}
`; - } - - function syncCardExpansion() { - const applyCardState = () => { - el.list.querySelectorAll(".card").forEach((details) => { - const id = details.dataset.id; - const shouldOpen = Boolean(id && state.openIds.has(id)); - - if (shouldOpen && !details.open) { - details.open = true; - } - - if (!shouldOpen && details.open) { - details.open = false; - return; - } - - if (shouldOpen) { - hydrate(details, getMessage(id)); - } - }); - }; - - applyCardState(); - window.requestAnimationFrame(applyCardState); - } - - function syncSecretExpansion() { - const applySecretState = () => { - el.secretsList.querySelectorAll(".secretCard").forEach((details) => { - const id = details.dataset.id; - const shouldOpen = Boolean(id && secretsState.openIds.has(id)); - - if (shouldOpen && !details.open) { - details.open = true; - } - - if (!shouldOpen && details.open) { - details.open = false; - return; - } - - if (shouldOpen) { - ensureSecretValue(id, { force: false }); - } - }); - }; - - applySecretState(); - window.requestAnimationFrame(applySecretState); - } - - function syncS3Expansion() { - const applyS3State = () => { - el.s3List.querySelectorAll(".s3Card").forEach((details) => { - const id = details.dataset.id; - const shouldOpen = Boolean(id && s3State.openIds.has(id)); - - if (shouldOpen && !details.open) { - details.open = true; - } - - if (!shouldOpen && details.open) { - details.open = false; - return; - } - - if (shouldOpen) { - ensureS3Preview(id, { force: false }); - } - }); - }; - - applyS3State(); - window.requestAnimationFrame(applyS3State); - } - - function resolveAttachmentIcon(attachment) { - const filename = String(attachment?.filename || "").toLowerCase(); - const contentType = String(attachment?.contentType || "").toLowerCase(); - - if (filename.endsWith(".pdf") || contentType.includes("pdf")) { - return "📄"; - } - - if ( - [".doc", ".docx", ".txt", ".rtf", ".md"].some((extension) => filename.endsWith(extension)) || - contentType.includes("word") || - contentType.startsWith("text/") - ) { - return "📝"; - } - - if ( - [".xls", ".xlsx", ".csv"].some((extension) => filename.endsWith(extension)) || - contentType.includes("sheet") || - contentType.includes("csv") - ) { - return "📊"; - } - - if ( - filename.endsWith(".json") || - filename.endsWith(".xml") || - filename.endsWith(".yaml") || - filename.endsWith(".yml") - ) { - return "🧾"; - } - - if (contentType.startsWith("image/")) { - return "🖼️"; - } - - if (contentType.startsWith("audio/")) { - return "🎵"; - } - - if (contentType.startsWith("video/")) { - return "🎬"; - } - - if ( - [".zip", ".rar", ".7z", ".tar", ".gz"].some((extension) => filename.endsWith(extension)) || - contentType.includes("zip") || - contentType.includes("compressed") - ) { - return "🗜️"; - } - - return "📎"; - } - - function hydrate(details, message) { - if (!details || !details.open || !message) { - return; - } - - const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text"); - - if (view !== "rendered" || !message.hasHtml) { - return; - } - - const iframe = details.querySelector("[data-frame]"); - - if (iframe) { - iframe.referrerPolicy = "no-referrer"; - iframe.sandbox = ""; - iframe.srcdoc = message.renderedHtml || ""; - } - } - - function getMessage(id) { - return state.messages.find((message) => message.id === id); - } - - function getLogEvent(id) { - return logsState.events.find((event) => event.id === id); - } - - function getSecret(id) { - return secretsState.items.find((secret) => secret.id === id); - } - - function getS3Object(id) { - return s3State.objects.find((object) => object.id === id); - } - - async function loadRaw(id) { - if (state.raw[id]?.status === "loaded") { - return state.raw[id].value; - } - - if (state.raw[id]?.status === "loading") { - return null; - } - - state.raw[id] = { status: "loading" }; - renderList(); - - try { - const response = await fetch(`/api/messages/${encodeURIComponent(id)}/raw`, { cache: "no-store" }); - - if (!response.ok) { - throw new Error((await response.text()) || `Request failed with ${response.status}`); - } - - const value = await response.text(); - state.raw[id] = { status: "loaded", value }; - return value; - } catch (error) { - state.raw[id] = { status: "error", error: error.message || "Unknown raw load error" }; - setStatus("Could not load the raw message source.", "bad"); - return null; - } finally { - renderList(); - } - } - - async function ensureSecretValue(id, options = {}) { - const { force = false } = options; - - if (!id) { - return null; - } - - if (!force && secretsState.values[id]?.status === "loaded") { - return secretsState.values[id]; - } - - if (secretsState.values[id]?.status === "loading") { - return null; - } - - secretsState.values[id] = { status: "loading" }; - renderSecretsAll(); - - try { - const response = await fetch(`/api/secrets/value?id=${encodeURIComponent(id)}`, { cache: "no-store" }); - - if (!response.ok) { - const payload = await safeJson(response); - throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); - } - - const payload = await response.json(); - const secretString = typeof payload.secretString === "string" ? payload.secretString : ""; - const secretBinary = typeof payload.secretBinary === "string" ? payload.secretBinary : ""; - const parsedString = secretString ? tryParseJsonText(secretString) : { ok: false, value: null }; - const entry = { - status: "loaded", - label: secretBinary ? "Binary" : parsedString.ok ? "JSON" : secretString ? "Text" : "Empty", - copyValue: secretBinary - ? secretBinary - : parsedString.ok - ? JSON.stringify(parsedString.value, null, 2) - : secretString || "No secret value.", - displayHtml: secretBinary - ? escapeHtml(secretBinary) - : parsedString.ok - ? highlightJsonText(JSON.stringify(parsedString.value, null, 2)) - : escapeHtml(secretString || "No secret value."), - isJson: parsedString.ok, - isBinary: Boolean(secretBinary), - versionId: payload.versionId || "", - versionStages: Array.isArray(payload.versionStages) ? payload.versionStages : [], - createdDate: payload.createdDate || "", - arn: payload.arn || "", - name: payload.name || "" - }; - - secretsState.values[id] = entry; - return entry; - } catch (error) { - secretsState.values[id] = { - status: "error", - error: error.message || "Unknown secret value error" - }; - setSecretsStatus("Could not load the secret value.", "bad"); - return null; - } finally { - renderSecretsAll(); - } - } - - async function ensureS3Preview(id, options = {}) { - const { force = false } = options; - - if (!id) { - return null; - } - - if (!force && s3State.previews[id]?.status === "loaded") { - return s3State.previews[id]; - } - - if (s3State.previews[id]?.status === "loading") { - return null; - } - - const object = getS3Object(id); - - if (!object) { - return null; - } - - s3State.previews[id] = { status: "loading" }; - renderS3All(); - - try { - const response = await fetch( - `/api/s3/object?bucket=${encodeURIComponent(object.bucket)}&key=${encodeURIComponent(object.key)}`, - { cache: "no-store" } - ); - - if (!response.ok) { - const payload = await safeJson(response); - throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); - } - - const payload = await response.json(); - const entry = { - status: "loaded", - previewType: payload.previewType || "binary", - previewText: payload.previewText || "", - imageDataUrl: payload.imageDataUrl || "", - contentType: payload.contentType || "", - contentLength: payload.contentLength || 0, - truncated: Boolean(payload.truncated), - metadata: payload.metadata || {} - }; - - s3State.previews[id] = entry; - return entry; - } catch (error) { - s3State.previews[id] = { - status: "error", - error: error.message || "Unknown S3 preview error" - }; - setS3Status("Could not load the S3 object preview.", "bad"); - return null; - } finally { - renderS3All(); - } - } - - async function copyText(value) { - try { - await navigator.clipboard.writeText(value); - } catch { - const input = document.createElement("textarea"); - input.value = value; - input.setAttribute("readonly", ""); - input.style.position = "fixed"; - input.style.opacity = "0"; - document.body.appendChild(input); - input.select(); - document.execCommand("copy"); - document.body.removeChild(input); - } - } - - function setStatus(message, tone) { - el.statusChip.className = "status"; - - if (tone) { - el.statusChip.classList.add(tone); - } - - el.statusChip.textContent = message; - } - - function setLogsStatus(message, tone) { - el.logsStatusChip.className = "status"; - - if (tone) { - el.logsStatusChip.classList.add(tone); - } - - el.logsStatusChip.textContent = message; - } - - function setSecretsStatus(message, tone) { - el.secretsStatusChip.className = "status"; - - if (tone) { - el.secretsStatusChip.classList.add(tone); - } - - el.secretsStatusChip.textContent = message; - } - - function setS3Status(message, tone) { - el.s3StatusChip.className = "status"; - - if (tone) { - el.s3StatusChip.classList.add(tone); - } - - el.s3StatusChip.textContent = message; - } - - function getInitialPanel() { - const storedPanel = readStoredValue(PANEL_STORAGE_KEY); - return ["emails", "logs", "secrets", "s3"].includes(storedPanel) ? storedPanel : "emails"; - } - - function getInitialTheme() { - const storedTheme = readStoredValue(THEME_STORAGE_KEY); - - if (storedTheme === "dark" || storedTheme === "light") { - return storedTheme; - } - - return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light"; - } - - function applyTheme(theme) { - const nextTheme = theme === "dark" ? "dark" : "light"; - appState.theme = nextTheme; - document.body.dataset.theme = nextTheme; - - try { - window.localStorage.setItem(THEME_STORAGE_KEY, nextTheme); - } catch {} - - renderThemeToggle(); - } - - function renderThemeToggle() { - if (!el.themeToggle) { - return; - } - - const isDark = appState.theme === "dark"; - el.themeToggle.textContent = isDark ? "🌙 Dark theme" : "☀️ Light theme"; - el.themeToggle.setAttribute("aria-pressed", isDark ? "true" : "false"); - el.themeToggle.setAttribute("aria-label", isDark ? "Switch to light theme" : "Switch to dark theme"); - el.themeToggle.title = isDark ? "Switch to light theme" : "Switch to dark theme"; - } - - function persistPanel() { - writeStoredValue(PANEL_STORAGE_KEY, appState.panel); - } - - function persistEmailPreferences() { - writeStoredJson(EMAIL_PREFERENCES_STORAGE_KEY, { - search: state.search, - auto: state.auto, - interval: state.interval - }); - } - - function persistLogPreferences() { - writeStoredJson(LOG_PREFERENCES_STORAGE_KEY, { - group: logsState.group, - stream: logsState.stream, - search: logsState.search, - auto: logsState.auto, - interval: logsState.interval, - windowMs: logsState.windowMs, - limit: logsState.limit, - wrapLines: logsState.wrapLines, - tailNewest: logsState.tailNewest - }); - } - - function persistSecretPreferences() { - writeStoredJson(SECRET_PREFERENCES_STORAGE_KEY, { - search: secretsState.search, - auto: secretsState.auto, - interval: secretsState.interval - }); - } - - function persistS3Preferences() { - writeStoredJson(S3_PREFERENCES_STORAGE_KEY, { - bucket: s3State.bucket, - prefix: s3State.prefix, - search: s3State.search, - auto: s3State.auto, - interval: s3State.interval - }); - } - - function resetSavedState() { - [ - THEME_STORAGE_KEY, - PANEL_STORAGE_KEY, - EMAIL_PREFERENCES_STORAGE_KEY, - LOG_PREFERENCES_STORAGE_KEY, - SECRET_PREFERENCES_STORAGE_KEY, - S3_PREFERENCES_STORAGE_KEY - ].forEach((key) => { - try { - window.localStorage.removeItem(key); - } catch {} - }); - - window.location.reload(); - } - - function getStoredPreferences(key) { - try { - const rawValue = window.localStorage.getItem(key); - - if (!rawValue) { - return null; - } - - const parsedValue = JSON.parse(rawValue); - return parsedValue && typeof parsedValue === "object" && !Array.isArray(parsedValue) ? parsedValue : null; - } catch { - return null; - } - } - - function readStoredValue(key) { - try { - return window.localStorage.getItem(key); - } catch { - return null; - } - } - - function writeStoredValue(key, value) { - try { - window.localStorage.setItem(key, String(value)); - } catch {} - } - - function writeStoredJson(key, value) { - try { - window.localStorage.setItem(key, JSON.stringify(value)); - } catch {} - } - - function getStoredText(value, fallback = "") { - return typeof value === "string" ? value : fallback; - } - - function getStoredBoolean(value, fallback) { - return typeof value === "boolean" ? value : fallback; - } - - function getStoredNumber(value, allowedValues, fallback) { - const normalizedValue = Number(value); - return Number.isFinite(normalizedValue) && allowedValues.includes(normalizedValue) ? normalizedValue : fallback; - } - - function scrollPaneToTop(element) { - if (!element) { - return; - } - - element.scrollTo({ top: 0, behavior: "smooth" }); - } - - function updatePaneTopButtonVisibility(pane, button) { - if (!pane || !button) { - return; - } - - button.classList.toggle("visible", pane.scrollTop > 140); - } - - function formatDateTime(value) { - if (!value) { - return "Unknown time"; - } - - const date = new Date(value); - return Number.isNaN(date.getTime()) - ? "Unknown time" - : new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date); - } - - function formatRelative(timestampMs) { - const seconds = Math.max(1, Math.round((Date.now() - timestampMs) / 1000)); - - if (seconds < 60) { - return `${seconds}s ago`; - } - - const minutes = Math.round(seconds / 60); - - if (minutes < 60) { - return `${minutes}m ago`; - } - - const hours = Math.round(minutes / 60); - - if (hours < 24) { - return `${hours}h ago`; - } - - return `${Math.round(hours / 24)}d ago`; - } - - function formatBytes(value) { - if (!value) { - return "0 B"; - } - - const units = ["B", "KB", "MB", "GB"]; - let size = value; - let index = 0; - - while (size >= 1024 && index < units.length - 1) { - size /= 1024; - index += 1; - } - - return `${index === 0 ? size : size.toFixed(1)} ${units[index]}`; - } - - function formatLogMessage(message) { - const value = String(message || "").trim(); - - if (!value) { - return "No log payload."; - } - - try { - return JSON.stringify(JSON.parse(value), null, 2); - } catch { - return value; - } - } - - function buildSecretPreview(secret) { - const fragments = []; - - if (secret.description) { - fragments.push(secret.description); - } - - if (secret.owningService) { - fragments.push(`Service: ${secret.owningService}`); - } - - if (secret.primaryRegion) { - fragments.push(`Region: ${secret.primaryRegion}`); - } - - if (secret.tagCount) { - fragments.push(`${secret.tagCount} tag${secret.tagCount === 1 ? "" : "s"}`); - } - - if (!fragments.length) { - fragments.push("No description or tags yet."); - } - - return fragments.join(" • "); - } - - function toggleSecretReveal(id) { - if (secretsState.revealedIds.has(id)) { - secretsState.revealedIds.delete(id); - } else { - secretsState.revealedIds.add(id); - } - - renderSecretsAll(); - } - - function maskSecretValue(value) { - const source = String(value || ""); - - if (!source) { - return "Secret is loaded but empty."; - } - - const lines = source.split("\n"); - return lines.map((line) => "•".repeat(Math.max(12, Math.min(line.length || 0, 48)))).join("\n"); - } - - function buildS3Preview(object) { - const fragments = []; - - if (object.storageClass) { - fragments.push(object.storageClass); - } - - fragments.push(formatBytes(object.size)); - - if (object.etag) { - fragments.push(`ETag ${object.etag.slice(0, 12)}`); - } - - return fragments.join(" • "); - } - - function prettyJsonOrText(value) { - const parsed = tryParseJsonText(value); - return parsed.ok ? JSON.stringify(parsed.value, null, 2) : String(value || ""); - } - - function detectLogLevel(event) { - const parsed = tryParseJsonText(event?.message); - const candidates = parsed.ok - ? [parsed.value?.level, parsed.value?.severity, parsed.value?.logLevel, parsed.value?.status, parsed.value?.lvl] - : [String(event?.message || "").match(/\b(error|warn|warning|info|debug|trace|fatal)\b/i)?.[0] || ""]; - const normalized = String(candidates.find(Boolean) || "").toLowerCase(); - - if (["fatal", "error", "critical"].includes(normalized)) { - return { label: normalized.toUpperCase(), className: "levelError" }; - } - - if (["warn", "warning"].includes(normalized)) { - return { label: "WARN", className: "levelWarn" }; - } - - if (["info", "notice"].includes(normalized)) { - return { label: normalized.toUpperCase(), className: "levelInfo" }; - } - - if (["debug", "trace"].includes(normalized)) { - return { label: normalized.toUpperCase(), className: "levelDebug" }; - } - - return null; - } - - function renderLogPreviewContent(event) { - const parsedLog = tryParseJsonText(event?.message); - - if (!parsedLog.ok) { - return escapeHtml(event?.preview || "No preview available."); - } - - const compactJson = JSON.stringify(parsedLog.value); - const previewText = compactJson.length > 220 ? `${compactJson.slice(0, 217)}...` : compactJson; - return highlightJsonText(previewText); - } - - function renderLogBodyContent(message) { - const parsedLog = tryParseJsonText(message); - - if (!parsedLog.ok) { - return escapeHtml(formatLogMessage(message)); - } - - return highlightJsonText(JSON.stringify(parsedLog.value, null, 2)); - } - - function tryParseJsonText(message) { - const value = String(message || "").trim(); - - if (!value) { - return { ok: false, value: null }; - } - - try { - return { ok: true, value: JSON.parse(value) }; - } catch { - return { ok: false, value: null }; - } - } - - function highlightJsonText(value) { - const source = String(value ?? ""); - const tokenRegex = - /("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?)/g; - let html = ""; - let lastIndex = 0; - - for (const match of source.matchAll(tokenRegex)) { - const [token] = match; - const index = match.index ?? 0; - let className = "jsonNumber"; - - html += escapeHtml(source.slice(lastIndex, index)); - - if (token.endsWith(":")) { - className = "jsonKey"; - } else if (token === "true" || token === "false") { - className = "jsonBoolean"; - } else if (token === "null") { - className = "jsonNull"; - } else if (token.startsWith('"')) { - className = "jsonString"; - } - - html += `${escapeHtml(token)}`; - lastIndex = index + token.length; - } - - html += escapeHtml(source.slice(lastIndex)); - return html; - } - - function escapeHtml(value) { - return String(value ?? "") - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - - async function safeJson(response) { - try { - return await response.json(); - } catch { - return null; - } - } -} - app.listen(PORT, () => { console.log(`LocalStack inspector is running on http://localhost:${PORT}`); console.log(`Watching LocalStack SES endpoint at ${SES_ENDPOINT}`); diff --git a/_reference/localEmailViewer/package.json b/_reference/localEmailViewer/package.json index 956794f4c..6972c7f95 100644 --- a/_reference/localEmailViewer/package.json +++ b/_reference/localEmailViewer/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "start": "node index.js", - "check": "node --check index.js" + "check": "node --check index.js && node --check public/client-app.js && node --check server/config.js && node --check server/localstack-service.js && node --check server/page.js" }, "keywords": [], "author": "", diff --git a/_reference/localEmailViewer/public/client-app.js b/_reference/localEmailViewer/public/client-app.js new file mode 100644 index 000000000..2e446c8fe --- /dev/null +++ b/_reference/localEmailViewer/public/client-app.js @@ -0,0 +1,3154 @@ +function clientApp(config) { + const THEME_STORAGE_KEY = "localstack-inspector-theme"; + const PANEL_STORAGE_KEY = "localstack-inspector-panel"; + const EMAIL_PREFERENCES_STORAGE_KEY = "localstack-inspector-email-preferences"; + const LOG_PREFERENCES_STORAGE_KEY = "localstack-inspector-log-preferences"; + const SECRET_PREFERENCES_STORAGE_KEY = "localstack-inspector-secret-preferences"; + const S3_PREFERENCES_STORAGE_KEY = "localstack-inspector-s3-preferences"; + const HEALTH_REFRESH_MS = 30000; + const REFRESH_INTERVALS = [5000, 10000, 15000, 30000, 60000]; + const LOG_WINDOWS = [300000, 900000, 3600000, 21600000, 86400000]; + const LOG_LIMITS = [100, 200, 300, 500]; + const storedEmailPreferences = getStoredPreferences(EMAIL_PREFERENCES_STORAGE_KEY); + const storedLogPreferences = getStoredPreferences(LOG_PREFERENCES_STORAGE_KEY); + const storedSecretPreferences = getStoredPreferences(SECRET_PREFERENCES_STORAGE_KEY); + const storedS3Preferences = getStoredPreferences(S3_PREFERENCES_STORAGE_KEY); + + const appState = { + panel: getInitialPanel(), + logsReady: false, + secretsReady: false, + s3Ready: false, + theme: getInitialTheme() + }; + + const state = { + messages: [], + filtered: [], + search: getStoredText(storedEmailPreferences?.search), + auto: getStoredBoolean(storedEmailPreferences?.auto, true), + interval: getStoredNumber(storedEmailPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs), + loading: false, + error: "", + updatedAt: 0, + source: "initial", + duration: 0, + parseErrors: 0, + newest: "", + newIds: new Set(), + knownIds: new Set(), + openIds: new Set(), + views: {}, + raw: {}, + listSignature: "" + }; + + const logsState = { + groups: [], + streams: [], + events: [], + filtered: [], + group: getStoredText(storedLogPreferences?.group, config.defaultLogGroup || ""), + stream: getStoredText(storedLogPreferences?.stream), + search: getStoredText(storedLogPreferences?.search), + auto: getStoredBoolean(storedLogPreferences?.auto, true), + interval: getStoredNumber(storedLogPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs), + windowMs: getStoredNumber(storedLogPreferences?.windowMs, LOG_WINDOWS, config.defaultLogWindowMs), + limit: getStoredNumber(storedLogPreferences?.limit, LOG_LIMITS, config.defaultLogLimit), + wrapLines: getStoredBoolean(storedLogPreferences?.wrapLines, true), + tailNewest: getStoredBoolean(storedLogPreferences?.tailNewest, false), + loading: false, + error: "", + updatedAt: 0, + source: "initial", + duration: 0, + newest: 0, + searchedLogStreams: 0, + openIds: new Set(), + listSignature: "" + }; + + const secretsState = { + items: [], + filtered: [], + search: getStoredText(storedSecretPreferences?.search), + auto: getStoredBoolean(storedSecretPreferences?.auto, true), + interval: getStoredNumber(storedSecretPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs), + loading: false, + error: "", + updatedAt: 0, + source: "initial", + duration: 0, + newest: "", + openIds: new Set(), + values: {}, + revealedIds: new Set(), + listSignature: "" + }; + + const s3State = { + buckets: [], + objects: [], + filtered: [], + bucket: getStoredText(storedS3Preferences?.bucket, config.defaultS3Bucket || ""), + prefix: getStoredText(storedS3Preferences?.prefix), + search: getStoredText(storedS3Preferences?.search), + auto: getStoredBoolean(storedS3Preferences?.auto, true), + interval: getStoredNumber(storedS3Preferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs), + loading: false, + error: "", + updatedAt: 0, + source: "initial", + duration: 0, + newest: "", + openIds: new Set(), + previews: {}, + listSignature: "" + }; + + const healthState = { + services: {}, + loading: false, + error: "", + updatedAt: 0, + source: "initial" + }; + + const el = { + themeToggle: document.getElementById("themeToggle"), + resetStateButton: document.getElementById("resetStateButton"), + healthRefreshButton: document.getElementById("healthRefreshButton"), + healthStrip: document.getElementById("healthStrip"), + emailsPanel: document.getElementById("emailsPanel"), + logsPanel: document.getElementById("logsPanel"), + secretsPanel: document.getElementById("secretsPanel"), + s3Panel: document.getElementById("s3Panel"), + refreshButton: document.getElementById("refreshButton"), + autoToggle: document.getElementById("autoToggle"), + intervalSelect: document.getElementById("intervalSelect"), + searchInput: document.getElementById("searchInput"), + clearSearchButton: document.getElementById("clearSearchButton"), + expandAllButton: document.getElementById("expandAllButton"), + collapseAllButton: document.getElementById("collapseAllButton"), + scrollToTopButton: document.getElementById("scrollToTopButton"), + statusChip: document.getElementById("statusChip"), + totalStat: document.getElementById("totalStat"), + visibleStat: document.getElementById("visibleStat"), + newStat: document.getElementById("newStat"), + newestStat: document.getElementById("newestStat"), + updatedStat: document.getElementById("updatedStat"), + fetchStat: document.getElementById("fetchStat"), + fetchDetail: document.getElementById("fetchDetail"), + banner: document.getElementById("banner"), + empty: document.getElementById("empty"), + list: document.getElementById("list"), + emailsContentPane: document.getElementById("emailsContentPane"), + logsRefreshButton: document.getElementById("logsRefreshButton"), + logsAutoToggle: document.getElementById("logsAutoToggle"), + logsIntervalSelect: document.getElementById("logsIntervalSelect"), + logsGroupSelect: document.getElementById("logsGroupSelect"), + logsStreamSelect: document.getElementById("logsStreamSelect"), + logsWindowSelect: document.getElementById("logsWindowSelect"), + logsLimitSelect: document.getElementById("logsLimitSelect"), + logsSearchInput: document.getElementById("logsSearchInput"), + logsClearSearchButton: document.getElementById("logsClearSearchButton"), + logsWrapToggle: document.getElementById("logsWrapToggle"), + logsTailToggle: document.getElementById("logsTailToggle"), + logsExpandAllButton: document.getElementById("logsExpandAllButton"), + logsCollapseAllButton: document.getElementById("logsCollapseAllButton"), + logsScrollToTopButton: document.getElementById("logsScrollToTopButton"), + logsStatusChip: document.getElementById("logsStatusChip"), + logsTotalStat: document.getElementById("logsTotalStat"), + logsVisibleStat: document.getElementById("logsVisibleStat"), + logsStreamsStat: document.getElementById("logsStreamsStat"), + logsNewestStat: document.getElementById("logsNewestStat"), + logsUpdatedStat: document.getElementById("logsUpdatedStat"), + logsFetchStat: document.getElementById("logsFetchStat"), + logsFetchDetail: document.getElementById("logsFetchDetail"), + logsBanner: document.getElementById("logsBanner"), + logsEmpty: document.getElementById("logsEmpty"), + logsList: document.getElementById("logsList"), + logsContentPane: document.getElementById("logsContentPane"), + secretsRefreshButton: document.getElementById("secretsRefreshButton"), + secretsAutoToggle: document.getElementById("secretsAutoToggle"), + secretsIntervalSelect: document.getElementById("secretsIntervalSelect"), + secretsSearchInput: document.getElementById("secretsSearchInput"), + secretsClearSearchButton: document.getElementById("secretsClearSearchButton"), + secretsExpandAllButton: document.getElementById("secretsExpandAllButton"), + secretsCollapseAllButton: document.getElementById("secretsCollapseAllButton"), + secretsScrollToTopButton: document.getElementById("secretsScrollToTopButton"), + secretsStatusChip: document.getElementById("secretsStatusChip"), + secretsTotalStat: document.getElementById("secretsTotalStat"), + secretsVisibleStat: document.getElementById("secretsVisibleStat"), + secretsLoadedStat: document.getElementById("secretsLoadedStat"), + secretsNewestStat: document.getElementById("secretsNewestStat"), + secretsUpdatedStat: document.getElementById("secretsUpdatedStat"), + secretsFetchStat: document.getElementById("secretsFetchStat"), + secretsFetchDetail: document.getElementById("secretsFetchDetail"), + secretsBanner: document.getElementById("secretsBanner"), + secretsEmpty: document.getElementById("secretsEmpty"), + secretsList: document.getElementById("secretsList"), + secretsContentPane: document.getElementById("secretsContentPane"), + s3RefreshButton: document.getElementById("s3RefreshButton"), + s3AutoToggle: document.getElementById("s3AutoToggle"), + s3IntervalSelect: document.getElementById("s3IntervalSelect"), + s3BucketSelect: document.getElementById("s3BucketSelect"), + s3PrefixInput: document.getElementById("s3PrefixInput"), + s3ApplyPrefixButton: document.getElementById("s3ApplyPrefixButton"), + s3SearchInput: document.getElementById("s3SearchInput"), + s3ClearSearchButton: document.getElementById("s3ClearSearchButton"), + s3ExpandAllButton: document.getElementById("s3ExpandAllButton"), + s3CollapseAllButton: document.getElementById("s3CollapseAllButton"), + s3ScrollToTopButton: document.getElementById("s3ScrollToTopButton"), + s3StatusChip: document.getElementById("s3StatusChip"), + s3TotalStat: document.getElementById("s3TotalStat"), + s3VisibleStat: document.getElementById("s3VisibleStat"), + s3BucketsStat: document.getElementById("s3BucketsStat"), + s3NewestStat: document.getElementById("s3NewestStat"), + s3UpdatedStat: document.getElementById("s3UpdatedStat"), + s3FetchStat: document.getElementById("s3FetchStat"), + s3FetchDetail: document.getElementById("s3FetchDetail"), + s3Banner: document.getElementById("s3Banner"), + s3Empty: document.getElementById("s3Empty"), + s3List: document.getElementById("s3List"), + s3ContentPane: document.getElementById("s3ContentPane") + }; + + el.autoToggle.checked = state.auto; + el.intervalSelect.value = String(state.interval); + el.searchInput.value = state.search; + el.logsAutoToggle.checked = logsState.auto; + el.logsIntervalSelect.value = String(logsState.interval); + el.logsWindowSelect.value = String(logsState.windowMs); + el.logsLimitSelect.value = String(logsState.limit); + el.logsSearchInput.value = logsState.search; + el.logsWrapToggle.checked = logsState.wrapLines; + el.logsTailToggle.checked = logsState.tailNewest; + el.secretsAutoToggle.checked = secretsState.auto; + el.secretsIntervalSelect.value = String(secretsState.interval); + el.secretsSearchInput.value = secretsState.search; + el.s3AutoToggle.checked = s3State.auto; + el.s3IntervalSelect.value = String(s3State.interval); + el.s3PrefixInput.value = s3State.prefix; + el.s3SearchInput.value = s3State.search; + applyTheme(appState.theme); + persistPanel(); + persistEmailPreferences(); + persistLogPreferences(); + persistSecretPreferences(); + persistS3Preferences(); + wire(); + renderWorkspace(); + renderAll(); + renderLogsAll(); + renderSecretsAll(); + renderS3All(); + renderHealthStrip(); + if (appState.panel === "logs") { + ensureLogsReady(); + } else if (appState.panel === "secrets") { + ensureSecretsReady(); + } else if (appState.panel === "s3") { + ensureS3Ready(); + } else { + refreshMessages("initial"); + } + refreshHealthSummary("initial"); + window.setInterval(() => { + if (!document.hidden) { + refreshHealthSummary("auto"); + } + }, HEALTH_REFRESH_MS); + window.setInterval(() => { + renderLiveClock(); + renderLogsLiveClock(); + renderSecretsLiveClock(); + renderS3LiveClock(); + renderHealthStrip(); + }, 1000); + + function wire() { + el.themeToggle.addEventListener("click", () => { + applyTheme(appState.theme === "dark" ? "light" : "dark"); + }); + + el.resetStateButton.addEventListener("click", () => { + resetSavedState(); + }); + + el.healthRefreshButton.addEventListener("click", () => { + refreshHealthSummary("manual"); + }); + + el.healthStrip.addEventListener("click", async (event) => { + const button = event.target.closest("button[data-health-panel]"); + + if (!button) { + return; + } + + await setPanel(button.dataset.healthPanel); + }); + + el.refreshButton.addEventListener("click", () => refreshMessages("manual")); + + el.autoToggle.addEventListener("change", () => { + state.auto = el.autoToggle.checked; + persistEmailPreferences(); + scheduleRefresh(); + renderStatus(); + }); + + el.intervalSelect.addEventListener("change", () => { + state.interval = Number(el.intervalSelect.value) || config.defaultRefreshMs; + persistEmailPreferences(); + scheduleRefresh(); + renderStatus(); + }); + + el.searchInput.addEventListener("input", (event) => { + state.search = event.target.value; + applyFilter(); + }); + + el.clearSearchButton.addEventListener("click", () => { + state.search = ""; + el.searchInput.value = ""; + applyFilter(); + }); + + el.expandAllButton.addEventListener("click", () => { + state.filtered.forEach((message) => state.openIds.add(message.id)); + syncCardExpansion(); + }); + + el.collapseAllButton.addEventListener("click", () => { + state.openIds.clear(); + syncCardExpansion(); + }); + + el.scrollToTopButton.addEventListener("click", () => { + scrollPaneToTop(el.emailsContentPane); + }); + + el.emailsContentPane.addEventListener("scroll", () => { + updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); + }); + + el.list.addEventListener("click", async (event) => { + const button = event.target.closest("button[data-action]"); + + if (!button) { + return; + } + + const id = button.dataset.id; + const message = getMessage(id); + + if (!message) { + return; + } + + if (button.dataset.action === "view") { + state.views[id] = button.dataset.view; + renderList(); + return; + } + + if (button.dataset.action === "load-raw") { + await loadRaw(id); + renderList(); + return; + } + + if (button.dataset.action === "copy-raw") { + const raw = await loadRaw(id); + + if (raw) { + await copyText(raw); + setStatus("Raw message copied to the clipboard.", "ok"); + } + } + }); + + el.logsRefreshButton.addEventListener("click", () => refreshLogs("manual")); + + el.logsAutoToggle.addEventListener("change", () => { + logsState.auto = el.logsAutoToggle.checked; + persistLogPreferences(); + scheduleLogsRefresh(); + renderLogsStatus(); + }); + + el.logsIntervalSelect.addEventListener("change", () => { + logsState.interval = Number(el.logsIntervalSelect.value) || config.defaultRefreshMs; + persistLogPreferences(); + scheduleLogsRefresh(); + renderLogsStatus(); + }); + + el.logsWindowSelect.addEventListener("change", () => { + logsState.windowMs = Number(el.logsWindowSelect.value) || config.defaultLogWindowMs; + persistLogPreferences(); + refreshLogs("window"); + }); + + el.logsLimitSelect.addEventListener("change", () => { + logsState.limit = Number(el.logsLimitSelect.value) || config.defaultLogLimit; + persistLogPreferences(); + refreshLogs("limit"); + }); + + el.logsSearchInput.addEventListener("input", (event) => { + logsState.search = event.target.value; + applyLogsFilter(); + }); + + el.logsClearSearchButton.addEventListener("click", () => { + logsState.search = ""; + el.logsSearchInput.value = ""; + applyLogsFilter(); + }); + + el.logsWrapToggle.addEventListener("change", () => { + logsState.wrapLines = el.logsWrapToggle.checked; + persistLogPreferences(); + renderLogsList(); + }); + + el.logsTailToggle.addEventListener("change", () => { + logsState.tailNewest = el.logsTailToggle.checked; + persistLogPreferences(); + }); + + el.logsExpandAllButton.addEventListener("click", () => { + logsState.filtered.forEach((event) => logsState.openIds.add(event.id)); + renderLogsList(); + }); + + el.logsCollapseAllButton.addEventListener("click", () => { + logsState.openIds.clear(); + renderLogsList(); + }); + + el.logsScrollToTopButton.addEventListener("click", () => { + scrollPaneToTop(el.logsContentPane); + }); + + el.logsContentPane.addEventListener("scroll", () => { + updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); + }); + + el.logsGroupSelect.addEventListener("change", async () => { + logsState.group = el.logsGroupSelect.value; + logsState.stream = ""; + persistLogPreferences(); + await refreshLogStreams(); + await refreshLogs("group"); + }); + + el.logsStreamSelect.addEventListener("change", () => { + logsState.stream = el.logsStreamSelect.value; + persistLogPreferences(); + refreshLogs("stream"); + }); + + el.logsList.addEventListener("click", async (event) => { + const button = event.target.closest("button[data-log-action]"); + + if (!button) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + const logEvent = getLogEvent(button.dataset.id); + + if (!logEvent) { + return; + } + + if (button.dataset.logAction === "copy") { + await copyText(formatLogMessage(logEvent.message)); + setLogsStatus("Log payload copied to the clipboard.", "ok"); + } + }); + + el.secretsRefreshButton.addEventListener("click", () => refreshSecrets("manual")); + + el.secretsAutoToggle.addEventListener("change", () => { + secretsState.auto = el.secretsAutoToggle.checked; + persistSecretPreferences(); + scheduleSecretsRefresh(); + renderSecretsStatus(); + }); + + el.secretsIntervalSelect.addEventListener("change", () => { + secretsState.interval = Number(el.secretsIntervalSelect.value) || config.defaultRefreshMs; + persistSecretPreferences(); + scheduleSecretsRefresh(); + renderSecretsStatus(); + }); + + el.secretsSearchInput.addEventListener("input", (event) => { + secretsState.search = event.target.value; + applySecretsFilter(); + }); + + el.secretsClearSearchButton.addEventListener("click", () => { + secretsState.search = ""; + el.secretsSearchInput.value = ""; + applySecretsFilter(); + }); + + el.secretsExpandAllButton.addEventListener("click", () => { + secretsState.filtered.forEach((secret) => secretsState.openIds.add(secret.id)); + syncSecretExpansion(); + }); + + el.secretsCollapseAllButton.addEventListener("click", () => { + secretsState.openIds.clear(); + syncSecretExpansion(); + }); + + el.secretsScrollToTopButton.addEventListener("click", () => { + scrollPaneToTop(el.secretsContentPane); + }); + + el.secretsContentPane.addEventListener("scroll", () => { + updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); + }); + + el.secretsList.addEventListener("click", async (event) => { + const button = event.target.closest("button[data-secret-action]"); + + if (!button) { + return; + } + + const id = button.dataset.id; + const secret = getSecret(id); + + if (!secret) { + return; + } + + if (button.dataset.secretAction === "load-value") { + await ensureSecretValue(id, { force: false }); + return; + } + + if (button.dataset.secretAction === "reload-value") { + await ensureSecretValue(id, { force: true }); + return; + } + + if (button.dataset.secretAction === "copy-value") { + const entry = await ensureSecretValue(id, { force: false }); + + if (entry?.status === "loaded") { + await copyText(entry.copyValue); + setSecretsStatus("Secret value copied to the clipboard.", "ok"); + } + + return; + } + + if (button.dataset.secretAction === "toggle-reveal") { + toggleSecretReveal(id); + return; + } + + if (button.dataset.secretAction === "copy-name") { + await copyText(secret.name || ""); + setSecretsStatus("Secret name copied to the clipboard.", "ok"); + return; + } + + if (button.dataset.secretAction === "copy-arn") { + await copyText(secret.arn || ""); + setSecretsStatus("Secret ARN copied to the clipboard.", "ok"); + return; + } + + if (button.dataset.secretAction === "copy-env") { + const entry = await ensureSecretValue(id, { force: false }); + + if (entry?.status === "loaded") { + await copyText(`${secret.name}=${entry.copyValue}`); + setSecretsStatus("Secret env line copied to the clipboard.", "ok"); + } + } + }); + + el.s3RefreshButton.addEventListener("click", async () => { + await refreshS3Buckets(); + await refreshS3("manual"); + }); + + el.s3AutoToggle.addEventListener("change", () => { + s3State.auto = el.s3AutoToggle.checked; + persistS3Preferences(); + scheduleS3Refresh(); + renderS3Status(); + }); + + el.s3IntervalSelect.addEventListener("change", () => { + s3State.interval = Number(el.s3IntervalSelect.value) || config.defaultRefreshMs; + persistS3Preferences(); + scheduleS3Refresh(); + renderS3Status(); + }); + + el.s3BucketSelect.addEventListener("change", () => { + s3State.bucket = el.s3BucketSelect.value; + persistS3Preferences(); + refreshS3("bucket"); + }); + + el.s3PrefixInput.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + s3State.prefix = el.s3PrefixInput.value.trim(); + persistS3Preferences(); + refreshS3("prefix"); + } + }); + + el.s3ApplyPrefixButton.addEventListener("click", () => { + s3State.prefix = el.s3PrefixInput.value.trim(); + persistS3Preferences(); + refreshS3("prefix"); + }); + + el.s3SearchInput.addEventListener("input", (event) => { + s3State.search = event.target.value; + applyS3Filter(); + }); + + el.s3ClearSearchButton.addEventListener("click", () => { + s3State.search = ""; + el.s3SearchInput.value = ""; + applyS3Filter(); + }); + + el.s3ExpandAllButton.addEventListener("click", () => { + s3State.filtered.forEach((object) => s3State.openIds.add(object.id)); + syncS3Expansion(); + }); + + el.s3CollapseAllButton.addEventListener("click", () => { + s3State.openIds.clear(); + syncS3Expansion(); + }); + + el.s3ScrollToTopButton.addEventListener("click", () => { + scrollPaneToTop(el.s3ContentPane); + }); + + el.s3ContentPane.addEventListener("scroll", () => { + updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); + }); + + el.s3List.addEventListener("click", async (event) => { + const button = event.target.closest("button[data-s3-action]"); + + if (!button) { + return; + } + + const id = button.dataset.id; + const object = getS3Object(id); + + if (!object) { + return; + } + + if (button.dataset.s3Action === "load-preview") { + await ensureS3Preview(id, { force: false }); + return; + } + + if (button.dataset.s3Action === "reload-preview") { + await ensureS3Preview(id, { force: true }); + return; + } + + if (button.dataset.s3Action === "copy-key") { + await copyText(object.key); + setS3Status("Object key copied to the clipboard.", "ok"); + return; + } + + if (button.dataset.s3Action === "copy-uri") { + await copyText(`s3://${object.bucket}/${object.key}`); + setS3Status("S3 URI copied to the clipboard.", "ok"); + } + }); + + document.addEventListener("visibilitychange", () => { + window.clearTimeout(state.timer); + window.clearTimeout(logsState.timer); + window.clearTimeout(secretsState.timer); + window.clearTimeout(s3State.timer); + + if (document.hidden) { + renderStatus(); + renderLogsStatus(); + renderSecretsStatus(); + renderS3Status(); + return; + } + + refreshHealthSummary("visibility"); + + if (appState.panel === "s3") { + if (s3State.auto) { + refreshS3("visibility"); + } else { + renderS3Status(); + } + return; + } + + if (appState.panel === "secrets") { + if (secretsState.auto) { + refreshSecrets("visibility"); + } else { + renderSecretsStatus(); + } + return; + } + + if (appState.panel === "logs") { + if (logsState.auto) { + refreshLogs("visibility"); + } else { + renderLogsStatus(); + } + return; + } + + if (state.auto) { + refreshMessages("visibility"); + } else { + renderStatus(); + } + }); + + window.addEventListener("keydown", (event) => { + const isField = + event.target instanceof HTMLElement && + (event.target.matches("input,textarea,select") || event.target.isContentEditable); + + if (!isField && event.key.toLowerCase() === "r") { + event.preventDefault(); + + if (appState.panel === "logs") { + refreshLogs("keyboard"); + return; + } + + if (appState.panel === "secrets") { + refreshSecrets("keyboard"); + return; + } + + if (appState.panel === "s3") { + refreshS3("keyboard"); + return; + } + + refreshMessages("keyboard"); + } + }); + + updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); + updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); + updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); + updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); + } + + async function setPanel(panel) { + if (!panel || panel === appState.panel) { + if (panel === "logs") { + await ensureLogsReady(); + } else if (panel === "secrets") { + await ensureSecretsReady(); + } else if (panel === "s3") { + await ensureS3Ready(); + } + + renderWorkspace(); + return; + } + + appState.panel = panel; + persistPanel(); + renderWorkspace(); + + if (panel === "logs") { + await ensureLogsReady(); + } else if (panel === "secrets") { + await ensureSecretsReady(); + } else if (panel === "s3") { + await ensureS3Ready(); + } else if (!state.updatedAt && !state.loading) { + await refreshMessages("panel"); + } + + scheduleRefresh(); + scheduleLogsRefresh(); + scheduleSecretsRefresh(); + scheduleS3Refresh(); + renderStatus(); + renderLogsStatus(); + renderSecretsStatus(); + renderS3Status(); + } + + function renderWorkspace() { + el.emailsPanel.hidden = appState.panel !== "emails"; + el.logsPanel.hidden = appState.panel !== "logs"; + el.secretsPanel.hidden = appState.panel !== "secrets"; + el.s3Panel.hidden = appState.panel !== "s3"; + renderHealthStrip(); + } + + async function ensureLogsReady() { + if (appState.logsReady) { + return; + } + + await refreshLogGroups(); + appState.logsReady = !logsState.error; + + if (logsState.group) { + await refreshLogs("initial"); + } + } + + async function ensureSecretsReady() { + if (appState.secretsReady) { + return; + } + + await refreshSecrets("initial"); + appState.secretsReady = !secretsState.error; + } + + async function ensureS3Ready() { + if (appState.s3Ready) { + return; + } + + await refreshS3Buckets(); + appState.s3Ready = !s3State.error; + + if (s3State.bucket) { + await refreshS3("initial"); + } + } + + async function refreshMessages(source) { + if (state.loading) { + return; + } + + let shouldRenderList = false; + + state.loading = true; + state.source = source; + state.error = ""; + renderStatus(); + renderFetch(); + + try { + const response = await fetch("/api/messages", { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + const messages = Array.isArray(payload.messages) ? payload.messages : []; + const nextIds = new Set(messages.map((message) => message.id)); + const nextSignature = computeListSignature(messages); + + shouldRenderList = nextSignature !== state.listSignature; + state.newIds = + state.updatedAt && shouldRenderList + ? new Set(messages.filter((message) => !state.knownIds.has(message.id)).map((message) => message.id)) + : state.newIds; + state.knownIds = nextIds; + state.messages = messages; + state.duration = payload.fetchDurationMs || 0; + state.parseErrors = payload.parseErrors || 0; + state.newest = payload.latestMessageTimestamp || ""; + state.updatedAt = Date.now(); + state.listSignature = nextSignature; + + pruneState(); + applyFilter(shouldRenderList); + setStatus(`Updated ${messages.length} message${messages.length === 1 ? "" : "s"}.`, "ok"); + } catch (error) { + state.error = error.message || "Unknown refresh error"; + setStatus(`Refresh failed: ${state.error}`, "bad"); + } finally { + state.loading = false; + scheduleRefresh(); + renderAll({ renderList: shouldRenderList }); + } + } + + async function refreshLogGroups() { + try { + const response = await fetch("/api/logs/groups", { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + logsState.groups = Array.isArray(payload.groups) ? payload.groups : []; + + const availableGroups = logsState.groups.map((group) => group.name).filter(Boolean); + + if (!availableGroups.includes(logsState.group)) { + logsState.group = availableGroups.includes(config.defaultLogGroup) + ? config.defaultLogGroup + : availableGroups[0] || ""; + } + + logsState.error = ""; + await refreshLogStreams(); + } catch (error) { + logsState.error = error.message || "Unknown log group refresh error"; + } finally { + persistLogPreferences(); + renderLogsAll(); + } + } + + async function refreshLogStreams() { + if (!logsState.group) { + logsState.streams = []; + logsState.stream = ""; + renderLogsAll(); + return; + } + + try { + const response = await fetch(`/api/logs/streams?group=${encodeURIComponent(logsState.group)}`, { + cache: "no-store" + }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + logsState.streams = Array.isArray(payload.streams) ? payload.streams : []; + + if (!logsState.streams.some((stream) => stream.name === logsState.stream)) { + logsState.stream = ""; + } + + logsState.error = ""; + } catch (error) { + logsState.streams = []; + logsState.stream = ""; + logsState.error = error.message || "Unknown log stream refresh error"; + } finally { + persistLogPreferences(); + renderLogsAll(); + } + } + + async function refreshLogs(source) { + if (logsState.loading) { + return; + } + + if (!appState.logsReady) { + await ensureLogsReady(); + return; + } + + if (!logsState.group) { + logsState.error = logsState.groups.length ? "Choose a log group to load events." : "No log groups found."; + renderLogsAll(); + return; + } + + let shouldRenderList = false; + + logsState.loading = true; + logsState.source = source; + logsState.error = ""; + renderLogsStatus(); + renderLogsFetch(); + + try { + const params = new URLSearchParams({ + group: logsState.group, + windowMs: String(logsState.windowMs), + limit: String(logsState.limit) + }); + + if (logsState.stream) { + params.set("stream", logsState.stream); + } + + const response = await fetch(`/api/logs/events?${params.toString()}`, { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + const events = Array.isArray(payload.events) ? payload.events : []; + const nextSignature = computeLogListSignature(events); + shouldRenderList = nextSignature !== logsState.listSignature; + logsState.events = events; + logsState.duration = payload.fetchDurationMs || 0; + logsState.newest = payload.latestTimestamp || 0; + logsState.updatedAt = Date.now(); + logsState.searchedLogStreams = payload.searchedLogStreams || (logsState.stream ? 1 : logsState.streams.length); + logsState.listSignature = nextSignature; + + pruneLogsState(); + applyLogsFilter(shouldRenderList); + setLogsStatus(`Updated ${logsState.events.length} log event${logsState.events.length === 1 ? "" : "s"}.`, "ok"); + } catch (error) { + logsState.error = error.message || "Unknown log refresh error"; + setLogsStatus(`Refresh failed: ${logsState.error}`, "bad"); + } finally { + logsState.loading = false; + scheduleLogsRefresh(); + renderLogsAll({ renderList: shouldRenderList }); + + if (shouldRenderList && logsState.tailNewest) { + scrollPaneToTop(el.logsContentPane); + } + } + } + + async function refreshSecrets(source) { + if (secretsState.loading) { + return; + } + + let shouldRenderList = false; + + secretsState.loading = true; + secretsState.source = source; + secretsState.error = ""; + renderSecretsStatus(); + renderSecretsFetch(); + + try { + const response = await fetch("/api/secrets", { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + const items = Array.isArray(payload.secrets) ? payload.secrets : []; + const nextSignature = computeSecretsListSignature(items); + shouldRenderList = nextSignature !== secretsState.listSignature; + secretsState.items = items; + secretsState.duration = payload.fetchDurationMs || 0; + secretsState.newest = payload.latestTimestamp || ""; + secretsState.updatedAt = Date.now(); + secretsState.listSignature = nextSignature; + + pruneSecretsState(); + applySecretsFilter(shouldRenderList); + appState.secretsReady = true; + setSecretsStatus(`Updated ${items.length} secret${items.length === 1 ? "" : "s"}.`, "ok"); + } catch (error) { + secretsState.error = error.message || "Unknown secrets refresh error"; + appState.secretsReady = false; + setSecretsStatus(`Refresh failed: ${secretsState.error}`, "bad"); + } finally { + secretsState.loading = false; + scheduleSecretsRefresh(); + renderSecretsAll({ renderList: shouldRenderList }); + } + } + + async function refreshHealthSummary(source) { + if (healthState.loading && source === "auto") { + return; + } + + healthState.loading = true; + healthState.source = source; + renderHealthStrip(); + + try { + const response = await fetch("/api/service-health", { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + healthState.services = payload.services || {}; + healthState.updatedAt = Date.now(); + healthState.error = ""; + } catch (error) { + healthState.error = error.message || "Unknown health refresh error"; + } finally { + healthState.loading = false; + renderHealthStrip(); + } + } + + async function refreshS3Buckets() { + try { + const response = await fetch("/api/s3/buckets", { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + s3State.buckets = Array.isArray(payload.buckets) ? payload.buckets : []; + + const availableBuckets = s3State.buckets.map((bucket) => bucket.name).filter(Boolean); + + if (!availableBuckets.includes(s3State.bucket)) { + s3State.bucket = availableBuckets.includes(config.defaultS3Bucket) + ? config.defaultS3Bucket + : availableBuckets[0] || ""; + } + + s3State.error = ""; + } catch (error) { + s3State.buckets = []; + s3State.bucket = ""; + s3State.error = error.message || "Unknown S3 bucket refresh error"; + } finally { + appState.s3Ready = !s3State.error; + persistS3Preferences(); + renderS3All(); + } + } + + async function refreshS3(source) { + if (s3State.loading) { + return; + } + + if (!appState.s3Ready) { + await ensureS3Ready(); + return; + } + + if (!s3State.bucket) { + s3State.error = s3State.buckets.length ? "Choose a bucket to load objects." : "No S3 buckets found."; + renderS3All(); + return; + } + + let shouldRenderList = false; + + s3State.loading = true; + s3State.source = source; + s3State.error = ""; + renderS3Status(); + renderS3Fetch(); + + try { + const params = new URLSearchParams({ + bucket: s3State.bucket + }); + + if (s3State.prefix) { + params.set("prefix", s3State.prefix); + } + + const response = await fetch(`/api/s3/objects?${params.toString()}`, { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + const objects = Array.isArray(payload.objects) ? payload.objects : []; + const nextSignature = computeS3ListSignature(objects); + shouldRenderList = nextSignature !== s3State.listSignature; + s3State.objects = objects; + s3State.duration = payload.fetchDurationMs || 0; + s3State.newest = payload.latestTimestamp || ""; + s3State.updatedAt = Date.now(); + s3State.listSignature = nextSignature; + + pruneS3State(); + applyS3Filter(shouldRenderList); + appState.s3Ready = true; + setS3Status(`Updated ${objects.length} object${objects.length === 1 ? "" : "s"}.`, "ok"); + } catch (error) { + s3State.error = error.message || "Unknown S3 refresh error"; + appState.s3Ready = false; + setS3Status(`Refresh failed: ${s3State.error}`, "bad"); + } finally { + s3State.loading = false; + scheduleS3Refresh(); + renderS3All({ renderList: shouldRenderList }); + } + } + + function applyLogsFilter(shouldRenderList = true) { + const search = logsState.search.trim().toLowerCase(); + logsState.filtered = !search + ? [...logsState.events] + : logsState.events.filter((event) => logHaystack(event).includes(search)); + persistLogPreferences(); + renderLogsAll({ renderList: shouldRenderList }); + } + + function applySecretsFilter(shouldRenderList = true) { + const search = secretsState.search.trim().toLowerCase(); + secretsState.filtered = !search + ? [...secretsState.items] + : secretsState.items.filter((secret) => secretHaystack(secret).includes(search)); + persistSecretPreferences(); + renderSecretsAll({ renderList: shouldRenderList }); + } + + function applyS3Filter(shouldRenderList = true) { + const search = s3State.search.trim().toLowerCase(); + s3State.filtered = !search + ? [...s3State.objects] + : s3State.objects.filter((object) => s3Haystack(object).includes(search)); + persistS3Preferences(); + renderS3All({ renderList: shouldRenderList }); + } + + function pruneState() { + const ids = new Set(state.messages.map((message) => message.id)); + state.openIds = new Set([...state.openIds].filter((id) => ids.has(id))); + state.newIds = new Set([...state.newIds].filter((id) => ids.has(id))); + + Object.keys(state.views).forEach((id) => { + if (!ids.has(id)) { + delete state.views[id]; + } + }); + + Object.keys(state.raw).forEach((id) => { + if (!ids.has(id)) { + delete state.raw[id]; + } + }); + } + + function pruneLogsState() { + const ids = new Set(logsState.events.map((event) => event.id)); + logsState.openIds = new Set([...logsState.openIds].filter((id) => ids.has(id))); + } + + function pruneSecretsState() { + const ids = new Set(secretsState.items.map((secret) => secret.id)); + secretsState.openIds = new Set([...secretsState.openIds].filter((id) => ids.has(id))); + + Object.keys(secretsState.values).forEach((id) => { + if (!ids.has(id)) { + delete secretsState.values[id]; + } + }); + + secretsState.revealedIds = new Set([...secretsState.revealedIds].filter((id) => ids.has(id))); + } + + function pruneS3State() { + const ids = new Set(s3State.objects.map((object) => object.id)); + s3State.openIds = new Set([...s3State.openIds].filter((id) => ids.has(id))); + + Object.keys(s3State.previews).forEach((id) => { + if (!ids.has(id)) { + delete s3State.previews[id]; + } + }); + } + + function applyFilter(shouldRenderList = true) { + const search = state.search.trim().toLowerCase(); + state.filtered = !search + ? [...state.messages] + : state.messages.filter((message) => haystack(message).includes(search)); + persistEmailPreferences(); + renderAll({ renderList: shouldRenderList }); + } + + function computeListSignature(messages) { + return messages + .map((message) => + [ + message.id, + message.timestampMs || 0, + message.rawSizeBytes || 0, + message.attachmentCount || 0, + message.hasHtml ? 1 : 0, + message.preview || "", + message.parseError || "" + ].join("::") + ) + .join("|"); + } + + function computeLogListSignature(events) { + return events + .map((event) => + [event.id, event.timestamp || 0, event.ingestionTime || 0, event.logStreamName || "", event.message || ""].join( + "::" + ) + ) + .join("|"); + } + + function computeSecretsListSignature(items) { + return items + .map((secret) => + [ + secret.id, + secret.name || "", + secret.arn || "", + secret.description || "", + secret.lastChangedDate || "", + secret.createdDate || "", + secret.rotationEnabled ? 1 : 0, + secret.owningService || "", + secret.primaryRegion || "", + secret.versionCount || 0, + secret.tagCount || 0 + ].join("::") + ) + .join("|"); + } + + function computeS3ListSignature(objects) { + return objects + .map((object) => + [object.id, object.key || "", object.size || 0, object.lastModified || "", object.etag || ""].join("::") + ) + .join("|"); + } + + function haystack(message) { + return [ + message.subject, + message.from, + message.to, + message.replyTo, + message.preview, + message.textContent, + message.region, + ...(message.attachments || []).flatMap((attachment) => [attachment.filename, attachment.contentType]) + ] + .filter(Boolean) + .join(" ") + .toLowerCase(); + } + + function logHaystack(event) { + return [event.logStreamName, event.message, event.preview].filter(Boolean).join(" ").toLowerCase(); + } + + function secretHaystack(secret) { + return [ + secret.name, + secret.arn, + secret.description, + secret.primaryRegion, + secret.owningService, + ...(secret.tags || []).flatMap((tag) => [tag.key, tag.value]) + ] + .filter(Boolean) + .join(" ") + .toLowerCase(); + } + + function s3Haystack(object) { + return [object.bucket, object.key, object.storageClass, object.etag].filter(Boolean).join(" ").toLowerCase(); + } + + function scheduleRefresh() { + window.clearTimeout(state.timer); + + if (appState.panel !== "emails" || !state.auto || document.hidden || state.loading) { + return; + } + + state.timer = window.setTimeout(() => refreshMessages("auto"), state.interval); + } + + function scheduleLogsRefresh() { + window.clearTimeout(logsState.timer); + + if (appState.panel !== "logs" || !logsState.auto || document.hidden || logsState.loading) { + return; + } + + logsState.timer = window.setTimeout(() => refreshLogs("auto"), logsState.interval); + } + + function scheduleSecretsRefresh() { + window.clearTimeout(secretsState.timer); + + if (appState.panel !== "secrets" || !secretsState.auto || document.hidden || secretsState.loading) { + return; + } + + secretsState.timer = window.setTimeout(() => refreshSecrets("auto"), secretsState.interval); + } + + function scheduleS3Refresh() { + window.clearTimeout(s3State.timer); + + if (appState.panel !== "s3" || !s3State.auto || document.hidden || s3State.loading) { + return; + } + + s3State.timer = window.setTimeout(() => refreshS3("auto"), s3State.interval); + } + + function renderAll(options = {}) { + const { renderList: shouldRenderList = true } = options; + renderStats(); + renderFetch(); + renderStatus(); + if (shouldRenderList) { + renderList(); + } + renderLiveClock(); + } + + function renderLogsAll(options = {}) { + const { renderList: shouldRenderList = true } = options; + renderLogsFilters(); + renderLogsStats(); + renderLogsFetch(); + renderLogsStatus(); + if (shouldRenderList) { + renderLogsList(); + } + renderLogsLiveClock(); + } + + function renderSecretsAll(options = {}) { + const { renderList: shouldRenderList = true } = options; + renderSecretsStats(); + renderSecretsFetch(); + renderSecretsStatus(); + if (shouldRenderList) { + renderSecretsList(); + } + renderSecretsLiveClock(); + } + + function renderS3All(options = {}) { + const { renderList: shouldRenderList = true } = options; + renderS3Filters(); + renderS3Stats(); + renderS3Fetch(); + renderS3Status(); + if (shouldRenderList) { + renderS3List(); + } + renderS3LiveClock(); + } + + function renderHealthStrip() { + const serviceOrder = [ + { key: "emails", panel: "emails", icon: "✉️", label: "SES Emails", shortLabel: "SES" }, + { key: "logs", panel: "logs", icon: "📜", label: "CloudWatch Logs", shortLabel: "Logs" }, + { key: "secrets", panel: "secrets", icon: "🔐", label: "Secrets Manager", shortLabel: "Secrets" }, + { key: "s3", panel: "s3", icon: "🪣", label: "S3 Explorer", shortLabel: "S3" } + ]; + + el.healthStrip.innerHTML = serviceOrder + .map((service) => { + const entry = healthState.services?.[service.key]; + const toneClass = healthState.loading && !entry ? "warn" : entry?.ok ? "ok" : entry ? "bad" : ""; + const activeClass = service.panel === appState.panel ? "active" : ""; + const className = ["healthBadge", toneClass, activeClass].filter(Boolean).join(" "); + const summary = entry?.summary || (healthState.loading ? "Checking..." : "Waiting"); + const detail = entry?.detail || (healthState.error ? healthState.error : "Not checked yet"); + const updatedMeta = healthState.updatedAt + ? `Updated ${formatRelative(healthState.updatedAt)} via ${healthState.source}` + : ""; + const titleParts = [`${service.label}: ${detail}`]; + + if (updatedMeta) { + titleParts.push(updatedMeta); + } + + return ``; + }) + .join(""); + } + + function renderStats() { + el.totalStat.textContent = String(state.messages.length); + el.visibleStat.textContent = `${state.filtered.length} visible${state.search ? " after search" : ""}`; + el.newStat.textContent = String(state.newIds.size); + el.newestStat.textContent = state.newest ? formatDateTime(state.newest) : "No messages"; + } + + function renderLogsFilters() { + const groups = logsState.groups.length + ? logsState.groups.map((group) => ``) + : ['']; + const streams = [ + '', + ...logsState.streams.map( + (stream) => `` + ) + ]; + + el.logsGroupSelect.innerHTML = groups.join(""); + el.logsStreamSelect.innerHTML = streams.join(""); + + if (logsState.group) { + el.logsGroupSelect.value = logsState.group; + } + + el.logsStreamSelect.value = logsState.stream; + el.logsWrapToggle.checked = logsState.wrapLines; + el.logsTailToggle.checked = logsState.tailNewest; + } + + function renderS3Filters() { + const bucketOptions = s3State.buckets.length + ? s3State.buckets.map( + (bucket) => `` + ) + : ['']; + + el.s3BucketSelect.innerHTML = bucketOptions.join(""); + + if (s3State.bucket) { + el.s3BucketSelect.value = s3State.bucket; + } + + el.s3PrefixInput.value = s3State.prefix; + el.s3SearchInput.value = s3State.search; + } + + function renderLogsStats() { + el.logsTotalStat.textContent = String(logsState.events.length); + el.logsVisibleStat.textContent = `${logsState.filtered.length} visible${logsState.search ? " after search" : ""}`; + el.logsStreamsStat.textContent = String(logsState.streams.length || logsState.searchedLogStreams || 0); + el.logsNewestStat.textContent = logsState.newest ? formatDateTime(logsState.newest) : "No events"; + } + + function renderSecretsStats() { + el.secretsTotalStat.textContent = String(secretsState.items.length); + el.secretsVisibleStat.textContent = `${secretsState.filtered.length} visible${secretsState.search ? " after search" : ""}`; + el.secretsLoadedStat.textContent = String( + Object.values(secretsState.values).filter((entry) => entry?.status === "loaded").length + ); + el.secretsNewestStat.textContent = secretsState.newest ? formatDateTime(secretsState.newest) : "No secrets"; + } + + function renderS3Stats() { + el.s3TotalStat.textContent = String(s3State.objects.length); + el.s3VisibleStat.textContent = `${s3State.filtered.length} visible${s3State.search ? " after search" : ""}`; + el.s3BucketsStat.textContent = String(s3State.buckets.length); + el.s3NewestStat.textContent = s3State.newest ? formatDateTime(s3State.newest) : "No objects"; + } + + function renderFetch() { + if (state.loading) { + el.fetchStat.textContent = "Refreshing..."; + el.fetchDetail.textContent = `Endpoint: ${config.endpoint}`; + return; + } + + if (state.error) { + el.fetchStat.textContent = "Needs attention"; + el.fetchDetail.textContent = state.error; + return; + } + + if (!state.updatedAt) { + el.fetchStat.textContent = "Idle"; + el.fetchDetail.textContent = `Endpoint: ${config.endpoint}`; + return; + } + + el.fetchStat.textContent = `${state.duration}ms`; + el.fetchDetail.textContent = `${state.parseErrors} parse error${state.parseErrors === 1 ? "" : "s"}. Endpoint: ${config.endpoint}`; + } + + function renderLogsFetch() { + if (logsState.loading) { + el.logsFetchStat.textContent = "Refreshing..."; + el.logsFetchDetail.textContent = `Endpoint: ${config.cloudWatchEndpoint} (${config.cloudWatchRegion})`; + return; + } + + if (logsState.error) { + el.logsFetchStat.textContent = "Needs attention"; + el.logsFetchDetail.textContent = logsState.error; + return; + } + + if (!logsState.updatedAt) { + el.logsFetchStat.textContent = "Idle"; + el.logsFetchDetail.textContent = `Endpoint: ${config.cloudWatchEndpoint} (${config.cloudWatchRegion})`; + return; + } + + el.logsFetchStat.textContent = `${logsState.duration}ms`; + el.logsFetchDetail.textContent = `${logsState.group || "No group selected"}. Endpoint: ${config.cloudWatchEndpoint}`; + } + + function renderSecretsFetch() { + if (secretsState.loading) { + el.secretsFetchStat.textContent = "Refreshing..."; + el.secretsFetchDetail.textContent = `Endpoint: ${config.secretsEndpoint} (${config.secretsRegion})`; + return; + } + + if (secretsState.error) { + el.secretsFetchStat.textContent = "Needs attention"; + el.secretsFetchDetail.textContent = secretsState.error; + return; + } + + if (!secretsState.updatedAt) { + el.secretsFetchStat.textContent = "Idle"; + el.secretsFetchDetail.textContent = `Endpoint: ${config.secretsEndpoint} (${config.secretsRegion})`; + return; + } + + el.secretsFetchStat.textContent = `${secretsState.duration}ms`; + el.secretsFetchDetail.textContent = `Endpoint: ${config.secretsEndpoint} (${config.secretsRegion})`; + } + + function renderS3Fetch() { + if (s3State.loading) { + el.s3FetchStat.textContent = "Refreshing..."; + el.s3FetchDetail.textContent = `Endpoint: ${config.s3Endpoint} (${config.s3Region})`; + return; + } + + if (s3State.error) { + el.s3FetchStat.textContent = "Needs attention"; + el.s3FetchDetail.textContent = s3State.error; + return; + } + + if (!s3State.updatedAt) { + el.s3FetchStat.textContent = "Idle"; + el.s3FetchDetail.textContent = `Endpoint: ${config.s3Endpoint} (${config.s3Region})`; + return; + } + + el.s3FetchStat.textContent = `${s3State.duration}ms`; + el.s3FetchDetail.textContent = `${s3State.bucket || "No bucket selected"}. Endpoint: ${config.s3Endpoint}`; + } + + function renderStatus() { + el.statusChip.className = "status"; + + if (state.loading) { + el.statusChip.classList.add("warn"); + el.statusChip.textContent = "Refreshing messages..."; + return; + } + + if (state.error) { + el.statusChip.classList.add("bad"); + el.statusChip.textContent = `Refresh failed: ${state.error}`; + return; + } + + if (!state.auto) { + el.statusChip.textContent = "Live refresh paused"; + return; + } + + if (document.hidden) { + el.statusChip.classList.add("warn"); + el.statusChip.textContent = "Tab hidden, live refresh paused"; + return; + } + + if (!state.updatedAt) { + el.statusChip.textContent = "Waiting for first refresh..."; + return; + } + + const seconds = Math.max(0, Math.ceil((state.updatedAt + state.interval - Date.now()) / 1000)); + el.statusChip.classList.add("ok"); + el.statusChip.textContent = `Live refresh on, next check in ${seconds}s`; + } + + function renderLogsStatus() { + el.logsStatusChip.className = "status"; + + if (logsState.loading) { + el.logsStatusChip.classList.add("warn"); + el.logsStatusChip.textContent = "Refreshing logs..."; + return; + } + + if (logsState.error) { + el.logsStatusChip.classList.add("bad"); + el.logsStatusChip.textContent = `Refresh failed: ${logsState.error}`; + return; + } + + if (!logsState.auto) { + el.logsStatusChip.textContent = "Live refresh paused"; + return; + } + + if (document.hidden) { + el.logsStatusChip.classList.add("warn"); + el.logsStatusChip.textContent = "Tab hidden, live refresh paused"; + return; + } + + if (!logsState.updatedAt) { + el.logsStatusChip.textContent = "Waiting for first refresh..."; + return; + } + + const seconds = Math.max(0, Math.ceil((logsState.updatedAt + logsState.interval - Date.now()) / 1000)); + el.logsStatusChip.classList.add("ok"); + el.logsStatusChip.textContent = `Live refresh on, next check in ${seconds}s`; + } + + function renderSecretsStatus() { + el.secretsStatusChip.className = "status"; + + if (secretsState.loading) { + el.secretsStatusChip.classList.add("warn"); + el.secretsStatusChip.textContent = "Refreshing secrets..."; + return; + } + + if (secretsState.error) { + el.secretsStatusChip.classList.add("bad"); + el.secretsStatusChip.textContent = `Refresh failed: ${secretsState.error}`; + return; + } + + if (!secretsState.auto) { + el.secretsStatusChip.textContent = "Live refresh paused"; + return; + } + + if (document.hidden) { + el.secretsStatusChip.classList.add("warn"); + el.secretsStatusChip.textContent = "Tab hidden, live refresh paused"; + return; + } + + if (!secretsState.updatedAt) { + el.secretsStatusChip.textContent = "Waiting for first refresh..."; + return; + } + + const seconds = Math.max(0, Math.ceil((secretsState.updatedAt + secretsState.interval - Date.now()) / 1000)); + el.secretsStatusChip.classList.add("ok"); + el.secretsStatusChip.textContent = `Live refresh on, next check in ${seconds}s`; + } + + function renderS3Status() { + el.s3StatusChip.className = "status"; + + if (s3State.loading) { + el.s3StatusChip.classList.add("warn"); + el.s3StatusChip.textContent = "Refreshing objects..."; + return; + } + + if (s3State.error) { + el.s3StatusChip.classList.add("bad"); + el.s3StatusChip.textContent = `Refresh failed: ${s3State.error}`; + return; + } + + if (!s3State.auto) { + el.s3StatusChip.textContent = "Live refresh paused"; + return; + } + + if (document.hidden) { + el.s3StatusChip.classList.add("warn"); + el.s3StatusChip.textContent = "Tab hidden, live refresh paused"; + return; + } + + if (!s3State.updatedAt) { + el.s3StatusChip.textContent = "Waiting for first refresh..."; + return; + } + + const seconds = Math.max(0, Math.ceil((s3State.updatedAt + s3State.interval - Date.now()) / 1000)); + el.s3StatusChip.classList.add("ok"); + el.s3StatusChip.textContent = `Live refresh on, next check in ${seconds}s`; + } + + function renderLiveClock() { + if (!state.updatedAt) { + el.updatedStat.textContent = "Not refreshed yet"; + return; + } + + el.updatedStat.textContent = `Updated ${formatRelative(state.updatedAt)} via ${state.source}`; + renderStatus(); + } + + function renderLogsLiveClock() { + if (!logsState.updatedAt) { + el.logsUpdatedStat.textContent = "Not refreshed yet"; + return; + } + + el.logsUpdatedStat.textContent = `Updated ${formatRelative(logsState.updatedAt)} via ${logsState.source}`; + renderLogsStatus(); + } + + function renderSecretsLiveClock() { + if (!secretsState.updatedAt) { + el.secretsUpdatedStat.textContent = "Not refreshed yet"; + return; + } + + el.secretsUpdatedStat.textContent = `Updated ${formatRelative(secretsState.updatedAt)} via ${secretsState.source}`; + renderSecretsStatus(); + } + + function renderS3LiveClock() { + if (!s3State.updatedAt) { + el.s3UpdatedStat.textContent = "Not refreshed yet"; + return; + } + + el.s3UpdatedStat.textContent = `Updated ${formatRelative(s3State.updatedAt)} via ${s3State.source}`; + renderS3Status(); + } + + function renderList() { + el.banner.hidden = !state.error; + el.banner.textContent = state.error ? `Refresh failed: ${state.error}` : ""; + + if (!state.filtered.length) { + el.list.innerHTML = ""; + el.empty.hidden = false; + el.empty.textContent = state.messages.length + ? "No messages match the current search." + : "No emails yet. Send one through LocalStack SES and refresh."; + updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); + return; + } + + el.empty.hidden = true; + el.list.innerHTML = state.filtered.map(renderCard).join(""); + bindCardToggles(); + syncCardExpansion(); + updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); + } + + function renderLogsList() { + el.logsBanner.hidden = !logsState.error; + el.logsBanner.textContent = logsState.error ? `Refresh failed: ${logsState.error}` : ""; + + if (!logsState.group && !logsState.groups.length) { + el.logsList.innerHTML = ""; + el.logsEmpty.hidden = false; + el.logsEmpty.textContent = "No CloudWatch log groups found in LocalStack yet."; + updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); + return; + } + + if (!logsState.filtered.length) { + el.logsList.innerHTML = ""; + el.logsEmpty.hidden = false; + el.logsEmpty.textContent = logsState.events.length + ? "No log events match the current search." + : "No log events found for the selected group, stream, and time window."; + updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); + return; + } + + el.logsEmpty.hidden = true; + el.logsList.innerHTML = logsState.filtered.map((event) => renderLogEvent(event)).join(""); + bindLogToggles(); + updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); + } + + function renderSecretsList() { + el.secretsBanner.hidden = !secretsState.error; + el.secretsBanner.textContent = secretsState.error ? `Refresh failed: ${secretsState.error}` : ""; + + if (!secretsState.filtered.length) { + el.secretsList.innerHTML = ""; + el.secretsEmpty.hidden = false; + el.secretsEmpty.textContent = secretsState.items.length + ? "No secrets match the current search." + : "No Secrets Manager entries found in LocalStack yet."; + updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); + return; + } + + el.secretsEmpty.hidden = true; + el.secretsList.innerHTML = secretsState.filtered.map((secret) => renderSecretCard(secret)).join(""); + bindSecretToggles(); + syncSecretExpansion(); + updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); + } + + function renderS3List() { + el.s3Banner.hidden = !s3State.error; + el.s3Banner.textContent = s3State.error ? `Refresh failed: ${s3State.error}` : ""; + + if (!s3State.bucket && !s3State.buckets.length) { + el.s3List.innerHTML = ""; + el.s3Empty.hidden = false; + el.s3Empty.textContent = "No S3 buckets found in LocalStack yet."; + updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); + return; + } + + if (!s3State.filtered.length) { + el.s3List.innerHTML = ""; + el.s3Empty.hidden = false; + el.s3Empty.textContent = s3State.objects.length + ? "No S3 objects match the current search." + : "No S3 objects found for the selected bucket and prefix."; + updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); + return; + } + + el.s3Empty.hidden = true; + el.s3List.innerHTML = s3State.filtered.map((object) => renderS3ObjectCard(object)).join(""); + bindS3Toggles(); + syncS3Expansion(); + updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton); + } + + function renderLogEvent(event) { + const level = detectLogLevel(event); + const levelTag = level ? `${escapeHtml(level.label)}` : ""; + + return ` +
+ +
+
+ ${escapeHtml(formatDateTime(event.timestamp))} + ${escapeHtml(event.logStreamName || "Unknown stream")} + ${levelTag} +
+
+ + ${escapeHtml(formatRelative(event.timestamp || Date.now()))} +
+
+

${renderLogPreviewContent(event)}

+
+
+
${renderLogBodyContent(event.message)}
+
+
+ `; + } + + function renderSecretCard(secret) { + const valueState = secretsState.values[secret.id]; + const tags = []; + + if (secret.owningService) { + tags.push(`${escapeHtml(secret.owningService)}`); + } + + if (secret.primaryRegion) { + tags.push(`${escapeHtml(secret.primaryRegion)}`); + } + + if (secret.rotationEnabled) { + tags.push('Rotation on'); + } else { + tags.push('Rotation off'); + } + + if (secret.tagCount) { + tags.push(`${secret.tagCount} tag${secret.tagCount === 1 ? "" : "s"}`); + } + + if (valueState?.status === "loaded") { + tags.push(`${escapeHtml(valueState.label)}`); + } + + if (valueState?.status === "error") { + tags.push('Value load failed'); + } + + return ` +
+ +
+
+

🔐 ${escapeHtml(secret.name)}

+

${escapeHtml(secret.description || secret.arn || "No description")}

+
+ ${escapeHtml(formatDateTime(secret.lastChangedDate || secret.createdDate))} +
+
${tags.join("")}
+

${escapeHtml(buildSecretPreview(secret))}

+
+
+
+ ${metaCard("Name", secret.name)} + ${metaCard("ARN", secret.arn || "Not available")} + ${metaCard("Description", secret.description || "None")} + ${metaCard("Last changed", formatDateTime(secret.lastChangedDate || secret.createdDate))} + ${metaCard("Created", formatDateTime(secret.createdDate))} + ${metaCard("Last accessed", formatDateTime(secret.lastAccessedDate))} + ${metaCard("Primary region", secret.primaryRegion || "Not set")} + ${metaCard("Owning service", secret.owningService || "Not set")} +
+ +
+ ${renderSecretValuePanel(secret, valueState)} +
+
+
+ `; + } + + function renderS3ObjectCard(object) { + const previewState = s3State.previews[object.id]; + const tags = [ + `${escapeHtml(object.bucket)}`, + `${escapeHtml(formatBytes(object.size))}` + ]; + + if (object.storageClass) { + tags.push(`${escapeHtml(object.storageClass)}`); + } + + if (previewState?.status === "loaded") { + tags.push(`${escapeHtml(previewState.previewType)}`); + } + + if (previewState?.status === "error") { + tags.push('Preview failed'); + } + + return ` +
+ +
+
+

🪣 ${escapeHtml(object.key)}

+

${escapeHtml(object.bucket)} • ${escapeHtml(formatBytes(object.size))}

+
+ ${escapeHtml(formatDateTime(object.lastModified))} +
+
${tags.join("")}
+

${escapeHtml(buildS3Preview(object))}

+
+
+
+
+ + + ⬇️ Download +
+
+ +
+ ${metaCard("Bucket", object.bucket)} + ${metaCard("Key", object.key)} + ${metaCard("Size", formatBytes(object.size))} + ${metaCard("Modified", formatDateTime(object.lastModified))} + ${metaCard("Storage class", object.storageClass || "STANDARD")} + ${metaCard("ETag", object.etag || "Not available")} +
+ +
${renderS3PreviewPanel(object, previewState)}
+
+
+ `; + } + + function bindLogToggles() { + el.logsList.querySelectorAll(".logEvent").forEach((details) => { + details.addEventListener("toggle", () => { + const id = details.dataset.id; + + if (!id) { + return; + } + + if (details.open) { + logsState.openIds.add(id); + } else { + logsState.openIds.delete(id); + } + }); + }); + } + + function bindSecretToggles() { + el.secretsList.querySelectorAll(".secretCard").forEach((details) => { + details.addEventListener("toggle", () => { + const id = details.dataset.id; + + if (!id) { + return; + } + + if (details.open) { + secretsState.openIds.add(id); + ensureSecretValue(id, { force: false }); + } else { + secretsState.openIds.delete(id); + } + }); + }); + } + + function bindS3Toggles() { + el.s3List.querySelectorAll(".s3Card").forEach((details) => { + details.addEventListener("toggle", () => { + const id = details.dataset.id; + + if (!id) { + return; + } + + if (details.open) { + s3State.openIds.add(id); + ensureS3Preview(id, { force: false }); + } else { + s3State.openIds.delete(id); + } + }); + }); + } + + function bindCardToggles() { + el.list.querySelectorAll(".card").forEach((details) => { + details.addEventListener("toggle", () => { + const id = details.dataset.id; + + if (!id) { + return; + } + + if (details.open) { + state.openIds.add(id); + } else { + state.openIds.delete(id); + } + + hydrate(details, getMessage(id)); + }); + }); + } + + function renderCard(message) { + const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text"); + const tags = []; + + if (state.newIds.has(message.id)) { + tags.push('New'); + } + + if (message.attachmentCount) { + tags.push( + `${message.attachmentCount} attachment${message.attachmentCount === 1 ? "" : "s"}` + ); + } + + tags.push(`${message.hasHtml ? "HTML" : "Text only"}`); + + if (message.parseError) { + tags.push('Parse issue'); + } + + return ` +
+ +
+
+

${escapeHtml(message.subject)}

+

${escapeHtml(message.from)} to ${escapeHtml(message.to)}

+
+ ${escapeHtml(formatDateTime(message.timestamp))} +
+
${tags.join("")}
+

${escapeHtml(message.preview)}

+
+
+
+
+ ${ + message.hasHtml + ? `` + : "" + } + + +
+
+ +
+
+ +
+ ${metaCard("From", message.from)} + ${metaCard("To", message.to)} + ${metaCard("Reply-To", message.replyTo || "None")} + ${metaCard("Sent", formatDateTime(message.timestamp))} + ${metaCard("Region", message.region || "Unknown region")} + ${metaCard("LocalStack Id", message.id)} + ${metaCard("Message-Id", message.messageId || "Not available")} + ${metaCard("Raw size", formatBytes(message.rawSizeBytes))} + ${message.parseError ? metaCard("Parse error", message.parseError) : ""} +
+ + ${ + message.attachments?.length + ? `
${message.attachments + .map((attachment) => { + const size = attachment.size ? `, ${formatBytes(attachment.size)}` : ""; + const icon = resolveAttachmentIcon(attachment); + return `${icon} ${escapeHtml(attachment.filename)} (${escapeHtml(attachment.contentType)}${escapeHtml(size)})`; + }) + .join("")}
` + : "" + } + +
${renderPanel(message, view)}
+
+
+ `; + } + + function renderPanel(message, view) { + if (view === "rendered" && message.hasHtml) { + return ``; + } + + if (view === "raw") { + const raw = state.raw[message.id]; + + if (!raw) { + return `
Raw MIME source is loaded on demand.
`; + } + + if (raw.status === "loading") { + return '
Loading raw source...
'; + } + + if (raw.status === "error") { + return `
Unable to load raw source: ${escapeHtml(raw.error)}
`; + } + + return `
${escapeHtml(raw.value)}
`; + } + + return `
${escapeHtml(message.textContent || "No plain-text content available for this message.")}
`; + } + + function renderSecretValuePanel(secret, valueState) { + if (!valueState) { + return `
Secret values are loaded on demand.
`; + } + + if (valueState.status === "loading") { + return '
Loading secret value...
'; + } + + if (valueState.status === "error") { + return `
Unable to load secret value: ${escapeHtml(valueState.error)}
`; + } + + const revealed = secretsState.revealedIds.has(secret.id); + const maskedHtml = escapeHtml(maskSecretValue(valueState.copyValue)); + + return ` +
+
+ ${escapeHtml(valueState.label)} + ${ + valueState.versionId + ? `Version ${escapeHtml(valueState.versionId.slice(0, 8))}` + : "" + } + ${ + valueState.versionStages.length + ? `${escapeHtml(valueState.versionStages.join(", "))}` + : "" + } +
+
+ + + + + + +
+
+
${revealed ? valueState.displayHtml : maskedHtml}
+ `; + } + + function renderS3PreviewPanel(object, previewState) { + if (!previewState) { + return `
Object previews load on demand.
`; + } + + if (previewState.status === "loading") { + return '
Loading object preview...
'; + } + + if (previewState.status === "error") { + return `
Unable to load object preview: ${escapeHtml(previewState.error)}
`; + } + + const truncatedTag = previewState.truncated ? 'Preview truncated' : ""; + let previewContent = `
No inline preview available for this object type.
`; + + if (previewState.previewType === "image" && previewState.imageDataUrl) { + previewContent = `${escapeHtml(object.key)}`; + } else if (previewState.previewType === "json") { + previewContent = `
${highlightJsonText(prettyJsonOrText(previewState.previewText))}
`; + } else if (previewState.previewType === "text" || previewState.previewType === "html") { + previewContent = `
${escapeHtml(previewState.previewText || "No preview text available.")}
`; + } + + return ` +
+
+ ${escapeHtml(previewState.previewType)} + ${truncatedTag} + ${previewState.contentType ? `${escapeHtml(previewState.contentType)}` : ""} +
+
+ + ⬇️ Download +
+
+ ${previewContent} + `; + } + + function metaCard(label, value) { + return `
${escapeHtml(label)}
${escapeHtml(value)}
`; + } + + function syncCardExpansion() { + const applyCardState = () => { + el.list.querySelectorAll(".card").forEach((details) => { + const id = details.dataset.id; + const shouldOpen = Boolean(id && state.openIds.has(id)); + + if (shouldOpen && !details.open) { + details.open = true; + } + + if (!shouldOpen && details.open) { + details.open = false; + return; + } + + if (shouldOpen) { + hydrate(details, getMessage(id)); + } + }); + }; + + applyCardState(); + window.requestAnimationFrame(applyCardState); + } + + function syncSecretExpansion() { + const applySecretState = () => { + el.secretsList.querySelectorAll(".secretCard").forEach((details) => { + const id = details.dataset.id; + const shouldOpen = Boolean(id && secretsState.openIds.has(id)); + + if (shouldOpen && !details.open) { + details.open = true; + } + + if (!shouldOpen && details.open) { + details.open = false; + return; + } + + if (shouldOpen) { + ensureSecretValue(id, { force: false }); + } + }); + }; + + applySecretState(); + window.requestAnimationFrame(applySecretState); + } + + function syncS3Expansion() { + const applyS3State = () => { + el.s3List.querySelectorAll(".s3Card").forEach((details) => { + const id = details.dataset.id; + const shouldOpen = Boolean(id && s3State.openIds.has(id)); + + if (shouldOpen && !details.open) { + details.open = true; + } + + if (!shouldOpen && details.open) { + details.open = false; + return; + } + + if (shouldOpen) { + ensureS3Preview(id, { force: false }); + } + }); + }; + + applyS3State(); + window.requestAnimationFrame(applyS3State); + } + + function resolveAttachmentIcon(attachment) { + const filename = String(attachment?.filename || "").toLowerCase(); + const contentType = String(attachment?.contentType || "").toLowerCase(); + + if (filename.endsWith(".pdf") || contentType.includes("pdf")) { + return "📄"; + } + + if ( + [".doc", ".docx", ".txt", ".rtf", ".md"].some((extension) => filename.endsWith(extension)) || + contentType.includes("word") || + contentType.startsWith("text/") + ) { + return "📝"; + } + + if ( + [".xls", ".xlsx", ".csv"].some((extension) => filename.endsWith(extension)) || + contentType.includes("sheet") || + contentType.includes("csv") + ) { + return "📊"; + } + + if ( + filename.endsWith(".json") || + filename.endsWith(".xml") || + filename.endsWith(".yaml") || + filename.endsWith(".yml") + ) { + return "🧾"; + } + + if (contentType.startsWith("image/")) { + return "🖼️"; + } + + if (contentType.startsWith("audio/")) { + return "🎵"; + } + + if (contentType.startsWith("video/")) { + return "🎬"; + } + + if ( + [".zip", ".rar", ".7z", ".tar", ".gz"].some((extension) => filename.endsWith(extension)) || + contentType.includes("zip") || + contentType.includes("compressed") + ) { + return "🗜️"; + } + + return "📎"; + } + + function hydrate(details, message) { + if (!details || !details.open || !message) { + return; + } + + const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text"); + + if (view !== "rendered" || !message.hasHtml) { + return; + } + + const iframe = details.querySelector("[data-frame]"); + + if (iframe) { + iframe.referrerPolicy = "no-referrer"; + iframe.sandbox = ""; + iframe.srcdoc = message.renderedHtml || ""; + } + } + + function getMessage(id) { + return state.messages.find((message) => message.id === id); + } + + function getLogEvent(id) { + return logsState.events.find((event) => event.id === id); + } + + function getSecret(id) { + return secretsState.items.find((secret) => secret.id === id); + } + + function getS3Object(id) { + return s3State.objects.find((object) => object.id === id); + } + + async function loadRaw(id) { + if (state.raw[id]?.status === "loaded") { + return state.raw[id].value; + } + + if (state.raw[id]?.status === "loading") { + return null; + } + + state.raw[id] = { status: "loading" }; + renderList(); + + try { + const response = await fetch(`/api/messages/${encodeURIComponent(id)}/raw`, { cache: "no-store" }); + + if (!response.ok) { + throw new Error((await response.text()) || `Request failed with ${response.status}`); + } + + const value = await response.text(); + state.raw[id] = { status: "loaded", value }; + return value; + } catch (error) { + state.raw[id] = { status: "error", error: error.message || "Unknown raw load error" }; + setStatus("Could not load the raw message source.", "bad"); + return null; + } finally { + renderList(); + } + } + + async function ensureSecretValue(id, options = {}) { + const { force = false } = options; + + if (!id) { + return null; + } + + if (!force && secretsState.values[id]?.status === "loaded") { + return secretsState.values[id]; + } + + if (secretsState.values[id]?.status === "loading") { + return null; + } + + secretsState.values[id] = { status: "loading" }; + renderSecretsAll(); + + try { + const response = await fetch(`/api/secrets/value?id=${encodeURIComponent(id)}`, { cache: "no-store" }); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + const secretString = typeof payload.secretString === "string" ? payload.secretString : ""; + const secretBinary = typeof payload.secretBinary === "string" ? payload.secretBinary : ""; + const parsedString = secretString ? tryParseJsonText(secretString) : { ok: false, value: null }; + const entry = { + status: "loaded", + label: secretBinary ? "Binary" : parsedString.ok ? "JSON" : secretString ? "Text" : "Empty", + copyValue: secretBinary + ? secretBinary + : parsedString.ok + ? JSON.stringify(parsedString.value, null, 2) + : secretString || "No secret value.", + displayHtml: secretBinary + ? escapeHtml(secretBinary) + : parsedString.ok + ? highlightJsonText(JSON.stringify(parsedString.value, null, 2)) + : escapeHtml(secretString || "No secret value."), + isJson: parsedString.ok, + isBinary: Boolean(secretBinary), + versionId: payload.versionId || "", + versionStages: Array.isArray(payload.versionStages) ? payload.versionStages : [], + createdDate: payload.createdDate || "", + arn: payload.arn || "", + name: payload.name || "" + }; + + secretsState.values[id] = entry; + return entry; + } catch (error) { + secretsState.values[id] = { + status: "error", + error: error.message || "Unknown secret value error" + }; + setSecretsStatus("Could not load the secret value.", "bad"); + return null; + } finally { + renderSecretsAll(); + } + } + + async function ensureS3Preview(id, options = {}) { + const { force = false } = options; + + if (!id) { + return null; + } + + if (!force && s3State.previews[id]?.status === "loaded") { + return s3State.previews[id]; + } + + if (s3State.previews[id]?.status === "loading") { + return null; + } + + const object = getS3Object(id); + + if (!object) { + return null; + } + + s3State.previews[id] = { status: "loading" }; + renderS3All(); + + try { + const response = await fetch( + `/api/s3/object?bucket=${encodeURIComponent(object.bucket)}&key=${encodeURIComponent(object.key)}`, + { cache: "no-store" } + ); + + if (!response.ok) { + const payload = await safeJson(response); + throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); + } + + const payload = await response.json(); + const entry = { + status: "loaded", + previewType: payload.previewType || "binary", + previewText: payload.previewText || "", + imageDataUrl: payload.imageDataUrl || "", + contentType: payload.contentType || "", + contentLength: payload.contentLength || 0, + truncated: Boolean(payload.truncated), + metadata: payload.metadata || {} + }; + + s3State.previews[id] = entry; + return entry; + } catch (error) { + s3State.previews[id] = { + status: "error", + error: error.message || "Unknown S3 preview error" + }; + setS3Status("Could not load the S3 object preview.", "bad"); + return null; + } finally { + renderS3All(); + } + } + + async function copyText(value) { + try { + await navigator.clipboard.writeText(value); + } catch { + const input = document.createElement("textarea"); + input.value = value; + input.setAttribute("readonly", ""); + input.style.position = "fixed"; + input.style.opacity = "0"; + document.body.appendChild(input); + input.select(); + document.execCommand("copy"); + document.body.removeChild(input); + } + } + + function setStatus(message, tone) { + el.statusChip.className = "status"; + + if (tone) { + el.statusChip.classList.add(tone); + } + + el.statusChip.textContent = message; + } + + function setLogsStatus(message, tone) { + el.logsStatusChip.className = "status"; + + if (tone) { + el.logsStatusChip.classList.add(tone); + } + + el.logsStatusChip.textContent = message; + } + + function setSecretsStatus(message, tone) { + el.secretsStatusChip.className = "status"; + + if (tone) { + el.secretsStatusChip.classList.add(tone); + } + + el.secretsStatusChip.textContent = message; + } + + function setS3Status(message, tone) { + el.s3StatusChip.className = "status"; + + if (tone) { + el.s3StatusChip.classList.add(tone); + } + + el.s3StatusChip.textContent = message; + } + + function getInitialPanel() { + const storedPanel = readStoredValue(PANEL_STORAGE_KEY); + return ["emails", "logs", "secrets", "s3"].includes(storedPanel) ? storedPanel : "emails"; + } + + function getInitialTheme() { + const storedTheme = readStoredValue(THEME_STORAGE_KEY); + + if (storedTheme === "dark" || storedTheme === "light") { + return storedTheme; + } + + return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light"; + } + + function applyTheme(theme) { + const nextTheme = theme === "dark" ? "dark" : "light"; + appState.theme = nextTheme; + document.body.dataset.theme = nextTheme; + + try { + window.localStorage.setItem(THEME_STORAGE_KEY, nextTheme); + } catch {} + + renderThemeToggle(); + } + + function renderThemeToggle() { + if (!el.themeToggle) { + return; + } + + const isDark = appState.theme === "dark"; + el.themeToggle.textContent = isDark ? "🌙 Dark theme" : "☀️ Light theme"; + el.themeToggle.setAttribute("aria-pressed", isDark ? "true" : "false"); + el.themeToggle.setAttribute("aria-label", isDark ? "Switch to light theme" : "Switch to dark theme"); + el.themeToggle.title = isDark ? "Switch to light theme" : "Switch to dark theme"; + } + + function persistPanel() { + writeStoredValue(PANEL_STORAGE_KEY, appState.panel); + } + + function persistEmailPreferences() { + writeStoredJson(EMAIL_PREFERENCES_STORAGE_KEY, { + search: state.search, + auto: state.auto, + interval: state.interval + }); + } + + function persistLogPreferences() { + writeStoredJson(LOG_PREFERENCES_STORAGE_KEY, { + group: logsState.group, + stream: logsState.stream, + search: logsState.search, + auto: logsState.auto, + interval: logsState.interval, + windowMs: logsState.windowMs, + limit: logsState.limit, + wrapLines: logsState.wrapLines, + tailNewest: logsState.tailNewest + }); + } + + function persistSecretPreferences() { + writeStoredJson(SECRET_PREFERENCES_STORAGE_KEY, { + search: secretsState.search, + auto: secretsState.auto, + interval: secretsState.interval + }); + } + + function persistS3Preferences() { + writeStoredJson(S3_PREFERENCES_STORAGE_KEY, { + bucket: s3State.bucket, + prefix: s3State.prefix, + search: s3State.search, + auto: s3State.auto, + interval: s3State.interval + }); + } + + function resetSavedState() { + [ + THEME_STORAGE_KEY, + PANEL_STORAGE_KEY, + EMAIL_PREFERENCES_STORAGE_KEY, + LOG_PREFERENCES_STORAGE_KEY, + SECRET_PREFERENCES_STORAGE_KEY, + S3_PREFERENCES_STORAGE_KEY + ].forEach((key) => { + try { + window.localStorage.removeItem(key); + } catch {} + }); + + window.location.reload(); + } + + function getStoredPreferences(key) { + try { + const rawValue = window.localStorage.getItem(key); + + if (!rawValue) { + return null; + } + + const parsedValue = JSON.parse(rawValue); + return parsedValue && typeof parsedValue === "object" && !Array.isArray(parsedValue) ? parsedValue : null; + } catch { + return null; + } + } + + function readStoredValue(key) { + try { + return window.localStorage.getItem(key); + } catch { + return null; + } + } + + function writeStoredValue(key, value) { + try { + window.localStorage.setItem(key, String(value)); + } catch {} + } + + function writeStoredJson(key, value) { + try { + window.localStorage.setItem(key, JSON.stringify(value)); + } catch {} + } + + function getStoredText(value, fallback = "") { + return typeof value === "string" ? value : fallback; + } + + function getStoredBoolean(value, fallback) { + return typeof value === "boolean" ? value : fallback; + } + + function getStoredNumber(value, allowedValues, fallback) { + const normalizedValue = Number(value); + return Number.isFinite(normalizedValue) && allowedValues.includes(normalizedValue) ? normalizedValue : fallback; + } + + function scrollPaneToTop(element) { + if (!element) { + return; + } + + element.scrollTo({ top: 0, behavior: "smooth" }); + } + + function updatePaneTopButtonVisibility(pane, button) { + if (!pane || !button) { + return; + } + + button.classList.toggle("visible", pane.scrollTop > 140); + } + + function formatDateTime(value) { + if (!value) { + return "Unknown time"; + } + + const date = new Date(value); + return Number.isNaN(date.getTime()) + ? "Unknown time" + : new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date); + } + + function formatRelative(timestampMs) { + const seconds = Math.max(1, Math.round((Date.now() - timestampMs) / 1000)); + + if (seconds < 60) { + return `${seconds}s ago`; + } + + const minutes = Math.round(seconds / 60); + + if (minutes < 60) { + return `${minutes}m ago`; + } + + const hours = Math.round(minutes / 60); + + if (hours < 24) { + return `${hours}h ago`; + } + + return `${Math.round(hours / 24)}d ago`; + } + + function formatBytes(value) { + if (!value) { + return "0 B"; + } + + const units = ["B", "KB", "MB", "GB"]; + let size = value; + let index = 0; + + while (size >= 1024 && index < units.length - 1) { + size /= 1024; + index += 1; + } + + return `${index === 0 ? size : size.toFixed(1)} ${units[index]}`; + } + + function formatLogMessage(message) { + const value = String(message || "").trim(); + + if (!value) { + return "No log payload."; + } + + try { + return JSON.stringify(JSON.parse(value), null, 2); + } catch { + return value; + } + } + + function buildSecretPreview(secret) { + const fragments = []; + + if (secret.description) { + fragments.push(secret.description); + } + + if (secret.owningService) { + fragments.push(`Service: ${secret.owningService}`); + } + + if (secret.primaryRegion) { + fragments.push(`Region: ${secret.primaryRegion}`); + } + + if (secret.tagCount) { + fragments.push(`${secret.tagCount} tag${secret.tagCount === 1 ? "" : "s"}`); + } + + if (!fragments.length) { + fragments.push("No description or tags yet."); + } + + return fragments.join(" • "); + } + + function toggleSecretReveal(id) { + if (secretsState.revealedIds.has(id)) { + secretsState.revealedIds.delete(id); + } else { + secretsState.revealedIds.add(id); + } + + renderSecretsAll(); + } + + function maskSecretValue(value) { + const source = String(value || ""); + + if (!source) { + return "Secret is loaded but empty."; + } + + const lines = source.split("\n"); + return lines.map((line) => "•".repeat(Math.max(12, Math.min(line.length || 0, 48)))).join("\n"); + } + + function buildS3Preview(object) { + const fragments = []; + + if (object.storageClass) { + fragments.push(object.storageClass); + } + + fragments.push(formatBytes(object.size)); + + if (object.etag) { + fragments.push(`ETag ${object.etag.slice(0, 12)}`); + } + + return fragments.join(" • "); + } + + function prettyJsonOrText(value) { + const parsed = tryParseJsonText(value); + return parsed.ok ? JSON.stringify(parsed.value, null, 2) : String(value || ""); + } + + function detectLogLevel(event) { + const parsed = tryParseJsonText(event?.message); + const candidates = parsed.ok + ? [parsed.value?.level, parsed.value?.severity, parsed.value?.logLevel, parsed.value?.status, parsed.value?.lvl] + : [String(event?.message || "").match(/\b(error|warn|warning|info|debug|trace|fatal)\b/i)?.[0] || ""]; + const normalized = String(candidates.find(Boolean) || "").toLowerCase(); + + if (["fatal", "error", "critical"].includes(normalized)) { + return { label: normalized.toUpperCase(), className: "levelError" }; + } + + if (["warn", "warning"].includes(normalized)) { + return { label: "WARN", className: "levelWarn" }; + } + + if (["info", "notice"].includes(normalized)) { + return { label: normalized.toUpperCase(), className: "levelInfo" }; + } + + if (["debug", "trace"].includes(normalized)) { + return { label: normalized.toUpperCase(), className: "levelDebug" }; + } + + return null; + } + + function renderLogPreviewContent(event) { + const parsedLog = tryParseJsonText(event?.message); + + if (!parsedLog.ok) { + return escapeHtml(event?.preview || "No preview available."); + } + + const compactJson = JSON.stringify(parsedLog.value); + const previewText = compactJson.length > 220 ? `${compactJson.slice(0, 217)}...` : compactJson; + return highlightJsonText(previewText); + } + + function renderLogBodyContent(message) { + const parsedLog = tryParseJsonText(message); + + if (!parsedLog.ok) { + return escapeHtml(formatLogMessage(message)); + } + + return highlightJsonText(JSON.stringify(parsedLog.value, null, 2)); + } + + function tryParseJsonText(message) { + const value = String(message || "").trim(); + + if (!value) { + return { ok: false, value: null }; + } + + try { + return { ok: true, value: JSON.parse(value) }; + } catch { + return { ok: false, value: null }; + } + } + + function highlightJsonText(value) { + const source = String(value ?? ""); + const tokenRegex = + /("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?)/g; + let html = ""; + let lastIndex = 0; + + for (const match of source.matchAll(tokenRegex)) { + const [token] = match; + const index = match.index ?? 0; + let className = "jsonNumber"; + + html += escapeHtml(source.slice(lastIndex, index)); + + if (token.endsWith(":")) { + className = "jsonKey"; + } else if (token === "true" || token === "false") { + className = "jsonBoolean"; + } else if (token === "null") { + className = "jsonNull"; + } else if (token.startsWith('"')) { + className = "jsonString"; + } + + html += `${escapeHtml(token)}`; + lastIndex = index + token.length; + } + + html += escapeHtml(source.slice(lastIndex)); + return html; + } + + function escapeHtml(value) { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + async function safeJson(response) { + try { + return await response.json(); + } catch { + return null; + } + } +} diff --git a/_reference/localEmailViewer/server/config.js b/_reference/localEmailViewer/server/config.js new file mode 100644 index 000000000..c5d4ed154 --- /dev/null +++ b/_reference/localEmailViewer/server/config.js @@ -0,0 +1,45 @@ +import { CloudWatchLogsClient } from "@aws-sdk/client-cloudwatch-logs"; +import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; +import { S3Client } from "@aws-sdk/client-s3"; + +export const PORT = Number(process.env.PORT || 3334); +export const SES_ENDPOINT = process.env.SES_VIEWER_ENDPOINT || "http://localhost:4566/_aws/ses"; +export const FETCH_TIMEOUT_MS = Number(process.env.SES_VIEWER_FETCH_TIMEOUT_MS || 5000); +export const DEFAULT_REFRESH_MS = Number(process.env.SES_VIEWER_REFRESH_MS || 10000); +export const CLOUDWATCH_ENDPOINT = process.env.CLOUDWATCH_VIEWER_ENDPOINT || "http://localhost:4566"; +export const CLOUDWATCH_REGION = + process.env.CLOUDWATCH_VIEWER_REGION || process.env.AWS_DEFAULT_REGION || "ca-central-1"; +export const CLOUDWATCH_DEFAULT_GROUP = process.env.CLOUDWATCH_VIEWER_LOG_GROUP || "development"; +export const CLOUDWATCH_DEFAULT_WINDOW_MS = Number(process.env.CLOUDWATCH_VIEWER_WINDOW_MS || 15 * 60 * 1000); +export const CLOUDWATCH_DEFAULT_LIMIT = Number(process.env.CLOUDWATCH_VIEWER_LIMIT || 200); +export const SECRETS_ENDPOINT = process.env.SECRETS_VIEWER_ENDPOINT || CLOUDWATCH_ENDPOINT; +export const SECRETS_REGION = process.env.SECRETS_VIEWER_REGION || CLOUDWATCH_REGION; +export const S3_ENDPOINT = process.env.S3_VIEWER_ENDPOINT || CLOUDWATCH_ENDPOINT; +export const S3_REGION = process.env.S3_VIEWER_REGION || CLOUDWATCH_REGION; +export const S3_DEFAULT_BUCKET = process.env.S3_VIEWER_BUCKET || ""; +export const S3_PREVIEW_MAX_BYTES = Number(process.env.S3_VIEWER_PREVIEW_BYTES || 256 * 1024); +export const S3_IMAGE_PREVIEW_MAX_BYTES = Number(process.env.S3_VIEWER_IMAGE_PREVIEW_BYTES || 1024 * 1024); + +export const LOCALSTACK_CREDENTIALS = { + accessKeyId: process.env.AWS_ACCESS_KEY_ID || "test", + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "test" +}; + +export const cloudWatchLogsClient = new CloudWatchLogsClient({ + region: CLOUDWATCH_REGION, + endpoint: CLOUDWATCH_ENDPOINT, + credentials: LOCALSTACK_CREDENTIALS +}); + +export const secretsManagerClient = new SecretsManagerClient({ + region: SECRETS_REGION, + endpoint: SECRETS_ENDPOINT, + credentials: LOCALSTACK_CREDENTIALS +}); + +export const s3Client = new S3Client({ + region: S3_REGION, + endpoint: S3_ENDPOINT, + credentials: LOCALSTACK_CREDENTIALS, + forcePathStyle: true +}); diff --git a/_reference/localEmailViewer/server/localstack-service.js b/_reference/localEmailViewer/server/localstack-service.js new file mode 100644 index 000000000..ad2b7473f --- /dev/null +++ b/_reference/localEmailViewer/server/localstack-service.js @@ -0,0 +1,845 @@ +import fetch from "node-fetch"; +import { + DescribeLogGroupsCommand, + DescribeLogStreamsCommand, + FilterLogEventsCommand +} from "@aws-sdk/client-cloudwatch-logs"; +import { GetSecretValueCommand, ListSecretsCommand } from "@aws-sdk/client-secrets-manager"; +import { GetObjectCommand, HeadObjectCommand, ListBucketsCommand, ListObjectsV2Command } from "@aws-sdk/client-s3"; +import { simpleParser } from "mailparser"; +import { + CLOUDWATCH_ENDPOINT, + CLOUDWATCH_REGION, + FETCH_TIMEOUT_MS, + S3_ENDPOINT, + S3_IMAGE_PREVIEW_MAX_BYTES, + S3_PREVIEW_MAX_BYTES, + S3_REGION, + SES_ENDPOINT, + SECRETS_ENDPOINT, + SECRETS_REGION, + cloudWatchLogsClient, + s3Client, + secretsManagerClient +} from "./config.js"; + +async function loadMessages() { + const startedAt = Date.now(); + const sesMessages = await fetchSesMessages(); + const messages = await Promise.all(sesMessages.map((message, index) => toMessageViewModel(message, index))); + + messages.sort((left, right) => { + if ((right.timestampMs || 0) !== (left.timestampMs || 0)) { + return (right.timestampMs || 0) - (left.timestampMs || 0); + } + + return right.index - left.index; + }); + + return { + endpoint: SES_ENDPOINT, + fetchedAt: new Date().toISOString(), + fetchDurationMs: Date.now() - startedAt, + totalMessages: messages.length, + parseErrors: messages.filter((message) => Boolean(message.parseError)).length, + latestMessageTimestamp: messages[0]?.timestamp || "", + messages + }; +} + +async function fetchSesMessages() { + const response = await fetch(SES_ENDPOINT, { + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) + }); + + if (!response.ok) { + throw new Error(`SES endpoint responded with ${response.status}`); + } + + const data = await response.json(); + return Array.isArray(data.messages) ? data.messages : []; +} + +async function loadLogGroups() { + const groups = []; + let nextToken; + let pageCount = 0; + + do { + const response = await cloudWatchLogsClient.send( + new DescribeLogGroupsCommand({ + nextToken, + limit: 50 + }) + ); + + groups.push( + ...(response.logGroups || []).map((group) => ({ + name: group.logGroupName || "", + arn: group.arn || "", + storedBytes: group.storedBytes || 0, + retentionInDays: group.retentionInDays || 0, + creationTime: group.creationTime || 0 + })) + ); + + nextToken = response.nextToken; + pageCount += 1; + } while (nextToken && pageCount < 10); + + return groups.sort((left, right) => left.name.localeCompare(right.name)); +} + +async function loadLogStreams(logGroupName) { + const streams = []; + let nextToken; + let pageCount = 0; + + do { + const response = await cloudWatchLogsClient.send( + new DescribeLogStreamsCommand({ + logGroupName, + descending: true, + orderBy: "LastEventTime", + nextToken, + limit: 50 + }) + ); + + streams.push( + ...(response.logStreams || []).map((stream) => ({ + name: stream.logStreamName || "", + arn: stream.arn || "", + lastEventTimestamp: stream.lastEventTimestamp || 0, + lastIngestionTime: stream.lastIngestionTime || 0, + storedBytes: stream.storedBytes || 0 + })) + ); + + nextToken = response.nextToken; + pageCount += 1; + } while (nextToken && pageCount < 6 && streams.length < 250); + + return streams; +} + +async function loadLogEvents({ logGroupName, logStreamName, windowMs, limit }) { + const startedAt = Date.now(); + const eventMap = new Map(); + const startTime = Date.now() - windowMs; + let nextToken; + let previousToken = ""; + let pageCount = 0; + let searchedLogStreams = 0; + + do { + const response = await cloudWatchLogsClient.send( + new FilterLogEventsCommand({ + logGroupName, + logStreamNames: logStreamName ? [logStreamName] : undefined, + startTime, + endTime: Date.now(), + limit, + nextToken + }) + ); + + for (const event of response.events || []) { + const id = + event.eventId || `${event.logStreamName || "stream"}-${event.timestamp || 0}-${event.ingestionTime || 0}`; + + if (!eventMap.has(id)) { + const message = String(event.message || "").trim(); + eventMap.set(id, { + id, + timestamp: event.timestamp || 0, + ingestionTime: event.ingestionTime || 0, + logStreamName: event.logStreamName || "", + message, + preview: buildLogPreview(message) + }); + } + } + + searchedLogStreams = Math.max(searchedLogStreams, (response.searchedLogStreams || []).length); + previousToken = nextToken || ""; + nextToken = response.nextToken; + pageCount += 1; + } while (nextToken && nextToken !== previousToken && pageCount < 10 && eventMap.size < limit); + + const events = [...eventMap.values()] + .sort((left, right) => { + if ((right.timestamp || 0) !== (left.timestamp || 0)) { + return (right.timestamp || 0) - (left.timestamp || 0); + } + + return left.logStreamName.localeCompare(right.logStreamName); + }) + .slice(0, limit); + + return { + endpoint: CLOUDWATCH_ENDPOINT, + region: CLOUDWATCH_REGION, + logGroupName, + logStreamName, + fetchDurationMs: Date.now() - startedAt, + latestTimestamp: events[0]?.timestamp || 0, + searchedLogStreams, + totalEvents: events.length, + events + }; +} + +async function loadSecrets() { + const startedAt = Date.now(); + const secrets = []; + let nextToken; + let pageCount = 0; + + do { + const response = await secretsManagerClient.send( + new ListSecretsCommand({ + NextToken: nextToken, + MaxResults: 50 + }) + ); + + secrets.push( + ...(response.SecretList || []).map((secret, index) => ({ + id: secret.ARN || secret.Name || `secret-${index}`, + name: secret.Name || "Unnamed secret", + arn: secret.ARN || "", + description: secret.Description || "", + createdDate: normalizeTimestamp(secret.CreatedDate), + lastChangedDate: normalizeTimestamp(secret.LastChangedDate), + lastAccessedDate: normalizeTimestamp(secret.LastAccessedDate), + deletedDate: normalizeTimestamp(secret.DeletedDate), + primaryRegion: secret.PrimaryRegion || "", + owningService: secret.OwningService || "", + rotationEnabled: Boolean(secret.RotationEnabled), + versionCount: Object.keys(secret.SecretVersionsToStages || {}).length, + tagCount: Array.isArray(secret.Tags) ? secret.Tags.length : 0, + tags: (secret.Tags || []) + .map((tag) => ({ + key: tag.Key || "", + value: tag.Value || "" + })) + .filter((tag) => tag.key || tag.value) + })) + ); + + nextToken = response.NextToken; + pageCount += 1; + } while (nextToken && pageCount < 10 && secrets.length < 500); + + secrets.sort((left, right) => { + const leftTime = Date.parse(left.lastChangedDate || left.createdDate || 0) || 0; + const rightTime = Date.parse(right.lastChangedDate || right.createdDate || 0) || 0; + + if (rightTime !== leftTime) { + return rightTime - leftTime; + } + + return left.name.localeCompare(right.name); + }); + + return { + endpoint: SECRETS_ENDPOINT, + region: SECRETS_REGION, + fetchedAt: new Date().toISOString(), + fetchDurationMs: Date.now() - startedAt, + totalSecrets: secrets.length, + latestTimestamp: secrets[0]?.lastChangedDate || secrets[0]?.createdDate || "", + secrets + }; +} + +async function loadSecretValue(secretId) { + const startedAt = Date.now(); + const response = await secretsManagerClient.send( + new GetSecretValueCommand({ + SecretId: secretId + }) + ); + + const secretBinary = response.SecretBinary + ? typeof response.SecretBinary === "string" + ? response.SecretBinary + : Buffer.from(response.SecretBinary).toString("base64") + : ""; + + return { + endpoint: SECRETS_ENDPOINT, + region: SECRETS_REGION, + fetchDurationMs: Date.now() - startedAt, + id: secretId, + name: response.Name || "", + arn: response.ARN || "", + versionId: response.VersionId || "", + versionStages: Array.isArray(response.VersionStages) ? response.VersionStages : [], + createdDate: normalizeTimestamp(response.CreatedDate), + secretString: typeof response.SecretString === "string" ? response.SecretString : "", + secretBinary + }; +} + +async function loadS3Buckets() { + const startedAt = Date.now(); + const response = await s3Client.send(new ListBucketsCommand({})); + const buckets = (response.Buckets || []) + .map((bucket) => ({ + name: bucket.Name || "", + creationDate: normalizeTimestamp(bucket.CreationDate) + })) + .filter((bucket) => bucket.name) + .sort((left, right) => left.name.localeCompare(right.name)); + + return { + endpoint: S3_ENDPOINT, + region: S3_REGION, + fetchedAt: new Date().toISOString(), + fetchDurationMs: Date.now() - startedAt, + totalBuckets: buckets.length, + buckets + }; +} + +async function loadS3Objects({ bucket, prefix }) { + const startedAt = Date.now(); + const objects = []; + let continuationToken; + let pageCount = 0; + + do { + const response = await s3Client.send( + new ListObjectsV2Command({ + Bucket: bucket, + Prefix: prefix || undefined, + ContinuationToken: continuationToken, + MaxKeys: 200 + }) + ); + + objects.push( + ...(response.Contents || []).map((object, index) => ({ + id: `${bucket}::${object.Key || index}`, + bucket, + key: object.Key || "", + size: object.Size || 0, + lastModified: normalizeTimestamp(object.LastModified), + etag: String(object.ETag || "").replace(/^"|"$/g, ""), + storageClass: object.StorageClass || "STANDARD" + })) + ); + + continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; + pageCount += 1; + } while (continuationToken && pageCount < 10 && objects.length < 1000); + + objects.sort((left, right) => { + const leftTime = Date.parse(left.lastModified || 0) || 0; + const rightTime = Date.parse(right.lastModified || 0) || 0; + + if (rightTime !== leftTime) { + return rightTime - leftTime; + } + + return left.key.localeCompare(right.key); + }); + + return { + endpoint: S3_ENDPOINT, + region: S3_REGION, + bucket, + prefix, + fetchedAt: new Date().toISOString(), + fetchDurationMs: Date.now() - startedAt, + totalObjects: objects.length, + latestTimestamp: objects[0]?.lastModified || "", + objects + }; +} + +async function loadS3ObjectPreview({ bucket, key }) { + const startedAt = Date.now(); + const head = await s3Client.send( + new HeadObjectCommand({ + Bucket: bucket, + Key: key + }) + ); + + const contentType = head.ContentType || guessObjectContentType(key); + const contentLength = Number(head.ContentLength || 0); + const previewType = resolveS3PreviewType(contentType, key); + const result = { + endpoint: S3_ENDPOINT, + region: S3_REGION, + bucket, + key, + fetchDurationMs: 0, + contentType, + contentLength, + etag: String(head.ETag || "").replace(/^"|"$/g, ""), + lastModified: normalizeTimestamp(head.LastModified), + metadata: head.Metadata || {}, + previewType, + previewText: "", + imageDataUrl: "", + truncated: false + }; + + const shouldLoadTextPreview = previewType === "json" || previewType === "text" || previewType === "html"; + const shouldLoadImagePreview = + previewType === "image" && contentLength > 0 && contentLength <= S3_IMAGE_PREVIEW_MAX_BYTES; + + if ((shouldLoadTextPreview || shouldLoadImagePreview) && contentLength > 0) { + const previewBytes = Math.max(1, Math.min(contentLength || S3_PREVIEW_MAX_BYTES, S3_PREVIEW_MAX_BYTES)); + const response = await s3Client.send( + new GetObjectCommand({ + Bucket: bucket, + Key: key, + Range: `bytes=0-${previewBytes - 1}` + }) + ); + const content = Buffer.from(await response.Body.transformToByteArray()); + result.truncated = contentLength > content.length; + + if (shouldLoadImagePreview) { + result.imageDataUrl = `data:${contentType};base64,${content.toString("base64")}`; + } else { + result.previewText = content.toString("utf8"); + } + } + + result.fetchDurationMs = Date.now() - startedAt; + return result; +} + +async function loadServiceHealthSummary() { + const startedAt = Date.now(); + const [sesResult, logsResult, secretsResult, s3Result] = await Promise.allSettled([ + fetchSesMessages(), + loadLogGroups(), + loadSecrets(), + loadS3Buckets() + ]); + + return { + fetchedAt: new Date().toISOString(), + fetchDurationMs: Date.now() - startedAt, + services: { + emails: summarizeHealthResult({ + icon: "✉️", + panel: "emails", + label: "SES Emails", + result: sesResult, + count: sesResult.status === "fulfilled" ? sesResult.value.length : 0, + detail: SES_ENDPOINT, + noun: "email" + }), + logs: summarizeHealthResult({ + icon: "📜", + panel: "logs", + label: "CloudWatch Logs", + result: logsResult, + count: logsResult.status === "fulfilled" ? logsResult.value.length : 0, + detail: `${CLOUDWATCH_ENDPOINT} (${CLOUDWATCH_REGION})`, + noun: "group" + }), + secrets: summarizeHealthResult({ + icon: "🔐", + panel: "secrets", + label: "Secrets Manager", + result: secretsResult, + count: secretsResult.status === "fulfilled" ? secretsResult.value.totalSecrets : 0, + detail: `${SECRETS_ENDPOINT} (${SECRETS_REGION})`, + noun: "secret" + }), + s3: summarizeHealthResult({ + icon: "🪣", + panel: "s3", + label: "S3 Explorer", + result: s3Result, + count: s3Result.status === "fulfilled" ? s3Result.value.totalBuckets : 0, + detail: `${S3_ENDPOINT} (${S3_REGION})`, + noun: "bucket" + }) + } + }; +} + +async function findSesMessageById(id) { + const messages = await fetchSesMessages(); + return messages.find((message, index) => resolveMessageId(message, index) === id) || null; +} + +async function parseSesMessageById(id) { + const message = await findSesMessageById(id); + + if (!message) { + return null; + } + + return simpleParser(message.RawData || ""); +} + +async function toMessageViewModel(message, index) { + const id = resolveMessageId(message, index); + + try { + const parsed = await simpleParser(message.RawData || ""); + const textContent = normalizeText(parsed.text || ""); + const renderedHtml = buildRenderedHtml(parsed.html || parsed.textAsHtml || ""); + const timestamp = normalizeTimestamp(message.Timestamp || parsed.date); + + return { + id, + index, + from: formatAddressList(parsed.from) || message.Source || "Unknown sender", + to: formatAddressList(parsed.to) || "No To Address", + replyTo: formatAddressList(parsed.replyTo), + subject: parsed.subject || "No Subject", + region: message.Region || "", + timestamp, + timestampMs: timestamp ? Date.parse(timestamp) : 0, + messageId: parsed.messageId || "", + rawSizeBytes: Buffer.byteLength(message.RawData || "", "utf8"), + attachmentCount: parsed.attachments.length, + attachments: parsed.attachments.map((attachment, attachmentIndex) => ({ + index: attachmentIndex, + filename: resolveAttachmentFilename(attachment, attachmentIndex), + contentType: attachment.contentType || "application/octet-stream", + size: attachment.size || 0 + })), + preview: buildPreview(textContent, renderedHtml), + textContent, + renderedHtml, + hasHtml: Boolean(renderedHtml), + parseError: "" + }; + } catch (error) { + return { + id, + index, + from: message.Source || "Unknown sender", + to: "Unknown recipient", + replyTo: "", + subject: "Unable to parse message", + region: message.Region || "", + timestamp: normalizeTimestamp(message.Timestamp), + timestampMs: message.Timestamp ? Date.parse(message.Timestamp) : 0, + messageId: "", + rawSizeBytes: Buffer.byteLength(message.RawData || "", "utf8"), + attachmentCount: 0, + attachments: [], + preview: "This message could not be parsed. Open the raw view to inspect the MIME source.", + textContent: "", + renderedHtml: "", + hasHtml: false, + parseError: error.message + }; + } +} + +function resolveMessageId(message, index = 0) { + return message.Id || `${message.Timestamp || "unknown"}-${message.Source || "unknown"}-${index}`; +} + +function resolveAttachmentFilename(attachment, index = 0) { + if (attachment?.filename) { + return attachment.filename; + } + + return `attachment-${index + 1}${attachmentExtension(attachment?.contentType)}`; +} + +function attachmentExtension(contentType) { + const normalized = String(contentType || "") + .split(";")[0] + .trim() + .toLowerCase(); + + return ( + { + "application/json": ".json", + "application/pdf": ".pdf", + "application/zip": ".zip", + "image/gif": ".gif", + "image/jpeg": ".jpg", + "image/png": ".png", + "image/webp": ".webp", + "text/calendar": ".ics", + "text/csv": ".csv", + "text/html": ".html", + "text/plain": ".txt" + }[normalized] || "" + ); +} + +function buildAttachmentDisposition(filename) { + const fallback = String(filename || "attachment") + .replace(/[^\x20-\x7e]/g, "_") + .replace(/["\\]/g, "_"); + + return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename || "attachment")}`; +} + +function buildInlineDisposition(filename) { + const fallback = String(filename || "file") + .replace(/[^\x20-\x7e]/g, "_") + .replace(/["\\]/g, "_"); + + return `inline; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename || "file")}`; +} + +function basenameFromKey(key) { + const value = String(key || ""); + const parts = value.split("/").filter(Boolean); + return parts[parts.length - 1] || "file"; +} + +function guessObjectContentType(key) { + const normalizedKey = String(key || "").toLowerCase(); + + if (normalizedKey.endsWith(".json")) { + return "application/json"; + } + + if (normalizedKey.endsWith(".csv")) { + return "text/csv"; + } + + if (normalizedKey.endsWith(".html") || normalizedKey.endsWith(".htm")) { + return "text/html"; + } + + if (normalizedKey.endsWith(".txt") || normalizedKey.endsWith(".log") || normalizedKey.endsWith(".md")) { + return "text/plain"; + } + + if (normalizedKey.endsWith(".png")) { + return "image/png"; + } + + if (normalizedKey.endsWith(".jpg") || normalizedKey.endsWith(".jpeg")) { + return "image/jpeg"; + } + + if (normalizedKey.endsWith(".gif")) { + return "image/gif"; + } + + if (normalizedKey.endsWith(".webp")) { + return "image/webp"; + } + + if (normalizedKey.endsWith(".svg")) { + return "image/svg+xml"; + } + + if (normalizedKey.endsWith(".pdf")) { + return "application/pdf"; + } + + return "application/octet-stream"; +} + +function resolveS3PreviewType(contentType, key) { + const normalizedType = String(contentType || "").toLowerCase(); + const normalizedKey = String(key || "").toLowerCase(); + + if (normalizedType.includes("json") || normalizedKey.endsWith(".json")) { + return "json"; + } + + if (normalizedType.startsWith("image/")) { + return "image"; + } + + if (normalizedType.includes("html") || normalizedKey.endsWith(".html") || normalizedKey.endsWith(".htm")) { + return "html"; + } + + if ( + normalizedType.startsWith("text/") || + [".txt", ".log", ".csv", ".xml", ".yml", ".yaml", ".md"].some((extension) => normalizedKey.endsWith(extension)) + ) { + return "text"; + } + + return "binary"; +} + +function summarizeHealthResult({ icon, panel, label, result, count, detail, noun }) { + if (result.status === "fulfilled") { + return { + ok: true, + icon, + panel, + label, + count, + summary: `${count} ${noun}${count === 1 ? "" : "s"}`, + detail + }; + } + + return { + ok: false, + icon, + panel, + label, + count: 0, + summary: "Needs attention", + detail: result.reason?.message || detail + }; +} + +function normalizeTimestamp(value) { + if (!value) { + return ""; + } + + const date = value instanceof Date ? value : new Date(value); + return Number.isNaN(date.getTime()) ? "" : date.toISOString(); +} + +function normalizeText(value) { + return String(value || "") + .replace(/\r\n/g, "\n") + .trim(); +} + +function buildPreview(textContent, renderedHtml) { + const source = (textContent || stripTags(renderedHtml)).replace(/\s+/g, " ").trim(); + + if (!source) { + return "No message preview available."; + } + + return source.length > 220 ? `${source.slice(0, 217)}...` : source; +} + +function buildLogPreview(message) { + const source = String(message || "") + .replace(/\s+/g, " ") + .trim(); + + if (!source) { + return "No log preview available."; + } + + return source.length > 220 ? `${source.slice(0, 217)}...` : source; +} + +function clampNumber(value, fallback, min, max) { + const parsed = Number(value); + + if (!Number.isFinite(parsed)) { + return fallback; + } + + return Math.min(Math.max(parsed, min), max); +} + +function buildRenderedHtml(html) { + if (!html) { + return ""; + } + + const value = String(html); + const hasDocument = /]/i.test(value) || / + + + + + + + + ${value} +`; +} + +function stripTags(value) { + return String(value || "") + .replace(//gi, " ") + .replace(//gi, " ") + .replace(/<[^>]+>/g, " "); +} + +function formatAddressList(addresses) { + if (!addresses?.value?.length) { + return ""; + } + + return addresses.value + .map(({ name, address }) => { + if (name && address) { + return `${name} <${address}>`; + } + + return address || name || ""; + }) + .filter(Boolean) + .join(", "); +} + +async function loadMessageAttachment(messageId, attachmentIndex) { + const parsed = await parseSesMessageById(messageId); + + if (!parsed) { + return null; + } + + const attachment = parsed.attachments?.[attachmentIndex]; + + if (!attachment) { + return null; + } + + return { + filename: resolveAttachmentFilename(attachment, attachmentIndex), + contentType: attachment.contentType || "application/octet-stream", + content: Buffer.isBuffer(attachment.content) ? attachment.content : Buffer.from(attachment.content || "") + }; +} + +async function loadS3ObjectDownload({ bucket, key }) { + const response = await s3Client.send( + new GetObjectCommand({ + Bucket: bucket, + Key: key + }) + ); + + return { + filename: basenameFromKey(key), + contentType: response.ContentType || guessObjectContentType(key), + content: Buffer.from(await response.Body.transformToByteArray()) + }; +} + +export { + buildAttachmentDisposition, + buildInlineDisposition, + clampNumber, + findSesMessageById, + loadLogEvents, + loadLogGroups, + loadLogStreams, + loadMessageAttachment, + loadMessages, + loadS3Buckets, + loadS3ObjectDownload, + loadS3ObjectPreview, + loadS3Objects, + loadSecretValue, + loadSecrets, + loadServiceHealthSummary +}; diff --git a/_reference/localEmailViewer/server/page.js b/_reference/localEmailViewer/server/page.js new file mode 100644 index 000000000..15cb6deea --- /dev/null +++ b/_reference/localEmailViewer/server/page.js @@ -0,0 +1,495 @@ +import { + CLOUDWATCH_DEFAULT_GROUP, + CLOUDWATCH_DEFAULT_LIMIT, + CLOUDWATCH_DEFAULT_WINDOW_MS, + CLOUDWATCH_ENDPOINT, + CLOUDWATCH_REGION, + DEFAULT_REFRESH_MS, + S3_DEFAULT_BUCKET, + S3_ENDPOINT, + S3_REGION, + SECRETS_ENDPOINT, + SECRETS_REGION, + SES_ENDPOINT +} from "./config.js"; + +function getClientConfig() { + return { + defaultRefreshMs: DEFAULT_REFRESH_MS, + endpoint: SES_ENDPOINT, + cloudWatchEndpoint: CLOUDWATCH_ENDPOINT, + cloudWatchRegion: CLOUDWATCH_REGION, + secretsEndpoint: SECRETS_ENDPOINT, + secretsRegion: SECRETS_REGION, + s3Endpoint: S3_ENDPOINT, + s3Region: S3_REGION, + defaultS3Bucket: S3_DEFAULT_BUCKET, + defaultLogGroup: CLOUDWATCH_DEFAULT_GROUP, + defaultLogWindowMs: CLOUDWATCH_DEFAULT_WINDOW_MS, + defaultLogLimit: CLOUDWATCH_DEFAULT_LIMIT + }; +} + +function renderHtml() { + return ` + + + + + LocalStack Inspector + + + +
+
+
+
+

LocalStack Toolbox

+

Inspector

+
+
+
+ + +
+
+
+ Stack +
+ +
+
+
+ +
+
+
+ + + + Waiting for first refresh... +
+
+ + + + +
+
+ +
+
Total00 visible
+
New0New since last refresh
+
NewestNo messagesNot refreshed yet
+
FetchIdleEndpoint: ${escapeHtml(SES_ENDPOINT)}
+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + + + +
+ + + +`; +} + +function renderStyles() { + return ` + :root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--log-shell:linear-gradient(180deg,rgba(239,246,255,.98),rgba(248,251,255,.99));--log-body:#f8fbff;--secret-shell:linear-gradient(180deg,rgba(239,251,246,.98),rgba(247,253,249,.99));--secret-body:#f6fcf8;--bucket-shell:linear-gradient(180deg,rgba(255,249,232,.98),rgba(255,252,243,.99));--bucket-body:#fffcf2;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--log-line:rgba(48,113,169,.22);--secret-line:rgba(31,143,101,.2);--bucket-line:rgba(181,137,37,.22);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--info:#3071a9;--info-soft:rgba(48,113,169,.1);--secret:#1f8f65;--secret-soft:rgba(31,143,101,.1);--bucket:#9d6b00;--bucket-soft:rgba(181,137,37,.12);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);--log-shadow:0 16px 32px rgba(48,113,169,.12);--secret-shadow:0 16px 32px rgba(31,143,101,.12);--bucket-shadow:0 16px 32px rgba(181,137,37,.12);} + *{box-sizing:border-box} + html,body{margin:0;height:100%;overflow:hidden} + body{color-scheme:light;background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.12),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:15px/1.45 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif;transition:background-color .18s ease,color .18s ease} + button,input,select,textarea{font:inherit} + button{cursor:pointer} + .page{display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;max-width:1360px;height:100vh;height:100dvh;margin:0 auto;padding:14px;overflow:hidden} + .hero{display:block;margin-bottom:0} + .heroShell,.toolControls,.stat{background:var(--panel);backdrop-filter:blur(14px);border:1px solid var(--line);box-shadow:var(--shadow)} + .card{background:var(--card-shell);border:1px solid var(--card-line);box-shadow:var(--card-shadow)} + .heroShell,.toolControls{border-radius:18px} + .heroShell{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px} + .toolControls{padding:12px} + .heroIdentity{display:grid;gap:3px;min-width:0} + .eyebrow{margin:0 0 4px;color:var(--accent);font-size:.72rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase} + h1{margin:0;font-size:clamp(1.8rem,3.6vw,2.85rem);line-height:.96;letter-spacing:-.05em} + .lede{margin:8px 0 0;max-width:54ch;color:var(--muted);font-size:.92rem} + .heroTopRow{display:flex;flex:1 1 360px;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end} + .heroActions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end} + .heroStatusRow{display:flex;flex:1 1 100%;flex-wrap:wrap;gap:8px;align-items:center} + .heroStatusLabel{color:var(--muted);font-size:.72rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase} + .helper{margin:0;color:var(--muted);font-size:.89rem} + .healthStrip{display:flex;flex:1 1 520px;flex-wrap:wrap;gap:6px;align-items:center;min-width:0} + .healthBadge{display:inline-flex;align-items:center;gap:8px;min-height:30px;max-width:100%;padding:0 10px;border-radius:999px;border:1px solid rgba(31,41,51,.1);background:rgba(255,255,255,.78);box-shadow:0 8px 18px rgba(15,23,42,.06);text-align:left;transition:transform .12s ease,background-color .12s ease,border-color .12s ease,box-shadow .12s ease} + .healthBadgeName{display:inline-flex;align-items:center;gap:6px;font-size:.8rem;font-weight:800;white-space:nowrap} + .healthBadgeSummary{min-width:0;overflow:hidden;color:var(--muted);font-size:.78rem;font-weight:700;text-overflow:ellipsis;white-space:nowrap} + .healthBadge.ok{border-color:rgba(31,143,101,.22);background:rgba(31,143,101,.1)} + .healthBadge.bad{border-color:rgba(179,58,58,.22);background:rgba(179,58,58,.1)} + .healthBadge.warn{border-color:rgba(157,95,0,.22);background:rgba(157,95,0,.1)} + .healthBadge.active{border-color:rgba(207,109,60,.28);background:rgba(207,109,60,.16);box-shadow:0 10px 24px rgba(207,109,60,.12)} + .healthBadge.active .healthBadgeName,.healthBadge.active .healthBadgeSummary{color:var(--ink)} + .healthRefreshButton{flex:0 0 auto;padding:0 10px} + .primary,.ghost,.mini,.tab{display:inline-flex;align-items:center;justify-content:center;gap:6px;border-radius:999px;border:1px solid transparent;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} + .themeToggle{white-space:nowrap} + .workspacePanel{display:grid;grid-template-rows:auto auto minmax(0,1fr);gap:6px;min-height:0} + .workspacePanel[hidden]{display:none} + .toolControls{display:grid;gap:8px} + .contentPane{height:100%;min-height:0;overflow:auto;scroll-behavior:smooth;padding-right:4px} + .contentStack{display:grid;gap:8px;min-width:100%;padding-bottom:18px} + .paneTopWrap{display:flex;justify-content:flex-end;position:sticky;bottom:14px;pointer-events:none;padding-right:10px} + .paneTopButton{display:inline-flex;align-items:center;justify-content:center;width:42px;height:42px;border-radius:999px;border:1px solid rgba(255,255,255,.32);background:rgba(31,41,51,.42);color:#fff;font-size:1.1rem;line-height:1;backdrop-filter:blur(12px);box-shadow:0 10px 24px rgba(31,41,51,.18);opacity:0;transform:translateY(8px);visibility:hidden;pointer-events:none;transition:opacity .16s ease,transform .16s ease,background-color .12s ease;z-index:6} + .paneTopButton.visible{opacity:.78;transform:translateY(0);visibility:visible;pointer-events:auto} + .paneTopButton.visible:hover{opacity:1;background:rgba(31,41,51,.62);transform:translateY(-1px)} + .row{display:flex;flex-wrap:wrap;gap:6px;align-items:center} + .primary,.ghost{min-height:34px;padding:0 12px;font-weight:700} + .mini,.tab{min-height:28px;padding:0 10px;font-weight:600} + .primary{background:var(--accent);color:#fff7f2} + .ghost,.mini{background:rgba(255,255,255,.76);border-color:var(--line);color:var(--ink)} + .tab{background:transparent;color:var(--muted)} + .tab.active{background:#fff;border-color:rgba(207,109,60,.18);color:var(--ink)} + .primary:hover,.ghost:hover,.mini:hover,.tab:hover{transform:translateY(-1px)} + .chip{display:inline-flex;align-items:center;gap:7px;min-height:34px;padding:0 10px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);font-weight:600;font-size:.88rem} + .chip input{margin:0;accent-color:var(--accent)} + .chip select{border:none;background:transparent;outline:none;color:var(--ink)} + .search{flex:1 1 260px;min-height:36px;padding:0 12px;border-radius:12px;border:1px solid var(--line);background:rgba(255,255,255,.82);color:var(--ink);outline:none} + .searchCompact{flex:1 1 220px} + .status{display:inline-flex;align-items:center;min-height:32px;padding:0 11px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);color:var(--muted);font-size:.86rem;font-weight:600} + .status.ok{color:var(--ok);border-color:rgba(31,143,101,.22);background:rgba(31,143,101,.08)} + .status.warn{color:var(--warn);border-color:rgba(157,95,0,.22);background:rgba(157,95,0,.08)} + .status.bad{color:var(--bad);border-color:rgba(179,58,58,.22);background:rgba(179,58,58,.08)} + .stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:0} + .stat{border-radius:16px;padding:10px 12px} + .stat span{display:block;margin-bottom:4px;color:var(--muted);font-size:.72rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} + .stat strong{display:block;font-size:clamp(1.6rem,3vw,2rem);line-height:1;letter-spacing:-.05em} + .stat strong.small{font-size:1.1rem;line-height:1.3;letter-spacing:-.02em} + .stat small{display:block;margin-top:4px;color:var(--muted);font-size:.82rem} + .banner,.empty{margin:0;padding:12px 14px;border-radius:14px;border:1px solid var(--line);background:rgba(255,255,255,.82)} + .banner{color:var(--bad);border-color:rgba(179,58,58,.24);background:rgba(179,58,58,.08)} + .list{display:grid;gap:12px;align-content:start} + .logList{display:grid;gap:10px;align-content:start;width:100%} + .card{overflow:hidden;border-radius:16px} + .card.new{border-color:rgba(31,143,101,.3);box-shadow:var(--card-shadow),0 0 0 1px rgba(31,143,101,.12)} + .summary{list-style:none;display:grid;gap:7px;padding:12px 14px;cursor:pointer;background:linear-gradient(180deg,rgba(255,250,244,.88),rgba(255,246,238,.96))} + .summary::-webkit-details-marker{display:none} + .top,.tags,.toolbar,.actions,.attachments{display:flex;flex-wrap:wrap;gap:8px;align-items:center} + .top{justify-content:space-between} + .head{min-width:0;flex:1 1 320px} + .head h2{margin:0;font-size:clamp(1rem,1.6vw,1.22rem);line-height:1.18;letter-spacing:-.03em;word-break:break-word} + .meta{margin:4px 0 0;color:var(--muted);font-size:.88rem;word-break:break-word} + .time,.tag{display:inline-flex;align-items:center;min-height:24px;padding:0 10px;border-radius:999px;font-size:.76rem;font-weight:700} + .time{background:rgba(31,41,51,.06)} + .tag{background:var(--accent-soft);color:#8d5632} + .tag.new{background:rgba(31,143,101,.1);color:var(--ok)} + .tag.bad{background:rgba(179,58,58,.1);color:var(--bad)} + .preview{margin:0;color:#324150;font-size:.9rem} + .body{display:grid;gap:10px;padding:10px 14px 14px;border-top:1px solid rgba(207,109,60,.14);background:var(--card-body)} + .toolbar{justify-content:space-between;align-items:center} + .tabs{display:inline-flex;gap:4px;padding:3px;border-radius:999px;background:rgba(207,109,60,.08)} + .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px} + .metaCard{padding:9px 11px;border-radius:12px;background:rgba(255,255,255,.78);border:1px solid rgba(207,109,60,.12)} + .metaCard dt{margin:0 0 4px;color:var(--muted);font-size:.72rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} + .metaCard dd{margin:0;word-break:break-word} + .attachments{gap:6px} + .attachment{display:inline-flex;align-items:center;gap:8px;padding:7px 10px;border-radius:10px;background:rgba(255,248,240,.96);border:1px solid rgba(207,109,60,.12);font-size:.84rem} + .attachmentLink{color:#8d5632;text-decoration:none;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} + .attachmentLink:hover{transform:translateY(-1px);background:#fff;border-color:rgba(207,109,60,.28)} + .panel{overflow:hidden;border-radius:12px;border:1px solid rgba(207,109,60,.14);background:#fff} + .logEvent{width:100%;overflow:hidden;border-radius:16px;border:1px solid var(--log-line);background:var(--log-shell);box-shadow:var(--log-shadow)} + .secretCard{background:var(--secret-shell);border:1px solid var(--secret-line);box-shadow:var(--secret-shadow)} + .s3Card{background:var(--bucket-shell);border:1px solid var(--bucket-line);box-shadow:var(--bucket-shadow)} + .logSummary{list-style:none;display:grid;gap:7px;padding:10px 12px;cursor:pointer} + .logSummary::-webkit-details-marker{display:none} + .secretSummary{background:linear-gradient(180deg,rgba(244,253,248,.9),rgba(236,249,242,.96))} + .s3Summary{background:linear-gradient(180deg,rgba(255,251,238,.92),rgba(255,246,223,.98))} + .logSummaryTop{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:center} + .logMeta{display:flex;flex-wrap:wrap;gap:8px;align-items:center} + .logSummaryActions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end} + .logTag{background:var(--info-soft);color:var(--info);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .secretTag{background:var(--secret-soft);color:var(--secret);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .bucketTag{background:var(--bucket-soft);color:var(--bucket);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .logPreview{margin:0;color:#324150;font:600 .88rem/1.45 "Cascadia Code","Consolas",monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + .logBody{padding:8px 12px 12px;border-top:1px solid rgba(48,113,169,.14);background:var(--log-body)} + .secretBody{border-top-color:rgba(31,143,101,.14);background:var(--secret-body)} + .s3Body{border-top-color:rgba(181,137,37,.18);background:var(--bucket-body)} + .logCopyButton{box-shadow:none} + .logBody pre{border-radius:12px;border:1px solid rgba(48,113,169,.14);padding:12px;background:linear-gradient(180deg,rgba(48,113,169,.04),transparent 140px),#fff} + .secretValuePanel{display:grid;gap:10px} + .secretValuePanel pre{border-radius:12px;border:1px solid rgba(31,143,101,.14);padding:12px;background:linear-gradient(180deg,rgba(31,143,101,.04),transparent 140px),#fff} + .s3PreviewPanel{display:grid;gap:10px} + .s3PreviewImage{max-width:min(100%,640px);border-radius:12px;border:1px solid rgba(181,137,37,.16);background:#fff} + .logBody.wrapOff pre{white-space:pre;word-break:normal} + .tag.levelError{background:rgba(179,58,58,.12);color:var(--bad)} + .tag.levelWarn{background:rgba(157,95,0,.12);color:var(--warn)} + .tag.levelInfo{background:rgba(48,113,169,.12);color:var(--info)} + .tag.levelDebug{background:rgba(96,112,128,.12);color:var(--muted)} + .jsonSyntax .jsonKey{color:#b55f2d} + .jsonSyntax .jsonString{color:#1f8f65} + .jsonSyntax .jsonNumber{color:#2f6ea9} + .jsonSyntax .jsonBoolean{color:#9d5f00} + .jsonSyntax .jsonNull{color:#b33a3a} + iframe{width:100%;min-height:560px;border:none;background:#fff} + pre{margin:0;padding:12px;white-space:pre-wrap;word-break:break-word;overflow:auto;font:12.5px/1.45 "Cascadia Code","Consolas",monospace;color:#102030;background:linear-gradient(180deg,rgba(207,109,60,.04),transparent 140px),#fff} + .placeholder,.inlineError{padding:12px} + .inlineError{color:var(--bad)} + body[data-theme="dark"]{color-scheme:dark;background:radial-gradient(circle at top left,rgba(207,109,60,.12),transparent 28%),radial-gradient(circle at top right,rgba(48,113,169,.12),transparent 26%),linear-gradient(180deg,#10161d,#17202a)} + body[data-theme="dark"] .heroShell, + body[data-theme="dark"] .toolControls, + body[data-theme="dark"] .stat{background:rgba(15,21,30,.84);border-color:rgba(148,163,184,.16);box-shadow:0 14px 30px rgba(0,0,0,.32)} + body[data-theme="dark"] .card{background:linear-gradient(180deg,rgba(50,35,28,.96),rgba(31,25,22,.98));border-color:rgba(207,109,60,.24);box-shadow:0 18px 34px rgba(0,0,0,.34)} + body[data-theme="dark"] .logEvent{background:linear-gradient(180deg,rgba(18,31,45,.96),rgba(14,24,36,.98));border-color:rgba(73,144,204,.22);box-shadow:0 16px 32px rgba(0,0,0,.34)} + body[data-theme="dark"] .secretCard{background:linear-gradient(180deg,rgba(19,39,31,.96),rgba(14,30,24,.98));border-color:rgba(64,170,126,.22);box-shadow:0 16px 32px rgba(0,0,0,.34)} + body[data-theme="dark"] .s3Card{background:linear-gradient(180deg,rgba(52,42,17,.96),rgba(37,30,13,.98));border-color:rgba(181,137,37,.24);box-shadow:0 16px 32px rgba(0,0,0,.34)} + body[data-theme="dark"] .healthBadge{background:rgba(15,21,30,.84);border-color:rgba(148,163,184,.18);box-shadow:0 10px 22px rgba(0,0,0,.28)} + body[data-theme="dark"] .healthBadge.active{border-color:rgba(207,109,60,.32);background:rgba(207,109,60,.18);box-shadow:0 12px 26px rgba(0,0,0,.3)} + body[data-theme="dark"] .healthBadge.active .healthBadgeName, + body[data-theme="dark"] .healthBadge.active .healthBadgeSummary{color:#f8ede6} + body[data-theme="dark"] .tab{color:#aab8c8} + body[data-theme="dark"] .tab.active, + body[data-theme="dark"] .ghost, + body[data-theme="dark"] .mini, + body[data-theme="dark"] .chip, + body[data-theme="dark"] .status, + body[data-theme="dark"] .search{background:rgba(18,25,35,.88);border-color:rgba(148,163,184,.18);color:#edf2f7} + body[data-theme="dark"] .chip select, + body[data-theme="dark"] .search::placeholder{color:#9fb0c2} + body[data-theme="dark"] .ghost, + body[data-theme="dark"] .mini, + body[data-theme="dark"] .tab.active{border-color:rgba(148,163,184,.18)} + body[data-theme="dark"] .summary{background:linear-gradient(180deg,rgba(58,40,31,.88),rgba(45,33,28,.96))} + body[data-theme="dark"] .body{background:#211a17;border-top-color:rgba(207,109,60,.18)} + body[data-theme="dark"] .logSummary{background:linear-gradient(180deg,rgba(21,34,47,.94),rgba(16,27,39,.98))} + body[data-theme="dark"] .logBody{background:#13212d;border-top-color:rgba(73,144,204,.18)} + body[data-theme="dark"] .secretSummary{background:linear-gradient(180deg,rgba(21,43,34,.94),rgba(16,34,27,.98))} + body[data-theme="dark"] .secretBody{background:#12241c;border-top-color:rgba(64,170,126,.18)} + body[data-theme="dark"] .s3Summary{background:linear-gradient(180deg,rgba(53,41,19,.94),rgba(39,31,15,.98))} + body[data-theme="dark"] .s3Body{background:#241d10;border-top-color:rgba(181,137,37,.18)} + body[data-theme="dark"] .metaCard{background:rgba(17,25,35,.64);border-color:rgba(148,163,184,.14)} + body[data-theme="dark"] .attachment{background:rgba(50,35,28,.9);border-color:rgba(207,109,60,.18)} + body[data-theme="dark"] .attachmentLink{color:#f6c4a9} + body[data-theme="dark"] .attachmentLink:hover{background:rgba(75,52,39,.96);border-color:rgba(246,196,169,.26)} + body[data-theme="dark"] .panel, + body[data-theme="dark"] pre, + body[data-theme="dark"] .logBody pre{background:linear-gradient(180deg,rgba(73,144,204,.06),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} + body[data-theme="dark"] .secretValuePanel pre{background:linear-gradient(180deg,rgba(64,170,126,.08),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} + body[data-theme="dark"] .s3PreviewPanel pre{background:linear-gradient(180deg,rgba(181,137,37,.08),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} + body[data-theme="dark"] .panel{border-color:rgba(148,163,184,.14)} + body[data-theme="dark"] .banner, + body[data-theme="dark"] .empty{background:rgba(15,21,30,.82);border-color:rgba(148,163,184,.16)} + body[data-theme="dark"] .time{background:rgba(148,163,184,.12);color:#e8edf3} + body[data-theme="dark"] .tag{background:rgba(207,109,60,.14);color:#f0c2aa} + body[data-theme="dark"] .logTag{background:rgba(73,144,204,.16);color:#93cfff} + body[data-theme="dark"] .secretTag{background:rgba(64,170,126,.16);color:#9fe0be} + body[data-theme="dark"] .bucketTag{background:rgba(181,137,37,.16);color:#f1d38c} + body[data-theme="dark"] .preview, + body[data-theme="dark"] .logPreview, + body[data-theme="dark"] .metaCard dd, + body[data-theme="dark"] .head h2, + body[data-theme="dark"] .stat strong, + body[data-theme="dark"] h1{color:#edf2f7} + body[data-theme="dark"] .jsonSyntax .jsonKey{color:#f0b08a} + body[data-theme="dark"] .jsonSyntax .jsonString{color:#80d5b0} + body[data-theme="dark"] .jsonSyntax .jsonNumber{color:#94c9ff} + body[data-theme="dark"] .jsonSyntax .jsonBoolean{color:#f0c274} + body[data-theme="dark"] .jsonSyntax .jsonNull{color:#ff9c9c} + body[data-theme="dark"] .meta, + body[data-theme="dark"] .helper, + body[data-theme="dark"] .lede, + body[data-theme="dark"] .stat small, + body[data-theme="dark"] .stat span, + body[data-theme="dark"] .chip, + body[data-theme="dark"] .tab{color:#aab8c8} + body[data-theme="dark"] .paneTopButton{border-color:rgba(255,255,255,.18);background:rgba(8,12,18,.58);color:#edf2f7} + body[data-theme="dark"] .paneTopButton.visible:hover{background:rgba(8,12,18,.8)} + @media (max-width:1080px){.stats{grid-template-columns:repeat(2,minmax(0,1fr))}} + @media (max-width:720px){.page{padding:12px}.heroShell,.heroTopRow,.toolbar,.row,.heroActions{align-items:stretch}.heroTopRow{justify-content:stretch;flex-basis:100%}.heroStatusRow{align-items:flex-start}.heroStatusLabel,.healthStrip{flex-basis:100%}.primary,.ghost,.chip,.themeToggle{width:100%;justify-content:center}.healthBadge{justify-content:flex-start}.logSummaryTop,.logSummaryActions{align-items:flex-start}.contentPane{min-height:300px}iframe{min-height:420px}} + `; +} + +function escapeHtml(value) { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +export { getClientConfig, renderHtml }; From 19f918b6958a409b64ba22a9c6d1fde517dfa836 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 20 Mar 2026 15:23:29 -0400 Subject: [PATCH 18/56] feature/IO-3587-Commision-Cut-Clean - Package Bumps --- client/package-lock.json | 282 +++++++++--------- client/package.json | 24 +- package-lock.json | 627 +++++++++++++++++++-------------------- package.json | 26 +- 4 files changed, 470 insertions(+), 489 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index c61164bdc..31b8bad67 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.1", "hasInstallScript": true, "dependencies": { - "@amplitude/analytics-browser": "^2.36.6", + "@amplitude/analytics-browser": "^2.36.8", "@ant-design/pro-layout": "^7.22.6", "@apollo/client": "^4.1.6", "@dnd-kit/core": "^6.3.1", @@ -18,15 +18,15 @@ "@dnd-kit/utilities": "^3.2.2", "@emotion/is-prop-valid": "^1.4.0", "@fingerprintjs/fingerprintjs": "^5.1.0", - "@firebase/analytics": "^0.10.20", - "@firebase/app": "^0.14.9", - "@firebase/auth": "^1.12.1", - "@firebase/firestore": "^4.12.0", - "@firebase/messaging": "^0.12.24", + "@firebase/analytics": "^0.10.21", + "@firebase/app": "^0.14.10", + "@firebase/auth": "^1.12.2", + "@firebase/firestore": "^4.13.0", + "@firebase/messaging": "^0.12.25", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.11.2", "@sentry/cli": "^3.3.3", - "@sentry/react": "^10.43.0", + "@sentry/react": "^10.45.0", "@sentry/vite-plugin": "^4.9.1", "@splitsoftware/splitio-react": "^2.6.1", "@tanem/react-nprogress": "^5.0.63", @@ -44,7 +44,7 @@ "exifr": "^7.1.3", "graphql": "^16.13.1", "graphql-ws": "^6.0.7", - "i18next": "^25.8.18", + "i18next": "^25.8.20", "i18next-browser-languagedetector": "^8.2.1", "immutability-helper": "^3.1.1", "libphonenumber-js": "^1.12.40", @@ -55,7 +55,7 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.71", - "posthog-js": "^1.360.2", + "posthog-js": "^1.363.1", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", @@ -88,7 +88,7 @@ "rxjs": "^7.8.2", "sass": "^1.98.0", "socket.io-client": "^4.8.3", - "styled-components": "^6.3.11", + "styled-components": "^6.3.12", "vite-plugin-ejs": "^1.7.0", "web-vitals": "^5.1.0" }, @@ -96,7 +96,7 @@ "@ant-design/icons": "^6.1.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.28.5", - "@dotenvx/dotenvx": "^1.55.1", + "@dotenvx/dotenvx": "^1.57.0", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", "@eslint/js": "^9.39.2", @@ -121,7 +121,7 @@ "redux-logger": "^3.0.6", "source-map-explorer": "^2.5.3", "vite": "^7.3.1", - "vite-plugin-babel": "^1.5.1", + "vite-plugin-babel": "^1.6.0", "vite-plugin-eslint": "^1.8.1", "vite-plugin-node-polyfills": "^0.25.0", "vite-plugin-pwa": "^1.2.0", @@ -151,17 +151,17 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-browser": { - "version": "2.36.6", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.36.6.tgz", - "integrity": "sha512-tnIObYiC/YtVAxn83lPEhsVCmGjKe3tzWQ4IwufDR6RIsaopqW3qgCOYLeCyWFAwUtwrIwoYWAOBi9bn1KkI6A==", + "version": "2.36.8", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.36.8.tgz", + "integrity": "sha512-c0+wsu/qxTwc0w1JPVNzl22djW8YdYqxXiYYJ457VHbgUB/BT335ORMyunIlg8xwJCOsrlhNEj+bJCBp11wT+g==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.41.6", - "@amplitude/plugin-autocapture-browser": "1.23.6", - "@amplitude/plugin-network-capture-browser": "1.9.6", - "@amplitude/plugin-page-url-enrichment-browser": "0.6.10", - "@amplitude/plugin-page-view-tracking-browser": "2.8.6", - "@amplitude/plugin-web-vitals-browser": "1.1.21", + "@amplitude/analytics-core": "2.42.0", + "@amplitude/plugin-autocapture-browser": "1.24.0", + "@amplitude/plugin-network-capture-browser": "1.9.8", + "@amplitude/plugin-page-url-enrichment-browser": "0.6.12", + "@amplitude/plugin-page-view-tracking-browser": "2.9.0", + "@amplitude/plugin-web-vitals-browser": "1.1.23", "tslib": "^2.4.1" } }, @@ -172,9 +172,9 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-core": { - "version": "2.41.6", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.41.6.tgz", - "integrity": "sha512-l8orw7J+fTDvPLtPUMIp3R+O7OHtHiSXFSPivi+YOpsxyZhm65Rs1EUVpWeHAavuTmzRR7VnVKxERVQqaZ2y/A==", + "version": "2.42.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.42.0.tgz", + "integrity": "sha512-LupBwVGlWhYb6OHSqifh8Jm9rDRNDdCZuqpjv11OmaaIW2rmPbu5JQcKmJoeqolb4+BmdkPAm/Vz7VNjlJAcqA==", "license": "MIT", "dependencies": { "@amplitude/analytics-connector": "^1.6.4", @@ -185,52 +185,52 @@ } }, "node_modules/@amplitude/plugin-autocapture-browser": { - "version": "1.23.6", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.23.6.tgz", - "integrity": "sha512-8WulIIySVfUEO9rhTfY8R/RQ6Ds8t2CTv4dBx0GXSiZrbncg7zbIXt3RBowCM53ZKu0q7raC0IHhD+XaaykqQA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.24.0.tgz", + "integrity": "sha512-H9CSIj7OM7mPOpCATtCtpXX2iKQFA11ALkRgnTfyx+75BQ9YF3622ncg5kI9kO+RW87gPGQL5l7A5Er+q1tNyA==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.41.6", + "@amplitude/analytics-core": "2.42.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-network-capture-browser": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.9.6.tgz", - "integrity": "sha512-9eLYTSJE+ORu76Wh0ECZpLldmI/F38h+x0SZCtoc2IaILZ0RQENRqQK6HW9Jv2CCj3K1/+hKWwSYOpew+rF9RQ==", + "version": "1.9.8", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.9.8.tgz", + "integrity": "sha512-zin6jYMdwveOgYHiY7yCfo4/j/dCjlP/VK5AD/uTYiRewiDMPVm077WTqHgaswZGgxT7DL0BxdIrJ2l27S1daw==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.41.6", + "@amplitude/analytics-core": "2.42.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-page-url-enrichment-browser": { - "version": "0.6.10", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.6.10.tgz", - "integrity": "sha512-jgp//bCJ93B6vz6AYAMg2vroHzK4rYpHxYVgmZwipD4fRD+18Y/GJebWcl6DYfQB4tvEzHQk2mxYBeZQG0d1Ug==", + "version": "0.6.12", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.6.12.tgz", + "integrity": "sha512-pyYk+nfHvDfr+ovw/BsK8GpwvyLWiBq01TWjAXef+gN+wa5CAIg/mHvAAdT+x90uRbzKV20TyaKhYd4tw65MKA==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.41.6", + "@amplitude/analytics-core": "2.42.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-page-view-tracking-browser": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.8.6.tgz", - "integrity": "sha512-P7hyBg4/d4Mdbgzx5nkciy+yHWxPtoclJyVOf++AOgYLSzAeGw9J5mJl0vUitBG50tIvdRxoVk7kTHImfzD4dQ==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.9.0.tgz", + "integrity": "sha512-PdG6ogdJA/XldHh1IgnsWTyCGseiHjBUXxMrMjY+MAfIyjtQZcGzNaaj237BG3ArsUa2YHxEpIGz5Uj68bQAlw==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.41.6", + "@amplitude/analytics-core": "2.42.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-web-vitals-browser": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.21.tgz", - "integrity": "sha512-ZivQlkkMIed0E0nrUuAbYTAZErYbvXTaBPMkQ+yPzixoBnMww/RF9q63WaH7mq83HMEVN66ISOmsUcmM0J/FVQ==", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.23.tgz", + "integrity": "sha512-fKqprLoOd08yxYQ0Ugl9WIRJPlZVk5t/mGxxqrWvr5umlW2iuxBDOZP+AlBdPs4ZFze767baAS/KDibZPfrffg==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.41.6", + "@amplitude/analytics-core": "2.42.0", "tslib": "^2.4.1", "web-vitals": "5.1.0" } @@ -2577,9 +2577,9 @@ } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.55.1.tgz", - "integrity": "sha512-WEuKyoe9CA7dfcFBnNbL0ndbCNcptaEYBygfFo9X1qEG+HD7xku4CYIplw6sbAHJavesZWbVBHeRSpvri0eKqw==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.57.0.tgz", + "integrity": "sha512-WsTEcqfHzKmLFZh3jLGd7o4iCkrIupp+qFH2FJUJtQXUh2GcOnLXD00DcrhlO4H8QSmaKnW9lugOEbrdpu25kA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3425,15 +3425,15 @@ "license": "MIT" }, "node_modules/@firebase/analytics": { - "version": "0.10.20", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.20.tgz", - "integrity": "sha512-adGTNVUWH5q66tI/OQuKLSN6mamPpfYhj0radlH2xt+3eL6NFPtXoOs+ulvs+UsmK27vNFx5FjRDfWk+TyduHg==", + "version": "0.10.21", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.21.tgz", + "integrity": "sha512-j2y2q65BlgLGB5Pwjhv/Jopw2X/TBTzvAtI5z/DSp56U4wBj7LfhBfzbdCtFPges+Wz0g55GdoawXibOH5jGng==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/installations": "0.6.20", + "@firebase/component": "0.7.2", + "@firebase/installations": "0.6.21", "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", + "@firebase/util": "1.15.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -3441,14 +3441,14 @@ } }, "node_modules/@firebase/app": { - "version": "0.14.9", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.9.tgz", - "integrity": "sha512-3gtUX0e584MYkKBQMgSECMvE1Dwzg+eONefDQ0wxVSe5YMBsZwdN5pL7UapwWBlV8+i8QCztF9TP947tEjZAGA==", + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.10.tgz", + "integrity": "sha512-PlPhdtjgWUra+LImQTnXOUqUa/jcufZhizdR93ZjlQSS3ahCtDTG6pJw7j0OwFal18DQjICXfeVNsUUrcNisfA==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.1", + "@firebase/component": "0.7.2", "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", + "@firebase/util": "1.15.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -3457,14 +3457,14 @@ } }, "node_modules/@firebase/auth": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.1.tgz", - "integrity": "sha512-nXKj7d5bMBlnq6XpcQQpmnSVwEeHBkoVbY/+Wk0P1ebLSICoH4XPtvKOFlXKfIHmcS84mLQ99fk3njlDGKSDtw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.2.tgz", + "integrity": "sha512-CZJL8V10Vzibs+pDTXdQF+hot1IigIoqF4a4lA/qr5Deo1srcefiyIfgg28B67Lk7IxZhwfJMuI+1bu2xBmV0A==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.1", + "@firebase/component": "0.7.2", "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", + "@firebase/util": "1.15.0", "tslib": "^2.1.0" }, "engines": { @@ -3481,12 +3481,12 @@ } }, "node_modules/@firebase/component": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.1.tgz", - "integrity": "sha512-mFzsm7CLHR60o08S23iLUY8m/i6kLpOK87wdEFPLhdlCahaxKmWOwSVGiWoENYSmFJJoDhrR3gKSCxz7ENdIww==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.2.tgz", + "integrity": "sha512-iyVDGc6Vjx7Rm0cAdccLH/NG6fADsgJak/XW9IA2lPf8AjIlsemOpFGKczYyPHxm4rnKdR8z6sK4+KEC7NwmEg==", "license": "Apache-2.0", "dependencies": { - "@firebase/util": "1.14.0", + "@firebase/util": "1.15.0", "tslib": "^2.1.0" }, "engines": { @@ -3494,14 +3494,14 @@ } }, "node_modules/@firebase/firestore": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.12.0.tgz", - "integrity": "sha512-PM47OyiiAAoAMB8kkq4Je14mTciaRoAPDd3ng3Ckqz9i2TX9D9LfxIRcNzP/OxzNV4uBKRq6lXoOggkJBQR3Gw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.13.0.tgz", + "integrity": "sha512-7i4cVNJXTMim7/P7UsNim0DwyLPk4QQ3y1oSNzv4l0ykJOKYCiFMOuEeUxUYvrReXDJxWHrT/4XMeVQm+13rRw==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.1", + "@firebase/component": "0.7.2", "@firebase/logger": "0.5.0", - "@firebase/util": "1.14.0", + "@firebase/util": "1.15.0", "@firebase/webchannel-wrapper": "1.0.5", "@grpc/grpc-js": "~1.9.0", "@grpc/proto-loader": "^0.7.8", @@ -3515,13 +3515,13 @@ } }, "node_modules/@firebase/installations": { - "version": "0.6.20", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.20.tgz", - "integrity": "sha512-LOzvR7XHPbhS0YB5ANXhqXB5qZlntPpwU/4KFwhSNpXNsGk/sBQ9g5hepi0y0/MfenJLe2v7t644iGOOElQaHQ==", + "version": "0.6.21", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.21.tgz", + "integrity": "sha512-xGFGTeICJZ5vhrmmDukeczIcFULFXybojML2+QSDFoKj5A7zbGN7KzFGSKNhDkIxpjzsYG9IleJyUebuAcmqWA==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/util": "1.14.0", + "@firebase/component": "0.7.2", + "@firebase/util": "1.15.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -3542,15 +3542,15 @@ } }, "node_modules/@firebase/messaging": { - "version": "0.12.24", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.24.tgz", - "integrity": "sha512-UtKoubegAhHyehcB7iQjvQ8OVITThPbbWk3g2/2ze42PrQr6oe6OmCElYQkBrE5RDCeMTNucXejbdulrQ2XwVg==", + "version": "0.12.25", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.25.tgz", + "integrity": "sha512-7RhDwoDHlOK1/ou0/LeubxmjcngsTjDdrY/ssg2vwAVpUuVAhQzQvuCAOYxcX5wNC1zCgQ54AP1vdngBwbCmOQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.7.1", - "@firebase/installations": "0.6.20", + "@firebase/component": "0.7.2", + "@firebase/installations": "0.6.21", "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.14.0", + "@firebase/util": "1.15.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -3565,9 +3565,9 @@ "license": "Apache-2.0" }, "node_modules/@firebase/util": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.14.0.tgz", - "integrity": "sha512-/gnejm7MKkVIXnSJGpc9L2CvvvzJvtDPeAEq5jAwgVlf/PeNxot+THx/bpD20wQ8uL5sz0xqgXy1nisOYMU+mw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.0.tgz", + "integrity": "sha512-AmWf3cHAOMbrCPG4xdPKQaj5iHnyYfyLKZxwz+Xf55bqKbpAmcYifB4jQinT2W9XhDRHISOoPyBOariJpCG6FA==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -4853,18 +4853,18 @@ } }, "node_modules/@posthog/core": { - "version": "1.23.4", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.23.4.tgz", - "integrity": "sha512-gSM1gnIuw5UOBUOTz0IhCTH8jOHoFr5rzSDb5m7fn9ofLHvz3boZT1L1f+bcuk+mvzNJfrJ3ByVQGKmUQnKQ8g==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.24.1.tgz", + "integrity": "sha512-e8AciAnc6MRFws89ux8lJKFAaI03yEon0ASDoUO7yS91FVqbUGXYekObUUR3LHplcg+pmyiJBI0jolY0SFbGRA==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6" } }, "node_modules/@posthog/types": { - "version": "1.360.2", - "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.360.2.tgz", - "integrity": "sha512-U48CbtmX5kETZvWjaJVlublSA1aLV99m71TQtgxWksBMXINS/3C7j+KqlMO6wH7SuaEZQnjaxh1KYGH4nRCaaA==", + "version": "1.363.1", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.363.1.tgz", + "integrity": "sha512-bFYk5XHgYEfVhQU0AwkG9MbMqq9QRbKDDJxOtYWGJ6Uw+/nLRNs/ZydXy3aMt0ldIdkNzZq+qaJ/p2Jg0+mP8g==", "license": "MIT" }, "node_modules/@protobufjs/aspromise": { @@ -6701,50 +6701,50 @@ ] }, "node_modules/@sentry-internal/browser-utils": { - "version": "10.43.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.43.0.tgz", - "integrity": "sha512-8zYTnzhAPvNkVH1Irs62wl0J/c+0QcJ62TonKnzpSFUUD3V5qz8YDZbjIDGfxy+1EB9fO0sxtddKCzwTHF/MbQ==", + "version": "10.45.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.45.0.tgz", + "integrity": "sha512-ZPZpeIarXKScvquGx2AfNKcYiVNDA4wegMmjyGVsTA2JPmP0TrJoO3UybJS6KGDeee8V3I3EfD/ruauMm7jOFQ==", "license": "MIT", "dependencies": { - "@sentry/core": "10.43.0" + "@sentry/core": "10.45.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "10.43.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.43.0.tgz", - "integrity": "sha512-YoXuwluP6eOcQxTeTtaWb090++MrLyWOVsUTejzUQQ6LFL13Jwt+bDPF1kvBugMq4a7OHw/UNKQfd6//rZMn2g==", + "version": "10.45.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.45.0.tgz", + "integrity": "sha512-vCSurazFVq7RUeYiM5X326jA5gOVrWYD6lYX2fbjBOMcyCEhDnveNxMT62zKkZDyNT/jyD194nz/cjntBUkyWA==", "license": "MIT", "dependencies": { - "@sentry/core": "10.43.0" + "@sentry/core": "10.45.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "10.43.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.43.0.tgz", - "integrity": "sha512-khCXlGrlH1IU7P5zCEAJFestMeH97zDVCekj8OsNNDtN/1BmCJ46k6Xi0EqAUzdJgrOLJeLdoYdgtiIjovZ8Sg==", + "version": "10.45.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.45.0.tgz", + "integrity": "sha512-vjosRoGA1bzhVAEO1oce+CsRdd70quzBeo7WvYqpcUnoLe/Rv8qpOMqWX3j26z7XfFHMExWQNQeLxmtYOArvlw==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.43.0", - "@sentry/core": "10.43.0" + "@sentry-internal/browser-utils": "10.45.0", + "@sentry/core": "10.45.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "10.43.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.43.0.tgz", - "integrity": "sha512-ZIw1UNKOFXo1LbPCJPMAx9xv7D8TMZQusLDUgb6BsPQJj0igAuwd7KRGTkjjgnrwBp2O/sxcQFRhQhknWk7QPg==", + "version": "10.45.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.45.0.tgz", + "integrity": "sha512-nvq/AocdZTuD7y0KSiWi3gVaY0s5HOFy86mC/v1kDZmT/jsBAzN5LDkk/f1FvsWma1peqQmpUqxvhC+YIW294Q==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "10.43.0", - "@sentry/core": "10.43.0" + "@sentry-internal/replay": "10.45.0", + "@sentry/core": "10.45.0" }, "engines": { "node": ">=18" @@ -6760,16 +6760,16 @@ } }, "node_modules/@sentry/browser": { - "version": "10.43.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.43.0.tgz", - "integrity": "sha512-2V3I3sXi3SMeiZpKixd9ztokSgK27cmvsD9J5oyOyjhGLTW/6QKCwHbKnluMgQMXq20nixQk5zN4wRjRUma3sg==", + "version": "10.45.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.45.0.tgz", + "integrity": "sha512-e/a8UMiQhqqv706McSIcG6XK+AoQf9INthi2pD+giZfNRTzXTdqHzUT5OIO5hg8Am6eF63nDJc+vrYNPhzs51Q==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.43.0", - "@sentry-internal/feedback": "10.43.0", - "@sentry-internal/replay": "10.43.0", - "@sentry-internal/replay-canvas": "10.43.0", - "@sentry/core": "10.43.0" + "@sentry-internal/browser-utils": "10.45.0", + "@sentry-internal/feedback": "10.45.0", + "@sentry-internal/replay": "10.45.0", + "@sentry-internal/replay-canvas": "10.45.0", + "@sentry/core": "10.45.0" }, "engines": { "node": ">=18" @@ -7178,22 +7178,22 @@ } }, "node_modules/@sentry/core": { - "version": "10.43.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.43.0.tgz", - "integrity": "sha512-l0SszQAPiQGWl/ferw8GP3ALyHXiGiRKJaOvNmhGO+PrTQyZTZ6OYyPnGijAFRg58dE1V3RCH/zw5d2xSUIiNg==", + "version": "10.45.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.45.0.tgz", + "integrity": "sha512-s69UXxvefeQxuZ5nY7/THtTrIEvJxNVCp3ns4kwoCw1qMpgpvn/296WCKVmM7MiwnaAdzEKnAvLAwaxZc2nM7Q==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "10.43.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.43.0.tgz", - "integrity": "sha512-shvErEpJ41i0Q3lIZl0CDWYQ7m8yHLi7ECG0gFvN8zf8pEdl5grQIOoe3t/GIUzcpCcor16F148ATmKJJypc/Q==", + "version": "10.45.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.45.0.tgz", + "integrity": "sha512-jLezuxi4BUIU3raKyAPR5xMbQG/nhwnWmKo5p11NCbLmWzkS+lxoyDTUB4B8TAKZLfdtdkKLOn1S0tFc8vbUHw==", "license": "MIT", "dependencies": { - "@sentry/browser": "10.43.0", - "@sentry/core": "10.43.0" + "@sentry/browser": "10.45.0", + "@sentry/core": "10.45.0" }, "engines": { "node": ">=18" @@ -11773,9 +11773,9 @@ } }, "node_modules/i18next": { - "version": "25.8.18", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.18.tgz", - "integrity": "sha512-lzY5X83BiL5AP77+9DydbrqkQHFN9hUzWGjqjLpPcp5ZOzuu1aSoKaU3xbBLSjWx9dAzW431y+d+aogxOZaKRA==", + "version": "25.8.20", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.20.tgz", + "integrity": "sha512-xjo9+lbX/P1tQt3xpO2rfJiBppNfUnNIPKgCvNsTKsvTOCro1Qr/geXVg1N47j5ScOSaXAPq8ET93raK3Rr06A==", "funding": [ { "type": "individual", @@ -15059,9 +15059,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.360.2", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.360.2.tgz", - "integrity": "sha512-/Wed0mOuRUfyEGT/BRQaokCqBlxrEceE7MDT9A00lU5tXo443/2Pg9ZiqN5sucUluZF47hwGORpYPoVUt32UFw==", + "version": "1.363.1", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.363.1.tgz", + "integrity": "sha512-iaDtRxCs/FiB+RXe83uo7RZXgpLlyB6qFoNHl3bNMgRCgrPI2nkzx2m9Va1l30HHl/zA1kPOXSy2/tZC5Ql5kg==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@opentelemetry/api": "^1.9.0", @@ -15069,8 +15069,8 @@ "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", - "@posthog/core": "1.23.4", - "@posthog/types": "1.360.2", + "@posthog/core": "1.24.1", + "@posthog/types": "1.363.1", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", @@ -17352,9 +17352,9 @@ } }, "node_modules/styled-components": { - "version": "6.3.11", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.11.tgz", - "integrity": "sha512-opzgceGlQ5rdZdGwf9ddLW7EM2F4L7tgsgLn6fFzQ2JgE5EVQ4HZwNkcgB1p8WfOBx1GEZP3fa66ajJmtXhSrA==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.12.tgz", + "integrity": "sha512-hFR6xsVkVYbsdcUlzPYFvFfoc6o2KlV0VvgRIQwSYMtdThM7SCxnjX9efh/cWce2kTq16I/Kl3xM98xiLptsXA==", "license": "MIT", "dependencies": { "@emotion/is-prop-valid": "1.4.0", @@ -18441,14 +18441,14 @@ } }, "node_modules/vite-plugin-babel": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/vite-plugin-babel/-/vite-plugin-babel-1.5.1.tgz", - "integrity": "sha512-TBBBsAYYg7V5yR+xPeZYHwritMmc2QvZrZKFSS26it7ZQ0Y8ESKwJJm2KUUcmHQZU/owvA4yKk4ibPVrfhlwJw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-babel/-/vite-plugin-babel-1.6.0.tgz", + "integrity": "sha512-VtYA4FSmQREA2oaZ7+jfLS/fBk1/xZMUR94YZzB5s6U9WyptbvThUD1HSSv7oNDU28jGuHmdBZ1wTVGNIoChoQ==", "dev": true, "license": "MIT", "peerDependencies": { "@babel/core": "^7.0.0", - "vite": "^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/vite-plugin-ejs": { diff --git a/client/package.json b/client/package.json index e44ee619a..890e14563 100644 --- a/client/package.json +++ b/client/package.json @@ -8,7 +8,7 @@ "private": true, "proxy": "http://localhost:4000", "dependencies": { - "@amplitude/analytics-browser": "^2.36.6", + "@amplitude/analytics-browser": "^2.36.8", "@ant-design/pro-layout": "^7.22.6", "@apollo/client": "^4.1.6", "@dnd-kit/core": "^6.3.1", @@ -17,15 +17,15 @@ "@dnd-kit/utilities": "^3.2.2", "@emotion/is-prop-valid": "^1.4.0", "@fingerprintjs/fingerprintjs": "^5.1.0", - "@firebase/analytics": "^0.10.20", - "@firebase/app": "^0.14.9", - "@firebase/auth": "^1.12.1", - "@firebase/firestore": "^4.12.0", - "@firebase/messaging": "^0.12.24", + "@firebase/analytics": "^0.10.21", + "@firebase/app": "^0.14.10", + "@firebase/auth": "^1.12.2", + "@firebase/firestore": "^4.13.0", + "@firebase/messaging": "^0.12.25", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.11.2", "@sentry/cli": "^3.3.3", - "@sentry/react": "^10.43.0", + "@sentry/react": "^10.45.0", "@sentry/vite-plugin": "^4.9.1", "@splitsoftware/splitio-react": "^2.6.1", "@tanem/react-nprogress": "^5.0.63", @@ -43,7 +43,7 @@ "exifr": "^7.1.3", "graphql": "^16.13.1", "graphql-ws": "^6.0.7", - "i18next": "^25.8.18", + "i18next": "^25.8.20", "i18next-browser-languagedetector": "^8.2.1", "immutability-helper": "^3.1.1", "libphonenumber-js": "^1.12.40", @@ -54,7 +54,7 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.71", - "posthog-js": "^1.360.2", + "posthog-js": "^1.363.1", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", @@ -87,7 +87,7 @@ "rxjs": "^7.8.2", "sass": "^1.98.0", "socket.io-client": "^4.8.3", - "styled-components": "^6.3.11", + "styled-components": "^6.3.12", "vite-plugin-ejs": "^1.7.0", "web-vitals": "^5.1.0" }, @@ -140,7 +140,7 @@ "@ant-design/icons": "^6.1.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.28.5", - "@dotenvx/dotenvx": "^1.55.1", + "@dotenvx/dotenvx": "^1.57.0", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", "@eslint/js": "^9.39.2", @@ -165,7 +165,7 @@ "redux-logger": "^3.0.6", "source-map-explorer": "^2.5.3", "vite": "^7.3.1", - "vite-plugin-babel": "^1.5.1", + "vite-plugin-babel": "^1.6.0", "vite-plugin-eslint": "^1.8.1", "vite-plugin-node-polyfills": "^0.25.0", "vite-plugin-pwa": "^1.2.0", diff --git a/package-lock.json b/package-lock.json index f09227e6b..0a63c96c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,16 @@ "version": "0.2.0", "license": "UNLICENSED", "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.1010.0", - "@aws-sdk/client-elasticache": "^3.1010.0", - "@aws-sdk/client-s3": "^3.1010.0", - "@aws-sdk/client-secrets-manager": "^3.1010.0", - "@aws-sdk/client-ses": "^3.1010.0", - "@aws-sdk/client-sqs": "^3.1010.0", - "@aws-sdk/client-textract": "^3.1010.0", - "@aws-sdk/credential-provider-node": "^3.972.21", - "@aws-sdk/lib-storage": "^3.1010.0", - "@aws-sdk/s3-request-presigner": "^3.1010.0", + "@aws-sdk/client-cloudwatch-logs": "^3.1014.0", + "@aws-sdk/client-elasticache": "^3.1014.0", + "@aws-sdk/client-s3": "^3.1014.0", + "@aws-sdk/client-secrets-manager": "^3.1014.0", + "@aws-sdk/client-ses": "^3.1014.0", + "@aws-sdk/client-sqs": "^3.1014.0", + "@aws-sdk/client-textract": "^3.1014.0", + "@aws-sdk/credential-provider-node": "^3.972.24", + "@aws-sdk/lib-storage": "^3.1014.0", + "@aws-sdk/s3-request-presigner": "^3.1014.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", @@ -37,19 +37,19 @@ "dinero.js": "^1.9.1", "dotenv": "^17.3.1", "express": "^4.21.1", - "fast-xml-parser": "^5.5.6", + "fast-xml-parser": "^5.5.8", "firebase-admin": "^13.7.0", "fuse.js": "^7.1.0", "graphql": "^16.13.1", "graphql-request": "^6.1.0", "intuit-oauth": "^4.2.2", - "ioredis": "^5.10.0", + "ioredis": "^5.10.1", "json-2-csv": "^5.5.10", "jsonwebtoken": "^9.0.3", "juice": "^11.1.1", "lodash": "^4.17.23", "moment": "^2.30.1", - "moment-timezone": "^0.6.0", + "moment-timezone": "^0.6.1", "multer": "^2.1.1", "mustache": "^4.2.0", "node-persist": "^4.0.4", @@ -292,26 +292,26 @@ } }, "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.1010.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1010.0.tgz", - "integrity": "sha512-oZA6//n5nC3OJ6fYyjLDs0cdHMMYjQE2BxKeYmmKSbREJErt81awtbqYNdgDo1kUbSRHCZmdkw8I5uShe7gLLg==", + "version": "3.1014.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1014.0.tgz", + "integrity": "sha512-T3TV8Rk4gyv18W5N1r/PeQaAbHTwJMV12JJ9KQEEdY0ZS1Bb/kx9JIPgMMlUjSQ7LeJ8FsdNRXDuAtCt3kCy+Q==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.24", + "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/util-user-agent-node": "^3.973.10", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", @@ -319,21 +319,21 @@ "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -345,45 +345,45 @@ } }, "node_modules/@aws-sdk/client-elasticache": { - "version": "3.1010.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.1010.0.tgz", - "integrity": "sha512-WFOE0JTqvV5uE+2PBVsTJ/UYKEJM0pL/kD8wKyLVF88SOXR+dpI7JZPwKHU8+pPFVlwNYA8WEjfQeSuaE2EFNA==", + "version": "3.1014.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.1014.0.tgz", + "integrity": "sha512-1gSai5MxtnmiGt80thfnmcfTWXRtj2vRDhDkSOa348e9MlSfFAttKuvp+bFb92M1FxbM9Dh8wQZxxluuQWu3wA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.24", + "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/util-user-agent-node": "^3.973.10", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -396,34 +396,34 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1010.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1010.0.tgz", - "integrity": "sha512-XUqXFrn/FGLLzO5OXu9iAtt492kj9Z7Yk8b0iPFxeJoIhaa61YOgR84chOExvnjm2+JTYyGNZiVPmgnFB3jxXA==", + "version": "3.1014.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1014.0.tgz", + "integrity": "sha512-0XLrOT4Cm3NEhhiME7l/8LbTXS4KdsbR4dSrY207KNKTcHLLTZ9EXt4ZpgnTfLvWQF3pGP2us4Zi1fYLo0N+Ow==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", - "@aws-sdk/middleware-flexible-checksums": "^3.974.0", + "@aws-sdk/middleware-flexible-checksums": "^3.974.3", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-sdk-s3": "^3.972.20", + "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/middleware-ssec": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", - "@aws-sdk/signature-v4-multi-region": "^3.996.8", + "@aws-sdk/middleware-user-agent": "^3.972.24", + "@aws-sdk/region-config-resolver": "^3.972.9", + "@aws-sdk/signature-v4-multi-region": "^3.996.11", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/util-user-agent-node": "^3.973.10", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", @@ -434,25 +434,25 @@ "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", - "@smithy/util-stream": "^4.5.19", + "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" @@ -462,45 +462,45 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.1010.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1010.0.tgz", - "integrity": "sha512-uAd22Cqy0Y3q78XBdABk1tBVZAjHJIEC8Q1dkwSRwUMr+sf2CjgWIFOeZfjewcNPInV3OpBzo5gmbVBayS3VyA==", + "version": "3.1014.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1014.0.tgz", + "integrity": "sha512-XRp6t0AHGxmCWwYqfELxJUiHnOIR5yUyWBNXJjeEp4t9Pzdg86pQw1aXfmfZ4sVdebT06lL9+D0NsP12ztoSlQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.24", + "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/util-user-agent-node": "^3.973.10", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -512,45 +512,45 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.1010.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.1010.0.tgz", - "integrity": "sha512-7bmFmJQZLLbyuzQZ9JBbmUvb9w3vBJ5xV5Dac2m1qa0HJchH2+nn1j1I5sRantTR5w9DuozhIcnGbVMNcqFMgw==", + "version": "3.1014.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.1014.0.tgz", + "integrity": "sha512-HXLdpIDatHAzEHFoiwwcoYQ5Sx6FP1pkjTwvhgW5zQ+4E69/zC7quQ0dzxX8I38Itvyr8Dq/CmNLUM76XErAVw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.24", + "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/util-user-agent-node": "^3.973.10", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -563,47 +563,47 @@ } }, "node_modules/@aws-sdk/client-sqs": { - "version": "3.1010.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1010.0.tgz", - "integrity": "sha512-yMMnHbbH6eo54zR5QIGINgf2P8u9DEkmEWOx0EtUvRzTUqjmTwAbKZZHITo1s395dJihIc5V9Jj2zNPvk01wgg==", + "version": "3.1014.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1014.0.tgz", + "integrity": "sha512-xZ/yAd5FtpDyeIRRSXlRV7/PC0wC3vUENBEx5h/j06UkrOosoELp7YguC7ecwvKYeO7mvOO4I94iEhfJspp7Dw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-sdk-sqs": "^3.972.15", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/middleware-sdk-sqs": "^3.972.17", + "@aws-sdk/middleware-user-agent": "^3.972.24", + "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/util-user-agent-node": "^3.973.10", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -615,45 +615,45 @@ } }, "node_modules/@aws-sdk/client-textract": { - "version": "3.1010.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-textract/-/client-textract-3.1010.0.tgz", - "integrity": "sha512-BfSW497703h3uT+XxvzSO88GVCgz73wx8diKRpxUjGN74Mh0UQgmi8XuLjz+qrfNIJWV72C443B6csF/DbddzQ==", + "version": "3.1014.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-textract/-/client-textract-3.1014.0.tgz", + "integrity": "sha512-go4Wsbz6UrrtbsEMT0urj/zESL9CJ36B6cOMf+WxgOr3qUq/40+hLO30BtIN5C9psyAMVGgNi6Wv9abaejuezQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.24", + "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/util-user-agent-node": "^3.973.10", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -665,19 +665,19 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", - "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", + "version": "3.973.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.23.tgz", + "integrity": "sha512-aoJncvD1XvloZ9JLnKqTRL9dBy+Szkryoag9VT+V1TqsuUgIxV9cnBVM/hrDi2vE8bDqLiDR8nirdRcCdtJu0w==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", - "@aws-sdk/xml-builder": "^3.972.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/xml-builder": "^3.972.15", + "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", @@ -702,12 +702,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.18.tgz", - "integrity": "sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.21.tgz", + "integrity": "sha512-BkAfKq8Bd4shCtec1usNz//urPJF/SZy14qJyxkSaRJQ/Vv1gVh0VZSTmS7aE6aLMELkFV5wHHrS9ZcdG8Kxsg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", @@ -718,20 +718,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.20.tgz", - "integrity": "sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.23.tgz", + "integrity": "sha512-4XZ3+Gu5DY8/n8zQFHBgcKTF7hWQl42G6CY9xfXVo2d25FM/lYkpmuzhYopYoPL1ITWkJ2OSBQfYEu5JRfHOhA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.19", + "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" }, "engines": { @@ -739,19 +739,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.20.tgz", - "integrity": "sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.23.tgz", + "integrity": "sha512-PZLSmU0JFpNCDFReidBezsgL5ji9jOBry8CnZdw4Jj6d0K2z3Ftnp44NXgADqYx5BLMu/ZHujfeJReaDoV+IwQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-env": "^3.972.18", - "@aws-sdk/credential-provider-http": "^3.972.20", - "@aws-sdk/credential-provider-login": "^3.972.20", - "@aws-sdk/credential-provider-process": "^3.972.18", - "@aws-sdk/credential-provider-sso": "^3.972.20", - "@aws-sdk/credential-provider-web-identity": "^3.972.20", - "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/credential-provider-env": "^3.972.21", + "@aws-sdk/credential-provider-http": "^3.972.23", + "@aws-sdk/credential-provider-login": "^3.972.23", + "@aws-sdk/credential-provider-process": "^3.972.21", + "@aws-sdk/credential-provider-sso": "^3.972.23", + "@aws-sdk/credential-provider-web-identity": "^3.972.23", + "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", @@ -764,13 +764,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", - "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.23.tgz", + "integrity": "sha512-OmE/pSkbMM3dCj1HdOnZ5kXnKK+R/Yz+kbBugraBecp0pGAs21eEURfQRz+1N2gzIHLVyGIP1MEjk/uSrFsngg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", @@ -783,17 +783,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.21.tgz", - "integrity": "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==", + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.24.tgz", + "integrity": "sha512-9Jwi7aps3AfUicJyF5udYadPypPpCwUZ6BSKr/QjRbVCpRVS1wc+1Q6AEZ/qz8J4JraeRd247pSzyMQSIHVebw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.18", - "@aws-sdk/credential-provider-http": "^3.972.20", - "@aws-sdk/credential-provider-ini": "^3.972.20", - "@aws-sdk/credential-provider-process": "^3.972.18", - "@aws-sdk/credential-provider-sso": "^3.972.20", - "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/credential-provider-env": "^3.972.21", + "@aws-sdk/credential-provider-http": "^3.972.23", + "@aws-sdk/credential-provider-ini": "^3.972.23", + "@aws-sdk/credential-provider-process": "^3.972.21", + "@aws-sdk/credential-provider-sso": "^3.972.23", + "@aws-sdk/credential-provider-web-identity": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", @@ -806,12 +806,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.18.tgz", - "integrity": "sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.21.tgz", + "integrity": "sha512-nRxbeOJ1E1gVA0lNQezuMVndx+ZcuyaW/RB05pUsznN5BxykSlH6KkZ/7Ca/ubJf3i5N3p0gwNO5zgPSCzj+ww==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -823,14 +823,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.20.tgz", - "integrity": "sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.23.tgz", + "integrity": "sha512-APUccADuYPLL0f2htpM8Z4czabSmHOdo4r41W6lKEZdy++cNJ42Radqy6x4TopENzr3hR6WYMyhiuiqtbf/nAA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/token-providers": "3.1009.0", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/nested-clients": "^3.996.13", + "@aws-sdk/token-providers": "3.1014.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -842,13 +842,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.20.tgz", - "integrity": "sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.23.tgz", + "integrity": "sha512-H5JNqtIwOu/feInmMMWcK0dL5r897ReEn7n2m16Dd0DPD9gA2Hg8Cq4UDzZ/9OzaLh/uqBM6seixz0U6Fi2Eag==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -860,14 +860,14 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.1010.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1010.0.tgz", - "integrity": "sha512-jafLXyFGKrlMz6BaiTpfQQYn2Lro5mKMOzBaprwIs1zY4j+W299cB+vf2wFrUAqw+MAPj2+hHRZTze7nMDdwoQ==", + "version": "3.1014.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1014.0.tgz", + "integrity": "sha512-mM0/YpIEKKQ1hM9n1Q2HhN1ztjk3oNKqFMYP+cZ57FR3XVU4UOw9Npu7gnjfzkNrS7WTDZMe0wCMuDvvtq1Oeg==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/smithy-client": "^4.12.5", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/smithy-client": "^4.12.7", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", @@ -877,7 +877,7 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.1010.0" + "@aws-sdk/client-s3": "^3.1014.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { @@ -914,15 +914,15 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.974.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.0.tgz", - "integrity": "sha512-BmdDjqvnuYaC4SY7ypHLXfCSsGYGUZkjCLSZyUAAYn1YT28vbNMJNDwhlfkvvE+hQHG5RJDlEmYuvBxcB9jX1g==", + "version": "3.974.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.3.tgz", + "integrity": "sha512-fB7FNLH1+VPUs0QL3PLrHW+DD4gKu6daFgWtyq3R0Y0Lx8DLZPvyGAxCZNFBxH+M2xt9KvBJX6USwjuqvitmCQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/crc64-nvme": "^3.972.5", "@aws-sdk/types": "^3.973.6", "@smithy/is-array-buffer": "^4.2.2", @@ -930,7 +930,7 @@ "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.19", + "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -998,23 +998,23 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.20.tgz", - "integrity": "sha512-yhva/xL5H4tWQgsBjwV+RRD0ByCzg0TcByDCLp3GXdn/wlyRNfy8zsswDtCvr1WSKQkSQYlyEzPuWkJG0f5HvQ==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.23.tgz", + "integrity": "sha512-50QgHGPQAb2veqFOmTF1A3GsAklLHZXL47KbY35khIkfbXH5PLvqpEc/gOAEBPj/yFxrlgxz/8mqWcWTNxBkwQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/core": "^3.23.11", + "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.19", + "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1023,13 +1023,13 @@ } }, "node_modules/@aws-sdk/middleware-sdk-sqs": { - "version": "3.972.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.15.tgz", - "integrity": "sha512-X7yt+gJzZEK247nppuUVWS1i83q8zhZdBk1H2b6/qeXNv1ILgw0bQLNbFNG4gJi3P7vZV+PhtPkax0nwXAvRtg==", + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.17.tgz", + "integrity": "sha512-LnzPRRoDXGtlFV2G1p2rsY6fRKrbf6Pvvc21KliSLw3+NmQca2+Aa1QIMRbpQvZYedsSqkGYwxe+qvXwQ2uxDw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", @@ -1054,15 +1054,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", - "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.24.tgz", + "integrity": "sha512-dLTWy6IfAMhNiSEvMr07g/qZ54be6pLqlxVblbF6AzafmmGAzMMj8qMoY9B4+YgT+gY9IcuxZslNh03L6PyMCQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", - "@smithy/core": "^3.23.11", + "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.12", @@ -1073,44 +1073,44 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", - "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", + "version": "3.996.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.13.tgz", + "integrity": "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", + "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.24", + "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", + "@aws-sdk/util-user-agent-node": "^3.973.10", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", + "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -1122,13 +1122,13 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", - "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.9.tgz", + "integrity": "sha512-eQ+dFU05ZRC/lC2XpYlYSPlXtX3VT8sn5toxN2Fv7EXlMoA2p9V7vUBKqHunfD4TRLpxUq8Y8Ol/nCqiv327Ng==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", - "@smithy/config-resolver": "^4.4.11", + "@smithy/config-resolver": "^4.4.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" @@ -1138,17 +1138,17 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.1010.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1010.0.tgz", - "integrity": "sha512-EP+LRZ5+FM9IFZz9vmRSCHAqtaD39Y3TcMbBApn2NgHIogvYxuLx+KUV9rcjGOyJFSDlbSUa0R9s1bM/YmeSCg==", + "version": "3.1014.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1014.0.tgz", + "integrity": "sha512-XEcK50lToSoLPrQztKQhONYQW45613H8oEL00mBUd/+OZgk0+3zJ8kSNDsIJioZ3H7Be+yC3CL6a22dZFIKUXQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/signature-v4-multi-region": "^3.996.8", + "@aws-sdk/signature-v4-multi-region": "^3.996.11", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-format-url": "^3.972.8", - "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-endpoint": "^4.4.27", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, @@ -1157,12 +1157,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.8.tgz", - "integrity": "sha512-n1qYFD+tbqZuyskVaxUE+t10AUz9g3qzDw3Tp6QZDKmqsjfDmZBd4GIk2EKJJNtcCBtE5YiUjDYA+3djFAFBBg==", + "version": "3.996.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.11.tgz", + "integrity": "sha512-SKgZY7x6AloLUXO20FJGnkKJ3a6CXzNDt6PYs2yqoPzgU0xKWcUoGGJGEBTsfM5eihKW42lbwp+sXzACLbSsaA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.20", + "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", @@ -1174,13 +1174,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1009.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1009.0.tgz", - "integrity": "sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==", + "version": "3.1014.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1014.0.tgz", + "integrity": "sha512-gHTHNUoaOGNrSWkl32A7wFsU78jlNTlqMccLu0byUk5CysYYXaxNMIonIVr4YcykC7vgtDS5ABuz83giy6fzJA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/core": "^3.973.23", + "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -1272,12 +1272,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", - "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", + "version": "3.973.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.10.tgz", + "integrity": "sha512-E99zeTscCc+pTMfsvnfi6foPpKmdD1cZfOC7/P8UUrjsoQdg9VEWPRD+xdFduKnfPXwcvby58AlO9jwwF6U96g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", @@ -1297,38 +1297,19 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz", - "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==", + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.15.tgz", + "integrity": "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", - "fast-xml-parser": "5.4.1", + "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", - "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "fast-xml-builder": "^1.0.0", - "strnum": "^2.1.2" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/@aws/lambda-invoke-store": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", @@ -2973,9 +2954,9 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", - "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz", + "integrity": "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.12", @@ -3210,9 +3191,9 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.26", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.26.tgz", - "integrity": "sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==", + "version": "4.4.27", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.27.tgz", + "integrity": "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.12", @@ -3229,15 +3210,15 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.43", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz", - "integrity": "sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==", + "version": "4.4.44", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.44.tgz", + "integrity": "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -3405,13 +3386,13 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.6", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.6.tgz", - "integrity": "sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==", + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.7.tgz", + "integrity": "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.12", - "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", @@ -3512,13 +3493,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.42", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.42.tgz", - "integrity": "sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==", + "version": "4.3.43", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.43.tgz", + "integrity": "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, @@ -3527,16 +3508,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.45", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.45.tgz", - "integrity": "sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==", + "version": "4.2.47", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.47.tgz", + "integrity": "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.11", + "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", + "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, @@ -6607,9 +6588,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.6", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.6.tgz", - "integrity": "sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==", + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", + "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", "funding": [ { "type": "github", @@ -6619,8 +6600,8 @@ "license": "MIT", "dependencies": { "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.1.3", - "strnum": "^2.1.2" + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -7707,9 +7688,9 @@ } }, "node_modules/ioredis": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz", - "integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", "license": "MIT", "dependencies": { "@ioredis/commands": "1.5.1", @@ -8808,9 +8789,9 @@ } }, "node_modules/moment-timezone": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.0.tgz", - "integrity": "sha512-ldA5lRNm3iJCWZcBCab4pnNL3HSZYXVb/3TYr75/1WCTWYuTqYUb5f/S384pncYjJ88lbO8Z4uPDvmoluHJc8Q==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.1.tgz", + "integrity": "sha512-1B9lmAhB9D9/sHaPC1N7wLFEVUoFldxOpOO96lOD1PvJ43vCd0ozDPbu0FEL3++VvawOlDkq8YD373tJmP5JHw==", "license": "MIT", "dependencies": { "moment": "^2.29.4" @@ -9383,9 +9364,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", - "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", + "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", "funding": [ { "type": "github", @@ -10964,9 +10945,9 @@ } }, "node_modules/strnum": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", - "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.1.tgz", + "integrity": "sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==", "funding": [ { "type": "github", diff --git a/package.json b/package.json index 307172f1a..622463441 100644 --- a/package.json +++ b/package.json @@ -18,16 +18,16 @@ "job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js" }, "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.1010.0", - "@aws-sdk/client-elasticache": "^3.1010.0", - "@aws-sdk/client-s3": "^3.1010.0", - "@aws-sdk/client-secrets-manager": "^3.1010.0", - "@aws-sdk/client-ses": "^3.1010.0", - "@aws-sdk/client-sqs": "^3.1010.0", - "@aws-sdk/client-textract": "^3.1010.0", - "@aws-sdk/credential-provider-node": "^3.972.21", - "@aws-sdk/lib-storage": "^3.1010.0", - "@aws-sdk/s3-request-presigner": "^3.1010.0", + "@aws-sdk/client-cloudwatch-logs": "^3.1014.0", + "@aws-sdk/client-elasticache": "^3.1014.0", + "@aws-sdk/client-s3": "^3.1014.0", + "@aws-sdk/client-secrets-manager": "^3.1014.0", + "@aws-sdk/client-ses": "^3.1014.0", + "@aws-sdk/client-sqs": "^3.1014.0", + "@aws-sdk/client-textract": "^3.1014.0", + "@aws-sdk/credential-provider-node": "^3.972.24", + "@aws-sdk/lib-storage": "^3.1014.0", + "@aws-sdk/s3-request-presigner": "^3.1014.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", @@ -46,19 +46,19 @@ "dinero.js": "^1.9.1", "dotenv": "^17.3.1", "express": "^4.21.1", - "fast-xml-parser": "^5.5.6", + "fast-xml-parser": "^5.5.8", "firebase-admin": "^13.7.0", "fuse.js": "^7.1.0", "graphql": "^16.13.1", "graphql-request": "^6.1.0", "intuit-oauth": "^4.2.2", - "ioredis": "^5.10.0", + "ioredis": "^5.10.1", "json-2-csv": "^5.5.10", "jsonwebtoken": "^9.0.3", "juice": "^11.1.1", "lodash": "^4.17.23", "moment": "^2.30.1", - "moment-timezone": "^0.6.0", + "moment-timezone": "^0.6.1", "multer": "^2.1.1", "mustache": "^4.2.0", "node-persist": "^4.0.4", From b5997d0b8f70b4c91dbe8eee8fd1e76670f4260f Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 23 Mar 2026 13:00:51 -0400 Subject: [PATCH 19/56] feature/IO-3587-Commision-Cut-Clean - Cleanup --- client/package-lock.json | 265 ++++++++++++++++++++------------------- client/package.json | 16 +-- client/vite.config.js | 8 +- server.js | 4 +- 4 files changed, 152 insertions(+), 141 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 31b8bad67..e9c5f52b6 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.1", "hasInstallScript": true, "dependencies": { - "@amplitude/analytics-browser": "^2.36.8", + "@amplitude/analytics-browser": "^2.37.0", "@ant-design/pro-layout": "^7.22.6", "@apollo/client": "^4.1.6", "@dnd-kit/core": "^6.3.1", @@ -44,7 +44,7 @@ "exifr": "^7.1.3", "graphql": "^16.13.1", "graphql-ws": "^6.0.7", - "i18next": "^25.8.20", + "i18next": "^25.10.5", "i18next-browser-languagedetector": "^8.2.1", "immutability-helper": "^3.1.1", "libphonenumber-js": "^1.12.40", @@ -55,7 +55,7 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.71", - "posthog-js": "^1.363.1", + "posthog-js": "^1.363.2", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", @@ -66,16 +66,16 @@ "react-dom": "^19.2.4", "react-grid-gallery": "^1.0.1", "react-grid-layout": "^2.2.2", - "react-i18next": "^16.5.8", + "react-i18next": "^16.6.2", "react-icons": "^5.6.0", "react-image-lightbox": "^5.1.4", "react-markdown": "^10.1.0", - "react-number-format": "^5.4.3", + "react-number-format": "^5.4.5", "react-popopo": "^2.1.9", "react-product-fruits": "^2.2.62", "react-redux": "^9.2.0", "react-resizable": "^3.1.3", - "react-router-dom": "^7.13.1", + "react-router-dom": "^7.13.2", "react-sticky": "^6.0.3", "react-virtuoso": "^4.18.3", "recharts": "^3.8.0", @@ -96,7 +96,7 @@ "@ant-design/icons": "^6.1.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.28.5", - "@dotenvx/dotenvx": "^1.57.0", + "@dotenvx/dotenvx": "^1.57.2", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", "@eslint/js": "^9.39.2", @@ -114,7 +114,7 @@ "eslint-plugin-react-compiler": "^19.1.0-rc.2", "globals": "^17.4.0", "jsdom": "^28.1.0", - "memfs": "^4.56.11", + "memfs": "^4.57.1", "os-browserify": "^0.3.0", "playwright": "^1.58.2", "react-error-overlay": "^6.1.0", @@ -151,17 +151,18 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-browser": { - "version": "2.36.8", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.36.8.tgz", - "integrity": "sha512-c0+wsu/qxTwc0w1JPVNzl22djW8YdYqxXiYYJ457VHbgUB/BT335ORMyunIlg8xwJCOsrlhNEj+bJCBp11wT+g==", + "version": "2.37.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.37.0.tgz", + "integrity": "sha512-/BWDneHRfq6+9bcPQC09Ep79SEj7aRJLZ1jJrPHtxA9KZJUz2au2COlJc1ReCaNzCcrA1xXv/MQ0Fv7TwoBglg==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.42.0", - "@amplitude/plugin-autocapture-browser": "1.24.0", - "@amplitude/plugin-network-capture-browser": "1.9.8", - "@amplitude/plugin-page-url-enrichment-browser": "0.6.12", - "@amplitude/plugin-page-view-tracking-browser": "2.9.0", - "@amplitude/plugin-web-vitals-browser": "1.1.23", + "@amplitude/analytics-core": "2.43.0", + "@amplitude/plugin-autocapture-browser": "1.24.1", + "@amplitude/plugin-custom-enrichment-browser": "0.1.0", + "@amplitude/plugin-network-capture-browser": "1.9.9", + "@amplitude/plugin-page-url-enrichment-browser": "0.7.0", + "@amplitude/plugin-page-view-tracking-browser": "2.9.1", + "@amplitude/plugin-web-vitals-browser": "1.1.24", "tslib": "^2.4.1" } }, @@ -172,9 +173,9 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-core": { - "version": "2.42.0", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.42.0.tgz", - "integrity": "sha512-LupBwVGlWhYb6OHSqifh8Jm9rDRNDdCZuqpjv11OmaaIW2rmPbu5JQcKmJoeqolb4+BmdkPAm/Vz7VNjlJAcqA==", + "version": "2.43.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.43.0.tgz", + "integrity": "sha512-rcDqi4cmI9Ro7hN5wjAuTm92IdN2i0lhIDAj+JOd9BP3SRMrhhiw2lzcScj3owig8CiV9X7EHPTuZe6XCTfIgQ==", "license": "MIT", "dependencies": { "@amplitude/analytics-connector": "^1.6.4", @@ -185,52 +186,62 @@ } }, "node_modules/@amplitude/plugin-autocapture-browser": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.24.0.tgz", - "integrity": "sha512-H9CSIj7OM7mPOpCATtCtpXX2iKQFA11ALkRgnTfyx+75BQ9YF3622ncg5kI9kO+RW87gPGQL5l7A5Er+q1tNyA==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.24.1.tgz", + "integrity": "sha512-cvjOFew2MFNBDTbk3+H7WNi3D0Jdp476m6faCaVhY99M5zqRCHDMRS7dC4HczvL9zYXlAcW9jAWucwES2m3TiQ==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.42.0", + "@amplitude/analytics-core": "2.43.0", + "tslib": "^2.4.1" + } + }, + "node_modules/@amplitude/plugin-custom-enrichment-browser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-custom-enrichment-browser/-/plugin-custom-enrichment-browser-0.1.0.tgz", + "integrity": "sha512-y3VmqZvCP1Z3jNgo/mtKVHON9L0P2SyqkMmUsbbFuLu1+TKIkicotnVq/lzlLU1TrW68mkInOM+We8JngasZBA==", + "license": "MIT", + "dependencies": { + "@amplitude/analytics-core": "2.43.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-network-capture-browser": { - "version": "1.9.8", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.9.8.tgz", - "integrity": "sha512-zin6jYMdwveOgYHiY7yCfo4/j/dCjlP/VK5AD/uTYiRewiDMPVm077WTqHgaswZGgxT7DL0BxdIrJ2l27S1daw==", + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.9.9.tgz", + "integrity": "sha512-SJIOQN04Mk9vCsnVd9QRcIvkMV7XSGZIKfbaKNQY5O3ueV33Kc8opm7YjPg2sWcxdzTcJijbCkOI0wCwOaRolg==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.42.0", + "@amplitude/analytics-core": "2.43.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-page-url-enrichment-browser": { - "version": "0.6.12", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.6.12.tgz", - "integrity": "sha512-pyYk+nfHvDfr+ovw/BsK8GpwvyLWiBq01TWjAXef+gN+wa5CAIg/mHvAAdT+x90uRbzKV20TyaKhYd4tw65MKA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.7.0.tgz", + "integrity": "sha512-MkM7TDq24k7ilUDNZISqjDSkVfmDJxWcnUagwYEXjLILhno5hGm7wdgFvVXXzKlZQHEogBxkbnq7wZXS9/YsMw==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.42.0", + "@amplitude/analytics-core": "2.43.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-page-view-tracking-browser": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.9.0.tgz", - "integrity": "sha512-PdG6ogdJA/XldHh1IgnsWTyCGseiHjBUXxMrMjY+MAfIyjtQZcGzNaaj237BG3ArsUa2YHxEpIGz5Uj68bQAlw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.9.1.tgz", + "integrity": "sha512-jkxz2lkJDAfsjj7mpbPUZx9N3qJssC3uYyv8Nk73z+p+v0wjBikWdOoKuNQkcuP09701zRdXp9ziU8+qwkGusw==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.42.0", + "@amplitude/analytics-core": "2.43.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-web-vitals-browser": { - "version": "1.1.23", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.23.tgz", - "integrity": "sha512-fKqprLoOd08yxYQ0Ugl9WIRJPlZVk5t/mGxxqrWvr5umlW2iuxBDOZP+AlBdPs4ZFze767baAS/KDibZPfrffg==", + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.24.tgz", + "integrity": "sha512-7AaytUK78RKdyDsblYJCKYan1lQi3Qzsp1WHItHJ+RSXPccmi4mCcvNtx0e8T9LmNJlUnsmYeEGR/6FaWvyvFg==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.42.0", + "@amplitude/analytics-core": "2.43.0", "tslib": "^2.4.1", "web-vitals": "5.1.0" } @@ -2293,9 +2304,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2577,9 +2588,9 @@ } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.57.0.tgz", - "integrity": "sha512-WsTEcqfHzKmLFZh3jLGd7o4iCkrIupp+qFH2FJUJtQXUh2GcOnLXD00DcrhlO4H8QSmaKnW9lugOEbrdpu25kA==", + "version": "1.57.2", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.57.2.tgz", + "integrity": "sha512-lv9+UZPnl/KOvShepevLWm3+/wc1It5kgO5Q580evnvOFMZcgKVEYFwxlL7Ohl9my1yjTsWo28N3PJYUEO8wFQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3931,14 +3942,14 @@ } }, "node_modules/@jsonjoy.com/fs-core": { - "version": "4.56.11", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.56.11.tgz", - "integrity": "sha512-wThHjzUp01ImIjfCwhs+UnFkeGPFAymwLEkOtenHewaKe2pTP12p6r1UuwikA9NEvNf9Vlck92r8fb8n/MWM5w==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.1.tgz", + "integrity": "sha512-YrEi/ZPmgc+GfdO0esBF04qv8boK9Dg9WpRQw/+vM8Qt3nnVIJWIa8HwZ/LXVZ0DB11XUROM8El/7yYTJX+WtA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.56.11", - "@jsonjoy.com/fs-node-utils": "4.56.11", + "@jsonjoy.com/fs-node-builtins": "4.57.1", + "@jsonjoy.com/fs-node-utils": "4.57.1", "thingies": "^2.5.0" }, "engines": { @@ -3953,15 +3964,15 @@ } }, "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.56.11", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.56.11.tgz", - "integrity": "sha512-ZYlF3XbMayyp97xEN8ZvYutU99PCHjM64mMZvnCseXkCJXJDVLAwlF8Q/7q/xiWQRsv3pQBj1WXHd9eEyYcaCQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.1.tgz", + "integrity": "sha512-ooEPvSW/HQDivPDPZMibHGKZf/QS4WRir1czGZmXmp3MsQqLECZEpN0JobrD8iV9BzsuwdIv+PxtWX9WpPLsIA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.56.11", - "@jsonjoy.com/fs-node-builtins": "4.56.11", - "@jsonjoy.com/fs-node-utils": "4.56.11", + "@jsonjoy.com/fs-core": "4.57.1", + "@jsonjoy.com/fs-node-builtins": "4.57.1", + "@jsonjoy.com/fs-node-utils": "4.57.1", "thingies": "^2.5.0" }, "engines": { @@ -3976,17 +3987,17 @@ } }, "node_modules/@jsonjoy.com/fs-node": { - "version": "4.56.11", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.56.11.tgz", - "integrity": "sha512-D65YrnP6wRuZyEWoSFnBJSr5zARVpVBGctnhie4rCsMuGXNzX7IHKaOt85/Aj7SSoG1N2+/xlNjWmkLvZ2H3Tg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.1.tgz", + "integrity": "sha512-3YaKhP8gXEKN+2O49GLNfNb5l2gbnCFHyAaybbA2JkkbQP3dpdef7WcUaHAulg/c5Dg4VncHsA3NWAUSZMR5KQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.56.11", - "@jsonjoy.com/fs-node-builtins": "4.56.11", - "@jsonjoy.com/fs-node-utils": "4.56.11", - "@jsonjoy.com/fs-print": "4.56.11", - "@jsonjoy.com/fs-snapshot": "4.56.11", + "@jsonjoy.com/fs-core": "4.57.1", + "@jsonjoy.com/fs-node-builtins": "4.57.1", + "@jsonjoy.com/fs-node-utils": "4.57.1", + "@jsonjoy.com/fs-print": "4.57.1", + "@jsonjoy.com/fs-snapshot": "4.57.1", "glob-to-regex.js": "^1.0.0", "thingies": "^2.5.0" }, @@ -4002,9 +4013,9 @@ } }, "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.56.11", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.56.11.tgz", - "integrity": "sha512-CNmt3a0zMCIhniFLXtzPWuUxXFU+U+2VyQiIrgt/rRVeEJNrMQUABaRbVxR0Ouw1LyR9RjaEkPM6nYpED+y43A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.1.tgz", + "integrity": "sha512-XHkFKQ5GSH3uxm8c3ZYXVrexGdscpWKIcMWKFQpMpMJc8gA3AwOMBJXJlgpdJqmrhPyQXxaY9nbkNeYpacC0Og==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4019,15 +4030,15 @@ } }, "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.56.11", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.56.11.tgz", - "integrity": "sha512-5OzGdvJDgZVo+xXWEYo72u81zpOWlxlbG4d4nL+hSiW+LKlua/dldNgPrpWxtvhgyntmdFQad2UTxFyGjJAGhA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.1.tgz", + "integrity": "sha512-pqGHyWWzNck4jRfaGV39hkqpY5QjRUQ/nRbNT7FYbBa0xf4bDG+TE1Gt2KWZrSkrkZZDE3qZUjYMbjwSliX6pg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-fsa": "4.56.11", - "@jsonjoy.com/fs-node-builtins": "4.56.11", - "@jsonjoy.com/fs-node-utils": "4.56.11" + "@jsonjoy.com/fs-fsa": "4.57.1", + "@jsonjoy.com/fs-node-builtins": "4.57.1", + "@jsonjoy.com/fs-node-utils": "4.57.1" }, "engines": { "node": ">=10.0" @@ -4041,13 +4052,13 @@ } }, "node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.56.11", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.56.11.tgz", - "integrity": "sha512-JADOZFDA3wRfsuxkT0+MYc4F9hJO2PYDaY66kRTG6NqGX3+bqmKu66YFYAbII/tEmQWPZeHoClUB23rtQM9UPg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.1.tgz", + "integrity": "sha512-vp+7ZzIB8v43G+GLXTS4oDUSQmhAsRz532QmmWBbdYA20s465JvwhkSFvX9cVTqRRAQg+vZ7zWDaIEh0lFe2gw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.56.11" + "@jsonjoy.com/fs-node-builtins": "4.57.1" }, "engines": { "node": ">=10.0" @@ -4061,13 +4072,13 @@ } }, "node_modules/@jsonjoy.com/fs-print": { - "version": "4.56.11", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.56.11.tgz", - "integrity": "sha512-rnaKRgCRIn8JGTjxhS0JPE38YM3Pj/H7SW4/tglhIPbfKEkky7dpPayNKV2qy25SZSL15oFVgH/62dMZ/z7cyA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.1.tgz", + "integrity": "sha512-Ynct7ZJmfk6qoXDOKfpovNA36ITUx8rChLmRQtW08J73VOiuNsU8PB6d/Xs7fxJC2ohWR3a5AqyjmLojfrw5yw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.56.11", + "@jsonjoy.com/fs-node-utils": "4.57.1", "tree-dump": "^1.1.0" }, "engines": { @@ -4082,14 +4093,14 @@ } }, "node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.56.11", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.56.11.tgz", - "integrity": "sha512-IIldPX+cIRQuUol9fQzSS3hqyECxVpYMJQMqdU3dCKZFRzEl1rkIkw4P6y7Oh493sI7YdxZlKr/yWdzEWZ1wGQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.1.tgz", + "integrity": "sha512-/oG8xBNFMbDXTq9J7vepSA1kerS5vpgd3p5QZSPd+nX59uwodGJftI51gDYyHRpP57P3WCQf7LHtBYPqwUg2Bg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.56.11", + "@jsonjoy.com/fs-node-utils": "4.57.1", "@jsonjoy.com/json-pack": "^17.65.0", "@jsonjoy.com/util": "^17.65.0" }, @@ -4862,9 +4873,9 @@ } }, "node_modules/@posthog/types": { - "version": "1.363.1", - "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.363.1.tgz", - "integrity": "sha512-bFYk5XHgYEfVhQU0AwkG9MbMqq9QRbKDDJxOtYWGJ6Uw+/nLRNs/ZydXy3aMt0ldIdkNzZq+qaJ/p2Jg0+mP8g==", + "version": "1.363.2", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.363.2.tgz", + "integrity": "sha512-UcUwHEd2LXxWq4bW/I4TbwYcA+BHO/cSuHcNpGXjRCp76eJk1eOuQnm/a3MrfHtbt2X11CQu+eWpqiSgcv+X6A==", "license": "MIT" }, "node_modules/@protobufjs/aspromise": { @@ -11773,26 +11784,26 @@ } }, "node_modules/i18next": { - "version": "25.8.20", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.20.tgz", - "integrity": "sha512-xjo9+lbX/P1tQt3xpO2rfJiBppNfUnNIPKgCvNsTKsvTOCro1Qr/geXVg1N47j5ScOSaXAPq8ET93raK3Rr06A==", + "version": "25.10.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.5.tgz", + "integrity": "sha512-jRnF7eRNsdcnh7AASSgaU3lj/8lJZuHkfsouetnLEDH0xxE1vVi7qhiJ9RhdSPUyzg4ltb7P7aXsFlTk9sxL2w==", "funding": [ { "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" + "url": "https://www.locize.com/i18next" }, { "type": "individual", "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" } ], "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.6" + "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5" @@ -13527,20 +13538,20 @@ "license": "CC0-1.0" }, "node_modules/memfs": { - "version": "4.56.11", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.11.tgz", - "integrity": "sha512-/GodtwVeKVIHZKLUSr2ZdOxKBC5hHki4JNCU22DoCGPEHr5o2PD5U721zvESKyWwCfTfavFl9WZYgA13OAYK0g==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.1.tgz", + "integrity": "sha512-WvzrWPwMQT+PtbX2Et64R4qXKK0fj/8pO85MrUCzymX3twwCiJCdvntW3HdhG1teLJcHDDLIKx5+c3HckWYZtQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.56.11", - "@jsonjoy.com/fs-fsa": "4.56.11", - "@jsonjoy.com/fs-node": "4.56.11", - "@jsonjoy.com/fs-node-builtins": "4.56.11", - "@jsonjoy.com/fs-node-to-fsa": "4.56.11", - "@jsonjoy.com/fs-node-utils": "4.56.11", - "@jsonjoy.com/fs-print": "4.56.11", - "@jsonjoy.com/fs-snapshot": "4.56.11", + "@jsonjoy.com/fs-core": "4.57.1", + "@jsonjoy.com/fs-fsa": "4.57.1", + "@jsonjoy.com/fs-node": "4.57.1", + "@jsonjoy.com/fs-node-builtins": "4.57.1", + "@jsonjoy.com/fs-node-to-fsa": "4.57.1", + "@jsonjoy.com/fs-node-utils": "4.57.1", + "@jsonjoy.com/fs-print": "4.57.1", + "@jsonjoy.com/fs-snapshot": "4.57.1", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", @@ -15059,9 +15070,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.363.1", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.363.1.tgz", - "integrity": "sha512-iaDtRxCs/FiB+RXe83uo7RZXgpLlyB6qFoNHl3bNMgRCgrPI2nkzx2m9Va1l30HHl/zA1kPOXSy2/tZC5Ql5kg==", + "version": "1.363.2", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.363.2.tgz", + "integrity": "sha512-4ZEWMrymlFzjgDSmh25VeJQT//2XUFbfKqEPDNUW4dxcqWiVMo1+gJFy5YhJgVYS46OAXLbMcJgmuZBCnDIgVg==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@opentelemetry/api": "^1.9.0", @@ -15070,7 +15081,7 @@ "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.24.1", - "@posthog/types": "1.363.1", + "@posthog/types": "1.363.2", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", @@ -15534,12 +15545,12 @@ "license": "MIT" }, "node_modules/react-i18next": { - "version": "16.5.8", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.8.tgz", - "integrity": "sha512-2ABeHHlakxVY+LSirD+OiERxFL6+zip0PaHo979bgwzeHg27Sqc82xxXWIrSFmfWX0ZkrvXMHwhsi/NGUf5VQg==", + "version": "16.6.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.6.2.tgz", + "integrity": "sha512-/S/GPzElTqEi5o2kzd0/O2627hPDmE6OGhJCCwCfUaQ3syyu+kaYH8/PYFtZeWc25NzfxTN/2fD1QjvrTgrFfA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4", + "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, @@ -15640,9 +15651,9 @@ } }, "node_modules/react-number-format": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz", - "integrity": "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.5.tgz", + "integrity": "sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw==", "license": "MIT", "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -15748,9 +15759,9 @@ } }, "node_modules/react-router": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", - "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", + "integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -15770,12 +15781,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", - "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz", + "integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==", "license": "MIT", "dependencies": { - "react-router": "7.13.1" + "react-router": "7.13.2" }, "engines": { "node": ">=20.0.0" diff --git a/client/package.json b/client/package.json index 890e14563..4f03ad75d 100644 --- a/client/package.json +++ b/client/package.json @@ -8,7 +8,7 @@ "private": true, "proxy": "http://localhost:4000", "dependencies": { - "@amplitude/analytics-browser": "^2.36.8", + "@amplitude/analytics-browser": "^2.37.0", "@ant-design/pro-layout": "^7.22.6", "@apollo/client": "^4.1.6", "@dnd-kit/core": "^6.3.1", @@ -43,7 +43,7 @@ "exifr": "^7.1.3", "graphql": "^16.13.1", "graphql-ws": "^6.0.7", - "i18next": "^25.8.20", + "i18next": "^25.10.5", "i18next-browser-languagedetector": "^8.2.1", "immutability-helper": "^3.1.1", "libphonenumber-js": "^1.12.40", @@ -54,7 +54,7 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.71", - "posthog-js": "^1.363.1", + "posthog-js": "^1.363.2", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", @@ -65,16 +65,16 @@ "react-dom": "^19.2.4", "react-grid-gallery": "^1.0.1", "react-grid-layout": "^2.2.2", - "react-i18next": "^16.5.8", + "react-i18next": "^16.6.2", "react-icons": "^5.6.0", "react-image-lightbox": "^5.1.4", "react-markdown": "^10.1.0", - "react-number-format": "^5.4.3", + "react-number-format": "^5.4.5", "react-popopo": "^2.1.9", "react-product-fruits": "^2.2.62", "react-redux": "^9.2.0", "react-resizable": "^3.1.3", - "react-router-dom": "^7.13.1", + "react-router-dom": "^7.13.2", "react-sticky": "^6.0.3", "react-virtuoso": "^4.18.3", "recharts": "^3.8.0", @@ -140,7 +140,7 @@ "@ant-design/icons": "^6.1.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.28.5", - "@dotenvx/dotenvx": "^1.57.0", + "@dotenvx/dotenvx": "^1.57.2", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", "@eslint/js": "^9.39.2", @@ -158,7 +158,7 @@ "eslint-plugin-react-compiler": "^19.1.0-rc.2", "globals": "^17.4.0", "jsdom": "^28.1.0", - "memfs": "^4.56.11", + "memfs": "^4.57.1", "os-browserify": "^0.3.0", "playwright": "^1.58.2", "react-error-overlay": "^6.1.0", diff --git a/client/vite.config.js b/client/vite.config.js index 4886ef13a..55c79e91d 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -173,12 +173,12 @@ export default defineConfig(({ command, mode }) => { open: true, proxy: { "/ws": { - target: "ws://localhost:4000", + target: "http://localhost:4000", secure: false, ws: true }, "/wss": { - target: "ws://localhost:4000", + target: "http://localhost:4000", secure: false, ws: true }, @@ -206,13 +206,13 @@ export default defineConfig(({ command, mode }) => { https: httpsCerts, proxy: { "/ws": { - target: "ws://localhost:4000", + target: "http://localhost:4000", rewriteWsOrigin: true, secure: false, ws: true }, "/wss": { - target: "ws://localhost:4000", + target: "http://localhost:4000", rewriteWsOrigin: true, secure: false, ws: true diff --git a/server.js b/server.js index 90f16f33d..879f95569 100644 --- a/server.js +++ b/server.js @@ -280,8 +280,8 @@ const connectToRedisCluster = async () => { redisCluster.on("node error", (error, node) => { console.dir(error); logger.log(`Redis node error`, "ERROR", "redis", "api", { - host: node.options.host, - port: node.options.port, + host: node?.options?.host, + port: node?.options?.port, message: error.message }); }); From a005f1bb45585e536174c4865e9e4625c1886ff9 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 23 Mar 2026 15:52:49 -0700 Subject: [PATCH 20/56] IO-3609 Bill Cost Calculation Toggle Signed-off-by: Allan Carr --- .../components/bill-form/bill-form.lines.component.jsx | 1 + .../shop-info.responsibilitycenters.component.jsx | 8 ++++++++ client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 5 files changed, 12 insertions(+) diff --git a/client/src/components/bill-form/bill-form.lines.component.jsx b/client/src/components/bill-form/bill-form.lines.component.jsx index b9c41074c..541608ec4 100644 --- a/client/src/components/bill-form/bill-form.lines.component.jsx +++ b/client/src/components/bill-form/bill-form.lines.component.jsx @@ -96,6 +96,7 @@ export function BillEnterModalLinesComponent({ // Only fill actual_cost when the user forward-tabs out of Retail (actual_price) const autofillActualCost = (index) => { + if (bodyshop.accountingconfig?.disableBillCostCalculation) return; Promise.resolve().then(() => { const retailRaw = form.getFieldValue(["billlines", index, "actual_price"]); const actualRaw = form.getFieldValue(["billlines", index, "actual_cost"]); diff --git a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx index 87013bdd2..3a9588272 100644 --- a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx @@ -342,6 +342,14 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { valuePropName="checked" > + , + + ] : []), diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index a79bf0edd..76e55c8f3 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -339,6 +339,7 @@ "require_actual_delivery_date": "Require Actual Delivery", "templates": "Delivery Templates" }, + "disableBillCostCalculation": "Disable Automatic Bill Cost Calculation", "dms": { "apcontrol": "AP Control Number", "appostingaccount": "AP Posting Account", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index e1061fb2e..683e1d5b8 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -339,6 +339,7 @@ "require_actual_delivery_date": "", "templates": "" }, + "disableBillCostCalculation": "", "dms": { "apcontrol": "", "appostingaccount": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index b4b37cf62..7ca765876 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -339,6 +339,7 @@ "require_actual_delivery_date": "", "templates": "" }, + "disableBillCostCalculation": "", "dms": { "apcontrol": "", "appostingaccount": "", From 85e60dcd6b08d3777ec12a8ca3600ce8132641ad Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 23 Mar 2026 16:00:12 -0700 Subject: [PATCH 21/56] IO-3623 Extend Vendor Discount to Precision 3 Signed-off-by: Allan Carr --- client/src/components/vendors-form/vendors-form.component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/vendors-form/vendors-form.component.jsx b/client/src/components/vendors-form/vendors-form.component.jsx index 547ff9a2a..ddd9b93d8 100644 --- a/client/src/components/vendors-form/vendors-form.component.jsx +++ b/client/src/components/vendors-form/vendors-form.component.jsx @@ -152,7 +152,7 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete {!isPartsEntry && ( <> - + From aa81cddcf182a0e690beadcad961068707336c3a Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 23 Mar 2026 16:46:28 -0700 Subject: [PATCH 22/56] IO-3622 Employee Delete Rate Signed-off-by: Allan Carr --- .../shop-employees/shop-employees-form.component.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/components/shop-employees/shop-employees-form.component.jsx b/client/src/components/shop-employees/shop-employees-form.component.jsx index 59a5b7e9f..e5bfbde23 100644 --- a/client/src/components/shop-employees/shop-employees-form.component.jsx +++ b/client/src/components/shop-employees/shop-employees-form.component.jsx @@ -316,9 +316,8 @@ export function ShopEmployeesFormComponent({ bodyshop }) { Date: Tue, 24 Mar 2026 10:54:42 -0400 Subject: [PATCH 23/56] IO-3624 Refresh shop config list rows and color UX --- .../form-fields-changed-alert.component.jsx | 2 +- .../phone-form-item.component.jsx | 85 +- .../form-list-item-title.utils.js | 30 + .../layout-form-row.component.jsx | 26 +- .../layout-form-row.styles.scss | 23 + .../parts-order-modal.component.jsx | 57 +- .../parts-receive-modal.component.jsx | 54 +- .../parts-email-presets.component.jsx | 66 +- .../parts-locations.component.jsx | 70 +- .../parts-order-comments.component.jsx | 99 +- .../shop-employees-form.component.jsx | 122 +- .../shop-info/shop-info.color.utils.js | 304 ++++ .../shop-info/shop-info.color.utils.test.js | 52 + .../shop-info/shop-info.general.component.jsx | 1396 ++++++++++------- .../shop-info/shop-info.intake.component.jsx | 429 ++--- .../shop-info.laborrates.component.jsx | 47 +- .../shop-info/shop-info.parts-scan.jsx | 280 ++-- ...p-info.responsibilitycenters.component.jsx | 840 ++++++---- .../shop-info.rostatus.component.jsx | 142 +- .../shop-info.scheduling.component.jsx | 337 ++-- .../shop-info.speedprint.component.jsx | 134 +- .../shop-info.task-presets.component.jsx | 314 ++-- 22 files changed, 3100 insertions(+), 1809 deletions(-) create mode 100644 client/src/components/form-list-move-arrows/form-list-item-title.utils.js create mode 100644 client/src/components/shop-info/shop-info.color.utils.js create mode 100644 client/src/components/shop-info/shop-info.color.utils.test.js diff --git a/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx b/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx index 0f265d7db..3254c9f74 100644 --- a/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx +++ b/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx @@ -17,7 +17,7 @@ export default function FormsFieldChanged({ form, skipPrompt }) { const errors = form.getFieldsError().filter((e) => e.errors.length > 0); if (form.isFieldsTouched()) return ( - + ; -} +/** + * Formats a phone number for display purposes. If the input value is a valid phone number, it will be formatted in a + * national format (e.g., (123) 456-7890 for US/CA). If the input is not a valid phone number, it will be returned as-is. + * @param value + * @returns {*} + */ +const formatPhoneDisplayValue = (value) => { + if (!value) return value; + + try { + const parsedPhone = parsePhoneNumber(value, "CA"); + return parsedPhone?.isValid() ? parsedPhone.formatNational() : value; + } catch { + return value; + } +}; + +/** + * Generates a "tel:" URL for a phone number if it's valid. If the input value is a valid phone number, it will return a + * URL in the format "tel:+1234567890". If the input is not a valid phone number, it will attempt to trim whitespace and + * return a "tel:" URL with the raw value, or null if the trimmed value is empty. + * @param value + * @returns {string|null} + */ +const getPhoneActionHref = (value) => { + if (!value) return null; + + try { + const parsedPhone = parsePhoneNumber(value, "CA"); + if (parsedPhone?.isValid()) return `tel:${parsedPhone.number}`; + } catch { + // Fall back to the raw value below. + } + + const trimmedValue = String(value).trim(); + return trimmedValue ? `tel:${trimmedValue}` : null; +}; + +const FormItemPhone = forwardRef(function FormItemPhone( + { formatDisplayOnly = false, showPhoneAction = false, value, onBlur, onFocus, ...props }, + ref +) { + const [isFocused, setIsFocused] = useState(false); + const displayValue = useMemo(() => { + if (!formatDisplayOnly || isFocused) return value; + return formatPhoneDisplayValue(value); + }, [formatDisplayOnly, isFocused, value]); + const phoneActionHref = useMemo(() => (showPhoneAction ? getPhoneActionHref(value) : null), [showPhoneAction, value]); + + const input = ( + { + setIsFocused(true); + onFocus?.(event); + }} + onBlur={(event) => { + setIsFocused(false); + onBlur?.(event); + }} + /> + ); + + if (!showPhoneAction) return input; + + return ( + + {input} + {phoneActionHref ? ( + } > - - history({ - search: `?tab=${search.tab}&subtab=${key}` - }) - } - items={tabItems} - /> +
+ + history({ + search: `?tab=${search.tab}&subtab=${key}` + }) + } + items={tabItems} + /> +
); } diff --git a/client/src/components/shop-info/shop-info.section-navigator.component.jsx b/client/src/components/shop-info/shop-info.section-navigator.component.jsx new file mode 100644 index 000000000..69fd36e85 --- /dev/null +++ b/client/src/components/shop-info/shop-info.section-navigator.component.jsx @@ -0,0 +1,137 @@ +import { Select } from "antd"; +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import "./shop-info.section-navigator.styles.scss"; + +const HIGHLIGHT_CLASS = "shop-info-section-navigator__target--active"; + +export default function ShopInfoSectionNavigator({ tabsRef, activeTabKey }) { + const { t } = useTranslation(); + const targetMapRef = useRef(new Map()); + const highlightedTargetRef = useRef(null); + const [options, setOptions] = useState([]); + const [selectedSection, setSelectedSection] = useState(undefined); + + useEffect(() => { + const tabsContainer = tabsRef.current; + if (!tabsContainer) return undefined; + + let animationFrameId = 0; + + const refreshOptions = () => { + const activePane = tabsContainer.querySelector(".ant-tabs-tabpane-active"); + if (!activePane) { + targetMapRef.current = new Map(); + setOptions([]); + return; + } + + const nextTargetMap = new Map(); + const nextOptions = Array.from(activePane.querySelectorAll(".imex-form-row")) + .filter((card) => { + const titleNode = getOwnCardTitleNode(card); + if (!titleNode?.textContent?.trim()) return false; + + const ancestorCard = card.parentElement?.closest(".imex-form-row"); + return !ancestorCard || !activePane.contains(ancestorCard); + }) + .map((card, index) => { + const label = getOwnCardTitleNode(card)?.textContent?.trim(); + const value = `${activeTabKey}-shop-info-section-${index}`; + + nextTargetMap.set(value, card); + + return { + label, + value + }; + }); + + targetMapRef.current = nextTargetMap; + setOptions((currentOptions) => (areOptionsEqual(currentOptions, nextOptions) ? currentOptions : nextOptions)); + }; + + const scheduleRefresh = () => { + cancelAnimationFrame(animationFrameId); + animationFrameId = requestAnimationFrame(refreshOptions); + }; + + scheduleRefresh(); + + const observer = new MutationObserver(scheduleRefresh); + observer.observe(tabsContainer, { + childList: true, + subtree: true, + characterData: true, + attributes: true, + attributeFilter: ["class"] + }); + + return () => { + cancelAnimationFrame(animationFrameId); + observer.disconnect(); + }; + }, [activeTabKey, tabsRef]); + + useEffect(() => { + clearHighlightedTarget(highlightedTargetRef); + setSelectedSection(undefined); + }, [activeTabKey]); + + const handleSectionChange = (value) => { + setSelectedSection(value); + + clearHighlightedTarget(highlightedTargetRef); + if (!value) return; + + const target = targetMapRef.current.get(value); + if (target) { + target.classList.add(HIGHLIGHT_CLASS); + highlightedTargetRef.current = target; + target.scrollIntoView({ + behavior: "smooth", + block: "start" + }); + } + + window.setTimeout(() => { + setSelectedSection(undefined); + }, 0); + }; + + return ( +
+ - - - - - - - - - - - - - - {bodyshop.pbs_serialnumber && ( + +
+ + {bodyshop.rr_dealerid && ( + + {form.getFieldValue("rr_dealerid")} + + )} + {bodyshop.cdk_dealerid && ( + + {form.getFieldValue("cdk_dealerid")} + + )} + {bodyshop.pbs_serialnumber && ( + + {form.getFieldValue("pbs_serialnumber")} + + )} + + - + - )} - {bodyshop.pbs_serialnumber && ( - + - )} - {bodyshop.pbs_serialnumber && ( - + - )} - {bodyshop.pbs_serialnumber && ( - - )} - {bodyshop.pbs_serialnumber && ( - - + + )} + {bodyshop.pbs_serialnumber && ( + + + + + + + + - - - - - - - { - remove(field.name); - }} - /> - + ))} + + - ))} - - - -
- ); - }} - -
- - )} - +
+ ); + }} + +
+ + )} + + )} {HasFeatureAccess({ featureName: "export", bodyshop }) && ( <> @@ -919,1255 +923,1275 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { {(fields, { add, remove }) => { return (
- {fields.map((field, index) => ( - -
- 0 ? false : true}> - - - - - - - { - remove(field.name); - }} - /> + {fields.map((field, index) => { + const dmsDefault = form.getFieldValue([ + "md_responsibility_centers", + "dms_defaults", + field.name + ]) || { name: undefined }; + + return ( + + } + onClick={() => { + remove(field.name); + }} + /> + } + > +
+ + + + + + + + + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-ats`} + name={[field.name, "costs", "ATS"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-LAB`} + name={[field.name, "costs", "LAB"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-LAE`} + name={[field.name, "costs", "LAE"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-LAG`} + name={[field.name, "costs", "LAG"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-LAR`} + name={[field.name, "costs", "LAR"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-LAU`} + name={[field.name, "costs", "LAU"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-LA2`} + name={[field.name, "costs", "LA2"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-LA4`} + name={[field.name, "costs", "LA4"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-PAC`} + name={[field.name, "costs", "PAC"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-PAL`} + name={[field.name, "costs", "PAL"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-PAN`} + name={[field.name, "costs", "PAN"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-PAP`} + name={[field.name, "costs", "PAP"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-PAS`} + name={[field.name, "costs", "PAS"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-TOW`} + name={[field.name, "costs", "TOW"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}costs-MASH`} + name={[field.name, "costs", "MASH"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-LAA`} + name={[field.name, "profits", "LAA"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-LAD`} + name={[field.name, "profits", "LAD"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-LAF`} + name={[field.name, "profits", "LAF"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-LAM`} + name={[field.name, "profits", "LAM"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-LAS`} + name={[field.name, "profits", "LAS"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-LA1`} + name={[field.name, "profits", "LA1"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-LA3`} + name={[field.name, "profits", "LA3"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-PAA`} + name={[field.name, "profits", "PAA"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-PAG`} + name={[field.name, "profits", "PAG"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-PAM`} + name={[field.name, "profits", "PAM"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-PAO`} + name={[field.name, "profits", "PAO"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-PAR`} + name={[field.name, "profits", "PAR"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-PASL`} + name={[field.name, "profits", "PASL"]} + > + ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); + } + }) + ]} + key={`${index}profits-MAPA`} + name={[field.name, "profits", "MAPA"]} + > + ({ value: item, label: item }))} + /> + + +
- - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-ats`} - name={[field.name, "costs", "ATS"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAB`} - name={[field.name, "costs", "LAB"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAE`} - name={[field.name, "costs", "LAE"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAG`} - name={[field.name, "costs", "LAG"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAR`} - name={[field.name, "costs", "LAR"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAU`} - name={[field.name, "costs", "LAU"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LA2`} - name={[field.name, "costs", "LA2"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LA4`} - name={[field.name, "costs", "LA4"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAC`} - name={[field.name, "costs", "PAC"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAL`} - name={[field.name, "costs", "PAL"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAN`} - name={[field.name, "costs", "PAN"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAP`} - name={[field.name, "costs", "PAP"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAS`} - name={[field.name, "costs", "PAS"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-TOW`} - name={[field.name, "costs", "TOW"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-MASH`} - name={[field.name, "costs", "MASH"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAA`} - name={[field.name, "profits", "LAA"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAD`} - name={[field.name, "profits", "LAD"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAF`} - name={[field.name, "profits", "LAF"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAM`} - name={[field.name, "profits", "LAM"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAS`} - name={[field.name, "profits", "LAS"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LA1`} - name={[field.name, "profits", "LA1"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LA3`} - name={[field.name, "profits", "LA3"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAA`} - name={[field.name, "profits", "PAA"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAG`} - name={[field.name, "profits", "PAG"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAM`} - name={[field.name, "profits", "PAM"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAO`} - name={[field.name, "profits", "PAO"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAR`} - name={[field.name, "profits", "PAR"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PASL`} - name={[field.name, "profits", "PASL"]} - > - ({ value: item, label: item }))} - /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-MAPA`} - name={[field.name, "profits", "MAPA"]} - > - ({ value: item, label: item }))} - /> - - -
-
- ))} + + ); + })}
+ } + wrapTitle extra={ - - } - key={`${index}color`} - name={[field.name, "color"]} - > - -
+ /> + + +
+ } + {...schedulingBucketSurfaceStyles} + > +
+
+ + + + + + + + + + + +
+
+ + + +
+
); diff --git a/client/src/components/shop-info/shop-info.scheduling.styles.scss b/client/src/components/shop-info/shop-info.scheduling.styles.scss new file mode 100644 index 000000000..b04a92e15 --- /dev/null +++ b/client/src/components/shop-info/shop-info.scheduling.styles.scss @@ -0,0 +1,58 @@ +.shop-info-scheduling__bucket-card-body { + display: flex; + gap: 12px; + align-items: stretch; +} + +.shop-info-scheduling__bucket-card-fields { + flex: 1 1 0; + min-width: 0; + display: grid; + grid-template-columns: repeat(3, minmax(92px, 1fr)); + gap: 0 12px; +} + +.shop-info-scheduling__bucket-card-fields .ant-form-item { + margin-bottom: 10px; +} + +.shop-info-scheduling__bucket-card-color { + flex: 0 0 360px; + min-width: 360px; + max-width: 360px; + display: flex; + align-items: stretch; +} + +.shop-info-scheduling__bucket-card-color .ant-form-item { + margin-bottom: 0; + width: 100%; +} + +.shop-info-scheduling__bucket-card-color .ant-form-item-control, +.shop-info-scheduling__bucket-card-color .ant-form-item-control-input, +.shop-info-scheduling__bucket-card-color .ant-form-item-control-input-content { + height: 100%; +} + +@media (max-width: 1199px) { + .shop-info-scheduling__bucket-card-body { + flex-direction: column; + } + + .shop-info-scheduling__bucket-card-fields { + grid-template-columns: repeat(2, minmax(120px, 1fr)); + } + + .shop-info-scheduling__bucket-card-color { + flex-basis: auto; + min-width: 0; + max-width: none; + } +} + +@media (max-width: 575px) { + .shop-info-scheduling__bucket-card-fields { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/client/src/components/shop-info/shop-info.speedprint.component.jsx b/client/src/components/shop-info/shop-info.speedprint.component.jsx index 4b9f919a8..062bea41e 100644 --- a/client/src/components/shop-info/shop-info.speedprint.component.jsx +++ b/client/src/components/shop-info/shop-info.speedprint.component.jsx @@ -1,17 +1,22 @@ -import { DeleteFilled } from "@ant-design/icons"; +import { DeleteFilled, HolderOutlined } from "@ant-design/icons"; import { Button, Form, Input, Select, Space } from "antd"; import { useTranslation } from "react-i18next"; import { TemplateList } from "../../utils/TemplateConstants"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; -import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import { + INLINE_TITLE_GROUP_STYLE, + INLINE_TITLE_HANDLE_STYLE, + INLINE_TITLE_INPUT_STYLE, + INLINE_TITLE_LABEL_STYLE, + INLINE_TITLE_ROW_STYLE, + INLINE_TITLE_SEPARATOR_STYLE +} from "../layout-form-row/inline-form-row-title.utils.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; export default function ShopInfoSpeedPrint() { const { t } = useTranslation(); - const form = Form.useFormInstance(); const allTemplates = TemplateList("job"); - const speedPrintItems = Form.useWatch(["speedprint"], form) || []; const TemplateListGenerated = InstanceRenderManager({ imex: Object.fromEntries(Object.entries(allTemplates).filter(([, { enhanced_payroll }]) => !enhanced_payroll)), rome: allTemplates @@ -24,18 +29,71 @@ export default function ShopInfoSpeedPrint() { return (
{fields.map((field, index) => { - const speedPrintItem = speedPrintItems[field.name] || {}; - return ( + +
+
{t("bodyshop.fields.speedprint.id")}
+ + + +
+
+
+
{t("bodyshop.fields.speedprint.label")}
+ + + +
+
+ } + wrapTitle extra={ } > @@ -229,8 +233,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
{t("employees.labels.active")}
@@ -241,8 +244,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
{t("employees.fields.flat_rate")}
@@ -396,147 +398,141 @@ export function ShopEmployeesFormComponent({ bodyshop }) { - - - {(fields, { add, remove, move }) => { - return ( + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }} + id="add-employee-rate-button" + > + {t("employees.actions.addrate")} + + ]} + >
- {fields.map((field, index) => { - return ( - - - -
-
{t("employees.fields.cost_center")}
- - ({ + value: c.name, + label: c.name + }))) + ]} + style={{ width: "100%" }} + styles={{ + selector: INLINE_TITLE_INPUT_STYLE + }} + /> + +
-
-
-
{t("employees.fields.rate")}
- - - -
-
- } - wrapTitle - extra={ - - - + } + wrapTitle + extra={ + +
- ); - }} - - + + ); + }} + - -
- -
- + {(data?.employees_by_pk?.employee_vacations ?? []).length === 0 ? ( + + ) : ( +
+
-
+ )} ); diff --git a/client/src/components/shop-employees/shop-employees-list.component.jsx b/client/src/components/shop-employees/shop-employees-list.component.jsx index e6ecda6cf..787554877 100644 --- a/client/src/components/shop-employees/shop-employees-list.component.jsx +++ b/client/src/components/shop-employees/shop-employees-list.component.jsx @@ -4,6 +4,8 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router-dom"; import { alphaSort } from "../../utils/sorters"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import ResponsiveTable from "../responsive-table/responsive-table.component"; export default function ShopEmployeesListComponent({ loading, employees }) { @@ -16,13 +18,28 @@ export default function ShopEmployeesListComponent({ loading, employees }) { filteredInfo: { text: "" } }); + const navigateToEmployee = (employeeId) => { + history({ + search: queryString.stringify({ + ...search, + employeeId + }) + }); + }; + + const clearEmployeeSelection = () => { + const { employeeId, ...nextSearch } = search; + void employeeId; + history({ + search: queryString.stringify(nextSearch) + }); + }; + const handleOnRowClick = (record) => { if (record) { - search.employeeId = record.id; - history({ search: queryString.stringify(search) }); + navigateToEmployee(record.id); } else { - delete search.employeeId; - history({ search: queryString.stringify(search) }); + clearEmployeeSelection(); } }; const handleTableChange = (pagination, filters, sorter) => { @@ -89,44 +106,39 @@ export default function ShopEmployeesListComponent({ loading, employees }) { } ]; return ( -
- { - return ( - - ); - }} - loading={loading} - pagination={{ placement: "top" }} - columns={columns} - mobileColumnKeys={["employee_number", "employee_name", "active"]} - rowKey="id" - dataSource={employees} - rowSelection={{ - onSelect: (props) => { - search.employeeId = props.id; - history({ search: queryString.stringify(search) }); - }, - type: "radio", - selectedRowKeys: [search.employeeId] - }} - onChange={handleTableChange} - onRow={(record) => { - return { - onClick: () => { - handleOnRowClick(record); - } - }; - }} - /> -
+ navigateToEmployee("new")}> + {t("employees.actions.new")} + + ]} + > + {employees.length === 0 ? ( + + ) : ( + navigateToEmployee(props.id), + type: "radio", + selectedRowKeys: [search.employeeId] + }} + onChange={handleTableChange} + onRow={(record) => { + return { + onClick: () => { + handleOnRowClick(record); + } + }; + }} + /> + )} + ); } diff --git a/client/src/components/shop-info/shop-info.component.jsx b/client/src/components/shop-info/shop-info.component.jsx index c74427724..ee7e40bd9 100644 --- a/client/src/components/shop-info/shop-info.component.jsx +++ b/client/src/components/shop-info/shop-info.component.jsx @@ -163,8 +163,14 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) { } extra={ - } > diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx index 00006c1f2..55ce20351 100644 --- a/client/src/components/shop-info/shop-info.general.component.jsx +++ b/client/src/components/shop-info/shop-info.general.component.jsx @@ -8,6 +8,7 @@ import CurrencyInput from "../form-items-formatted/currency-form-item.component" import FormItemEmail from "../form-items-formatted/email-form-item.component"; import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { INLINE_TITLE_GROUP_STYLE, @@ -15,7 +16,8 @@ import { INLINE_TITLE_INPUT_STYLE, INLINE_TITLE_LABEL_STYLE, INLINE_TITLE_ROW_STYLE, - INLINE_TITLE_SEPARATOR_STYLE + INLINE_TITLE_SEPARATOR_STYLE, + INLINE_TITLE_SWITCH_GROUP_STYLE } from "../layout-form-row/inline-form-row-title.utils.js"; const timeZonesList = Intl.supportedValuesOf("timeZone"); @@ -28,6 +30,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral); export function ShopInfoGeneral({ form }) { const { t } = useTranslation(); + const buildSectionActionButton = (key, label, onClick, id) => ( + + ); + const renderListOrEmpty = (fields, actionLabel, renderItems) => + fields.length === 0 ? : renderItems(); return (
@@ -449,715 +458,22 @@ export function ShopInfoGeneral({ form }) { - - - {(fields, { add, remove, move }) => { - return ( + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + >
- {fields.map((field, index) => { - return ( - - - -
-
{t("bodyshop.fields.messaginglabel_short")}
- - - -
-
-
-
{t("bodyshop.fields.messagingtext_short")}
- - - -
-
- } - extra={ - - -
-
- ); - }} -
-
- - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => { - return ( - - - -
-
{t("bodyshop.fields.noteslabel_short")}
- - - -
-
-
-
{t("bodyshop.fields.notestext_short")}
- - - -
-
- } - extra={ - - -
-
- ); - }} -
-
- - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => { - return ( - - - -
-
{t("bodyshop.fields.partslocation")}
- - - -
-
- } - extra={ - - - -
- ); - }} - - - - {/*Start Insurance Provider Row */} - {t("bodyshop.labels.insurancecos")}} - id="insurancecos" - > - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => { - return ( - - - -
-
{t("bodyshop.fields.md_ins_co.name")}
- ({ - validator: async (_, value) => { - const normalizedValue = (value ?? "").toString().trim().toLowerCase(); - if (!normalizedValue) return Promise.resolve(); - - const list = getFieldValue(["md_ins_cos"]) || []; - const normalizedNames = list - .map((c) => (c?.name ?? "").toString().trim().toLowerCase()) - .filter(Boolean); - - const count = normalizedNames.filter((n) => n === normalizedValue).length; - - if (count > 1) { - throw new Error(t("bodyshop.errors.duplicate_insurance_company")); - } - - return Promise.resolve(); - } - }) - ]} - > - - -
-
-
-
{t("bodyshop.fields.md_ins_co.private")}
- - - -
-
- } - wrapTitle - extra={ - - -
-
- ); - }} -
-
- {/*End Insurance Provider Row */} - - - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => { - return ( - - - -
-
{t("jobs.fields.est_ct_fn_short")}
- - - -
-
-
-
{t("jobs.fields.est_ct_ln_short")}
- - - -
-
-
-
{t("jobs.fields.est_co_nm_short")}
- - - -
-
- } - wrapTitle - extra={ - - - -
- ); - }} - -
- - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => { - return ( - - - -
-
{t("jobs.fields.ins_ct_fn_short")}
- - - -
-
-
-
{t("jobs.fields.ins_ct_ln_short")}
- - - -
-
- } - wrapTitle - extra={ - - -
-
- ); - }} -
-
- null}> - - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => { + {renderListOrEmpty(fields, t("bodyshop.actions.add_messaging_preset"), () => + fields.map((field, index) => { return ( -
+
{t("bodyshop.fields.messaginglabel_short")}
+ + + +
+
+ } + extra={ + +
+
+ ); + }} + + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.add_note_preset"), () => + fields.map((field, index) => { + return ( + + + +
+
{t("bodyshop.fields.noteslabel_short")}
+ + + +
+
+ } + extra={ + +
+
+ ); + }} + + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.addpartslocation"), () => + fields.map((field, index) => { + return ( + + + +
+
{t("bodyshop.fields.partslocation")}
+ + + +
+
+ } + extra={ + +
+ + ); + }} + + + {/*Start Insurance Provider Row */} + + {(fields, { add, remove, move }) => { + return ( + {t("bodyshop.labels.insurancecos")}} + id="insurancecos" + actions={[ + buildSectionActionButton( + "add-insurance-company", + t("bodyshop.actions.add_insurance_company"), + () => { + add(); + }, + "insurancecos-add-button" + ) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.add_insurance_company"), () => + fields.map((field, index) => { + return ( + + + +
+
{t("bodyshop.fields.md_ins_co.name")}
+ ({ + validator: async (_, value) => { + const normalizedValue = (value ?? "").toString().trim().toLowerCase(); + if (!normalizedValue) return Promise.resolve(); + + const list = getFieldValue(["md_ins_cos"]) || []; + const normalizedNames = list + .map((c) => (c?.name ?? "").toString().trim().toLowerCase()) + .filter(Boolean); + + const count = normalizedNames.filter((n) => n === normalizedValue).length; + + if (count > 1) { + throw new Error(t("bodyshop.errors.duplicate_insurance_company")); + } + + return Promise.resolve(); + } + }) + ]} + > + + +
+
+
+
{t("bodyshop.fields.md_ins_co.private")}
+ + + +
+
+ } + wrapTitle + extra={ + +
+
+ ); + }} +
+ {/*End Insurance Provider Row */} + + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.add_estimator"), () => + fields.map((field, index) => { + return ( + + + +
+
{t("jobs.fields.est_ct_fn_short")}
+ + + +
+
+
+
{t("jobs.fields.est_ct_ln_short")}
+ + + +
+
+
+
{t("jobs.fields.est_co_nm_short")}
+ + + +
+
+ } + wrapTitle + extra={ + +
+
+ ); + }} + + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.add_adjuster"), () => + fields.map((field, index) => { + return ( + + + +
+
{t("jobs.fields.ins_ct_fn_short")}
+ + + +
+
+
+
{t("jobs.fields.ins_ct_ln_short")}
+ + + +
+
+ } + wrapTitle + extra={ + +
+
+ ); + }} +
+ null}> + + {(fields, { add, remove, move }) => { + return ( + { + add(); + } + ) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.add_courtesy_car_rate_preset"), () => + fields.map((field, index) => { + return ( + + + +
+
{t("general.labels.label")}
+ + + +
+
+ } + wrapTitle + extra={ + +
+
+ ); + }} +
+ + + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.add_jobline_preset"), () => + fields.map((field, index) => { + return ( + + + +
+
{t("general.labels.label")}
+ + + +
+
+
+
{t("joblines.fields.line_desc")}
+ + + +
+
+ } + wrapTitle + extra={ + +
+
+ ); + }} +
+ + {(fields, { add, remove, move }) => { + return ( + { + add(); + } + ) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.add_parts_order_comment"), () => + fields.map((field, index) => { + return ( + + + +
{t("general.labels.label")}
- -
- ); - }} - -
- - - - - {(fields, { add, remove, move }) => { - return ( + }) + )} +
+
+ ); + }} +
+ + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + >
- {fields.map((field, index) => { - return ( - - - -
-
{t("general.labels.label")}
- - - + {renderListOrEmpty(fields, t("bodyshop.actions.add_to_email_preset"), () => + fields.map((field, index) => { + return ( + + + +
+
{t("general.labels.label")}
+ + + +
-
-
-
{t("joblines.fields.line_desc")}
- - - -
-
- } - wrapTitle - extra={ - - -
+ style={{ marginBottom: 0 }} + > + + + + + ); + }) + )}
- ); - }} -
- - - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => { - return ( - - - -
-
{t("general.labels.label")}
- - - -
-
-
-
{t("parts_orders.fields.comments")}
- - - -
-
- } - wrapTitle - extra={ - - -
-
- ); - }} -
-
- - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => { - return ( - - - -
-
{t("general.labels.label")}
- - - -
-
-
-
{t("bodyshop.labels.md_to_emails_emails")}
- - - -
-
- } - wrapTitle - extra={ - - -
-
- ); - }} -
-
+ + ); + }} +
); } diff --git a/client/src/components/shop-info/shop-info.intake.component.jsx b/client/src/components/shop-info/shop-info.intake.component.jsx index 4a938f873..182b4c878 100644 --- a/client/src/components/shop-info/shop-info.intake.component.jsx +++ b/client/src/components/shop-info/shop-info.intake.component.jsx @@ -5,6 +5,7 @@ import styled from "styled-components"; import { TemplateList } from "../../utils/TemplateConstants"; import ConfigFormTypes from "../config-form-components/config-form-types"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { INLINE_TITLE_GROUP_STYLE, @@ -12,7 +13,8 @@ import { INLINE_TITLE_INPUT_STYLE, INLINE_TITLE_LABEL_STYLE, INLINE_TITLE_ROW_STYLE, - INLINE_TITLE_SEPARATOR_STYLE + INLINE_TITLE_SEPARATOR_STYLE, + INLINE_TITLE_SWITCH_GROUP_STYLE } from "../layout-form-row/inline-form-row-title.utils.js"; const SelectorDiv = styled.div` @@ -87,320 +89,318 @@ export default function ShopInfoIntakeChecklistComponent({ form }) { - - - {(fields, { add, remove, move }) => { - return ( + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }} + > + {t("bodyshop.actions.add_intake_checklist_item")} + + ]} + >
- {fields.map((field, index) => { - return ( - - - -
-
{t("jobs.fields.intake.name")}
- - - -
-
-
-
{t("jobs.fields.intake.required")}
- - - -
-
- } - wrapTitle - extra={ - -
+
+ ); + }} +
+ + {(fields, { add, remove, move }) => { + return ( + { + add(); + }} + > + {t("bodyshop.actions.add_delivery_checklist_item")} + + ]} + > +
+ {fields.length === 0 ? ( + + ) : ( + fields.map((field, index) => { + return ( + + + +
+
{t("jobs.fields.intake.name")}
- + - - ); - }} - - - - ); - })} - - - -
- ); - }} - -
- - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => { - return ( - - - -
-
{t("jobs.fields.intake.name")}
- - - +
+
+
+
{t("jobs.fields.intake.required")}
+ + + +
-
-
-
{t("jobs.fields.intake.required")}
- - - -
-
- } - wrapTitle - extra={ - - -
+ + {() => { + if (form.getFieldValue(["deliverchecklist", "form", index, "type"]) !== "slider") + return null; + return ( + <> + + + + + + + + ); + }} + + + + ); + }) + )}
- ); - }} -
-
+ + ); + }} +
); } diff --git a/client/src/components/shop-info/shop-info.laborrates.component.jsx b/client/src/components/shop-info/shop-info.laborrates.component.jsx index bb61db15b..3e1b16705 100644 --- a/client/src/components/shop-info/shop-info.laborrates.component.jsx +++ b/client/src/components/shop-info/shop-info.laborrates.component.jsx @@ -3,6 +3,7 @@ import { Button, Form, Input, Space } from "antd"; import { useTranslation } from "react-i18next"; import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { INLINE_TITLE_GROUP_STYLE, @@ -25,361 +26,364 @@ export default function ShopInfoLaborRates() {
- - - {(fields, { add, remove, move }) => { - return ( + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }} + > + {t("bodyshop.actions.newlaborrate")} + + ]} + >
- {fields.map((field, index) => { - return ( - - - -
-
{t("jobs.fields.labor_rate_desc")}
- - - + {fields.length === 0 ? ( + + ) : ( + fields.map((field, index) => { + return ( + + + +
+
{t("jobs.fields.labor_rate_desc")}
+ + + +
-
- } - wrapTitle - extra={ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + // + // + // + // + // + // + } + + + + + + +
+ + ); + }) + )}
- ); - }} - -
+ + ); + }} + ); } diff --git a/client/src/components/shop-info/shop-info.parts-scan.jsx b/client/src/components/shop-info/shop-info.parts-scan.jsx index 3f7e94906..64a038479 100644 --- a/client/src/components/shop-info/shop-info.parts-scan.jsx +++ b/client/src/components/shop-info/shop-info.parts-scan.jsx @@ -3,6 +3,7 @@ import { Button, Col, Form, Input, Row, Select, Space, Switch } from "antd"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { INLINE_TITLE_GROUP_STYLE, @@ -10,7 +11,8 @@ import { INLINE_TITLE_INPUT_STYLE, INLINE_TITLE_LABEL_STYLE, INLINE_TITLE_ROW_STYLE, - INLINE_TITLE_SEPARATOR_STYLE + INLINE_TITLE_SEPARATOR_STYLE, + INLINE_TITLE_SWITCH_GROUP_STYLE } from "../layout-form-row/inline-form-row-title.utils.js"; import i18n from "i18next"; @@ -76,229 +78,221 @@ export default function ShopInfoPartsScan({ form }) { return (
- - - {(fields, { add, remove, move }) => ( + + {(fields, { add, remove, move }) => ( + + add({ + field: "line_desc", + operation: "contains", + mark_critical: true, + caseInsensitive: true + }) + } + > + {t("bodyshop.actions.addpartsrule")} + + ]} + >
- {fields.map((field, index) => { - const selectedField = watchedFields?.[index]?.field || "line_desc"; - const fieldType = getFieldType(selectedField); + {fields.length === 0 ? ( + + ) : ( + fields.map((field, index) => { + const selectedField = watchedFields?.[index]?.field || "line_desc"; + const fieldType = getFieldType(selectedField); - return ( - - - -
-
{t("bodyshop.fields.md_parts_scan.field")}
+ return ( + + + +
+
{t("bodyshop.fields.md_parts_scan.field")}
+ + + + + )} + + {/* Value */} + {fieldType && ( + + + {fieldType === "predefined" ? ( + + )} + + + )} + + {/* Update Field */} + + - )} - - {/* Value */} - {fieldType && ( - - - {fieldType === "predefined" ? ( - - )} - - - )} - - {/* Update Field */} - - - - - - - - - ); - })} - - - - + + + + ); + }) + )}
- )} - -
+ + )} +
); } diff --git a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx index 48550c24c..965c406e3 100644 --- a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx @@ -12,6 +12,7 @@ import DataLabel from "../data-label/data-label.component"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { INLINE_TITLE_GROUP_STYLE, @@ -42,6 +43,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoResponsibili export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { const { t } = useTranslation(); const dmsPayers = Form.useWatch(["cdk_configuration", "payers"], form) || []; + const buildSectionActionButton = (key, label, onClick) => ( + + ); + const renderListOrEmpty = (fields, actionLabel, renderItems) => + fields.length === 0 ? : renderItems(); const hasDMSKey = bodyshopHasDmsKey(bodyshop); @@ -513,42 +521,116 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { {!bodyshop.rr_dealerid && ( <> - - - {(fields, { add, remove, move }) => { - return ( + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + >
- {fields.map((field, index) => { - const dmsPayer = dmsPayers[field.name] || {}; + {renderListOrEmpty(fields, t("jobs.actions.addpayer"), () => + fields.map((field, index) => { + const dmsPayer = dmsPayers[field.name] || {}; - return ( + return ( + + + - -
- ); - }} -
-
+
+ ); + }} + )}
@@ -659,142 +671,266 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { )} {HasFeatureAccess({ featureName: "export", bodyshop }) && ( <> - - - {(fields, { add, remove, move }) => { - return ( + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + >
- {fields.map((field, index) => { - const hasProfitCenterBodyFields = - (hasDMSKey && !bodyshop.rr_dealerid) || bodyshop.cdk_dealerid || bodyshop.rr_dealerid; - - return ( - - - -
-
- {t("bodyshop.fields.responsibilitycenter")} -
- - - -
-
-
-
- {t("bodyshop.fields.responsibilitycenter_accountname")} -
- - - -
-
-
-
- {t("bodyshop.fields.responsibilitycenter_accountdesc")} -
- - - -
- {!hasDMSKey && ( - <> -
-
-
- {t("bodyshop.fields.responsibilitycenter_accountitem")} -
- - - + {renderListOrEmpty(fields, t("bodyshop.actions.add_cost_center"), () => + fields.map((field, index) => { + return ( + + + +
+
+ {t("bodyshop.fields.responsibilitycenter")}
- - )} -
- } - wrapTitle - extra={ - -
+
+
+
+ {t("bodyshop.fields.responsibilitycenter_accountdesc")} +
+ + + +
+
+ } + wrapTitle + extra={ + +
+ + ); + }} + + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.add_profit_center"), () => + fields.map((field, index) => { + return ( + + + +
+
+ {t("bodyshop.fields.responsibilitycenter")} +
+ + + +
+
+
+
+ {t("bodyshop.fields.responsibilitycenter_accountdesc")} +
+ + + +
+
+ } + wrapTitle + extra={ + + -
-
- ); - }} -
- - - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => { - const hasProfitCenterBodyFields = - (hasDMSKey && !bodyshop.rr_dealerid) || bodyshop.cdk_dealerid || bodyshop.rr_dealerid; - - return ( - - - -
-
- {t("bodyshop.fields.responsibilitycenter")} -
- - - -
-
-
+ , + -
- {t("bodyshop.fields.responsibilitycenter_accountdesc")} -
- - - -
- {!hasDMSKey && ( - <> -
-
-
- {t("bodyshop.fields.responsibilitycenter_accountitem")} -
- - - -
- - )} - {bodyshop.rr_dealerid && ( - <> -
-
-
- {t("bodyshop.fields.responsibilitycenters.item_type")} -
- - - - )} - {bodyshop.cdk_dealerid && ( - - - - )} - {bodyshop.rr_dealerid && [ - - - , - - + + ]} + + + ); + }) + )}
- ); - }} - - + + ); + }} + {hasDMSKey && ( - - - {(fields, { add, remove }) => { - return ( + + {(fields, { add, remove }) => { + return ( + { + add(); + } + ) + ]} + >
- {fields.map((field, index) => { - const dmsDefault = form.getFieldValue([ - "md_responsibility_centers", - "dms_defaults", - field.name - ]) || { name: undefined }; + {renderListOrEmpty(fields, t("bodyshop.actions.add_dms_allocation"), () => + fields.map((field, index) => { + const dmsDefault = form.getFieldValue([ + "md_responsibility_centers", + "dms_defaults", + field.name + ]) || { name: undefined }; - return ( - - } - onClick={() => { - remove(field.name); - }} - /> - } - > -
- - - - - - - - - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); + return ( + + } + onClick={() => { + remove(field.name); + }} + /> + } + > +
+ + + + + + + + + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-ats`} - name={[field.name, "costs", "ATS"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAA`} - name={[field.name, "costs", "LAA"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAB`} - name={[field.name, "costs", "LAB"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAD`} - name={[field.name, "costs", "LAD"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAE`} - name={[field.name, "costs", "LAE"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAF`} - name={[field.name, "costs", "LAF"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAG`} - name={[field.name, "costs", "LAG"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAM`} - name={[field.name, "costs", "LAM"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAR`} - name={[field.name, "costs", "LAR"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAS`} - name={[field.name, "costs", "LAS"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAU`} - name={[field.name, "costs", "LAU"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LA1`} - name={[field.name, "costs", "LA1"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LA2`} - name={[field.name, "costs", "LA2"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LA3`} - name={[field.name, "costs", "LA3"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LA4`} - name={[field.name, "costs", "LA4"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAA`} - name={[field.name, "costs", "PAA"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAC`} - name={[field.name, "costs", "PAC"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAG`} - name={[field.name, "costs", "PAG"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAL`} - name={[field.name, "costs", "PAL"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAM`} - name={[field.name, "costs", "PAM"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAN`} - name={[field.name, "costs", "PAN"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAO`} - name={[field.name, "costs", "PAO"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAP`} - name={[field.name, "costs", "PAP"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAR`} - name={[field.name, "costs", "PAR"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAS`} - name={[field.name, "costs", "PAS"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PASL`} - name={[field.name, "costs", "PASL"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-TOW`} - name={[field.name, "costs", "TOW"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-MAPA`} - name={[field.name, "costs", "MAPA"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (costOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-MASH`} - name={[field.name, "costs", "MASH"]} - > - ({ value: item, label: item }))} + /> + + + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-ATS`} - name={[field.name, "profits", "ATS"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAA`} - name={[field.name, "profits", "LAA"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAB`} - name={[field.name, "profits", "LAB"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAD`} - name={[field.name, "profits", "LAD"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAE`} - name={[field.name, "profits", "LAE"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAF`} - name={[field.name, "profits", "LAF"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAG`} - name={[field.name, "profits", "LAG"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAM`} - name={[field.name, "profits", "LAM"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAR`} - name={[field.name, "profits", "LAR"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAS`} - name={[field.name, "profits", "LAS"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAU`} - name={[field.name, "profits", "LAU"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LA1`} - name={[field.name, "profits", "LA1"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LA2`} - name={[field.name, "profits", "LA2"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LA3`} - name={[field.name, "profits", "LA3"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LA4`} - name={[field.name, "profits", "LA4"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAA`} - name={[field.name, "profits", "PAA"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAC`} - name={[field.name, "profits", "PAC"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAG`} - name={[field.name, "profits", "PAG"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAL`} - name={[field.name, "profits", "PAL"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAM`} - name={[field.name, "profits", "PAM"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAN`} - name={[field.name, "profits", "PAN"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAO`} - name={[field.name, "profits", "PAO"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAP`} - name={[field.name, "profits", "PAP"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAR`} - name={[field.name, "profits", "PAR"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAS`} - name={[field.name, "profits", "PAS"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PASL`} - name={[field.name, "profits", "PASL"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-TOW`} - name={[field.name, "profits", "TOW"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-MAPA`} - name={[field.name, "profits", "MAPA"]} - > - ({ value: item, label: item }))} + /> + + ({ + validator(rule, value) { + if (profitOptions.includes(value)) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.centermustexist")); } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-MASH`} - name={[field.name, "profits", "MASH"]} - > - ({ value: item, label: item }))} + /> + + +
+
+
+ ); + }) + )}
- ); - }} - -
+ + ); + }} + )} @@ -3601,121 +3525,114 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
)} - - - {(fields, { add, remove }) => { - return ( + + {(fields, { add, remove }) => { + return ( + { + add(); + }) + ]} + >
- {fields.map((field, index) => { - return ( - - - -
-
- {t("bodyshop.fields.responsibilitycenters.sales_tax_codes.description")} + {renderListOrEmpty(fields, t("bodyshop.actions.newsalestaxcode"), () => + fields.map((field, index) => { + return ( + + + +
+
+ {t("bodyshop.fields.responsibilitycenters.sales_tax_codes.description")} +
+ + +
- - - -
-
-
-
- {t("bodyshop.fields.responsibilitycenters.sales_tax_codes.code")} +
+
+
+ {t("bodyshop.fields.responsibilitycenters.sales_tax_codes.code")} +
+ + +
- - -
-
- } - wrapTitle - extra={ - - + + + + + + + + + + + + ); + }) + )}
- ); - }} - - + + ); + }} + )} diff --git a/client/src/components/shop-info/shop-info.rostatus.component.jsx b/client/src/components/shop-info/shop-info.rostatus.component.jsx index 273d04bae..05fe27305 100644 --- a/client/src/components/shop-info/shop-info.rostatus.component.jsx +++ b/client/src/components/shop-info/shop-info.rostatus.component.jsx @@ -7,6 +7,7 @@ import { ChromePicker } from "react-color"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { DEFAULT_TRANSLUCENT_CARD_COLOR, getTintedCardSurfaceStyles } from "./shop-info.color.utils"; @@ -405,106 +406,116 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) { {Production_List_Status_Colors.treatment === "on" && ( - - - {(fields, { add, remove }) => { - return ( + + {(fields, { add, remove }) => { + return ( + { + add({ + color: { ...DEFAULT_TRANSLUCENT_CARD_COLOR } + }); + }} + > + {t("bodyshop.actions.add_production_status_color")} + + ]} + >
- - {fields.map((field, index) => { - const productionColor = productionColors[field.name] || {}; - const productionColorSurfaceStyles = getTintedCardSurfaceStyles(productionColor.color); - const selectedProductionColorStatuses = productionColors - .map((item) => item?.status) - .filter(Boolean); - const productionColorStatusOptions = [ - ...new Set([productionColor.status, ...availableProductionStatuses]) - ] - .filter(Boolean) - .filter( - (status) => - status === productionColor.status || !selectedProductionColorStatuses.includes(status) - ); + {fields.length === 0 ? ( + + ) : ( + + {fields.map((field, index) => { + const productionColor = productionColors[field.name] || {}; + const productionColorSurfaceStyles = getTintedCardSurfaceStyles(productionColor.color); + const selectedProductionColorStatuses = productionColors + .map((item) => item?.status) + .filter(Boolean); + const productionColorStatusOptions = [ + ...new Set([productionColor.status, ...availableProductionStatuses]) + ] + .filter(Boolean) + .filter( + (status) => + status === productionColor.status || !selectedProductionColorStatuses.includes(status) + ); - return ( - - ({ + value: item, + label: item + }))} + /> + + } + extra={ + - + } + {...productionColorSurfaceStyles} + style={{ width: 260, marginBottom: 0 }} + > +
+ + + +
+
+ ); + })} +
+ )}
- ); - }} -
-
+ + ); + }} + )} ); diff --git a/client/src/components/shop-info/shop-info.scheduling.component.jsx b/client/src/components/shop-info/shop-info.scheduling.component.jsx index 12d998d01..8ee367304 100644 --- a/client/src/components/shop-info/shop-info.scheduling.component.jsx +++ b/client/src/components/shop-info/shop-info.scheduling.component.jsx @@ -7,6 +7,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { ColorPicker } from "./shop-info.rostatus.component"; import { @@ -227,195 +228,74 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) { ))} - - - {(fields, { add, remove, move }) => { - return ( + + {(fields, { add, remove, move }) => { + return ( + { + add({ + color: { + ...DEFAULT_TRANSLUCENT_PICKER_COLOR, + rgb: { ...DEFAULT_TRANSLUCENT_PICKER_COLOR.rgb } + } + }); + }} + > + {t("bodyshop.actions.addapptcolor")} + + ]} + >
- {fields.map((field, index) => { - const appointmentColor = - appointmentColors[field.name] || form.getFieldValue(["appt_colors", field.name]) || {}; - const appointmentColorSurfaceStyles = getTintedCardSurfaceStyles(appointmentColor.color); - - return ( - - - - - -
- } - extra={ - - - -
- ); - }} - - - {HasFeatureAccess({ featureName: "smartscheduling", bodyshop }) && ( - - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => { - const schedulingBucket = - schedulingBuckets[field.name] || form.getFieldValue(["ssbuckets", field.name]) || {}; - const schedulingBucketSurfaceStyles = getTintedCardSurfaceStyles(schedulingBucket.color); + {fields.length === 0 ? ( + + ) : ( + fields.map((field, index) => { + const appointmentColor = + appointmentColors[field.name] || form.getFieldValue(["appt_colors", field.name]) || {}; + const appointmentColorSurfaceStyles = getTintedCardSurfaceStyles(appointmentColor.color); return ( -
-
{t("bodyshop.fields.ssbuckets.id")}
- - - -
-
+ -
- {t("bodyshop.fields.ssbuckets.label")} -
- - - -
+ +
} extra={ - + }) + )} +
+
+ ); + }} + + {HasFeatureAccess({ featureName: "smartscheduling", bodyshop }) && ( + + {(fields, { add, remove, move }) => { + return ( + { + add({ + color: { ...DEFAULT_TRANSLUCENT_CARD_COLOR } + }); + }} + > + {t("bodyshop.actions.addbucket")} + + ]} + > +
+ {fields.length === 0 ? ( + + ) : ( + fields.map((field, index) => { + const schedulingBucket = + schedulingBuckets[field.name] || form.getFieldValue(["ssbuckets", field.name]) || {}; + const schedulingBucketSurfaceStyles = getTintedCardSurfaceStyles(schedulingBucket.color); + + return ( + + +
+
{t("bodyshop.fields.ssbuckets.id")}
+ + + +
+
+
+ {t("bodyshop.fields.ssbuckets.label")} +
+ + + +
+
+ } + extra={ + +
- ); - }} -
-
+ + ); + }} + )}
); diff --git a/client/src/components/shop-info/shop-info.speedprint.component.jsx b/client/src/components/shop-info/shop-info.speedprint.component.jsx index 062bea41e..33b5e55fe 100644 --- a/client/src/components/shop-info/shop-info.speedprint.component.jsx +++ b/client/src/components/shop-info/shop-info.speedprint.component.jsx @@ -3,6 +3,7 @@ import { Button, Form, Input, Select, Space } from "antd"; import { useTranslation } from "react-i18next"; import { TemplateList } from "../../utils/TemplateConstants"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { INLINE_TITLE_GROUP_STYLE, @@ -23,133 +24,131 @@ export default function ShopInfoSpeedPrint() { }); return ( - - - {(fields, { add, remove, move }) => { - return ( + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }} + > + {t("bodyshop.actions.addspeedprint")} + + ]} + >
- {fields.map((field, index) => { - return ( - - - -
-
{t("bodyshop.fields.speedprint.id")}
- - - + {fields.length === 0 ? ( + + ) : ( + fields.map((field, index) => { + return ( + + + +
+
{t("bodyshop.fields.speedprint.id")}
+ + + +
+
+
+
{t("bodyshop.fields.speedprint.label")}
+ + + +
-
-
-
{t("bodyshop.fields.speedprint.label")}
- - - -
-
- } - wrapTitle - extra={ - -
- ); - }} - -
+ + ); + }} + ); } diff --git a/client/src/components/shop-info/shop-info.task-presets.component.jsx b/client/src/components/shop-info/shop-info.task-presets.component.jsx index dfc12adfb..d3d3cb66a 100644 --- a/client/src/components/shop-info/shop-info.task-presets.component.jsx +++ b/client/src/components/shop-info/shop-info.task-presets.component.jsx @@ -3,6 +3,7 @@ import { Button, Checkbox, Col, Form, Input, InputNumber, Row, Select, Space, Sw import { useTranslation } from "react-i18next"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { connect } from "react-redux"; @@ -78,208 +79,216 @@ export function ShopInfoTaskPresets({ bodyshop }) {
- - { - const allocationErrors = getTaskPresetAllocationErrors(presets, t); + { + const allocationErrors = getTaskPresetAllocationErrors(presets, t); - if (allocationErrors.length > 0) { - throw new Error(allocationErrors.join(" ")); - } + if (allocationErrors.length > 0) { + throw new Error(allocationErrors.join(" ")); } } - ]} - > - {(fields, { add, remove, move }, { errors }) => { - return ( + } + ]} + > + {(fields, { add, remove, move }, { errors }) => { + return ( + { + add(); + }} + > + {t("bodyshop.actions.add_task_preset")} + + ]} + >
- {fields.map((field, index) => { - const taskPreset = taskPresets[field.name] || {}; + {fields.length === 0 ? ( + + ) : ( + fields.map((field, index) => { + const taskPreset = taskPresets[field.name] || {}; - return ( - - - - + + {payoutMethod === "commission" ? ( + + ) : ( + + )} + + + ))} + + ); + }} + +
+
+ + ); + }) + )}
- ); - }} -
-
+
+ ); + }} +
)} diff --git a/client/src/components/shop-teams/shop-employee-teams.form.component.test.jsx b/client/src/components/shop-teams/shop-employee-teams.form.component.test.jsx index b7239dbde..da20b74c5 100644 --- a/client/src/components/shop-teams/shop-employee-teams.form.component.test.jsx +++ b/client/src/components/shop-teams/shop-employee-teams.form.component.test.jsx @@ -42,9 +42,11 @@ vi.mock("react-i18next", () => ({ "employee_teams.options.commission": "Commission", "employee_teams.options.commission_percentage": "Commission", "employee_teams.actions.newmember": "New Team Member", + "employee_teams.actions.save_team": "Save Employee Team", "employee_teams.errors.minimum_one_member": "Add at least one team member.", "employee_teams.errors.duplicate_member": "Team members must be unique.", "employee_teams.errors.allocation_total_exact": "Allocation must total exactly 100%.", + "general.labels.click_to_begin": `Click ${values.action ?? ""} to begin`, "general.actions.save": "Save", "employees.successes.save": "Saved" }; @@ -101,11 +103,12 @@ vi.mock("../form-items-formatted/currency-form-item.component", () => ({ })); vi.mock("../layout-form-row/layout-form-row.component", () => ({ - default: ({ title, extra, children }) => ( + default: ({ title, extra, actions, children }) => (
{title} {extra} {children} + {actions}
) })); @@ -211,7 +214,7 @@ describe("ShopEmployeeTeamsFormComponent", () => { rate: 27.5 }); - fireEvent.click(screen.getByRole("button", { name: "Save" })); + fireEvent.click(screen.getByRole("button", { name: "Save Employee Team" })); await waitFor(() => { expect(insertEmployeeTeamMock).toHaveBeenCalledWith({ diff --git a/client/src/components/shop-teams/shop-employee-teams.list.jsx b/client/src/components/shop-teams/shop-employee-teams.list.jsx index 4c0ac837b..8cd351201 100644 --- a/client/src/components/shop-teams/shop-employee-teams.list.jsx +++ b/client/src/components/shop-teams/shop-employee-teams.list.jsx @@ -2,6 +2,8 @@ import { Button } from "antd"; import queryString from "query-string"; import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router-dom"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import ResponsiveTable from "../responsive-table/responsive-table.component"; export default function ShopEmployeeTeamsListComponent({ loading, employee_teams }) { @@ -9,13 +11,28 @@ export default function ShopEmployeeTeamsListComponent({ loading, employee_teams const history = useNavigate(); const search = queryString.parse(useLocation().search); + const navigateToTeam = (employeeTeamId) => { + history({ + search: queryString.stringify({ + ...search, + employeeTeamId + }) + }); + }; + + const clearTeamSelection = () => { + const { employeeTeamId, ...nextSearch } = search; + void employeeTeamId; + history({ + search: queryString.stringify(nextSearch) + }); + }; + const handleOnRowClick = (record) => { if (record) { - search.employeeTeamId = record.id; - history({ search: queryString.stringify(search) }); + navigateToTeam(record.id); } else { - delete search.employeeTeamId; - history({ search: queryString.stringify(search) }); + clearTeamSelection(); } }; const columns = [ @@ -27,43 +44,38 @@ export default function ShopEmployeeTeamsListComponent({ loading, employee_teams ]; return ( -
- { - return ( - - ); - }} - loading={loading} - pagination={{ placement: "top" }} - columns={columns} - mobileColumnKeys={["name"]} - rowKey="id" - dataSource={employee_teams} - rowSelection={{ - onSelect: (props) => { - search.employeeTeamId = props.id; - history({ search: queryString.stringify(search) }); - }, - type: "radio", - selectedRowKeys: [search.employeeTeamId] - }} - onRow={(record) => { - return { - onClick: () => { - handleOnRowClick(record); - } - }; - }} - /> -
+ navigateToTeam("new")}> + {t("employee_teams.actions.new")} + + ]} + > + {employee_teams.length === 0 ? ( + + ) : ( + navigateToTeam(props.id), + type: "radio", + selectedRowKeys: [search.employeeTeamId] + }} + onRow={(record) => { + return { + onClick: () => { + handleOnRowClick(record); + } + }; + }} + /> + )} + ); } diff --git a/client/src/components/shop-users/shop-users.component.jsx b/client/src/components/shop-users/shop-users.component.jsx index d3fd16b45..d4e972b11 100644 --- a/client/src/components/shop-users/shop-users.component.jsx +++ b/client/src/components/shop-users/shop-users.component.jsx @@ -6,6 +6,7 @@ import { createStructuredSelector } from "reselect"; import { QUERY_SHOP_ASSOCIATIONS } from "../../graphql/user.queries"; import { selectBodyshop } from "../../redux/user/user.selectors"; import AlertComponent from "../alert/alert.component"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import ResponsiveTable from "../responsive-table/responsive-table.component"; import ShopUsersAuthEdit from "../shop-users-auth-edit/shop-users-auth-edit.component"; @@ -66,7 +67,7 @@ export function ShopInfoUsersComponent({ bodyshop }) { return ; } return ( -
+ -
+
); } diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index d2584bea0..428894541 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -292,7 +292,23 @@ }, "bodyshop": { "actions": { + "add_adjuster": "Add Adjuster", + "add_control_number": "Add Control Number", + "add_cost_center": "Add Cost Center", + "add_courtesy_car_rate_preset": "Add Courtesy Car Contract Rate Preset", + "add_delivery_checklist_item": "Add Delivery Checklist Item", + "add_dms_allocation": "Add DMS Allocation", + "add_estimator": "Add Estimator", + "add_insurance_company": "Add Insurance Company", + "add_intake_checklist_item": "Add Intake Checklist Item", + "add_jobline_preset": "Add Jobline Preset", + "add_messaging_preset": "Add Messaging Preset", + "add_note_preset": "Add Note Preset", + "add_parts_order_comment": "Add Parts Order Comment", + "add_production_status_color": "Add Production Status Color", + "add_profit_center": "Add Profit Center", "add_task_preset": "Add Task Preset", + "add_to_email_preset": "Add To Email Preset", "addapptcolor": "Add Appointment Color", "addbucket": "Add Definition", "addpartslocation": "Add Parts Location", @@ -301,6 +317,7 @@ "addtemplate": "Add Template", "newlaborrate": "New Labor Rate", "newsalestaxcode": "New Sales Tax Code", + "save_shop_information": "Save Shop Information", "newstatus": "Add Status", "testrender": "Test Render" }, @@ -1206,7 +1223,8 @@ "employee_teams": { "actions": { "new": "New Team", - "newmember": "New Team Member" + "newmember": "New Team Member", + "save_team": "Save Employee Team" }, "errors": { "allocation_total_exact": "Team allocation must total exactly 100%.", @@ -1236,9 +1254,11 @@ }, "employees": { "actions": { + "addrate": "Add Rate", "addvacation": "Add Vacation", "new": "New Employee", "newrate": "New Rate", + "save_employee": "Save Employee", "select": "Select Employee" }, "errors": { @@ -1403,6 +1423,7 @@ "beta": "BETA", "cancel": "Are you sure you want to cancel? Your changes will not be saved.", "changelog": "Change Log", + "click_to_begin": "Click {{action}} to begin", "clear": "Clear", "confirmpassword": "Confirm Password", "created_at": "Created At", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 0fd4e9622..4a12af3c8 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -292,7 +292,23 @@ }, "bodyshop": { "actions": { + "add_adjuster": "", + "add_control_number": "", + "add_cost_center": "", + "add_courtesy_car_rate_preset": "", + "add_delivery_checklist_item": "", + "add_dms_allocation": "", + "add_estimator": "", + "add_insurance_company": "", + "add_intake_checklist_item": "", + "add_jobline_preset": "", + "add_messaging_preset": "", + "add_note_preset": "", + "add_parts_order_comment": "", + "add_production_status_color": "", + "add_profit_center": "", "add_task_preset": "", + "add_to_email_preset": "", "addapptcolor": "", "addbucket": "", "addpartslocation": "", @@ -301,6 +317,7 @@ "addtemplate": "", "newlaborrate": "", "newsalestaxcode": "", + "save_shop_information": "", "newstatus": "", "testrender": "" }, @@ -1206,7 +1223,8 @@ "employee_teams": { "actions": { "new": "", - "newmember": "" + "newmember": "", + "save_team": "" }, "errors": { "allocation_total_exact": "", @@ -1236,9 +1254,11 @@ }, "employees": { "actions": { + "addrate": "", "addvacation": "", "new": "Nuevo empleado", "newrate": "", + "save_employee": "", "select": "" }, "errors": { @@ -1403,6 +1423,7 @@ "beta": "", "cancel": "", "changelog": "", + "click_to_begin": "", "clear": "", "confirmpassword": "", "created_at": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 11b4fdc7f..b0391e190 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -292,7 +292,23 @@ }, "bodyshop": { "actions": { + "add_adjuster": "", + "add_control_number": "", + "add_cost_center": "", + "add_courtesy_car_rate_preset": "", + "add_delivery_checklist_item": "", + "add_dms_allocation": "", + "add_estimator": "", + "add_insurance_company": "", + "add_intake_checklist_item": "", + "add_jobline_preset": "", + "add_messaging_preset": "", + "add_note_preset": "", + "add_parts_order_comment": "", + "add_production_status_color": "", + "add_profit_center": "", "add_task_preset": "", + "add_to_email_preset": "", "addapptcolor": "", "addbucket": "", "addpartslocation": "", @@ -301,6 +317,7 @@ "addtemplate": "", "newlaborrate": "", "newsalestaxcode": "", + "save_shop_information": "", "newstatus": "", "testrender": "" }, @@ -1206,7 +1223,8 @@ "employee_teams": { "actions": { "new": "", - "newmember": "" + "newmember": "", + "save_team": "" }, "errors": { "allocation_total_exact": "", @@ -1236,9 +1254,11 @@ }, "employees": { "actions": { + "addrate": "", "addvacation": "", "new": "Nouvel employé", "newrate": "", + "save_employee": "", "select": "" }, "errors": { @@ -1403,6 +1423,7 @@ "beta": "", "cancel": "", "changelog": "", + "click_to_begin": "", "clear": "", "confirmpassword": "", "created_at": "", From e49500887d63b213d74236f0ee0e3befc06755cd Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 25 Mar 2026 15:25:59 -0400 Subject: [PATCH 36/56] IO-3624 Finalize admin config UX and validation polish --- .../src/components/alert/alert.component.jsx | 4 +- .../form-fields-changed-alert.component.jsx | 220 ++++- .../form-fields-changed.styles.scss | 43 + .../url-form-item.component.jsx | 34 + .../config-list-actions.utils.jsx | 17 + .../inline-validated-form-row.component.jsx | 47 + .../layout-form-row.styles.scss | 22 + .../shop-employees-form.component.jsx | 86 +- .../shop-employees-list.component.jsx | 14 +- .../shop-employees.container.jsx | 41 +- .../shop-info/shop-info.component.jsx | 3 +- .../shop-info/shop-info.container.jsx | 21 +- .../shop-info/shop-info.general.component.jsx | 758 +++++++++------- .../shop-info/shop-info.intake.component.jsx | 61 +- .../shop-info.laborrates.component.jsx | 52 +- .../shop-info/shop-info.parts-scan.jsx | 9 +- ...p-info.responsibilitycenters.component.jsx | 837 ++++++++++-------- ....responsibilitycenters.taxes.component.jsx | 44 +- .../shop-info/shop-info.roguard.component.jsx | 2 +- .../shop-info.rostatus.component.jsx | 244 ++++- .../shop-info.scheduling.component.jsx | 192 ++-- .../shop-info.section-navigator.component.jsx | 94 +- .../shop-info.section-navigator.styles.scss | 26 + .../shop-info.speedprint.component.jsx | 13 +- .../shop-info.task-presets.component.jsx | 2 +- .../shop-employee-teams.form.component.jsx | 125 ++- ...hop-employee-teams.form.component.test.jsx | 4 + .../shop-teams/shop-employee-teams.list.jsx | 14 +- .../shop-teams/shop-teams.container.jsx | 32 +- .../hooks/useConfirmDirtyFormNavigation.jsx | 11 + client/src/translations/en_us/common.json | 37 + client/src/translations/es/common.json | 37 + client/src/translations/fr/common.json | 37 + 33 files changed, 2223 insertions(+), 960 deletions(-) create mode 100644 client/src/components/form-items-formatted/url-form-item.component.jsx create mode 100644 client/src/components/layout-form-row/config-list-actions.utils.jsx create mode 100644 client/src/components/layout-form-row/inline-validated-form-row.component.jsx create mode 100644 client/src/hooks/useConfirmDirtyFormNavigation.jsx diff --git a/client/src/components/alert/alert.component.jsx b/client/src/components/alert/alert.component.jsx index 439822f68..f57dcb5fd 100644 --- a/client/src/components/alert/alert.component.jsx +++ b/client/src/components/alert/alert.component.jsx @@ -1,5 +1,5 @@ import { Alert } from "antd"; -export default function AlertComponent(props) { - return ; +export default function AlertComponent({ title, message, ...props }) { + return ; } diff --git a/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx b/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx index 3254c9f74..69c5a1831 100644 --- a/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx +++ b/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx @@ -4,17 +4,200 @@ import AlertComponent from "../alert/alert.component"; import "./form-fields-changed.styles.scss"; import Prompt from "../../utils/prompt"; -export default function FormsFieldChanged({ form, skipPrompt }) { +export default function FormsFieldChanged({ form, skipPrompt, onErrorNavigate, onReset, onDirtyChange }) { const { t } = useTranslation(); + const normalizeNamePath = (namePath) => (Array.isArray(namePath) ? namePath.filter((part) => part !== undefined) : [namePath]); + + const getFieldIdCandidates = (namePath) => { + const normalizedNamePath = normalizeNamePath(namePath).map((part) => String(part)); + const underscoreId = normalizedNamePath.join("_"); + const dashId = normalizedNamePath.join("-"); + const dotName = normalizedNamePath.join("."); + + return [underscoreId, dashId, dotName].filter(Boolean); + }; + + const clearFormMeta = () => { + const fieldMeta = form.getFieldsError().map(({ name }) => ({ + name, + touched: false, + validating: false, + errors: [], + warnings: [] + })); + + if (fieldMeta.length > 0) { + form.setFields(fieldMeta); + } + + onDirtyChange?.(false); + }; + const handleReset = () => { - form.resetFields(); + if (onReset) { + onReset(); + } else { + form.resetFields(); + } + + window.requestAnimationFrame(() => { + clearFormMeta(); + }); + }; + + const getFieldDomNode = (namePath) => { + const fieldInstance = form.getFieldInstance?.(namePath); + const fieldIdCandidates = getFieldIdCandidates(namePath); + const domCandidates = [ + fieldInstance?.nativeElement, + fieldInstance?.input, + fieldInstance?.resizableTextArea?.textArea, + fieldInstance + ]; + + fieldIdCandidates.forEach((fieldId) => { + const escapedFieldId = CSS.escape(fieldId); + const directNode = document.getElementById(fieldId) || document.querySelector(`#${escapedFieldId}`); + const labelNode = document.querySelector(`label[for="${escapedFieldId}"]`); + const namedNode = document.querySelector(`[name="${escapedFieldId}"]`); + const formItemNode = + directNode?.closest?.(".ant-form-item") || + labelNode?.closest?.(".ant-form-item") || + namedNode?.closest?.(".ant-form-item"); + + domCandidates.push(directNode); + domCandidates.push(namedNode); + domCandidates.push(formItemNode); + domCandidates.push(formItemNode?.querySelector?.("input, textarea, select, .ant-select-selector")); + }); + + return domCandidates.find((candidate) => candidate instanceof HTMLElement) ?? null; + }; + + const waitForAnimationFrames = (frameCount = 1) => + new Promise((resolve) => { + let remainingFrames = frameCount; + const nextFrame = () => { + if (remainingFrames <= 0) { + resolve(); + return; + } + remainingFrames -= 1; + window.requestAnimationFrame(nextFrame); + }; + window.requestAnimationFrame(nextFrame); + }); + + const getFieldOwningTabMeta = (namePath) => { + const fieldDomNode = getFieldDomNode(namePath); + const owningTabPane = fieldDomNode?.closest?.(".ant-tabs-tabpane"); + const paneId = owningTabPane?.getAttribute?.("id") || null; + const owningTabButton = paneId + ? document.querySelector(`[role="tab"][aria-controls="${paneId.replace(/"/g, '\\"')}"]`) + : null; + const tabLabel = owningTabButton?.textContent?.trim() || null; + + return { + owningTabPane, + owningTabButton, + tabLabel + }; + }; + + const openFieldOwningTab = async (namePath) => { + const { owningTabPane, owningTabButton } = getFieldOwningTabMeta(namePath); + if (!owningTabPane || owningTabPane.classList.contains("ant-tabs-tabpane-active")) return false; + + if (!(owningTabButton instanceof HTMLElement)) return false; + + owningTabButton.click(); + + for (let index = 0; index < 24; index += 1) { + await waitForAnimationFrames(); + if (owningTabPane.classList.contains("ant-tabs-tabpane-active")) return true; + } + + return owningTabPane.classList.contains("ant-tabs-tabpane-active"); + }; + + const scrollToErrorField = (namePath) => { + const normalizedNamePath = normalizeNamePath(namePath); + if (!normalizedNamePath.length) return; + + try { + form.scrollToField(normalizedNamePath, { + behavior: "smooth", + block: "center", + focus: true + }); + window.requestAnimationFrame(() => { + const fallbackNode = getFieldDomNode(normalizedNamePath); + fallbackNode?.focus?.(); + }); + return; + } catch { + const fallbackTarget = document.getElementById(normalizedNamePath[0]?.toString?.() ?? ""); + fallbackTarget?.scrollIntoView({ + behavior: "smooth", + block: "center" + }); + } + }; + + const handleErrorClick = async (namePath) => { + const normalizedNamePath = normalizeNamePath(namePath); + if (!normalizedNamePath.length) return; + + const switchedTab = await openFieldOwningTab(normalizedNamePath); + if (!switchedTab) { + const navigationDelayMs = onErrorNavigate?.(normalizedNamePath) ?? 0; + if (navigationDelayMs > 0) { + window.setTimeout(() => { + window.requestAnimationFrame(() => { + scrollToErrorField(normalizedNamePath); + }); + }, navigationDelayMs); + return; + } + } + + await waitForAnimationFrames(switchedTab ? 2 : 1); + scrollToErrorField(normalizedNamePath); }; //if (!form.isFieldsTouched()) return <>; return ( {() => { - const errors = form.getFieldsError().filter((e) => e.errors.length > 0); + const errors = form + .getFieldsError() + .filter((fieldError) => fieldError.errors.length > 0) + .flatMap((fieldError) => { + const tabMeta = getFieldOwningTabMeta(fieldError.name); + + return fieldError.errors.map((errorMessage, errorIndex) => ({ + key: `${(fieldError.name || []).join(".")}-${errorIndex}-${errorMessage}`, + message: errorMessage, + namePath: fieldError.name, + tabLabel: tabMeta.tabLabel + })); + }); + + const groupedErrors = errors.reduce((groups, error) => { + const groupKey = error.tabLabel || "__ungrouped__"; + if (!groups[groupKey]) { + groups[groupKey] = { + key: groupKey, + label: error.tabLabel, + errors: [] + }; + } + groups[groupKey].errors.push(error); + return groups; + }, {}); + const errorGroups = Object.values(groupedErrors); + const hasTabbedErrorGroups = errorGroups.some((group) => Boolean(group.label)); + if (form.isFieldsTouched()) return ( @@ -39,10 +222,35 @@ export default function FormsFieldChanged({ form, skipPrompt }) { {errors.length > 0 && ( -
    {errors.map((e, idx) => e.errors.map((e2, idx2) =>
  • {e2}
  • ))}
+
+ {errorGroups.map((group) => ( +
+ {hasTabbedErrorGroups && group.label ? ( +
{group.label}
+ ) : null} +
    + {group.errors.map((error) => ( +
  • + {Array.isArray(error.namePath) && error.namePath.length > 0 ? ( + + ) : ( + error.message + )} +
  • + ))} +
+
+ ))}
} showIcon diff --git a/client/src/components/form-fields-changed-alert/form-fields-changed.styles.scss b/client/src/components/form-fields-changed-alert/form-fields-changed.styles.scss index 155407907..cb3e3940f 100644 --- a/client/src/components/form-fields-changed-alert/form-fields-changed.styles.scss +++ b/client/src/components/form-fields-changed-alert/form-fields-changed.styles.scss @@ -4,4 +4,47 @@ min-height: unset !important; } } + + &__error-list { + margin: 0; + padding-left: 18px; + } + + &__error-groups { + display: grid; + gap: 10px; + } + + &__error-group { + display: grid; + gap: 4px; + } + + &__error-group-title { + font-weight: 600; + } + + &__error-link { + display: inline; + padding: 0; + border: 0; + background: none; + color: inherit; + font: inherit; + line-height: inherit; + text-align: left; + cursor: pointer; + text-decoration: underline; + text-underline-offset: 2px; + + &:hover { + color: color-mix(in srgb, var(--ant-color-error) 82%, var(--ant-color-text)); + } + + &:focus-visible { + outline: 2px solid color-mix(in srgb, var(--ant-color-error) 32%, transparent); + outline-offset: 2px; + border-radius: 4px; + } + } } diff --git a/client/src/components/form-items-formatted/url-form-item.component.jsx b/client/src/components/form-items-formatted/url-form-item.component.jsx new file mode 100644 index 000000000..c038442fc --- /dev/null +++ b/client/src/components/form-items-formatted/url-form-item.component.jsx @@ -0,0 +1,34 @@ +import { LinkOutlined } from "@ant-design/icons"; +import { Button, Input, Space } from "antd"; +import { forwardRef, useMemo } from "react"; + +const HAS_URL_PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z\d+.-]*:/; +const LOCALHOST_OR_IP_REGEX = /^(localhost|127(?:\.\d{1,3}){3}|\d{1,3}(?:\.\d{1,3}){3})(:\d+)?(\/.*)?$/i; + +const getUrlActionHref = (value) => { + const trimmedValue = String(value ?? "").trim(); + if (!trimmedValue) return null; + + if (HAS_URL_PROTOCOL_REGEX.test(trimmedValue)) return trimmedValue; + if (trimmedValue.startsWith("//")) return `https:${trimmedValue}`; + if (LOCALHOST_OR_IP_REGEX.test(trimmedValue)) return `http://${trimmedValue}`; + + return `https://${trimmedValue}`; +}; + +const FormItemUrl = forwardRef(function FormItemUrl({ value, defaultValue, ...props }, ref) { + const urlActionHref = useMemo(() => getUrlActionHref(value ?? defaultValue), [defaultValue, value]); + + return ( + + + {urlActionHref ? ( + +); + +export const renderConfigListOrEmpty = ({ fields, actionLabel, renderItems }) => + fields.length === 0 ? : renderItems(); + +export const buildSectionActionButton = (key, label, onClick, id) => + buildConfigListActionButton({ key, label, onClick, id }); + +export const renderListOrEmpty = (fields, actionLabel, renderItems) => + renderConfigListOrEmpty({ fields, actionLabel, renderItems }); diff --git a/client/src/components/layout-form-row/inline-validated-form-row.component.jsx b/client/src/components/layout-form-row/inline-validated-form-row.component.jsx new file mode 100644 index 000000000..70910856b --- /dev/null +++ b/client/src/components/layout-form-row/inline-validated-form-row.component.jsx @@ -0,0 +1,47 @@ +import { Form } from "antd"; +import LayoutFormRow from "./layout-form-row.component"; + +export default function InlineValidatedFormRow({ actions, errorNames = [], extraErrors = [], form, ...layoutFormRowProps }) { + const normalizedErrorNames = Array.isArray(errorNames) ? errorNames : [errorNames]; + const normalizedExtraErrors = Array.isArray(extraErrors) ? extraErrors.filter(Boolean) : [extraErrors].filter(Boolean); + + return ( + + {() => { + const fieldErrors = normalizedErrorNames.flatMap((name) => form?.getFieldError?.(name) || []); + const errors = [...new Set([...fieldErrors, ...normalizedExtraErrors])]; + const resolvedClassName = [ + layoutFormRowProps.className, + errors.length > 0 ? "imex-form-row--error" : null + ] + .filter(Boolean) + .join(" "); + + const normalizedActions = Array.isArray(actions) ? actions.filter(Boolean) : [actions].filter(Boolean); + const resolvedActions = + errors.length > 0 + ? [ +
0 ? 8 : 0, + width: "100%", + textAlign: "left" + }} + > + + {normalizedActions.length > 0 ?
{normalizedActions}
: null} +
+ ] + : normalizedActions.length > 0 + ? normalizedActions + : undefined; + + return ; + }} +
+ ); +} diff --git a/client/src/components/layout-form-row/layout-form-row.styles.scss b/client/src/components/layout-form-row/layout-form-row.styles.scss index 3e8674362..2c95c7973 100644 --- a/client/src/components/layout-form-row/layout-form-row.styles.scss +++ b/client/src/components/layout-form-row/layout-form-row.styles.scss @@ -50,11 +50,23 @@ html[data-theme="dark"] { border-color: var(--imex-form-surface-border); } + &.imex-form-row--error.ant-card { + border-color: var(--ant-color-error); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--ant-color-error) 24%, transparent); + } + .ant-card-head { background: var(--imex-form-surface-head); border-bottom-color: var(--imex-form-surface-border); } + &.imex-form-row--error { + .ant-card-head, + .ant-card-actions { + border-color: color-mix(in srgb, var(--ant-color-error) 34%, var(--imex-form-surface-border)); + } + } + &.imex-form-row--compact { .ant-card-head { min-height: 40px; @@ -189,3 +201,13 @@ html[data-theme="dark"] { font-size: var(--ant-font-size); line-height: 1.5; } + +.imex-inline-form-row-errors { + color: var(--ant-color-error); + + .ant-form-item-explain, + .ant-form-item-explain-error, + .ant-form-item-additional { + color: var(--ant-color-error); + } +} diff --git a/client/src/components/shop-employees/shop-employees-form.component.jsx b/client/src/components/shop-employees/shop-employees-form.component.jsx index cd677e6d1..a47e577b3 100644 --- a/client/src/components/shop-employees/shop-employees-form.component.jsx +++ b/client/src/components/shop-employees/shop-employees-form.component.jsx @@ -3,9 +3,8 @@ import { useApolloClient, useMutation, useQuery } from "@apollo/client/react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd"; import ResponsiveTable from "../responsive-table/responsive-table.component"; -import { useForm } from "antd/es/form/Form"; import queryString from "query-string"; -import { useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { useLocation, useNavigate } from "react-router-dom"; @@ -26,8 +25,10 @@ import { DateFormatter } from "../../utils/DateFormatter"; import dayjs from "../../utils/day"; import AlertComponent from "../alert/alert.component"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx"; +import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; +import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { INLINE_TITLE_GROUP_STYLE, @@ -49,9 +50,10 @@ const mapDispatchToProps = () => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export function ShopEmployeesFormComponent({ bodyshop }) { +export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDirty }) { const { t } = useTranslation(); - const [form] = useForm(); + const [internalIsDirty, setInternalIsDirty] = useState(false); + const resolvedIsDirty = typeof isDirty === "boolean" ? isDirty : internalIsDirty; const employeeNumber = Form.useWatch("employee_number", form); const firstName = Form.useWatch("first_name", form); const lastName = Form.useWatch("last_name", form); @@ -66,17 +68,19 @@ export function ShopEmployeesFormComponent({ bodyshop }) { const history = useNavigate(); const search = queryString.parse(useLocation().search); const [deleteVacation] = useMutation(DELETE_VACATION); - const { error, data } = useQuery(QUERY_EMPLOYEE_BY_ID, { + const { error, data, refetch } = useQuery(QUERY_EMPLOYEE_BY_ID, { variables: { id: search.employeeId }, skip: !search.employeeId || search.employeeId === "new", fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); const notification = useNotification(); + const isNewEmployee = search.employeeId === "new"; + const currentEmployeeData = data?.employees_by_pk?.id === search.employeeId ? data.employees_by_pk : null; const employeeTitleName = [firstName, lastName].filter(Boolean).join(" ").trim(); const employeeCardTitle = [employeeNumber, employeeTitleName].filter(Boolean).join(" - ") || - (search.employeeId === "new" ? t("employees.actions.new") : t("bodyshop.labels.employees")); + (isNewEmployee ? t("employees.actions.new") : t("bodyshop.labels.employees")); const { treatments: { Enhanced_Payroll } @@ -86,13 +90,49 @@ export function ShopEmployeesFormComponent({ bodyshop }) { splitKey: bodyshop.imexshopid }); + const updateDirtyState = useCallback( + (nextDirtyState) => { + if (typeof isDirty !== "boolean") { + setInternalIsDirty(nextDirtyState); + } + + onDirtyChange?.(nextDirtyState); + }, + [isDirty, onDirtyChange] + ); + const client = useApolloClient(); - useEffect(() => { - if (data && data.employees_by_pk) form.setFieldsValue(data.employees_by_pk); - else { - form.resetFields(); + const clearEmployeeFormMeta = useCallback(() => { + const fieldMeta = form.getFieldsError().map(({ name }) => ({ + name, + touched: false, + validating: false, + errors: [], + warnings: [] + })); + + if (fieldMeta.length > 0) { + form.setFields(fieldMeta); } - }, [form, data, search.employeeId]); + + updateDirtyState(false); + }, [form, updateDirtyState]); + + const resetEmployeeFormToCurrentData = useCallback(() => { + form.resetFields(); + + if (currentEmployeeData) { + form.setFieldsValue(currentEmployeeData); + } + + window.requestAnimationFrame(() => { + clearEmployeeFormMeta(); + }); + }, [clearEmployeeFormMeta, currentEmployeeData, form]); + + useEffect(() => { + resetEmployeeFormToCurrentData(); + }, [resetEmployeeFormToCurrentData, search.employeeId]); const [updateEmployee] = useMutation(UPDATE_EMPLOYEE); const [insertEmployees] = useMutation(INSERT_EMPLOYEES); @@ -112,6 +152,8 @@ export function ShopEmployeesFormComponent({ bodyshop }) { } }) .then(() => { + updateDirtyState(false); + void refetch(); notification.success({ title: t("employees.successes.save") }); @@ -131,6 +173,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) { variables: { employees: [{ ...values, shopid: bodyshop.id }] }, refetchQueries: ["QUERY_EMPLOYEES"] }).then((r) => { + updateDirtyState(false); search.employeeId = r.data.insert_employees.returning[0].id; history({ search: queryString.stringify(search) }); notification.success({ @@ -199,12 +242,21 @@ export function ShopEmployeesFormComponent({ bodyshop }) { form.submit()} style={{ minWidth: 170 }}> + } > -
+ { + updateDirtyState(form.isFieldsTouched()); + }} + > + { return ( - - + @@ -495,7 +549,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) { > - + ); }) diff --git a/client/src/components/shop-employees/shop-employees-list.component.jsx b/client/src/components/shop-employees/shop-employees-list.component.jsx index 787554877..45e434b5a 100644 --- a/client/src/components/shop-employees/shop-employees-list.component.jsx +++ b/client/src/components/shop-employees/shop-employees-list.component.jsx @@ -8,7 +8,12 @@ import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.com import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import ResponsiveTable from "../responsive-table/responsive-table.component"; -export default function ShopEmployeesListComponent({ loading, employees }) { +export default function ShopEmployeesListComponent({ + loading, + employees, + onRequestEmployeeChange, + selectedEmployeeId +}) { const { t } = useTranslation(); const history = useNavigate(); const search = queryString.parse(useLocation().search); @@ -19,6 +24,11 @@ export default function ShopEmployeesListComponent({ loading, employees }) { }); const navigateToEmployee = (employeeId) => { + if (onRequestEmployeeChange) { + onRequestEmployeeChange(employeeId); + return; + } + history({ search: queryString.stringify({ ...search, @@ -127,7 +137,7 @@ export default function ShopEmployeesListComponent({ loading, employees }) { rowSelection={{ onSelect: (props) => navigateToEmployee(props.id), type: "radio", - selectedRowKeys: [search.employeeId] + selectedRowKeys: [selectedEmployeeId || search.employeeId] }} onChange={handleTableChange} onRow={(record) => { diff --git a/client/src/components/shop-employees/shop-employees.container.jsx b/client/src/components/shop-employees/shop-employees.container.jsx index 59ab1a611..3582ee40c 100644 --- a/client/src/components/shop-employees/shop-employees.container.jsx +++ b/client/src/components/shop-employees/shop-employees.container.jsx @@ -1,10 +1,12 @@ -import { Drawer, Grid } from "antd"; +import { Drawer, Form, Grid } from "antd"; import { useQuery } from "@apollo/client/react"; import queryString from "query-string"; import { connect } from "react-redux"; +import { useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { QUERY_EMPLOYEES } from "../../graphql/employees.queries"; +import useConfirmDirtyFormNavigation from "../../hooks/useConfirmDirtyFormNavigation.jsx"; import AlertComponent from "../alert/alert.component"; import ShopEmployeesFormComponent from "./shop-employees-form.component"; import ShopEmployeesListComponent from "./shop-employees-list.component"; @@ -14,6 +16,8 @@ import "./shop-employees.styles.scss"; const mapStateToProps = createStructuredSelector({}); function ShopEmployeesContainer() { + const [form] = Form.useForm(); + const [isEmployeeFormDirty, setIsEmployeeFormDirty] = useState(false); const location = useLocation(); const navigate = useNavigate(); const search = queryString.parse(location.search); @@ -41,10 +45,28 @@ function ShopEmployeesContainer() { else if (screens.sm) drawerPercentage = bpoints.sm; else if (screens.xs) drawerPercentage = bpoints.xs; - const handleDrawerClose = () => { - delete search.employeeId; + const hasDirtyEmployeeForm = Boolean(search.employeeId) && (isEmployeeFormDirty || form.isFieldsTouched()); + const confirmCloseDirtyEmployee = useConfirmDirtyFormNavigation(hasDirtyEmployeeForm); + + const navigateToEmployee = (employeeId) => { + if (employeeId === search.employeeId) return; + if (!confirmCloseDirtyEmployee()) return; + + const nextSearch = { ...search, employeeId }; + setIsEmployeeFormDirty(false); navigate({ - search: queryString.stringify(search) + search: queryString.stringify(nextSearch) + }); + }; + + const handleDrawerClose = () => { + if (!confirmCloseDirtyEmployee()) return; + + const nextSearch = { ...search }; + delete nextSearch.employeeId; + setIsEmployeeFormDirty(false); + navigate({ + search: queryString.stringify(nextSearch) }); }; @@ -54,7 +76,12 @@ function ShopEmployeesContainer() {
- +
- {hasSelectedEmployee ? : null} + {hasSelectedEmployee ? ( + + ) : null}
); diff --git a/client/src/components/shop-info/shop-info.component.jsx b/client/src/components/shop-info/shop-info.component.jsx index ee7e40bd9..901a14afe 100644 --- a/client/src/components/shop-info/shop-info.component.jsx +++ b/client/src/components/shop-info/shop-info.component.jsx @@ -35,7 +35,7 @@ const mapDispatchToProps = () => ({ }); export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent); -export function ShopInfoComponent({ bodyshop, form, saveLoading }) { +export function ShopInfoComponent({ bodyshop, form, saveLoading, isDirty }) { const { treatments: { CriticalPartsScanning, Enhanced_Payroll } } = useTreatmentsWithConfig({ @@ -165,6 +165,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) { extra={ - ); - const renderListOrEmpty = (fields, actionLabel, renderItems) => - fields.length === 0 ? : renderItems(); + const insuranceCompanies = Form.useWatch(["md_ins_cos"], form) || []; + const duplicateInsuranceCompanyIndexes = getDuplicateIndexSetByNormalizedName(insuranceCompanies, "name"); return (
@@ -99,7 +96,7 @@ export function ShopInfoGeneral({ form }) { - + - + - + - + - + - + - + null}> - + +
+ {t("bodyshop.labels.scoreboardsetup")} +
+
+
+
+
{t("bodyshop.fields.scoreboard_setup.ignore_blocked_days")}
+ + + +
+
+
+ } + wrapTitle + > - + - - - - +
- {[ + <> - , - - - , - - - , - - - , - - - , - - - , - ({ - validator(rule, value) { - if (!value && !getFieldValue(["md_hour_split", "paint"])) { - return Promise.resolve(); - } - if (value + getFieldValue(["md_hour_split", "paint"]) === 1) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.larsplit")); - } - }) - ]} - > - - , - ({ - validator(rule, value) { - if (!value && !getFieldValue(["md_hour_split", "paint"])) { - return Promise.resolve(); - } - if (value + getFieldValue(["md_hour_split", "prep"]) === 1) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.larsplit")); - } - }) - ]} - > - - , - - - , - - - , - - - , - - - , - - - , - - - , - - - , - - - , - - - , - - - , - - - , - - - ]} - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ {t("bodyshop.labels.localmediaserver")} +
+
+
+
+
+ {t("bodyshop.fields.system_settings.local_media_server.enabled")} +
+ + + +
+
+
+ } + wrapTitle + > + + + + + + + + + +
+ + + + + + + + + + - + ); }) @@ -882,8 +1001,14 @@ export function ShopInfoGeneral({ form }) { {renderListOrEmpty(fields, t("bodyshop.actions.add_estimator"), () => fields.map((field, index) => { return ( - - + @@ -974,7 +1099,7 @@ export function ShopInfoGeneral({ form }) { > - + ); }) @@ -1001,7 +1126,7 @@ export function ShopInfoGeneral({ form }) { {renderListOrEmpty(fields, t("bodyshop.actions.add_adjuster"), () => fields.map((field, index) => { return ( - + fields.map((field, index) => { return ( - - + @@ -1166,7 +1293,7 @@ export function ShopInfoGeneral({ form }) { key={`${index}actax`} name={[field.name, "actax"]} > - + - + - + - + - + - + - + - + - + - + ); }) @@ -1260,8 +1387,13 @@ export function ShopInfoGeneral({ form }) { {renderListOrEmpty(fields, t("bodyshop.actions.add_jobline_preset"), () => fields.map((field, index) => { return ( - - + @@ -1355,7 +1487,7 @@ export function ShopInfoGeneral({ form }) { key={`${index}mod_lb_hrs`} name={[field.name, "mod_lb_hrs"]} > - + - + - + - + ); }) @@ -1444,8 +1576,10 @@ export function ShopInfoGeneral({ form }) { {renderListOrEmpty(fields, t("bodyshop.actions.add_parts_order_comment"), () => fields.map((field, index) => { return ( - - + - + ); }) @@ -1538,8 +1672,10 @@ export function ShopInfoGeneral({ form }) { {renderListOrEmpty(fields, t("bodyshop.actions.add_to_email_preset"), () => fields.map((field, index) => { return ( - - + @@ -1601,7 +1737,7 @@ export function ShopInfoGeneral({ form }) { > - + ); }) @@ -1614,3 +1750,23 @@ export function ShopInfoGeneral({ form }) {
); } + +function getDuplicateIndexSetByNormalizedName(list, key) { + const indexes = new Set(); + const firstIndexByValue = new Map(); + + (Array.isArray(list) ? list : []).forEach((item, index) => { + const normalizedValue = (item?.[key] ?? "").toString().trim().toLowerCase(); + if (!normalizedValue) return; + + if (firstIndexByValue.has(normalizedValue)) { + indexes.add(firstIndexByValue.get(normalizedValue)); + indexes.add(index); + return; + } + + firstIndexByValue.set(normalizedValue, index); + }); + + return indexes; +} diff --git a/client/src/components/shop-info/shop-info.intake.component.jsx b/client/src/components/shop-info/shop-info.intake.component.jsx index 182b4c878..cd0d80033 100644 --- a/client/src/components/shop-info/shop-info.intake.component.jsx +++ b/client/src/components/shop-info/shop-info.intake.component.jsx @@ -6,6 +6,7 @@ import { TemplateList } from "../../utils/TemplateConstants"; import ConfigFormTypes from "../config-form-components/config-form-types"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; +import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { INLINE_TITLE_GROUP_STYLE, @@ -32,6 +33,7 @@ export default function ShopInfoIntakeChecklistComponent({ form }) { - ({ + value: TemplateListGenerated[i].key, + label: TemplateListGenerated[i].title + }))} + /> + - , - - - 2 - 3 - - , - - {() => { - return ( - - - {t("bodyshop.labels.2tiername")} - {t("bodyshop.labels.2tiersource")} - - - ); - }} - , - - - , - - - , - - {ReceivableCustomFieldSelect} - , - - {ReceivableCustomFieldSelect} - , - - {ReceivableCustomFieldSelect} - , - { - return { - required: getFieldValue("enforce_class"), - //message: t("general.validation.required"), - type: "array" - }; - } - ]} - > - - , - - - , - InstanceRenderManager({ - imex: ( - - - - ) - }), - - - , - ...(HasFeatureAccess({ featureName: "bills", bodyshop }) - ? [ - InstanceRenderManager({ - imex: ( - - - - ) - }), - - - , - - - - ] - : []), + <> - ] - : []), - ...(HasFeatureAccess({ featureName: "bills", bodyshop }) - ? [ + + - - , - - + - ] - : []), - ...(HasFeatureAccess({ featureName: "export", bodyshop }) - ? [ - ...(ClosingPeriod.treatment === "on" - ? [ - - + + {InstanceRenderManager({ + imex: ( + + + + + + ) + })} + + + + + + {HasFeatureAccess({ featureName: "export", bodyshop }) && + ClosingPeriod.treatment === "on" && ( + + + + + + )} + {HasFeatureAccess({ featureName: "export", bodyshop }) && + ADPPayroll.treatment === "on" && ( + + + + + + )} + {HasFeatureAccess({ featureName: "export", bodyshop }) && + ADPPayroll.treatment === "on" && ( + + + + + + )} + {HasFeatureAccess({ featureName: "export", bodyshop }) && !hasDMSKey && ( + <> + + + + 2 + 3 + + + + + + {() => { + return ( + + + {t("bodyshop.labels.2tiername")} + {t("bodyshop.labels.2tiersource")} + + + ); + }} + + + + { + return { + required: getFieldValue("enforce_class"), + //message: t("general.validation.required"), + type: "array" + }; + } + ]} + > + - - ] - : []), - ...(ADPPayroll.treatment === "on" - ? [ - - - - ] - : []) - ] - : []) - ]} +
+ {InstanceRenderManager({ + imex: ( +
+ {t("bodyshop.labels.qbo_usa")} + + {() => ( + + + + )} + +
+ ) + })} +
+ } + > + + + + + {ReceivableCustomFieldSelect} + + + {ReceivableCustomFieldSelect} + + + {ReceivableCustomFieldSelect} + + + )} + + {HasFeatureAccess({ featureName: "bills", bodyshop }) && ( + + {InstanceRenderManager({ + imex: ( + + + + ) + })} + + + + + + + + )} +
+ {hasDMSKey && ( @@ -465,7 +509,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { label={t("bodyshop.fields.dms.sendmaterialscosting")} name={["cdk_configuration", "sendmaterialscosting"]} > - + {bodyshop.pbs_serialnumber && ( fields.map((field, index) => { return ( - - + @@ -804,7 +853,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { )} - + ); }) @@ -830,8 +879,13 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { {renderListOrEmpty(fields, t("bodyshop.actions.add_profit_center"), () => fields.map((field, index) => { return ( - - + @@ -974,7 +1028,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { /> ]} - + ); }) @@ -3277,90 +3331,110 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { )} - - - - - - - - - - {hasDMSKey && ( - + - - - )} - - - - {InstanceRenderManager({ - imex: [ - , + - , + - , - hasDMSKey ? ( + + + + + {hasDMSKey && ( - ) : null, - - - - ], - rome: null - })} + )} + + + {InstanceRenderManager({ + imex: ( + + + + + + + + + + + + + + {hasDMSKey && ( + + + + )} + + ), + rome: null + })} +
{DmsAp.treatment === "on" && ( @@ -3400,7 +3474,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { rules={[{ required: true }]} name={["md_responsibility_centers", "taxes", "federal_itc", "rate"]} > - + )} @@ -3541,8 +3615,13 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { {renderListOrEmpty(fields, t("bodyshop.actions.newsalestaxcode"), () => fields.map((field, index) => { return ( - - + - + ); }) diff --git a/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.component.jsx b/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.component.jsx index cfa0f589d..d742628eb 100644 --- a/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.component.jsx +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.component.jsx @@ -112,7 +112,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAB", "lbr_adjp"]} > - + - + ); }} @@ -185,7 +185,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAD", "lbr_adjp"]} > - + - + ); }} @@ -258,7 +258,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAE", "lbr_adjp"]} > - + - + ); }} @@ -331,7 +331,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAF", "lbr_adjp"]} > - + - + ); }} @@ -404,7 +404,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAG", "lbr_adjp"]} > - + - + ); }} @@ -477,7 +477,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAM", "lbr_adjp"]} > - + - + ); }} @@ -550,7 +550,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAR", "lbr_adjp"]} > - + - + ); }} @@ -623,7 +623,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAS", "lbr_adjp"]} > - + - + ); }} @@ -790,7 +790,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.materials.mat_adjp")} name={["md_responsibility_centers", "cieca_pfm", "MAPA", "mat_adjp"]} > - + - + ); }} @@ -875,7 +875,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.materials.mat_adjp")} name={["md_responsibility_centers", "cieca_pfm", "MASH", "mat_adjp"]} > - + - + ); }} @@ -2318,7 +2318,7 @@ function getTierTaxFormItems({ typeNum, typeNumIterator, t }) { ]} name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `ty${typeNum}_rate${typeNumIterator}`]} > - + , - + ]; } diff --git a/client/src/components/shop-info/shop-info.roguard.component.jsx b/client/src/components/shop-info/shop-info.roguard.component.jsx index 117f5e92a..66638a9f5 100644 --- a/client/src/components/shop-info/shop-info.roguard.component.jsx +++ b/client/src/components/shop-info/shop-info.roguard.component.jsx @@ -32,7 +32,7 @@ export default function ShopInfoRoGuard({ form }) { } ]} > - + [...new Set((statuses || []).map((item) => item?.trim()).filter(Boolean))]; +const getTranslatedDragRect = (active, delta) => { + const rect = active?.rect?.current?.initial || active?.rect?.current?.translated; + + if (!rect) return null; + + const x = delta?.x || 0; + const y = delta?.y || 0; + + return { + left: rect.left + x, + right: rect.right + x, + top: rect.top + y, + bottom: rect.bottom + y, + width: rect.width, + height: rect.height + }; +}; + +const isPointWithinRect = (point, rect) => { + if (!point || !rect) return false; + + return point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom; +}; + const DraggableStatusTag = ({ label, value, closable, onClose }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: value @@ -99,11 +163,13 @@ const DraggableStatusTag = ({ label, value, closable, onClose }) => { { - if (event.target.closest(".ant-tag-close-icon")) { - event.stopPropagation(); - } + onMouseDown={(event) => { + event.stopPropagation(); + }} + onClick={(event) => { + event.stopPropagation(); }} {...attributes} {...listeners} @@ -117,9 +183,21 @@ const DraggableStatusTag = ({ label, value, closable, onClose }) => { } event.preventDefault(); + event.stopPropagation(); + }} + onClick={(event) => { + if (event.target.closest(".ant-select-selection-item-remove")) { + event.stopPropagation(); + return; + } + + event.stopPropagation(); }} title={labelText} > + + + {labelText} {closable ? ( { ); }; -const SortableStatusesSelect = ({ value, onChange }) => { +const SortableStatusesSelect = ({ value, onChange, mode = "tags", options = [] }) => { const statuses = normalizeStatuses(value); + const isTagsMode = mode === "tags"; + const [knownStatuses, setKnownStatuses] = useState(statuses); + const selectWrapperRef = useRef(null); + const dragRectRef = useRef(null); const tagSensors = useSensors( useSensor(PointerSensor, { activationConstraint: { @@ -151,16 +233,75 @@ const SortableStatusesSelect = ({ value, onChange }) => { ); const handleStatusesChange = (nextValues) => { - onChange?.(normalizeStatuses(nextValues)); + const normalizedNextValues = normalizeStatuses(nextValues); + if (isTagsMode) { + setKnownStatuses((currentKnownStatuses) => normalizeStatuses([...currentKnownStatuses, ...normalizedNextValues])); + } + onChange?.(normalizedNextValues); }; - const handleStatusSortEnd = ({ active, over }) => { - if (!over || active.id === over.id) return; + useEffect(() => { + if (isTagsMode) { + setKnownStatuses((currentKnownStatuses) => normalizeStatuses([...currentKnownStatuses, ...statuses])); + } + }, [isTagsMode, statuses]); + const shouldMoveStatusToEnd = (activeId, dragRect) => { + const selectRect = + selectWrapperRef.current?.querySelector?.(".ant-select-selector")?.getBoundingClientRect?.() || + selectWrapperRef.current?.getBoundingClientRect?.(); + if (!dragRect || !selectRect) return false; + + const dragLeadingPoint = { + x: dragRect.left, + y: dragRect.top + }; + const dragTrailingPoint = { + x: dragRect.right, + y: dragRect.bottom + }; + + if (!isPointWithinRect(dragLeadingPoint, selectRect) && !isPointWithinRect(dragTrailingPoint, selectRect)) { + return false; + } + + const trailingStatus = statuses.filter((status) => status !== activeId).at(-1); + if (!trailingStatus) return false; + + const trailingTagNode = selectWrapperRef.current?.querySelector?.( + `.job-statuses-source-tag-wrapper[data-status-tag-value="${CSS.escape(String(trailingStatus))}"]` + ); + const trailingTagRect = trailingTagNode?.getBoundingClientRect?.(); + + if (!trailingTagRect) return false; + + const isOnTrailingRow = dragRect.bottom >= trailingTagRect.top && dragRect.top <= trailingTagRect.bottom; + if (isOnTrailingRow) { + return dragRect.left >= trailingTagRect.right - 4; + } + + return dragRect.top >= trailingTagRect.bottom - 4; + }; + + const handleStatusSortEnd = ({ active, over, delta }) => { const oldIndex = statuses.indexOf(active.id); + const dragRect = dragRectRef.current || getTranslatedDragRect(active, delta); + dragRectRef.current = null; + + if (oldIndex < 0) return; + + if (!over) { + if (oldIndex !== statuses.length - 1 && shouldMoveStatusToEnd(active.id, dragRect)) { + onChange?.(arrayMove(statuses, oldIndex, statuses.length - 1)); + } + return; + } + + if (active.id === over.id) return; + const newIndex = statuses.indexOf(over.id); - if (oldIndex < 0 || newIndex < 0) return; + if (newIndex < 0) return; onChange?.(arrayMove(statuses, oldIndex, newIndex)); }; @@ -169,18 +310,50 @@ const SortableStatusesSelect = ({ value, onChange }) => { return ; }; + const statusSelectOptions = isTagsMode + ? knownStatuses.map((status) => ({ + value: status, + label: status + })) + : options; + + if (statuses.length === 0) { + return ( + - - +
+ { + dragRectRef.current = null; + }} + onDragEnd={handleStatusSortEnd} + onDragMove={({ active, delta }) => { + dragRectRef.current = getTranslatedDragRect(active, delta); + }} + sensors={tagSensors} + > + + + - + - + - - - - - - - - - - + + + option?.searchLabel?.toLowerCase().includes(input.toLowerCase())} onChange={handleSectionChange} />
@@ -120,6 +120,77 @@ function getOwnCardTitleNode(card) { return headNode?.querySelector(".ant-card-head-title"); } +function getOwnCardTitle(card) { + return getOwnCardTitleNode(card)?.textContent?.trim(); +} + +function getAncestorCards(card, activePane) { + const ancestors = []; + let currentCard = card.parentElement?.closest(".imex-form-row"); + + while (currentCard && activePane.contains(currentCard)) { + ancestors.push(currentCard); + currentCard = currentCard.parentElement?.closest(".imex-form-row"); + } + + return ancestors.reverse(); +} + +function getCardDepth(card, activePane) { + return getAncestorCards(card, activePane).length; +} + +function isVisibleCard(card) { + return card.offsetParent !== null; +} + +function isNavigatorEligibleSubsection(card) { + return ( + !card.classList.contains("imex-form-row--compact") && + !card.classList.contains("imex-form-row--title-only") && + !card.querySelector(":scope > .ant-card-actions") + ); +} + +function shouldIncludeCardInNavigator(card, activePane) { + const title = getOwnCardTitle(card); + if (!title || !isVisibleCard(card)) return false; + + const depth = getCardDepth(card, activePane); + if (depth === 0) return true; + if (depth === 1) return isNavigatorEligibleSubsection(card); + + return false; +} + +function getCardNavigatorInfo(card, activePane) { + const title = getOwnCardTitle(card); + const ancestors = getAncestorCards(card, activePane); + const depth = ancestors.length; + const parentTitle = depth === 1 ? getOwnCardTitle(ancestors[0]) : null; + + return { + title, + depth, + searchLabel: parentTitle ? `${parentTitle} ${title}` : title + }; +} + +function renderNavigatorOptionLabel(title, depth) { + return ( + 0 ? "shop-info-section-navigator__option--subsection" : null + ] + .filter(Boolean) + .join(" ")} + > + {title} + + ); +} + function clearHighlightedTarget(highlightedTargetRef) { if (highlightedTargetRef.current) { highlightedTargetRef.current.classList.remove(HIGHLIGHT_CLASS); @@ -132,6 +203,11 @@ function areOptionsEqual(currentOptions, nextOptions) { return currentOptions.every((option, index) => { const nextOption = nextOptions[index]; - return option.label === nextOption.label && option.value === nextOption.value; + return ( + option.labelText === nextOption.labelText && + option.searchLabel === nextOption.searchLabel && + option.depth === nextOption.depth && + option.value === nextOption.value + ); }); } diff --git a/client/src/components/shop-info/shop-info.section-navigator.styles.scss b/client/src/components/shop-info/shop-info.section-navigator.styles.scss index eff4d1a0f..ba9a1b9d0 100644 --- a/client/src/components/shop-info/shop-info.section-navigator.styles.scss +++ b/client/src/components/shop-info/shop-info.section-navigator.styles.scss @@ -7,6 +7,32 @@ } } +.shop-info-section-navigator__option { + display: inline-flex; + align-items: center; + min-height: 24px; +} + +.shop-info-section-navigator__option--subsection { + position: relative; + padding-left: 18px; +} + +.shop-info-section-navigator__option--subsection::before { + content: ""; + position: absolute; + left: 6px; + top: 50%; + width: 8px; + height: 1px; + background: var(--ant-colorTextDescription); + transform: translateY(-50%); +} + +.shop-info-section-navigator__option-label { + display: inline-block; +} + .imex-form-row.shop-info-section-navigator__target--active.ant-card { border-color: color-mix( in srgb, diff --git a/client/src/components/shop-info/shop-info.speedprint.component.jsx b/client/src/components/shop-info/shop-info.speedprint.component.jsx index 33b5e55fe..4498170d1 100644 --- a/client/src/components/shop-info/shop-info.speedprint.component.jsx +++ b/client/src/components/shop-info/shop-info.speedprint.component.jsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { TemplateList } from "../../utils/TemplateConstants"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; +import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { INLINE_TITLE_GROUP_STYLE, @@ -17,6 +18,7 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr"; export default function ShopInfoSpeedPrint() { const { t } = useTranslation(); + const form = Form.useFormInstance(); const allTemplates = TemplateList("job"); const TemplateListGenerated = InstanceRenderManager({ imex: Object.fromEntries(Object.entries(allTemplates).filter(([, { enhanced_payroll }]) => !enhanced_payroll)), @@ -48,8 +50,13 @@ export default function ShopInfoSpeedPrint() { ) : ( fields.map((field, index) => { return ( - - + @@ -140,7 +147,7 @@ export default function ShopInfoSpeedPrint() { }))} /> - +
); }) diff --git a/client/src/components/shop-info/shop-info.task-presets.component.jsx b/client/src/components/shop-info/shop-info.task-presets.component.jsx index d3d3cb66a..987451f5e 100644 --- a/client/src/components/shop-info/shop-info.task-presets.component.jsx +++ b/client/src/components/shop-info/shop-info.task-presets.component.jsx @@ -257,7 +257,7 @@ export function ShopInfoTaskPresets({ bodyshop }) { ]} name={[field.name, "percent"]} > - + { return `${numericValue.toFixed(2).replace(/\.?0+$/, "")}%`; }; -export function ShopEmployeeTeamsFormComponent({ bodyshop }) { +export function ShopEmployeeTeamsFormComponent({ bodyshop, form, onDirtyChange, isDirty }) { const { t } = useTranslation(); - const [form] = Form.useForm(); + const [internalForm] = Form.useForm(); + const [internalIsDirty, setInternalIsDirty] = useState(false); + const teamForm = form ?? internalForm; + const resolvedIsDirty = typeof isDirty === "boolean" ? isDirty : internalIsDirty; const history = useNavigate(); const search = querystring.parse(useLocation().search); const notification = useNotification(); const [hydratedTeamId, setHydratedTeamId] = useState(search.employeeTeamId === "new" ? "new" : null); const isNewTeam = search.employeeTeamId === "new"; - const { error, data, loading } = useQuery(QUERY_EMPLOYEE_TEAM_BY_ID, { + const { error, data, loading, refetch } = useQuery(QUERY_EMPLOYEE_TEAM_BY_ID, { variables: { id: search.employeeTeamId }, skip: !search.employeeTeamId || isNewTeam, fetchPolicy: "network-only", @@ -78,38 +83,71 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) { notifyOnNetworkStatusChange: true }); - useEffect(() => { - if (!search.employeeTeamId) return; + const currentTeamData = data?.employee_teams_by_pk?.id === search.employeeTeamId ? data.employee_teams_by_pk : null; + + const updateDirtyState = useCallback( + (nextDirtyState) => { + if (typeof isDirty !== "boolean") { + setInternalIsDirty(nextDirtyState); + } + + onDirtyChange?.(nextDirtyState); + }, + [isDirty, onDirtyChange] + ); + + const clearTeamFormMeta = useCallback(() => { + const fieldMeta = teamForm.getFieldsError().map(({ name }) => ({ + name, + touched: false, + validating: false, + errors: [], + warnings: [] + })); + + if (fieldMeta.length > 0) { + teamForm.setFields(fieldMeta); + } + + updateDirtyState(false); + }, [teamForm, updateDirtyState]); + + const resetTeamFormToCurrentData = useCallback(() => { + let hydrationFrameId; + + teamForm.resetFields(); if (isNewTeam) { - form.resetFields(); setHydratedTeamId("new"); - return; + hydrationFrameId = window.requestAnimationFrame(() => { + clearTeamFormMeta(); + }); + return () => { + if (hydrationFrameId) window.cancelAnimationFrame(hydrationFrameId); + }; } setHydratedTeamId(null); - }, [form, isNewTeam, search.employeeTeamId]); - useEffect(() => { - if (!search.employeeTeamId || isNewTeam || loading) return; - let hydrationFrameId; - - if (data?.employee_teams_by_pk?.id === search.employeeTeamId) { - form.setFieldsValue(normalizeEmployeeTeam(data.employee_teams_by_pk)); - hydrationFrameId = window.requestAnimationFrame(() => { - setHydratedTeamId(search.employeeTeamId); - }); - } else { - form.resetFields(); - hydrationFrameId = window.requestAnimationFrame(() => { - setHydratedTeamId(search.employeeTeamId); - }); + if (loading) { + return undefined; } + if (currentTeamData) { + teamForm.setFieldsValue(normalizeEmployeeTeam(currentTeamData)); + } + + hydrationFrameId = window.requestAnimationFrame(() => { + setHydratedTeamId(search.employeeTeamId); + clearTeamFormMeta(); + }); + return () => { if (hydrationFrameId) window.cancelAnimationFrame(hydrationFrameId); }; - }, [data, form, isNewTeam, loading, search.employeeTeamId]); + }, [clearTeamFormMeta, currentTeamData, isNewTeam, loading, search.employeeTeamId, teamForm]); + + useEffect(() => resetTeamFormToCurrentData(), [resetTeamFormToCurrentData]); const [updateEmployeeTeam] = useMutation(UPDATE_EMPLOYEE_TEAM); const [insertEmployeeTeam] = useMutation(INSERT_EMPLOYEE_TEAM); @@ -117,8 +155,8 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) { label: t(labelKey), value })); - const teamName = Form.useWatch("name", form); - const teamMembers = Form.useWatch(["employee_team_members"], form) || []; + const teamName = Form.useWatch("name", teamForm); + const teamMembers = Form.useWatch(["employee_team_members"], teamForm) || []; const isTeamHydrating = !isNewTeam && Boolean(search.employeeTeamId) && hydratedTeamId !== search.employeeTeamId; const isAllocationTotalExact = hasExactSplitTotal(teamMembers); const allocationTotalValue = formatAllocationPercentage(getSplitTotal(teamMembers))?.replace("%", "") || "0"; @@ -172,6 +210,8 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) { }); if (!result.errors) { + updateDirtyState(false); + void refetch(); notification.success({ title: t("employees.successes.save") }); @@ -195,6 +235,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) { }, refetchQueries: ["QUERY_TEAMS"] }).then((response) => { + updateDirtyState(false); search.employeeTeamId = response.data.insert_employee_teams_one.id; history({ search: querystring.stringify(search) }); notification.success({ @@ -211,7 +252,12 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) { form.submit()} disabled={isTeamHydrating} style={{ minWidth: 190 }}> + } @@ -219,7 +265,16 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) { {isTeamHydrating ? ( ) : ( -
+ { + updateDirtyState(teamForm.isFieldsTouched()); + }} + > + { return ( - + - @@ -410,7 +471,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) { > {() => { const payoutMethod = - form.getFieldValue(["employee_team_members", field.name, "payout_method"]) || + teamForm.getFieldValue(["employee_team_members", field.name, "payout_method"]) || "hourly"; const fieldName = payoutMethod === "commission" ? "commission_rates" : "labor_rates"; @@ -443,7 +504,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) { }}
-
+
); }) diff --git a/client/src/components/shop-teams/shop-employee-teams.form.component.test.jsx b/client/src/components/shop-teams/shop-employee-teams.form.component.test.jsx index da20b74c5..9208b6a64 100644 --- a/client/src/components/shop-teams/shop-employee-teams.form.component.test.jsx +++ b/client/src/components/shop-teams/shop-employee-teams.form.component.test.jsx @@ -68,6 +68,10 @@ vi.mock("../../contexts/Notifications/notificationContext.jsx", () => ({ useNotification: () => notification })); +vi.mock("../form-fields-changed-alert/form-fields-changed-alert.component.jsx", () => ({ + default: () => null +})); + vi.mock("../../firebase/firebase.utils", () => ({ logImEXEvent: vi.fn() })); diff --git a/client/src/components/shop-teams/shop-employee-teams.list.jsx b/client/src/components/shop-teams/shop-employee-teams.list.jsx index 8cd351201..6acb89f45 100644 --- a/client/src/components/shop-teams/shop-employee-teams.list.jsx +++ b/client/src/components/shop-teams/shop-employee-teams.list.jsx @@ -6,12 +6,22 @@ import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.com import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import ResponsiveTable from "../responsive-table/responsive-table.component"; -export default function ShopEmployeeTeamsListComponent({ loading, employee_teams }) { +export default function ShopEmployeeTeamsListComponent({ + loading, + employee_teams, + onRequestTeamChange, + selectedTeamId +}) { const { t } = useTranslation(); const history = useNavigate(); const search = queryString.parse(useLocation().search); const navigateToTeam = (employeeTeamId) => { + if (onRequestTeamChange) { + onRequestTeamChange(employeeTeamId); + return; + } + history({ search: queryString.stringify({ ...search, @@ -65,7 +75,7 @@ export default function ShopEmployeeTeamsListComponent({ loading, employee_teams rowSelection={{ onSelect: (props) => navigateToTeam(props.id), type: "radio", - selectedRowKeys: [search.employeeTeamId] + selectedRowKeys: [selectedTeamId || search.employeeTeamId] }} onRow={(record) => { return { diff --git a/client/src/components/shop-teams/shop-teams.container.jsx b/client/src/components/shop-teams/shop-teams.container.jsx index 997cc309d..b55534b2d 100644 --- a/client/src/components/shop-teams/shop-teams.container.jsx +++ b/client/src/components/shop-teams/shop-teams.container.jsx @@ -1,9 +1,12 @@ +import { Form } from "antd"; import { useQuery } from "@apollo/client/react"; import queryString from "query-string"; import { connect } from "react-redux"; -import { useLocation } from "react-router-dom"; +import { useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { QUERY_TEAMS } from "../../graphql/employee_teams.queries"; +import useConfirmDirtyFormNavigation from "../../hooks/useConfirmDirtyFormNavigation.jsx"; import AlertComponent from "../alert/alert.component"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import ShopEmployeeTeamsListComponent from "./shop-employee-teams.list"; @@ -13,12 +16,30 @@ import "./shop-teams.styles.scss"; const mapStateToProps = createStructuredSelector({}); function ShopTeamsContainer() { + const [form] = Form.useForm(); + const [isTeamFormDirty, setIsTeamFormDirty] = useState(false); + const navigate = useNavigate(); const search = queryString.parse(useLocation().search); const { loading, error, data } = useQuery(QUERY_TEAMS, { fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); const hasSelectedTeam = Boolean(search.employeeTeamId); + const hasDirtyTeamForm = Boolean(search.employeeTeamId) && isTeamFormDirty; + const confirmCloseDirtyTeam = useConfirmDirtyFormNavigation(hasDirtyTeamForm); + + const navigateToTeam = (employeeTeamId) => { + if (employeeTeamId === search.employeeTeamId) return; + if (!confirmCloseDirtyTeam()) return; + + setIsTeamFormDirty(false); + navigate({ + search: queryString.stringify({ + ...search, + employeeTeamId + }) + }); + }; if (error) return ; @@ -30,11 +51,16 @@ function ShopTeamsContainer() { .join(" ")} >
- +
{hasSelectedTeam ? (
- +
) : null}
diff --git a/client/src/hooks/useConfirmDirtyFormNavigation.jsx b/client/src/hooks/useConfirmDirtyFormNavigation.jsx new file mode 100644 index 000000000..5b77bbf88 --- /dev/null +++ b/client/src/hooks/useConfirmDirtyFormNavigation.jsx @@ -0,0 +1,11 @@ +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; + +export default function useConfirmDirtyFormNavigation(isDirty) { + const { t } = useTranslation(); + + return useCallback(() => { + if (!isDirty) return true; + return window.confirm(t("general.messages.unsavedchangespopup")); + }, [isDirty, t]); +} diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 428894541..b4cfffcf4 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -421,6 +421,35 @@ "logo_img_path": "Shop Logo", "logo_img_path_height": "Logo Image Height", "logo_img_path_width": "Logo Image Width", + "scoreboard_setup": { + "daily_body_target": "Daily Body Target", + "daily_paint_target": "Daily Paint Target", + "ignore_blocked_days": "Ignore Blocked Days", + "last_number_working_days": "Last Number of Working Days", + "production_target_hours": "Production Target Hours" + }, + "system_settings": { + "auto_email": { + "attach_pdf_to_email": "Attach PDF to Sent Emails?", + "from_emails": "Additional From Emails", + "parts_order_cc": "Parts Orders CC", + "parts_return_slip_cc": "Parts Returns CC" + }, + "job_costing": { + "paint_hour_split": "Paint Hour Split", + "paint_materials_hourly_cost_rate": "Paint Materials Hourly Cost Rate", + "prep_hour_split": "Prep Hour Split", + "shop_materials_hourly_cost_rate": "Shop Materials Hourly Cost Rate", + "target_touch_time": "Target Touch Time", + "use_paint_scale_data": "Use Paint Scale Data" + }, + "local_media_server": { + "enabled": "Enabled", + "http_path": "HTTP Path", + "network_path": "Network Path", + "token": "Token" + } + }, "md_categories": "Categories", "md_ccc_rates": "Courtesy Car Contract Rate Presets", "md_classes": "Classes", @@ -621,6 +650,9 @@ "federal_tax_itc": "Federal Tax Credit", "gogcode": "GOG Code (BreakOut)", "gst_override": "GST Override Account #", + "invoice_federal_tax_rate_short": "Federal Tax Rate", + "invoice_local_tax_rate_short": "Local Tax Rate", + "invoice_state_tax_rate_short": "State Tax Rate", "invoiceexemptcode": "QuickBooks US - Invoice Tax Exempt Code", "invoiceexemptcode_short": "Invoice Tax Exempt Code", "item_type": "Item Type", @@ -806,7 +838,9 @@ "responsibilitycenters": { "costs": "Cost Centers", "default_tax_setup": "Default Tax Setup", + "invoices": "Invoices", "profits": "Profit Centers", + "quickbooks_qbd": "QuickBooks / QBD", "quickbooks_us": "QuickBooks US", "sales_tax_codes": "Sales Tax Codes", "tax_accounts": "Tax Accounts", @@ -823,6 +857,9 @@ "roguard": { "title": "RO Guard" }, + "autoemail": "Auto Email", + "jobcosting": "Job Costing", + "localmediaserver": "Local Media Server", "romepay": "Rome Pay", "scheduling": "SMART Scheduling", "scoreboardsetup": "Scoreboard Setup", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 4a12af3c8..55de91164 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -421,6 +421,35 @@ "logo_img_path": "", "logo_img_path_height": "", "logo_img_path_width": "", + "scoreboard_setup": { + "daily_body_target": "", + "daily_paint_target": "", + "ignore_blocked_days": "", + "last_number_working_days": "", + "production_target_hours": "" + }, + "system_settings": { + "auto_email": { + "attach_pdf_to_email": "", + "from_emails": "", + "parts_order_cc": "", + "parts_return_slip_cc": "" + }, + "job_costing": { + "paint_hour_split": "", + "paint_materials_hourly_cost_rate": "", + "prep_hour_split": "", + "shop_materials_hourly_cost_rate": "", + "target_touch_time": "", + "use_paint_scale_data": "" + }, + "local_media_server": { + "enabled": "", + "http_path": "", + "network_path": "", + "token": "" + } + }, "md_categories": "", "md_ccc_rates": "", "md_classes": "", @@ -621,6 +650,9 @@ "federal_tax_itc": "", "gogcode": "", "gst_override": "", + "invoice_federal_tax_rate_short": "", + "invoice_local_tax_rate_short": "", + "invoice_state_tax_rate_short": "", "invoiceexemptcode": "", "invoiceexemptcode_short": "", "item_type": "Item Type", @@ -806,7 +838,9 @@ "responsibilitycenters": { "costs": "", "default_tax_setup": "", + "invoices": "", "profits": "", + "quickbooks_qbd": "", "quickbooks_us": "", "sales_tax_codes": "", "tax_accounts": "", @@ -823,6 +857,9 @@ "roguard": { "title": "" }, + "autoemail": "", + "jobcosting": "", + "localmediaserver": "", "romepay": "", "scheduling": "", "scoreboardsetup": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index b0391e190..76030002e 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -421,6 +421,35 @@ "logo_img_path": "", "logo_img_path_height": "", "logo_img_path_width": "", + "scoreboard_setup": { + "daily_body_target": "", + "daily_paint_target": "", + "ignore_blocked_days": "", + "last_number_working_days": "", + "production_target_hours": "" + }, + "system_settings": { + "auto_email": { + "attach_pdf_to_email": "", + "from_emails": "", + "parts_order_cc": "", + "parts_return_slip_cc": "" + }, + "job_costing": { + "paint_hour_split": "", + "paint_materials_hourly_cost_rate": "", + "prep_hour_split": "", + "shop_materials_hourly_cost_rate": "", + "target_touch_time": "", + "use_paint_scale_data": "" + }, + "local_media_server": { + "enabled": "", + "http_path": "", + "network_path": "", + "token": "" + } + }, "md_categories": "", "md_ccc_rates": "", "md_classes": "", @@ -621,6 +650,9 @@ "federal_tax_itc": "", "gogcode": "", "gst_override": "", + "invoice_federal_tax_rate_short": "", + "invoice_local_tax_rate_short": "", + "invoice_state_tax_rate_short": "", "invoiceexemptcode": "", "invoiceexemptcode_short": "", "item_type": "Item Type", @@ -806,7 +838,9 @@ "responsibilitycenters": { "costs": "", "default_tax_setup": "", + "invoices": "", "profits": "", + "quickbooks_qbd": "", "quickbooks_us": "", "sales_tax_codes": "", "tax_accounts": "", @@ -823,6 +857,9 @@ "roguard": { "title": "" }, + "autoemail": "", + "jobcosting": "", + "localmediaserver": "", "romepay": "", "scheduling": "", "scoreboardsetup": "", From d497ec9f7daf77cf6745cde831b7870d1f683cb6 Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 25 Mar 2026 15:58:51 -0400 Subject: [PATCH 37/56] feature/IO-3624-Shop-Config-UX-Refresh -Final Push! --- .../inline-form-row-title.utils.js | 3 +++ .../shop-employees-form.component.jsx | 14 +++++------ .../shop-info/shop-info.general.component.jsx | 25 ++++++++++--------- .../shop-info/shop-info.intake.component.jsx | 9 ++++--- .../shop-info.laborrates.component.jsx | 7 +++--- .../shop-info/shop-info.parts-scan.jsx | 7 +++--- ...p-info.responsibilitycenters.component.jsx | 11 ++++---- .../shop-info.speedprint.component.jsx | 7 +++--- .../shop-employee-teams.form.component.jsx | 14 +++++------ 9 files changed, 51 insertions(+), 46 deletions(-) diff --git a/client/src/components/layout-form-row/inline-form-row-title.utils.js b/client/src/components/layout-form-row/inline-form-row-title.utils.js index 1647422c1..70c0e8dd2 100644 --- a/client/src/components/layout-form-row/inline-form-row-title.utils.js +++ b/client/src/components/layout-form-row/inline-form-row-title.utils.js @@ -1,3 +1,5 @@ +import { UnorderedListOutlined } from "@ant-design/icons"; + export const inlineFormRowTitleStyles = Object.freeze({ input: Object.freeze({ background: "transparent", @@ -68,6 +70,7 @@ export const inlineFormRowTitleStyles = Object.freeze({ export const INLINE_TITLE_INPUT_STYLE = inlineFormRowTitleStyles.input; export const INLINE_TITLE_ROW_STYLE = inlineFormRowTitleStyles.row; export const INLINE_TITLE_GROUP_STYLE = inlineFormRowTitleStyles.group; +export const InlineTitleListIcon = UnorderedListOutlined; export const INLINE_TITLE_SWITCH_GROUP_STYLE = Object.freeze({ ...inlineFormRowTitleStyles.group, flex: "0 0 auto" diff --git a/client/src/components/shop-employees/shop-employees-form.component.jsx b/client/src/components/shop-employees/shop-employees-form.component.jsx index a47e577b3..5aed82b8e 100644 --- a/client/src/components/shop-employees/shop-employees-form.component.jsx +++ b/client/src/components/shop-employees/shop-employees-form.component.jsx @@ -1,4 +1,4 @@ -import { DeleteFilled, HolderOutlined } from "@ant-design/icons"; +import { DeleteFilled } from "@ant-design/icons"; import { useApolloClient, useMutation, useQuery } from "@apollo/client/react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd"; @@ -38,7 +38,8 @@ import { INLINE_TITLE_ROW_STYLE, INLINE_TITLE_SEPARATOR_STYLE, INLINE_TITLE_SWITCH_GROUP_STYLE, - INLINE_TITLE_TEXT_STYLE + INLINE_TITLE_TEXT_STYLE, + InlineTitleListIcon } from "../layout-form-row/inline-form-row-title.utils.js"; import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component.jsx"; @@ -92,13 +93,10 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi const updateDirtyState = useCallback( (nextDirtyState) => { - if (typeof isDirty !== "boolean") { - setInternalIsDirty(nextDirtyState); - } - + setInternalIsDirty(nextDirtyState); onDirtyChange?.(nextDirtyState); }, - [isDirty, onDirtyChange] + [onDirtyChange] ); const client = useApolloClient(); @@ -482,7 +480,7 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi noDivider title={
- +
{t("employees.fields.cost_center")}
- +
{t("bodyshop.fields.messaginglabel_short")}
- +
{t("bodyshop.fields.noteslabel_short")}
- +
{t("bodyshop.fields.partslocation")}
- +
{t("bodyshop.fields.md_ins_co.name")}
- +
{t("jobs.fields.est_ct_fn_short")}
@@ -1131,7 +1132,7 @@ export function ShopInfoGeneral({ form }) { noDivider title={
- +
{t("jobs.fields.ins_ct_fn_short")}
@@ -1243,7 +1244,7 @@ export function ShopInfoGeneral({ form }) { noDivider title={
- +
{t("general.labels.label")}
- +
{t("general.labels.label")}
- +
{t("general.labels.label")}
- +
{t("general.labels.label")}
- +
{t("jobs.fields.intake.name")}
- +
{t("jobs.fields.intake.name")}
- +
{t("jobs.fields.labor_rate_desc")}
- +
{t("bodyshop.fields.md_parts_scan.field")}
- +
{t("bodyshop.fields.responsibilitycenter")} @@ -889,7 +890,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { noDivider title={
- +
{t("bodyshop.fields.responsibilitycenter")} @@ -3626,7 +3627,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { noDivider title={
- +
{t("bodyshop.fields.responsibilitycenters.sales_tax_codes.description")} diff --git a/client/src/components/shop-info/shop-info.speedprint.component.jsx b/client/src/components/shop-info/shop-info.speedprint.component.jsx index 4498170d1..7dedf39c4 100644 --- a/client/src/components/shop-info/shop-info.speedprint.component.jsx +++ b/client/src/components/shop-info/shop-info.speedprint.component.jsx @@ -1,4 +1,4 @@ -import { DeleteFilled, HolderOutlined } from "@ant-design/icons"; +import { DeleteFilled } from "@ant-design/icons"; import { Button, Form, Input, Select, Space } from "antd"; import { useTranslation } from "react-i18next"; import { TemplateList } from "../../utils/TemplateConstants"; @@ -12,7 +12,8 @@ import { INLINE_TITLE_INPUT_STYLE, INLINE_TITLE_LABEL_STYLE, INLINE_TITLE_ROW_STYLE, - INLINE_TITLE_SEPARATOR_STYLE + INLINE_TITLE_SEPARATOR_STYLE, + InlineTitleListIcon } from "../layout-form-row/inline-form-row-title.utils.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; @@ -60,7 +61,7 @@ export default function ShopInfoSpeedPrint() { noDivider title={
- +
{t("bodyshop.fields.speedprint.id")}
{ - if (typeof isDirty !== "boolean") { - setInternalIsDirty(nextDirtyState); - } - + setInternalIsDirty(nextDirtyState); onDirtyChange?.(nextDirtyState); }, - [isDirty, onDirtyChange] + [onDirtyChange] ); const clearTeamFormMeta = useCallback(() => { @@ -376,7 +374,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop, form, onDirtyChange, grow title={
- +
{t("employee_teams.fields.employeeid")}
Date: Fri, 27 Mar 2026 13:46:28 -0400 Subject: [PATCH 38/56] Bump deps --- client/package-lock.json | 450 ++++++++++++++++++++------------------- client/package.json | 28 +-- package-lock.json | 434 ++++++++++++++++++------------------- package.json | 24 +-- 4 files changed, 457 insertions(+), 479 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index e9c5f52b6..9e8d4ef46 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.1", "hasInstallScript": true, "dependencies": { - "@amplitude/analytics-browser": "^2.37.0", + "@amplitude/analytics-browser": "^2.37.2", "@ant-design/pro-layout": "^7.22.6", "@apollo/client": "^4.1.6", "@dnd-kit/core": "^6.3.1", @@ -25,26 +25,26 @@ "@firebase/messaging": "^0.12.25", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.11.2", - "@sentry/cli": "^3.3.3", - "@sentry/react": "^10.45.0", + "@sentry/cli": "^3.3.4", + "@sentry/react": "^10.46.0", "@sentry/vite-plugin": "^4.9.1", "@splitsoftware/splitio-react": "^2.6.1", "@tanem/react-nprogress": "^5.0.63", - "antd": "^6.3.3", + "antd": "^6.3.4", "apollo-link-logger": "^3.0.0", "autosize": "^6.0.1", "axios": "^1.13.6", "classnames": "^2.5.1", "css-box-model": "^1.2.1", "dayjs": "^1.11.20", - "dayjs-business-days2": "^1.3.2", + "dayjs-business-days2": "^1.3.3", "dinero.js": "^1.9.1", "dotenv": "^17.3.1", "env-cmd": "^11.0.0", "exifr": "^7.1.3", - "graphql": "^16.13.1", + "graphql": "^16.13.2", "graphql-ws": "^6.0.7", - "i18next": "^25.10.5", + "i18next": "^25.10.10", "i18next-browser-languagedetector": "^8.2.1", "immutability-helper": "^3.1.1", "libphonenumber-js": "^1.12.40", @@ -55,7 +55,7 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.71", - "posthog-js": "^1.363.2", + "posthog-js": "^1.363.6", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", @@ -65,8 +65,8 @@ "react-cookie": "^8.0.1", "react-dom": "^19.2.4", "react-grid-gallery": "^1.0.1", - "react-grid-layout": "^2.2.2", - "react-i18next": "^16.6.2", + "react-grid-layout": "^2.2.3", + "react-i18next": "^16.6.6", "react-icons": "^5.6.0", "react-image-lightbox": "^5.1.4", "react-markdown": "^10.1.0", @@ -78,7 +78,7 @@ "react-router-dom": "^7.13.2", "react-sticky": "^6.0.3", "react-virtuoso": "^4.18.3", - "recharts": "^3.8.0", + "recharts": "^3.8.1", "redux": "^5.0.1", "redux-actions": "^3.0.3", "redux-persist": "^6.0.0", @@ -90,10 +90,10 @@ "socket.io-client": "^4.8.3", "styled-components": "^6.3.12", "vite-plugin-ejs": "^1.7.0", - "web-vitals": "^5.1.0" + "web-vitals": "^5.2.0" }, "devDependencies": { - "@ant-design/icons": "^6.1.0", + "@ant-design/icons": "^6.1.1", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.28.5", "@dotenvx/dotenvx": "^1.57.2", @@ -126,7 +126,7 @@ "vite-plugin-node-polyfills": "^0.25.0", "vite-plugin-pwa": "^1.2.0", "vite-plugin-style-import": "^2.0.0", - "vitest": "^4.1.0", + "vitest": "^4.1.2", "workbox-window": "^7.4.0" }, "engines": { @@ -151,18 +151,18 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-browser": { - "version": "2.37.0", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.37.0.tgz", - "integrity": "sha512-/BWDneHRfq6+9bcPQC09Ep79SEj7aRJLZ1jJrPHtxA9KZJUz2au2COlJc1ReCaNzCcrA1xXv/MQ0Fv7TwoBglg==", + "version": "2.37.2", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.37.2.tgz", + "integrity": "sha512-4us5NDIsxsVVi9s6ojS94CO7D5CGcxSpAQg+cPmnATtxCFagxfXfO9I59h2veQ0ohL2nO45nVTKR7RrOyGxA/A==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", - "@amplitude/plugin-autocapture-browser": "1.24.1", - "@amplitude/plugin-custom-enrichment-browser": "0.1.0", - "@amplitude/plugin-network-capture-browser": "1.9.9", - "@amplitude/plugin-page-url-enrichment-browser": "0.7.0", - "@amplitude/plugin-page-view-tracking-browser": "2.9.1", - "@amplitude/plugin-web-vitals-browser": "1.1.24", + "@amplitude/analytics-core": "2.43.1", + "@amplitude/plugin-autocapture-browser": "1.24.2", + "@amplitude/plugin-custom-enrichment-browser": "0.1.1", + "@amplitude/plugin-network-capture-browser": "1.9.10", + "@amplitude/plugin-page-url-enrichment-browser": "0.7.2", + "@amplitude/plugin-page-view-tracking-browser": "2.9.3", + "@amplitude/plugin-web-vitals-browser": "1.1.25", "tslib": "^2.4.1" } }, @@ -173,9 +173,9 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-core": { - "version": "2.43.0", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.43.0.tgz", - "integrity": "sha512-rcDqi4cmI9Ro7hN5wjAuTm92IdN2i0lhIDAj+JOd9BP3SRMrhhiw2lzcScj3owig8CiV9X7EHPTuZe6XCTfIgQ==", + "version": "2.43.1", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.43.1.tgz", + "integrity": "sha512-8Qi0SZ49wNAcLOmtvk6/+a5FvV351G7ctMPyP9A2saZb5I106gb00VWiz1ENQdUG3cNse0iedSlNCjRbX0eksg==", "license": "MIT", "dependencies": { "@amplitude/analytics-connector": "^1.6.4", @@ -186,66 +186,72 @@ } }, "node_modules/@amplitude/plugin-autocapture-browser": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.24.1.tgz", - "integrity": "sha512-cvjOFew2MFNBDTbk3+H7WNi3D0Jdp476m6faCaVhY99M5zqRCHDMRS7dC4HczvL9zYXlAcW9jAWucwES2m3TiQ==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.24.2.tgz", + "integrity": "sha512-IXcCEpd0J5M+Cz1u7RbAANfKr5D5jqkpVrumcOA+zYdmvrd7/1quuFHvR1Dnhk1fipeSwU5TEpadE/RBS80VvQ==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", + "@amplitude/analytics-core": "2.43.1", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-custom-enrichment-browser": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-custom-enrichment-browser/-/plugin-custom-enrichment-browser-0.1.0.tgz", - "integrity": "sha512-y3VmqZvCP1Z3jNgo/mtKVHON9L0P2SyqkMmUsbbFuLu1+TKIkicotnVq/lzlLU1TrW68mkInOM+We8JngasZBA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-custom-enrichment-browser/-/plugin-custom-enrichment-browser-0.1.1.tgz", + "integrity": "sha512-RacUi5M9h1bDUvfRCK3561NP8gK7VuNycvXLPpY/2LDIdzkLMe9sNowghKbr/uLdIySiVcPa+KHCBRfFaJzfJA==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", + "@amplitude/analytics-core": "2.43.1", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-network-capture-browser": { - "version": "1.9.9", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.9.9.tgz", - "integrity": "sha512-SJIOQN04Mk9vCsnVd9QRcIvkMV7XSGZIKfbaKNQY5O3ueV33Kc8opm7YjPg2sWcxdzTcJijbCkOI0wCwOaRolg==", + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.9.10.tgz", + "integrity": "sha512-HubSKD8uzb5cLCGWeiLekC9Qv9NHcIwDQiV34Ju1cFrhTON78xfcg1Wz91aIFX+Hub94zqRv8kE/vjZ1ZU+B6Q==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", + "@amplitude/analytics-core": "2.43.1", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-page-url-enrichment-browser": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.7.0.tgz", - "integrity": "sha512-MkM7TDq24k7ilUDNZISqjDSkVfmDJxWcnUagwYEXjLILhno5hGm7wdgFvVXXzKlZQHEogBxkbnq7wZXS9/YsMw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.7.2.tgz", + "integrity": "sha512-CLcK/e9O0oa5eWruWe6UHhcqyVYK3JG4wsHR+WShG200VqLpNgsDxEM+qNWVYp5Q+aadVYOusik7exxkQayjUg==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", + "@amplitude/analytics-core": "2.43.1", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-page-view-tracking-browser": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.9.1.tgz", - "integrity": "sha512-jkxz2lkJDAfsjj7mpbPUZx9N3qJssC3uYyv8Nk73z+p+v0wjBikWdOoKuNQkcuP09701zRdXp9ziU8+qwkGusw==", + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.9.3.tgz", + "integrity": "sha512-bhsQkGI7D2AQ5EwPLYtZKxdI1jrA/la41ClUX2nHjvmDpY6i7zHVmi7L7xEce4JIF0tb5wUHIvnMt/C2yPBJvA==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", + "@amplitude/analytics-core": "2.43.1", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-web-vitals-browser": { - "version": "1.1.24", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.24.tgz", - "integrity": "sha512-7AaytUK78RKdyDsblYJCKYan1lQi3Qzsp1WHItHJ+RSXPccmi4mCcvNtx0e8T9LmNJlUnsmYeEGR/6FaWvyvFg==", + "version": "1.1.25", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.25.tgz", + "integrity": "sha512-Ror17zbsSx4uroXdAn+53agTqaZ865U/gjwTgoAb9SdOKPmFGC6Xy+ntxaDCSSQYYayb/0+/g0NYe8WC3OuwCQ==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", + "@amplitude/analytics-core": "2.43.1", "tslib": "^2.4.1", "web-vitals": "5.1.0" } }, + "node_modules/@amplitude/plugin-web-vitals-browser/node_modules/web-vitals": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", + "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==", + "license": "Apache-2.0" + }, "node_modules/@ant-design/colors": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", @@ -327,9 +333,9 @@ } }, "node_modules/@ant-design/icons": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.0.tgz", - "integrity": "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.1.tgz", + "integrity": "sha512-AMT4N2y++TZETNHiM77fs4a0uPVCJGuL5MTonk13Pvv7UN7sID1cNEZOc1qNqx6zLKAOilTEFAdAoAFKa0U//Q==", "license": "MIT", "dependencies": { "@ant-design/colors": "^8.0.0", @@ -4873,9 +4879,9 @@ } }, "node_modules/@posthog/types": { - "version": "1.363.2", - "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.363.2.tgz", - "integrity": "sha512-UcUwHEd2LXxWq4bW/I4TbwYcA+BHO/cSuHcNpGXjRCp76eJk1eOuQnm/a3MrfHtbt2X11CQu+eWpqiSgcv+X6A==", + "version": "1.363.6", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.363.6.tgz", + "integrity": "sha512-SPU8psjrpK8prfFyYwvb25F1AgqSM32zdU1XPVIhUa107Cyw+VGw38Gv+AeqkEoAYQE2TFlWJT8DWUrw/mNDoQ==", "license": "MIT" }, "node_modules/@protobufjs/aspromise": { @@ -5139,9 +5145,9 @@ } }, "node_modules/@rc-component/form": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.7.2.tgz", - "integrity": "sha512-5C90rXH7aZvvvxB4M5ew+QxROvimdL/lqhSshR8NsyiR7HKOoGQYSitxdfENnH6/0KNFxEy2ranVe2LrTnHZIw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.0.tgz", + "integrity": "sha512-eUD5KKYnIZWmJwRA0vnyO/ovYUfHGU1svydY1OrqU5fw8Oz9Tdqvxvrlh0wl6xI/EW69dT7II49xpgOWzK3T5A==", "license": "MIT", "dependencies": { "@rc-component/async-validator": "^5.1.0", @@ -5166,14 +5172,14 @@ } }, "node_modules/@rc-component/image": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.6.0.tgz", - "integrity": "sha512-tSfn2ZE/oP082g4QIOxeehkmgnXB7R+5AFj/lIFr4k7pEuxHBdyGIq9axoCY9qea8NN0DY6p4IB/F07tLqaT5A==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.8.0.tgz", + "integrity": "sha512-Dr41bFevLB5NgVaJhEUmNvbEf+ynAhim6W98ZW2xvCsdFISc2TYP4ZvCVdie3eaZdum2kieVcvpNHu+UrzAAHA==", "license": "MIT", "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", - "@rc-component/util": "^1.3.0", + "@rc-component/util": "^1.10.0", "clsx": "^2.1.1" }, "peerDependencies": { @@ -5938,9 +5944,9 @@ } }, "node_modules/@rc-component/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.9.0.tgz", - "integrity": "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.10.0.tgz", + "integrity": "sha512-aY9GLBuiUdpyfIUpAWSYer4Tu3mVaZCo5A0q9NtXcazT3MRiI3/WNHCR+DUn5VAtR6iRRf0ynCqQUcHli5UdYw==", "license": "MIT", "dependencies": { "is-mobile": "^5.0.0", @@ -6712,50 +6718,50 @@ ] }, "node_modules/@sentry-internal/browser-utils": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.45.0.tgz", - "integrity": "sha512-ZPZpeIarXKScvquGx2AfNKcYiVNDA4wegMmjyGVsTA2JPmP0TrJoO3UybJS6KGDeee8V3I3EfD/ruauMm7jOFQ==", + "version": "10.46.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.46.0.tgz", + "integrity": "sha512-WB1gBT9G13V02ekZ6NpUhoI1aGHV2eNfjEPthkU2bGBvFpQKnstwzjg7waIRGR7cu+YSW2Q6UI6aQLgBeOPD1g==", "license": "MIT", "dependencies": { - "@sentry/core": "10.45.0" + "@sentry/core": "10.46.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.45.0.tgz", - "integrity": "sha512-vCSurazFVq7RUeYiM5X326jA5gOVrWYD6lYX2fbjBOMcyCEhDnveNxMT62zKkZDyNT/jyD194nz/cjntBUkyWA==", + "version": "10.46.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.46.0.tgz", + "integrity": "sha512-c4pI/z9nZCQXe9GYEw/hE/YTY9AxGBp8/wgKI+T8zylrN35SGHaXv63szzE1WbI8lacBY8lBF7rstq9bQVCaHw==", "license": "MIT", "dependencies": { - "@sentry/core": "10.45.0" + "@sentry/core": "10.46.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.45.0.tgz", - "integrity": "sha512-vjosRoGA1bzhVAEO1oce+CsRdd70quzBeo7WvYqpcUnoLe/Rv8qpOMqWX3j26z7XfFHMExWQNQeLxmtYOArvlw==", + "version": "10.46.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.46.0.tgz", + "integrity": "sha512-JBsWeXG6bRbxBFK8GzWymWGOB9QE7Kl57BeF3jzgdHTuHSWZ2mRnAmb1K05T4LU+gVygk6yW0KmdC8Py9Qzg9A==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.45.0", - "@sentry/core": "10.45.0" + "@sentry-internal/browser-utils": "10.46.0", + "@sentry/core": "10.46.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.45.0.tgz", - "integrity": "sha512-nvq/AocdZTuD7y0KSiWi3gVaY0s5HOFy86mC/v1kDZmT/jsBAzN5LDkk/f1FvsWma1peqQmpUqxvhC+YIW294Q==", + "version": "10.46.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.46.0.tgz", + "integrity": "sha512-ub314MWUsekVCuoH0/HJbbimlI24SkV745UW2pj9xRbxOAEf1wjkmIzxKrMDbTgJGuEunug02XZVdJFJUzOcDw==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "10.45.0", - "@sentry/core": "10.45.0" + "@sentry-internal/replay": "10.46.0", + "@sentry/core": "10.46.0" }, "engines": { "node": ">=18" @@ -6771,16 +6777,16 @@ } }, "node_modules/@sentry/browser": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.45.0.tgz", - "integrity": "sha512-e/a8UMiQhqqv706McSIcG6XK+AoQf9INthi2pD+giZfNRTzXTdqHzUT5OIO5hg8Am6eF63nDJc+vrYNPhzs51Q==", + "version": "10.46.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.46.0.tgz", + "integrity": "sha512-80DmGlTk5Z2/OxVOzLNxwolMyouuAYKqG8KUcoyintZqHbF6kO1RulI610HmyUt3OagKeBCqt9S7w0VIfCRL+Q==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.45.0", - "@sentry-internal/feedback": "10.45.0", - "@sentry-internal/replay": "10.45.0", - "@sentry-internal/replay-canvas": "10.45.0", - "@sentry/core": "10.45.0" + "@sentry-internal/browser-utils": "10.46.0", + "@sentry-internal/feedback": "10.46.0", + "@sentry-internal/replay": "10.46.0", + "@sentry-internal/replay-canvas": "10.46.0", + "@sentry/core": "10.46.0" }, "engines": { "node": ">=18" @@ -7004,9 +7010,9 @@ } }, "node_modules/@sentry/cli": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-3.3.3.tgz", - "integrity": "sha512-4CZtfgiOraX+BntMjYQhfLDArXwpqt3sEo5Zdj2pqWSZSd4yI3ncfQ21CsxLcI/sUQrjmD5Vzidu4/1OShyxtA==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-3.3.4.tgz", + "integrity": "sha512-r97H1GTdaRs1qhTvbzyomclPesrt4vpjY2W7KGtgSOa5ynQsXKajsM5oJOtNW99O1pNNMZFlR1mmGDMHOxYm4g==", "hasInstallScript": true, "license": "FSL-1.1-MIT", "dependencies": { @@ -7022,20 +7028,20 @@ "node": ">= 18" }, "optionalDependencies": { - "@sentry/cli-darwin": "3.3.3", - "@sentry/cli-linux-arm": "3.3.3", - "@sentry/cli-linux-arm64": "3.3.3", - "@sentry/cli-linux-i686": "3.3.3", - "@sentry/cli-linux-x64": "3.3.3", - "@sentry/cli-win32-arm64": "3.3.3", - "@sentry/cli-win32-i686": "3.3.3", - "@sentry/cli-win32-x64": "3.3.3" + "@sentry/cli-darwin": "3.3.4", + "@sentry/cli-linux-arm": "3.3.4", + "@sentry/cli-linux-arm64": "3.3.4", + "@sentry/cli-linux-i686": "3.3.4", + "@sentry/cli-linux-x64": "3.3.4", + "@sentry/cli-win32-arm64": "3.3.4", + "@sentry/cli-win32-i686": "3.3.4", + "@sentry/cli-win32-x64": "3.3.4" } }, "node_modules/@sentry/cli-darwin": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-3.3.3.tgz", - "integrity": "sha512-P8DoL79eX5fhKCfBHHl7xwwTShDPOb2drJC8lizZ3v1iS1JLPrNweM1KEzDefR30zH1wghbLSwsYv/svWdM3wA==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-3.3.4.tgz", + "integrity": "sha512-1cFHgwJq0yJ4lAvxQISag2R5R/wRtY7e4YX54Da4n+Isw1WHdF2CLmdT0ufOyT04iiF406UD2d7qsPEVDniAmw==", "license": "FSL-1.1-MIT", "optional": true, "os": [ @@ -7046,9 +7052,9 @@ } }, "node_modules/@sentry/cli-linux-arm": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-3.3.3.tgz", - "integrity": "sha512-a7o/huozveLIImXHe0HDwEMVhvDopOP2tLcopvV7sQsVE8f/QOShR5FudKjmiaZz2opdLzPJO9pv5WuF9jAZPg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-3.3.4.tgz", + "integrity": "sha512-8XDDmUZ/4X7Dw2hoSU6T9tpD8qMwtVKHYLQjcY+xNBQujPrSq+YCrNXK/iIN9UgX8Rza2q4IftsIkJADOxLFow==", "cpu": [ "arm" ], @@ -7064,9 +7070,9 @@ } }, "node_modules/@sentry/cli-linux-arm64": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-3.3.3.tgz", - "integrity": "sha512-9jaX9RGyTpjo9u2urNi5ciBDpRdTt107YJpFXev+BFHJ6Lwz/owgRuYzPRfAen8hKkOOFheZ3iy07kl576eZzw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-3.3.4.tgz", + "integrity": "sha512-SD9YQjUPXjIBkt4q41lHMopeL9lKskaxc7qpt1ZuQpoHOszDOUNP3WPvxpeiaMjFMmgMkGojyDBk2XY9eyfGNQ==", "cpu": [ "arm64" ], @@ -7082,9 +7088,9 @@ } }, "node_modules/@sentry/cli-linux-i686": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-3.3.3.tgz", - "integrity": "sha512-VngQYzR2kDm2oojCuYF20ebLTK8HKvEwxe785J6gxob8Ef9JvZkERyUqENYppBa9aVgN0pandqPAqOECWykTMA==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-3.3.4.tgz", + "integrity": "sha512-LNQlRDPrHLTDgxxJsAzT+1+sJ8Kv/Lq8E4Ob8RjqkwuZxl/wR6QJ4O83cxYGJPPnmjEAT+lOUQt1pTPXAwIwLQ==", "cpu": [ "x86", "ia32" @@ -7101,9 +7107,9 @@ } }, "node_modules/@sentry/cli-linux-x64": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-3.3.3.tgz", - "integrity": "sha512-rBxXQeIYGefUNI2cXHxEr0y3bhxDQjOD4G6j/gqLz/Dj+l8gJ/iKP64kTudnoViNIpn0pdYccG69th7zmzM/Fg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-3.3.4.tgz", + "integrity": "sha512-yEOVI4a0RTBYcHJBEaiFU2s4GzcfkXDToMUeLlUg4B3Bgz8AX76163RTEJH5dKavKkoMLKzOrKgVylXPxo1JLQ==", "cpu": [ "x64" ], @@ -7119,9 +7125,9 @@ } }, "node_modules/@sentry/cli-win32-arm64": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-3.3.3.tgz", - "integrity": "sha512-c52g+YS6BO0rzH8AEHqQPmpqZrw0GJjMWqy0tQ5jcqaGdaLVnxk0mMEubv8R6Dv5MR2LShoKjiNsaeVfrWIMUg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-3.3.4.tgz", + "integrity": "sha512-aLGgnIf7FHK+yRsemXGQ1yF0Q4R3D/jwCf/20k1miUgFP9fn5mZt+fArGDHr5k3vFfh3bUTf22Ga4CUwXqwkvQ==", "cpu": [ "arm64" ], @@ -7135,9 +7141,9 @@ } }, "node_modules/@sentry/cli-win32-i686": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-3.3.3.tgz", - "integrity": "sha512-DygYzSY/+tS7oFj/mfeg/yzYxsQx3fO8cI+IWc2pns/at+JcJ9O5xyM/x/q55wOxpnwla7RL1D3rsqK2mqkYfg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-3.3.4.tgz", + "integrity": "sha512-mLD5NpgI3G3+f1iBWGqTTC1kvdQ0CzmkvM9aIRiXUYWXZiaZVd4YuqhoDvTU6zNFEUXI+9jUEp84VF171B0Pqg==", "cpu": [ "x86", "ia32" @@ -7152,9 +7158,9 @@ } }, "node_modules/@sentry/cli-win32-x64": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-3.3.3.tgz", - "integrity": "sha512-i0glPcHwkqbVA2Y+0Yz7CD/l8TSkfft1a+lTU9yk/+DDU8WGkyArEAxAji9bGo4p+k5HIFC8OC2MwpKdcdFM4Q==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-3.3.4.tgz", + "integrity": "sha512-dNWifGo3VLx7n3N3/m+7+rLGNZEb7JmnLwLLAHoz11DneCa6OTBSMCKFABArxqLinlzTbiSOYc8QbvCTcLk5FA==", "cpu": [ "x64" ], @@ -7189,22 +7195,22 @@ } }, "node_modules/@sentry/core": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.45.0.tgz", - "integrity": "sha512-s69UXxvefeQxuZ5nY7/THtTrIEvJxNVCp3ns4kwoCw1qMpgpvn/296WCKVmM7MiwnaAdzEKnAvLAwaxZc2nM7Q==", + "version": "10.46.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.46.0.tgz", + "integrity": "sha512-N3fj4zqBQOhXliS1Ne9euqIKuciHCGOJfPGQLwBoW9DNz03jF+NB8+dUKtrJ79YLoftjVgf8nbgwtADK7NR+2Q==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.45.0.tgz", - "integrity": "sha512-jLezuxi4BUIU3raKyAPR5xMbQG/nhwnWmKo5p11NCbLmWzkS+lxoyDTUB4B8TAKZLfdtdkKLOn1S0tFc8vbUHw==", + "version": "10.46.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.46.0.tgz", + "integrity": "sha512-Rb1S+9OuUPVwsz7GWnQ6Kgf3azbsseUymIegg3JZHNcW/fM1nPpaljzTBnuineia113DH0pgMBcdrrZDLaosFQ==", "license": "MIT", "dependencies": { - "@sentry/browser": "10.45.0", - "@sentry/core": "10.45.0" + "@sentry/browser": "10.46.0", + "@sentry/core": "10.46.0" }, "engines": { "node": ">=18" @@ -7751,31 +7757,31 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", - "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", "chai": "^6.2.2", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", - "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.0", + "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -7784,7 +7790,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -7816,26 +7822,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", - "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", - "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.0", + "@vitest/utils": "4.1.2", "pathe": "^2.0.3" }, "funding": { @@ -7850,14 +7856,14 @@ "license": "MIT" }, "node_modules/@vitest/snapshot": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", - "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -7883,9 +7889,9 @@ "license": "MIT" }, "node_modules/@vitest/spy": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", - "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", "dev": true, "license": "MIT", "funding": { @@ -7893,15 +7899,15 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", - "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", + "@vitest/pretty-format": "4.1.2", "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -8036,9 +8042,9 @@ } }, "node_modules/antd": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/antd/-/antd-6.3.3.tgz", - "integrity": "sha512-T8FAQelw36zS96cZw2U/qEjpYny5yFc7hg+1W7DvVr8xMoSXWvyB8WvmiDVH0nS0LPYV4y2sxetsJoGZt7rhhw==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.3.4.tgz", + "integrity": "sha512-Bu6JivPP7bFfYIdVj+61dxhwSOz+A3m0W7PlDasFGC3H3sNMYQ9gJXZoo11/rQh7pTlOQa351q5Ig/zjI98XYw==", "license": "MIT", "dependencies": { "@ant-design/colors": "^8.0.1", @@ -8055,8 +8061,8 @@ "@rc-component/dialog": "~1.8.4", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", - "@rc-component/form": "~1.7.2", - "@rc-component/image": "~1.6.0", + "@rc-component/form": "~1.8.0", + "@rc-component/image": "~1.8.0", "@rc-component/input": "~1.1.2", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.6.0", @@ -8071,7 +8077,7 @@ "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.1", "@rc-component/segmented": "~1.3.0", - "@rc-component/select": "~1.6.14", + "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", @@ -8084,7 +8090,7 @@ "@rc-component/tree-select": "~1.8.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", - "@rc-component/util": "^1.9.0", + "@rc-component/util": "^1.10.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", @@ -9738,12 +9744,12 @@ "license": "MIT" }, "node_modules/dayjs-business-days2": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dayjs-business-days2/-/dayjs-business-days2-1.3.2.tgz", - "integrity": "sha512-UDJcMw5tM6hoIu8QgP4ASKuVPrnFqB8WMbtfmLe2WNMX/n6zmXfPKwULJS2CKpS+N/+Jdq5Vmo8dkke0sIaV1A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/dayjs-business-days2/-/dayjs-business-days2-1.3.3.tgz", + "integrity": "sha512-ogedXtGep3W1rl/rhzrSbZU7cOA7Cr3s9HY7iiXDqkDv/LarERc1AEI3kvQ+sF43K1HLoXfByu2XQvU7jfqF9w==", "license": "MIT", "dependencies": { - "dayjs": "^1.11.19" + "dayjs": "^1.11.20" } }, "node_modules/debug": { @@ -11411,9 +11417,9 @@ "license": "ISC" }, "node_modules/graphql": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz", - "integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==", + "version": "16.13.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", + "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" @@ -11784,9 +11790,9 @@ } }, "node_modules/i18next": { - "version": "25.10.5", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.5.tgz", - "integrity": "sha512-jRnF7eRNsdcnh7AASSgaU3lj/8lJZuHkfsouetnLEDH0xxE1vVi7qhiJ9RhdSPUyzg4ltb7P7aXsFlTk9sxL2w==", + "version": "25.10.10", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.10.tgz", + "integrity": "sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==", "funding": [ { "type": "individual", @@ -11806,7 +11812,7 @@ "@babel/runtime": "^7.29.2" }, "peerDependencies": { - "typescript": "^5" + "typescript": "^5 || ^6" }, "peerDependenciesMeta": { "typescript": { @@ -15070,9 +15076,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.363.2", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.363.2.tgz", - "integrity": "sha512-4ZEWMrymlFzjgDSmh25VeJQT//2XUFbfKqEPDNUW4dxcqWiVMo1+gJFy5YhJgVYS46OAXLbMcJgmuZBCnDIgVg==", + "version": "1.363.6", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.363.6.tgz", + "integrity": "sha512-eQ+Ypml3JOEMQWt21XEea6J8vD77TNyoz4Yv/xxjlTRja+ilmJtQw/SuVAB3BobjgHKYUomXX7Fc4gH/zTVpbg==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@opentelemetry/api": "^1.9.0", @@ -15081,7 +15087,7 @@ "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.24.1", - "@posthog/types": "1.363.2", + "@posthog/types": "1.363.6", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", @@ -15512,16 +15518,16 @@ } }, "node_modules/react-grid-layout": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.2.tgz", - "integrity": "sha512-yNo9pxQWoxHWRAwHGSVT4DEGELYPyQ7+q9lFclb5jcqeFzva63/2F72CryS/jiTIr/SBIlTaDdyjqH+ODg8oBw==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.3.tgz", + "integrity": "sha512-OAEJHBxmfuxQfVtZwRzmsokijGlBgzYIJ7MUlLk/VSa43SaGzu15w5D0P2RDrfX5EvP9POMbL6bFrai/huDzbQ==", "license": "MIT", "dependencies": { "clsx": "^2.1.1", "fast-equals": "^4.0.3", "prop-types": "^15.8.1", "react-draggable": "^4.4.6", - "react-resizable": "^3.0.5", + "react-resizable": "^3.1.3", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { @@ -15545,9 +15551,9 @@ "license": "MIT" }, "node_modules/react-i18next": { - "version": "16.6.2", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.6.2.tgz", - "integrity": "sha512-/S/GPzElTqEi5o2kzd0/O2627hPDmE6OGhJCCwCfUaQ3syyu+kaYH8/PYFtZeWc25NzfxTN/2fD1QjvrTgrFfA==", + "version": "16.6.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.6.6.tgz", + "integrity": "sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", @@ -15555,9 +15561,9 @@ "use-sync-external-store": "^1.6.0" }, "peerDependencies": { - "i18next": ">= 25.6.2", + "i18next": ">= 25.10.9", "react": ">= 16.8.0", - "typescript": "^5" + "typescript": "^5 || ^6" }, "peerDependenciesMeta": { "react-dom": { @@ -15858,9 +15864,9 @@ } }, "node_modules/recharts": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz", - "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", "license": "MIT", "workspaces": [ "www" @@ -18613,19 +18619,19 @@ } }, "node_modules/vitest": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", - "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.0", - "@vitest/mocker": "4.1.0", - "@vitest/pretty-format": "4.1.0", - "@vitest/runner": "4.1.0", - "@vitest/snapshot": "4.1.0", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -18636,8 +18642,8 @@ "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -18653,13 +18659,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.0", - "@vitest/browser-preview": "4.1.0", - "@vitest/browser-webdriverio": "4.1.0", - "@vitest/ui": "4.1.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -18757,9 +18763,9 @@ } }, "node_modules/web-vitals": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", - "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.2.0.tgz", + "integrity": "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==", "license": "Apache-2.0" }, "node_modules/webidl-conversions": { diff --git a/client/package.json b/client/package.json index 4f03ad75d..295da31ab 100644 --- a/client/package.json +++ b/client/package.json @@ -8,7 +8,7 @@ "private": true, "proxy": "http://localhost:4000", "dependencies": { - "@amplitude/analytics-browser": "^2.37.0", + "@amplitude/analytics-browser": "^2.37.2", "@ant-design/pro-layout": "^7.22.6", "@apollo/client": "^4.1.6", "@dnd-kit/core": "^6.3.1", @@ -24,26 +24,26 @@ "@firebase/messaging": "^0.12.25", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.11.2", - "@sentry/cli": "^3.3.3", - "@sentry/react": "^10.45.0", + "@sentry/cli": "^3.3.4", + "@sentry/react": "^10.46.0", "@sentry/vite-plugin": "^4.9.1", "@splitsoftware/splitio-react": "^2.6.1", "@tanem/react-nprogress": "^5.0.63", - "antd": "^6.3.3", + "antd": "^6.3.4", "apollo-link-logger": "^3.0.0", "autosize": "^6.0.1", "axios": "^1.13.6", "classnames": "^2.5.1", "css-box-model": "^1.2.1", "dayjs": "^1.11.20", - "dayjs-business-days2": "^1.3.2", + "dayjs-business-days2": "^1.3.3", "dinero.js": "^1.9.1", "dotenv": "^17.3.1", "env-cmd": "^11.0.0", "exifr": "^7.1.3", - "graphql": "^16.13.1", + "graphql": "^16.13.2", "graphql-ws": "^6.0.7", - "i18next": "^25.10.5", + "i18next": "^25.10.10", "i18next-browser-languagedetector": "^8.2.1", "immutability-helper": "^3.1.1", "libphonenumber-js": "^1.12.40", @@ -54,7 +54,7 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.71", - "posthog-js": "^1.363.2", + "posthog-js": "^1.363.6", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", @@ -64,8 +64,8 @@ "react-cookie": "^8.0.1", "react-dom": "^19.2.4", "react-grid-gallery": "^1.0.1", - "react-grid-layout": "^2.2.2", - "react-i18next": "^16.6.2", + "react-grid-layout": "^2.2.3", + "react-i18next": "^16.6.6", "react-icons": "^5.6.0", "react-image-lightbox": "^5.1.4", "react-markdown": "^10.1.0", @@ -77,7 +77,7 @@ "react-router-dom": "^7.13.2", "react-sticky": "^6.0.3", "react-virtuoso": "^4.18.3", - "recharts": "^3.8.0", + "recharts": "^3.8.1", "redux": "^5.0.1", "redux-actions": "^3.0.3", "redux-persist": "^6.0.0", @@ -89,7 +89,7 @@ "socket.io-client": "^4.8.3", "styled-components": "^6.3.12", "vite-plugin-ejs": "^1.7.0", - "web-vitals": "^5.1.0" + "web-vitals": "^5.2.0" }, "scripts": { "postinstall": "echo 'when updating react-big-calendar, remember to check to localizer in the calendar wrapper'", @@ -137,7 +137,7 @@ "@rollup/rollup-linux-x64-gnu": "4.6.1" }, "devDependencies": { - "@ant-design/icons": "^6.1.0", + "@ant-design/icons": "^6.1.1", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.28.5", "@dotenvx/dotenvx": "^1.57.2", @@ -170,7 +170,7 @@ "vite-plugin-node-polyfills": "^0.25.0", "vite-plugin-pwa": "^1.2.0", "vite-plugin-style-import": "^2.0.0", - "vitest": "^4.1.0", + "vitest": "^4.1.2", "workbox-window": "^7.4.0" } } diff --git a/package-lock.json b/package-lock.json index 53ba01a44..1aeb8663b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,16 @@ "version": "0.2.0", "license": "UNLICENSED", "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.1016.0", - "@aws-sdk/client-elasticache": "^3.1016.0", - "@aws-sdk/client-s3": "^3.1016.0", - "@aws-sdk/client-secrets-manager": "^3.1016.0", - "@aws-sdk/client-ses": "^3.1016.0", - "@aws-sdk/client-sqs": "^3.1016.0", - "@aws-sdk/client-textract": "^3.1016.0", - "@aws-sdk/credential-provider-node": "^3.972.25", - "@aws-sdk/lib-storage": "^3.1016.0", - "@aws-sdk/s3-request-presigner": "^3.1016.0", + "@aws-sdk/client-cloudwatch-logs": "^3.1018.0", + "@aws-sdk/client-elasticache": "^3.1018.0", + "@aws-sdk/client-s3": "^3.1017.0", + "@aws-sdk/client-secrets-manager": "^3.1018.0", + "@aws-sdk/client-ses": "^3.1017.0", + "@aws-sdk/client-sqs": "^3.1017.0", + "@aws-sdk/client-textract": "^3.1017.0", + "@aws-sdk/credential-provider-node": "^3.972.26", + "@aws-sdk/lib-storage": "^3.1017.0", + "@aws-sdk/s3-request-presigner": "^3.1017.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", @@ -27,7 +27,7 @@ "axios": "^1.13.6", "axios-curlirize": "^2.0.0", "better-queue": "^3.8.12", - "bullmq": "^5.71.0", + "bullmq": "^5.71.1", "chart.js": "^4.5.1", "cloudinary": "^2.9.0", "compression": "^1.8.1", @@ -82,7 +82,7 @@ "p-limit": "^3.1.0", "prettier": "^3.8.1", "supertest": "^7.2.2", - "vitest": "^4.1.1" + "vitest": "^4.1.2" }, "engines": { "node": ">=22.13.0", @@ -292,24 +292,24 @@ } }, "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.1016.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1016.0.tgz", - "integrity": "sha512-ufI94kxGQDZJ+0EsnOjF3ZtTBYF2ZArfb5a8lExFRPwat99pBBAWny8tVpBT1bRfjlxnbBjUTH+ZThmzw1VgJw==", + "version": "3.1018.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1018.0.tgz", + "integrity": "sha512-y1Siaj4PP7PRDtUyjYHQnEl6l2uU2h8zApqcxZnP/ThTy745JFRAKbJRMoAzmV9dPPub3ymCuxVnXm3XYWFbtg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.24", - "@aws-sdk/credential-provider-node": "^3.972.25", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/credential-provider-node": "^3.972.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.25", - "@aws-sdk/region-config-resolver": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.9", + "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.11", + "@aws-sdk/util-user-agent-node": "^3.973.12", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", @@ -345,24 +345,24 @@ } }, "node_modules/@aws-sdk/client-elasticache": { - "version": "3.1016.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.1016.0.tgz", - "integrity": "sha512-Y9hbJsU7QWX1dFMch1zOLRr7Ym6u8jAStP2rKluF9CQAkDSJGIO0xa6IdA2vjnrKh5H2tjC3A35zKJ8dKeJ9fQ==", + "version": "3.1018.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.1018.0.tgz", + "integrity": "sha512-SPkP6CZ8C6CPZmyz0CithnX+Q/GztW0AbjbuUyQkycHSkEAZ7H+DeH+CzzwN1/8rWMd7WJEF7CjE5WFatYh4Qg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.24", - "@aws-sdk/credential-provider-node": "^3.972.25", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/credential-provider-node": "^3.972.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.25", - "@aws-sdk/region-config-resolver": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.9", + "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.11", + "@aws-sdk/util-user-agent-node": "^3.973.12", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", @@ -396,9 +396,9 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1016.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1016.0.tgz", - "integrity": "sha512-E9umet1PolP6I8TpjQQ2W88aIIguyiRQJE98ag6N6QeLgjSZsF+h9l3KclwCRvqUFU68x+HRwrgXxvbIBVFLbA==", + "version": "3.1017.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1017.0.tgz", + "integrity": "sha512-WmmPn2NEfkxxzDA0D7rlf3f32gqmqpaTABhlz4EnZbg/RfNWaOu3ecaI5xY0ragrLhvPB+1aPN9GRDnivJukvg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", @@ -413,11 +413,11 @@ "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-sdk-s3": "^3.972.24", + "@aws-sdk/middleware-sdk-s3": "^3.972.25", "@aws-sdk/middleware-ssec": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.25", "@aws-sdk/region-config-resolver": "^3.972.9", - "@aws-sdk/signature-v4-multi-region": "^3.996.12", + "@aws-sdk/signature-v4-multi-region": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", @@ -462,24 +462,24 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.1016.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1016.0.tgz", - "integrity": "sha512-gDo0HdDDSUVJZICI5eYSkiTrDyKfzZhul88j0+1+JSQhiB6Yld+xVSsRHewkU0yc3w1adbHjQ9p9b0gv9sz04A==", + "version": "3.1018.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1018.0.tgz", + "integrity": "sha512-svV2NpjS+SfMVk8O0b1thuzNlQ5GhIgxAawIPOUCuLf1fLpGhGNgFcgUXlTnkcAoytZpfFdJTBq5JxtV0AgL/A==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.24", - "@aws-sdk/credential-provider-node": "^3.972.25", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/credential-provider-node": "^3.972.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.25", - "@aws-sdk/region-config-resolver": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.9", + "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.11", + "@aws-sdk/util-user-agent-node": "^3.973.12", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", @@ -512,9 +512,9 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.1016.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.1016.0.tgz", - "integrity": "sha512-vIGhTe07ihJs80SIvtWqETrFC0oLVJZ9+mtNuTs1WFeDg8YBGQU1ryaExi9spH2dibE/s4gKhBMFitWcTh1vUQ==", + "version": "3.1017.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.1017.0.tgz", + "integrity": "sha512-2PWXGyniy/j1TZ9sixv5fbudedXV9RihJGq+RjmmNEnlse+oGN7XByFCPLACAvi7Mgr9jjW8Vj1iWB9ezS1zJQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -563,9 +563,9 @@ } }, "node_modules/@aws-sdk/client-sqs": { - "version": "3.1016.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1016.0.tgz", - "integrity": "sha512-31OP7m98ZXuHF7DbOapmjp2hJyrduwwNaZPYrPyDcSTZ+60Qg+F/AUA6snw+QpOPfVnxFqkofhxyBKPPbaQWig==", + "version": "3.1017.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1017.0.tgz", + "integrity": "sha512-Y5FRcAo1lkeOMp6+q7bGSAP3NUdR61VLYzW9J+ksz1KhHLQfCQEzNaTzjwIJyEW2FjJ8w08b/tcScG0Fde0NiA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -615,9 +615,9 @@ } }, "node_modules/@aws-sdk/client-textract": { - "version": "3.1016.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-textract/-/client-textract-3.1016.0.tgz", - "integrity": "sha512-OvG7BO0oBNX2t684v3gB00oL19cm7zxcA00RuacirVciLmhEEokqbcfHa00aa8X1aeiM89q1ZoEKOcd1BnSTFQ==", + "version": "3.1017.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-textract/-/client-textract-3.1017.0.tgz", + "integrity": "sha512-uGwy8wO3qLlvO8OSfnjchBlDYAQx6iHmCOPlEPDF+hhBpd3IUAOXImd0MXK1fbn9gZ/i1i8//1f2UC/PxEah4w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -665,13 +665,13 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.24.tgz", - "integrity": "sha512-vvf82RYQu2GidWAuQq+uIzaPz9V0gSCXVqdVzRosgl5rXcspXOpSD3wFreGGW6AYymPr97Z69kjVnLePBxloDw==", + "version": "3.973.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.25.tgz", + "integrity": "sha512-TNrx7eq6nKNOO62HWPqoBqPLXEkW6nLZQGwjL6lq1jZtigWYbK1NbCnT7mKDzbLMHZfuOECUt3n6CzxjUW9HWQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", - "@aws-sdk/xml-builder": "^3.972.15", + "@aws-sdk/xml-builder": "^3.972.16", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", @@ -702,12 +702,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.22.tgz", - "integrity": "sha512-cXp0VTDWT76p3hyK5D51yIKEfpf6/zsUvMfaB8CkyqadJxMQ8SbEeVroregmDlZbtG31wkj9ei0WnftmieggLg==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.23.tgz", + "integrity": "sha512-EamaclJcCEaPHp6wiVknNMM2RlsPMjAHSsYSFLNENBM8Wz92QPc6cOn3dif6vPDQt0Oo4IEghDy3NMDCzY/IvA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.24", + "@aws-sdk/core": "^3.973.25", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", @@ -718,12 +718,12 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.24.tgz", - "integrity": "sha512-h694K7+tRuepSRJr09wTvQfaEnjzsKZ5s7fbESrVds02GT/QzViJ94/HCNwM7bUfFxqpPXHxulZfL6Cou0dwPg==", + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.25.tgz", + "integrity": "sha512-qPymamdPcLp6ugoVocG1y5r69ScNiRzb0hogX25/ij+Wz7c7WnsgjLTaz7+eB5BfRxeyUwuw5hgULMuwOGOpcw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.24", + "@aws-sdk/core": "^3.973.25", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", @@ -739,19 +739,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.24.tgz", - "integrity": "sha512-O46fFmv0RDFWiWEA9/e6oW92BnsyAXuEgTTasxHligjn2RCr9L/DK773m/NoFaL3ZdNAUz8WxgxunleMnHAkeQ==", + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.25.tgz", + "integrity": "sha512-G/v/PicYn4qs7xCv4vT6I4QKdvMyRvsgIFNBkUueCGlbLo7/PuKcNKgUozmLSsaYnE7jIl6UrfkP07EUubr48w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.24", - "@aws-sdk/credential-provider-env": "^3.972.22", - "@aws-sdk/credential-provider-http": "^3.972.24", - "@aws-sdk/credential-provider-login": "^3.972.24", - "@aws-sdk/credential-provider-process": "^3.972.22", - "@aws-sdk/credential-provider-sso": "^3.972.24", - "@aws-sdk/credential-provider-web-identity": "^3.972.24", - "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/credential-provider-env": "^3.972.23", + "@aws-sdk/credential-provider-http": "^3.972.25", + "@aws-sdk/credential-provider-login": "^3.972.25", + "@aws-sdk/credential-provider-process": "^3.972.23", + "@aws-sdk/credential-provider-sso": "^3.972.25", + "@aws-sdk/credential-provider-web-identity": "^3.972.25", + "@aws-sdk/nested-clients": "^3.996.15", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", @@ -764,13 +764,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.24.tgz", - "integrity": "sha512-sIk8oa6AzDoUhxsR11svZESqvzGuXesw62Rl2oW6wguZx8i9cdGCvkFg+h5K7iucUZP8wyWibUbJMc+J66cu5g==", + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.25.tgz", + "integrity": "sha512-bUdmyJeVua7SmD+g2a65x2/0YqsGn4K2k4GawI43js0odaNaIzpIhLtHehUnPnfLuyhPWbJR1NyuIO4iMVfM0w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.24", - "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/nested-clients": "^3.996.15", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", @@ -783,17 +783,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.25.tgz", - "integrity": "sha512-m7dR0Dsva2P+VUpL+VkC0WwiDby5pgmWXkRVDB5rlwv0jXJrQJf7YMtCoM8Wjk0H9jPeCYOxOXXcIgp/qp5Alg==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.26.tgz", + "integrity": "sha512-5XSK74rCXxCNj+UWv5bjq1EccYkiyW4XOHFU9NXnsCcQF8dJuHdua1qFg0m/LIwVOWklbKsrcnMtfxIXwgvwzQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.22", - "@aws-sdk/credential-provider-http": "^3.972.24", - "@aws-sdk/credential-provider-ini": "^3.972.24", - "@aws-sdk/credential-provider-process": "^3.972.22", - "@aws-sdk/credential-provider-sso": "^3.972.24", - "@aws-sdk/credential-provider-web-identity": "^3.972.24", + "@aws-sdk/credential-provider-env": "^3.972.23", + "@aws-sdk/credential-provider-http": "^3.972.25", + "@aws-sdk/credential-provider-ini": "^3.972.25", + "@aws-sdk/credential-provider-process": "^3.972.23", + "@aws-sdk/credential-provider-sso": "^3.972.25", + "@aws-sdk/credential-provider-web-identity": "^3.972.25", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", @@ -806,12 +806,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.22.tgz", - "integrity": "sha512-Os32s8/4gTZjBk5BtoS/cuTILaj+K72d0dVG7TCJX/fC4598cxwLDmf1AEHEpER5oL3K//yETjvFaz0V8oO5Xw==", + "version": "3.972.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.23.tgz", + "integrity": "sha512-IL/TFW59++b7MpHserjUblGrdP5UXy5Ekqqx1XQkERXBFJcZr74I7VaSrQT5dxdRMU16xGK4L0RQ5fQG1pMgnA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.24", + "@aws-sdk/core": "^3.973.25", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -823,14 +823,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.24.tgz", - "integrity": "sha512-PaFv7snEfypU2yXkpvfyWgddEbDLtgVe51wdZlinhc2doubBjUzJZZpgwuF2Jenl1FBydMhNpMjD6SBUM3qdSA==", + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.25.tgz", + "integrity": "sha512-r4OGAfHmlEa1QBInHWz+/dOD4tRljcjVNQe9wJ/AJNXEj1d2WdsRLppvRFImRV6FIs+bTpjtL0a23V5ELQpRPw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.24", - "@aws-sdk/nested-clients": "^3.996.14", - "@aws-sdk/token-providers": "3.1015.0", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/nested-clients": "^3.996.15", + "@aws-sdk/token-providers": "3.1018.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -842,13 +842,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.24.tgz", - "integrity": "sha512-J6H4R1nvr3uBTqD/EeIPAskrBtET4WFfNhpFySr2xW7bVZOXpQfPjrLSIx65jcNjBmLXzWq8QFLdVoGxiGG/SA==", + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.25.tgz", + "integrity": "sha512-uM1OtoJgj+yK3MlAmda8uR9WJJCdm5HB25JyCeFL5a5q1Fbafalf4uKidFO3/L0Pgd+Fsflkb4cM6jHIswi3QQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.24", - "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/nested-clients": "^3.996.15", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -860,14 +860,16 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.1016.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1016.0.tgz", - "integrity": "sha512-zSHj28SAKCinrxUiJp2fsAFMqc4AWM9WNOOMayysTgOAAticMip9nVcsbcDJCh0PCB9U+ikZdW7OWSCuw7E5xA==", + "version": "3.1017.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1017.0.tgz", + "integrity": "sha512-sWUsr3c46WWZxmBAFSYpI+Gr8tFhP+L3h4XmXZxAyWimHpIeW5+A4wR9SAYohBL/+pPMlN9PEYj3H+KfsYCAyA==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", @@ -877,7 +879,7 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.1016.0" + "@aws-sdk/client-s3": "^3.1017.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { @@ -982,9 +984,9 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", - "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.9.tgz", + "integrity": "sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -998,12 +1000,12 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.24.tgz", - "integrity": "sha512-4sXxVC/enYgMkZefNMOzU6C6KtAXEvwVJLgNcUx1dvROH6GvKB5Sm2RGnGzTp0/PwkibIyMw4kOzF8tbLfaBAQ==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.26.tgz", + "integrity": "sha512-5q7UGSTtt7/KF0Os8wj2VZtlLxeWJVb0e2eDrDJlWot2EIxUNKDDMPFq/FowUqrwZ40rO2bu6BypxaKNvQhI+g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.24", + "@aws-sdk/core": "^3.973.25", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.12", @@ -1054,12 +1056,12 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.25.tgz", - "integrity": "sha512-QxiMPofvOt8SwSynTOmuZfvvPM1S9QfkESBxB22NMHTRXCJhR5BygLl8IXfC4jELiisQgwsgUby21GtXfX3f/g==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.26.tgz", + "integrity": "sha512-AilFIh4rI/2hKyyGN6XrB0yN96W2o7e7wyrPWCM6QjZM1mcC/pVkW3IWWRvuBWMpVP8Fg+rMpbzeLQ6dTM4gig==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.24", + "@aws-sdk/core": "^3.973.25", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.12", @@ -1073,23 +1075,23 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.14.tgz", - "integrity": "sha512-fSESKvh1VbfjtV3QMnRkCPZWkUbQof6T/DOpiLp33yP2wA+rbwwnZeG3XT3Ekljgw2I8X4XaQPnw+zSR8yxJ5Q==", + "version": "3.996.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.15.tgz", + "integrity": "sha512-k6WAVNkub5DrU46iPQvH1m0xc1n+0dX79+i287tYJzf5g1yU2rX3uf4xNeL5JvK1NtYgfwMnsxHqhOXFBn367A==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.24", + "@aws-sdk/core": "^3.973.25", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.25", - "@aws-sdk/region-config-resolver": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.9", + "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.11", + "@aws-sdk/util-user-agent-node": "^3.973.12", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", @@ -1122,9 +1124,9 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.9.tgz", - "integrity": "sha512-eQ+dFU05ZRC/lC2XpYlYSPlXtX3VT8sn5toxN2Fv7EXlMoA2p9V7vUBKqHunfD4TRLpxUq8Y8Ol/nCqiv327Ng==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.10.tgz", + "integrity": "sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -1138,12 +1140,12 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.1016.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1016.0.tgz", - "integrity": "sha512-BROPno9Y8xYltQu5k1AupDPaWdFR9Ig8zfDSZzTE+MTvKpif6wyAHFJRW0C0xIwZckaHya2oFoTZbPHtyIlQkg==", + "version": "3.1017.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1017.0.tgz", + "integrity": "sha512-PSR8VJEkCy53uhAeuvht6ub3kzfdqoTAmLliQJ63MkC/1FuMmrmqWRGoZuzZvAbpzTcZtuibSGvawDa47gsckA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/signature-v4-multi-region": "^3.996.12", + "@aws-sdk/signature-v4-multi-region": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-format-url": "^3.972.8", "@smithy/middleware-endpoint": "^4.4.27", @@ -1157,12 +1159,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.12.tgz", - "integrity": "sha512-abRObSqjVeKUUHIZfAp78PTYrEsxCgVKDs/YET357pzT5C02eDDEvmWyeEC2wglWcYC4UTbBFk22gd2YJUlCQg==", + "version": "3.996.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.14.tgz", + "integrity": "sha512-4nZSrBr1NO+48HCM/6BRU8mnRjuHZjcpziCvLXZk5QVftwWz5Mxqbhwdz4xf7WW88buaTB8uRO2MHklSX1m0vg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.24", + "@aws-sdk/middleware-sdk-s3": "^3.972.26", "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", @@ -1174,13 +1176,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1015.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1015.0.tgz", - "integrity": "sha512-3OSD4y110nisRhHzFOjoEeHU4GQL4KpzkX9PxzWaiZe0Yg2+thZKM0Pn9DjYwezH5JYfh/K++xK/SE0IHGrmCQ==", + "version": "3.1018.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1018.0.tgz", + "integrity": "sha512-97OPNJHy37wmGOX44xAcu6E9oSTiqK9uPcy/fWpmN5uB3JuEp1f6x60Xot/jp+FxwhQWIFUsVJFnm3QKqt7T6Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.24", - "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/nested-clients": "^3.996.15", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -1272,12 +1274,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.11.tgz", - "integrity": "sha512-1qdXbXo2s5MMLpUvw00284LsbhtlQ4ul7Zzdn5n+7p4WVgCMLqhxImpHIrjSoc72E/fyc4Wq8dLtUld2Gsh+lA==", + "version": "3.973.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.12.tgz", + "integrity": "sha512-8phW0TS8ntENJgDcFewYT/Q8dOmarpvSxEjATu2GUBAutiHr++oEGCiBUwxslCMNvwW2cAPZNT53S/ym8zm/gg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.25", + "@aws-sdk/middleware-user-agent": "^3.972.26", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", @@ -1297,9 +1299,9 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.15.tgz", - "integrity": "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA==", + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.16.tgz", + "integrity": "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -3984,31 +3986,31 @@ "license": "MIT" }, "node_modules/@vitest/expect": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz", - "integrity": "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.1", - "@vitest/utils": "4.1.1", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", "chai": "^6.2.2", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.1.tgz", - "integrity": "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.1", + "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -4029,26 +4031,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz", - "integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.1.tgz", - "integrity": "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.1", + "@vitest/utils": "4.1.2", "pathe": "^2.0.3" }, "funding": { @@ -4056,14 +4058,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz", - "integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.1", - "@vitest/utils": "4.1.1", + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -4072,9 +4074,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz", - "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", "dev": true, "license": "MIT", "funding": { @@ -4082,15 +4084,15 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz", - "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.1", + "@vitest/pretty-format": "4.1.2", "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -4719,13 +4721,13 @@ } }, "node_modules/bullmq": { - "version": "5.71.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.71.0.tgz", - "integrity": "sha512-aeNWh4drsafSKnAJeiNH/nZP/5O8ZdtdMbnOPZmpjXj7NZUP5YC901U3bIH41iZValm7d1i3c34ojv7q31m30w==", + "version": "5.71.1", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.71.1.tgz", + "integrity": "sha512-kOBfdcsHmO6wwmIjpersoVdYQ7jkjTgky4Yop0loc7QwSdgxliSzD69U9ijZuRrkyCJwz5p5eqxeGeQkJ0YGZQ==", "license": "MIT", "dependencies": { "cron-parser": "4.9.0", - "ioredis": "5.9.3", + "ioredis": "5.10.1", "msgpackr": "1.11.5", "node-abort-controller": "3.1.1", "semver": "7.7.4", @@ -4733,36 +4735,6 @@ "uuid": "11.1.0" } }, - "node_modules/bullmq/node_modules/@ioredis/commands": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", - "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", - "license": "MIT" - }, - "node_modules/bullmq/node_modules/ioredis": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.3.tgz", - "integrity": "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "1.5.0", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -11620,19 +11592,19 @@ } }, "node_modules/vitest": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.1.tgz", - "integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.1", - "@vitest/mocker": "4.1.1", - "@vitest/pretty-format": "4.1.1", - "@vitest/runner": "4.1.1", - "@vitest/snapshot": "4.1.1", - "@vitest/spy": "4.1.1", - "@vitest/utils": "4.1.1", + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -11643,7 +11615,7 @@ "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", + "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, @@ -11660,10 +11632,10 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.1", - "@vitest/browser-preview": "4.1.1", - "@vitest/browser-webdriverio": "4.1.1", - "@vitest/ui": "4.1.1", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index 0c9d30c2f..3e144902c 100644 --- a/package.json +++ b/package.json @@ -18,16 +18,16 @@ "job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js" }, "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.1016.0", - "@aws-sdk/client-elasticache": "^3.1016.0", - "@aws-sdk/client-s3": "^3.1016.0", - "@aws-sdk/client-secrets-manager": "^3.1016.0", - "@aws-sdk/client-ses": "^3.1016.0", - "@aws-sdk/client-sqs": "^3.1016.0", - "@aws-sdk/client-textract": "^3.1016.0", - "@aws-sdk/credential-provider-node": "^3.972.25", - "@aws-sdk/lib-storage": "^3.1016.0", - "@aws-sdk/s3-request-presigner": "^3.1016.0", + "@aws-sdk/client-cloudwatch-logs": "^3.1018.0", + "@aws-sdk/client-elasticache": "^3.1018.0", + "@aws-sdk/client-s3": "^3.1017.0", + "@aws-sdk/client-secrets-manager": "^3.1018.0", + "@aws-sdk/client-ses": "^3.1017.0", + "@aws-sdk/client-sqs": "^3.1017.0", + "@aws-sdk/client-textract": "^3.1017.0", + "@aws-sdk/credential-provider-node": "^3.972.26", + "@aws-sdk/lib-storage": "^3.1017.0", + "@aws-sdk/s3-request-presigner": "^3.1017.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", @@ -36,7 +36,7 @@ "axios": "^1.13.6", "axios-curlirize": "^2.0.0", "better-queue": "^3.8.12", - "bullmq": "^5.71.0", + "bullmq": "^5.71.1", "chart.js": "^4.5.1", "cloudinary": "^2.9.0", "compression": "^1.8.1", @@ -91,6 +91,6 @@ "p-limit": "^3.1.0", "prettier": "^3.8.1", "supertest": "^7.2.2", - "vitest": "^4.1.1" + "vitest": "^4.1.2" } } From 132fc0a20f0f6b25e8ec206ea410d9490188042c Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 27 Mar 2026 14:36:37 -0400 Subject: [PATCH 39/56] hotfix/2026-03-27 - Missing chatter stuff. --- hasura/metadata/cron_triggers.yaml | 9 +++ hasura/metadata/tables.yaml | 114 +++++++++++++++++++++++++++++ localstack/init/10-bootstrap.sh | 3 +- server/chatter/createLocation.js | 2 +- 4 files changed, 126 insertions(+), 2 deletions(-) diff --git a/hasura/metadata/cron_triggers.yaml b/hasura/metadata/cron_triggers.yaml index 2c8c4c91d..845cb60f6 100644 --- a/hasura/metadata/cron_triggers.yaml +++ b/hasura/metadata/cron_triggers.yaml @@ -24,6 +24,15 @@ - name: x-imex-auth value_from_env: DATAPUMP_AUTH comment: Project Mexico +- name: Chatter API Data Pump + webhook: '{{HASURA_API_URL}}/data/chatter-api' + schedule: 45 4 * * * + include_in_metadata: true + payload: {} + headers: + - name: x-imex-auth + value_from_env: DATAPUMP_AUTH + comment: "" - name: Chatter Data Pump webhook: '{{HASURA_API_URL}}/data/chatter' schedule: 45 5 * * * diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 5d3d3eb85..d6f3f1c25 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -2156,10 +2156,12 @@ - active: _eq: true columns: + - commission_rates - created_at - employeeid - id - labor_rates + - payout_method - percentage - teamid - updated_at @@ -2167,10 +2169,12 @@ - role: user permission: columns: + - commission_rates - created_at - employeeid - id - labor_rates + - payout_method - percentage - teamid - updated_at @@ -2188,10 +2192,12 @@ - role: user permission: columns: + - commission_rates - created_at - employeeid - id - labor_rates + - payout_method - percentage - teamid - updated_at @@ -2560,6 +2566,101 @@ _eq: X-Hasura-User-Id - active: _eq: true +- table: + name: esignature_documents + schema: public + object_relationships: + - name: document + using: + foreign_key_constraint_on: documentid + - name: job + using: + foreign_key_constraint_on: jobid + insert_permissions: + - role: user + permission: + check: + job: + bodyshop: + associations: + _and: + - active: + _eq: true + - user: + authid: + _eq: X-Hasura-User-Id + columns: + - completed + - documentid + - external_document_id + - jobid + - message + - opened + - recipients + - rejected + - status + - subject + - title + comment: "" + select_permissions: + - role: user + permission: + columns: + - completed + - completed_at + - created_at + - documentid + - external_document_id + - id + - jobid + - message + - opened + - recipients + - rejected + - status + - subject + - title + - updated_at + filter: + job: + bodyshop: + associations: + _and: + - active: + _eq: true + - user: + authid: + _eq: X-Hasura-User-Id + comment: "" + update_permissions: + - role: user + permission: + columns: + - completed + - completed_at + - created_at + - documentid + - external_document_id + - message + - opened + - recipients + - rejected + - status + - subject + - title + - updated_at + filter: + job: + bodyshop: + associations: + _and: + - active: + _eq: true + - user: + authid: + _eq: X-Hasura-User-Id + check: null + comment: "" - table: name: eula_acceptances schema: public @@ -3458,6 +3559,13 @@ table: name: email_audit_trail schema: public + - name: esignature_documents + using: + foreign_key_constraint_on: + column: jobid + table: + name: esignature_documents + schema: public - name: exportlogs using: foreign_key_constraint_on: @@ -6506,6 +6614,7 @@ - id - jobid - memo + - payout_context - productivehrs - rate - task_name @@ -6531,6 +6640,7 @@ - id - jobid - memo + - payout_context - productivehrs - rate - task_name @@ -6565,6 +6675,7 @@ - id - jobid - memo + - payout_context - productivehrs - rate - task_name @@ -6748,6 +6859,7 @@ - id - jobid - memo + - payout_context - productivehrs - rate - updated_at @@ -6768,6 +6880,7 @@ - id - jobid - memo + - payout_context - productivehrs - rate - updated_at @@ -6798,6 +6911,7 @@ - id - jobid - memo + - payout_context - productivehrs - rate - updated_at diff --git a/localstack/init/10-bootstrap.sh b/localstack/init/10-bootstrap.sh index ee8183d1e..72a18528a 100755 --- a/localstack/init/10-bootstrap.sh +++ b/localstack/init/10-bootstrap.sh @@ -51,7 +51,8 @@ awslocal ses verify-email-identity --email-address noreply@imex.online --region # Secrets ensure_secret_file "CHATTER_PRIVATE_KEY" "/tmp/certs/io-ftp-test.key" -ensure_secret_string "CHATTER_COMPANY_KEY_6713" "${CHATTER_COMPANY_KEY_6713:-REPLACE_ME}" +ensure_secret_string "CHATTER_COMPANY_KEY_6713" "${CHATTER_COMPANY_KEY_6713}" +ensure_secret_string "CHATTER_COMPANY_KEY_6746" "${CHATTER_COMPANY_KEY_6746}" # Logs ensure_log_group "development" diff --git a/server/chatter/createLocation.js b/server/chatter/createLocation.js index 46766dcf8..18c51305f 100644 --- a/server/chatter/createLocation.js +++ b/server/chatter/createLocation.js @@ -67,7 +67,7 @@ const createLocation = async (req, res) => { const chatterApi = await createChatterClient(DEFAULT_COMPANY_ID); - const locationIdentifier = `${DEFAULT_COMPANY_ID}-${bodyshop.id}`; + const locationIdentifier = bodyshop?.imexshopid ?? `${DEFAULT_COMPANY_ID}-${bodyshop.id}`; const locationPayload = { name: bodyshop.shopname, From 8fd368ebb45a6a29f935799acee8401dfd249b75 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 27 Mar 2026 14:45:01 -0400 Subject: [PATCH 40/56] Revert hasura metadata tables changes --- hasura/metadata/tables.yaml | 938 +++--------------------------------- 1 file changed, 68 insertions(+), 870 deletions(-) diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index d6f3f1c25..7a9de6b10 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -69,6 +69,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: jobline: job: @@ -179,6 +180,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: bodyshop: associations: @@ -198,14 +200,6 @@ - name: user using: foreign_key_constraint_on: useremail - array_relationships: - - name: notifications - using: - foreign_key_constraint_on: - column: associationid - table: - name: notifications - schema: public select_permissions: - role: user permission: @@ -215,9 +209,6 @@ - default_prod_list_view - id - kanban_settings - - new_message_sound - - notification_settings - - notifications_autoadd - qbo_realmId - shopid - useremail @@ -233,9 +224,6 @@ - authlevel - default_prod_list_view - kanban_settings - - new_message_sound - - notification_settings - - notifications_autoadd - qbo_realmId filter: user: @@ -399,6 +387,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: bodyshop: associations: @@ -515,6 +504,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: bill: job: @@ -681,6 +671,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: _and: - job: @@ -695,25 +686,6 @@ - exported: _eq: false event_triggers: - - name: notifications_bills - definition: - enable_manual: false - insert: - columns: '*' - retry_conf: - interval_sec: 10 - num_retries: 0 - timeout_sec: 60 - webhook_from_env: HASURA_API_URL - headers: - - name: event-secret - value_from_env: EVENT_SECRET - request_transform: - method: POST - query_params: {} - template_engine: Kriti - url: '{{$base_url}}/notifications/events/handleBillsChange' - version: 2 - name: os_bills definition: delete: @@ -944,14 +916,10 @@ - autohouseid - bill_allow_post_to_closed - bill_tax_rates - - carfax_exclude - cdk_configuration - cdk_dealerid - - chatter_company_id - - chatterid - city - claimscorpid - - convenient_company - country - created_at - default_adjustment_rate @@ -963,7 +931,6 @@ - enforce_referral - entegral_configuration - entegral_id - - external_shop_id - features - federal_tax_id - id @@ -971,8 +938,6 @@ - inhousevendorid - insurance_vendor_id - intakechecklist - - intellipay_config - - intellipay_merchant_id - jc_hourly_rates - jobsizelimit - last_name_first @@ -1009,16 +974,12 @@ - md_tasks_presets - md_to_emails - messagingservicesid - - notification_followers - pbs_configuration - pbs_serialnumber - phone - - podiumid - prodtargethrs - production_config - region_config - - rr_configuration - - rr_dealerid - schedule_end_time - schedule_start_time - scoreboard_target @@ -1035,7 +996,6 @@ - template_header - textid - timezone - - tours_enabled - tt_allow_post_to_invoiced - tt_enforce_hours_for_tech_console - updated_at @@ -1064,7 +1024,6 @@ - bill_allow_post_to_closed - bill_tax_rates - cdk_configuration - - chatter_company_id - city - country - created_at @@ -1075,13 +1034,11 @@ - enforce_conversion_category - enforce_conversion_csr - enforce_referral - - external_shop_id - federal_tax_id - id - inhousevendorid - insurance_vendor_id - intakechecklist - - intellipay_config - jc_hourly_rates - last_name_first - localmediaserverhttp @@ -1116,12 +1073,10 @@ - md_ro_statuses - md_tasks_presets - md_to_emails - - notification_followers - pbs_configuration - phone - prodtargethrs - production_config - - rr_configuration - schedule_end_time - schedule_start_time - scoreboard_target @@ -1152,50 +1107,6 @@ - active: _eq: true check: null - event_triggers: - - name: cache_bodyshop - definition: - enable_manual: false - update: - columns: - - imexshopid - - timezone - - shopname - - notification_followers - - state - - md_order_statuses - retry_conf: - interval_sec: 10 - num_retries: 0 - timeout_sec: 60 - webhook_from_env: HASURA_API_URL - headers: - - name: event-secret - value_from_env: EVENT_SECRET - request_transform: - body: - action: transform - template: |- - { - "created_at": {{$body.created_at}}, - "delivery_info": {{$body.delivery_info}}, - "event": { - "data": { - "new": { - "id": {{$body.event.data.new.id}}, - "shopname": {{$body.event.data.new.shopname}}, - "md_order_statuses": {{$body.event.data.new.md_order_statuses}} - } - }, - "op": {{$body.event.op}}, - "session_variables": {{$body.event.session_variables}} - } - } - method: POST - query_params: {} - template_engine: Kriti - url: '{{$base_url}}/bodyshop-cache' - version: 2 - table: name: cccontracts schema: public @@ -1370,6 +1281,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: courtesycar: bodyshop: @@ -1610,6 +1522,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: bodyshop: associations: @@ -1869,6 +1782,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: bodyshop: associations: @@ -2002,6 +1916,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: _or: - job: @@ -2021,29 +1936,6 @@ _eq: X-Hasura-User-Id - active: _eq: true - event_triggers: - - name: notifications_documents - definition: - enable_manual: false - insert: - columns: '*' - update: - columns: - - jobid - retry_conf: - interval_sec: 10 - num_retries: 0 - timeout_sec: 60 - webhook_from_env: HASURA_API_URL - headers: - - name: event-secret - value_from_env: EVENT_SECRET - request_transform: - method: POST - query_params: {} - template_engine: Kriti - url: '{{$base_url}}/notifications/events/handleDocumentsChange' - version: 2 - table: name: email_audit_trail schema: public @@ -2156,12 +2048,10 @@ - active: _eq: true columns: - - commission_rates - created_at - employeeid - id - labor_rates - - payout_method - percentage - teamid - updated_at @@ -2169,12 +2059,10 @@ - role: user permission: columns: - - commission_rates - created_at - employeeid - id - labor_rates - - payout_method - percentage - teamid - updated_at @@ -2192,12 +2080,10 @@ - role: user permission: columns: - - commission_rates - created_at - employeeid - id - labor_rates - - payout_method - percentage - teamid - updated_at @@ -2215,6 +2101,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: employee_team: bodyshop: @@ -2377,6 +2264,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: employee: bodyshop: @@ -2447,13 +2335,6 @@ table: name: jobs schema: public - - name: tasks - using: - foreign_key_constraint_on: - column: assigned_to - table: - name: tasks - schema: public - name: timetickets using: foreign_key_constraint_on: @@ -2557,6 +2438,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: bodyshop: associations: @@ -2566,101 +2448,6 @@ _eq: X-Hasura-User-Id - active: _eq: true -- table: - name: esignature_documents - schema: public - object_relationships: - - name: document - using: - foreign_key_constraint_on: documentid - - name: job - using: - foreign_key_constraint_on: jobid - insert_permissions: - - role: user - permission: - check: - job: - bodyshop: - associations: - _and: - - active: - _eq: true - - user: - authid: - _eq: X-Hasura-User-Id - columns: - - completed - - documentid - - external_document_id - - jobid - - message - - opened - - recipients - - rejected - - status - - subject - - title - comment: "" - select_permissions: - - role: user - permission: - columns: - - completed - - completed_at - - created_at - - documentid - - external_document_id - - id - - jobid - - message - - opened - - recipients - - rejected - - status - - subject - - title - - updated_at - filter: - job: - bodyshop: - associations: - _and: - - active: - _eq: true - - user: - authid: - _eq: X-Hasura-User-Id - comment: "" - update_permissions: - - role: user - permission: - columns: - - completed - - completed_at - - created_at - - documentid - - external_document_id - - message - - opened - - recipients - - rejected - - status - - subject - - title - - updated_at - filter: - job: - bodyshop: - associations: - _and: - - active: - _eq: true - - user: - authid: - _eq: X-Hasura-User-Id - check: null - comment: "" - table: name: eula_acceptances schema: public @@ -2796,9 +2583,6 @@ - active: _eq: true allow_aggregations: true -- table: - name: integration_log - schema: public - table: name: inventory schema: public @@ -2901,6 +2685,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: bodyshop: associations: @@ -3012,6 +2797,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: conversation: bodyshop: @@ -3022,76 +2808,6 @@ _eq: X-Hasura-User-Id - active: _eq: true -- table: - name: job_watchers - schema: public - object_relationships: - - name: job - using: - foreign_key_constraint_on: jobid - - name: user - using: - foreign_key_constraint_on: user_email - insert_permissions: - - role: user - permission: - check: - job: - bodyshop: - associations: - user: - authid: - _eq: X-Hasura-User-Id - columns: - - user_email - - created_at - - id - - jobid - comment: "" - select_permissions: - - role: user - permission: - columns: - - user_email - - created_at - - id - - jobid - filter: - job: - bodyshop: - associations: - user: - authid: - _eq: X-Hasura-User-Id - comment: "" - update_permissions: - - role: user - permission: - columns: - - user_email - - created_at - - id - - jobid - filter: - job: - bodyshop: - associations: - user: - authid: - _eq: X-Hasura-User-Id - check: null - comment: "" - delete_permissions: - - role: user - permission: - filter: - job: - bodyshop: - associations: - user: - authid: - _eq: X-Hasura-User-Id - comment: "" - table: name: joblines schema: public @@ -3183,7 +2899,6 @@ - est_seq - glass_flag - id - - include_in_part_cnt - ioucreated - jobid - lbr_amt @@ -3253,7 +2968,6 @@ - est_seq - glass_flag - id - - include_in_part_cnt - ioucreated - jobid - lbr_amt @@ -3334,7 +3048,6 @@ - est_seq - glass_flag - id - - include_in_part_cnt - ioucreated - jobid - lbr_amt @@ -3399,6 +3112,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: job: bodyshop: @@ -3409,31 +3123,6 @@ _eq: X-Hasura-User-Id - active: _eq: true - event_triggers: - - name: notifications_joblines - definition: - enable_manual: false - update: - columns: - - critical - - status - retry_conf: - interval_sec: 10 - num_retries: 0 - timeout_sec: 60 - webhook_from_env: HASURA_API_URL - headers: - - name: event-secret - value_from_env: EVENT_SECRET - request_transform: - body: - action: transform - template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"jobid\": {{$body.event.data.old.jobid}},\r\n \"critical\": {{$body.event.data.old.critical}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"line_desc\": {{$body.event.data.old.line_desc}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"jobid\": {{$body.event.data.new.jobid}},\r\n \"critical\": {{$body.event.data.new.critical}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"line_desc\": {{$body.event.data.new.line_desc}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_joblines\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"joblines\"\r\n }\r\n}\r\n" - method: POST - query_params: {} - template_engine: Kriti - url: '{{$base_url}}/notifications/events/handleJobLinesChange' - version: 2 - table: name: joblines_status schema: public @@ -3559,13 +3248,6 @@ table: name: email_audit_trail schema: public - - name: esignature_documents - using: - foreign_key_constraint_on: - column: jobid - table: - name: esignature_documents - schema: public - name: exportlogs using: foreign_key_constraint_on: @@ -3587,13 +3269,6 @@ table: name: job_conversations schema: public - - name: job_watchers - using: - foreign_key_constraint_on: - column: jobid - table: - name: job_watchers - schema: public - name: joblines using: foreign_key_constraint_on: @@ -3624,13 +3299,6 @@ table: name: notes schema: public - - name: notifications - using: - foreign_key_constraint_on: - column: jobid - table: - name: notifications - schema: public - name: parts_dispatches using: foreign_key_constraint_on: @@ -3724,12 +3392,10 @@ - actual_completion - actual_delivery - actual_in - - acv_amount - adj_g_disc - adj_strdis - adj_towdis - adjustment_bottom_line - - admin_clerk - agt_addr1 - agt_addr2 - agt_city @@ -3794,7 +3460,6 @@ - completed_tasks - converted - created_at - - created_user_email - cust_pr - date_estimated - date_exported @@ -3812,10 +3477,7 @@ - ded_status - deliverchecklist - depreciation_taxes - - dms_advisor_id - dms_allocation - - dms_customer_id - - dms_id - driveable - employee_body - employee_csr @@ -3832,12 +3494,8 @@ - est_ph1 - est_st - est_zip - - estimate_approved - - estimate_sent_approval - federal_tax_rate - - flat_rate_ats - g_bett_amt - - hit_and_run - id - inproduction - ins_addr1 @@ -3911,10 +3569,8 @@ - ownr_fn - ownr_ln - ownr_ph1 - - ownr_ph1_ty - ownr_ph1x - ownr_ph2 - - ownr_ph2_ty - ownr_ph2x - ownr_st - ownr_title @@ -3933,7 +3589,6 @@ - qb_multiple_payers - queued_for_parts - rate_ats - - rate_ats_flat - rate_la1 - rate_la2 - rate_la3 @@ -4006,12 +3661,10 @@ - actual_completion - actual_delivery - actual_in - - acv_amount - adj_g_disc - adj_strdis - adj_towdis - adjustment_bottom_line - - admin_clerk - agt_addr1 - agt_addr2 - agt_city @@ -4076,7 +3729,6 @@ - completed_tasks - converted - created_at - - created_user_email - cust_pr - date_estimated - date_exported @@ -4095,10 +3747,7 @@ - ded_status - deliverchecklist - depreciation_taxes - - dms_advisor_id - dms_allocation - - dms_customer_id - - dms_id - driveable - employee_body - employee_csr @@ -4115,12 +3764,8 @@ - est_ph1 - est_st - est_zip - - estimate_approved - - estimate_sent_approval - federal_tax_rate - - flat_rate_ats - g_bett_amt - - hit_and_run - id - inproduction - ins_addr1 @@ -4195,10 +3840,8 @@ - ownr_fn - ownr_ln - ownr_ph1 - - ownr_ph1_ty - ownr_ph1x - ownr_ph2 - - ownr_ph2_ty - ownr_ph2x - ownr_st - ownr_title @@ -4217,7 +3860,6 @@ - qb_multiple_payers - queued_for_parts - rate_ats - - rate_ats_flat - rate_la1 - rate_la2 - rate_la3 @@ -4301,12 +3943,10 @@ - actual_completion - actual_delivery - actual_in - - acv_amount - adj_g_disc - adj_strdis - adj_towdis - adjustment_bottom_line - - admin_clerk - agt_addr1 - agt_addr2 - agt_city @@ -4371,7 +4011,6 @@ - completed_tasks - converted - created_at - - created_user_email - cust_pr - date_estimated - date_exported @@ -4390,10 +4029,7 @@ - ded_status - deliverchecklist - depreciation_taxes - - dms_advisor_id - dms_allocation - - dms_customer_id - - dms_id - driveable - employee_body - employee_csr @@ -4410,12 +4046,8 @@ - est_ph1 - est_st - est_zip - - estimate_approved - - estimate_sent_approval - federal_tax_rate - - flat_rate_ats - g_bett_amt - - hit_and_run - id - inproduction - ins_addr1 @@ -4490,10 +4122,8 @@ - ownr_fn - ownr_ln - ownr_ph1 - - ownr_ph1_ty - ownr_ph1x - ownr_ph2 - - ownr_ph2_ty - ownr_ph2x - ownr_st - ownr_title @@ -4512,7 +4142,6 @@ - qb_multiple_payers - queued_for_parts - rate_ats - - rate_ats_flat - rate_la1 - rate_la2 - rate_la3 @@ -4592,6 +4221,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: bodyshop: associations: @@ -4602,69 +4232,6 @@ - active: _eq: true event_triggers: - - name: job_modified - definition: - enable_manual: false - update: - columns: - - employee_prep - - clm_total - - suspended - - employee_body - - ro_number - - ownr_co_nm - - v_vin - - scheduled_completion - - special_coverage_policy - - scheduled_delivery - - actual_delivery - - actual_completion - - kanbanparent - - est_ct_fn - - alt_transport - - v_model_desc - - clm_no - - v_make_desc - - date_next_contact - - status - - employee_csr - - actual_in - - v_model_yr - - comment - - job_totals - - ownr_fn - - v_color - - ca_gst_registrant - - employee_refinish - - ownr_ph1 - - date_last_contacted - - inproduction - - est_ct_ln - - production_vars - - category - - date_invoiced - - est_co_nm - - ownr_ln - retry_conf: - interval_sec: 10 - num_retries: 0 - timeout_sec: 60 - webhook_from_env: HASURA_API_URL - headers: - - name: event-secret - value_from_env: EVENT_SECRET - request_transform: - body: - action: transform - template: |- - { - "data": {{$body?.event?.data?.new}} - } - method: POST - query_params: {} - template_engine: Kriti - url: '{{$base_url}}/job/job-updated' - version: 2 - name: job_status_transition definition: enable_manual: true @@ -4701,77 +4268,6 @@ template_engine: Kriti url: '{{$base_url}}/record-handler/arms' version: 2 - - name: notifications_jobs - definition: - enable_manual: false - insert: - columns: '*' - update: - columns: - - queued_for_parts - - employee_prep - - clm_total - - towin - - employee_body - - converted - - scheduled_in - - scheduled_completion - - scheduled_delivery - - actual_delivery - - actual_completion - - alt_transport - - date_exported - - status - - employee_csr - - actual_in - - deliverchecklist - - comment - - job_totals - - employee_refinish - - inproduction - - production_vars - - intakechecklist - - cieca_ttl - - date_invoiced - retry_conf: - interval_sec: 10 - num_retries: 0 - timeout_sec: 60 - webhook_from_env: HASURA_API_URL - headers: - - name: event-secret - value_from_env: EVENT_SECRET - request_transform: - body: - action: transform - template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": {{$body.event.op}},\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.old.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.old.employee_prep}},\r\n \"clm_total\": {{$body.event.data.old.clm_total}},\r\n \"towin\": {{$body.event.data.old.towin}},\r\n \"employee_body\": {{$body.event.data.old.employee_body}},\r\n \"converted\": {{$body.event.data.old.converted}},\r\n \"scheduled_in\": {{$body.event.data.old.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.old.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.old.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.old.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.old.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.old.alt_transport}},\r\n \"date_exported\": {{$body.event.data.old.date_exported}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"employee_csr\": {{$body.event.data.old.employee_csr}},\r\n \"actual_in\": {{$body.event.data.old.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.old.deliverchecklist}},\r\n \"comment\": {{$body.event.data.old.comment}},\r\n \"employee_refinish\": {{$body.event.data.old.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.old.inproduction}},\r\n \"production_vars\": {{$body.event.data.old.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.old.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.old.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.old.date_invoiced}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.new.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.new.employee_prep}},\r\n \"clm_total\": {{$body.event.data.new.clm_total}},\r\n \"towin\": {{$body.event.data.new.towin}},\r\n \"employee_body\": {{$body.event.data.new.employee_body}},\r\n \"converted\": {{$body.event.data.new.converted}},\r\n \"scheduled_in\": {{$body.event.data.new.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.new.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.new.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.new.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.new.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.new.alt_transport}},\r\n \"date_exported\": {{$body.event.data.new.date_exported}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"employee_csr\": {{$body.event.data.new.employee_csr}},\r\n \"actual_in\": {{$body.event.data.new.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.new.deliverchecklist}},\r\n \"comment\": {{$body.event.data.new.comment}},\r\n \"employee_refinish\": {{$body.event.data.new.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.new.inproduction}},\r\n \"production_vars\": {{$body.event.data.new.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.new.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.new.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.new.date_invoiced}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n" - method: POST - query_params: {} - template_engine: Kriti - url: '{{$base_url}}/notifications/events/handleJobsChange' - version: 2 - - name: notifications_jobs_autoadd - definition: - enable_manual: false - insert: - columns: '*' - retry_conf: - interval_sec: 10 - num_retries: 0 - timeout_sec: 60 - webhook_from_env: HASURA_API_URL - headers: - - name: event-secret - value_from_env: EVENT_SECRET - request_transform: - body: - action: transform - template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": {{$body.event.op}},\r\n \"data\": {\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"shopid\": {{$body.event.data.new?.shopid}},\r\n \"ro_number\": {{$body.event.data.new?.ro_number}},\r\n \"created_user_email\": {{$body.event.data.new?.created_user_email}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs_autoadd\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n" - method: POST - query_params: {} - template_engine: Kriti - url: '{{$base_url}}/notifications/events/handleAutoAdd' - version: 2 - name: os_jobs definition: delete: @@ -4795,35 +4291,6 @@ template_engine: Kriti url: '{{$base_url}}/opensearch' version: 2 -- table: - name: jobs_inproduction - schema: public - object_relationships: - - name: bodyshop - using: - manual_configuration: - column_mapping: - shopid: id - insertion_order: null - remote_table: - name: bodyshops - schema: public - select_permissions: - - role: user - permission: - columns: - - id - - shopid - - updated_at - filter: - bodyshop: - associations: - _and: - - user: - authid: - _eq: X-Hasura-User-Id - - active: - _eq: true - table: name: masterdata schema: public @@ -4840,34 +4307,6 @@ - key - value filter: {} -- table: - name: media_analytics - schema: public - object_relationships: - - name: bodyshop - using: - foreign_key_constraint_on: bodyshopid - array_relationships: - - name: media_analytics_details - using: - foreign_key_constraint_on: - column: media_analytics_id - table: - name: media_analytics_detail - schema: public -- table: - name: media_analytics_detail - schema: public - object_relationships: - - name: bodyshop - using: - foreign_key_constraint_on: bodyshopid - - name: job - using: - foreign_key_constraint_on: jobid - - name: media_analytic - using: - foreign_key_constraint_on: media_analytics_id - table: name: messages schema: public @@ -4913,7 +4352,6 @@ - id - image - image_path - - is_system - isoutbound - msid - read @@ -4961,6 +4399,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: conversation: bodyshop: @@ -5078,7 +4517,6 @@ - critical - id - jobid - - pinned - private - text - type @@ -5093,7 +4531,6 @@ - critical - id - jobid - - pinned - private - text - type @@ -5118,7 +4555,6 @@ - critical - id - jobid - - pinned - private - text - type @@ -5137,6 +4573,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: job: bodyshop: @@ -5147,110 +4584,6 @@ _eq: X-Hasura-User-Id - active: _eq: true - event_triggers: - - name: notifications_notes - definition: - enable_manual: false - insert: - columns: '*' - retry_conf: - interval_sec: 10 - num_retries: 0 - timeout_sec: 60 - webhook_from_env: HASURA_API_URL - headers: - - name: event-secret - value_from_env: EVENT_SECRET - request_transform: - method: POST - query_params: {} - template_engine: Kriti - url: '{{$base_url}}/notifications/events/handleNotesChange' - version: 2 -- table: - name: notifications - schema: public - object_relationships: - - name: association - using: - foreign_key_constraint_on: associationid - - name: job - using: - foreign_key_constraint_on: jobid - insert_permissions: - - role: user - permission: - check: - job: - bodyshop: - associations: - _and: - - user: - authid: - _eq: X-Hasura-User-Id - - active: - _eq: true - columns: - - scenario_meta - - scenario_text - - fcm_text - - created_at - - read - - updated_at - - associationid - - id - - jobid - comment: "" - select_permissions: - - role: user - permission: - columns: - - scenario_meta - - scenario_text - - fcm_text - - created_at - - read - - updated_at - - associationid - - id - - jobid - filter: - job: - bodyshop: - associations: - _and: - - user: - authid: - _eq: X-Hasura-User-Id - - active: - _eq: true - allow_aggregations: true - comment: "" - update_permissions: - - role: user - permission: - columns: - - scenario_meta - - scenario_text - - fcm_text - - created_at - - read - - updated_at - - associationid - - id - - jobid - filter: {} - check: - job: - bodyshop: - associations: - _and: - - user: - authid: - _eq: X-Hasura-User-Id - - active: - _eq: true - comment: "" - table: name: owners schema: public @@ -5293,9 +4626,7 @@ - ownr_fn - ownr_ln - ownr_ph1 - - ownr_ph1_ty - ownr_ph2 - - ownr_ph2_ty - ownr_st - ownr_title - ownr_zip @@ -5320,9 +4651,7 @@ - ownr_fn - ownr_ln - ownr_ph1 - - ownr_ph1_ty - ownr_ph2 - - ownr_ph2_ty - ownr_st - ownr_title - ownr_zip @@ -5358,9 +4687,7 @@ - ownr_fn - ownr_ln - ownr_ph1 - - ownr_ph1_ty - ownr_ph2 - - ownr_ph2_ty - ownr_st - ownr_title - ownr_zip @@ -5381,6 +4708,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: bodyshop: associations: @@ -5685,6 +5013,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: parts_order: job: @@ -5817,6 +5146,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: job: bodyshop: @@ -5992,6 +5322,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: job: bodyshop: @@ -6003,25 +5334,6 @@ - active: _eq: true event_triggers: - - name: notifications_payments - definition: - enable_manual: false - insert: - columns: '*' - retry_conf: - interval_sec: 10 - num_retries: 0 - timeout_sec: 60 - webhook_from_env: HASURA_API_URL - headers: - - name: event-secret - value_from_env: EVENT_SECRET - request_transform: - method: POST - query_params: {} - template_engine: Kriti - url: '{{$base_url}}/notifications/events/handlePaymentsChange' - version: 2 - name: os_payments definition: delete: @@ -6045,32 +5357,6 @@ template_engine: Kriti url: '{{$base_url}}/opensearch' version: 2 -- table: - name: phone_number_opt_out - schema: public - object_relationships: - - name: bodyshop - using: - foreign_key_constraint_on: bodyshopid - select_permissions: - - role: user - permission: - columns: - - phone_number - - created_at - - updated_at - - bodyshopid - - id - filter: - bodyshop: - associations: - _and: - - user: - authid: - _eq: X-Hasura-User-Id - - active: - _eq: true - comment: "" - table: name: phonebook schema: public @@ -6176,6 +5462,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: bodyshop: associations: @@ -6286,6 +5573,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: _or: - parentjob_rel: @@ -6375,6 +5663,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: job: bodyshop: @@ -6389,9 +5678,6 @@ name: tasks schema: public object_relationships: - - name: assigned_to_employee - using: - foreign_key_constraint_on: assigned_to - name: bill using: foreign_key_constraint_on: billid @@ -6407,6 +5693,9 @@ - name: parts_order using: foreign_key_constraint_on: partsorderid + - name: user + using: + foreign_key_constraint_on: assigned_to - name: userByCreatedBy using: foreign_key_constraint_on: created_by @@ -6423,50 +5712,48 @@ - active: _eq: true columns: - - assigned_to - - billid - - bodyshopid - completed + - deleted + - priority + - assigned_to + - created_by + - description + - title - completed_at - created_at - - created_by - - deleted - deleted_at - - description - due_date + - remind_at + - updated_at + - billid + - bodyshopid - id - jobid - joblineid - partsorderid - - priority - - remind_at - - remind_at_sent - - title - - updated_at select_permissions: - role: user permission: columns: - - assigned_to - - billid - - bodyshopid - completed + - deleted + - priority + - assigned_to + - created_by + - description + - title - completed_at - created_at - - created_by - - deleted - deleted_at - - description - due_date + - remind_at + - updated_at + - billid + - bodyshopid - id - jobid - joblineid - partsorderid - - priority - - remind_at - - remind_at_sent - - title - - updated_at filter: bodyshop: associations: @@ -6481,26 +5768,25 @@ - role: user permission: columns: - - assigned_to - - billid - - bodyshopid - completed + - deleted + - priority + - assigned_to + - created_by + - description + - title - completed_at - created_at - - created_by - - deleted - deleted_at - - description - due_date + - remind_at + - updated_at + - billid + - bodyshopid - id - jobid - joblineid - partsorderid - - priority - - remind_at - - remind_at_sent - - title - - updated_at filter: bodyshop: associations: @@ -6511,61 +5797,6 @@ - active: _eq: true check: null - event_triggers: - - name: notifications_tasks - definition: - enable_manual: false - insert: - columns: '*' - update: - columns: - - joblineid - - assigned_to - - due_date - - deleted - - partsorderid - - completed - - description - - billid - - title - - jobid - - priority - retry_conf: - interval_sec: 10 - num_retries: 0 - timeout_sec: 60 - webhook_from_env: HASURA_API_URL - headers: - - name: event-secret - value_from_env: EVENT_SECRET - request_transform: - method: POST - query_params: {} - template_engine: Kriti - url: '{{$base_url}}/notifications/events/handleTasksChange' - version: 2 - - name: tasks_assigned_changed - definition: - enable_manual: false - insert: - columns: '*' - update: - columns: - - assigned_to - retry_conf: - interval_sec: 10 - num_retries: 3 - timeout_sec: 60 - webhook_from_env: HASURA_API_URL - headers: - - name: event-secret - value_from_env: EVENT_SECRET - request_transform: - method: POST - query_params: {} - template_engine: Kriti - url: '{{$base_url}}/tasks-assigned-handler' - version: 2 - table: name: timetickets schema: public @@ -6614,7 +5845,6 @@ - id - jobid - memo - - payout_context - productivehrs - rate - task_name @@ -6640,7 +5870,6 @@ - id - jobid - memo - - payout_context - productivehrs - rate - task_name @@ -6675,7 +5904,6 @@ - id - jobid - memo - - payout_context - productivehrs - rate - task_name @@ -6694,6 +5922,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: bodyshop: associations: @@ -6703,26 +5932,6 @@ _eq: X-Hasura-User-Id - active: _eq: true - event_triggers: - - name: notifications_time_tickets - definition: - enable_manual: false - insert: - columns: '*' - retry_conf: - interval_sec: 10 - num_retries: 0 - timeout_sec: 60 - webhook_from_env: HASURA_API_URL - headers: - - name: event-secret - value_from_env: EVENT_SECRET - request_transform: - method: POST - query_params: {} - template_engine: Kriti - url: '{{$base_url}}/notifications/events/handleTimeTicketsChange' - version: 2 - table: name: transitions schema: public @@ -6859,7 +6068,6 @@ - id - jobid - memo - - payout_context - productivehrs - rate - updated_at @@ -6880,7 +6088,6 @@ - id - jobid - memo - - payout_context - productivehrs - rate - updated_at @@ -6911,7 +6118,6 @@ - id - jobid - memo - - payout_context - productivehrs - rate - updated_at @@ -6960,13 +6166,6 @@ table: name: email_audit_trail schema: public - - name: employees - using: - foreign_key_constraint_on: - column: user_email - table: - name: employees - schema: public - name: eula_acceptances using: foreign_key_constraint_on: @@ -6988,13 +6187,6 @@ table: name: ioevents schema: public - - name: job_watchers - using: - foreign_key_constraint_on: - column: user_email - table: - name: job_watchers - schema: public - name: messages using: foreign_key_constraint_on: @@ -7023,6 +6215,13 @@ table: name: parts_orders schema: public + - name: tasks + using: + foreign_key_constraint_on: + column: assigned_to + table: + name: tasks + schema: public - name: tasksByCreatedBy using: foreign_key_constraint_on: @@ -7219,6 +6418,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: bodyshop: associations: @@ -7304,7 +6504,6 @@ - state - street1 - street2 - - tags - updated_at - zip select_permissions: @@ -7328,7 +6527,6 @@ - state - street1 - street2 - - tags - updated_at - zip filter: @@ -7362,7 +6560,6 @@ - state - street1 - street2 - - tags - updated_at - zip filter: @@ -7378,6 +6575,7 @@ delete_permissions: - role: user permission: + backend_only: false filter: bodyshop: associations: From f89d7865fac369bc95ce7cdafe2951e38ffd7268 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 27 Mar 2026 14:46:00 -0400 Subject: [PATCH 41/56] Restore hasura metadata tables from master-AIO --- hasura/metadata/tables.yaml | 836 ++++++++++++++++++++++++++++++++---- 1 file changed, 762 insertions(+), 74 deletions(-) diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 7a9de6b10..5d3d3eb85 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -69,7 +69,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: jobline: job: @@ -180,7 +179,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: bodyshop: associations: @@ -200,6 +198,14 @@ - name: user using: foreign_key_constraint_on: useremail + array_relationships: + - name: notifications + using: + foreign_key_constraint_on: + column: associationid + table: + name: notifications + schema: public select_permissions: - role: user permission: @@ -209,6 +215,9 @@ - default_prod_list_view - id - kanban_settings + - new_message_sound + - notification_settings + - notifications_autoadd - qbo_realmId - shopid - useremail @@ -224,6 +233,9 @@ - authlevel - default_prod_list_view - kanban_settings + - new_message_sound + - notification_settings + - notifications_autoadd - qbo_realmId filter: user: @@ -387,7 +399,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: bodyshop: associations: @@ -504,7 +515,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: bill: job: @@ -671,7 +681,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: _and: - job: @@ -686,6 +695,25 @@ - exported: _eq: false event_triggers: + - name: notifications_bills + definition: + enable_manual: false + insert: + columns: '*' + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/notifications/events/handleBillsChange' + version: 2 - name: os_bills definition: delete: @@ -916,10 +944,14 @@ - autohouseid - bill_allow_post_to_closed - bill_tax_rates + - carfax_exclude - cdk_configuration - cdk_dealerid + - chatter_company_id + - chatterid - city - claimscorpid + - convenient_company - country - created_at - default_adjustment_rate @@ -931,6 +963,7 @@ - enforce_referral - entegral_configuration - entegral_id + - external_shop_id - features - federal_tax_id - id @@ -938,6 +971,8 @@ - inhousevendorid - insurance_vendor_id - intakechecklist + - intellipay_config + - intellipay_merchant_id - jc_hourly_rates - jobsizelimit - last_name_first @@ -974,12 +1009,16 @@ - md_tasks_presets - md_to_emails - messagingservicesid + - notification_followers - pbs_configuration - pbs_serialnumber - phone + - podiumid - prodtargethrs - production_config - region_config + - rr_configuration + - rr_dealerid - schedule_end_time - schedule_start_time - scoreboard_target @@ -996,6 +1035,7 @@ - template_header - textid - timezone + - tours_enabled - tt_allow_post_to_invoiced - tt_enforce_hours_for_tech_console - updated_at @@ -1024,6 +1064,7 @@ - bill_allow_post_to_closed - bill_tax_rates - cdk_configuration + - chatter_company_id - city - country - created_at @@ -1034,11 +1075,13 @@ - enforce_conversion_category - enforce_conversion_csr - enforce_referral + - external_shop_id - federal_tax_id - id - inhousevendorid - insurance_vendor_id - intakechecklist + - intellipay_config - jc_hourly_rates - last_name_first - localmediaserverhttp @@ -1073,10 +1116,12 @@ - md_ro_statuses - md_tasks_presets - md_to_emails + - notification_followers - pbs_configuration - phone - prodtargethrs - production_config + - rr_configuration - schedule_end_time - schedule_start_time - scoreboard_target @@ -1107,6 +1152,50 @@ - active: _eq: true check: null + event_triggers: + - name: cache_bodyshop + definition: + enable_manual: false + update: + columns: + - imexshopid + - timezone + - shopname + - notification_followers + - state + - md_order_statuses + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + body: + action: transform + template: |- + { + "created_at": {{$body.created_at}}, + "delivery_info": {{$body.delivery_info}}, + "event": { + "data": { + "new": { + "id": {{$body.event.data.new.id}}, + "shopname": {{$body.event.data.new.shopname}}, + "md_order_statuses": {{$body.event.data.new.md_order_statuses}} + } + }, + "op": {{$body.event.op}}, + "session_variables": {{$body.event.session_variables}} + } + } + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/bodyshop-cache' + version: 2 - table: name: cccontracts schema: public @@ -1281,7 +1370,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: courtesycar: bodyshop: @@ -1522,7 +1610,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: bodyshop: associations: @@ -1782,7 +1869,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: bodyshop: associations: @@ -1916,7 +2002,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: _or: - job: @@ -1936,6 +2021,29 @@ _eq: X-Hasura-User-Id - active: _eq: true + event_triggers: + - name: notifications_documents + definition: + enable_manual: false + insert: + columns: '*' + update: + columns: + - jobid + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/notifications/events/handleDocumentsChange' + version: 2 - table: name: email_audit_trail schema: public @@ -2101,7 +2209,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: employee_team: bodyshop: @@ -2264,7 +2371,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: employee: bodyshop: @@ -2335,6 +2441,13 @@ table: name: jobs schema: public + - name: tasks + using: + foreign_key_constraint_on: + column: assigned_to + table: + name: tasks + schema: public - name: timetickets using: foreign_key_constraint_on: @@ -2438,7 +2551,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: bodyshop: associations: @@ -2583,6 +2695,9 @@ - active: _eq: true allow_aggregations: true +- table: + name: integration_log + schema: public - table: name: inventory schema: public @@ -2685,7 +2800,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: bodyshop: associations: @@ -2797,7 +2911,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: conversation: bodyshop: @@ -2808,6 +2921,76 @@ _eq: X-Hasura-User-Id - active: _eq: true +- table: + name: job_watchers + schema: public + object_relationships: + - name: job + using: + foreign_key_constraint_on: jobid + - name: user + using: + foreign_key_constraint_on: user_email + insert_permissions: + - role: user + permission: + check: + job: + bodyshop: + associations: + user: + authid: + _eq: X-Hasura-User-Id + columns: + - user_email + - created_at + - id + - jobid + comment: "" + select_permissions: + - role: user + permission: + columns: + - user_email + - created_at + - id + - jobid + filter: + job: + bodyshop: + associations: + user: + authid: + _eq: X-Hasura-User-Id + comment: "" + update_permissions: + - role: user + permission: + columns: + - user_email + - created_at + - id + - jobid + filter: + job: + bodyshop: + associations: + user: + authid: + _eq: X-Hasura-User-Id + check: null + comment: "" + delete_permissions: + - role: user + permission: + filter: + job: + bodyshop: + associations: + user: + authid: + _eq: X-Hasura-User-Id + comment: "" - table: name: joblines schema: public @@ -2899,6 +3082,7 @@ - est_seq - glass_flag - id + - include_in_part_cnt - ioucreated - jobid - lbr_amt @@ -2968,6 +3152,7 @@ - est_seq - glass_flag - id + - include_in_part_cnt - ioucreated - jobid - lbr_amt @@ -3048,6 +3233,7 @@ - est_seq - glass_flag - id + - include_in_part_cnt - ioucreated - jobid - lbr_amt @@ -3112,7 +3298,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: job: bodyshop: @@ -3123,6 +3308,31 @@ _eq: X-Hasura-User-Id - active: _eq: true + event_triggers: + - name: notifications_joblines + definition: + enable_manual: false + update: + columns: + - critical + - status + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + body: + action: transform + template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"jobid\": {{$body.event.data.old.jobid}},\r\n \"critical\": {{$body.event.data.old.critical}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"line_desc\": {{$body.event.data.old.line_desc}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"jobid\": {{$body.event.data.new.jobid}},\r\n \"critical\": {{$body.event.data.new.critical}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"line_desc\": {{$body.event.data.new.line_desc}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_joblines\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"joblines\"\r\n }\r\n}\r\n" + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/notifications/events/handleJobLinesChange' + version: 2 - table: name: joblines_status schema: public @@ -3269,6 +3479,13 @@ table: name: job_conversations schema: public + - name: job_watchers + using: + foreign_key_constraint_on: + column: jobid + table: + name: job_watchers + schema: public - name: joblines using: foreign_key_constraint_on: @@ -3299,6 +3516,13 @@ table: name: notes schema: public + - name: notifications + using: + foreign_key_constraint_on: + column: jobid + table: + name: notifications + schema: public - name: parts_dispatches using: foreign_key_constraint_on: @@ -3392,10 +3616,12 @@ - actual_completion - actual_delivery - actual_in + - acv_amount - adj_g_disc - adj_strdis - adj_towdis - adjustment_bottom_line + - admin_clerk - agt_addr1 - agt_addr2 - agt_city @@ -3460,6 +3686,7 @@ - completed_tasks - converted - created_at + - created_user_email - cust_pr - date_estimated - date_exported @@ -3477,7 +3704,10 @@ - ded_status - deliverchecklist - depreciation_taxes + - dms_advisor_id - dms_allocation + - dms_customer_id + - dms_id - driveable - employee_body - employee_csr @@ -3494,8 +3724,12 @@ - est_ph1 - est_st - est_zip + - estimate_approved + - estimate_sent_approval - federal_tax_rate + - flat_rate_ats - g_bett_amt + - hit_and_run - id - inproduction - ins_addr1 @@ -3569,8 +3803,10 @@ - ownr_fn - ownr_ln - ownr_ph1 + - ownr_ph1_ty - ownr_ph1x - ownr_ph2 + - ownr_ph2_ty - ownr_ph2x - ownr_st - ownr_title @@ -3589,6 +3825,7 @@ - qb_multiple_payers - queued_for_parts - rate_ats + - rate_ats_flat - rate_la1 - rate_la2 - rate_la3 @@ -3661,10 +3898,12 @@ - actual_completion - actual_delivery - actual_in + - acv_amount - adj_g_disc - adj_strdis - adj_towdis - adjustment_bottom_line + - admin_clerk - agt_addr1 - agt_addr2 - agt_city @@ -3729,6 +3968,7 @@ - completed_tasks - converted - created_at + - created_user_email - cust_pr - date_estimated - date_exported @@ -3747,7 +3987,10 @@ - ded_status - deliverchecklist - depreciation_taxes + - dms_advisor_id - dms_allocation + - dms_customer_id + - dms_id - driveable - employee_body - employee_csr @@ -3764,8 +4007,12 @@ - est_ph1 - est_st - est_zip + - estimate_approved + - estimate_sent_approval - federal_tax_rate + - flat_rate_ats - g_bett_amt + - hit_and_run - id - inproduction - ins_addr1 @@ -3840,8 +4087,10 @@ - ownr_fn - ownr_ln - ownr_ph1 + - ownr_ph1_ty - ownr_ph1x - ownr_ph2 + - ownr_ph2_ty - ownr_ph2x - ownr_st - ownr_title @@ -3860,6 +4109,7 @@ - qb_multiple_payers - queued_for_parts - rate_ats + - rate_ats_flat - rate_la1 - rate_la2 - rate_la3 @@ -3943,10 +4193,12 @@ - actual_completion - actual_delivery - actual_in + - acv_amount - adj_g_disc - adj_strdis - adj_towdis - adjustment_bottom_line + - admin_clerk - agt_addr1 - agt_addr2 - agt_city @@ -4011,6 +4263,7 @@ - completed_tasks - converted - created_at + - created_user_email - cust_pr - date_estimated - date_exported @@ -4029,7 +4282,10 @@ - ded_status - deliverchecklist - depreciation_taxes + - dms_advisor_id - dms_allocation + - dms_customer_id + - dms_id - driveable - employee_body - employee_csr @@ -4046,8 +4302,12 @@ - est_ph1 - est_st - est_zip + - estimate_approved + - estimate_sent_approval - federal_tax_rate + - flat_rate_ats - g_bett_amt + - hit_and_run - id - inproduction - ins_addr1 @@ -4122,8 +4382,10 @@ - ownr_fn - ownr_ln - ownr_ph1 + - ownr_ph1_ty - ownr_ph1x - ownr_ph2 + - ownr_ph2_ty - ownr_ph2x - ownr_st - ownr_title @@ -4142,6 +4404,7 @@ - qb_multiple_payers - queued_for_parts - rate_ats + - rate_ats_flat - rate_la1 - rate_la2 - rate_la3 @@ -4221,7 +4484,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: bodyshop: associations: @@ -4232,6 +4494,69 @@ - active: _eq: true event_triggers: + - name: job_modified + definition: + enable_manual: false + update: + columns: + - employee_prep + - clm_total + - suspended + - employee_body + - ro_number + - ownr_co_nm + - v_vin + - scheduled_completion + - special_coverage_policy + - scheduled_delivery + - actual_delivery + - actual_completion + - kanbanparent + - est_ct_fn + - alt_transport + - v_model_desc + - clm_no + - v_make_desc + - date_next_contact + - status + - employee_csr + - actual_in + - v_model_yr + - comment + - job_totals + - ownr_fn + - v_color + - ca_gst_registrant + - employee_refinish + - ownr_ph1 + - date_last_contacted + - inproduction + - est_ct_ln + - production_vars + - category + - date_invoiced + - est_co_nm + - ownr_ln + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + body: + action: transform + template: |- + { + "data": {{$body?.event?.data?.new}} + } + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/job/job-updated' + version: 2 - name: job_status_transition definition: enable_manual: true @@ -4268,6 +4593,77 @@ template_engine: Kriti url: '{{$base_url}}/record-handler/arms' version: 2 + - name: notifications_jobs + definition: + enable_manual: false + insert: + columns: '*' + update: + columns: + - queued_for_parts + - employee_prep + - clm_total + - towin + - employee_body + - converted + - scheduled_in + - scheduled_completion + - scheduled_delivery + - actual_delivery + - actual_completion + - alt_transport + - date_exported + - status + - employee_csr + - actual_in + - deliverchecklist + - comment + - job_totals + - employee_refinish + - inproduction + - production_vars + - intakechecklist + - cieca_ttl + - date_invoiced + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + body: + action: transform + template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": {{$body.event.op}},\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.old.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.old.employee_prep}},\r\n \"clm_total\": {{$body.event.data.old.clm_total}},\r\n \"towin\": {{$body.event.data.old.towin}},\r\n \"employee_body\": {{$body.event.data.old.employee_body}},\r\n \"converted\": {{$body.event.data.old.converted}},\r\n \"scheduled_in\": {{$body.event.data.old.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.old.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.old.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.old.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.old.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.old.alt_transport}},\r\n \"date_exported\": {{$body.event.data.old.date_exported}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"employee_csr\": {{$body.event.data.old.employee_csr}},\r\n \"actual_in\": {{$body.event.data.old.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.old.deliverchecklist}},\r\n \"comment\": {{$body.event.data.old.comment}},\r\n \"employee_refinish\": {{$body.event.data.old.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.old.inproduction}},\r\n \"production_vars\": {{$body.event.data.old.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.old.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.old.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.old.date_invoiced}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.new.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.new.employee_prep}},\r\n \"clm_total\": {{$body.event.data.new.clm_total}},\r\n \"towin\": {{$body.event.data.new.towin}},\r\n \"employee_body\": {{$body.event.data.new.employee_body}},\r\n \"converted\": {{$body.event.data.new.converted}},\r\n \"scheduled_in\": {{$body.event.data.new.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.new.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.new.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.new.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.new.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.new.alt_transport}},\r\n \"date_exported\": {{$body.event.data.new.date_exported}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"employee_csr\": {{$body.event.data.new.employee_csr}},\r\n \"actual_in\": {{$body.event.data.new.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.new.deliverchecklist}},\r\n \"comment\": {{$body.event.data.new.comment}},\r\n \"employee_refinish\": {{$body.event.data.new.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.new.inproduction}},\r\n \"production_vars\": {{$body.event.data.new.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.new.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.new.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.new.date_invoiced}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n" + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/notifications/events/handleJobsChange' + version: 2 + - name: notifications_jobs_autoadd + definition: + enable_manual: false + insert: + columns: '*' + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + body: + action: transform + template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": {{$body.event.op}},\r\n \"data\": {\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"shopid\": {{$body.event.data.new?.shopid}},\r\n \"ro_number\": {{$body.event.data.new?.ro_number}},\r\n \"created_user_email\": {{$body.event.data.new?.created_user_email}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs_autoadd\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n" + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/notifications/events/handleAutoAdd' + version: 2 - name: os_jobs definition: delete: @@ -4291,6 +4687,35 @@ template_engine: Kriti url: '{{$base_url}}/opensearch' version: 2 +- table: + name: jobs_inproduction + schema: public + object_relationships: + - name: bodyshop + using: + manual_configuration: + column_mapping: + shopid: id + insertion_order: null + remote_table: + name: bodyshops + schema: public + select_permissions: + - role: user + permission: + columns: + - id + - shopid + - updated_at + filter: + bodyshop: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true - table: name: masterdata schema: public @@ -4307,6 +4732,34 @@ - key - value filter: {} +- table: + name: media_analytics + schema: public + object_relationships: + - name: bodyshop + using: + foreign_key_constraint_on: bodyshopid + array_relationships: + - name: media_analytics_details + using: + foreign_key_constraint_on: + column: media_analytics_id + table: + name: media_analytics_detail + schema: public +- table: + name: media_analytics_detail + schema: public + object_relationships: + - name: bodyshop + using: + foreign_key_constraint_on: bodyshopid + - name: job + using: + foreign_key_constraint_on: jobid + - name: media_analytic + using: + foreign_key_constraint_on: media_analytics_id - table: name: messages schema: public @@ -4352,6 +4805,7 @@ - id - image - image_path + - is_system - isoutbound - msid - read @@ -4399,7 +4853,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: conversation: bodyshop: @@ -4517,6 +4970,7 @@ - critical - id - jobid + - pinned - private - text - type @@ -4531,6 +4985,7 @@ - critical - id - jobid + - pinned - private - text - type @@ -4555,6 +5010,7 @@ - critical - id - jobid + - pinned - private - text - type @@ -4573,7 +5029,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: job: bodyshop: @@ -4584,6 +5039,110 @@ _eq: X-Hasura-User-Id - active: _eq: true + event_triggers: + - name: notifications_notes + definition: + enable_manual: false + insert: + columns: '*' + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/notifications/events/handleNotesChange' + version: 2 +- table: + name: notifications + schema: public + object_relationships: + - name: association + using: + foreign_key_constraint_on: associationid + - name: job + using: + foreign_key_constraint_on: jobid + insert_permissions: + - role: user + permission: + check: + job: + bodyshop: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true + columns: + - scenario_meta + - scenario_text + - fcm_text + - created_at + - read + - updated_at + - associationid + - id + - jobid + comment: "" + select_permissions: + - role: user + permission: + columns: + - scenario_meta + - scenario_text + - fcm_text + - created_at + - read + - updated_at + - associationid + - id + - jobid + filter: + job: + bodyshop: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true + allow_aggregations: true + comment: "" + update_permissions: + - role: user + permission: + columns: + - scenario_meta + - scenario_text + - fcm_text + - created_at + - read + - updated_at + - associationid + - id + - jobid + filter: {} + check: + job: + bodyshop: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true + comment: "" - table: name: owners schema: public @@ -4626,7 +5185,9 @@ - ownr_fn - ownr_ln - ownr_ph1 + - ownr_ph1_ty - ownr_ph2 + - ownr_ph2_ty - ownr_st - ownr_title - ownr_zip @@ -4651,7 +5212,9 @@ - ownr_fn - ownr_ln - ownr_ph1 + - ownr_ph1_ty - ownr_ph2 + - ownr_ph2_ty - ownr_st - ownr_title - ownr_zip @@ -4687,7 +5250,9 @@ - ownr_fn - ownr_ln - ownr_ph1 + - ownr_ph1_ty - ownr_ph2 + - ownr_ph2_ty - ownr_st - ownr_title - ownr_zip @@ -4708,7 +5273,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: bodyshop: associations: @@ -5013,7 +5577,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: parts_order: job: @@ -5146,7 +5709,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: job: bodyshop: @@ -5322,7 +5884,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: job: bodyshop: @@ -5334,6 +5895,25 @@ - active: _eq: true event_triggers: + - name: notifications_payments + definition: + enable_manual: false + insert: + columns: '*' + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/notifications/events/handlePaymentsChange' + version: 2 - name: os_payments definition: delete: @@ -5357,6 +5937,32 @@ template_engine: Kriti url: '{{$base_url}}/opensearch' version: 2 +- table: + name: phone_number_opt_out + schema: public + object_relationships: + - name: bodyshop + using: + foreign_key_constraint_on: bodyshopid + select_permissions: + - role: user + permission: + columns: + - phone_number + - created_at + - updated_at + - bodyshopid + - id + filter: + bodyshop: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true + comment: "" - table: name: phonebook schema: public @@ -5462,7 +6068,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: bodyshop: associations: @@ -5573,7 +6178,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: _or: - parentjob_rel: @@ -5663,7 +6267,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: job: bodyshop: @@ -5678,6 +6281,9 @@ name: tasks schema: public object_relationships: + - name: assigned_to_employee + using: + foreign_key_constraint_on: assigned_to - name: bill using: foreign_key_constraint_on: billid @@ -5693,9 +6299,6 @@ - name: parts_order using: foreign_key_constraint_on: partsorderid - - name: user - using: - foreign_key_constraint_on: assigned_to - name: userByCreatedBy using: foreign_key_constraint_on: created_by @@ -5712,48 +6315,50 @@ - active: _eq: true columns: - - completed - - deleted - - priority - assigned_to - - created_by - - description - - title - - completed_at - - created_at - - deleted_at - - due_date - - remind_at - - updated_at - billid - bodyshopid + - completed + - completed_at + - created_at + - created_by + - deleted + - deleted_at + - description + - due_date - id - jobid - joblineid - partsorderid + - priority + - remind_at + - remind_at_sent + - title + - updated_at select_permissions: - role: user permission: columns: - - completed - - deleted - - priority - assigned_to - - created_by - - description - - title - - completed_at - - created_at - - deleted_at - - due_date - - remind_at - - updated_at - billid - bodyshopid + - completed + - completed_at + - created_at + - created_by + - deleted + - deleted_at + - description + - due_date - id - jobid - joblineid - partsorderid + - priority + - remind_at + - remind_at_sent + - title + - updated_at filter: bodyshop: associations: @@ -5768,25 +6373,26 @@ - role: user permission: columns: - - completed - - deleted - - priority - assigned_to - - created_by - - description - - title - - completed_at - - created_at - - deleted_at - - due_date - - remind_at - - updated_at - billid - bodyshopid + - completed + - completed_at + - created_at + - created_by + - deleted + - deleted_at + - description + - due_date - id - jobid - joblineid - partsorderid + - priority + - remind_at + - remind_at_sent + - title + - updated_at filter: bodyshop: associations: @@ -5797,6 +6403,61 @@ - active: _eq: true check: null + event_triggers: + - name: notifications_tasks + definition: + enable_manual: false + insert: + columns: '*' + update: + columns: + - joblineid + - assigned_to + - due_date + - deleted + - partsorderid + - completed + - description + - billid + - title + - jobid + - priority + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/notifications/events/handleTasksChange' + version: 2 + - name: tasks_assigned_changed + definition: + enable_manual: false + insert: + columns: '*' + update: + columns: + - assigned_to + retry_conf: + interval_sec: 10 + num_retries: 3 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/tasks-assigned-handler' + version: 2 - table: name: timetickets schema: public @@ -5922,7 +6583,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: bodyshop: associations: @@ -5932,6 +6592,26 @@ _eq: X-Hasura-User-Id - active: _eq: true + event_triggers: + - name: notifications_time_tickets + definition: + enable_manual: false + insert: + columns: '*' + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/notifications/events/handleTimeTicketsChange' + version: 2 - table: name: transitions schema: public @@ -6166,6 +6846,13 @@ table: name: email_audit_trail schema: public + - name: employees + using: + foreign_key_constraint_on: + column: user_email + table: + name: employees + schema: public - name: eula_acceptances using: foreign_key_constraint_on: @@ -6187,6 +6874,13 @@ table: name: ioevents schema: public + - name: job_watchers + using: + foreign_key_constraint_on: + column: user_email + table: + name: job_watchers + schema: public - name: messages using: foreign_key_constraint_on: @@ -6215,13 +6909,6 @@ table: name: parts_orders schema: public - - name: tasks - using: - foreign_key_constraint_on: - column: assigned_to - table: - name: tasks - schema: public - name: tasksByCreatedBy using: foreign_key_constraint_on: @@ -6418,7 +7105,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: bodyshop: associations: @@ -6504,6 +7190,7 @@ - state - street1 - street2 + - tags - updated_at - zip select_permissions: @@ -6527,6 +7214,7 @@ - state - street1 - street2 + - tags - updated_at - zip filter: @@ -6560,6 +7248,7 @@ - state - street1 - street2 + - tags - updated_at - zip filter: @@ -6575,7 +7264,6 @@ delete_permissions: - role: user permission: - backend_only: false filter: bodyshop: associations: From 8db8744782c53753ad588f69c72cd05c95626cb2 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 27 Mar 2026 15:38:27 -0700 Subject: [PATCH 42/56] IO-3629 PostBatchWip Rtn != 0 error Signed-off-by: Allan Carr --- server/fortellis/fortellis.js | 52 +++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/server/fortellis/fortellis.js b/server/fortellis/fortellis.js index 02ed67852..ee01691e9 100644 --- a/server/fortellis/fortellis.js +++ b/server/fortellis/fortellis.js @@ -334,30 +334,48 @@ async function FortellisSelectedCustomer({ socket, redisHelpers, selectedCustome socket.emit("export-success", JobData.id); } else { //There was something wrong. Throw an error to trigger clean up. - //throw new Error("Error posting DMS Batch Transaction"); + const batchPostError = new Error(DmsBatchTxnPost.sendline || "Error posting DMS Batch Transaction"); + batchPostError.errorData = { DMSTransHeader, DmsBatchTxnPost }; + throw batchPostError; } - } catch { + } catch (error) { //Clean up the transaction and insert a faild error code // //Get the error code CreateFortellisLogEvent(socket, "DEBUG", `{6.1} Getting errors for Transaction ID ${DMSTransHeader.transID}`); - const DmsError = await QueryDmsErrWip({ socket, redisHelpers, JobData }); - // //Delete the transaction + let dmsErrors = []; + try { + const DmsError = await QueryDmsErrWip({ socket, redisHelpers, JobData }); + dmsErrors = Array.isArray(DmsError?.errMsg) ? DmsError.errMsg.filter((e) => e !== null && e !== "") : []; + + dmsErrors.forEach((e) => { + CreateFortellisLogEvent(socket, "ERROR", `Error encountered in posting transaction => ${e} `); + }); + } catch (queryError) { + CreateFortellisLogEvent( + socket, + "ERROR", + `{6.1} Unable to read ErrWIP for Transaction ID ${DMSTransHeader.transID}: ${queryError.message}` + ); + } + + //Delete the transaction, even if querying ErrWIP fails. CreateFortellisLogEvent(socket, "DEBUG", `{6.2} Deleting Transaction ID ${DMSTransHeader.transID}`); + try { + await DeleteDmsWip({ socket, redisHelpers, JobData }); + } catch (cleanupError) { + CreateFortellisLogEvent( + socket, + "ERROR", + `{6.2} Failed cleanup for Transaction ID ${DMSTransHeader.transID}: ${cleanupError.message}` + ); + } - await DeleteDmsWip({ socket, redisHelpers, JobData }); - - DmsError.errMsg.map( - (e) => - e !== null && - e !== "" && - CreateFortellisLogEvent(socket, "ERROR", `Error encountered in posting transaction => ${e} `) - ); - await InsertFailedExportLog({ - socket, - JobData, - error: DmsError.errMsg - }); + if (!error.errorData || typeof error.errorData !== "object") { + error.errorData = {}; + } + error.errorData.issues = dmsErrors.length ? dmsErrors : [error.message]; + throw error; } } } catch (error) { From 7739d487411dc0cd780e663bb9a9f425bc7cca13 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 30 Mar 2026 11:44:38 -0700 Subject: [PATCH 43/56] IO-3356 Add CSR and Shop values per Grant @ RC Ford. --- server/accounting/pbs/pbs-job-export.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/accounting/pbs/pbs-job-export.js b/server/accounting/pbs/pbs-job-export.js index ee6360682..7e0499023 100644 --- a/server/accounting/pbs/pbs-job-export.js +++ b/server/accounting/pbs/pbs-job-export.js @@ -787,7 +787,8 @@ async function RepairOrderChange(socket) { // "DatePromisedUTC": "0001-01-01T00:00:00.0000000Z", DateVehicleCompleted: socket.JobData.actual_completion, // "DateCustomerNotified": "0001-01-01T00:00:00.0000000Z", - // "CSR": "String", + "CSR": "IMEX", //Hardcoded for now as the only shop that uses this RO posting is RC and they paid for a custom build. + Shop: "RB", // "CSRRef": "00000000000000000000000000000000", // "BookingUser": "String", // "BookingUserRef": "00000000000000000000000000000000", From 1ea7798eeb299060ed7e297d3a7c844a48c45036 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 30 Mar 2026 11:59:08 -0700 Subject: [PATCH 44/56] IO-3515 Retain discount application when AI vendor added. --- client/src/components/bill-form/bill-form.component.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/components/bill-form/bill-form.component.jsx b/client/src/components/bill-form/bill-form.component.jsx index 86d4e0265..7b324b3d1 100644 --- a/client/src/components/bill-form/bill-form.component.jsx +++ b/client/src/components/bill-form/bill-form.component.jsx @@ -52,6 +52,7 @@ export function BillFormComponent({ const [discount, setDiscount] = useState(0); const notification = useNotification(); const jobIdFormWatch = Form.useWatch("jobid", form); + const vendorIdFormWatch = Form.useWatch("vendorid", form); const { treatments: { Extended_Bill_Posting, ClosingPeriod } @@ -118,6 +119,7 @@ export function BillFormComponent({ } }, [ form, + vendorIdFormWatch, billEdit, loadOutstandingReturns, loadInventory, From 5d95275c0b0df96034858cf2fb69760547544013 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 30 Mar 2026 15:07:57 -0400 Subject: [PATCH 45/56] feature/IO-3624-Shop-Config-UX-Refresh - bump deps --- client/package-lock.json | 251 ++++++++++++++++++++------------------- client/package.json | 18 +-- package-lock.json | 234 ++++++++++++++++++------------------ package.json | 22 ++-- 4 files changed, 268 insertions(+), 257 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 9e8d4ef46..11e3ce3bd 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.1", "hasInstallScript": true, "dependencies": { - "@amplitude/analytics-browser": "^2.37.2", + "@amplitude/analytics-browser": "^2.38.0", "@ant-design/pro-layout": "^7.22.6", "@apollo/client": "^4.1.6", "@dnd-kit/core": "^6.3.1", @@ -25,15 +25,15 @@ "@firebase/messaging": "^0.12.25", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.11.2", - "@sentry/cli": "^3.3.4", + "@sentry/cli": "^3.3.5", "@sentry/react": "^10.46.0", "@sentry/vite-plugin": "^4.9.1", "@splitsoftware/splitio-react": "^2.6.1", "@tanem/react-nprogress": "^5.0.63", - "antd": "^6.3.4", + "antd": "^6.3.5", "apollo-link-logger": "^3.0.0", "autosize": "^6.0.1", - "axios": "^1.13.6", + "axios": "^1.14.0", "classnames": "^2.5.1", "css-box-model": "^1.2.1", "dayjs": "^1.11.20", @@ -43,11 +43,11 @@ "env-cmd": "^11.0.0", "exifr": "^7.1.3", "graphql": "^16.13.2", - "graphql-ws": "^6.0.7", + "graphql-ws": "^6.0.8", "i18next": "^25.10.10", "i18next-browser-languagedetector": "^8.2.1", "immutability-helper": "^3.1.1", - "libphonenumber-js": "^1.12.40", + "libphonenumber-js": "^1.12.41", "lightningcss": "^1.32.0", "logrocket": "^12.1.0", "markerjs2": "^2.32.7", @@ -55,7 +55,7 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.71", - "posthog-js": "^1.363.6", + "posthog-js": "^1.364.2", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", @@ -96,7 +96,7 @@ "@ant-design/icons": "^6.1.1", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.28.5", - "@dotenvx/dotenvx": "^1.57.2", + "@dotenvx/dotenvx": "^1.59.1", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", "@eslint/js": "^9.39.2", @@ -123,7 +123,7 @@ "vite": "^7.3.1", "vite-plugin-babel": "^1.6.0", "vite-plugin-eslint": "^1.8.1", - "vite-plugin-node-polyfills": "^0.25.0", + "vite-plugin-node-polyfills": "^0.26.0", "vite-plugin-pwa": "^1.2.0", "vite-plugin-style-import": "^2.0.0", "vitest": "^4.1.2", @@ -151,18 +151,18 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-browser": { - "version": "2.37.2", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.37.2.tgz", - "integrity": "sha512-4us5NDIsxsVVi9s6ojS94CO7D5CGcxSpAQg+cPmnATtxCFagxfXfO9I59h2veQ0ohL2nO45nVTKR7RrOyGxA/A==", + "version": "2.38.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.38.0.tgz", + "integrity": "sha512-MhqyEkr1gGAR4s4GSSflDhFVheIx9Nv3FfElQu9NlNrXB2Hh3BEOyVgdK7hgfi6NJwFyfw30+t5lym+njtA8hA==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.1", - "@amplitude/plugin-autocapture-browser": "1.24.2", - "@amplitude/plugin-custom-enrichment-browser": "0.1.1", - "@amplitude/plugin-network-capture-browser": "1.9.10", - "@amplitude/plugin-page-url-enrichment-browser": "0.7.2", - "@amplitude/plugin-page-view-tracking-browser": "2.9.3", - "@amplitude/plugin-web-vitals-browser": "1.1.25", + "@amplitude/analytics-core": "2.44.0", + "@amplitude/plugin-autocapture-browser": "1.25.0", + "@amplitude/plugin-custom-enrichment-browser": "0.1.2", + "@amplitude/plugin-network-capture-browser": "1.9.11", + "@amplitude/plugin-page-url-enrichment-browser": "0.7.3", + "@amplitude/plugin-page-view-tracking-browser": "2.9.4", + "@amplitude/plugin-web-vitals-browser": "1.1.26", "tslib": "^2.4.1" } }, @@ -173,9 +173,9 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-core": { - "version": "2.43.1", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.43.1.tgz", - "integrity": "sha512-8Qi0SZ49wNAcLOmtvk6/+a5FvV351G7ctMPyP9A2saZb5I106gb00VWiz1ENQdUG3cNse0iedSlNCjRbX0eksg==", + "version": "2.44.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.44.0.tgz", + "integrity": "sha512-z9QuTxLqEQ8KIeAT6Vmy6K48rP9TUmjnb4GwUMYoV/fxu3B9ClTaN18zqXQMmDw9HwUiIreHiVbwTb7OQRN5aA==", "license": "MIT", "dependencies": { "@amplitude/analytics-connector": "^1.6.4", @@ -186,62 +186,62 @@ } }, "node_modules/@amplitude/plugin-autocapture-browser": { - "version": "1.24.2", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.24.2.tgz", - "integrity": "sha512-IXcCEpd0J5M+Cz1u7RbAANfKr5D5jqkpVrumcOA+zYdmvrd7/1quuFHvR1Dnhk1fipeSwU5TEpadE/RBS80VvQ==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.25.0.tgz", + "integrity": "sha512-YuWsz8XmJuKu3NlMxkvlhLey/5tGCeOwwfsROHficR0yDWO9gNG0WtHl7A0Pw1PUc9iaXjqfG2AjYumAtiq16Q==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.1", + "@amplitude/analytics-core": "2.44.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-custom-enrichment-browser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-custom-enrichment-browser/-/plugin-custom-enrichment-browser-0.1.1.tgz", - "integrity": "sha512-RacUi5M9h1bDUvfRCK3561NP8gK7VuNycvXLPpY/2LDIdzkLMe9sNowghKbr/uLdIySiVcPa+KHCBRfFaJzfJA==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-custom-enrichment-browser/-/plugin-custom-enrichment-browser-0.1.2.tgz", + "integrity": "sha512-ZX9BKqs1E1OI7l7QCGu9JnB/1kqLN+zqIePgM2tuEhZNFQJaw4NhAMUaMRqvNnaCkHlmpVRISzSj/4D3tWMRtA==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.1", + "@amplitude/analytics-core": "2.44.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-network-capture-browser": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.9.10.tgz", - "integrity": "sha512-HubSKD8uzb5cLCGWeiLekC9Qv9NHcIwDQiV34Ju1cFrhTON78xfcg1Wz91aIFX+Hub94zqRv8kE/vjZ1ZU+B6Q==", + "version": "1.9.11", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.9.11.tgz", + "integrity": "sha512-49o3zYnKUmRdrxgAEcr1iHnXR1um40e1icO0hzugSq04k19hs27zcl3zpEk9geO+nNKwO744ryE1q93gqVbHrQ==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.1", + "@amplitude/analytics-core": "2.44.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-page-url-enrichment-browser": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.7.2.tgz", - "integrity": "sha512-CLcK/e9O0oa5eWruWe6UHhcqyVYK3JG4wsHR+WShG200VqLpNgsDxEM+qNWVYp5Q+aadVYOusik7exxkQayjUg==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.7.3.tgz", + "integrity": "sha512-3UZq/zKg4lcsRgziWAPSEeaUsNsbyjjxmsAE9kSDi/hIj5RaWnwWhY6TGhv45UAReugTA4vVZyFRg9btf3c/Fg==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.1", + "@amplitude/analytics-core": "2.44.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-page-view-tracking-browser": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.9.3.tgz", - "integrity": "sha512-bhsQkGI7D2AQ5EwPLYtZKxdI1jrA/la41ClUX2nHjvmDpY6i7zHVmi7L7xEce4JIF0tb5wUHIvnMt/C2yPBJvA==", + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.9.4.tgz", + "integrity": "sha512-J16zmEadnzNpkHSmzpTiQN2q9pGJ/4SkHONA9O8KxUsMU/MYTDgof3rAYY/w5B5rmvdxfMRCjqWtvnkizzgZ6w==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.1", + "@amplitude/analytics-core": "2.44.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-web-vitals-browser": { - "version": "1.1.25", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.25.tgz", - "integrity": "sha512-Ror17zbsSx4uroXdAn+53agTqaZ865U/gjwTgoAb9SdOKPmFGC6Xy+ntxaDCSSQYYayb/0+/g0NYe8WC3OuwCQ==", + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.26.tgz", + "integrity": "sha512-wiD4vy+f2fepr+8Lnn26TYYjDEnWsmlGhJog99x+xfbZ/D+stGdaCIOz5AOjU1TpTRvxvamEu2XuOh+8EZOCSA==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.1", + "@amplitude/analytics-core": "2.44.0", "tslib": "^2.4.1", "web-vitals": "5.1.0" } @@ -2594,9 +2594,9 @@ } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.57.2", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.57.2.tgz", - "integrity": "sha512-lv9+UZPnl/KOvShepevLWm3+/wc1It5kgO5Q580evnvOFMZcgKVEYFwxlL7Ohl9my1yjTsWo28N3PJYUEO8wFQ==", + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.59.1.tgz", + "integrity": "sha512-Qg+meC+XFxliuVSDlEPkKnaUjdaJKK6FNx/Wwl2UxhQR8pyPIuLhMavsF7ePdB9qFZUWV1jEK3ckbJir/WmF4w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4870,18 +4870,18 @@ } }, "node_modules/@posthog/core": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.24.1.tgz", - "integrity": "sha512-e8AciAnc6MRFws89ux8lJKFAaI03yEon0ASDoUO7yS91FVqbUGXYekObUUR3LHplcg+pmyiJBI0jolY0SFbGRA==", + "version": "1.24.4", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.24.4.tgz", + "integrity": "sha512-S+TolwBHSSJz7WWtgaELQWQqXviSm3uf1e+qorWUts0bZcgPwWzhnmhCUZAhvn0NVpTQHDJ3epv+hHbPLl5dHg==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6" } }, "node_modules/@posthog/types": { - "version": "1.363.6", - "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.363.6.tgz", - "integrity": "sha512-SPU8psjrpK8prfFyYwvb25F1AgqSM32zdU1XPVIhUa107Cyw+VGw38Gv+AeqkEoAYQE2TFlWJT8DWUrw/mNDoQ==", + "version": "1.364.2", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.364.2.tgz", + "integrity": "sha512-SMTdaYanvRmatgheXtu2XkewhuhdXe8C3JCi7m/Hd2n+sa2DaJphcwg3nAkPtfV69JHMxJLe/gyOt7yFtbQSjQ==", "license": "MIT" }, "node_modules/@protobufjs/aspromise": { @@ -5309,9 +5309,9 @@ } }, "node_modules/@rc-component/motion": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.1.tgz", - "integrity": "sha512-Wo1mkd0tCcHtvYvpPOmlYJz546z16qlsiwaygmW7NPJpOZOF9GBjhGzdzZSsC2lEJ1IUkWLF4gMHlRA1aSA+Yw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.2.tgz", + "integrity": "sha512-itfd+GztzJYAb04Z4RkEub1TbJAfZc2Iuy8p44U44xD1F5+fNYFKI3897ijlbIyfvXkTmMm+KGcjkQQGMHywEQ==", "license": "MIT", "dependencies": { "@rc-component/util": "^1.2.0", @@ -5561,9 +5561,9 @@ } }, "node_modules/@rc-component/resize-observer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.1.1.tgz", - "integrity": "sha512-NfXXMmiR+SmUuKE1NwJESzEUYUFWIDUn2uXpxCTOLwiRUUakd62DRNFjRJArgzyFW8S5rsL4aX5XlyIXyC/vRA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.1.2.tgz", + "integrity": "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q==", "license": "MIT", "dependencies": { "@rc-component/util": "^1.2.0" @@ -7010,9 +7010,9 @@ } }, "node_modules/@sentry/cli": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-3.3.4.tgz", - "integrity": "sha512-r97H1GTdaRs1qhTvbzyomclPesrt4vpjY2W7KGtgSOa5ynQsXKajsM5oJOtNW99O1pNNMZFlR1mmGDMHOxYm4g==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-3.3.5.tgz", + "integrity": "sha512-eyLHTj0rpeCsOUX+1ZU8UEWRXy6nXvTXNdhtAt1t6YXan9gSsAexZf28zVmDcYcP8WRbK0D2JMLp7NcaQCQgEA==", "hasInstallScript": true, "license": "FSL-1.1-MIT", "dependencies": { @@ -7028,20 +7028,20 @@ "node": ">= 18" }, "optionalDependencies": { - "@sentry/cli-darwin": "3.3.4", - "@sentry/cli-linux-arm": "3.3.4", - "@sentry/cli-linux-arm64": "3.3.4", - "@sentry/cli-linux-i686": "3.3.4", - "@sentry/cli-linux-x64": "3.3.4", - "@sentry/cli-win32-arm64": "3.3.4", - "@sentry/cli-win32-i686": "3.3.4", - "@sentry/cli-win32-x64": "3.3.4" + "@sentry/cli-darwin": "3.3.5", + "@sentry/cli-linux-arm": "3.3.5", + "@sentry/cli-linux-arm64": "3.3.5", + "@sentry/cli-linux-i686": "3.3.5", + "@sentry/cli-linux-x64": "3.3.5", + "@sentry/cli-win32-arm64": "3.3.5", + "@sentry/cli-win32-i686": "3.3.5", + "@sentry/cli-win32-x64": "3.3.5" } }, "node_modules/@sentry/cli-darwin": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-3.3.4.tgz", - "integrity": "sha512-1cFHgwJq0yJ4lAvxQISag2R5R/wRtY7e4YX54Da4n+Isw1WHdF2CLmdT0ufOyT04iiF406UD2d7qsPEVDniAmw==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-3.3.5.tgz", + "integrity": "sha512-E/SIY6+j2nt6Ri9nMt78sYle3LiF6uZyz4HGmvcEMU6HXjegmAayhy0J10JST+vZTzN6VixD8sUsa5UeJiOPcg==", "license": "FSL-1.1-MIT", "optional": true, "os": [ @@ -7052,9 +7052,9 @@ } }, "node_modules/@sentry/cli-linux-arm": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-3.3.4.tgz", - "integrity": "sha512-8XDDmUZ/4X7Dw2hoSU6T9tpD8qMwtVKHYLQjcY+xNBQujPrSq+YCrNXK/iIN9UgX8Rza2q4IftsIkJADOxLFow==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-3.3.5.tgz", + "integrity": "sha512-EGuEIvC2OQyar/vu+jAQEmovTMgxpoxdx5knnzL5dLhIemjEUNqwv/sXq+m/Aj+ThqCMofcTWB2TOZXsTtl0Tw==", "cpu": [ "arm" ], @@ -7070,9 +7070,9 @@ } }, "node_modules/@sentry/cli-linux-arm64": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-3.3.4.tgz", - "integrity": "sha512-SD9YQjUPXjIBkt4q41lHMopeL9lKskaxc7qpt1ZuQpoHOszDOUNP3WPvxpeiaMjFMmgMkGojyDBk2XY9eyfGNQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-3.3.5.tgz", + "integrity": "sha512-/W7HTk2OFKD0bguTvQR1ue6pkFQWaGiqPafOSIQKyq0aGfbZhBn/Uj+IRefgMZMhJQ29xRz0y/iGRGKE+ef4Vg==", "cpu": [ "arm64" ], @@ -7088,9 +7088,9 @@ } }, "node_modules/@sentry/cli-linux-i686": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-3.3.4.tgz", - "integrity": "sha512-LNQlRDPrHLTDgxxJsAzT+1+sJ8Kv/Lq8E4Ob8RjqkwuZxl/wR6QJ4O83cxYGJPPnmjEAT+lOUQt1pTPXAwIwLQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-3.3.5.tgz", + "integrity": "sha512-qODMEWLEeUNp3IUlwwISB37EXSo8qgMmHQuLKfxDjpIKw+7NAFfptOloqPrHkLWK3TzFr+Nv643wIKZaYrz28Q==", "cpu": [ "x86", "ia32" @@ -7107,9 +7107,9 @@ } }, "node_modules/@sentry/cli-linux-x64": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-3.3.4.tgz", - "integrity": "sha512-yEOVI4a0RTBYcHJBEaiFU2s4GzcfkXDToMUeLlUg4B3Bgz8AX76163RTEJH5dKavKkoMLKzOrKgVylXPxo1JLQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-3.3.5.tgz", + "integrity": "sha512-DCz7lQ4PySjQ1WvWOQ/uTdwauRo1D7hSHazxZ+fUAnK/epSPM9qLkjDMlD8uM5CaLoR8+ZTs7N94vV5LZs2QpA==", "cpu": [ "x64" ], @@ -7125,9 +7125,9 @@ } }, "node_modules/@sentry/cli-win32-arm64": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-3.3.4.tgz", - "integrity": "sha512-aLGgnIf7FHK+yRsemXGQ1yF0Q4R3D/jwCf/20k1miUgFP9fn5mZt+fArGDHr5k3vFfh3bUTf22Ga4CUwXqwkvQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-3.3.5.tgz", + "integrity": "sha512-VMNsHiyZcP8Ft3fcK/1zoO4L66soe1eSfXg2tglFQSc/2MYA5v1Br9B1GtjBwDIc3EmdPtFZhOGLyqIzszMxJw==", "cpu": [ "arm64" ], @@ -7141,9 +7141,9 @@ } }, "node_modules/@sentry/cli-win32-i686": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-3.3.4.tgz", - "integrity": "sha512-mLD5NpgI3G3+f1iBWGqTTC1kvdQ0CzmkvM9aIRiXUYWXZiaZVd4YuqhoDvTU6zNFEUXI+9jUEp84VF171B0Pqg==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-3.3.5.tgz", + "integrity": "sha512-BE6aHOIpsm4jgavsvvXNcSikAr/8NSva3rk1N3BzoOLuX+dcFxBI60K1i2VzB1vsgtivJJo9YySNCi60dBgWTg==", "cpu": [ "x86", "ia32" @@ -7158,9 +7158,9 @@ } }, "node_modules/@sentry/cli-win32-x64": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-3.3.4.tgz", - "integrity": "sha512-dNWifGo3VLx7n3N3/m+7+rLGNZEb7JmnLwLLAHoz11DneCa6OTBSMCKFABArxqLinlzTbiSOYc8QbvCTcLk5FA==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-3.3.5.tgz", + "integrity": "sha512-MSU+PtBuiLjEbiPFOvxk4CI3TCagwkIg9kvJ+DrI3+pBY0Sga/dOyeWKTIgb01xSVcfjdw0UkpU52VCvzTT9ew==", "cpu": [ "x64" ], @@ -8042,16 +8042,16 @@ } }, "node_modules/antd": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/antd/-/antd-6.3.4.tgz", - "integrity": "sha512-Bu6JivPP7bFfYIdVj+61dxhwSOz+A3m0W7PlDasFGC3H3sNMYQ9gJXZoo11/rQh7pTlOQa351q5Ig/zjI98XYw==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.3.5.tgz", + "integrity": "sha512-8BPz9lpZWQm42PTx7yL4KxWAotVuqINiKcoYRcLtdd5BFmAcAZicVyFTnBJyRDlzGZFZeRW3foGu6jXYFnej6Q==", "license": "MIT", "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", - "@ant-design/icons": "^6.1.0", + "@ant-design/icons": "^6.1.1", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.28.4", "@rc-component/cascader": "~1.14.0", @@ -8067,7 +8067,7 @@ "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.6.0", "@rc-component/menu": "~1.2.0", - "@rc-component/motion": "^1.3.1", + "@rc-component/motion": "^1.3.2", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~1.2.0", "@rc-component/pagination": "~1.2.0", @@ -8075,7 +8075,7 @@ "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", - "@rc-component/resize-observer": "^1.1.1", + "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", @@ -8426,14 +8426,23 @@ } }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" } }, "node_modules/babel-plugin-macros": { @@ -11441,9 +11450,9 @@ } }, "node_modules/graphql-ws": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.7.tgz", - "integrity": "sha512-yoLRW+KRlDmnnROdAu7sX77VNLC0bsFoZyGQJLy1cF+X/SkLg/fWkRGrEEYQK8o2cafJ2wmEaMqMEZB3U3DYDg==", + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.8.tgz", + "integrity": "sha512-m3EOaNsUBXwAnkBWbzPfe0Nq8pXUfxsWnolC54sru3FzHvhTZL0Ouf/BoQsaGAXqM+YPerXOJ47BUnmgmoupCw==", "license": "MIT", "engines": { "node": ">=20" @@ -12933,9 +12942,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.12.40", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.40.tgz", - "integrity": "sha512-HKGs7GowShNls3Zh+7DTr6wYpPk5jC78l508yQQY3e8ZgJChM3A9JZghmMJZuK+5bogSfuTafpjksGSR3aMIEg==", + "version": "1.12.41", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.41.tgz", + "integrity": "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA==", "license": "MIT" }, "node_modules/lightningcss": { @@ -15076,9 +15085,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.363.6", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.363.6.tgz", - "integrity": "sha512-eQ+Ypml3JOEMQWt21XEea6J8vD77TNyoz4Yv/xxjlTRja+ilmJtQw/SuVAB3BobjgHKYUomXX7Fc4gH/zTVpbg==", + "version": "1.364.2", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.364.2.tgz", + "integrity": "sha512-ryeCFcaORLouVI5wsKxnraIDvKFM6RAxbbKlKuqo+A8VFZ9JvvRpwzfiMQR2trGsJUYcn6B3R4Rn0Xht9NrhAQ==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@opentelemetry/api": "^1.9.0", @@ -15086,8 +15095,8 @@ "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", - "@posthog/core": "1.24.1", - "@posthog/types": "1.363.6", + "@posthog/core": "1.24.4", + "@posthog/types": "1.364.2", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", @@ -18513,9 +18522,9 @@ } }, "node_modules/vite-plugin-node-polyfills": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.25.0.tgz", - "integrity": "sha512-rHZ324W3LhfGPxWwQb2N048TThB6nVvnipsqBUJEzh3R9xeK9KI3si+GMQxCuAcpPJBVf0LpDtJ+beYzB3/chg==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.26.0.tgz", + "integrity": "sha512-BAe5YzJf368XGev02hDvioidx4uVH8dqEJlG73bjQSxM26/AQnGcKFomq9n3vGq5yqpSHKN4h1XQNxx9l98mBg==", "dev": true, "license": "MIT", "dependencies": { @@ -18526,7 +18535,7 @@ "url": "https://github.com/sponsors/davidmyersdev" }, "peerDependencies": { - "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/vite-plugin-pwa": { diff --git a/client/package.json b/client/package.json index 295da31ab..dfa8f4de2 100644 --- a/client/package.json +++ b/client/package.json @@ -8,7 +8,7 @@ "private": true, "proxy": "http://localhost:4000", "dependencies": { - "@amplitude/analytics-browser": "^2.37.2", + "@amplitude/analytics-browser": "^2.38.0", "@ant-design/pro-layout": "^7.22.6", "@apollo/client": "^4.1.6", "@dnd-kit/core": "^6.3.1", @@ -24,15 +24,15 @@ "@firebase/messaging": "^0.12.25", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.11.2", - "@sentry/cli": "^3.3.4", + "@sentry/cli": "^3.3.5", "@sentry/react": "^10.46.0", "@sentry/vite-plugin": "^4.9.1", "@splitsoftware/splitio-react": "^2.6.1", "@tanem/react-nprogress": "^5.0.63", - "antd": "^6.3.4", + "antd": "^6.3.5", "apollo-link-logger": "^3.0.0", "autosize": "^6.0.1", - "axios": "^1.13.6", + "axios": "^1.14.0", "classnames": "^2.5.1", "css-box-model": "^1.2.1", "dayjs": "^1.11.20", @@ -42,11 +42,11 @@ "env-cmd": "^11.0.0", "exifr": "^7.1.3", "graphql": "^16.13.2", - "graphql-ws": "^6.0.7", + "graphql-ws": "^6.0.8", "i18next": "^25.10.10", "i18next-browser-languagedetector": "^8.2.1", "immutability-helper": "^3.1.1", - "libphonenumber-js": "^1.12.40", + "libphonenumber-js": "^1.12.41", "lightningcss": "^1.32.0", "logrocket": "^12.1.0", "markerjs2": "^2.32.7", @@ -54,7 +54,7 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.71", - "posthog-js": "^1.363.6", + "posthog-js": "^1.364.2", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", @@ -140,7 +140,7 @@ "@ant-design/icons": "^6.1.1", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.28.5", - "@dotenvx/dotenvx": "^1.57.2", + "@dotenvx/dotenvx": "^1.59.1", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", "@eslint/js": "^9.39.2", @@ -167,7 +167,7 @@ "vite": "^7.3.1", "vite-plugin-babel": "^1.6.0", "vite-plugin-eslint": "^1.8.1", - "vite-plugin-node-polyfills": "^0.25.0", + "vite-plugin-node-polyfills": "^0.26.0", "vite-plugin-pwa": "^1.2.0", "vite-plugin-style-import": "^2.0.0", "vitest": "^4.1.2", diff --git a/package-lock.json b/package-lock.json index 1aeb8663b..8d4aad6aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,22 +9,22 @@ "version": "0.2.0", "license": "UNLICENSED", "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.1018.0", - "@aws-sdk/client-elasticache": "^3.1018.0", - "@aws-sdk/client-s3": "^3.1017.0", - "@aws-sdk/client-secrets-manager": "^3.1018.0", - "@aws-sdk/client-ses": "^3.1017.0", - "@aws-sdk/client-sqs": "^3.1017.0", - "@aws-sdk/client-textract": "^3.1017.0", - "@aws-sdk/credential-provider-node": "^3.972.26", - "@aws-sdk/lib-storage": "^3.1017.0", - "@aws-sdk/s3-request-presigner": "^3.1017.0", + "@aws-sdk/client-cloudwatch-logs": "^3.1019.0", + "@aws-sdk/client-elasticache": "^3.1019.0", + "@aws-sdk/client-s3": "^3.1019.0", + "@aws-sdk/client-secrets-manager": "^3.1019.0", + "@aws-sdk/client-ses": "^3.1019.0", + "@aws-sdk/client-sqs": "^3.1019.0", + "@aws-sdk/client-textract": "^3.1019.0", + "@aws-sdk/credential-provider-node": "^3.972.27", + "@aws-sdk/lib-storage": "^3.1019.0", + "@aws-sdk/s3-request-presigner": "^3.1019.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.1", "aws4": "^1.13.2", - "axios": "^1.13.6", + "axios": "^1.14.0", "axios-curlirize": "^2.0.0", "better-queue": "^3.8.12", "bullmq": "^5.71.1", @@ -292,15 +292,15 @@ } }, "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.1018.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1018.0.tgz", - "integrity": "sha512-y1Siaj4PP7PRDtUyjYHQnEl6l2uU2h8zApqcxZnP/ThTy745JFRAKbJRMoAzmV9dPPub3ymCuxVnXm3XYWFbtg==", + "version": "3.1019.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1019.0.tgz", + "integrity": "sha512-1sXELLPuRMg0x2RWXJrLeMvXLkRtDQjk/1qf9b2trusbf/Z+8+vb+NGXZo2y9xsvl+zd03ZVXrGJ9jWrbEgnfQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.25", - "@aws-sdk/credential-provider-node": "^3.972.26", + "@aws-sdk/credential-provider-node": "^3.972.27", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", @@ -345,15 +345,15 @@ } }, "node_modules/@aws-sdk/client-elasticache": { - "version": "3.1018.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.1018.0.tgz", - "integrity": "sha512-SPkP6CZ8C6CPZmyz0CithnX+Q/GztW0AbjbuUyQkycHSkEAZ7H+DeH+CzzwN1/8rWMd7WJEF7CjE5WFatYh4Qg==", + "version": "3.1019.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.1019.0.tgz", + "integrity": "sha512-QAv2UF61rXboCsdcGhsnj14m73Xf2Vh44vbNwp0c/CjculBe80XmJh6d7GQX0AmAWDM2EG9+pCJDJ2turq00Kw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.25", - "@aws-sdk/credential-provider-node": "^3.972.26", + "@aws-sdk/credential-provider-node": "^3.972.27", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", @@ -396,32 +396,32 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1017.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1017.0.tgz", - "integrity": "sha512-WmmPn2NEfkxxzDA0D7rlf3f32gqmqpaTABhlz4EnZbg/RfNWaOu3ecaI5xY0ragrLhvPB+1aPN9GRDnivJukvg==", + "version": "3.1019.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1019.0.tgz", + "integrity": "sha512-0pb9x7PPhS4oEi4c0rL3vzQQoXA4cWKtPuGga/UfVYLZ68yrqdq0NDKg0fr55qzdhNvWFCpmGx73g9Iyy03kkA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.24", - "@aws-sdk/credential-provider-node": "^3.972.25", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/credential-provider-node": "^3.972.27", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", - "@aws-sdk/middleware-flexible-checksums": "^3.974.4", + "@aws-sdk/middleware-flexible-checksums": "^3.974.5", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-sdk-s3": "^3.972.25", + "@aws-sdk/middleware-recursion-detection": "^3.972.9", + "@aws-sdk/middleware-sdk-s3": "^3.972.26", "@aws-sdk/middleware-ssec": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.25", - "@aws-sdk/region-config-resolver": "^3.972.9", - "@aws-sdk/signature-v4-multi-region": "^3.996.13", + "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/region-config-resolver": "^3.972.10", + "@aws-sdk/signature-v4-multi-region": "^3.996.14", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.11", + "@aws-sdk/util-user-agent-node": "^3.973.12", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", @@ -462,15 +462,15 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.1018.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1018.0.tgz", - "integrity": "sha512-svV2NpjS+SfMVk8O0b1thuzNlQ5GhIgxAawIPOUCuLf1fLpGhGNgFcgUXlTnkcAoytZpfFdJTBq5JxtV0AgL/A==", + "version": "3.1019.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1019.0.tgz", + "integrity": "sha512-pocE77Q7wmnt8grxi0qNKUIq05GW1USIqZ6jwr/pC9zd5lwp9BHIFWxg/pkJ4ffNPbX9LekimpoL8IYl3ScWbQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.25", - "@aws-sdk/credential-provider-node": "^3.972.26", + "@aws-sdk/credential-provider-node": "^3.972.27", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", @@ -512,24 +512,24 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.1017.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.1017.0.tgz", - "integrity": "sha512-2PWXGyniy/j1TZ9sixv5fbudedXV9RihJGq+RjmmNEnlse+oGN7XByFCPLACAvi7Mgr9jjW8Vj1iWB9ezS1zJQ==", + "version": "3.1019.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.1019.0.tgz", + "integrity": "sha512-O7WfdVfl6OEo2b/aS7J6XaEsJvDbAKelFG6GfNpVHSi0OU2buYmpApjkeikw69tafCGBdd+rye5Hs+8M9BFLUQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.24", - "@aws-sdk/credential-provider-node": "^3.972.25", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/credential-provider-node": "^3.972.27", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.25", - "@aws-sdk/region-config-resolver": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.9", + "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.11", + "@aws-sdk/util-user-agent-node": "^3.973.12", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", @@ -563,25 +563,25 @@ } }, "node_modules/@aws-sdk/client-sqs": { - "version": "3.1017.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1017.0.tgz", - "integrity": "sha512-Y5FRcAo1lkeOMp6+q7bGSAP3NUdR61VLYzW9J+ksz1KhHLQfCQEzNaTzjwIJyEW2FjJ8w08b/tcScG0Fde0NiA==", + "version": "3.1019.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1019.0.tgz", + "integrity": "sha512-6QEA/YWHV6Zd0J32YTsWzkWNziIiYbEZDw+xAsU9JyfT1JSA+kPZ7rbDP4LoV80ltEnoACLEODzdSoFTaUHW7A==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.24", - "@aws-sdk/credential-provider-node": "^3.972.25", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/credential-provider-node": "^3.972.27", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-sdk-sqs": "^3.972.17", - "@aws-sdk/middleware-user-agent": "^3.972.25", - "@aws-sdk/region-config-resolver": "^3.972.9", + "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.11", + "@aws-sdk/util-user-agent-node": "^3.973.12", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", @@ -615,24 +615,24 @@ } }, "node_modules/@aws-sdk/client-textract": { - "version": "3.1017.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-textract/-/client-textract-3.1017.0.tgz", - "integrity": "sha512-uGwy8wO3qLlvO8OSfnjchBlDYAQx6iHmCOPlEPDF+hhBpd3IUAOXImd0MXK1fbn9gZ/i1i8//1f2UC/PxEah4w==", + "version": "3.1019.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-textract/-/client-textract-3.1019.0.tgz", + "integrity": "sha512-5NG2gBu8X7noIUcj20L6k2cAYNl4ptafTpJbxRxeGKK1i0w8SY1W9Nv/doFRaI5GlyNZHx3y7QAukRMbhJv4jw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.24", - "@aws-sdk/credential-provider-node": "^3.972.25", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/credential-provider-node": "^3.972.27", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.25", - "@aws-sdk/region-config-resolver": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.9", + "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.11", + "@aws-sdk/util-user-agent-node": "^3.973.12", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", @@ -739,19 +739,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.25.tgz", - "integrity": "sha512-G/v/PicYn4qs7xCv4vT6I4QKdvMyRvsgIFNBkUueCGlbLo7/PuKcNKgUozmLSsaYnE7jIl6UrfkP07EUubr48w==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.26.tgz", + "integrity": "sha512-xKxEAMuP6GYx2y5GET+d3aGEroax3AgGfwBE65EQAUe090lzyJ/RzxPX9s8v7Z6qAk0XwfQl+LrmH05X7YvTeg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.25", "@aws-sdk/credential-provider-env": "^3.972.23", "@aws-sdk/credential-provider-http": "^3.972.25", - "@aws-sdk/credential-provider-login": "^3.972.25", + "@aws-sdk/credential-provider-login": "^3.972.26", "@aws-sdk/credential-provider-process": "^3.972.23", - "@aws-sdk/credential-provider-sso": "^3.972.25", - "@aws-sdk/credential-provider-web-identity": "^3.972.25", - "@aws-sdk/nested-clients": "^3.996.15", + "@aws-sdk/credential-provider-sso": "^3.972.26", + "@aws-sdk/credential-provider-web-identity": "^3.972.26", + "@aws-sdk/nested-clients": "^3.996.16", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", @@ -764,13 +764,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.25.tgz", - "integrity": "sha512-bUdmyJeVua7SmD+g2a65x2/0YqsGn4K2k4GawI43js0odaNaIzpIhLtHehUnPnfLuyhPWbJR1NyuIO4iMVfM0w==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.26.tgz", + "integrity": "sha512-EFcM8RM3TUxnZOfMJo++3PnyxFu1fL/huzmn3Vh+8IWRgqZawUD3cRwwOr+/4bE9DpyHaLOWFAjY0lfK5X9ZkQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.25", - "@aws-sdk/nested-clients": "^3.996.15", + "@aws-sdk/nested-clients": "^3.996.16", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", @@ -783,17 +783,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.26.tgz", - "integrity": "sha512-5XSK74rCXxCNj+UWv5bjq1EccYkiyW4XOHFU9NXnsCcQF8dJuHdua1qFg0m/LIwVOWklbKsrcnMtfxIXwgvwzQ==", + "version": "3.972.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.27.tgz", + "integrity": "sha512-jXpxSolfFnPVj6GCTtx3xIdWNoDR7hYC/0SbetGZxOC9UnNmipHeX1k6spVstf7eWJrMhXNQEgXC0pD1r5tXIg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.23", "@aws-sdk/credential-provider-http": "^3.972.25", - "@aws-sdk/credential-provider-ini": "^3.972.25", + "@aws-sdk/credential-provider-ini": "^3.972.26", "@aws-sdk/credential-provider-process": "^3.972.23", - "@aws-sdk/credential-provider-sso": "^3.972.25", - "@aws-sdk/credential-provider-web-identity": "^3.972.25", + "@aws-sdk/credential-provider-sso": "^3.972.26", + "@aws-sdk/credential-provider-web-identity": "^3.972.26", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", @@ -823,14 +823,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.25.tgz", - "integrity": "sha512-r4OGAfHmlEa1QBInHWz+/dOD4tRljcjVNQe9wJ/AJNXEj1d2WdsRLppvRFImRV6FIs+bTpjtL0a23V5ELQpRPw==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.26.tgz", + "integrity": "sha512-c6ghvRb6gTlMznWhGxn/bpVCcp0HRaz4DobGVD9kI4vwHq186nU2xN/S7QGkm0lo0H2jQU8+dgpUFLxfTcwCOg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.25", - "@aws-sdk/nested-clients": "^3.996.15", - "@aws-sdk/token-providers": "3.1018.0", + "@aws-sdk/nested-clients": "^3.996.16", + "@aws-sdk/token-providers": "3.1019.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -842,13 +842,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.25.tgz", - "integrity": "sha512-uM1OtoJgj+yK3MlAmda8uR9WJJCdm5HB25JyCeFL5a5q1Fbafalf4uKidFO3/L0Pgd+Fsflkb4cM6jHIswi3QQ==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.26.tgz", + "integrity": "sha512-cXcS3+XD3iwhoXkM44AmxjmbcKueoLCINr1e+IceMmCySda5ysNIfiGBGe9qn5EMiQ9Jd7pP0AGFtcd6OV3Lvg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.25", - "@aws-sdk/nested-clients": "^3.996.15", + "@aws-sdk/nested-clients": "^3.996.16", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -860,12 +860,11 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.1017.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1017.0.tgz", - "integrity": "sha512-sWUsr3c46WWZxmBAFSYpI+Gr8tFhP+L3h4XmXZxAyWimHpIeW5+A4wR9SAYohBL/+pPMlN9PEYj3H+KfsYCAyA==", + "version": "3.1019.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1019.0.tgz", + "integrity": "sha512-btVQ5wp4VWzP1A1RpCB1MYGyblMxj+uDs07tQa8LGQEml0QTdKxfsYypcGMKGlsV0PN1es3XGnMSjlfWSFO0kQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", @@ -879,7 +878,7 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.1017.0" + "@aws-sdk/client-s3": "^3.1019.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { @@ -916,15 +915,15 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.974.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.4.tgz", - "integrity": "sha512-fhCbZXPAyy8btnNbnBlR7Cc1nD54cETSvGn2wey71ehsM89AKPO8Dpco9DBAAgvrUdLrdHQepBXcyX4vxC5OwA==", + "version": "3.974.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.5.tgz", + "integrity": "sha512-SPSvF0G1t8m8CcB0L+ClNFszzQOvXaxmRj25oRWDf6aU+TuN2PXPFAJ9A6lt1IvX4oGAqqbTdMPTYs/SSHUYYQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.973.24", + "@aws-sdk/core": "^3.973.25", "@aws-sdk/crc64-nvme": "^3.972.5", "@aws-sdk/types": "^3.973.6", "@smithy/is-array-buffer": "^4.2.2", @@ -1075,9 +1074,9 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.15.tgz", - "integrity": "sha512-k6WAVNkub5DrU46iPQvH1m0xc1n+0dX79+i287tYJzf5g1yU2rX3uf4xNeL5JvK1NtYgfwMnsxHqhOXFBn367A==", + "version": "3.996.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.16.tgz", + "integrity": "sha512-L7Qzoj/qQU1cL5GnYLQP5LbI+wlLCLoINvcykR3htKcQ4tzrPf2DOs72x933BM7oArYj1SKrkb2lGlsJHIic3g==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -1140,12 +1139,12 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.1017.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1017.0.tgz", - "integrity": "sha512-PSR8VJEkCy53uhAeuvht6ub3kzfdqoTAmLliQJ63MkC/1FuMmrmqWRGoZuzZvAbpzTcZtuibSGvawDa47gsckA==", + "version": "3.1019.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1019.0.tgz", + "integrity": "sha512-KFv5UaIORIF6MTmEc79MQTPQSnRZjUmOIaOzXn9g6ujtViQLIrNYJiaSmVw8LqK1ebcndS6L6s4bBdLd9AQVJA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/signature-v4-multi-region": "^3.996.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.14", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-format-url": "^3.972.8", "@smithy/middleware-endpoint": "^4.4.27", @@ -1176,13 +1175,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1018.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1018.0.tgz", - "integrity": "sha512-97OPNJHy37wmGOX44xAcu6E9oSTiqK9uPcy/fWpmN5uB3JuEp1f6x60Xot/jp+FxwhQWIFUsVJFnm3QKqt7T6Q==", + "version": "3.1019.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1019.0.tgz", + "integrity": "sha512-OF+2RfRmUKyjzrRWlDcyju3RBsuqcrYDQ8TwrJg8efcOotMzuZN4U9mpVTIdATpmEc4lWNZBMSjPzrGm6JPnAQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.25", - "@aws-sdk/nested-clients": "^3.996.15", + "@aws-sdk/nested-clients": "^3.996.16", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -4527,14 +4526,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" } }, "node_modules/axios-curlirize": { @@ -9634,10 +9633,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", diff --git a/package.json b/package.json index 3e144902c..116300433 100644 --- a/package.json +++ b/package.json @@ -18,22 +18,22 @@ "job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js" }, "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.1018.0", - "@aws-sdk/client-elasticache": "^3.1018.0", - "@aws-sdk/client-s3": "^3.1017.0", - "@aws-sdk/client-secrets-manager": "^3.1018.0", - "@aws-sdk/client-ses": "^3.1017.0", - "@aws-sdk/client-sqs": "^3.1017.0", - "@aws-sdk/client-textract": "^3.1017.0", - "@aws-sdk/credential-provider-node": "^3.972.26", - "@aws-sdk/lib-storage": "^3.1017.0", - "@aws-sdk/s3-request-presigner": "^3.1017.0", + "@aws-sdk/client-cloudwatch-logs": "^3.1019.0", + "@aws-sdk/client-elasticache": "^3.1019.0", + "@aws-sdk/client-s3": "^3.1019.0", + "@aws-sdk/client-secrets-manager": "^3.1019.0", + "@aws-sdk/client-ses": "^3.1019.0", + "@aws-sdk/client-sqs": "^3.1019.0", + "@aws-sdk/client-textract": "^3.1019.0", + "@aws-sdk/credential-provider-node": "^3.972.27", + "@aws-sdk/lib-storage": "^3.1019.0", + "@aws-sdk/s3-request-presigner": "^3.1019.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.1", "aws4": "^1.13.2", - "axios": "^1.13.6", + "axios": "^1.14.0", "axios-curlirize": "^2.0.0", "better-queue": "^3.8.12", "bullmq": "^5.71.1", From 6759bc5865b9c03795851e656c1f70981bb950ba Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 30 Mar 2026 17:24:42 -0400 Subject: [PATCH 46/56] release/2026-04-03 - Remove console.dir --- server/chatter/createLocation.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/chatter/createLocation.js b/server/chatter/createLocation.js index 18c51305f..93d5e11db 100644 --- a/server/chatter/createLocation.js +++ b/server/chatter/createLocation.js @@ -33,8 +33,6 @@ const createLocation = async (req, res) => { const { logger } = req; const { bodyshopID, googlePlaceID } = req.body; - console.dir({ body: req.body }); - if (!DEFAULT_COMPANY_ID) { logger.log("chatter-create-location-no-default-company", "warn", null, null, { bodyshopID }); return res.json({ success: false, message: "No default company set" }); From c0a37d7c1a2c9468eac51000db43a5cc7a2afc5a Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 31 Mar 2026 09:50:08 -0700 Subject: [PATCH 47/56] IO-1366 Bill Reexport Audit Log Signed-off-by: Allan Carr --- .../bill-reexport-button.component.jsx | 14 +++++++++++--- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + client/src/utils/AuditTrailMappings.js | 1 + 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/client/src/components/bill-reexport-button/bill-reexport-button.component.jsx b/client/src/components/bill-reexport-button/bill-reexport-button.component.jsx index ae7641b38..80f35df57 100644 --- a/client/src/components/bill-reexport-button/bill-reexport-button.component.jsx +++ b/client/src/components/bill-reexport-button/bill-reexport-button.component.jsx @@ -9,18 +9,20 @@ import { createStructuredSelector } from "reselect"; import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors"; import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import { insertAuditTrail } from "../../redux/application/application.actions.js"; +import AuditTrailMapping from "../../utils/AuditTrailMappings.js"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, authLevel: selectAuthLevel }); -const mapDispatchToProps = () => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) +const mapDispatchToProps = (dispatch) => ({ + insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })) }); export default connect(mapStateToProps, mapDispatchToProps)(BillMarkForReexportButton); -export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) { +export function BillMarkForReexportButton({ bodyshop, authLevel, bill, insertAuditTrail }) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); const notification = useNotification(); @@ -47,6 +49,12 @@ export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) { notification.success({ title: t("bills.successes.reexport") }); + insertAuditTrail({ + jobid: bill.jobid, + billid: bill.id, + operation: AuditTrailMapping.billmarkforreexport(bill.invoice_number), + type: "billmarkforreexport" + }); } else { notification.error({ title: t("bills.errors.saving", { diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 76e55c8f3..f0d1ebbff 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -120,6 +120,7 @@ "appointmentinsert": "Appointment created. Appointment Date: {{start}}.", "assignedlinehours": "Assigned job lines totaling {{hours}} units to {{team}}.", "billdeleted": "Bill with invoice number {{invoice_number}} deleted.", + "billmarkforreexport": "Bill with invoice number {{invoice_number}} marked for re-export.", "billposted": "Bill with invoice number {{invoice_number}} posted.", "billupdated": "Bill with invoice number {{invoice_number}} updated.", "failedpayment": "Failed payment attempt.", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 683e1d5b8..3f1d15ce6 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -121,6 +121,7 @@ "assignedlinehours": "", "billdeleted": "", "billposted": "", + "billmarkforreexport": "", "billupdated": "", "failedpayment": "", "jobassignmentchange": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 7ca765876..c1520a5e3 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -120,6 +120,7 @@ "appointmentinsert": "", "assignedlinehours": "", "billdeleted": "", + "billmarkforreexport": "", "billposted": "", "billupdated": "", "failedpayment": "", diff --git a/client/src/utils/AuditTrailMappings.js b/client/src/utils/AuditTrailMappings.js index ea1494fec..e33082c20 100644 --- a/client/src/utils/AuditTrailMappings.js +++ b/client/src/utils/AuditTrailMappings.js @@ -8,6 +8,7 @@ const AuditTrailMapping = { appointmentcancel: (lost_sale_reason) => i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }), appointmentinsert: (start) => i18n.t("audit_trail.messages.appointmentinsert", { start }), billdeleted: (invoice_number) => i18n.t("audit_trail.messages.billdeleted", { invoice_number }), + billmarkforreexport: (invoice_number) => i18n.t("audit_trail.messages.billmarkforreexport", { invoice_number }), billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }), billupdated: (invoice_number) => i18n.t("audit_trail.messages.billupdated", { invoice_number }), jobassignmentchange: (operation, name) => i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }), From d2dd276ce71d531dc82356847df16b6ed3d6b53c Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 31 Mar 2026 16:16:14 -0400 Subject: [PATCH 48/56] feature/.feature/IO-3624-Shop-Config-UX-Refresh - Bump Deps --- client/package-lock.json | 143 +++++----- client/package.json | 8 +- package-lock.json | 575 +++++++++++++++++++-------------------- package.json | 20 +- 4 files changed, 367 insertions(+), 379 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 11e3ce3bd..8ff1ef726 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -26,7 +26,7 @@ "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.11.2", "@sentry/cli": "^3.3.5", - "@sentry/react": "^10.46.0", + "@sentry/react": "^10.47.0", "@sentry/vite-plugin": "^4.9.1", "@splitsoftware/splitio-react": "^2.6.1", "@tanem/react-nprogress": "^5.0.63", @@ -55,14 +55,14 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.71", - "posthog-js": "^1.364.2", + "posthog-js": "^1.364.4", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", "react": "^19.2.4", "react-big-calendar": "^1.19.4", "react-color": "^2.19.3", - "react-cookie": "^8.0.1", + "react-cookie": "^8.1.0", "react-dom": "^19.2.4", "react-grid-gallery": "^1.0.1", "react-grid-layout": "^2.2.3", @@ -106,7 +106,7 @@ "@testing-library/react": "^16.3.2", "@vitejs/plugin-react": "^5.1.4", "babel-plugin-react-compiler": "^1.0.0", - "browserslist": "^4.28.1", + "browserslist": "^4.28.2", "browserslist-to-esbuild": "^2.1.1", "chalk": "^5.6.2", "eslint": "^9.39.2", @@ -4879,9 +4879,9 @@ } }, "node_modules/@posthog/types": { - "version": "1.364.2", - "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.364.2.tgz", - "integrity": "sha512-SMTdaYanvRmatgheXtu2XkewhuhdXe8C3JCi7m/Hd2n+sa2DaJphcwg3nAkPtfV69JHMxJLe/gyOt7yFtbQSjQ==", + "version": "1.364.4", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.364.4.tgz", + "integrity": "sha512-U7NpIy9XWrzz1q/66xyDu8Wm12a7avNRKRn5ISPT5kuCJQRaeAaHuf+dpgrFnuqjCCgxg+oIY/ReJdlZ+8/z4Q==", "license": "MIT" }, "node_modules/@protobufjs/aspromise": { @@ -6718,50 +6718,50 @@ ] }, "node_modules/@sentry-internal/browser-utils": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.46.0.tgz", - "integrity": "sha512-WB1gBT9G13V02ekZ6NpUhoI1aGHV2eNfjEPthkU2bGBvFpQKnstwzjg7waIRGR7cu+YSW2Q6UI6aQLgBeOPD1g==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.47.0.tgz", + "integrity": "sha512-bVFRAeJWMBcBCvJKIFCMJ1/yQToL4vPGqfmlnDZeypcxkqUDKQ/Y3ziLHXoDL2sx0lagcgU2vH1QhCQ67Aujjw==", "license": "MIT", "dependencies": { - "@sentry/core": "10.46.0" + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.46.0.tgz", - "integrity": "sha512-c4pI/z9nZCQXe9GYEw/hE/YTY9AxGBp8/wgKI+T8zylrN35SGHaXv63szzE1WbI8lacBY8lBF7rstq9bQVCaHw==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.47.0.tgz", + "integrity": "sha512-pdvMmi4dQpX5S/vAAzrhHPIw3T3HjUgDNgUiCBrlp7N9/6zGO2gNPhUnNekP+CjgI/z0rvf49RLqlDenpNrMOg==", "license": "MIT", "dependencies": { - "@sentry/core": "10.46.0" + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.46.0.tgz", - "integrity": "sha512-JBsWeXG6bRbxBFK8GzWymWGOB9QE7Kl57BeF3jzgdHTuHSWZ2mRnAmb1K05T4LU+gVygk6yW0KmdC8Py9Qzg9A==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.47.0.tgz", + "integrity": "sha512-ScdovxP7hJxgMt70+7hFvwT02GIaIUAxdEM/YPsayZBeCoAukPW8WiwztJfoKtsfPyKJ5A6f0H3PIxTPcA9Row==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.46.0", - "@sentry/core": "10.46.0" + "@sentry-internal/browser-utils": "10.47.0", + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.46.0.tgz", - "integrity": "sha512-ub314MWUsekVCuoH0/HJbbimlI24SkV745UW2pj9xRbxOAEf1wjkmIzxKrMDbTgJGuEunug02XZVdJFJUzOcDw==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.47.0.tgz", + "integrity": "sha512-A5OY8friSe6g8WAK4L8IeOPiEd9D3Ps40DzRH5j2f6SUja0t90mKMvHRcRf8zq0d4BkdB+JM7tjOkwxpuv8heA==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "10.46.0", - "@sentry/core": "10.46.0" + "@sentry-internal/replay": "10.47.0", + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" @@ -6777,16 +6777,16 @@ } }, "node_modules/@sentry/browser": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.46.0.tgz", - "integrity": "sha512-80DmGlTk5Z2/OxVOzLNxwolMyouuAYKqG8KUcoyintZqHbF6kO1RulI610HmyUt3OagKeBCqt9S7w0VIfCRL+Q==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.47.0.tgz", + "integrity": "sha512-rC0agZdxKA5XWfL4VwPOr/rJMogXDqZgnVzr93YWpFn9DMZT/7LzxSJVPIJwRUjx3bFEby3PcTa3YaX7pxm1AA==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.46.0", - "@sentry-internal/feedback": "10.46.0", - "@sentry-internal/replay": "10.46.0", - "@sentry-internal/replay-canvas": "10.46.0", - "@sentry/core": "10.46.0" + "@sentry-internal/browser-utils": "10.47.0", + "@sentry-internal/feedback": "10.47.0", + "@sentry-internal/replay": "10.47.0", + "@sentry-internal/replay-canvas": "10.47.0", + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" @@ -7195,22 +7195,22 @@ } }, "node_modules/@sentry/core": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.46.0.tgz", - "integrity": "sha512-N3fj4zqBQOhXliS1Ne9euqIKuciHCGOJfPGQLwBoW9DNz03jF+NB8+dUKtrJ79YLoftjVgf8nbgwtADK7NR+2Q==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.47.0.tgz", + "integrity": "sha512-nsYRAx3EWezDut+Zl+UwwP07thh9uY7CfSAi2whTdcJl5hu1nSp2z8bba7Vq/MGbNLnazkd3A+GITBEML924JA==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.46.0.tgz", - "integrity": "sha512-Rb1S+9OuUPVwsz7GWnQ6Kgf3azbsseUymIegg3JZHNcW/fM1nPpaljzTBnuineia113DH0pgMBcdrrZDLaosFQ==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.47.0.tgz", + "integrity": "sha512-ZtJV6xxF8jUVE9e3YQUG3Do0XapG1GjniyLyqMPgN6cNvs/HaRJODf7m60By+VGqcl5XArEjEPTvx8CdPUXDfA==", "license": "MIT", "dependencies": { - "@sentry/browser": "10.46.0", - "@sentry/core": "10.46.0" + "@sentry/browser": "10.47.0", + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" @@ -8560,12 +8560,15 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.12.tgz", - "integrity": "sha512-Mij6Lij93pTAIsSYy5cyBQ975Qh9uLEc5rwGTpomiZeXZL9yIS6uORJakb3ScHgfs0serMMfIbXzokPMuEiRyw==", + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz", + "integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==", "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bidi-js": { @@ -8797,9 +8800,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "funding": [ { "type": "opencollective", @@ -8816,11 +8819,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -8987,9 +8990,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001762", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", - "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "version": "1.0.30001782", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz", + "integrity": "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==", "funding": [ { "type": "opencollective", @@ -10103,9 +10106,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.329", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.329.tgz", + "integrity": "sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==", "license": "ISC" }, "node_modules/elliptic": { @@ -14325,9 +14328,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "license": "MIT" }, "node_modules/node-stdlib-browser": { @@ -15085,9 +15088,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.364.2", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.364.2.tgz", - "integrity": "sha512-ryeCFcaORLouVI5wsKxnraIDvKFM6RAxbbKlKuqo+A8VFZ9JvvRpwzfiMQR2trGsJUYcn6B3R4Rn0Xht9NrhAQ==", + "version": "1.364.4", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.364.4.tgz", + "integrity": "sha512-T71zr06gH5YcrjS7c+sdzqfZKMxqqXC/a0w++zMQIPbL1ejvF9PdfUi0Kyd6Sy78Ocbb2smobdzBh8vXLwC+lQ==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@opentelemetry/api": "^1.9.0", @@ -15096,7 +15099,7 @@ "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.24.4", - "@posthog/types": "1.364.2", + "@posthog/types": "1.364.4", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", @@ -15462,12 +15465,12 @@ } }, "node_modules/react-cookie": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-8.0.1.tgz", - "integrity": "sha512-QNdAd0MLuAiDiLcDU/2s/eyKmmfMHtjPUKJ2dZ/5CcQ9QKUium4B3o61/haq6PQl/YWFqC5PO8GvxeHKhy3GFA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-8.1.0.tgz", + "integrity": "sha512-Qs+gD3gpQmUXnJUZafhJtNWhhNdi8OYbOAF5YQRAZa/D171ILOIEMfXDz/tmhkE+nOthllmqryHH6I/qmvIYWQ==", "license": "MIT", "dependencies": { - "@types/hoist-non-react-statics": "^3.3.6", + "@types/hoist-non-react-statics": "^3.3.7", "hoist-non-react-statics": "^3.3.2", "universal-cookie": "^8.0.0" }, diff --git a/client/package.json b/client/package.json index dfa8f4de2..7e95380a6 100644 --- a/client/package.json +++ b/client/package.json @@ -25,7 +25,7 @@ "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.11.2", "@sentry/cli": "^3.3.5", - "@sentry/react": "^10.46.0", + "@sentry/react": "^10.47.0", "@sentry/vite-plugin": "^4.9.1", "@splitsoftware/splitio-react": "^2.6.1", "@tanem/react-nprogress": "^5.0.63", @@ -54,14 +54,14 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.71", - "posthog-js": "^1.364.2", + "posthog-js": "^1.364.4", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", "react": "^19.2.4", "react-big-calendar": "^1.19.4", "react-color": "^2.19.3", - "react-cookie": "^8.0.1", + "react-cookie": "^8.1.0", "react-dom": "^19.2.4", "react-grid-gallery": "^1.0.1", "react-grid-layout": "^2.2.3", @@ -150,7 +150,7 @@ "@testing-library/react": "^16.3.2", "@vitejs/plugin-react": "^5.1.4", "babel-plugin-react-compiler": "^1.0.0", - "browserslist": "^4.28.1", + "browserslist": "^4.28.2", "browserslist-to-esbuild": "^2.1.1", "chalk": "^5.6.2", "eslint": "^9.39.2", diff --git a/package-lock.json b/package-lock.json index 8d4aad6aa..3354b9ed7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,16 @@ "version": "0.2.0", "license": "UNLICENSED", "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.1019.0", - "@aws-sdk/client-elasticache": "^3.1019.0", - "@aws-sdk/client-s3": "^3.1019.0", - "@aws-sdk/client-secrets-manager": "^3.1019.0", - "@aws-sdk/client-ses": "^3.1019.0", - "@aws-sdk/client-sqs": "^3.1019.0", - "@aws-sdk/client-textract": "^3.1019.0", - "@aws-sdk/credential-provider-node": "^3.972.27", - "@aws-sdk/lib-storage": "^3.1019.0", - "@aws-sdk/s3-request-presigner": "^3.1019.0", + "@aws-sdk/client-cloudwatch-logs": "^3.1020.0", + "@aws-sdk/client-elasticache": "^3.1020.0", + "@aws-sdk/client-s3": "^3.1020.0", + "@aws-sdk/client-secrets-manager": "^3.1020.0", + "@aws-sdk/client-ses": "^3.1020.0", + "@aws-sdk/client-sqs": "^3.1020.0", + "@aws-sdk/client-textract": "^3.1020.0", + "@aws-sdk/credential-provider-node": "^3.972.28", + "@aws-sdk/lib-storage": "^3.1020.0", + "@aws-sdk/s3-request-presigner": "^3.1020.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", @@ -292,26 +292,26 @@ } }, "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.1019.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1019.0.tgz", - "integrity": "sha512-1sXELLPuRMg0x2RWXJrLeMvXLkRtDQjk/1qf9b2trusbf/Z+8+vb+NGXZo2y9xsvl+zd03ZVXrGJ9jWrbEgnfQ==", + "version": "3.1020.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1020.0.tgz", + "integrity": "sha512-ezS+wZbSJYbncXtfO0BOyNdqeRnKPSRPZNq111aydoZoW0Q/FnTiRbISZoMLqOCnwJVOX3EZMGNGpUvVyJ+CGQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.25", - "@aws-sdk/credential-provider-node": "^3.972.27", + "@aws-sdk/core": "^3.973.26", + "@aws-sdk/credential-provider-node": "^3.972.28", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", - "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/middleware-user-agent": "^3.972.27", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.12", + "@aws-sdk/util-user-agent-node": "^3.973.13", "@smithy/config-resolver": "^4.4.13", - "@smithy/core": "^3.23.12", + "@smithy/core": "^3.23.13", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", @@ -319,21 +319,21 @@ "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.27", - "@smithy/middleware-retry": "^4.4.44", - "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-endpoint": "^4.4.28", + "@smithy/middleware-retry": "^4.4.45", + "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.5.0", + "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.43", - "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-defaults-mode-browser": "^4.3.44", + "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -345,50 +345,50 @@ } }, "node_modules/@aws-sdk/client-elasticache": { - "version": "3.1019.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.1019.0.tgz", - "integrity": "sha512-QAv2UF61rXboCsdcGhsnj14m73Xf2Vh44vbNwp0c/CjculBe80XmJh6d7GQX0AmAWDM2EG9+pCJDJ2turq00Kw==", + "version": "3.1020.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.1020.0.tgz", + "integrity": "sha512-cuYpuMTjsXXbQrrWuqMCZID+oi4D8w3w8aixdVe4Yri//x++mcebxe+oZGRZlKGWZPKQYg1GpCTm+bu8CWygYA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.25", - "@aws-sdk/credential-provider-node": "^3.972.27", + "@aws-sdk/core": "^3.973.26", + "@aws-sdk/credential-provider-node": "^3.972.28", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", - "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/middleware-user-agent": "^3.972.27", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.12", + "@aws-sdk/util-user-agent-node": "^3.973.13", "@smithy/config-resolver": "^4.4.13", - "@smithy/core": "^3.23.12", + "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.27", - "@smithy/middleware-retry": "^4.4.44", - "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-endpoint": "^4.4.28", + "@smithy/middleware-retry": "^4.4.45", + "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.5.0", + "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.43", - "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-defaults-mode-browser": "^4.3.44", + "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.13", + "@smithy/util-waiter": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -396,34 +396,34 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1019.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1019.0.tgz", - "integrity": "sha512-0pb9x7PPhS4oEi4c0rL3vzQQoXA4cWKtPuGga/UfVYLZ68yrqdq0NDKg0fr55qzdhNvWFCpmGx73g9Iyy03kkA==", + "version": "3.1020.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1020.0.tgz", + "integrity": "sha512-ibfxjP5zLUqpujLE0OTgD+jZ3KStx9dTASL7d7Eekw4sv7ZHv1UN6CPDcKnCNXdPzlBWi5Wc5lWJ4sU1M8ygEQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.25", - "@aws-sdk/credential-provider-node": "^3.972.27", + "@aws-sdk/core": "^3.973.26", + "@aws-sdk/credential-provider-node": "^3.972.28", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", - "@aws-sdk/middleware-flexible-checksums": "^3.974.5", + "@aws-sdk/middleware-flexible-checksums": "^3.974.6", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", - "@aws-sdk/middleware-sdk-s3": "^3.972.26", + "@aws-sdk/middleware-sdk-s3": "^3.972.27", "@aws-sdk/middleware-ssec": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/middleware-user-agent": "^3.972.27", "@aws-sdk/region-config-resolver": "^3.972.10", - "@aws-sdk/signature-v4-multi-region": "^3.996.14", + "@aws-sdk/signature-v4-multi-region": "^3.996.15", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.12", + "@aws-sdk/util-user-agent-node": "^3.973.13", "@smithy/config-resolver": "^4.4.13", - "@smithy/core": "^3.23.12", + "@smithy/core": "^3.23.13", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", @@ -434,27 +434,27 @@ "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.27", - "@smithy/middleware-retry": "^4.4.44", - "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-endpoint": "^4.4.28", + "@smithy/middleware-retry": "^4.4.45", + "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.5.0", + "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.43", - "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-defaults-mode-browser": "^4.3.44", + "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", - "@smithy/util-stream": "^4.5.20", + "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.13", + "@smithy/util-waiter": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -462,45 +462,45 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.1019.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1019.0.tgz", - "integrity": "sha512-pocE77Q7wmnt8grxi0qNKUIq05GW1USIqZ6jwr/pC9zd5lwp9BHIFWxg/pkJ4ffNPbX9LekimpoL8IYl3ScWbQ==", + "version": "3.1020.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1020.0.tgz", + "integrity": "sha512-FRQUPEAeRWK/DO/IJWWVQdmfVYifeNQksrToREokbGjJbCD912eNP5szOtqP7kd52QweznXlfLqv3OeTVnP5Ew==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.25", - "@aws-sdk/credential-provider-node": "^3.972.27", + "@aws-sdk/core": "^3.973.26", + "@aws-sdk/credential-provider-node": "^3.972.28", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", - "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/middleware-user-agent": "^3.972.27", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.12", + "@aws-sdk/util-user-agent-node": "^3.973.13", "@smithy/config-resolver": "^4.4.13", - "@smithy/core": "^3.23.12", + "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.27", - "@smithy/middleware-retry": "^4.4.44", - "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-endpoint": "^4.4.28", + "@smithy/middleware-retry": "^4.4.45", + "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.5.0", + "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.43", - "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-defaults-mode-browser": "^4.3.44", + "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -512,50 +512,50 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.1019.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.1019.0.tgz", - "integrity": "sha512-O7WfdVfl6OEo2b/aS7J6XaEsJvDbAKelFG6GfNpVHSi0OU2buYmpApjkeikw69tafCGBdd+rye5Hs+8M9BFLUQ==", + "version": "3.1020.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.1020.0.tgz", + "integrity": "sha512-UfksKJVrZp4esWxF8GuIBCth++jYpGLqkINj0lQaHq/6kz6u6/Md/q8PlTVmLRQo38NCcb9XKg4G0sKOYN7+Hw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.25", - "@aws-sdk/credential-provider-node": "^3.972.27", + "@aws-sdk/core": "^3.973.26", + "@aws-sdk/credential-provider-node": "^3.972.28", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", - "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/middleware-user-agent": "^3.972.27", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.12", + "@aws-sdk/util-user-agent-node": "^3.973.13", "@smithy/config-resolver": "^4.4.13", - "@smithy/core": "^3.23.12", + "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.27", - "@smithy/middleware-retry": "^4.4.44", - "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-endpoint": "^4.4.28", + "@smithy/middleware-retry": "^4.4.45", + "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.5.0", + "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.43", - "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-defaults-mode-browser": "^4.3.44", + "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.13", + "@smithy/util-waiter": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -563,47 +563,47 @@ } }, "node_modules/@aws-sdk/client-sqs": { - "version": "3.1019.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1019.0.tgz", - "integrity": "sha512-6QEA/YWHV6Zd0J32YTsWzkWNziIiYbEZDw+xAsU9JyfT1JSA+kPZ7rbDP4LoV80ltEnoACLEODzdSoFTaUHW7A==", + "version": "3.1020.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1020.0.tgz", + "integrity": "sha512-DDKv7bucz6usrvOZ+O6f8mCrm5mTD1yOC9rYDjbUmUPP50S0HLCT6ZVqcXlKaucHcO0l9sWBx8K4jrvMTCkfDg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.25", - "@aws-sdk/credential-provider-node": "^3.972.27", + "@aws-sdk/core": "^3.973.26", + "@aws-sdk/credential-provider-node": "^3.972.28", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", - "@aws-sdk/middleware-sdk-sqs": "^3.972.17", - "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/middleware-sdk-sqs": "^3.972.18", + "@aws-sdk/middleware-user-agent": "^3.972.27", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.12", + "@aws-sdk/util-user-agent-node": "^3.973.13", "@smithy/config-resolver": "^4.4.13", - "@smithy/core": "^3.23.12", + "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.27", - "@smithy/middleware-retry": "^4.4.44", - "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-endpoint": "^4.4.28", + "@smithy/middleware-retry": "^4.4.45", + "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.5.0", + "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.43", - "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-defaults-mode-browser": "^4.3.44", + "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -615,45 +615,45 @@ } }, "node_modules/@aws-sdk/client-textract": { - "version": "3.1019.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-textract/-/client-textract-3.1019.0.tgz", - "integrity": "sha512-5NG2gBu8X7noIUcj20L6k2cAYNl4ptafTpJbxRxeGKK1i0w8SY1W9Nv/doFRaI5GlyNZHx3y7QAukRMbhJv4jw==", + "version": "3.1020.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-textract/-/client-textract-3.1020.0.tgz", + "integrity": "sha512-7aVLbqnrDeom8Zygv04kcDYdwZR450X4pvR1vF4w6LNZxIBjt4VGhZ4I0Tlvri0KV9aeOPy97xDEKwktDGEgBQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.25", - "@aws-sdk/credential-provider-node": "^3.972.27", + "@aws-sdk/core": "^3.973.26", + "@aws-sdk/credential-provider-node": "^3.972.28", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", - "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/middleware-user-agent": "^3.972.27", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.12", + "@aws-sdk/util-user-agent-node": "^3.973.13", "@smithy/config-resolver": "^4.4.13", - "@smithy/core": "^3.23.12", + "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.27", - "@smithy/middleware-retry": "^4.4.44", - "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-endpoint": "^4.4.28", + "@smithy/middleware-retry": "^4.4.45", + "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.5.0", + "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.43", - "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-defaults-mode-browser": "^4.3.44", + "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -665,19 +665,19 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.25.tgz", - "integrity": "sha512-TNrx7eq6nKNOO62HWPqoBqPLXEkW6nLZQGwjL6lq1jZtigWYbK1NbCnT7mKDzbLMHZfuOECUt3n6CzxjUW9HWQ==", + "version": "3.973.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.26.tgz", + "integrity": "sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.16", - "@smithy/core": "^3.23.12", + "@smithy/core": "^3.23.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", @@ -702,12 +702,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.23", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.23.tgz", - "integrity": "sha512-EamaclJcCEaPHp6wiVknNMM2RlsPMjAHSsYSFLNENBM8Wz92QPc6cOn3dif6vPDQt0Oo4IEghDy3NMDCzY/IvA==", + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.24.tgz", + "integrity": "sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.25", + "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", @@ -718,20 +718,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.25.tgz", - "integrity": "sha512-qPymamdPcLp6ugoVocG1y5r69ScNiRzb0hogX25/ij+Wz7c7WnsgjLTaz7+eB5BfRxeyUwuw5hgULMuwOGOpcw==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.26.tgz", + "integrity": "sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.25", + "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.0", + "@smithy/node-http-handler": "^4.5.1", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.20", + "@smithy/util-stream": "^4.5.21", "tslib": "^2.6.2" }, "engines": { @@ -739,19 +739,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.26.tgz", - "integrity": "sha512-xKxEAMuP6GYx2y5GET+d3aGEroax3AgGfwBE65EQAUe090lzyJ/RzxPX9s8v7Z6qAk0XwfQl+LrmH05X7YvTeg==", + "version": "3.972.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.27.tgz", + "integrity": "sha512-Um26EsNSUfVUX0wUXnUA1W3wzKhVy6nviEElsh5lLZUYj9bk6DXOPnpte0gt+WHubcVfVsRk40bbm4KaroTEag==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.25", - "@aws-sdk/credential-provider-env": "^3.972.23", - "@aws-sdk/credential-provider-http": "^3.972.25", - "@aws-sdk/credential-provider-login": "^3.972.26", - "@aws-sdk/credential-provider-process": "^3.972.23", - "@aws-sdk/credential-provider-sso": "^3.972.26", - "@aws-sdk/credential-provider-web-identity": "^3.972.26", - "@aws-sdk/nested-clients": "^3.996.16", + "@aws-sdk/core": "^3.973.26", + "@aws-sdk/credential-provider-env": "^3.972.24", + "@aws-sdk/credential-provider-http": "^3.972.26", + "@aws-sdk/credential-provider-login": "^3.972.27", + "@aws-sdk/credential-provider-process": "^3.972.24", + "@aws-sdk/credential-provider-sso": "^3.972.27", + "@aws-sdk/credential-provider-web-identity": "^3.972.27", + "@aws-sdk/nested-clients": "^3.996.17", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", @@ -764,13 +764,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.26.tgz", - "integrity": "sha512-EFcM8RM3TUxnZOfMJo++3PnyxFu1fL/huzmn3Vh+8IWRgqZawUD3cRwwOr+/4bE9DpyHaLOWFAjY0lfK5X9ZkQ==", + "version": "3.972.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.27.tgz", + "integrity": "sha512-t3ehEtHomGZwg5Gixw4fYbYtG9JBnjfAjSDabxhPEu/KLLUp0BB37/APX7MSKXQhX6ZH7pseuACFJ19NrAkNdg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.25", - "@aws-sdk/nested-clients": "^3.996.16", + "@aws-sdk/core": "^3.973.26", + "@aws-sdk/nested-clients": "^3.996.17", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", @@ -783,17 +783,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.27.tgz", - "integrity": "sha512-jXpxSolfFnPVj6GCTtx3xIdWNoDR7hYC/0SbetGZxOC9UnNmipHeX1k6spVstf7eWJrMhXNQEgXC0pD1r5tXIg==", + "version": "3.972.28", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.28.tgz", + "integrity": "sha512-rren+P6k5rShG5PX61iVi40kKdueyuMLBRTctQbyR5LooO9Ygr5L6R7ilG7RF1957NSH3KC3TU206fZuKwjSpQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.23", - "@aws-sdk/credential-provider-http": "^3.972.25", - "@aws-sdk/credential-provider-ini": "^3.972.26", - "@aws-sdk/credential-provider-process": "^3.972.23", - "@aws-sdk/credential-provider-sso": "^3.972.26", - "@aws-sdk/credential-provider-web-identity": "^3.972.26", + "@aws-sdk/credential-provider-env": "^3.972.24", + "@aws-sdk/credential-provider-http": "^3.972.26", + "@aws-sdk/credential-provider-ini": "^3.972.27", + "@aws-sdk/credential-provider-process": "^3.972.24", + "@aws-sdk/credential-provider-sso": "^3.972.27", + "@aws-sdk/credential-provider-web-identity": "^3.972.27", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", @@ -806,12 +806,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.23", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.23.tgz", - "integrity": "sha512-IL/TFW59++b7MpHserjUblGrdP5UXy5Ekqqx1XQkERXBFJcZr74I7VaSrQT5dxdRMU16xGK4L0RQ5fQG1pMgnA==", + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.24.tgz", + "integrity": "sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.25", + "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -823,14 +823,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.26.tgz", - "integrity": "sha512-c6ghvRb6gTlMznWhGxn/bpVCcp0HRaz4DobGVD9kI4vwHq186nU2xN/S7QGkm0lo0H2jQU8+dgpUFLxfTcwCOg==", + "version": "3.972.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.27.tgz", + "integrity": "sha512-CWXeGjlbBuHcm9appZUgXKP2zHDyTti0/+gXpSFJ2J3CnSwf1KWjicjN0qG2ozkMH6blrrzMrimeIOEYNl238Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.25", - "@aws-sdk/nested-clients": "^3.996.16", - "@aws-sdk/token-providers": "3.1019.0", + "@aws-sdk/core": "^3.973.26", + "@aws-sdk/nested-clients": "^3.996.17", + "@aws-sdk/token-providers": "3.1020.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -842,13 +842,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.26.tgz", - "integrity": "sha512-cXcS3+XD3iwhoXkM44AmxjmbcKueoLCINr1e+IceMmCySda5ysNIfiGBGe9qn5EMiQ9Jd7pP0AGFtcd6OV3Lvg==", + "version": "3.972.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.27.tgz", + "integrity": "sha512-CUY4hQIFswdQNEsRGEzGBUKGMK5KpqmNDdu2ROMgI+45PLFS8H0y3Tm7kvM16uvvw3n1pVxk85tnRVUTgtaa1w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.25", - "@aws-sdk/nested-clients": "^3.996.16", + "@aws-sdk/core": "^3.973.26", + "@aws-sdk/nested-clients": "^3.996.17", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -860,14 +860,14 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.1019.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1019.0.tgz", - "integrity": "sha512-btVQ5wp4VWzP1A1RpCB1MYGyblMxj+uDs07tQa8LGQEml0QTdKxfsYypcGMKGlsV0PN1es3XGnMSjlfWSFO0kQ==", + "version": "3.1020.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1020.0.tgz", + "integrity": "sha512-SvFM+jukgDkCCeszTtGTJ59kAvWeCI5vnBV0eMau4Uj1w0KItwahDVGDo+xQa+r7pp3kolZF1vDmdv37A5+J8A==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-endpoint": "^4.4.28", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "buffer": "5.6.0", "events": "3.3.0", @@ -878,7 +878,7 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.1019.0" + "@aws-sdk/client-s3": "^3.1020.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { @@ -915,15 +915,15 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.974.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.5.tgz", - "integrity": "sha512-SPSvF0G1t8m8CcB0L+ClNFszzQOvXaxmRj25oRWDf6aU+TuN2PXPFAJ9A6lt1IvX4oGAqqbTdMPTYs/SSHUYYQ==", + "version": "3.974.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.6.tgz", + "integrity": "sha512-YckB8k1ejbyCg/g36gUMFLNzE4W5cERIa4MtsdO+wpTmJEP0+TB7okWIt7d8TDOvnb7SwvxJ21E4TGOBxFpSWQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.973.25", + "@aws-sdk/core": "^3.973.26", "@aws-sdk/crc64-nvme": "^3.972.5", "@aws-sdk/types": "^3.973.6", "@smithy/is-array-buffer": "^4.2.2", @@ -931,7 +931,7 @@ "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.20", + "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -999,23 +999,23 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.26.tgz", - "integrity": "sha512-5q7UGSTtt7/KF0Os8wj2VZtlLxeWJVb0e2eDrDJlWot2EIxUNKDDMPFq/FowUqrwZ40rO2bu6BypxaKNvQhI+g==", + "version": "3.972.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.27.tgz", + "integrity": "sha512-gomO6DZwx+1D/9mbCpcqO5tPBqYBK7DtdgjTIjZ4yvfh/S7ETwAPS0XbJgP2JD8Ycr5CwVrEkV1sFtu3ShXeOw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.25", + "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/core": "^3.23.12", + "@smithy/core": "^3.23.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.20", + "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1024,13 +1024,13 @@ } }, "node_modules/@aws-sdk/middleware-sdk-sqs": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.17.tgz", - "integrity": "sha512-LnzPRRoDXGtlFV2G1p2rsY6fRKrbf6Pvvc21KliSLw3+NmQca2+Aa1QIMRbpQvZYedsSqkGYwxe+qvXwQ2uxDw==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.18.tgz", + "integrity": "sha512-BdsGFuBJUX5PnuZkEV6JRB5g/6ts7iGmN3pXwyoiGCCM2HHXrlFqjkBs+iPX7yO884WqYeQJpme7nwn4DzU5xw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", @@ -1055,15 +1055,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.26.tgz", - "integrity": "sha512-AilFIh4rI/2hKyyGN6XrB0yN96W2o7e7wyrPWCM6QjZM1mcC/pVkW3IWWRvuBWMpVP8Fg+rMpbzeLQ6dTM4gig==", + "version": "3.972.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.27.tgz", + "integrity": "sha512-TIRLO5UR2+FVUGmhYoAwVkKhcVzywEDX/5LzR9tjy1h8FQAXOtFg2IqgmwvxU7y933rkTn9rl6AdgcAUgQ1/Kg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.25", + "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", - "@smithy/core": "^3.23.12", + "@smithy/core": "^3.23.13", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.12", @@ -1074,44 +1074,44 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.16.tgz", - "integrity": "sha512-L7Qzoj/qQU1cL5GnYLQP5LbI+wlLCLoINvcykR3htKcQ4tzrPf2DOs72x933BM7oArYj1SKrkb2lGlsJHIic3g==", + "version": "3.996.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.17.tgz", + "integrity": "sha512-7B0HIX0tEFmOSJuWzdHZj1WhMXSryM+h66h96ZkqSncoY7J6wq61KOu4Kr57b/YnJP3J/EeQYVFulgR281h+7A==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.25", + "@aws-sdk/core": "^3.973.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", - "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/middleware-user-agent": "^3.972.27", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.12", + "@aws-sdk/util-user-agent-node": "^3.973.13", "@smithy/config-resolver": "^4.4.13", - "@smithy/core": "^3.23.12", + "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.27", - "@smithy/middleware-retry": "^4.4.44", - "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-endpoint": "^4.4.28", + "@smithy/middleware-retry": "^4.4.45", + "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.5.0", + "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.43", - "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-defaults-mode-browser": "^4.3.44", + "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -1139,17 +1139,17 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.1019.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1019.0.tgz", - "integrity": "sha512-KFv5UaIORIF6MTmEc79MQTPQSnRZjUmOIaOzXn9g6ujtViQLIrNYJiaSmVw8LqK1ebcndS6L6s4bBdLd9AQVJA==", + "version": "3.1020.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1020.0.tgz", + "integrity": "sha512-13slasDcOC+Dfi252bcB6MCDavfLP11DsAAxROKr3fyvMTWOh/gFZJyE1a5sBhKAQElzMyqlOLvxPp8cyqvEQQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/signature-v4-multi-region": "^3.996.14", + "@aws-sdk/signature-v4-multi-region": "^3.996.15", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-format-url": "^3.972.8", - "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-endpoint": "^4.4.28", "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, @@ -1158,12 +1158,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.14.tgz", - "integrity": "sha512-4nZSrBr1NO+48HCM/6BRU8mnRjuHZjcpziCvLXZk5QVftwWz5Mxqbhwdz4xf7WW88buaTB8uRO2MHklSX1m0vg==", + "version": "3.996.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.15.tgz", + "integrity": "sha512-Ukw2RpqvaL96CjfH/FgfBmy/ZosHBqoHBCFsN61qGg99F33vpntIVii8aNeh65XuOja73arSduskoa4OJea9RQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.26", + "@aws-sdk/middleware-sdk-s3": "^3.972.27", "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", @@ -1175,13 +1175,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1019.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1019.0.tgz", - "integrity": "sha512-OF+2RfRmUKyjzrRWlDcyju3RBsuqcrYDQ8TwrJg8efcOotMzuZN4U9mpVTIdATpmEc4lWNZBMSjPzrGm6JPnAQ==", + "version": "3.1020.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1020.0.tgz", + "integrity": "sha512-T61KA/VKl0zVUubdxigr1ut7SEpwE1/4CIKb14JDLyTAOne2yWKtQE1dDCSHl0UqrZNwW/bTt+EBHfQbslZJdw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.25", - "@aws-sdk/nested-clients": "^3.996.16", + "@aws-sdk/core": "^3.973.26", + "@aws-sdk/nested-clients": "^3.996.17", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", @@ -1273,12 +1273,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.12.tgz", - "integrity": "sha512-8phW0TS8ntENJgDcFewYT/Q8dOmarpvSxEjATu2GUBAutiHr++oEGCiBUwxslCMNvwW2cAPZNT53S/ym8zm/gg==", + "version": "3.973.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.13.tgz", + "integrity": "sha512-s1dCJ0J9WU9UPkT3FFqhKTSquYTkqWXGRaapHFyWwwJH86ZussewhNST5R5TwXVL1VSHq4aJVl9fWK+svaRVCQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/middleware-user-agent": "^3.972.27", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", @@ -2936,19 +2936,6 @@ "win32" ] }, - "node_modules/@smithy/abort-controller": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", - "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/chunked-blob-reader": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", @@ -2992,9 +2979,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.12", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", - "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", + "version": "3.23.13", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.13.tgz", + "integrity": "sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.12", @@ -3003,7 +2990,7 @@ "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.20", + "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" @@ -3212,13 +3199,13 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.27", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.27.tgz", - "integrity": "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==", + "version": "4.4.28", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.28.tgz", + "integrity": "sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.12", - "@smithy/middleware-serde": "^4.2.15", + "@smithy/core": "^3.23.13", + "@smithy/middleware-serde": "^4.2.16", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", @@ -3231,15 +3218,15 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.44", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.44.tgz", - "integrity": "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA==", + "version": "4.4.45", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.45.tgz", + "integrity": "sha512-td1PxpwDIaw5/oP/xIRxBGxJKoF1L4DBAwbZ8wjMuXBYOP/r2ZE/Ocou+mBHx/yk9knFEtDBwhSrYVn+Mz4pHw==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", @@ -3251,12 +3238,12 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.15", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", - "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", + "version": "4.2.16", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.16.tgz", + "integrity": "sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.12", + "@smithy/core": "^3.23.13", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" @@ -3294,12 +3281,11 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", - "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.1.tgz", + "integrity": "sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", @@ -3407,17 +3393,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.7", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.7.tgz", - "integrity": "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==", + "version": "4.12.8", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.8.tgz", + "integrity": "sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.12", - "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/core": "^3.23.13", + "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.20", + "@smithy/util-stream": "^4.5.21", "tslib": "^2.6.2" }, "engines": { @@ -3514,13 +3500,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.43", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.43.tgz", - "integrity": "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ==", + "version": "4.3.44", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.44.tgz", + "integrity": "sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, @@ -3529,16 +3515,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.47", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.47.tgz", - "integrity": "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ==", + "version": "4.2.48", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.48.tgz", + "integrity": "sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.7", + "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, @@ -3600,13 +3586,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.20", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", - "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", + "version": "4.5.21", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.21.tgz", + "integrity": "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==", "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.0", + "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", @@ -3644,12 +3630,11 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.13.tgz", - "integrity": "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.14.tgz", + "integrity": "sha512-2zqq5o/oizvMaFUlNiTyZ7dbgYv1a893aGut2uaxtbzTx/VYYnRxWzDHuD/ftgcw94ffenua+ZNLrbqwUYE+Bg==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, diff --git a/package.json b/package.json index 116300433..35c2d8f33 100644 --- a/package.json +++ b/package.json @@ -18,16 +18,16 @@ "job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js" }, "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.1019.0", - "@aws-sdk/client-elasticache": "^3.1019.0", - "@aws-sdk/client-s3": "^3.1019.0", - "@aws-sdk/client-secrets-manager": "^3.1019.0", - "@aws-sdk/client-ses": "^3.1019.0", - "@aws-sdk/client-sqs": "^3.1019.0", - "@aws-sdk/client-textract": "^3.1019.0", - "@aws-sdk/credential-provider-node": "^3.972.27", - "@aws-sdk/lib-storage": "^3.1019.0", - "@aws-sdk/s3-request-presigner": "^3.1019.0", + "@aws-sdk/client-cloudwatch-logs": "^3.1020.0", + "@aws-sdk/client-elasticache": "^3.1020.0", + "@aws-sdk/client-s3": "^3.1020.0", + "@aws-sdk/client-secrets-manager": "^3.1020.0", + "@aws-sdk/client-ses": "^3.1020.0", + "@aws-sdk/client-sqs": "^3.1020.0", + "@aws-sdk/client-textract": "^3.1020.0", + "@aws-sdk/credential-provider-node": "^3.972.28", + "@aws-sdk/lib-storage": "^3.1020.0", + "@aws-sdk/s3-request-presigner": "^3.1020.0", "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", From 7688f221618ccc6ca9e307ed9b5e510f42703282 Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 1 Apr 2026 14:39:34 -0400 Subject: [PATCH 49/56] release/2026-04-03 - Clean up localstack endpoints / env check --- server/chatter/chatter-client.js | 11 +++--- server/data/carfax.js | 5 ++- server/data/chatter.js | 9 +++-- server/email/mailer.js | 9 ++--- server/utils/instanceMgr.js | 62 +++++++++++++++++++++++++++----- server/utils/logger.js | 7 ++-- server/utils/s3.js | 14 +++----- 7 files changed, 73 insertions(+), 44 deletions(-) diff --git a/server/chatter/chatter-client.js b/server/chatter/chatter-client.js index 047e94459..b9b024737 100644 --- a/server/chatter/chatter-client.js +++ b/server/chatter/chatter-client.js @@ -1,20 +1,17 @@ const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager"); const { defaultProvider } = require("@aws-sdk/credential-provider-node"); -const { isString, isEmpty } = require("lodash"); +const { InstanceRegion, InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("../utils/instanceMgr"); const CHATTER_BASE_URL = process.env.CHATTER_API_BASE_URL || "https://api.chatterresearch.com"; -const AWS_REGION = process.env.AWS_REGION || "ca-central-1"; // Configure SecretsManager client with localstack support const secretsClientOptions = { - region: AWS_REGION, + region: InstanceRegion(), credentials: defaultProvider() }; -const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME); - -if (isLocal) { - secretsClientOptions.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`; +if (InstanceIsLocalStackEnabled()) { + secretsClientOptions.endpoint = InstanceLocalStackEndpoint(); } const secretsClient = new SecretsManagerClient(secretsClientOptions); diff --git a/server/data/carfax.js b/server/data/carfax.js index 250d553bb..ec5c7b4a2 100644 --- a/server/data/carfax.js +++ b/server/data/carfax.js @@ -3,7 +3,7 @@ const Dinero = require("dinero.js"); const moment = require("moment-timezone"); const logger = require("../utils/logger"); const InstanceManager = require("../utils/instanceMgr").default; -const { isString, isEmpty } = require("lodash"); +const { InstanceIsLocalStackEnabled } = require("../utils/instanceMgr"); const fs = require("fs"); const client = require("../graphql-client/graphql-client").client; const { sendServerEmail, sendMexicoBillingEmail } = require("../email/sendemail"); @@ -35,10 +35,9 @@ const S3_BUCKET_NAME = InstanceManager({ rome: "rome-carfax-uploads" }); const region = InstanceManager.InstanceRegion; -const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME); const uploadToS3 = (jsonObj, bucketName = S3_BUCKET_NAME) => { - const webPath = isLocal + const webPath = InstanceIsLocalStackEnabled() ? `https://${bucketName}.s3.localhost.localstack.cloud:4566/${jsonObj.filename}` : `https://${bucketName}.s3.${region}.amazonaws.com/${jsonObj.filename}`; diff --git a/server/data/chatter.js b/server/data/chatter.js index 86182fbf9..5da405b35 100644 --- a/server/data/chatter.js +++ b/server/data/chatter.js @@ -5,7 +5,8 @@ const logger = require("../utils/logger"); const fs = require("fs"); const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager"); const { defaultProvider } = require("@aws-sdk/credential-provider-node"); -const { isString, isEmpty } = require("lodash"); +const { InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("../utils/instanceMgr"); + let Client = require("ssh2-sftp-client"); const client = require("../graphql-client/graphql-client").client; @@ -151,10 +152,8 @@ async function getPrivateKey() { credentials: defaultProvider() }; - const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME); - - if (isLocal) { - secretsClientOptions.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`; + if (InstanceIsLocalStackEnabled()) { + secretsClientOptions.endpoint = InstanceLocalStackEndpoint(); } const client = new SecretsManagerClient(secretsClientOptions); diff --git a/server/email/mailer.js b/server/email/mailer.js index 6134053b6..41300caa9 100644 --- a/server/email/mailer.js +++ b/server/email/mailer.js @@ -1,20 +1,17 @@ -const { isString, isEmpty } = require("lodash"); const { defaultProvider } = require("@aws-sdk/credential-provider-node"); -const { InstanceRegion } = require("../utils/instanceMgr"); +const { InstanceRegion, InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("../utils/instanceMgr"); const aws = require("@aws-sdk/client-ses"); const nodemailer = require("nodemailer"); const logger = require("../utils/logger"); -const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME); - const sesConfig = { apiVersion: "latest", credentials: defaultProvider(), region: InstanceRegion() }; -if (isLocal) { - sesConfig.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`; +if (InstanceIsLocalStackEnabled()) { + sesConfig.endpoint = InstanceLocalStackEndpoint(); logger.logger.debug(`SES Mailer set to LocalStack end point: ${sesConfig.endpoint}`); } diff --git a/server/utils/instanceMgr.js b/server/utils/instanceMgr.js index 07759ac32..29e90eda1 100644 --- a/server/utils/instanceMgr.js +++ b/server/utils/instanceMgr.js @@ -7,14 +7,24 @@ * @property { string | object | function } promanager Return this prop if Rome. * @property { string | object | function } imex Return this prop if Rome. */ +const { isString, isEmpty } = require("lodash"); -function InstanceManager({ args, instance, debug, executeFunction, rome, promanager, imex }) { +/** + * InstanceManager is a utility function that determines which property to return based on the current instance type. + * @param param0 + * @param param0.args + * @param param0.instance + * @param param0.debug + * @param param0.executeFunction + * @param param0.rome + * @param param0.promanager + * @param param0.imex + * @returns {*|null} + * @constructor + */ +const InstanceManager = ({ args, instance, debug, executeFunction, rome, promanager, imex }) => { let propToReturn = null; - //TODO: Remove after debugging. - if (promanager) { - console.trace("ProManager Prop was used"); - } switch (instance || process.env.INSTANCE) { case "IMEX": propToReturn = imex; @@ -50,15 +60,42 @@ function InstanceManager({ args, instance, debug, executeFunction, rome, promana } if (executeFunction && typeof propToReturn === "function") return propToReturn(...args); return propToReturn === undefined ? null : propToReturn; -} +}; -exports.InstanceRegion = () => +/** + * Returns the AWS region to be used for the current instance, which is determined by the INSTANCE environment variable. + * @returns {*} + * @constructor + */ +const InstanceRegion = () => InstanceManager({ imex: "ca-central-1", rome: "us-east-2" }); -exports.InstanceEndpoints = () => +/** + * Checks if the instance is configured to use LocalStack by verifying the presence of the LOCALSTACK_HOSTNAME + * environment variable. + * @returns {boolean} + * @constructor + */ +const InstanceIsLocalStackEnabled = () => + isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME); + +/** + * Returns the LocalStack endpoint URL based on the LOCALSTACK_HOSTNAME environment variable. + * @returns {`http://${*}:4566`} + * @constructor + */ +const InstanceLocalStackEndpoint = () => `http://${process.env.LOCALSTACK_HOSTNAME}:4566`; + +/** + * Returns the appropriate endpoints for the current instance, which can be used for making API calls or other network + * requests. + * @returns {*|null} + * @constructor + */ +const InstanceEndpoints = () => InstanceManager({ imex: process.env?.NODE_ENV === "development" @@ -74,4 +111,11 @@ exports.InstanceEndpoints = () => : "https://romeonline.io" }); -exports.default = InstanceManager; +module.exports = { + InstanceManager, + InstanceRegion, + InstanceIsLocalStackEnabled, + InstanceLocalStackEndpoint, + InstanceEndpoints, + default: InstanceManager +}; diff --git a/server/utils/logger.js b/server/utils/logger.js index 9b4bea937..cf4391890 100644 --- a/server/utils/logger.js +++ b/server/utils/logger.js @@ -2,10 +2,9 @@ const InstanceManager = require("../utils/instanceMgr").default; const winston = require("winston"); const WinstonCloudWatch = require("winston-cloudwatch"); -const { isString, isEmpty } = require("lodash"); const { uploadFileToS3 } = require("./s3"); const { v4 } = require("uuid"); -const { InstanceRegion } = require("./instanceMgr"); +const { InstanceRegion, InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("./instanceMgr"); const getHostNameOrIP = require("./getHostNameOrIP"); const client = require("../graphql-client/graphql-client").client; const queries = require("../graphql-client/queries"); @@ -48,7 +47,7 @@ const normalizeLevel = (level) => (level ? level.toLowerCase() : LOG_LEVELS.debu const createLogger = () => { try { - const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME); + const isLocal = InstanceIsLocalStackEnabled(); const logGroupName = isLocal ? "development" : process.env.CLOUDWATCH_LOG_GROUP; const winstonCloudwatchTransportDefaults = { @@ -60,7 +59,7 @@ const createLogger = () => { }; if (isLocal) { - winstonCloudwatchTransportDefaults.awsOptions.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`; + winstonCloudwatchTransportDefaults.awsOptions.endpoint = InstanceLocalStackEndpoint(); } const levelFilter = (levels) => { diff --git a/server/utils/s3.js b/server/utils/s3.js index 2ba1f0d47..bc6f82373 100644 --- a/server/utils/s3.js +++ b/server/utils/s3.js @@ -7,8 +7,7 @@ const { CopyObjectCommand } = require("@aws-sdk/client-s3"); const { defaultProvider } = require("@aws-sdk/credential-provider-node"); -const { InstanceRegion } = require("./instanceMgr"); -const { isString, isEmpty } = require("lodash"); +const { InstanceRegion, InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("./instanceMgr"); const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); const createS3Client = () => { @@ -17,10 +16,8 @@ const createS3Client = () => { credentials: defaultProvider() }; - const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME); - - if (isLocal) { - S3Options.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`; + if (InstanceIsLocalStackEnabled()) { + S3Options.endpoint = InstanceLocalStackEndpoint(); S3Options.forcePathStyle = true; // Needed for LocalStack to avoid bucket name as hostname } @@ -105,7 +102,7 @@ const createS3Client = () => { }); const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 360 }); return presignedUrl; - } + }; return { uploadFileToS3, @@ -119,7 +116,4 @@ const createS3Client = () => { }; }; - - - module.exports = createS3Client(); From fe848b5de451d29920e79552ba670299a9170995 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 1 Apr 2026 22:51:05 -0700 Subject: [PATCH 50/56] IO-1366 Extend Audit Log Extend for Time Ticket Changes, Manual Lines, Manual Job, Bill Edits Signed-off-by: Allan Carr --- .../bill-detail-edit-component.jsx | 89 ++++++++++++++++++- .../job-lines-upsert-modal.container.jsx | 40 ++++++++- .../time-ticket-modal.container.jsx | 51 ++++++++++- .../jobs-create/jobs-create.container.jsx | 13 ++- client/src/translations/en_us/common.json | 8 +- client/src/translations/es/common.json | 6 +- client/src/translations/fr/common.json | 6 +- client/src/utils/AuditTrailMappings.js | 8 +- 8 files changed, 205 insertions(+), 16 deletions(-) diff --git a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx index d04f86cd2..0824dbf60 100644 --- a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx +++ b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx @@ -134,11 +134,96 @@ export function BillDetailEditcontainer({ insertAuditTrail, bodyshop }) { await Promise.all(updates); + const details = (() => { + const original = data?.bills_by_pk ?? {}; + const updated = { ...bill, billlines }; + + const fmtVal = (key, val) => + val == null || val === "" + ? "<>" + : typeof val === "number" && key.toLowerCase().includes("price") + ? `$${val.toFixed(2)}` + : val; + + const keysToTrack = ["line_desc", "quantity", "actual_price", "actual_cost", "cost_center"]; + const lineVals = (obj) => keysToTrack.map((k) => fmtVal(k, obj[k])).join(", "); + + const changed = Object.entries(updated) + .filter(([k, v]) => v != null && v !== "" && k !== "billlines" && k !== "__typename") + .map(([k, v]) => { + const orig = original[k]; + if (k === "date") { + const a = orig ? dayjs(orig) : null; + const b = v ? (dayjs.isDayjs(v) ? v : dayjs(v)) : null; + return (a && b && a.isSame(b, "day")) || (!a && !b) + ? null + : `date: ${a?.format("YYYY-MM-DD") ?? "<>"} → ${b?.format("YYYY-MM-DD") ?? "<>"}`; + } + return typeof orig === "object" || typeof v === "object" || String(orig) === String(v) + ? null + : `${k}: ${fmtVal(k, orig)} → ${fmtVal(k, v)}`; + }) + .filter(Boolean); + + const origLines = original.billlines ?? []; + const updLines = updated.billlines ?? []; + + const addedObjs = updLines + .filter((l) => !l.id) + .map((u) => ({ label: u.line_desc || u.description || "new line", vals: lineVals(u), handled: false })); + + const removedObjs = origLines + .filter((o) => !updLines.some((u) => u.id === o.id)) + .map((o) => ({ + label: o.line_desc || o.description || o.id || "removed line", + vals: lineVals(o), + handled: false + })); + + const labelToAdded = addedObjs.reduce((m, a) => m.set(a.label, [...(m.get(a.label) ?? []), a]), new Map()); + + const modified = [ + ...removedObjs.reduce((acc, r) => { + const candidates = labelToAdded.get(r.label) ?? []; + const exact = candidates.find((c) => c.vals === r.vals && !c.handled); + if (exact) { + exact.handled = r.handled = true; + return acc; + } // identical → cancel out + const diff = candidates.find((c) => c.vals !== r.vals && !c.handled); + if (diff) { + diff.handled = r.handled = true; + acc.push(`${r.label}: ${r.vals} → ${diff.vals}`); + } + return acc; + }, []), + ...updLines + .filter((u) => u.id) + .flatMap((u) => { + const o = origLines.find((x) => x.id === u.id); + if (!o) return []; + const diffs = keysToTrack + .filter((k) => String(o[k]) !== String(u[k])) + .map((k) => `${fmtVal(k, o[k])} → ${fmtVal(k, u[k])}`); + return diffs.length ? [`${u.line_desc || u.description || u.id}: ${diffs.join("; ")}`] : []; + }) + ]; + + [ + ["added", addedObjs.filter((a) => !a.handled).map((a) => `+${a.label} (${a.vals})`)], + ["removed", removedObjs.filter((r) => !r.handled).map((r) => `-${r.label} (${r.vals})`)], + ["modified", modified] + ].forEach(([type, items]) => { + if (items.length) changed.push(`billlines ${type}: ${items.join(" | ")}`); + }); + + return changed.length ? changed.join("; ") : bill.invoice_number || "No changes"; + })(); + insertAuditTrail({ jobid: bill.jobid, billid: search.billid, - operation: AuditTrailMapping.billupdated(bill.invoice_number), - type: "billupdated" + operation: AuditTrailMapping.billupdated(bill.invoice_number, details) }); await refetch(); diff --git a/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.container.jsx b/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.container.jsx index 14e1d0a85..ab5862f83 100644 --- a/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.container.jsx +++ b/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.container.jsx @@ -14,16 +14,19 @@ import CriticalPartsScan from "../../utils/criticalPartsScan"; import UndefinedToNull from "../../utils/undefinedtonull"; import JobLinesUpdsertModal from "./job-lines-upsert-modal.component"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import AuditTrailMapping from "../../utils/AuditTrailMappings.js"; +import { insertAuditTrail } from "../../redux/application/application.actions.js"; const mapStateToProps = createStructuredSelector({ jobLineEditModal: selectJobLineEditModal, bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit")) + toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit")), + insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })) }); -function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bodyshop }) { +function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bodyshop, insertAuditTrail }) { const { treatments: { CriticalPartsScanning } } = useTreatmentsWithConfig({ @@ -74,6 +77,16 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo notification.success({ title: t("joblines.successes.created") }); + insertAuditTrail({ + jobid: jobLineEditModal.context.jobid, + operation: AuditTrailMapping.jobmanuallineinsert( + Object.entries(values) + .filter(([k, v]) => v != null && v !== "" && k !== "ah_detail_line" && k !== "prt_dsmk_p") + .map(([k, v]) => `${k}: ${v}`) + .join("; ") + ), + type: "jobmanuallineinsert" + }); } else { notification.error({ title: t("joblines.errors.creating", { @@ -103,6 +116,29 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo notification.success({ title: t("joblines.successes.updated") }); + insertAuditTrail({ + jobid: jobLineEditModal.context.jobid, + operation: AuditTrailMapping.joblineupdate( + (() => { + const original = jobLineEditModal.context || {}; + const changed = Object.entries(values) + .filter(([k, v]) => v != null && v !== "" && k !== "ah_detail_line" && k !== "prt_dsmk_p") + .map(([k, v]) => { + const orig = original[k]; + if (String(orig) === String(v)) return null; + const fmt = (key, val) => { + if (val == null || val === "") return "<>"; + if (typeof val === "number" && key.toLowerCase().includes("price")) return `$${val.toFixed(2)}`; + return val; + }; + return `${k}: ${fmt(k, orig)} → ${fmt(k, v)}`; + }) + .filter(Boolean); + return changed.length ? changed.join("; ") : "No changes"; + })() + ), + type: "joblineupdate" + }); } else { notification.success({ title: t("joblines.errors.updating", { diff --git a/client/src/components/time-ticket-modal/time-ticket-modal.container.jsx b/client/src/components/time-ticket-modal/time-ticket-modal.container.jsx index ba3de7760..a88e6472b 100644 --- a/client/src/components/time-ticket-modal/time-ticket-modal.container.jsx +++ b/client/src/components/time-ticket-modal/time-ticket-modal.container.jsx @@ -2,7 +2,7 @@ import { PageHeader } from "@ant-design/pro-layout"; import { useMutation, useQuery } from "@apollo/client/react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { Button, Form, Modal, Space } from "antd"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -15,21 +15,26 @@ import { selectBodyshop } from "../../redux/user/user.selectors"; import dayjs from "../../utils/day"; import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component"; import TimeTicketModalComponent from "./time-ticket-modal.component"; +import { insertAuditTrail } from "../../redux/application/application.actions.js"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; const mapStateToProps = createStructuredSelector({ timeTicketModal: selectTimeTicket, bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - toggleModalVisible: () => dispatch(toggleModalVisible("timeTicket")) + toggleModalVisible: () => dispatch(toggleModalVisible("timeTicket")), + insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })) }); -export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible, bodyshop }) { +export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible, bodyshop, insertAuditTrail }) { const [form] = Form.useForm(); const [loading, setLoading] = useState(false); const { t } = useTranslation(); const [enterAgain, setEnterAgain] = useState(false); + const lastSubmittedRef = useRef(null); + const [lineTicketRefreshKey, setLineTicketRefreshKey] = useState(0); const [insertTicket] = useMutation(INSERT_NEW_TIME_TICKET); @@ -50,6 +55,8 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible, }); const handleFinish = (values) => { + // Save submitted values so we can compute audit-trail details after the mutation completes + lastSubmittedRef.current = values; setLoading(true); const emps = EmployeeAutoCompleteData?.employees.filter((e) => e.id === values.employeeid); if (timeTicketModal.context.id) { @@ -89,6 +96,44 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible, title: t("timetickets.successes.created") }); + const timeticket = timeTicketModal.context?.timeticket ?? {}; + const original = timeticket || {}; + const submitted = lastSubmittedRef.current || {}; + + const fmt = (key, val) => { + if (val == null || val === "") return "<>"; + const k = key.toLowerCase(); + if (dayjs.isDayjs?.(val)) return dayjs(val).format(k.includes("clock") ? "YYYY-MM-DD HH:mm" : "YYYY-MM-DD"); + if (typeof val === "number") + return k.includes("hrs") + ? val.toFixed(1) + : k.includes("rate") || k.includes("price") + ? `$${val.toFixed(2)}` + : String(val); + if (key === "employeeid") { + const emp = EmployeeAutoCompleteData?.employees?.find(({ id }) => id === val); + return emp ? `${emp.first_name} ${emp.last_name}` : String(val); + } + return String(val); + }; + + const changed = Object.entries(submitted) + .filter(([, v]) => v != null && v !== "") + .map(([k, v]) => { + const origVal = k === "jobid" ? (original.job?.id ?? original.jobid ?? original[k]) : original[k]; + return String(fmt(k, origVal)) !== String(fmt(k, v)) ? `${k}: ${fmt(k, origVal)} → ${fmt(k, v)}` : null; + }) + .filter(Boolean); + + insertAuditTrail({ + jobid: timeticket.job?.id ?? timeticket.jobid, + operation: AuditTrailMapping.timeticketupdated( + [original.employee.first_name, original.employee.last_name].filter(Boolean).join(" "), + original.date ? dayjs(original.date).format("YYYY-MM-DD") : "<>", + changed.length ? changed.join("; ") : "No changes" + ) + }); + // Refresh parent screens (Job Labor tab, etc.) if (timeTicketModal.actions.refetch) timeTicketModal.actions.refetch(); diff --git a/client/src/pages/jobs-create/jobs-create.container.jsx b/client/src/pages/jobs-create/jobs-create.container.jsx index 5b5627ef1..b3cc808b2 100644 --- a/client/src/pages/jobs-create/jobs-create.container.jsx +++ b/client/src/pages/jobs-create/jobs-create.container.jsx @@ -8,13 +8,14 @@ import { createStructuredSelector } from "reselect"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import { INSERT_NEW_JOB } from "../../graphql/jobs.queries"; import { QUERY_OWNER_FOR_JOB_CREATION } from "../../graphql/owners.queries"; -import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions"; +import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; import JobsCreateComponent from "./jobs-create.component"; import JobCreateContext from "./jobs-create.context"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { logImEXEvent } from "../../firebase/firebase.utils"; +import AuditTrailMapping from "../../utils/AuditTrailMappings.js"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -22,10 +23,11 @@ const mapStateToProps = createStructuredSelector({ }); const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), - setSelectedHeader: (key) => dispatch(setSelectedHeader(key)) + setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), + insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })) }); -function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, currentUser }) { +function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, currentUser, insertAuditTrail }) { const { t } = useTranslation(); const notification = useNotification(); @@ -84,6 +86,11 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, curr newJobId: resp.data.insert_jobs.returning[0].id }); logImEXEvent("manual_job_create_completed", {}); + insertAuditTrail({ + jobid: resp.data.insert_jobs.returning[0].id, + operation: AuditTrailMapping.jobmanualcreate(), + type: "jobmanualcreate" + }); setIsSubmitting(false); }) .catch((error) => { diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index f0d1ebbff..f22dbdcd1 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -122,7 +122,7 @@ "billdeleted": "Bill with invoice number {{invoice_number}} deleted.", "billmarkforreexport": "Bill with invoice number {{invoice_number}} marked for re-export.", "billposted": "Bill with invoice number {{invoice_number}} posted.", - "billupdated": "Bill with invoice number {{invoice_number}} updated.", + "billupdated": "Bill with invoice number {{invoice_number}} updated with the following details: {{values}}.", "failedpayment": "Failed payment attempt.", "jobassignmentchange": "Employee {{name}} assigned to {{operation}}", "jobassignmentremoved": "Employee assignment removed for {{operation}}", @@ -137,6 +137,9 @@ "jobintake": "Job intake completed. Status set to {{status}}. Scheduled completion is {{scheduled_completion}}.", "jobinvoiced": "Job has been invoiced.", "jobioucreated": "IOU Created.", + "joblineupdate": "Job line {{line_desc}} updated.", + "jobmanualcreate": "Job manually created.", + "jobmanuallineinsert": "Job line manually added with the following details: {{values}}.", "jobmodifylbradj": "Labor adjustments modified {{mod_lbr_ty}} / {{hours}}.", "jobnoteadded": "Note added to Job.", "jobnotedeleted": "Note deleted from Job.", @@ -152,7 +155,8 @@ "tasks_deleted": "Task '{{title}}' deleted by {{deletedBy}}", "tasks_uncompleted": "Task '{{title}}' uncompleted by {{uncompletedBy}}", "tasks_undeleted": "Task '{{title}}' undeleted by {{undeletedBy}}", - "tasks_updated": "Task '{{title}}' updated by {{updatedBy}}" + "tasks_updated": "Task '{{title}}' updated by {{updatedBy}}", + "timeticketupdated": "Time Ticket for {{employee}} on {{date}} updated with the following details: {{details}}" } }, "billlines": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 3f1d15ce6..3f53233fb 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -137,6 +137,9 @@ "jobintake": "", "jobinvoiced": "", "jobioucreated": "", + "joblineupdate": "", + "jobmanualcreate": "", + "jobmanuallineinsert": "", "jobmodifylbradj": "", "jobnoteadded": "", "jobnotedeleted": "", @@ -152,7 +155,8 @@ "tasks_deleted": "", "tasks_uncompleted": "", "tasks_undeleted": "", - "tasks_updated": "" + "tasks_updated": "", + "timeticketupdated": "" } }, "billlines": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index c1520a5e3..a9612b05b 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -137,6 +137,9 @@ "jobintake": "", "jobinvoiced": "", "jobioucreated": "", + "joblineupdate": "", + "jobmanualcreate": "", + "jobmanuallineinsert": "", "jobmodifylbradj": "", "jobnoteadded": "", "jobnotedeleted": "", @@ -152,7 +155,8 @@ "tasks_deleted": "", "tasks_uncompleted": "", "tasks_undeleted": "", - "tasks_updated": "" + "tasks_updated": "", + "timeticketupdated": "" } }, "billlines": { diff --git a/client/src/utils/AuditTrailMappings.js b/client/src/utils/AuditTrailMappings.js index e33082c20..5e4c160b7 100644 --- a/client/src/utils/AuditTrailMappings.js +++ b/client/src/utils/AuditTrailMappings.js @@ -10,7 +10,7 @@ const AuditTrailMapping = { billdeleted: (invoice_number) => i18n.t("audit_trail.messages.billdeleted", { invoice_number }), billmarkforreexport: (invoice_number) => i18n.t("audit_trail.messages.billmarkforreexport", { invoice_number }), billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }), - billupdated: (invoice_number) => i18n.t("audit_trail.messages.billupdated", { invoice_number }), + billupdated: (invoice_number, values) => i18n.t("audit_trail.messages.billupdated", { invoice_number, values }), jobassignmentchange: (operation, name) => i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }), jobassignmentremoved: (operation) => i18n.t("audit_trail.messages.jobassignmentremoved", { operation }), jobchecklist: (type, inproduction, status) => @@ -26,6 +26,9 @@ const AuditTrailMapping = { jobinproductionchange: (inproduction) => i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }), jobinvoiced: () => i18n.t("audit_trail.messages.jobinvoiced"), jobclosedwithbypass: () => i18n.t("audit_trail.messages.jobclosedwithbypass"), + joblineupdate: (line_desc) => i18n.t("audit_trail.messages.joblineupdate", { line_desc }), + jobmanualcreate: () => i18n.t("audit_trail.messages.jobmanualcreate"), + jobmanuallineinsert: (values) => i18n.t("audit_trail.messages.jobmanuallineinsert", { values }), jobmodifylbradj: ({ mod_lbr_ty, hours }) => i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }), jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"), jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"), @@ -72,7 +75,8 @@ const AuditTrailMapping = { i18n.t("audit_trail.messages.tasks_uncompleted", { title, uncompletedBy - }) + }), + timeticketupdated: (employee, date, details) => i18n.t("audit_trail.messages.timeticketupdated", { employee, date, details }) }; export default AuditTrailMapping; From 9a86a337bb1acfa414665c27b012bcce520edf08 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 2 Apr 2026 15:50:56 -0700 Subject: [PATCH 51/56] IO-3637 DMS ID Production Board Column Signed-off-by: Allan Carr --- .../production-list-columns.add.component.jsx | 1 + .../production-list-columns.data.jsx | 14 +++++++++++++- .../production-list-config-manager.component.jsx | 2 ++ client/src/graphql/jobs.queries.js | 3 +++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/client/src/components/production-list-columns/production-list-columns.add.component.jsx b/client/src/components/production-list-columns/production-list-columns.add.component.jsx index c75834363..a88b2a474 100644 --- a/client/src/components/production-list-columns/production-list-columns.add.component.jsx +++ b/client/src/components/production-list-columns/production-list-columns.add.component.jsx @@ -58,6 +58,7 @@ export function ProductionColumnsComponent({ const columnKeys = columns.map((i) => i.key); const cols = dataSource({ + bodyshop, technician, data, state: tableState, diff --git a/client/src/components/production-list-columns/production-list-columns.data.jsx b/client/src/components/production-list-columns/production-list-columns.data.jsx index f44bbb3cc..ff7b4c517 100644 --- a/client/src/components/production-list-columns/production-list-columns.data.jsx +++ b/client/src/components/production-list-columns/production-list-columns.data.jsx @@ -609,7 +609,19 @@ const productionListColumnsData = ({ technician, state, activeStatuses, data, bo ellipsis: true, render: (text, record) => {record.date_repairstarted} - } + }, + ...(bodyshop && bodyshop.rr_dealerid + ? [ + { + title: i18n.t("jobs.fields.dms.id"), + dataIndex: "dms_id", + key: "dms_id", + ellipsis: true, + sorter: (a, b) => alphaSort(a.dms_id, b.dms_id), + sortOrder: state.sortedInfo.columnKey === "dms_id" && state.sortedInfo.order + } + ] + : []), ]; }; export default productionListColumnsData; diff --git a/client/src/components/production-list-table/production-list-config-manager.component.jsx b/client/src/components/production-list-table/production-list-config-manager.component.jsx index 75e15b75d..a061551a3 100644 --- a/client/src/components/production-list-table/production-list-config-manager.component.jsx +++ b/client/src/components/production-list-table/production-list-config-manager.component.jsx @@ -244,6 +244,7 @@ export function ProductionListConfigManager({ nextConfig.columns.columnKeys.map((k) => { return { ...ProductionListColumns({ + bodyshop, technician, state: ensureDefaultState(state), refetch, @@ -270,6 +271,7 @@ export function ProductionListConfigManager({ activeConfig.columns.columnKeys.map((k) => { return { ...ProductionListColumns({ + bodyshop, technician, state: ensureDefaultState(state), refetch, diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 35a3a1f26..e12d23da3 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -197,6 +197,7 @@ export const QUERY_EXACT_JOB_IN_PRODUCTION = gql` employee_prep employee_csr date_repairstarted + dms_id joblines_status { part_type status @@ -269,6 +270,7 @@ export const QUERY_EXACT_JOBS_IN_PRODUCTION = gql` employee_prep employee_csr date_repairstarted + dms_id joblines_status { part_type status @@ -2671,6 +2673,7 @@ export const QUERY_JOBS_IN_PRODUCTION = gql` suspended job_totals date_repairstarted + dms_id joblines_status { part_type status From 704543d8230419b839c45b39120a8729abb5aa18 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 2 Apr 2026 21:40:59 -0400 Subject: [PATCH 52/56] IO-1366 Refine audit trail detail logging --- .../bill-detail-edit-component.jsx | 96 +-------- .../job-lines-upsert-modal.container.jsx | 30 +-- .../time-ticket-modal.container.jsx | 121 ++++++------ client/src/translations/en_us/common.json | 7 +- client/src/translations/es/common.json | 7 +- client/src/translations/fr/common.json | 7 +- client/src/utils/AuditTrailMappings.js | 12 +- client/src/utils/auditTrailDetails.js | 186 ++++++++++++++++++ 8 files changed, 272 insertions(+), 194 deletions(-) create mode 100644 client/src/utils/auditTrailDetails.js diff --git a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx index 0824dbf60..2704986b5 100644 --- a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx +++ b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx @@ -13,6 +13,7 @@ import { insertAuditTrail } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import dayjs from "../../utils/day"; +import { buildBillUpdateAuditDetails } from "../../utils/auditTrailDetails"; import AlertComponent from "../alert/alert.component"; import BillFormContainer from "../bill-form/bill-form.container"; import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component"; @@ -134,96 +135,17 @@ export function BillDetailEditcontainer({ insertAuditTrail, bodyshop }) { await Promise.all(updates); - const details = (() => { - const original = data?.bills_by_pk ?? {}; - const updated = { ...bill, billlines }; - - const fmtVal = (key, val) => - val == null || val === "" - ? "<>" - : typeof val === "number" && key.toLowerCase().includes("price") - ? `$${val.toFixed(2)}` - : val; - - const keysToTrack = ["line_desc", "quantity", "actual_price", "actual_cost", "cost_center"]; - const lineVals = (obj) => keysToTrack.map((k) => fmtVal(k, obj[k])).join(", "); - - const changed = Object.entries(updated) - .filter(([k, v]) => v != null && v !== "" && k !== "billlines" && k !== "__typename") - .map(([k, v]) => { - const orig = original[k]; - if (k === "date") { - const a = orig ? dayjs(orig) : null; - const b = v ? (dayjs.isDayjs(v) ? v : dayjs(v)) : null; - return (a && b && a.isSame(b, "day")) || (!a && !b) - ? null - : `date: ${a?.format("YYYY-MM-DD") ?? "<>"} → ${b?.format("YYYY-MM-DD") ?? "<>"}`; - } - return typeof orig === "object" || typeof v === "object" || String(orig) === String(v) - ? null - : `${k}: ${fmtVal(k, orig)} → ${fmtVal(k, v)}`; - }) - .filter(Boolean); - - const origLines = original.billlines ?? []; - const updLines = updated.billlines ?? []; - - const addedObjs = updLines - .filter((l) => !l.id) - .map((u) => ({ label: u.line_desc || u.description || "new line", vals: lineVals(u), handled: false })); - - const removedObjs = origLines - .filter((o) => !updLines.some((u) => u.id === o.id)) - .map((o) => ({ - label: o.line_desc || o.description || o.id || "removed line", - vals: lineVals(o), - handled: false - })); - - const labelToAdded = addedObjs.reduce((m, a) => m.set(a.label, [...(m.get(a.label) ?? []), a]), new Map()); - - const modified = [ - ...removedObjs.reduce((acc, r) => { - const candidates = labelToAdded.get(r.label) ?? []; - const exact = candidates.find((c) => c.vals === r.vals && !c.handled); - if (exact) { - exact.handled = r.handled = true; - return acc; - } // identical → cancel out - const diff = candidates.find((c) => c.vals !== r.vals && !c.handled); - if (diff) { - diff.handled = r.handled = true; - acc.push(`${r.label}: ${r.vals} → ${diff.vals}`); - } - return acc; - }, []), - ...updLines - .filter((u) => u.id) - .flatMap((u) => { - const o = origLines.find((x) => x.id === u.id); - if (!o) return []; - const diffs = keysToTrack - .filter((k) => String(o[k]) !== String(u[k])) - .map((k) => `${fmtVal(k, o[k])} → ${fmtVal(k, u[k])}`); - return diffs.length ? [`${u.line_desc || u.description || u.id}: ${diffs.join("; ")}`] : []; - }) - ]; - - [ - ["added", addedObjs.filter((a) => !a.handled).map((a) => `+${a.label} (${a.vals})`)], - ["removed", removedObjs.filter((r) => !r.handled).map((r) => `-${r.label} (${r.vals})`)], - ["modified", modified] - ].forEach(([type, items]) => { - if (items.length) changed.push(`billlines ${type}: ${items.join(" | ")}`); - }); - - return changed.length ? changed.join("; ") : bill.invoice_number || "No changes"; - })(); + const details = buildBillUpdateAuditDetails({ + originalBill: data?.bills_by_pk, + bill, + billlines + }); insertAuditTrail({ - jobid: bill.jobid, + jobid: bill.jobid ?? data?.bills_by_pk?.jobid, billid: search.billid, - operation: AuditTrailMapping.billupdated(bill.invoice_number, details) + operation: AuditTrailMapping.billupdated(bill.invoice_number, details), + type: "billupdated" }); await refetch(); diff --git a/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.container.jsx b/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.container.jsx index ab5862f83..5140e43dd 100644 --- a/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.container.jsx +++ b/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.container.jsx @@ -16,6 +16,7 @@ import JobLinesUpdsertModal from "./job-lines-upsert-modal.component"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import AuditTrailMapping from "../../utils/AuditTrailMappings.js"; import { insertAuditTrail } from "../../redux/application/application.actions.js"; +import { buildJobLineInsertAuditDetails, buildJobLineUpdateAuditDetails } from "../../utils/auditTrailDetails.js"; const mapStateToProps = createStructuredSelector({ jobLineEditModal: selectJobLineEditModal, @@ -79,12 +80,7 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo }); insertAuditTrail({ jobid: jobLineEditModal.context.jobid, - operation: AuditTrailMapping.jobmanuallineinsert( - Object.entries(values) - .filter(([k, v]) => v != null && v !== "" && k !== "ah_detail_line" && k !== "prt_dsmk_p") - .map(([k, v]) => `${k}: ${v}`) - .join("; ") - ), + operation: AuditTrailMapping.jobmanuallineinsert(buildJobLineInsertAuditDetails(values)), type: "jobmanuallineinsert" }); } else { @@ -119,23 +115,11 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo insertAuditTrail({ jobid: jobLineEditModal.context.jobid, operation: AuditTrailMapping.joblineupdate( - (() => { - const original = jobLineEditModal.context || {}; - const changed = Object.entries(values) - .filter(([k, v]) => v != null && v !== "" && k !== "ah_detail_line" && k !== "prt_dsmk_p") - .map(([k, v]) => { - const orig = original[k]; - if (String(orig) === String(v)) return null; - const fmt = (key, val) => { - if (val == null || val === "") return "<>"; - if (typeof val === "number" && key.toLowerCase().includes("price")) return `$${val.toFixed(2)}`; - return val; - }; - return `${k}: ${fmt(k, orig)} → ${fmt(k, v)}`; - }) - .filter(Boolean); - return changed.length ? changed.join("; ") : "No changes"; - })() + values.line_desc || jobLineEditModal.context.line_desc || "manual line", + buildJobLineUpdateAuditDetails({ + originalLine: jobLineEditModal.context, + values + }) ), type: "joblineupdate" }); diff --git a/client/src/components/time-ticket-modal/time-ticket-modal.container.jsx b/client/src/components/time-ticket-modal/time-ticket-modal.container.jsx index a88e6472b..4f664a11c 100644 --- a/client/src/components/time-ticket-modal/time-ticket-modal.container.jsx +++ b/client/src/components/time-ticket-modal/time-ticket-modal.container.jsx @@ -17,6 +17,7 @@ import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time import TimeTicketModalComponent from "./time-ticket-modal.component"; import { insertAuditTrail } from "../../redux/application/application.actions.js"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; +import { buildTimeTicketAuditSummary } from "../../utils/auditTrailDetails.js"; const mapStateToProps = createStructuredSelector({ timeTicketModal: selectTimeTicket, @@ -53,87 +54,77 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible, fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); + const employees = EmployeeAutoCompleteData?.employees ?? []; const handleFinish = (values) => { - // Save submitted values so we can compute audit-trail details after the mutation completes lastSubmittedRef.current = values; setLoading(true); - const emps = EmployeeAutoCompleteData?.employees.filter((e) => e.id === values.employeeid); - if (timeTicketModal.context.id) { - updateTicket({ - variables: { - timeticketId: timeTicketModal.context.id, - timeticket: { - ...values, - rate: emps.length === 1 ? emps[0].rates.filter((r) => r.cost_center === values.cost_center)[0]?.rate : null - } - } - }) - .then(handleMutationSuccess) - .catch(handleMutationError); - } else { - //Get selected employee rate. - insertTicket({ - variables: { - timeTicketInput: [ - { + const isEdit = Boolean(timeTicketModal.context.id); + const emps = employees.filter((employee) => employee.id === values.employeeid); + const mutation = isEdit + ? updateTicket({ + variables: { + timeticketId: timeTicketModal.context.id, + timeticket: { ...values, rate: - emps.length === 1 ? emps[0].rates.filter((r) => r.cost_center === values.cost_center)[0].rate : null, - bodyshopid: bodyshop.id, - created_by: timeTicketModal.context.created_by + emps.length === 1 ? emps[0].rates.filter((r) => r.cost_center === values.cost_center)[0]?.rate : null } - ] - } - }) - .then(handleMutationSuccess) - .catch(handleMutationError); - } + } + }) + : insertTicket({ + variables: { + timeTicketInput: [ + { + ...values, + rate: + emps.length === 1 ? emps[0].rates.filter((r) => r.cost_center === values.cost_center)[0].rate : null, + bodyshopid: bodyshop.id, + created_by: timeTicketModal.context.created_by + } + ] + } + }); + + mutation.then((result) => handleMutationSuccess(result, isEdit)).catch(handleMutationError); }; - const handleMutationSuccess = () => { + const handleMutationSuccess = (result, isEdit) => { notification.success({ title: t("timetickets.successes.created") }); - const timeticket = timeTicketModal.context?.timeticket ?? {}; - const original = timeticket || {}; - const submitted = lastSubmittedRef.current || {}; - - const fmt = (key, val) => { - if (val == null || val === "") return "<>"; - const k = key.toLowerCase(); - if (dayjs.isDayjs?.(val)) return dayjs(val).format(k.includes("clock") ? "YYYY-MM-DD HH:mm" : "YYYY-MM-DD"); - if (typeof val === "number") - return k.includes("hrs") - ? val.toFixed(1) - : k.includes("rate") || k.includes("price") - ? `$${val.toFixed(2)}` - : String(val); - if (key === "employeeid") { - const emp = EmployeeAutoCompleteData?.employees?.find(({ id }) => id === val); - return emp ? `${emp.first_name} ${emp.last_name}` : String(val); - } - return String(val); + const savedTicket = + result?.data?.update_timetickets?.returning?.[0] ?? result?.data?.insert_timetickets?.returning?.[0] ?? {}; + const originalTicket = timeTicketModal.context?.timeticket ?? {}; + const submittedValues = { + ...(lastSubmittedRef.current ?? {}), + date: lastSubmittedRef.current?.date ?? savedTicket.date ?? originalTicket.date ?? null, + employeeid: lastSubmittedRef.current?.employeeid ?? savedTicket.employeeid ?? originalTicket.employeeid ?? null, + jobid: + lastSubmittedRef.current?.jobid ?? + savedTicket.jobid ?? + timeTicketModal.context.jobId ?? + originalTicket.job?.id ?? + originalTicket.jobid ?? + null }; - - const changed = Object.entries(submitted) - .filter(([, v]) => v != null && v !== "") - .map(([k, v]) => { - const origVal = k === "jobid" ? (original.job?.id ?? original.jobid ?? original[k]) : original[k]; - return String(fmt(k, origVal)) !== String(fmt(k, v)) ? `${k}: ${fmt(k, origVal)} → ${fmt(k, v)}` : null; - }) - .filter(Boolean); - - insertAuditTrail({ - jobid: timeticket.job?.id ?? timeticket.jobid, - operation: AuditTrailMapping.timeticketupdated( - [original.employee.first_name, original.employee.last_name].filter(Boolean).join(" "), - original.date ? dayjs(original.date).format("YYYY-MM-DD") : "<>", - changed.length ? changed.join("; ") : "No changes" - ) + const auditSummary = buildTimeTicketAuditSummary({ + originalTicket, + submittedValues, + employees }); + if (auditSummary.jobid) { + insertAuditTrail({ + jobid: auditSummary.jobid, + operation: isEdit + ? AuditTrailMapping.timeticketupdated(auditSummary.employeeName, auditSummary.date, auditSummary.details) + : AuditTrailMapping.timeticketcreated(auditSummary.employeeName, auditSummary.date, auditSummary.details), + type: isEdit ? "timeticketupdated" : "timeticketcreated" + }); + } + // Refresh parent screens (Job Labor tab, etc.) if (timeTicketModal.actions.refetch) timeTicketModal.actions.refetch(); diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index f22dbdcd1..9e48806ec 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -122,7 +122,7 @@ "billdeleted": "Bill with invoice number {{invoice_number}} deleted.", "billmarkforreexport": "Bill with invoice number {{invoice_number}} marked for re-export.", "billposted": "Bill with invoice number {{invoice_number}} posted.", - "billupdated": "Bill with invoice number {{invoice_number}} updated with the following details: {{values}}.", + "billupdated": "Bill with invoice number {{invoice_number}} updated with the following details: {{details}}.", "failedpayment": "Failed payment attempt.", "jobassignmentchange": "Employee {{name}} assigned to {{operation}}", "jobassignmentremoved": "Employee assignment removed for {{operation}}", @@ -137,9 +137,9 @@ "jobintake": "Job intake completed. Status set to {{status}}. Scheduled completion is {{scheduled_completion}}.", "jobinvoiced": "Job has been invoiced.", "jobioucreated": "IOU Created.", - "joblineupdate": "Job line {{line_desc}} updated.", + "joblineupdate": "Job line {{lineDescription}} updated with the following details: {{details}}.", "jobmanualcreate": "Job manually created.", - "jobmanuallineinsert": "Job line manually added with the following details: {{values}}.", + "jobmanuallineinsert": "Job line manually added with the following details: {{details}}.", "jobmodifylbradj": "Labor adjustments modified {{mod_lbr_ty}} / {{hours}}.", "jobnoteadded": "Note added to Job.", "jobnotedeleted": "Note deleted from Job.", @@ -156,6 +156,7 @@ "tasks_uncompleted": "Task '{{title}}' uncompleted by {{uncompletedBy}}", "tasks_undeleted": "Task '{{title}}' undeleted by {{undeletedBy}}", "tasks_updated": "Task '{{title}}' updated by {{updatedBy}}", + "timeticketcreated": "Time Ticket for {{employee}} on {{date}} created with the following details: {{details}}.", "timeticketupdated": "Time Ticket for {{employee}} on {{date}} updated with the following details: {{details}}" } }, diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 3f53233fb..cb5613c61 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -122,7 +122,6 @@ "billdeleted": "", "billposted": "", "billmarkforreexport": "", - "billupdated": "", "failedpayment": "", "jobassignmentchange": "", "jobassignmentremoved": "", @@ -137,9 +136,6 @@ "jobintake": "", "jobinvoiced": "", "jobioucreated": "", - "joblineupdate": "", - "jobmanualcreate": "", - "jobmanuallineinsert": "", "jobmodifylbradj": "", "jobnoteadded": "", "jobnotedeleted": "", @@ -155,8 +151,7 @@ "tasks_deleted": "", "tasks_uncompleted": "", "tasks_undeleted": "", - "tasks_updated": "", - "timeticketupdated": "" + "tasks_updated": "" } }, "billlines": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index a9612b05b..745e8cd65 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -122,7 +122,6 @@ "billdeleted": "", "billmarkforreexport": "", "billposted": "", - "billupdated": "", "failedpayment": "", "jobassignmentchange": "", "jobassignmentremoved": "", @@ -137,9 +136,6 @@ "jobintake": "", "jobinvoiced": "", "jobioucreated": "", - "joblineupdate": "", - "jobmanualcreate": "", - "jobmanuallineinsert": "", "jobmodifylbradj": "", "jobnoteadded": "", "jobnotedeleted": "", @@ -155,8 +151,7 @@ "tasks_deleted": "", "tasks_uncompleted": "", "tasks_undeleted": "", - "tasks_updated": "", - "timeticketupdated": "" + "tasks_updated": "" } }, "billlines": { diff --git a/client/src/utils/AuditTrailMappings.js b/client/src/utils/AuditTrailMappings.js index 5e4c160b7..23a2600ed 100644 --- a/client/src/utils/AuditTrailMappings.js +++ b/client/src/utils/AuditTrailMappings.js @@ -10,7 +10,7 @@ const AuditTrailMapping = { billdeleted: (invoice_number) => i18n.t("audit_trail.messages.billdeleted", { invoice_number }), billmarkforreexport: (invoice_number) => i18n.t("audit_trail.messages.billmarkforreexport", { invoice_number }), billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }), - billupdated: (invoice_number, values) => i18n.t("audit_trail.messages.billupdated", { invoice_number, values }), + billupdated: (invoice_number, details) => i18n.t("audit_trail.messages.billupdated", { invoice_number, details }), jobassignmentchange: (operation, name) => i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }), jobassignmentremoved: (operation) => i18n.t("audit_trail.messages.jobassignmentremoved", { operation }), jobchecklist: (type, inproduction, status) => @@ -26,9 +26,10 @@ const AuditTrailMapping = { jobinproductionchange: (inproduction) => i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }), jobinvoiced: () => i18n.t("audit_trail.messages.jobinvoiced"), jobclosedwithbypass: () => i18n.t("audit_trail.messages.jobclosedwithbypass"), - joblineupdate: (line_desc) => i18n.t("audit_trail.messages.joblineupdate", { line_desc }), + joblineupdate: (lineDescription, details) => + i18n.t("audit_trail.messages.joblineupdate", { details, lineDescription }), jobmanualcreate: () => i18n.t("audit_trail.messages.jobmanualcreate"), - jobmanuallineinsert: (values) => i18n.t("audit_trail.messages.jobmanuallineinsert", { values }), + jobmanuallineinsert: (details) => i18n.t("audit_trail.messages.jobmanuallineinsert", { details }), jobmodifylbradj: ({ mod_lbr_ty, hours }) => i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }), jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"), jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"), @@ -76,7 +77,10 @@ const AuditTrailMapping = { title, uncompletedBy }), - timeticketupdated: (employee, date, details) => i18n.t("audit_trail.messages.timeticketupdated", { employee, date, details }) + timeticketcreated: (employee, date, details) => + i18n.t("audit_trail.messages.timeticketcreated", { employee, date, details }), + timeticketupdated: (employee, date, details) => + i18n.t("audit_trail.messages.timeticketupdated", { employee, date, details }) }; export default AuditTrailMapping; diff --git a/client/src/utils/auditTrailDetails.js b/client/src/utils/auditTrailDetails.js new file mode 100644 index 000000000..16398267a --- /dev/null +++ b/client/src/utils/auditTrailDetails.js @@ -0,0 +1,186 @@ +import dayjs from "./day"; + +const EMPTY_VALUE = "<>"; +const NO_CHANGES = "No changes"; + +const BILL_LINE_KEYS = ["line_desc", "quantity", "actual_price", "actual_cost", "cost_center"]; +const JOB_LINE_SKIP_KEYS = new Set(["ah_detail_line", "prt_dsmk_p"]); +const DATE_ONLY_KEYS = new Set(["date"]); +const DATE_TIME_KEYS = new Set(["clockon", "clockoff"]); +const CURRENCY_KEYS = new Set(["actual_price", "actual_cost", "act_price", "db_price", "rate"]); +const HOUR_KEYS = new Set(["productivehrs", "actualhrs", "mod_lb_hrs"]); + +const isBlank = (value) => value == null || value === ""; + +const isStructuredValue = (value) => value != null && typeof value === "object" && !dayjs.isDayjs?.(value); + +const formatDate = (value) => (isBlank(value) ? EMPTY_VALUE : dayjs(value).format("YYYY-MM-DD")); + +const formatDateTime = (value) => (isBlank(value) ? EMPTY_VALUE : dayjs(value).format("YYYY-MM-DD HH:mm")); + +const formatNumber = (value, fractionDigits) => + typeof value === "number" ? value.toFixed(fractionDigits) : String(value); + +const compareValue = (key, value) => { + if (isBlank(value)) return EMPTY_VALUE; + if (DATE_TIME_KEYS.has(key)) return formatDateTime(value); + if (DATE_ONLY_KEYS.has(key)) return formatDate(value); + if (dayjs.isDayjs?.(value)) return formatDateTime(value); + return String(value); +}; + +const buildFieldChangeDetails = ({ keys, original = {}, updated = {}, displayValue, skippedKeys = new Set() }) => + keys + .filter((key) => key !== "__typename" && !skippedKeys.has(key)) + .filter((key) => !isStructuredValue(original[key]) && !isStructuredValue(updated[key])) + .map((key) => { + if (compareValue(key, original[key]) === compareValue(key, updated[key])) return null; + return `${key}: ${displayValue(key, original[key])} -> ${displayValue(key, updated[key])}`; + }) + .filter(Boolean); + +const formatBillValue = (key, value) => { + if (isBlank(value)) return EMPTY_VALUE; + if (DATE_ONLY_KEYS.has(key)) return formatDate(value); + if (CURRENCY_KEYS.has(key)) return typeof value === "number" ? `$${value.toFixed(2)}` : String(value); + return String(value); +}; + +const formatJobLineValue = (key, value) => { + if (isBlank(value)) return EMPTY_VALUE; + if (CURRENCY_KEYS.has(key)) return typeof value === "number" ? `$${value.toFixed(2)}` : String(value); + if (HOUR_KEYS.has(key)) return formatNumber(value, 1); + return String(value); +}; + +const getEmployeeName = (employeeId, employees = [], fallbackEmployee) => { + if ( + (employeeId == null || fallbackEmployee?.id === employeeId) && + (fallbackEmployee?.first_name || fallbackEmployee?.last_name) + ) { + return [fallbackEmployee.first_name, fallbackEmployee.last_name].filter(Boolean).join(" "); + } + + const employee = employees.find(({ id }) => id === employeeId); + if (employee) { + return [employee.first_name, employee.last_name].filter(Boolean).join(" "); + } + + return employeeId ? String(employeeId) : EMPTY_VALUE; +}; + +const formatTimeTicketValue = (key, value, { employees = [], fallbackEmployee } = {}) => { + if (isBlank(value)) return EMPTY_VALUE; + if (key === "employeeid") return getEmployeeName(value, employees, fallbackEmployee); + if (DATE_TIME_KEYS.has(key)) return formatDateTime(value); + if (DATE_ONLY_KEYS.has(key)) return formatDate(value); + if (CURRENCY_KEYS.has(key)) return typeof value === "number" ? `$${value.toFixed(2)}` : String(value); + if (HOUR_KEYS.has(key)) return formatNumber(value, 1); + if (typeof value === "boolean") return value ? "true" : "false"; + return String(value); +}; + +const buildBillLineSummary = (line) => + BILL_LINE_KEYS.map((key) => `${key}: ${formatBillValue(key, line[key])}`).join(", "); + +export function buildBillUpdateAuditDetails({ originalBill = {}, bill = {}, billlines = [] }) { + const updatedBill = { ...bill, billlines }; + const billKeys = Array.from(new Set([...Object.keys(originalBill), ...Object.keys(updatedBill)])).filter( + (key) => key !== "billlines" + ); + + const changed = buildFieldChangeDetails({ + keys: billKeys, + original: originalBill, + updated: updatedBill, + displayValue: formatBillValue + }); + + const originalBillLines = originalBill.billlines ?? []; + const updatedBillLines = updatedBill.billlines ?? []; + + const addedLines = updatedBillLines + .filter((line) => !line.id) + .map((line) => `+${line.line_desc || line.description || "new line"} (${buildBillLineSummary(line)})`); + + const removedLines = originalBillLines + .filter((line) => !updatedBillLines.some((updatedLine) => updatedLine.id === line.id)) + .map( + (line) => `-${line.line_desc || line.description || line.id || "removed line"} (${buildBillLineSummary(line)})` + ); + + const modifiedLines = updatedBillLines + .filter((line) => line.id) + .flatMap((line) => { + const originalLine = originalBillLines.find(({ id }) => id === line.id); + if (!originalLine) return []; + + const lineChanges = buildFieldChangeDetails({ + keys: BILL_LINE_KEYS, + original: originalLine, + updated: line, + displayValue: formatBillValue + }); + + if (!lineChanges.length) return []; + + return [`${line.line_desc || line.description || line.id}: ${lineChanges.join("; ")}`]; + }); + + if (addedLines.length) changed.push(`billlines added: ${addedLines.join(" | ")}`); + if (removedLines.length) changed.push(`billlines removed: ${removedLines.join(" | ")}`); + if (modifiedLines.length) changed.push(`billlines modified: ${modifiedLines.join(" | ")}`); + + return changed.length ? changed.join("; ") : NO_CHANGES; +} + +export function buildJobLineInsertAuditDetails(values = {}) { + const details = Object.entries(values) + .filter(([key, value]) => !JOB_LINE_SKIP_KEYS.has(key) && !isBlank(value)) + .map(([key, value]) => `${key}: ${formatJobLineValue(key, value)}`); + + return details.length ? details.join("; ") : NO_CHANGES; +} + +export function buildJobLineUpdateAuditDetails({ originalLine = {}, values = {} }) { + const details = buildFieldChangeDetails({ + keys: Object.keys(values), + original: originalLine, + updated: values, + displayValue: formatJobLineValue, + skippedKeys: JOB_LINE_SKIP_KEYS + }); + + return details.length ? details.join("; ") : NO_CHANGES; +} + +export function buildTimeTicketAuditSummary({ originalTicket = {}, submittedValues = {}, employees = [] }) { + const normalizedOriginal = { + ...originalTicket, + jobid: originalTicket.job?.id ?? originalTicket.jobid ?? null + }; + + const details = buildFieldChangeDetails({ + keys: Object.keys(submittedValues), + original: normalizedOriginal, + updated: submittedValues, + displayValue: (key, value) => + formatTimeTicketValue(key, value, { + employees, + fallbackEmployee: key === "employeeid" ? normalizedOriginal.employee : null + }) + }); + + const employeeName = getEmployeeName( + submittedValues.employeeid ?? normalizedOriginal.employeeid, + employees, + normalizedOriginal.employee + ); + + return { + date: formatDate(submittedValues.date ?? normalizedOriginal.date), + details: details.length ? details.join("; ") : NO_CHANGES, + employeeName, + jobid: submittedValues.jobid ?? normalizedOriginal.jobid ?? null + }; +} From 8a4679f86c6f2f3bd7f2f958357addc259af47d2 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 9 Apr 2026 11:14:17 -0400 Subject: [PATCH 53/56] feature/IO-3638-Reynolds-OpenSearch - Add Search on DMS id in Reynolds shops --- os-loader.js | 1 + server/graphql-client/queries.js | 3 + server/opensearch/os-handler.js | 47 +++---------- server/opensearch/os-search-config.js | 69 +++++++++++++++++++ .../opensearch/tests/os-search-config.test.js | 21 ++++++ 5 files changed, 103 insertions(+), 38 deletions(-) create mode 100644 server/opensearch/os-search-config.js create mode 100644 server/opensearch/tests/os-search-config.test.js diff --git a/os-loader.js b/os-loader.js index 2e46c6fa7..982f841a7 100644 --- a/os-loader.js +++ b/os-loader.js @@ -32,6 +32,7 @@ async function OpenSearchUpdateHandler(req, res) { clm_no clm_total comment + dms_id ins_co_nm owner_owing ownr_co_nm diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 846759de7..56f9b58a8 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -2442,6 +2442,9 @@ exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) { associations(where: {active: {_eq: true}, useremail: {_eq: $user}}) { id shopid + bodyshop { + rr_dealerid + } } }`; diff --git a/server/opensearch/os-handler.js b/server/opensearch/os-handler.js index 663e8a015..6068c6464 100644 --- a/server/opensearch/os-handler.js +++ b/server/opensearch/os-handler.js @@ -4,6 +4,7 @@ const queries = require("../graphql-client/queries"); const client = require("../graphql-client/graphql-client").client; const { pick, isNil } = require("lodash"); const { getClient } = require("../../libs/awsUtils"); +const { JOB_DOCUMENT_FIELDS, getGlobalSearchQueryStringFields } = require("./os-search-config"); async function OpenSearchUpdateHandler(req, res) { try { @@ -21,27 +22,7 @@ async function OpenSearchUpdateHandler(req, res) { switch (req.body.table.name) { case "jobs": - document = pick(req.body.event.data.new, [ - "id", - "bodyshopid", - "clm_no", - "clm_total", - "comment", - "ins_co_nm", - "owner_owing", - "ownr_co_nm", - "ownr_fn", - "ownr_ln", - "ownr_ph1", - "ownr_ph2", - "plate_no", - "ro_number", - "status", - "v_model_yr", - "v_make_desc", - "v_model_desc", - "v_vin" - ]); + document = pick(req.body.event.data.new, JOB_DOCUMENT_FIELDS); document.bodyshopid = req.body.event.data.new.shopid; break; case "vehicles": @@ -197,15 +178,18 @@ async function OpenSearchSearchHandler(req, res) { user: req.user.email }); - if (assocs.length === 0) { + if (assocs.associations.length === 0) { res.sendStatus(401); + return; } const osClient = await getClient(); + const activeAssociation = assocs.associations[0]; const bodyShopIdMatchOverride = isNil(process.env.BODY_SHOP_ID_MATCH_OVERRIDE) - ? assocs.associations[0].shopid + ? activeAssociation.shopid : process.env.BODY_SHOP_ID_MATCH_OVERRIDE; + const isReynoldsEnabled = Boolean(activeAssociation.bodyshop?.rr_dealerid); const { body } = await osClient.search({ ...(index ? { index } : { index: ["jobs", "vehicles", "owners", "bills", "payments"] }), @@ -241,21 +225,8 @@ async function OpenSearchSearchHandler(req, res) { query: `*${search}*`, // Weighted Fields fields: [ - "*ro_number^20", - "*clm_no^14", - "*v_vin^12", - "*plate_no^12", - "*ownr_ln^10", - "transactionid^10", - "paymentnum^10", - "invoice_number^10", - "*ownr_fn^8", - "*ownr_co_nm^8", - "*ownr_ph1^8", - "*ownr_ph2^8", - "*vendor.name^8", - "*comment^6" - // "*" + ...getGlobalSearchQueryStringFields({ isReynoldsEnabled }) + // "*" ] } } diff --git a/server/opensearch/os-search-config.js b/server/opensearch/os-search-config.js new file mode 100644 index 000000000..3ef8bdcdf --- /dev/null +++ b/server/opensearch/os-search-config.js @@ -0,0 +1,69 @@ +/** + * Fields to be included in the job document indexed in OpenSearch. These fields are used for both indexing and + * searching. + * @type {string[]} + */ +const JOB_DOCUMENT_FIELDS = [ + "id", + "bodyshopid", + "clm_no", + "clm_total", + "comment", + "dms_id", + "ins_co_nm", + "owner_owing", + "ownr_co_nm", + "ownr_fn", + "ownr_ln", + "ownr_ph1", + "ownr_ph2", + "plate_no", + "ro_number", + "status", + "v_model_yr", + "v_make_desc", + "v_model_desc", + "v_vin" +]; + +/** + * Fields to be included in the global search query string. These fields are used for constructing the search query. + * @type {string[]} + */ +const BASE_GLOBAL_SEARCH_QUERY_STRING_FIELDS = [ + "*ro_number^20", + "*clm_no^14", + "*v_vin^12", + "*plate_no^12", + "*ownr_ln^10", + "transactionid^10", + "paymentnum^10", + "invoice_number^10", + "*ownr_fn^8", + "*ownr_co_nm^8", + "*ownr_ph1^8", + "*ownr_ph2^8", + "*vendor.name^8", + "*comment^6" +]; + +/** + * Returns the fields to be included in the global search query string. If Reynolds is enabled, it includes the dms_id + * field with a higher boost. + * @param param0 + * @param param0.isReynoldsEnabled + * @returns {string[]} + */ +const getGlobalSearchQueryStringFields = ({ isReynoldsEnabled = false } = {}) => { + if (!isReynoldsEnabled) { + return BASE_GLOBAL_SEARCH_QUERY_STRING_FIELDS; + } + + return ["*dms_id^20", ...BASE_GLOBAL_SEARCH_QUERY_STRING_FIELDS]; +}; + +module.exports = { + JOB_DOCUMENT_FIELDS, + BASE_GLOBAL_SEARCH_QUERY_STRING_FIELDS, + getGlobalSearchQueryStringFields +}; diff --git a/server/opensearch/tests/os-search-config.test.js b/server/opensearch/tests/os-search-config.test.js new file mode 100644 index 000000000..d6e08a006 --- /dev/null +++ b/server/opensearch/tests/os-search-config.test.js @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { createRequire } from "module"; + +const require = createRequire(import.meta.url); +const { JOB_DOCUMENT_FIELDS, BASE_GLOBAL_SEARCH_QUERY_STRING_FIELDS, getGlobalSearchQueryStringFields } = require( + "../os-search-config" +); + +describe("os-search-config", () => { + it("indexes dms_id on job documents", () => { + expect(JOB_DOCUMENT_FIELDS).toContain("dms_id"); + }); + + it("includes dms_id in global search fields for Reynolds shops", () => { + expect(getGlobalSearchQueryStringFields({ isReynoldsEnabled: true })).toContain("*dms_id^20"); + }); + + it("keeps the default search fields unchanged for non-Reynolds shops", () => { + expect(getGlobalSearchQueryStringFields()).toEqual(BASE_GLOBAL_SEARCH_QUERY_STRING_FIELDS); + }); +}); From 6bda497d8c506b6b2a8607cd0c92f511cc5f16b7 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 9 Apr 2026 13:54:48 -0400 Subject: [PATCH 54/56] feature/IO-3647-Reynolds-Integration-Phase-2 - Enhance early RO with meaningful amounts. --- .../rr-dms-allocations-summary.component.jsx | 47 +++--- server/rr/rr-calculate-allocations.js | 14 ++ server/rr/rr-job-export.js | 96 +++++------- server/rr/rr-job-helpers.js | 142 +++++++++++++++++- server/rr/rr-job-helpers.test.js | 118 +++++++++++++++ 5 files changed, 337 insertions(+), 80 deletions(-) create mode 100644 server/rr/rr-job-helpers.test.js diff --git a/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx b/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx index 4cffcfd85..a367caee6 100644 --- a/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx +++ b/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx @@ -64,7 +64,7 @@ function normalizeJobAllocations(ack) { * RR-specific DMS Allocations Summary * Focused on what we actually send to RR: * - ROGOG (split by taxable / non-taxable segments) - * - ROLABOR shell + * - ROLABOR labor rows with bill hours / rates * * The heavy lifting (ROGOG/ROLABOR split, cost allocation, tax flags) * is now done on the backend via buildRogogFromAllocations/buildRolaborFromRogog. @@ -181,21 +181,30 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat const rolaborRows = useMemo(() => { if (!rolaborPreview || !Array.isArray(rolaborPreview.ops)) return []; - return rolaborPreview.ops.map((op, idx) => { - const rowOpCode = opCode || op.opCode; + return rolaborPreview.ops + .filter((op) => + [op.bill?.jobTotalHrs, op.bill?.billTime, op.bill?.billRate, op.amount?.custPrice, op.amount?.totalAmt] + .map((value) => Number.parseFloat(value ?? "0")) + .some((value) => !Number.isNaN(value) && value !== 0) + ) + .map((op, idx) => { + const rowOpCode = opCode || op.opCode; - return { - key: `${op.jobNo}-${idx}`, - opCode: rowOpCode, - jobNo: op.jobNo, - custPayTypeFlag: op.custPayTypeFlag, - custTxblNtxblFlag: op.custTxblNtxblFlag, - payType: op.bill?.payType, - amtType: op.amount?.amtType, - custPrice: op.amount?.custPrice, - totalAmt: op.amount?.totalAmt - }; - }); + return { + key: `${op.jobNo}-${idx}`, + opCode: rowOpCode, + jobNo: op.jobNo, + custPayTypeFlag: op.custPayTypeFlag, + custTxblNtxblFlag: op.custTxblNtxblFlag, + payType: op.bill?.payType, + jobTotalHrs: op.bill?.jobTotalHrs, + billTime: op.bill?.billTime, + billRate: op.bill?.billRate, + amtType: op.amount?.amtType, + custPrice: op.amount?.custPrice, + totalAmt: op.amount?.totalAmt + }; + }); }, [rolaborPreview, opCode]); // Totals for ROGOG (sum custPrice + dlrCost over all lines) @@ -245,6 +254,9 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat { title: "CustPayType", dataIndex: "custPayTypeFlag", key: "custPayTypeFlag" }, { title: "CustTxblFlag", dataIndex: "custTxblNtxblFlag", key: "custTxblNtxblFlag" }, { title: "PayType", dataIndex: "payType", key: "payType" }, + { title: "JobTotalHrs", dataIndex: "jobTotalHrs", key: "jobTotalHrs" }, + { title: "BillTime", dataIndex: "billTime", key: "billTime" }, + { title: "BillRate", dataIndex: "billRate", key: "billRate" }, { title: "AmtType", dataIndex: "amtType", key: "amtType" }, { title: "CustPrice", dataIndex: "custPrice", key: "custPrice" }, { title: "TotalAmt", dataIndex: "totalAmt", key: "totalAmt" } @@ -317,12 +329,13 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat children: ( <> - This mirrors the shell that would be sent for ROLABOR when all financials are carried in GOG. + This mirrors the labor rows RR will receive, including weighted bill hours and rates derived from the + job's labor lines. cost: summarizeMoney(a.cost) })); +const toFiniteNumber = (value) => { + const parsed = Number.parseFloat(value); + return Number.isFinite(parsed) ? parsed : 0; +}; + /** * Internal per-center bucket shape for *sales*. * We keep separate buckets for RR so we can split @@ -62,6 +67,8 @@ function emptyCenterBucket() { // Labor laborTaxableSale: zero, // labor that should be taxed in RR laborNonTaxableSale: zero, // labor that should NOT be taxed in RR + laborTaxableHours: 0, + laborNonTaxableHours: 0, // Extras (MAPA/MASH/towing/PAO/etc) extrasSale: zero, // total extras (taxable + non-taxable) @@ -453,6 +460,7 @@ function buildProfitCenterHash(job, debugLog, taxContext) { const rateKey = `rate_${val.mod_lbr_ty.toLowerCase()}`; const rate = job[rateKey]; + const lineHours = toFiniteNumber(val.mod_lb_hrs); const laborAmount = Dinero({ amount: Math.round(rate * 100) @@ -460,8 +468,10 @@ function buildProfitCenterHash(job, debugLog, taxContext) { if (isLaborTaxable(val, taxContext)) { bucket.laborTaxableSale = bucket.laborTaxableSale.add(laborAmount); + bucket.laborTaxableHours += lineHours; } else { bucket.laborNonTaxableSale = bucket.laborNonTaxableSale.add(laborAmount); + bucket.laborNonTaxableHours += lineHours; } } @@ -478,6 +488,8 @@ function buildProfitCenterHash(job, debugLog, taxContext) { partsNonTaxable: summarizeMoney(b.partsNonTaxableSale), laborTaxable: summarizeMoney(b.laborTaxableSale), laborNonTaxable: summarizeMoney(b.laborNonTaxableSale), + laborTaxableHours: b.laborTaxableHours, + laborNonTaxableHours: b.laborNonTaxableHours, extras: summarizeMoney(b.extrasSale), extrasTaxable: summarizeMoney(b.extrasTaxableSale), extrasNonTaxable: summarizeMoney(b.extrasNonTaxableSale) @@ -916,6 +928,8 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo // Labor laborTaxableSale: bucket.laborTaxableSale, laborNonTaxableSale: bucket.laborNonTaxableSale, + laborTaxableHours: bucket.laborTaxableHours, + laborNonTaxableHours: bucket.laborNonTaxableHours, // Extras extrasSale, diff --git a/server/rr/rr-job-export.js b/server/rr/rr-job-export.js index 5eefbf19a..15e2f2a34 100644 --- a/server/rr/rr-job-export.js +++ b/server/rr/rr-job-export.js @@ -1,4 +1,4 @@ -const { buildRRRepairOrderPayload } = require("./rr-job-helpers"); +const { buildRRRepairOrderPayload, buildMinimalRolaborFromJob } = require("./rr-job-helpers"); const { buildClientAndOpts } = require("./rr-lookup"); const CreateRRLogEvent = require("./rr-logger-event"); const { withRRRequestXml } = require("./rr-log-xml"); @@ -56,6 +56,27 @@ const deriveRRStatus = (rrRes = {}) => { }; }; +const resolveRROpCode = (bodyshop, txEnvelope = {}) => { + const resolvedBaseOpCode = resolveRROpCodeFromBodyshop(bodyshop); + let opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null; + + if (!opCodeOverride) { + const opPrefix = txEnvelope?.opPrefix ?? txEnvelope?.op_prefix ?? null; + const opBase = txEnvelope?.opBase ?? txEnvelope?.op_base ?? null; + const opSuffix = txEnvelope?.opSuffix ?? txEnvelope?.op_suffix ?? null; + + if (opPrefix || opBase || opSuffix) { + const combined = `${opPrefix || ""}${opBase || ""}${opSuffix || ""}`.trim(); + if (combined) { + opCodeOverride = combined; + } + } + } + + if (!opCodeOverride && !resolvedBaseOpCode) return null; + return String(opCodeOverride || resolvedBaseOpCode).trim() || null; +}; + /** * Early RO Creation: Create a minimal RR Repair Order with basic info (customer, advisor, mileage, story). * Used when creating RO from convert button or admin page before full job export. @@ -93,7 +114,9 @@ const createMinimalRRRepairOrder = async (args) => { const story = txEnvelope?.story ? String(txEnvelope.story).trim() : null; const makeOverride = txEnvelope?.makeOverride ? String(txEnvelope.makeOverride).trim() : null; - // Build minimal RO payload - just header, no allocations/parts/labor + // Build minimal RO payload for early review mode. + // We keep it lightweight, but include a single labor row when we can so Ignite + // exposes the labor subsection for editing. const cleanVin = (job?.v_vin || "") .toString() @@ -116,6 +139,12 @@ const createMinimalRRRepairOrder = async (args) => { resolvedMileageIn: mileageIn }); + const earlyRoOpCode = resolveRROpCode(bodyshop, txEnvelope); + const earlyRoLabor = buildMinimalRolaborFromJob(job, { + opCode: earlyRoOpCode, + payType: "Cust" + }); + const payload = { customerNo: String(selected), advisorNo: String(advisorNo), @@ -141,9 +170,14 @@ const createMinimalRRRepairOrder = async (args) => { if (makeOverride) { payload.makeOverride = makeOverride; } + if (earlyRoLabor) { + payload.rolabor = earlyRoLabor; + } CreateRRLogEvent(socket, "INFO", "Creating minimal RR Repair Order (early creation)", { - payload + payload, + earlyRoOpCode, + hasRolabor: !!earlyRoLabor }); const response = await client.createRepairOrder(payload, finalOpts); @@ -221,15 +255,10 @@ const updateRRRepairOrderWithFullData = async (args) => { const story = txEnvelope?.story ? String(txEnvelope.story).trim() : null; const makeOverride = txEnvelope?.makeOverride ? String(txEnvelope.makeOverride).trim() : null; - // Optional RR OpCode segments coming from the FE (RRPostForm) - const opPrefix = txEnvelope?.opPrefix ?? txEnvelope?.op_prefix ?? null; - const opBase = txEnvelope?.opBase ?? txEnvelope?.op_base ?? null; - const opSuffix = txEnvelope?.opSuffix ?? txEnvelope?.op_suffix ?? null; - // RR-only extras let rrCentersConfig = null; let allocations = null; - let opCode = null; + const opCode = resolveRROpCode(bodyshop, txEnvelope); // 1) Responsibility center config (for visibility / debugging) try { @@ -280,28 +309,9 @@ const updateRRRepairOrderWithFullData = async (args) => { allocations = []; } - const resolvedBaseOpCode = resolveRROpCodeFromBodyshop(bodyshop); - - let opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null; - - // If the FE only sends segments, combine them here. - if (!opCodeOverride && (opPrefix || opBase || opSuffix)) { - const combined = `${opPrefix || ""}${opBase || ""}${opSuffix || ""}`.trim(); - if (combined) { - opCodeOverride = combined; - } - } - - if (opCodeOverride || resolvedBaseOpCode) { - opCode = String(opCodeOverride || resolvedBaseOpCode).trim() || null; - } - CreateRRLogEvent(socket, "SILLY", "RR OP config resolved", { opCode, - baseFromConfig: resolvedBaseOpCode, - opPrefix, - opBase, - opSuffix + baseFromConfig: resolveRROpCodeFromBodyshop(bodyshop) }); // Build full RO payload for update with allocations @@ -426,15 +436,10 @@ const exportJobToRR = async (args) => { const story = txEnvelope?.story ? String(txEnvelope.story).trim() : null; const makeOverride = txEnvelope?.makeOverride ? String(txEnvelope.makeOverride).trim() : null; - // Optional RR OpCode segments coming from the FE (RRPostForm) - const opPrefix = txEnvelope?.opPrefix ?? txEnvelope?.op_prefix ?? null; - const opBase = txEnvelope?.opBase ?? txEnvelope?.op_base ?? null; - const opSuffix = txEnvelope?.opSuffix ?? txEnvelope?.op_suffix ?? null; - // RR-only extras let rrCentersConfig = null; let allocations = null; - let opCode = null; + const opCode = resolveRROpCode(bodyshop, txEnvelope); // 1) Responsibility center config (for visibility / debugging) try { @@ -477,28 +482,9 @@ const exportJobToRR = async (args) => { allocations = []; } - const resolvedBaseOpCode = resolveRROpCodeFromBodyshop(bodyshop); - - let opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null; - - // If the FE only sends segments, combine them here. - if (!opCodeOverride && (opPrefix || opBase || opSuffix)) { - const combined = `${opPrefix || ""}${opBase || ""}${opSuffix || ""}`.trim(); - if (combined) { - opCodeOverride = combined; - } - } - - if (opCodeOverride || resolvedBaseOpCode) { - opCode = String(opCodeOverride || resolvedBaseOpCode).trim() || null; - } - CreateRRLogEvent(socket, "SILLY", "RR OP config resolved", { opCode, - baseFromConfig: resolvedBaseOpCode, - opPrefix, - opBase, - opSuffix + baseFromConfig: resolveRROpCodeFromBodyshop(bodyshop) }); // Build RO payload for create. diff --git a/server/rr/rr-job-helpers.js b/server/rr/rr-job-helpers.js index 13ca83fac..23d003ccf 100644 --- a/server/rr/rr-job-helpers.js +++ b/server/rr/rr-job-helpers.js @@ -52,6 +52,19 @@ const asN2 = (dineroLike) => { return amount.toFixed(2); }; +const toFiniteNumber = (value) => { + if (typeof value === "number") { + return Number.isFinite(value) ? value : 0; + } + + if (typeof value === "string") { + const parsed = Number.parseFloat(value); + return Number.isFinite(parsed) ? parsed : 0; + } + + return 0; +}; + /** * Normalize various "money-like" shapes to integer cents. * Supports: @@ -100,6 +113,100 @@ const toMoneyCents = (value) => { const asN2FromCents = (cents) => asN2({ amount: Number.isFinite(cents) ? cents : 0, precision: 2 }); +const formatDecimal = (value, maxDecimals = 2) => { + const factor = Math.pow(10, maxDecimals); + const rounded = Math.round(Math.max(0, toFiniteNumber(value)) * factor) / factor; + if (!Number.isFinite(rounded)) return "0"; + return rounded.toFixed(maxDecimals).replace(/\.?0+$/, "") || "0"; +}; + +const buildRolaborBillFields = ({ amountUnits = 0, hours = 0, rate = 0 } = {}) => { + const normalizedAmount = toFiniteNumber(amountUnits); + + if (normalizedAmount <= 0) { + return { + jobTotalHrs: "0", + billTime: "0", + billRate: "0" + }; + } + + let resolvedHours = toFiniteNumber(hours); + let resolvedRate = toFiniteNumber(rate); + + if (resolvedHours > 0 && resolvedRate <= 0) { + resolvedRate = normalizedAmount / resolvedHours; + } else if (resolvedRate > 0 && resolvedHours <= 0) { + resolvedHours = normalizedAmount / resolvedRate; + } else if (resolvedHours <= 0 && resolvedRate <= 0) { + // Keep the math internally consistent even if the source job has dollars but no usable hours. + resolvedHours = 1; + resolvedRate = normalizedAmount; + } + + return { + jobTotalHrs: formatDecimal(resolvedHours), + billTime: formatDecimal(resolvedHours), + billRate: resolvedRate.toFixed(2) + }; +}; + +const buildMinimalRolaborFromJob = (job, { opCode, payType = "Cust" } = {}) => { + const trimmedOpCode = opCode != null ? String(opCode).trim() : ""; + if (!job || !trimmedOpCode) return null; + + let totalHours = 0; + let totalAmountUnits = 0; + + for (const line of job?.joblines || []) { + const laborType = typeof line?.mod_lbr_ty === "string" ? line.mod_lbr_ty.trim() : ""; + if (!laborType) continue; + + const lineHours = toFiniteNumber(line?.mod_lb_hrs ?? line?.db_hrs); + const configuredRate = toFiniteNumber(job?.[`rate_${laborType.toLowerCase()}`]); + let lineAmountUnits = toFiniteNumber(line?.lbr_amt); + + if (lineAmountUnits <= 0 && lineHours > 0 && configuredRate > 0) { + lineAmountUnits = lineHours * configuredRate; + } + + if (lineAmountUnits <= 0 && lineHours <= 0) continue; + + totalHours += lineHours; + totalAmountUnits += lineAmountUnits; + } + + if (totalAmountUnits <= 0 && totalHours <= 0) return null; + + const bill = buildRolaborBillFields({ + amountUnits: totalAmountUnits, + hours: totalHours, + rate: totalHours > 0 ? totalAmountUnits / totalHours : 0 + }); + const formattedAmount = totalAmountUnits.toFixed(2); + + return { + ops: [ + { + opCode: trimmedOpCode, + jobNo: "1", + custPayTypeFlag: "C", + custTxblNtxblFlag: toFiniteNumber(job?.tax_lbr_rt) > 0 ? "T" : "N", + bill: { + payType, + ...bill + }, + amount: { + payType, + amtType: "Job", + custPrice: formattedAmount, + totalAmt: formattedAmount + } + } + ] + }; +}; + /** * Build RR estimate block from allocation totals. * @param {Array} allocations @@ -326,6 +433,13 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo // Each segment becomes its own op / JobNo with a single line segments.forEach((seg, idx) => { const jobNo = String(ops.length + 1); // global, 1-based JobNo across all centers/segments + const isLaborSegment = seg.kind === "laborTaxable" || seg.kind === "laborNonTaxable"; + const segmentHours = isLaborSegment + ? seg.kind === "laborTaxable" + ? toFiniteNumber(alloc.laborTaxableHours) + : toFiniteNumber(alloc.laborNonTaxableHours) + : 0; + const segmentBillRate = isLaborSegment && segmentHours > 0 ? seg.saleCents / 100 / segmentHours : 0; const line = { breakOut, @@ -349,7 +463,9 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo // Extra metadata for UI / debugging segmentKind: seg.kind, segmentIndex: idx, - segmentCount + segmentCount, + segmentHours, + segmentBillRate }); }); } @@ -368,9 +484,9 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo * * We still keep a 1:1 mapping with GOG ops: each op gets a corresponding * OpCodeLaborInfo entry using the same JobNo and the same tax flag as its - * GOG line. Labor-specific hours/rate remain zeroed out, but actual labor - * sale amounts are mirrored into ROLABOR for labor segments so RR receives - * the expected labor pricing on updates. Non-labor ops remain zeroed. + * GOG line. Labor sale amounts are mirrored into ROLABOR and, when hours + * are available from allocations, weighted bill hours/rates are also + * populated so the labor subsection is editable in Ignite. * * @param {Object} rogg - result of buildRogogFromAllocations * @param {Object} opts @@ -391,6 +507,17 @@ const buildRolaborFromRogog = (rogg, { payType = "Cust" } = {}) => { const linePayType = firstLine.custPayTypeFlag || "C"; const isLaborSegment = op.segmentKind === "laborTaxable" || op.segmentKind === "laborNonTaxable"; const laborAmount = isLaborSegment ? String(firstLine?.amount?.custPrice ?? "0") : "0"; + const laborBill = isLaborSegment + ? buildRolaborBillFields({ + amountUnits: laborAmount, + hours: op.segmentHours, + rate: op.segmentBillRate + }) + : { + jobTotalHrs: "0", + billTime: "0", + billRate: "0" + }; return { opCode: op.opCode, @@ -399,9 +526,7 @@ const buildRolaborFromRogog = (rogg, { payType = "Cust" } = {}) => { custTxblNtxblFlag: txFlag, bill: { payType, - jobTotalHrs: "0", - billTime: "0", - billRate: "0" + ...laborBill }, amount: { payType, @@ -686,5 +811,6 @@ module.exports = { normalizeCustomerCandidates, normalizeVehicleCandidates, buildRogogFromAllocations, - buildRolaborFromRogog + buildRolaborFromRogog, + buildMinimalRolaborFromJob }; diff --git a/server/rr/rr-job-helpers.test.js b/server/rr/rr-job-helpers.test.js new file mode 100644 index 000000000..c3a6ee5f3 --- /dev/null +++ b/server/rr/rr-job-helpers.test.js @@ -0,0 +1,118 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const mock = require("mock-require"); + +const graphClientModuleId = require.resolve("../graphql-client/graphql-client"); +const queriesModuleId = require.resolve("../graphql-client/queries"); +const helpersModuleId = require.resolve("./rr-job-helpers"); + +const loadHelpers = () => { + mock.stopAll(); + mock(graphClientModuleId, { client: { request: async () => ({}) } }); + mock(queriesModuleId, { GET_JOB_BY_PK: "GET_JOB_BY_PK" }); + delete require.cache[helpersModuleId]; + return require(helpersModuleId); +}; + +afterEach(() => { + mock.stopAll(); + delete require.cache[helpersModuleId]; +}); + +describe("server/rr/rr-job-helpers", () => { + it("builds a single early-RO labor row from aggregated job labor", () => { + const { buildMinimalRolaborFromJob } = loadHelpers(); + + const rolabor = buildMinimalRolaborFromJob( + { + tax_lbr_rt: 13, + joblines: [ + { mod_lbr_ty: "LAB", mod_lb_hrs: 2, lbr_amt: 200 }, + { mod_lbr_ty: "LAD", mod_lb_hrs: 1.5, lbr_amt: 180 } + ] + }, + { opCode: "51DOZ" } + ); + + expect(rolabor).toEqual({ + ops: [ + { + opCode: "51DOZ", + jobNo: "1", + custPayTypeFlag: "C", + custTxblNtxblFlag: "T", + bill: { + payType: "Cust", + jobTotalHrs: "3.5", + billTime: "3.5", + billRate: "108.57" + }, + amount: { + payType: "Cust", + amtType: "Job", + custPrice: "380.00", + totalAmt: "380.00" + } + } + ] + }); + }); + + it("populates labor bill fields from allocation hours on the full RR payload", () => { + const { buildRRRepairOrderPayload } = loadHelpers(); + + const payload = buildRRRepairOrderPayload({ + job: { + id: "job-1", + ro_number: "RO-123", + v_vin: "1HGBH41JXMN109186" + }, + selectedCustomer: { customerNo: "1134485" }, + advisorNo: "70754", + allocations: [ + { + center: "Body Labor", + partsSale: { amount: 0, precision: 2 }, + laborTaxableSale: { amount: 24000, precision: 2 }, + laborNonTaxableSale: { amount: 0, precision: 2 }, + extrasSale: { amount: 0, precision: 2 }, + totalSale: { amount: 24000, precision: 2 }, + cost: { amount: 12000, precision: 2 }, + laborTaxableHours: 2, + laborNonTaxableHours: 0, + profitCenter: { + rr_gogcode: "BL", + rr_item_type: "G", + accountdesc: "BODY LABOR" + } + } + ], + opCode: "51DOZ" + }); + + expect(payload.rolabor).toEqual({ + ops: [ + { + opCode: "51DOZ", + jobNo: "1", + custPayTypeFlag: "C", + custTxblNtxblFlag: "T", + bill: { + payType: "Cust", + jobTotalHrs: "2", + billTime: "2", + billRate: "120.00" + }, + amount: { + payType: "Cust", + amtType: "Job", + custPrice: "240.00", + totalAmt: "240.00" + } + } + ] + }); + }); +}); From fb29fa2caadef006923a8960eb8a15e54cda59fa Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 10 Apr 2026 11:38:56 -0400 Subject: [PATCH 55/56] hotfix/2026-04-10 - Fix Location Identifier in chatter-api --- server/data/chatter-api.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/data/chatter-api.js b/server/data/chatter-api.js index 1aecb7cfb..010f75aa6 100644 --- a/server/data/chatter-api.js +++ b/server/data/chatter-api.js @@ -250,7 +250,8 @@ async function processBatchApi({ shopsToProcess, start, end, skipUpload, allShop function buildInteractionPayload(bodyshop, j) { const isCompany = Boolean(j.ownr_co_nm); - const locationIdentifier = `${bodyshop.chatter_company_id}-${bodyshop.id}`; + const locationIdentifier = bodyshop?.imexshopid ?? `${bodyshop.chatter_company_id}-${bodyshop.id}`; + const timestamp = formatChatterTimestamp(j.actual_delivery, bodyshop.timezone); if (j.actual_delivery && !timestamp) { From 2b8990950b2ce2fb8251bcce4a7fa99cd198d693 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 20 Apr 2026 11:40:19 -0400 Subject: [PATCH 56/56] hotfix/2026-04-20 - Remove item from Cost centers --- .../shop-info.responsibilitycenters.component.jsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx index b8784a4bf..c88650e13 100644 --- a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx @@ -810,16 +810,6 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { > - {!hasDMSKey && ( - - - - )} {hasDMSKey && !bodyshop.rr_dealerid && ( <>