# PRD: Morning NOC Summary — Teams Adaptive Card **Version:** 1.1 **Last updated:** 2026-03-10 --- ## 1. Introduction / Overview Wulf Consulting management needs a daily morning briefing on overnight infrastructure activity across all ~46 client sites monitored by Zabbix. This feature automates that into a rich Teams direct message delivered before the workday starts, with configurable recipients and optional channel escalation actions directly from the card. **Goal:** Post a Zabbix-sourced morning summary as a Teams Adaptive Card to one or more configured Teams channel webhooks. The primary targets are the **On-Call** and/or **Outages-Issues** channels in the **Technical** team. The card runs at 6:30 AM ET on weekdays (with a Monday extended window option) and can be triggered on-demand from the Pulse admin UI. No bot registration required — delivery is via incoming webhook URLs stored in Pulse config. --- ## 2. Goals 1. Post a formatted Teams Adaptive Card to configured channel webhook(s) at 6:30 AM Mon–Fri. 2. Surface currently-open Zabbix problems (client name, trigger name, duration, severity). 3. Surface problems that resolved overnight (during the report window). 4. Show headline stats: open count, resolved count, avg MTTR, clients affected. 5. Highlight open problems active > 4 hours without acknowledgement ("Needs Attention" flag). 6. Support posting to multiple webhooks (e.g. both On-Call and Outages-Issues simultaneously). 7. Provide a **manual trigger** in the Pulse admin UI with per-webhook test send capability. 8. Support configurable weekend suppression; when Monday is the run day, extend the report window to cover the full weekend (Friday 6 PM → Monday 6:30 AM). 9. Persist each generated summary to the Pulse database and show it as a dashboard widget. --- ## 3. User Stories - **As Tom Carlin / Lorentz Hinrichsen**, I want to see the morning NOC summary posted to the Teams channels I already monitor so I get the briefing without checking a separate tool. - **As a Pulse admin**, I want to configure which channel webhooks receive the summary without touching code or env files. - **As a Pulse admin**, I want to send a test post to a specific webhook to verify formatting before the scheduled run. - **As a Pulse admin**, I want to toggle weekend suppression and Monday extended window. - **As any Pulse user**, I want to see the most recent summary on the Pulse dashboard. --- ## 4. Functional Requirements ### 4.1 Data Collection (Zabbix only — v1) 1. The system **must** query `problem.get` (with `recent: true`, `selectHosts: extend`, `selectAcknowledges: extend`, `severities: [2,3,4,5]`) to retrieve all currently open problems at run time. 2. The system **must** query `event.get` with `value: 0` (recovery events) between `time_from` = window start and `time_to` = run time to retrieve resolved problems. 3. The system **must** query `host.get` with `selectHostGroups: ['name']` once per run to build a `hostid → { clientName, hostName }` map using groups prefixed `Clients/`. 4. The report window **must** be determined as follows: - **Tuesday–Friday runs:** window start = previous calendar day at 18:00 ET. - **Monday run (weekend mode on):** window start = the preceding Friday at 18:00 ET, covering the full Sat + Sun + Mon pre-6:30 AM period. - **Monday run (weekend mode off):** same as Tue–Fri (Sunday 18:00 ET → Monday 6:30 AM). 5. The system **must** calculate: - **Open count** — number of active problems meeting severity filter. - **Resolved count** — recovery events in the window. - **Avg MTTR (minutes)** — mean of `(r_clock - clock)` for resolved events; `null` if none. - **Clients affected** — deduplicated client names from open problems. 6. Any open problem where `clock` is > 4 hours before run time **and** `acknowledged` array is empty **must** be flagged `needsAttention: true`. ### 4.2 Webhook Configuration — Pulse UI 7. The admin UI **must** provide a **Webhooks** section on `/admin/morning-summary` to manage the list of Teams incoming webhook URLs that receive the summary. 8. The admin **must** be able to **add** a webhook by providing: - A display label (e.g. `Technical / On-Call`) - The incoming webhook URL - An enabled toggle 9. The admin **must** be able to **remove** or **disable** any webhook. 10. Webhooks **must** be stored in Postgres (new table `morning_summary_webhooks`) with: - `id` (serial PK) - `label` (text) — human-friendly name - `webhook_url` (text) - `enabled` (boolean, default true) - `last_delivered_at` (timestamptz, nullable) - `last_status` (text, nullable) — `success` or `failed` - `created_at` (timestamptz) 11. The admin UI **must** provide a **"Test"** button next to each webhook that posts the current summary card to that URL only, independent of the scheduled run. 12. The test endpoint **must** be `POST /api/notifications/morning-summary/test` with body `{ "webhookId": }`. 13. The UI **must** display per-webhook last delivery status inline. ### 4.3 Teams Adaptive Card — Structure 14. The card **must** include a header `TextBlock`: `☀️ Morning NOC Summary — {date}`. - On Monday with weekend mode: append ` (Weekend Coverage)` to the header. 15. The card **must** include a stat `ColumnSet`: Open (red), Resolved (green), Avg MTTR. 16. If there are **open problems**, the card **must** include a red-styled `Container` labelled `🔴 OPEN ISSUES` with a `FactSet`: - **Title:** `{ClientName} / {HostName}` - **Value:** `{TriggerName} — {duration}` (+ ` ⚠️ Needs Attention` if flagged) 17. If there are **no open problems**, the card **must** show a green "✅ All Clear" container. 18. If there are **resolved overnight** events, the card **must** include a green-styled `Container` labelled `🟢 RESOLVED OVERNIGHT`. If more than 5, show 5 and append `+{N} more — all resolved`. 19. The card **must** include three action buttons: - `Open Zabbix` → `https://zabbix.wulfconsulting.cloud` - `Open Pulse` → `https://pulse.wulfconsulting.cloud` - `View Problems` → `https://zabbix.wulfconsulting.cloud/zabbix.php?action=problem.view` 20. The card **must** use Adaptive Card schema version `1.4`. 21. The card payload **must** be wrapped in the Teams incoming webhook envelope: `{ "type": "message", "attachments": [{ "contentType": "application/vnd.microsoft.card.adaptive", "content": }] }` ### 4.4 Delivery via Incoming Webhooks 22. The system **must** POST the Adaptive Card envelope to each enabled webhook URL. 23. Delivery **must** be attempted for all enabled webhooks regardless of individual failures. 24. Each delivery result (HTTP status, error message) **must** be recorded in `morning_summaries.delivery_status` keyed by webhook ID. 25. The send endpoint `POST /api/notifications/morning-summary/send` **must** accept an optional `{ "webhookIds": [1, 2] }` body to target specific webhooks; if omitted, all enabled webhooks are used. 26. Incoming webhook URLs are obtained from Teams: channel → connectors → "Incoming Webhook". The admin pastes the URL into the Pulse webhook config UI. ### 4.5 MS Graph Usage (read-only) 27. MS Graph is **not** used for delivery in v1 (webhooks handle that). 28. MS Graph `getUsers()` **may** still be used in the admin UI to resolve display names when configuring webhook labels, but is not required for the core send flow. 29. No `ChatMessage.Send` or `ChannelMessage.Send` Graph permissions are needed for v1. ### 4.6 Scheduling & Weekend Mode 33. The job **must** be registered in `SyncScheduler` with cron `30 6 * * 1-5`. 34. A `morning_summary_config` table (single-row settings) **must** store: - `weekend_suppression` (boolean) — if true, skip Sat/Sun runs. - `monday_extended_window` (boolean) — if true and today is Monday, set window start to preceding Friday 18:00 ET. - `severity_filter` (int, default 2) — minimum Zabbix severity to include. - `updated_at` (timestamptz) 35. The admin UI **must** expose toggles for `weekend_suppression` and `monday_extended_window` with a clear label: _"On Mondays, extend window to cover the full weekend (Fri 6 PM → Mon 6:30 AM)"_. 36. The admin UI **must** display the next scheduled run time calculated from the cron expression and current settings. ### 4.7 Manual Trigger — Pulse UI 37. The admin UI at `/admin/morning-summary` **must** provide: - **Schedule status:** next run time, last run time, last run result. - **"Send Now"** button — fires `POST /api/notifications/morning-summary/send` to all enabled webhooks immediately. - **Per-webhook "Test"** buttons (see §4.2 req 11). - **Weekend mode toggles** (see §4.6 req 35). - **Webhooks section** — add/remove/enable-disable webhook entries (see §4.2). - **Last card preview** — most recent `card_payload` as formatted JSON (collapsed by default). 38. The UI **must** show a toast on send and display per-webhook delivery results inline. ### 4.8 Dashboard Widget & Persistence 39. Each run (scheduled or manual/test) **must** persist to `morning_summaries`: - `id` (serial PK) - `generated_at` (timestamptz) - `window_from` (timestamptz) - `window_to` (timestamptz) - `open_count` (int) - `resolved_count` (int) - `mttr_minutes` (int, nullable) - `clients_affected` (text[]) - `is_weekend_window` (boolean) - `card_payload` (jsonb) - `delivery_status` (jsonb) — `{ [webhookId]: { success: bool, httpStatus?: number, error?: string } }` 40. The Pulse home/dashboard **must** display a "Morning Summary" widget with: open count, resolved count, MTTR, window label, last generated timestamp, and a link to the admin page. --- ## 5. Non-Goals (Out of Scope for v1) - Veeam backup failure data (deferred to v2). - PSA/Autotask ticket correlation and "unmatched alerts" (deferred to v2). - Direct Teams DMs to individual users (requires bot registration — out of scope). - Email or ntfy delivery channels (webhooks only for v1). - ISP outage detection / grouping by ISP. - Per-channel content customization (all webhooks receive the same card). --- ## 6. Design Considerations ### Adaptive Card Layout ``` ┌──────────────────────────────────────────────────┐ │ ☀️ Morning NOC Summary — Mon Mar 16 (Weekend) │ ├──────────┬────────────┬────────────────────────── │ │ 3 Open │ 12 Resolved│ 22m Avg MTTR │ ├──────────────────────────────────────────────────┤ │ 🔴 OPEN ISSUES │ │ Kuhn's / FW-01 Host Unreachable — 6h 12m ⚠️ │ │ ADM / SW-Core High Packet Loss — 2h 5m │ │ Seubert / DC-01 Host Unreachable — 1h 20m │ ├──────────────────────────────────────────────────┤ │ 🟢 RESOLVED OVERNIGHT │ │ Brodaks / RTR-01 Slow Response — 22m │ │ +11 more — all resolved │ ├──────────────────────────────────────────────────┤ │[Open Zabbix][Open Pulse][View Problems] │ │[📣 Notify On Call] [🚨 Post to Outages] │ └──────────────────────────────────────────────────┘ ``` - `"style": "attention"` container → red in Teams (open issues). - `"style": "good"` container → green in Teams (resolved). - Duration: `Xh Ym` for ≥ 1 hour, `Xm` for < 1 hour. - Escalation buttons open a Pulse URL in browser; Pulse posts to channel server-side and returns a simple confirmation page. ### Recipient Management UI (on `/admin/morning-summary`) ``` Recipients ┌────────────────────────────┬──────────┬────────────────────┐ │ Name / Email │ Status │ Actions │ ├────────────────────────────┼──────────┼────────────────────┤ │ Tom Carlin │ ✅ Last: │ [Test] [Remove] │ │ tom@wulfconsulting.com │ today │ │ ├────────────────────────────┼──────────┼────────────────────┤ │ Lorentz Hinrichsen │ ✅ Last: │ [Test] [Remove] │ │ lorentz@wulfconsulting.com │ today │ │ └────────────────────────────┴──────────┴────────────────────┘ [+ Add Recipient ▾] ← searchable dropdown of tenant users ``` ### Webhook Delivery Flow ``` // For each enabled webhook in morning_summary_webhooks: POST {webhook_url} Content-Type: application/json { "type": "message", "attachments": [{ "contentType": "application/vnd.microsoft.card.adaptive", "contentUrl": null, "content": { ...adaptive card JSON... } }] } ``` To get a webhook URL in Teams: go to the target channel → **...** → Connectors → **Incoming Webhook** → Configure → copy URL → paste into Pulse admin. --- ## 7. Technical Considerations ### Existing Infrastructure to Reuse - **`ZabbixClient`** (`lib/services/zabbix-client.ts`) — add `getOpenProblems()` and `getResolvedEvents(from: Date, to: Date)` methods. - **`MsGraphClient`** — no changes needed for v1. - **`SyncScheduler`** — add `'morning-summary'` sync type; register in `initialize()`. - **Postgres** — three new tables: `morning_summaries`, `morning_summary_webhooks`, `morning_summary_config`. ### New Files to Create | File | Purpose | |------|---------| | `lib/services/morning-summary-service.ts` | Aggregation, card building, webhook delivery | | `app/api/notifications/morning-summary/send/route.ts` | `POST` — send to all/selected webhooks | | `app/api/notifications/morning-summary/test/route.ts` | `POST` — test send to single webhook | | `app/api/notifications/morning-summary/webhooks/route.ts` | `GET`/`POST` — manage webhook list | | `app/api/notifications/morning-summary/webhooks/[id]/route.ts` | `PUT`/`DELETE` — update/remove webhook | | `app/api/notifications/morning-summary/config/route.ts` | `GET`/`PUT` — read/update settings | | `app/admin/morning-summary/page.tsx` | Admin UI: webhooks, schedule, config, preview | | `db/migrations/XXXX_morning_summary.sql` | All three new tables | ### Zabbix API Notes - `problem.get` returns `acknowledged` as `"0"/"1"` strings and `clock` as Unix timestamp string. - Use `selectAcknowledges: 'extend'` — problem is unacknowledged if the array is empty. - `event.get` with `value: 0` returns recoveries; duration = `r_clock - clock` (both seconds). - Enrich problems with host/client by cross-referencing `objectid` (triggerid) against a trigger→host map built from `trigger.get` with `selectHosts: ['hostid', 'name']`, or directly via `problem.get` with `selectSuppressionData` + a pre-built hostid→client map. ### MS Graph Permissions No Graph permissions are required for v1 delivery. Delivery uses Teams incoming webhook URLs (plain HTTPS POST — no auth needed beyond the secret embedded in the URL). Existing granted permissions (`Chat.Create`, `Team.ReadBasic.All`, `Channel.ReadBasic.All`, `ChatMessage.Send`, `ChannelMessage.Send`) are unused by this feature but can remain for future use. ### Timezone - Cron `30 6 * * 1-5` fires at 6:30 AM server time — ensure server is set to ET or adjust cron. - Report window computed in ET using `Intl.DateTimeFormat` or `date-fns-tz`. - Monday extended window: `startOfDay(subDays(monday, 3)) + 18h` = Friday 18:00 ET. --- ## 8. Success Metrics - All configured recipients receive the DM at 6:30 AM on weekdays without manual action. - Test send from the UI delivers to the selected individual within 30 seconds. - Escalation button (On Call or Outages) posts to the correct Teams channel within 10 seconds and the user sees a confirmation page. - Monday card correctly shows the extended weekend window label and data. - Dashboard widget reflects the most recent summary within 1 minute of generation. - Zero unhandled errors surfaced to end users; all failures logged and shown in the admin UI. --- ## 9. Open Questions 1. ~~**MS Graph permissions**~~ — Not needed for v1 webhook delivery. 2. ~~**Team/channel names**~~ — Confirmed: team = `Technical`, channels = `On-Call` and `Outages-Issues`. 3. ~~**App membership in Technical team**~~ — Not needed; webhook URLs bypass Graph entirely. 4. **Error alerting** — If the 6:30 AM scheduled job fails entirely, how should that be surfaced? (ntfy push, Pulse admin badge, or both?) 5. **Severity filter default** — Spec defaults to Warning (2) and above. Should Information (1) problems be included? Confirm with Tom/Lorentz. 6. **Webhook URLs** — Lorentz needs to create incoming webhooks in the `On-Call` and `Outages-Issues` channels (Teams channel → ... → Connectors → Incoming Webhook → Configure) and paste the URLs into the Pulse admin UI once the feature is deployed.