feat(claude): Claude spend alerts in Microsoft Teams (spec 030)#97
Conversation
Hourly digest + edge-triggered threshold breach and forecast-at-risk
cards posted to a Workflows incoming webhook on every successful
Anthropic cost sync. Off by default; enable via TEAMS_WEBHOOK_URL.
- New table `anthropic_alert_state` (idempotency ledger, one row per
workspace×month with nullable threshold timestamps + forecast bool)
- Auth-free `src/lib/anthropic/queries.ts` data layer reused by the
dashboard server actions and the cron-time evaluator
- Pure `forecastWorkspaceMonth()` (7-day trailing rate projection);
`loadCostHistory()` batches per-workspace history into one query
- Teams module: webhook POST with Retry-After-aware retry, Adaptive
Card v1.4 renderers (digest, breach, forecast, stale-data warning),
evaluator with fire-once-per-month threshold semantics
- 322 unit tests pass; verified end-to-end against a Neon DB branch
Spec: specs/030-claude-spend-teams-alerts/{plan,mockup,implementation-notes}.html
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR adds an opt-in Microsoft Teams alerting pipeline for Claude (Anthropic) API spend, posting Adaptive Card messages to a Teams channel at the end of each successful hourly cost sync. It fits into the existing Anthropic cost-sync subsystem by adding a best-effort post-sync evaluator plus an idempotency ledger to prevent repeated alert firing.
Changes:
- Introduces a Teams alerts module (
src/lib/teams/*) with card rendering, webhook posting (with retry), evaluation/diffing logic, and DB-backed idempotency state. - Extracts an auth-free Anthropic read layer (
src/lib/anthropic/queries.ts) so cron-time code (no NextAuth session) can reuse the same data as the admin-gated dashboard actions. - Adds schema + migration for
anthropic_alert_state, plus unit tests and Vitest configuration updates (including aserver-onlyshim).
Reviewed changes
Copilot reviewed 25 out of 27 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
vitest.config.ts |
Adds a server-only alias to a test shim so Vitest can import Next’s marker package. |
vitest.config.integration.mts |
Adds .env.local loading, server-only alias, and increases integration hook timeout. |
tests/unit/teams/webhook.test.ts |
Unit tests for Teams webhook POST + retry behavior and URL secrecy. |
tests/unit/teams/format.test.ts |
Unit tests for Teams card formatting helpers. |
tests/unit/teams/forecast-workspace.test.ts |
Unit tests for workspace-month forecast math and edge cases. |
tests/unit/teams/evaluator-diff.test.ts |
Unit tests for the alert diff state machine (threshold + forecast edges). |
tests/unit/teams/cards.test.ts |
Unit tests validating shape/content of rendered Adaptive Card payloads. |
tests/shims/server-only.ts |
Empty shim module used by Vitest aliasing for server-only. |
src/lib/teams/webhook.ts |
Implements fetch-based Teams Workflows webhook POST with retry/backoff and sanitized errors. |
src/lib/teams/types.ts |
Defines shared Teams alert types (envelope, inputs, evaluator diff/state). |
src/lib/teams/state.ts |
Reads/writes anthropic_alert_state as the idempotency ledger (upsert logic). |
src/lib/teams/format.ts |
Formatting helpers for currency/percent/relative-age text in cards. |
src/lib/teams/evaluator.ts |
Orchestrates loading data, computing diffs, posting cards, and persisting state. |
src/lib/teams/cards.ts |
Pure Adaptive Card renderers for digest/breach/forecast/stale variants. |
src/lib/sync/sources/anthropic-workspace.ts |
Hooks Teams evaluation into the end of a successful hourly sync (non-fatal). |
src/lib/env.ts |
Adds TEAMS_WEBHOOK_URL and TEAMS_DASHBOARD_BASE_URL env vars. |
src/lib/db/schema.ts |
Adds anthropic_alert_state table definition and indexes/check constraint. |
src/lib/db/migrations/meta/_journal.json |
Registers the new migration in Drizzle’s journal. |
src/lib/db/migrations/0019_bouncy_scourge.sql |
Creates the anthropic_alert_state table and indexes/check. |
src/lib/anthropic/queries.ts |
Adds auth-free DB queries for sync status, dashboard KPIs, workspace list, and cost history. |
src/lib/anthropic/forecast-workspace.ts |
Adds a pure workspace-month forecast function based on trailing daily run rate. |
src/actions/anthropic-global.ts |
Refactors admin-gated actions to delegate to the new auth-free query layer. |
specs/030-claude-spend-teams-alerts/plan.html |
Adds/updates the implementation plan for spec 030. |
specs/030-claude-spend-teams-alerts/mockup.html |
Adds the visual contract mockup for Teams card variants. |
specs/030-claude-spend-teams-alerts/implementation-notes.html |
Adds implementation notes documenting decisions/deviations/tradeoffs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| resolve: { | ||
| alias: { | ||
| "server-only": path.resolve(__dirname, "tests/shims/server-only.ts"), | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Good catch — fixed in 6130c98. Switched to path.dirname(fileURLToPath(import.meta.url)) and routed loadEnv() through the same derived path so it works regardless of CWD. The non-.mts vitest.config.ts keeps __dirname since the project has no "type": "module" in package.json (CJS), so it's safe there.
|
|
||
| const now = opts?.now ?? new Date(); | ||
| const month = format(now, "yyyy-MM"); | ||
| const dashboardBase = | ||
| env.TEAMS_DASHBOARD_BASE_URL || env.NEXTAUTH_URL || "http://localhost:3000"; |
There was a problem hiding this comment.
Good call — fixed in 6130c98. The evaluator now refuses to post when VERCEL_ENV indicates production-like and neither TEAMS_DASHBOARD_BASE_URL nor NEXTAUTH_URL is set; returns posted: 0, skipped: ["dashboard_base_url_missing"] so the operator sees the reason in cron logs. Local dev still falls back to http://localhost:3000 so the kill switch can be tested without extra env wiring.
Step-by-step for provisioning the Teams Workflows webhook, configuring TEAMS_WEBHOOK_URL on Vercel, verifying the wiring, and troubleshooting. Covers cadence expectations, idempotency guarantees, tuning knobs, the disable path, and a FAQ for common operator questions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Use fileURLToPath(import.meta.url) in vitest.config.integration.mts instead of __dirname (ESM-safe per Node spec — was relying on Vitest's esbuild __dirname polyfill). - Refuse to post Teams cards in production when neither TEAMS_DASHBOARD_BASE_URL nor NEXTAUTH_URL is set — avoids posting cards with broken http://localhost:3000 deep links. Local dev keeps the localhost fallback so the kill switch can be tested without extra env wiring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
forecastWorkspaceMonthflips a workspace from on_track → at_risk).TEAMS_WEBHOOK_URLin Vercel. The evaluator no-ops without it.src/lib/anthropic/queries.tsso the cron (no NextAuth session) can read the same data the dashboard does. UI access stays admin-gated.anthropic_alert_statetable — one row per (workspace, billing_month) — is the idempotency ledger.Docs
See
specs/030-claude-spend-teams-alerts/:how-to.html— operator guide: provision the Teams webhook, configure Vercel, verify, troubleshoot, tune, disable.mockup.html— visual contract: Teams chrome + the three card variants stacked.plan.html— full implementation plan with revisions reflecting self-review.implementation-notes.html— running log of deviations, tradeoffs, decisions.verification-dashboard.png— Chrome screenshot of the existing/claudedashboard rendering unchanged through the refactored server actions.Test plan
pnpm typecheck— cleanpnpm lint— cleanpnpm test— 322/322 unit tests pass (37 new across cards / format / evaluator-diff / webhook / forecast-workspace)/clauderenders unchanged after the server-action refactor (chrome-devtools MCP screenshot in spec folder)TEAMS_WEBHOOK_URL=https://httpbin.org/anything:posted=0 skipped=webhook_disabledposted=4(digest + 3 forecast edges)posted=1(digest only — idempotency proven, no re-fire)forecast_at_risk=true0019_bouncy_scourge.sqlreviewed bydrizzle-migration-revieweragent — additive new table, no FK refs, safe to applyformatCurrencyreuse,formatDistanceToNowreuse, etc.); 15 lower-value findings noted and deferredRollout
Backward-compatible. Schema migration is purely additive. Feature is silent until an operator (1) provisions a Workflows webhook in Teams, (2) sets
TEAMS_WEBHOOK_URLon Vercel Production — full steps inhow-to.html. Backout is just unsetting the env var.🤖 Generated with Claude Code