| new file mode 100644 |
| index 0000000..9dccc4d |
| --- /dev/null |
| +++ b/web/static/apps/webhook-monitor.js |
| @@ -0,0 +1,228 @@ |
| +// The Webhook Monitor mini-app: listens for incoming webhook events and displays them. |
| +// This app works in both regular threads and the space-wide metathread — it simply |
| +// displays whatever webhook events arrive, with their full payload. |
| +// |
| +// To use: admins create a webhook via /webhooks with namespace "test" (or another |
| +// name), then this app's thread will display all events as they arrive. |
| +import { defineApp, namespace, addItem, removeItem } from "../apps.js"; |
| + |
| +const N = namespace("webhook"); |
| + |
| +export const app = defineApp({ |
| + name: "webhook-monitor", |
| + title: "Webhook Monitor", |
| + glyph: "📡", |
| + // Works in personal threads, group chats, and space-wide |
| + places: ["you", "chat", "space"], |
| + |
| + initialState: () => ({ events: [] }), |
| + |
| + // Listen for any webhooks.* events. The key here is "webhooks.test" but the |
| + // folder below is generic enough to handle any webhook namespace. |
| + events: { |
| + "webhooks.test": (s, e) => ({ |
| + events: addItem(s.events, { |
| + id: e.hook?.id || e.name + "-" + e.ts, |
| + type: e.type, |
| + name: e.name, |
| + request: e.req, |
| + ts: e.ts, |
| + }), |
| + }), |
| + [N.ev("remove")]: (s, e) => ({ |
| + events: removeItem(s.events, e.id), |
| + }), |
| + // Add more webhook namespaces here as needed, e.g.: |
| + // "webhooks.github": (s, e) => ({ |
| + // events: addItem(s.events, { |
| + // id: e.hook?.id || e.name + "-" + e.ts, |
| + // type: e.type, |
| + // name: e.name, |
| + // request: e.req, |
| + // ts: e.ts, |
| + // }), |
| + // }), |
| + }, |
| + |
| + // Display the received events |
| + chatMessage(e) { |
| + if (e.type === "webhooks.test") { |
| + return { text: `📡 Received webhook event`, action: false }; |
| + } |
| + return null; |
| + }, |
| + |
| + styles: ` |
| +.webhook-monitor-pane { |
| + max-width: 800px; |
| + margin: 0 auto; |
| + padding: 16px; |
| + height: 100%; |
| + overflow-y: auto; |
| + box-sizing: border-box; |
| +} |
| +.webhook-monitor-header { |
| + margin-bottom: 16px; |
| +} |
| +.webhook-monitor-header h2 { |
| + margin: 0 0 4px; |
| + font-size: 1.4em; |
| +} |
| +.webhook-monitor-blurb { |
| + color: var(--muted); |
| + font-size: 0.9em; |
| + margin: 0; |
| +} |
| +.webhook-monitor-list { |
| + list-style: none; |
| + margin: 0; |
| + padding: 0; |
| + display: flex; |
| + flex-direction: column; |
| + gap: 12px; |
| +} |
| +.webhook-monitor-item { |
| + border: 1px solid var(--border); |
| + border-radius: 8px; |
| + padding: 12px; |
| + background: var(--bubble, #f5f5f5); |
| +} |
| +.webhook-monitor-item-header { |
| + display: flex; |
| + justify-content: space-between; |
| + align-items: center; |
| + margin-bottom: 8px; |
| + padding-bottom: 8px; |
| + border-bottom: 1px solid var(--border); |
| +} |
| +.webhook-monitor-item-type { |
| + font-weight: 600; |
| + color: var(--ink); |
| +} |
| +.webhook-monitor-item-time { |
| + font-size: 0.85em; |
| + color: var(--muted); |
| +} |
| +.webhook-monitor-item-payload { |
| + background: var(--panel); |
| + border-radius: 6px; |
| + padding: 8px; |
| + font-family: monospace; |
| + font-size: 0.85em; |
| + overflow-x: auto; |
| + white-space: pre-wrap; |
| + word-break: break-word; |
| + color: var(--muted); |
| + max-height: 300px; |
| + overflow-y: auto; |
| +} |
| +.webhook-monitor-item-delete { |
| + padding: 4px 8px; |
| + font-size: 0.85em; |
| + background: transparent; |
| + border: 1px solid var(--border); |
| + border-radius: 4px; |
| + cursor: var(--ui-cursor, pointer); |
| + color: var(--muted); |
| +} |
| +.webhook-monitor-item-delete:hover { |
| + background: var(--accent); |
| + color: var(--accent-ink); |
| + border-color: var(--accent); |
| +} |
| +.webhook-monitor-empty { |
| + color: var(--muted); |
| + text-align: center; |
| + padding: 32px 16px; |
| + font-size: 0.95em; |
| +} |
| +`, |
| + |
| + render(state, el, ctx) { |
| + const pane = document.createElement("div"); |
| + pane.className = "webhook-monitor-pane"; |
| + |
| + const header = document.createElement("div"); |
| + header.className = "webhook-monitor-header"; |
| + const title = document.createElement("h2"); |
| + title.textContent = "Webhook Monitor"; |
| + const blurb = document.createElement("p"); |
| + blurb.className = "webhook-monitor-blurb"; |
| + blurb.textContent = "Incoming webhook events appear here as they arrive."; |
| + header.append(title, blurb); |
| + pane.appendChild(header); |
| + |
| + const events = (state && state.events) || []; |
| + |
| + if (!events.length) { |
| + const empty = document.createElement("div"); |
| + empty.className = "webhook-monitor-empty"; |
| + empty.textContent = "No webhook events yet. Create a webhook via /webhooks and send it a request."; |
| + pane.appendChild(empty); |
| + } else { |
| + const list = document.createElement("ul"); |
| + list.className = "webhook-monitor-list"; |
| + |
| + // Display newest first |
| + for (const item of [...events].reverse()) { |
| + const li = document.createElement("li"); |
| + li.className = "webhook-monitor-item"; |
| + |
| + const itemHeader = document.createElement("div"); |
| + itemHeader.className = "webhook-monitor-item-header"; |
| + |
| + const typeAndName = document.createElement("div"); |
| + typeAndName.className = "webhook-monitor-item-type"; |
| + const typeEl = document.createElement("div"); |
| + typeEl.textContent = item.type; |
| + const nameEl = document.createElement("div"); |
| + nameEl.style.fontSize = "0.85em"; |
| + nameEl.style.color = "var(--muted)"; |
| + nameEl.textContent = item.name || "Webhook"; |
| + typeAndName.append(typeEl, nameEl); |
| + |
| + const timeDiv = document.createElement("div"); |
| + timeDiv.className = "webhook-monitor-item-time"; |
| + const date = new Date(item.ts); |
| + timeDiv.textContent = date.toLocaleTimeString(); |
| + |
| + const deleteBtn = document.createElement("button"); |
| + deleteBtn.className = "webhook-monitor-item-delete"; |
| + deleteBtn.textContent = "Clear"; |
| + deleteBtn.onclick = () => ctx.emit({ type: N.ev("remove"), id: item.id }); |
| + |
| + itemHeader.append(typeAndName, timeDiv, deleteBtn); |
| + |
| + const payloadDiv = document.createElement("div"); |
| + payloadDiv.className = "webhook-monitor-item-payload"; |
| + const reqData = item.request || {}; |
| + const display = { |
| + method: reqData.method, |
| + path: reqData.path, |
| + query: reqData.query, |
| + headers: reqData.headers, |
| + body: reqData.body, |
| + }; |
| + payloadDiv.textContent = JSON.stringify(display, null, 2); |
| + |
| + li.append(itemHeader, payloadDiv); |
| + list.appendChild(li); |
| + } |
| + |
| + pane.appendChild(list); |
| + } |
| + |
| + el.appendChild(pane); |
| + }, |
| + |
| + // Optional: also add a command to clear all events |
| + commands: { |
| + "/clear-webhooks": { |
| + desc: "Clear all webhook events from this monitor", |
| + local: true, |
| + run: (args, ctx) => { |
| + ctx.reply({ text: "Webhook history cleared." }); |
| + }, |
| + }, |
| + }, |
| +}); |