From e4c6cb27ff9e0cf60bea6e03860533b6ac58f051 Mon Sep 17 00:00:00 2001 From: Alex Harrison Date: Mon, 5 Oct 2026 11:14:55 -0600 Subject: [PATCH] Coach alerts: document the hourly cap on email and push The portal guide, the Coach API and the Claude connector describe hourly_cap: on by default at one email and one push an hour per athlete, urgent alerts included; 1 to 12 an hour or off; held alerts go out together, none dropped; in-portal and webhook never limited; presets keep it; a full-replace write that leaves it out clears it at that scope. Co-Authored-By: Claude Opus 5.5 (1M context) Ci-From: linux Ci-Session: 1cf6b5b8-7b0a-4233-8f51-be4880ee968a --- coaching/roster/configuring-alerts.mdx | 1 + guides/coach-api.mdx | 11 +++++++---- guides/coach-connector.mdx | 4 +++- guides/mcp-integration.mdx | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/coaching/roster/configuring-alerts.mdx b/coaching/roster/configuring-alerts.mdx index 31dea78..171e2ab 100644 --- a/coaching/roster/configuring-alerts.mdx +++ b/coaching/roster/configuring-alerts.mdx @@ -46,6 +46,7 @@ When an athlete belongs to more than one group, Saturday does not pick a group a - **Urgent versus digest.** Real time for urgent items, a once-a-day bundle for the rest, one bundle per athlete per day. - **Channels.** In-portal, email, and mobile push. - **Quiet hours.** Hold non-urgent pings overnight, in your time zone. The default window is 22:00 to 07:00. +- **Hourly limit.** By default you get at most one email and one push an hour about each athlete, urgent alerts included. What the limit holds arrives together in one message when the hour frees; nothing is dropped. You can allow up to 12 an hour, or switch the limit off. Quiet hours and the daily digest still apply either way. In-portal alerts are never limited. - **Combinations.** Two-trigger AND rules, for example "ping me only when sodium is short and there is a symptom in the same session". Both legs must be session-level triggers; the sleep trend and the gone-quiet flag describe a window rather than a single session, so they cannot be combined. diff --git a/guides/coach-api.mdx b/guides/coach-api.mdx index e73a319..5db5e5a 100644 --- a/guides/coach-api.mdx +++ b/guides/coach-api.mdx @@ -245,7 +245,7 @@ Served from cache unless a newer session has landed or `refresh=true`. ## Configuration -Most of the Coach API is a configuration surface. It writes the same alert-rule and report-settings model the portal reads: channels, triggers, per-trigger thresholds, the overall/group/athlete scope hierarchy, cadence, quiet hours, two-trigger combinators, and AI-report defaults. The portal UI and the API are two views of one config model. +Most of the Coach API is a configuration surface. It writes the same alert-rule and report-settings model the portal reads: channels, triggers, per-trigger thresholds, the overall/group/athlete scope hierarchy, cadence, quiet hours, the hourly cap, two-trigger combinators, and AI-report defaults. The portal UI and the API are two views of one config model. **Scope precedence.** Config applies at one of three scopes: `overall` (the whole roster), `group` (a coach group), or `athlete` (one athlete). When resolving what an athlete sees, the most specific scope wins (athlete over group over overall). `scope_id` is required for `group` and `athlete`, and omitted for `overall`. @@ -255,7 +255,7 @@ Most of the Coach API is a configuration surface. It writes the same alert-rule `GET /v1/coach/config/notification-rules?scope=overall` reads the rules set at exactly that scope, not the merged resolution. A scope with no rules returns an empty rule set. -`PUT /v1/coach/config/notification-rules` replaces the rule set at a scope and answers `{"ok": true, "scope": "..."}`. It is an idempotent upsert: re-running with the same body is a no-op, so no per-request idempotency key is needed. A malformed rule set (an unknown trigger, channel, or cadence, a bad combinator, or quiet hours enabled without valid `HH:MM` times) returns `400 invalid_config`. +`PUT /v1/coach/config/notification-rules` replaces the rule set at a scope and answers `{"ok": true, "scope": "..."}`. It is an idempotent upsert: re-running with the same body is a no-op, so no per-request idempotency key is needed. A malformed rule set (an unknown trigger, channel, or cadence, a bad combinator, quiet hours enabled without valid `HH:MM` times, or an hourly cap outside 1 to 12) returns `400 invalid_config`. Every field you leave out is cleared at that scope, `hourly_cap` included, so the broader scope's value applies. ```bash curl -X PUT https://api.saturday.fit/v1/coach/config/notification-rules \ @@ -272,7 +272,8 @@ curl -X PUT https://api.saturday.fit/v1/coach/config/notification-rules \ "combinators": [ { "trigger_a": "under_fuel", "trigger_b": "symptom", "channel": "push", "cadence": "realtime" } ], - "quiet_hours": { "enabled": true, "start": "22:00", "end": "06:00", "tz": "America/Denver" } + "quiet_hours": { "enabled": true, "start": "22:00", "end": "06:00", "tz": "America/Denver" }, + "hourly_cap": { "enabled": true, "per_hour": 2 } } }' ``` @@ -285,11 +286,13 @@ curl -X PUT https://api.saturday.fit/v1/coach/config/notification-rules \ **Thresholds** are fractions of the as-used prescription. `urgent_threshold` sets the urgent band (`0.6` means below 60%). `amber_threshold` is optional and decouples this trigger's marker line from the shared cutoff; omit it to use the resolved concern cutoff. +**Hourly cap** limits the emails, and separately the pushes, that one athlete's alerts send you in any 60 minutes, urgent alerts included. `per_hour` is a whole number from 1 to 12; with no scope setting `hourly_cap`, it is on at 1 per hour. Alerts over the limit wait and go out together in one email (or one push) when the hour frees, so none is dropped. `"enabled": false` removes the limit; quiet hours and each rule's cadence still apply. In-portal alerts and webhook events are never limited. When an athlete is in two groups that set different caps, the looser one applies: off, or else the higher `per_hour`. The GET response carries `hourly_cap` only where that scope sets it. + **Combinators** are bounded two-trigger ANDs: both legs must be markers on the same session ("sodium short AND a cramp"). Exactly two distinct session-level triggers (`under_fuel`, `symptom`, `low_rating`, `hyponatremia_pattern`, `dial_down`), with no OR, NOT, or nesting. `sleep_trend` and `went_quiet` are window-level and cannot be combined. ### Presets -`POST /v1/coach/config/preset` applies a named starting point at a scope, which you can then tweak rule by rule. It answers `{"ok": true, "preset": "..."}`; an unknown preset returns `400 invalid_config`. +`POST /v1/coach/config/preset` applies a named starting point at a scope, which you can then tweak rule by rule. It leaves the scope's `hourly_cap` as it was. It answers `{"ok": true, "preset": "..."}`; an unknown preset returns `400 invalid_config`. ```bash curl -X POST https://api.saturday.fit/v1/coach/config/preset \ diff --git a/guides/coach-connector.mdx b/guides/coach-connector.mdx index d38d497..113957d 100644 --- a/guides/coach-connector.mdx +++ b/guides/coach-connector.mdx @@ -68,7 +68,7 @@ A Pro Coach or above additionally sees the tools below. Every athlete argument i |------|------|----------| | `get_notification_rules` | `scope?`, `scope_id?` | read the rules at one scope | | `set_notification_rules` | `rules`, `scope?`, `scope_id?` | replace the rules at a scope (idempotent upsert) | -| `apply_alert_preset` | `preset`, `scope?`, `scope_id?` | apply `hands_off` / `balanced` / `hands_on` | +| `apply_alert_preset` | `preset`, `scope?`, `scope_id?` | apply `hands_off` / `balanced` / `hands_on`; the scope's hourly cap stays as it was | | `get_report_settings` | `scope?`, `scope_id?` | read AI-report + concern settings | | `set_report_settings` | `settings`, `scope?`, `scope_id?` | upsert report window/focus + concern cutoffs | | `list_webhooks` | `active_only?` | list webhook endpoints (no secrets); `active_only` narrows to active ones | @@ -79,6 +79,8 @@ A Pro Coach or above additionally sees the tools below. Every athlete argument i Config writes are idempotent: `set_notification_rules` replaces the rule set at a scope, so re-running the same call is a no-op, and the MCP path needs no idempotency key. +The rules carry `hourly_cap` (`{ enabled, per_hour }`), the limit on emails, and separately pushes, that one athlete's alerts send the coach in any 60 minutes. `per_hour` runs from 1 to 12, and a value outside that range is refused; with no scope setting it, the cap is on at 1 per hour. Alerts over the limit go out together in one message when the hour frees, so none is dropped, and `enabled: false` removes the limit (quiet hours and each rule's cadence still apply). In-portal alerts and webhook events are never limited. Because `set_notification_rules` replaces the whole rule set, leaving `hourly_cap` out clears it at that scope; read the rules first and send it back to keep it. + Webhook delivery requires the Business or Enterprise tier. `register_webhook` succeeds below that line, but nothing is delivered until the tier qualifies. diff --git a/guides/mcp-integration.mdx b/guides/mcp-integration.mdx index 7b7eb89..fe1f4ce 100644 --- a/guides/mcp-integration.mdx +++ b/guides/mcp-integration.mdx @@ -112,7 +112,7 @@ On the Claude connector, `list_athletes` returns the signed-in athlete alone and `build_bottling_plan` takes `carb_g`, `sodium_mg`, `fluid_ml`, and `duration_min` from a prescription, plus optional `activity_id`, `activity_type`, `strategy`, `vessel_count`, `vessels`, and `products`, and returns the plan twice: as text in `content`, and as `structuredContent` carrying the `plan` object with a `createdAt` and `seq` election key, the `products` you passed, and the athlete's `saved` carriage context when one exists. `strategy` is `evenly`, `concentrate`, or `goldilocks` (the default, which balances the two). When the totals will not fit the vessels, the plan carries an escalation state: carry it more concentrated and top up with water, rather than a dead end. `record_bottling_choice` takes the committed `plan` object back, once, when the athlete settles on it. - **Coach tools.** When a **coach on Pro Coach tier or higher** (Pro Coach, Head Coach, Business, or Enterprise) connects via the [Claude connector](/guides/coach-connector), an additional set of roster and configuration tools appears: `get_roster`, `get_roster_digest`, `get_athlete_fueling_rollup`, `get_athlete_report`, `get_session_detail`, `get_notification_rules`, `set_notification_rules`, `apply_alert_preset`, `get_report_settings`, `set_report_settings`, `list_webhooks`, `register_webhook`, and `delete_webhook`. These are invisible to athlete-only users and to other partners, and tier is re-checked per request, so a lapsed coach loses them without an error. Every athlete argument is confined to the coach's roster. See the [Claude Connector guide](/guides/coach-connector) for the full coach tool catalog and the [Coach API](/guides/coach-api) for the equivalent REST surface. + **Coach tools.** When a **coach on Pro Coach tier or higher** (Pro Coach, Head Coach, Business, or Enterprise) connects via the [Claude connector](/guides/coach-connector), an additional set of roster and configuration tools appears: `get_roster`, `get_roster_digest`, `get_athlete_fueling_rollup`, `get_athlete_report`, `get_session_detail`, `get_notification_rules`, `set_notification_rules`, `apply_alert_preset`, `get_report_settings`, `set_report_settings`, `list_webhooks`, `register_webhook`, and `delete_webhook`. These are invisible to athlete-only users and to other partners, and tier is re-checked per request, so a lapsed coach loses them without an error. Every athlete argument is confined to the coach's roster. The notification rules include `hourly_cap`, the limit (1 to 12 an hour, default 1) on alert emails and pushes about one athlete. See the [Claude Connector guide](/guides/coach-connector) for the full coach tool catalog and the [Coach API](/guides/coach-api) for the equivalent REST surface. The webhook tools are the one place where being able to call a tool does not mean it will do anything. Registration succeeds on any coach tier and hands back a signing secret and an endpoint marked active, but concern events are delivered only to coaches on **Business or Enterprise**; delivery re-reads the tier each time, so a Pro Coach or Head Coach endpoint stays quiet.