Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

06 - Screen Specifications (Webhooks Module)

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)

StateRenderingSource of truth
idle/loadingAppSkeleton per block; list rows as skeleton tiles00-shared/06 §3.1
error offlineAppOfflineBanner + native retry; data shown stale if cached00-shared/10 §2
error APIAppErrorState(code, message, onRetry); 401 → re-auth; 403 → permission copy00-shared/03
emptyAppEmptyState with CTA00-shared/03
404detail screens → AppErrorState + backwebhooks.service.ts:39-43
403hidden actions; if invoked anyway, permission copypermissions.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).
  • AppListTile rows: leading globe icon; title name; subtitle url (monospace, ellipsized); chips row: event types (max 3 + "+n"); trailing AppBadge (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); sort createdAt: -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). secret is 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 → AppDialog confirm → 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.
  • AppCard form (see 08_Form_Specifications.md §1):
    1. Name (AppTextField, required)
    2. URL (AppTextField, .url keyboard, required)
    3. Events (EventPickerChipField - multi-select chips, required ≥ 1)
    4. Secret (SecretField - obscure + generate, required)
    5. Enabled (AppSwitch, default on)
  • 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 schema webhook.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: AppErrorState in 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, SecretField masked 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; name optional; secret shown 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), responseCode when present, attemptedAt relative 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); sort createdAt: -1, hard limit: 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: job eventType: '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 failed log (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

  • AppDialog or inline AppMenu confirm: "Pause this webhook?" / "Resume?".

Wire contract

  • POST /api/v1/webhooks/:id/pausesetEnabled(false) (webhooks.controller.ts:78-83, webhooks.service.ts:158-161).
  • POST /api/v1/webhooks/:id/resumesetEnabled(true) (webhooks.controller.ts:85-90).
  • Replies: { message: 'Webhook paused' } / { message: 'Webhook resumed' }.
  • Effect on fan-out: findActiveByEvent filters enabled: 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 public scope ("(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.