06 - Screen Specifications (Webhooks Module)
- 0. Shared states (every screen)
- 1. Webhooks List (
/webhooks) - 2. Create Webhook (
/webhooks/new) - 3. Webhook Detail (
/webhooks/:id) - 4. Edit Webhook (
/webhooks/:id/edit) - 5. Delivery Logs (
/webhooks/:id/logs) - 6. Delivery Log Detail (bottom sheet)
- 7. Test Delivery (dialog)
- 8. Retry Delivery (dialog)
- 9. Pause / Resume (inline confirm)
- 10. Inbound Webhook Receiver
(planned)
Detailed specifications per screen: layout, wire contract, states, interactions, errors, permissions, a11y and analytics. Read alongside 05_Screen_Inventory.md and 00-shared/03 (components), 00-shared/06 (state), 00-shared/09 (a11y).
0. Shared states (every screen)
| State | Rendering | Source of truth |
|---|---|---|
| idle/loading | AppSkeleton per block; list rows as skeleton tiles | 00-shared/06 §3.1 |
| error offline | AppOfflineBanner + native retry; data shown stale if cached | 00-shared/10 §2 |
| error API | AppErrorState(code, message, onRetry); 401 → re-auth; 403 → permission copy | 00-shared/03 |
| empty | AppEmptyState with CTA | 00-shared/03 |
| 404 | detail screens → AppErrorState + back | webhooks.service.ts:39-43 |
| 403 | hidden actions; if invoked anyway, permission copy | permissions.constants.ts:89-92 |
Error envelope: { statusCode, message, timestamp, path } per 00-shared/07 §3
(HttpExceptionFilter).
1. Webhooks List (/webhooks)
Layout
- AppBar: "Webhooks" + FAB "New webhook" (
.create). AppListTilerows: leading globe icon; titlename; subtitleurl(monospace, ellipsized); chips row: event types (max 3 + "+n"); trailingAppBadge(enabled→ success "Active", else neutral "Paused") +AppMenu(Edit / Pause or Resume / Delete).- Pull-to-refresh; no pagination UI (API returns full list).
Wire contract
GET /api/v1/webhooks(webhooks.controller.ts:27-31); sortcreatedAt: -1(webhooks.service.ts:35-37).- Item shape:
{ _id, name, url, events[], secret, enabled, lastTriggeredAt?, failureCount, tenantId, isDeleted, version, createdAt, updatedAt }(webhook.schema.ts:8-30,base.schema.ts:10-34).secretis present in plaintext - client renders it only in detail; list rows never display it.
States
- loading: 6 skeleton tiles. empty: "No webhooks yet" + "New webhook" CTA.
- error per §0.
Interactions
- Tap row → S3. Menu Edit → S4; Pause/Resume → S9 confirm;
Delete →
AppDialogconfirm →DELETE /api/v1/webhooks/:id(webhooks.controller.ts:45-50) → optimistic remove, rollback on failure; server replies{ message: 'Webhook deleted' }(:49). - FAB → S2.
a11y / motion
- Badge read as part of tile semantics: "Active" / "Paused". Stagger fade
m-base(00-shared/08).
Analytics (proposed)
webhooks.list.open, webhooks.list.create, webhooks.list.open_detail.
2. Create Webhook (/webhooks/new)
Layout
- AppBar: "New webhook", back.
AppCardform (see 08_Form_Specifications.md §1):- Name (
AppTextField, required) - URL (
AppTextField,.urlkeyboard, required) - Events (
EventPickerChipField- multi-select chips, required ≥ 1) - Secret (
SecretField- obscure + generate, required) - Enabled (
AppSwitch, default on)
- Name (
- Primary CTA "Create webhook" (full width), disabled while submitting.
Wire contract
POST /api/v1/webhooks(webhooks.controller.ts:21-25); body =CreateWebhookDto(create-webhook.dto.ts:11-34):name: string(@IsString,:13-14)url: string(@IsUrl({ require_tld: false }),:17-18)events: string[](@IsArray+@ArrayMinSize(1)+@IsString({each:true}),:20-24)secret: string(@IsString,:27-28)enabled?: boolean(@IsOptional+@IsBoolean, default true server-side via schemawebhook.schema.ts:22-23,:30-33)
- Response: created document (envelope
{ data }, 00-shared/07 §2).
States
- submitting → CTA spinner, fields locked.
- success → pop to S3 (new id).
- error 400: field-level messages from class-validator (missing/invalid fields).
- error 401/500:
AppErrorStatein place of form? No - inline snackbar + form stays intact.
Interactions
- Event chips: tap toggles; "Select all" / "Clear" helpers; picker vocabulary from
event-queue-map.ts:6-43(see §0 note in 08). - Secret: "Generate" produces a strong random secret client-side (server has no generator); "Reveal" toggle.
- Enabled switch defaults on (
create-webhook.dto.ts:30-33).
a11y / motion
- Labels linked; first invalid field focused on submit (00-shared/09);
chips have
semanticsLabel"Event selected: X".
Analytics (proposed)
webhooks.create.submit, webhooks.create.success, webhooks.create.failure.
3. Webhook Detail (/webhooks/:id)
Layout
- AppBar: webhook
name; menu → Edit / Delete. - Header
AppCard: status badge (Active/Paused),url(monospace, tappable → copy), events chips,SecretFieldmasked with reveal (server returns plaintext secret; client keeps it in memory only,webhooks.service.ts:39-43). - Metrics tile: 4
AppStatTile- Total / Success / Failed / Pending (webhooks.service.ts:142-156). - "Recent deliveries" preview: last 5 log rows (
DeliveryLogTile), tap → S5. - Action row: "Test" (S7), "Retry" (S8, enabled only when
failed > 0), "Pause"/"Resume" (S9).
Wire contract
GET /api/v1/webhooks/:id(webhooks.controller.ts:33-37); 404 →NotFound(webhooks.service.ts:39-43).GET /api/v1/webhooks/:id/metrics(webhooks.controller.ts:72-76) →{ total, success, failed, pending }via 4 count queries (webhooks.service.ts:149-154).
States
- loading: skeleton card + tiles. 404:
AppErrorState"Webhook not found" + back. - metrics tile: independent per-tile load - one failure keeps the rest visible.
Interactions
- Retry enabled iff
failed > 0; Test always available (.update). - Pause/Resume swap based on
enabled. - Copy url/secret buttons (
Clipboard.setData).
a11y / motion
- StatTile semantics "Total deliveries: 12" etc.; masked secret announced as "Secret, hidden".
Analytics (proposed)
webhooks.detail.open, webhooks.detail.pause, webhooks.detail.resume.
4. Edit Webhook (/webhooks/:id/edit)
Layout
- Same form as S2, pre-filled: url, events, enabled;
nameoptional;secretshown masked with placeholder "Leave blank to keep current secret". - CTA "Save changes".
Wire contract
PATCH /api/v1/webhooks/:id(webhooks.controller.ts:39-43); body =UpdateWebhookDto=PartialType(CreateWebhookDto)- every field optional (update-webhook.dto.ts:4). Empty PATCH body is accepted (no-op).- Response: updated document or null (
webhooks.service.ts:45-48).
States
- as S2; plus 404 handling (webhook deleted meanwhile) → error + back.
Interactions
- Only dirty fields are sent (diff on submit).
- Secret rotation UX: reveal old → type new → save; dedicated rotation flow with
re-verification is
(forward-looking).
Analytics (proposed)
webhooks.edit.submit, webhooks.edit.success, webhooks.edit.rotate_secret.
5. Delivery Logs (/webhooks/:id/logs)
Layout
- AppBar: "Delivery logs"; subtitle = webhook name.
- List of
DeliveryLogTile: eventType (monospace), status badge (pending / success / failed),responseCodewhen present,attemptedAtrelative time; trailing chevron → S6. - Pull-to-refresh; empty state "No deliveries yet - trigger an event or use Test".
Wire contract
GET /api/v1/webhooks/:id/logs(webhooks.controller.ts:52-56); sortcreatedAt: -1, hardlimit: 50(webhooks.service.ts:88-93). No pagination params today.- Log shape:
{ _id, webhookId, eventType, payload, status, responseCode?, responseBody?, attemptCount, attemptedAt, completedAt?, createdAt, updatedAt }(webhook-delivery-log.schema.ts:7-39).
States
- loading, empty, error per §0; one row per worker attempt - retries appear as
new rows (
webhooks.service.ts:62-67, 84-89).
Interactions
- Tap row → S6 sheet. Pull-to-refresh re-fetches.
- Failed rows: "Retry" quick action (S8) without opening the sheet.
a11y / motion
- Status communicated by badge + text (never color alone, 00-shared/09).
Analytics (proposed)
webhooks.logs.open, webhooks.logs.refresh, webhooks.logs.open_detail.
6. Delivery Log Detail (bottom sheet)
Layout
- Header: eventType + status badge + timing (attemptedAt → completedAt).
- Sections: Request (payload JSON, monospace, collapsed by default, copy button);
Response (
responseCode+responseBody, monospace, collapsed;undefined→ "no response recorded"); attempt metadata (attemptCount - always 0 today,webhook-delivery-log.schema.ts:31-32, see 14). - Footer action: "Retry webhook" (S8) when failed.
Wire contract
- No extra endpoint - data already loaded by S5 list call.
States
- payload/response rendering errors (huge/non-JSON body) → truncated view + copy raw.
Analytics (proposed)
webhooks.logs.detail_open.
7. Test Delivery (dialog)
Layout
AppDialog: title "Test webhook"; body "Send a test event to?"; primary "Send test", secondary "Cancel".
Wire contract
POST /api/v1/webhooks/:id/test(webhooks.controller.ts:65-70) → server replies{ message: 'Test delivery queued' }immediately (:69); delivery is async via queue: jobeventType: 'WebhookTested',payload: { test: true, webhookId }(webhooks.service.ts:130-138).- Delivery attempt appears in logs within seconds (
webhook-delivery.worker.ts:62-89).
States / interactions
- submitting → spinner on primary; success → close + snackbar "Test delivery queued".
- After close, navigate to S5 to watch the attempt.
Analytics (proposed)
webhooks.logs.test.
8. Retry Delivery (dialog)
Layout
AppDialog: "Retry latest failed delivery?"; body shows the failed eventType + time; primary "Retry", secondary "Cancel".
Wire contract
POST /api/v1/webhooks/:id/retry(webhooks.controller.ts:58-63).- Server finds latest
failedlog (webhooks.service.ts:103-106); if none →NotFoundException('No failed deliveries to retry')(:107-108) - dialog shows "Nothing to retry" state. - Job re-queued with the failed attempt's eventType + payload
(
webhooks.service.ts:110-118);correlationId: ''(:117). - Reply:
{ message: 'Retry queued' }(webhooks.controller.ts:62).
States / interactions
- success → close + snackbar "Retry queued" → S5 refresh.
- 404 → inline note "No failed deliveries to retry" + close.
Analytics (proposed)
webhooks.logs.retry.
9. Pause / Resume (inline confirm)
Layout
AppDialogor inlineAppMenuconfirm: "Pause this webhook?" / "Resume?".
Wire contract
POST /api/v1/webhooks/:id/pause→setEnabled(false)(webhooks.controller.ts:78-83,webhooks.service.ts:158-161).POST /api/v1/webhooks/:id/resume→setEnabled(true)(webhooks.controller.ts:85-90).- Replies:
{ message: 'Webhook paused' }/{ message: 'Webhook resumed' }. - Effect on fan-out:
findActiveByEventfiltersenabled: true(webhook.repository.ts:23-28) - paused webhooks stop receiving immediately.
States / interactions
- optimistic badge flip + snackbar; rollback on 404/500.
Analytics (proposed)
webhooks.detail.pause, webhooks.detail.resume.
10. Inbound Webhook Receiver (planned)
- No screen and no endpoint in source. Planned as unauthenticated
publicscope ("(webhooks, health)", 30 req/min, 1 min window,IMPLEMENTATION_PLAN.md:48). - When implemented (e.g. test-series integration,
IMPLEMENTATION_PLAN.md:856), this screen would show tenant-scoped receipts of inbound vendor events.