import express from "express"; import fetch from "node-fetch"; import { CloudWatchLogsClient, DescribeLogGroupsCommand, DescribeLogStreamsCommand, 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(); 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 }); 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, 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()); } 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 }); } }); app.get("/api/secrets", async (req, res) => { try { res.json(await loadSecrets()); } catch (error) { console.error("Error fetching secrets:", error); res.status(502).json({ error: "Unable to fetch Secrets Manager secrets from LocalStack", details: error.message, endpoint: SECRETS_ENDPOINT }); } }); app.get("/api/secrets/value", async (req, res) => { try { const secretId = String(req.query.id || ""); if (!secretId) { res.status(400).json({ error: "Query parameter 'id' is required" }); return; } res.json(await loadSecretValue(secretId)); } catch (error) { if (error?.name === "ResourceNotFoundException") { res.status(404).json({ error: "Secret not found", details: error.message, endpoint: SECRETS_ENDPOINT }); return; } console.error("Error fetching secret value:", error); res.status(502).json({ error: "Unable to fetch Secrets Manager value from LocalStack", details: error.message, endpoint: SECRETS_ENDPOINT }); } }); 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(); 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}`); 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})`); });