import express from "express"; import fetch from "node-fetch"; import { CloudWatchLogsClient, DescribeLogGroupsCommand, DescribeLogStreamsCommand, FilterLogEventsCommand } from "@aws-sdk/client-cloudwatch-logs"; import { simpleParser } from "mailparser"; 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 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 }); app.use((req, res, next) => { res.set("Cache-Control", "no-store"); next(); }); app.get("/", (req, res) => { res.type("html").send(renderHtml()); }); app.get("/app.js", (req, res) => { res.type("application/javascript").send(`(${clientApp.toString()})(${JSON.stringify(getClientConfig())});`); }); app.get("/health", (req, res) => { res.json({ ok: true, endpoint: SES_ENDPOINT, endpoints: { ses: SES_ENDPOINT, cloudWatchLogs: CLOUDWATCH_ENDPOINT }, port: PORT, defaultRefreshMs: DEFAULT_REFRESH_MS }); }); app.get("/api/messages", async (req, res) => { try { res.json(await loadMessages()); } catch (error) { console.error("Error fetching messages:", error); res.status(502).json({ error: "Unable to fetch messages from LocalStack SES", details: error.message, endpoint: SES_ENDPOINT }); } }); app.get("/api/messages/:id/raw", async (req, res) => { try { const message = await findSesMessageById(req.params.id); if (!message) { res.status(404).type("text/plain").send("Message not found"); return; } res.type("text/plain").send(message.RawData || ""); } catch (error) { console.error("Error fetching raw message:", error); res.status(502).type("text/plain").send(`Unable to fetch raw message: ${error.message}`); } }); 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}`); } }); app.get("/api/logs/groups", async (req, res) => { try { const groups = await loadLogGroups(); res.json({ endpoint: CLOUDWATCH_ENDPOINT, region: CLOUDWATCH_REGION, defaultGroup: CLOUDWATCH_DEFAULT_GROUP, groups }); } catch (error) { console.error("Error fetching log groups:", error); res.status(502).json({ error: "Unable to fetch CloudWatch log groups from LocalStack", details: error.message, endpoint: CLOUDWATCH_ENDPOINT }); } }); app.get("/api/logs/streams", async (req, res) => { try { const logGroupName = String(req.query.group || ""); if (!logGroupName) { res.status(400).json({ error: "Query parameter 'group' is required" }); return; } res.json({ logGroupName, streams: await loadLogStreams(logGroupName) }); } catch (error) { console.error("Error fetching log streams:", error); res.status(502).json({ error: "Unable to fetch CloudWatch log streams from LocalStack", details: error.message, endpoint: CLOUDWATCH_ENDPOINT }); } }); app.get("/api/logs/events", async (req, res) => { try { const logGroupName = String(req.query.group || ""); const logStreamName = String(req.query.stream || ""); const windowMs = clampNumber(req.query.windowMs, CLOUDWATCH_DEFAULT_WINDOW_MS, 60 * 1000, 24 * 60 * 60 * 1000); const limit = clampNumber(req.query.limit, CLOUDWATCH_DEFAULT_LIMIT, 25, 500); if (!logGroupName) { res.status(400).json({ error: "Query parameter 'group' is required" }); return; } res.json(await loadLogEvents({ logGroupName, logStreamName, windowMs, limit })); } catch (error) { console.error("Error fetching log events:", error); res.status(502).json({ error: "Unable to fetch CloudWatch log events from LocalStack", details: error.message, endpoint: CLOUDWATCH_ENDPOINT }); } }); 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 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 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, defaultLogGroup: CLOUDWATCH_DEFAULT_GROUP, defaultLogWindowMs: CLOUDWATCH_DEFAULT_WINDOW_MS, defaultLogLimit: CLOUDWATCH_DEFAULT_LIMIT }; } function renderHtml() { return ` LocalStack Inspector

LocalStack Toolbox

Inspector

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

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

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;--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{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,.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{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: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 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} .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} .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;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} .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: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:.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: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{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}.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}} `; } function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function clientApp(config) { const appState = { panel: "emails", logsReady: false }; 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: {}, 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"), 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"), 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(); 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", () => { 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("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; 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) { renderStatus(); renderLogsStatus(); return; } if (appState.panel === "logs" && logsState.auto) { refreshLogs("visibility"); 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; } 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; } 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 { 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))); 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 applyFilter(shouldRenderList = true) { const search = state.search.trim().toLowerCase(); state.filtered = !search ? [...state.messages] : state.messages.filter((message) => haystack(message).includes(search)); 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 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 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 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 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; } 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..."; 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 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 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 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(""); bindCardToggles(); 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", () => { 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"); 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); } 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; } 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 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"; } 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 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}`); console.log(`Watching LocalStack CloudWatch Logs endpoint at ${CLOUDWATCH_ENDPOINT} (${CLOUDWATCH_REGION})`); });