diff --git a/apps/api/src/db.ts b/apps/api/src/db.ts index 83b43ae..a8d49d2 100644 --- a/apps/api/src/db.ts +++ b/apps/api/src/db.ts @@ -64,6 +64,14 @@ export async function migrate() { await sql`CREATE INDEX IF NOT EXISTS idx_pings_monitor ON pings(monitor_id, checked_at DESC)`; await sql`CREATE INDEX IF NOT EXISTS idx_pings_checked_at ON pings(checked_at)`; + // Response bodies stored separately to keep pings table lean + await sql` + CREATE TABLE IF NOT EXISTS ping_bodies ( + ping_id BIGINT PRIMARY KEY REFERENCES pings(id) ON DELETE CASCADE, + body TEXT + ) + `; + await sql` CREATE TABLE IF NOT EXISTS api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), diff --git a/apps/api/src/routes/pings.ts b/apps/api/src/routes/pings.ts index 8938f22..8b37819 100644 --- a/apps/api/src/routes/pings.ts +++ b/apps/api/src/routes/pings.ts @@ -65,6 +65,10 @@ export const ingest = new Elysia() const meta = body.meta ? { ...body.meta } : {}; if (body.cert_expiry_days != null) meta.cert_expiry_days = body.cert_expiry_days; + // Extract response body from meta — stored separately + const responseBody: string | null = meta.body_preview ?? null; + delete meta.body_preview; + const checkedAt = body.checked_at ? new Date(body.checked_at) : null; const scheduledAt = body.scheduled_at ? new Date(body.scheduled_at) : null; const jitterMs = body.jitter_ms ?? null; @@ -87,7 +91,12 @@ export const ingest = new Elysia() RETURNING * `; - // Look up account and publish to account-level bus + // Store response body separately + if (responseBody != null && ping) { + await sql`INSERT INTO ping_bodies (ping_id, body) VALUES (${ping.id}, ${responseBody})`; + } + + // Look up account and publish to account-level bus (without body to keep SSE lean) const [monitor] = await sql`SELECT account_id FROM monitors WHERE id = ${body.monitor_id}`; if (monitor) publish(monitor.account_id, ping); @@ -110,6 +119,28 @@ export const ingest = new Elysia() detail: { hide: true }, }) + // Fetch response body for a specific ping + .get("/pings/:id/body", async ({ params, headers, cookie, set }) => { + const authHeader = headers["authorization"] ?? ""; + const bearer = authHeader.match(/^bearer\s+(.+)$/i)?.[1]?.trim(); + const key = bearer ?? cookie?.pingql_key?.value; + if (!key) { set.status = 401; return { error: "Unauthorized" }; } + + const resolved = await resolveKey(key); + if (!resolved) { set.status = 401; return { error: "Unauthorized" }; } + + // Verify the ping belongs to this account + const [ping] = await sql` + SELECT p.id FROM pings p + JOIN monitors m ON m.id = p.monitor_id + WHERE p.id = ${params.id} AND m.account_id = ${resolved.accountId} + `; + if (!ping) { set.status = 404; return { error: "Not found" }; } + + const [row] = await sql`SELECT body FROM ping_bodies WHERE ping_id = ${params.id}`; + return { body: row?.body ?? null }; + }, { detail: { hide: true } }) + // SSE: single stream for all of the account's monitors .get("/account/stream", async ({ headers, cookie }) => { const authHeader = headers["authorization"] ?? ""; diff --git a/apps/web/src/db.ts b/apps/web/src/db.ts index 3652634..d2951bc 100644 --- a/apps/web/src/db.ts +++ b/apps/web/src/db.ts @@ -61,6 +61,14 @@ export async function migrate() { await sql`CREATE INDEX IF NOT EXISTS idx_pings_monitor ON pings(monitor_id, checked_at DESC)`; await sql`CREATE INDEX IF NOT EXISTS idx_pings_checked_at ON pings(checked_at)`; + // Response bodies stored separately to keep pings table lean + await sql` + CREATE TABLE IF NOT EXISTS ping_bodies ( + ping_id BIGINT PRIMARY KEY REFERENCES pings(id) ON DELETE CASCADE, + body TEXT + ) + `; + await sql` CREATE TABLE IF NOT EXISTS api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), diff --git a/apps/web/src/views/detail.ejs b/apps/web/src/views/detail.ejs index ea3ad39..de1bced 100644 --- a/apps/web/src/views/detail.ejs +++ b/apps/web/src/views/detail.ejs @@ -175,10 +175,9 @@ // ── Ping detail modal ────────────────────────────────────────── function openPingModal(ping) { - const body = document.getElementById('ping-modal-body'); + const modalBody = document.getElementById('ping-modal-body'); const meta = ping.meta || {}; const headers = meta.headers || {}; - const bodyPreview = meta.body_preview || null; const time = new Date(ping.checked_at); const scheduled = ping.scheduled_at ? new Date(ping.scheduled_at) : null; @@ -223,17 +222,34 @@ html += ''; } - // Body preview - if (bodyPreview) { - html += '
${escapeHtml(bodyPreview)}`;
- html += '