diff --git a/docs/requirements/Component-AuditLog.md b/docs/requirements/Component-AuditLog.md index ee470dc2..175dfd08 100644 --- a/docs/requirements/Component-AuditLog.md +++ b/docs/requirements/Component-AuditLog.md @@ -452,7 +452,8 @@ MS SQL for direct-write events). Unredacted secrets never persist. `UX_AuditLog_EventId` until a later tick's CATCH branch rebuilt it. - **Per-channel retention overrides (M5.5 T3):** `AuditLog:PerChannelRetentionDays` is a dictionary keyed by canonical channel name (`ApiOutbound`, `DbOutbound`, - `Notification`, `ApiInbound`) whose value is a retention window in days that + `Notification`, `ApiInbound`, `SecuredWrite` — all five `AuditChannel` values are + accepted by the validator) whose value is a retention window in days that MUST be strictly shorter than the global `RetentionDays`. After the daily partition switch-out, the purge actor runs a bounded, batched row DELETE (`PurgeChannelOlderThanAsync`) for each channel whose override is shorter than diff --git a/docs/requirements/Component-CentralUI.md b/docs/requirements/Component-CentralUI.md index eb6582d5..795f4ed4 100644 --- a/docs/requirements/Component-CentralUI.md +++ b/docs/requirements/Component-CentralUI.md @@ -75,7 +75,7 @@ Central cluster only. Sites have no user interface. ### Notification List Management (Design Role) - Create, edit, and delete notification lists. -- Each notification list has a **`Type`** — `Email` now, with `Teams` and other types planned. The type determines the type-specific targets a list carries. +- Each notification list has a **`Type`** — `Email` or `Sms` (SMS via Twilio shipped 2026-06-19; a Teams adapter was evaluated and dropped). The type determines the type-specific targets a list carries. - Manage recipients (name + email) within each `Email` list. - Configure SMTP settings. diff --git a/docs/requirements/Component-Commons.md b/docs/requirements/Component-Commons.md index 9213a50c..ce5b3025 100644 --- a/docs/requirements/Component-Commons.md +++ b/docs/requirements/Component-Commons.md @@ -43,11 +43,12 @@ Commons must define shared primitive and utility types used across multiple comp - **`TrackedOperationId`**: A GUID identifying a tracked store-and-forward operation (`ExternalSystem.CachedCall`, `Database.CachedWrite`, `Notify.Send`). Generated caller-side at the site at call time, returned to the script as a tracking handle, and reused as the idempotency key for telemetry sent to central. The notification domain's existing `NotificationId` is the notification-specific name for this same concept. - **`TrackedOperationKind` enum**: ExternalCall, DatabaseWrite. Discriminates the two cached-call kinds carried by a tracked operation (notifications are tracked separately via the `NotificationType` enum). - **`TrackedOperationStatus` enum**: Pending, Retrying, Delivered, Parked, Failed, Discarded. The unified lifecycle state shared by all tracked store-and-forward operations. This is the operation's externally-observable lifecycle status in the site-local tracking table (the status record); it is related to but distinct from the S&F buffer's own `StoreAndForwardMessageStatus`, which tracks a buffered message's retry state within the buffer (the retry mechanism). `Failed` (permanent failure) has no notification analogue — notifications use only the other five states (the `NotificationStatus` enum omits `Failed`). -- **`AuditChannel` enum**: ApiOutbound, DbOutbound, Notification, ApiInbound. Discriminates the script-trust-boundary channel that produced an `AuditEvent`. Owned by the Audit Log component. -- **`AuditKind` enum**: ApiCall, ApiCallCached, DbWrite, DbWriteCached, NotifySend, NotifyDeliver, InboundRequest, InboundAuthFailure, CachedSubmit, CachedResolve. Channel-specific event kind — the valid `Kind` values for each `AuditChannel` are listed in the Audit Log component design (`Component-AuditLog.md`). +- **`AuditChannel` enum**: ApiOutbound, DbOutbound, Notification, ApiInbound, SecuredWrite. Discriminates the script-trust-boundary channel that produced an `AuditEvent` (`SecuredWrite` extends the boundary to the two-person MxGateway secured-write flow). Owned by the Audit Log component. +- **`AuditKind` enum**: ApiCall, ApiCallCached, DbWrite, DbWriteCached, NotifySend, NotifyDeliver, InboundRequest, InboundAuthFailure, CachedSubmit, CachedResolve, SecuredWriteSubmit, SecuredWriteApprove, SecuredWriteReject, SecuredWriteExecute, ReconciliationAbandoned. Channel-specific event kind — the valid `Kind` values for each `AuditChannel` are listed in the Audit Log component design (`Component-AuditLog.md`). (`SecuredWrite*` are the four secured-write lifecycle events; `ReconciliationAbandoned` is the durable record emitted when a reconciliation pull permanently fails to insert a lost row — arch-review 04.) - **`AuditStatus` enum**: Submitted, Forwarded, Attempted, Delivered, Failed, Parked, Discarded, Skipped. Lifecycle status of an audit event row; cached operations transit Submitted → Forwarded → Attempted → Delivered/Parked/Discarded. `Skipped` covers short-circuited (e.g. dry-run) actions that should still be audited. - **`AuditForwardState` enum**: Pending, Forwarded, Reconciled. Site-local SQLite flag governing the telemetry/reconciliation loop (set on a row but never sent to central). - **`AuditEvent`** (`ZB.MOM.WW.Audit.AuditEvent` — external NuGet package, not a Commons entity class): The canonical, transport-agnostic audit record from the `ZB.MOM.WW.Audit` package that Commons references. Carries the 10 canonical fields: `EventId` (GUID, idempotency key), `OccurredAtUtc`, `Actor`, `Action` (format `"{channel}.{kind}"`), `Outcome` (`AuditOutcome`: `Success`/`Failure`/`Denied`), `Category` (channel name for ScadaBridge), `Target`, `SourceNode`, `CorrelationId`, and `DetailsJson` (JSON extension bag). All ScadaBridge-specific fields — `Channel`, `Kind`, `Status`, `ExecutionId`, `ParentExecutionId`, `SourceSiteId`, `SourceInstanceId`, `SourceScript`, `HttpStatus`, `DurationMs`, `ErrorMessage`, `ErrorDetail`, `RequestSummary`, `ResponseSummary`, `PayloadTruncated`, `Extra`, `IngestedAtUtc` — are carried in `DetailsJson` and are not top-level properties of `AuditEvent`. The `AuditChannel`, `AuditKind`, `AuditStatus`, and `AuditForwardState` enums defined in Commons (under `Types/Enums/`) describe the vocabulary encoded in `DetailsJson` and in the `AuditLogRow` computed columns. The site-local `ForwardState` (`AuditForwardState`) flag used by the telemetry/reconciliation loop is stored separately in the site SQLite `AuditLog` table. See `Component-AuditLog.md` for the persistence schema and ingest semantics. + - **`AuditLogRow` deviation (documented, intentional):** the central `dbo.AuditLog` EF entity `AuditLogRow` is a **persistence-only projection** of the external `ZB.MOM.WW.Audit.AuditEvent` contract — it lives in the Configuration Database component, **not** as a Commons POCO. This is a deliberate exception to the "entities are persistence-ignorant POCOs in Commons" rule: the canonical record is owned by the external `ZB.MOM.WW.Audit` package, and `AuditLogRow` exists only to give EF a table shape (canonical columns + `DetailsJson`-derived computed columns) to read/write. Do not lift it into Commons. - **`SiteCall`**: A record carrying the central `SiteCalls` operational-mirror row — `TrackedOperationId`, `Channel` (the trust-boundary channel, `ApiOutbound` or `DbOutbound`), `Target` (human-readable call target), `SourceSite` (site id), `SourceNode` (emitting node, nullable), `Status` (string form of `AuditStatus`; monotonic), `RetryCount`, `LastError`, `HttpStatus`, `CreatedAtUtc`, `UpdatedAtUtc`, `TerminalAtUtc`, `IngestedAtUtc` — fed by site `CachedCallTelemetry` and the periodic reconciliation pull. Types defined here must be immutable and thread-safe. @@ -86,7 +87,7 @@ Entity classes are organized by domain area: - **Deployment**: `DeploymentRecord`, `SystemArtifactDeploymentRecord`, `DeployedConfigSnapshot`. - **Audit**: `AuditLogEntry` (configuration-change audit, owned by Configuration Database), `SiteCall` (`SiteCalls` operational-mirror row). (`AuditEvent` is `ZB.MOM.WW.Audit.AuditEvent` from the external NuGet package — not a Commons entity class file; see REQ-COM-1.) -The **`Notification`** entity is the persistence-ignorant POCO for a row of the central `Notifications` table — the durable notification queue owned by the Notification Outbox. It is a plain class with properties for `NotificationId` (GUID, the idempotency key), `Type` (`NotificationType` enum discriminator), `ListName`, `Subject`, `Body`, `TypeData` (a JSON string — the type-agnostic extensibility hook), `Status` (`NotificationStatus` enum), `RetryCount`, `LastError`, `ResolvedTargets`, the provenance fields `SourceSiteId` / `SourceInstanceId` / `SourceScript`, and the UTC timestamps `SiteEnqueuedAt`, `CreatedAt`, `LastAttemptAt`, `NextAttemptAt`, `DeliveredAt`. As with every entity class it has no EF dependency; the Configuration Database component supplies the Fluent API mapping, value conversions, and indexes. The `Type` and `Status` enums (`NotificationType`: `Email` — currently the only value; `Teams` and other channels are planned but not yet present in the enum; `NotificationStatus`: `Pending`, `Retrying`, `Delivered`, `Parked`, `Discarded`) are defined under `Types/Enums/` per REQ-COM-1. +The **`Notification`** entity is the persistence-ignorant POCO for a row of the central `Notifications` table — the durable notification queue owned by the Notification Outbox. It is a plain class with properties for `NotificationId` (GUID, the idempotency key), `Type` (`NotificationType` enum discriminator), `ListName`, `Subject`, `Body`, `TypeData` (a JSON string — the type-agnostic extensibility hook), `Status` (`NotificationStatus` enum), `RetryCount`, `LastError`, `ResolvedTargets`, the provenance fields `SourceSiteId` / `SourceInstanceId` / `SourceScript`, and the UTC timestamps `SiteEnqueuedAt`, `CreatedAt`, `LastAttemptAt`, `NextAttemptAt`, `DeliveredAt`. As with every entity class it has no EF dependency; the Configuration Database component supplies the Fluent API mapping, value conversions, and indexes. The `Type` and `Status` enums (`NotificationType`: `Email`, `Sms` — SMS via Twilio shipped 2026-06-19; a Microsoft Teams adapter was evaluated and dropped, because an outbound-only Graph client cannot send 1:1 chat bodies without a Bot Framework inbound endpoint; `NotificationStatus`: `Pending`, `Retrying`, `Delivered`, `Parked`, `Discarded`) are defined under `Types/Enums/` per REQ-COM-1. ### REQ-COM-4: Per-Component Repository Interfaces diff --git a/docs/requirements/Component-ConfigurationDatabase.md b/docs/requirements/Component-ConfigurationDatabase.md index fbc2478f..1d9f3244 100644 --- a/docs/requirements/Component-ConfigurationDatabase.md +++ b/docs/requirements/Component-ConfigurationDatabase.md @@ -55,13 +55,20 @@ The configuration database stores all central system data, organized by domain a - **Database Connection Definitions**: Named database connections (name, connection details, retry settings). ### Notifications -- **Notification Lists**: List definitions (name, `Type` discriminator — `Email` / `Teams` / … — selecting the list's notification type and type-specific targets). +- **Notification Lists**: List definitions (name, `Type` discriminator — `Email` / `Sms` — selecting the list's notification type and type-specific targets). - **Notification Recipients**: Recipients per list (name, email address). - **SMTP Configuration**: Email server settings. -- **Notifications**: The durable central notification queue owned by the Notification Outbox — one row per notification, the single source of audit truth. The schema is **type-agnostic** so it records any notification type the system supports (email today, Microsoft Teams and others later): a `Type` discriminator selects the type, and a `TypeData` JSON column (`nvarchar(max)`) carries any future per-type fields without a schema change. Columns: `NotificationId` (GUID, primary key — generated at the site, used as the idempotency key), `Type`, `ListName`, `Subject`, `Body`, `TypeData`, `Status`, `RetryCount`, `LastError`, `ResolvedTargets`, `SourceSiteId`, `SourceInstanceId`, `SourceScript`, `SiteEnqueuedAt`, `CreatedAt`, `LastAttemptAt`, `NextAttemptAt`, `DeliveredAt`. `Status` is a `NotificationStatus` enum stored with values `Pending`, `Retrying`, `Delivered`, `Parked`, `Discarded` (the site-local `Forwarding` state is never persisted centrally). Indexed on `Status` and `NextAttemptAt` for efficient dispatcher polling of due rows, and on `SourceSiteId` and `CreatedAt` for KPI computation and the Central UI query page. Terminal rows are removed by a daily purge job — see Scheduled Maintenance below. See Component-NotificationOutbox.md for the full lifecycle. +- **Notifications**: The durable central notification queue owned by the Notification Outbox — one row per notification, the single source of audit truth. The schema is **type-agnostic** so it records any notification type the system supports (email and SMS today; the type-agnostic shape absorbs future types without a migration): a `Type` discriminator selects the type, and a `TypeData` JSON column (`nvarchar(max)`) carries any future per-type fields without a schema change. Columns: `NotificationId` (GUID, primary key — generated at the site, used as the idempotency key), `Type`, `ListName`, `Subject`, `Body`, `TypeData`, `Status`, `RetryCount`, `LastError`, `ResolvedTargets`, `SourceSiteId`, `SourceInstanceId`, `SourceScript`, `SiteEnqueuedAt`, `CreatedAt`, `LastAttemptAt`, `NextAttemptAt`, `DeliveredAt`. `Status` is a `NotificationStatus` enum stored with values `Pending`, `Retrying`, `Delivered`, `Parked`, `Discarded` (the site-local `Forwarding` state is never persisted centrally). Indexed on `Status` and `NextAttemptAt` for efficient dispatcher polling of due rows, and on `SourceSiteId` and `CreatedAt` for KPI computation and the Central UI query page. Terminal rows are removed by a daily purge job — see Scheduled Maintenance below. See Component-NotificationOutbox.md for the full lifecycle. ### Site Calls -- **SiteCalls**: The central audit table for cached site calls — `ExternalSystem.CachedCall()` and `Database.CachedWrite()` — owned by the Site Call Audit component and a sibling of the `Notifications` table. One row per cached operation. Columns: `TrackedOperationId` (`varchar(36)`, primary key — the GUID idempotency key stored in "D"-format string, matching the site SQLite and gRPC wire format), `Channel` (`varchar(32)` — `ApiOutbound` or `DbOutbound`; the trust-boundary channel that produced the call), `Target` (`varchar(256)` — human-readable target, e.g. `ERP.GetOrder` for an external call), `SourceSite` (`varchar(64)` — the site that submitted the cached call), `SourceNode` (`varchar(64)` NULL — cluster node that emitted the call; null for reconciled rows from retired nodes), `Status` (`varchar(32)` — `AuditStatus` enum name: `Submitted`, `Forwarded`, `Attempted`, `Delivered`, `Failed`, `Parked`, `Discarded`; lifecycle is monotonic), `RetryCount` (`int`), `LastError` (`nvarchar(1024)` NULL), `HttpStatus` (`int` NULL — last HTTP status code for API calls), `CreatedAtUtc` (`datetime2`), `UpdatedAtUtc` (`datetime2`), `TerminalAtUtc` (`datetime2` NULL), `IngestedAtUtc` (`datetime2` — central ingest timestamp). The table is populated **only** by Site Call Audit telemetry and reconciliation pulls — sites are the source of truth and the row is an eventually-consistent mirror, never written by a central dispatcher. Ingestion is **insert-if-not-exists** keyed on `TrackedOperationId`, then **upsert-on-newer-status**; the lifecycle is monotonic, so at-least-once and out-of-order telemetry are harmless. Indexes: `IX_SiteCalls_Source_Created (SourceSite, CreatedAtUtc DESC)` for per-site queries and `IX_SiteCalls_Status_Updated (Status, UpdatedAtUtc DESC)` for status-filtered views. Terminal rows are removed by a daily purge job — see Scheduled Maintenance below. See Component-SiteCallAudit.md for the full lifecycle. +- **SiteCalls**: The central audit table for cached site calls — `ExternalSystem.CachedCall()` and `Database.CachedWrite()` — owned by the Site Call Audit component and a sibling of the `Notifications` table. One row per cached operation. Columns: `TrackedOperationId` (`varchar(36)`, primary key — the GUID idempotency key stored in "D"-format string, matching the site SQLite and gRPC wire format), `Channel` (`varchar(32)` — `ApiOutbound` or `DbOutbound`; the trust-boundary channel that produced the call), `Target` (`varchar(256)` — human-readable target, e.g. `ERP.GetOrder` for an external call), `SourceSite` (`varchar(64)` — the site that submitted the cached call), `SourceNode` (`varchar(64)` NULL — cluster node that emitted the call; null for reconciled rows from retired nodes), `Status` (`varchar(32)` — `AuditStatus` enum name: `Submitted`, `Forwarded`, `Attempted`, `Delivered`, `Failed`, `Parked`, `Discarded`; lifecycle is monotonic), `RetryCount` (`int`), `LastError` (`nvarchar(1024)` NULL), `HttpStatus` (`int` NULL — last HTTP status code for API calls), `CreatedAtUtc` (`datetime2`), `UpdatedAtUtc` (`datetime2`), `TerminalAtUtc` (`datetime2` NULL), `IngestedAtUtc` (`datetime2` — central ingest timestamp). The table is populated **only** by Site Call Audit telemetry and reconciliation pulls — sites are the source of truth and the row is an eventually-consistent mirror, never written by a central dispatcher. Ingestion is **insert-if-not-exists** keyed on `TrackedOperationId`, then **upsert-on-newer-status**; the lifecycle is monotonic, so at-least-once and out-of-order telemetry are harmless. Indexes: `IX_SiteCalls_Source_Created (SourceSite, CreatedAtUtc DESC)` for per-site queries, `IX_SiteCalls_Status_Updated (Status, UpdatedAtUtc DESC)` for status-filtered views, and the filtered `IX_SiteCalls_NonTerminal` (on `CreatedAtUtc`, `WHERE TerminalAtUtc IS NULL`, INCLUDE `SourceSite, SourceNode, Status`) that backs the live-queue KPI predicates. Terminal rows are removed by a daily purge job — see Scheduled Maintenance below. See Component-SiteCallAudit.md for the full lifecycle. + +### Data-layer conventions & tracked follow-ups (arch-review 04) + +- **New tables use UTC `DateTime` + the UTC value converter**, not `DateTimeOffset`. The `Notification` entity's `DateTimeOffset` timestamps are the **documented legacy exception** — on the SQLite test harness the `DateTimeOffset`→ISO-8601-string converter forced in-memory reductions in a few KPI paths (now pushed server-side where translatable). Prefer `datetime2` UTC columns for any new table. +- **New repositories write MSSQL-only SQL with MSSQL-backed test fixtures** (`SkippableFact` gated on the live SQL Server container). The `NotificationOutboxRepository` dual SQLite/T-SQL dialect is the **documented legacy exception** (finding S8) — it predates the MSSQL-fixture convention; do not copy that pattern. +- **AuditLog clustered key.** The `dbo.AuditLog` primary/clustered key is `(EventId, OccurredAtUtc)` — `OccurredAtUtc` is required by the monthly partition scheme (`ps_AuditLog_Month`), and `EventId` carries a separate unique index for `InsertIfNotExists` idempotency. **Tracked follow-up (P3):** benchmark whether flipping the clustered key to `(OccurredAtUtc, EventId)` improves range-scan locality once real volume exists. +- **Tracked follow-ups (P4/P6):** the per-node KPI sampling cadence and the AuditLog reconciliation-pull keyset paging (the SiteCalls reconciliation pull already uses a composite keyset cursor; `PullAuditEvents` remains inclusive-`>=`, idempotent on `EventId`) are deferred, lower-urgency hardening items. ### Audit Log - **AuditLog**: The central, append-only audit table owned by the Audit Log component — one row per script-trust-boundary lifecycle event across all channels (outbound API calls, outbound DB writes/reads, notifications, and inbound API requests). Sibling of the `Notifications` and `SiteCalls` tables but distinct: `AuditLog` is the immutable history that observes the other subsystems, not an operational state store. diff --git a/docs/requirements/Component-SiteCallAudit.md b/docs/requirements/Component-SiteCallAudit.md index 51af77ff..0b2c4f09 100644 --- a/docs/requirements/Component-SiteCallAudit.md +++ b/docs/requirements/Component-SiteCallAudit.md @@ -32,28 +32,50 @@ central node. Registered as component #22 in the Host role configuration. ## The `SiteCalls` Table Lives in the central MS SQL configuration database — a sibling of the -`Notifications` table. One row per `TrackedOperationId`: +`Notifications` table. One row per `TrackedOperationId` (the shipped columns, as +mapped by `SiteCallEntityTypeConfiguration` — the source of truth is +`Component-ConfigurationDatabase.md` § Site Calls): -- **TrackedOperationId** — GUID, primary key. Generated site-side at call time. -- **SourceSite** — site that issued the call. -- **SourceNode** — the cluster node on which the call was issued (`node-a` / - `node-b`, qualified by `SourceSite`). Nullable. Stamped site-side at submit - time and carried verbatim through the combined `CachedCallTelemetry` packet, - reconciliation pulls, and the central upsert. -- **Kind** — `TrackedOperationKind` enum: `ExternalCall` or `DatabaseWrite`. -- **TargetSummary** — external system + method name for an `ExternalCall`; for a - `DatabaseWrite`, just the database connection name — intentionally not the SQL - statement or table, a deliberate scoping choice. -- **Status** — `Pending`, `Retrying`, `Delivered`, `Parked`, `Failed`, `Discarded`. -- **RetryCount** — attempts so far. -- **LastError** — most recent error detail, if any. -- **Provenance** — source instance / script. -- **CreatedAtUtc**, **UpdatedAtUtc**, **TerminalAtUtc** — key timestamps. +- **TrackedOperationId** — GUID (`varchar(36)`, "D"-format), primary key. Generated + site-side at call time. +- **Channel** — `varchar(32)`, the trust-boundary channel that produced the call: + `ApiOutbound` (`ExternalSystem.CachedCall()`) or `DbOutbound` (`Database.CachedWrite()`). +- **Target** — `varchar(256)`, human-readable target (e.g. `ERP.GetOrder` for an + external call, or the database connection name for a cached write — intentionally + not the SQL statement or table, a deliberate scoping choice). +- **SourceSite** — `varchar(64)`, site that issued the call. +- **SourceNode** — `varchar(64)` NULL, the cluster node on which the call was issued + (`node-a` / `node-b`, qualified by `SourceSite`). Stamped site-side at submit time + and carried verbatim through the combined `CachedCallTelemetry` packet, + reconciliation pulls, and the central upsert; NULL for reconciled rows from a + retired node. +- **Status** — `varchar(32)`, the `AuditStatus` enum name (**not** the tracking-lifecycle + names): `Submitted`, `Forwarded`, `Attempted`, `Delivered`, `Failed`, `Parked`, + `Discarded`. The lifecycle is monotonic, so out-of-order/at-least-once telemetry is + harmless. +- **RetryCount** — `int`, attempts so far. +- **LastError** — `nvarchar(1024)` NULL, most recent error detail, if any. +- **HttpStatus** — `int` NULL, last HTTP status code for API calls. +- **CreatedAtUtc**, **UpdatedAtUtc** (`datetime2`), **TerminalAtUtc** (`datetime2` NULL), + **IngestedAtUtc** (`datetime2`, central ingest timestamp) — key timestamps. + +There are **no `Kind`, `TargetSummary`, or provenance (instance/script) columns** — +those were an earlier design that did not ship; provenance detail for a cached call +lives on the site's own tracking store and in the AuditLog rows. ## Status Lifecycle `Pending → Retrying → Delivered / Parked / Failed / Discarded` +> **Stored vs. tracking view.** This lifecycle is the operator-facing *tracking* +> view (what the site's tracking store and `Tracking.Status()` express). The +> persisted `SiteCalls.Status` **column** stores the monotonic `AuditStatus`-derived +> string (`Submitted` / `Forwarded` / `Attempted` / `Delivered` / `Failed` / `Parked` +> / `Discarded`) carried by the combined telemetry packet — the mirror records the +> audit-event status, not the tracking enum name. The two agree on the terminal +> outcomes; the non-terminal `Submitted`/`Forwarded`/`Attempted` strings are the +> ingest-phase equivalents of `Pending`/`Retrying`. + - **Pending** — non-terminal: buffered after a transient failure, awaiting its first retry. - **Retrying** — non-terminal: undergoing retry attempts. diff --git a/docs/requirements/HighLevelReqs.md b/docs/requirements/HighLevelReqs.md index acfe27b7..1ab9b729 100644 --- a/docs/requirements/HighLevelReqs.md +++ b/docs/requirements/HighLevelReqs.md @@ -329,7 +329,7 @@ Scripts can interact with databases in two modes: ### 6.1 Notification Lists - Notification lists are **system-wide**, managed by users with the **Design** role. -- Each list has a **name** and a **`Type`** (`Email` now; `Teams` and other types planned). The type determines the type-specific targets the list carries. +- Each list has a **name** and a **`Type`** (`Email` or `Sms`; SMS via Twilio shipped 2026-06-19, a Teams adapter was evaluated and dropped). The type determines the type-specific targets the list carries. - An `Email` list contains one or more **recipients**, each with a **name** and an **email address**. - Notification lists are defined and stored **centrally only** — they are **not deployed to sites** and have no local SQLite copy. Recipient resolution happens at central, at delivery time.