Skip to content

feat(claude): Claude spend alerts in Microsoft Teams (spec 030)#97

Merged
studert merged 3 commits into
mainfrom
worktree-ms-teams-report
May 21, 2026
Merged

feat(claude): Claude spend alerts in Microsoft Teams (spec 030)#97
studert merged 3 commits into
mainfrom
worktree-ms-teams-report

Conversation

@studert
Copy link
Copy Markdown
Member

@studert studert commented May 21, 2026

Summary

  • Post Adaptive Card alerts about Claude API spend into a Microsoft Teams channel via a Workflows incoming webhook, at the end of every successful hourly Anthropic cost sync.
  • Three card types: hourly digest (always, unless data is stale), threshold breach (fire-once-per-workspace-per-month at 80/100/120%), forecast at-risk (edge-triggered when forecastWorkspaceMonth flips a workspace from on_track → at_risk).
  • Off by default. Enable by setting TEAMS_WEBHOOK_URL in Vercel. The evaluator no-ops without it.
  • New auth-free data layer at src/lib/anthropic/queries.ts so the cron (no NextAuth session) can read the same data the dashboard does. UI access stays admin-gated.
  • New anthropic_alert_state table — 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 /claude dashboard rendering unchanged through the refactored server actions.

Test plan

  • pnpm typecheck — clean
  • pnpm lint — clean
  • pnpm test322/322 unit tests pass (37 new across cards / format / evaluator-diff / webhook / forecast-workspace)
  • Manual: dashboard at /claude renders unchanged after the server-action refactor (chrome-devtools MCP screenshot in spec folder)
  • Manual end-to-end against a Neon DB branch with TEAMS_WEBHOOK_URL=https://httpbin.org/anything:
    • kill switch (env unset) → posted=0 skipped=webhook_disabled
    • first run on clean state → posted=4 (digest + 3 forecast edges)
    • re-run → posted=1 (digest only — idempotency proven, no re-fire)
    • state ledger contains exactly the expected three rows with forecast_at_risk=true
  • Migration 0019_bouncy_scourge.sql reviewed by drizzle-migration-reviewer agent — additive new table, no FK refs, safe to apply
  • Three xhigh-effort review agents (reuse, quality, efficiency) → 9 high-value findings applied (N+1 query collapsed, state batch upsert, formatCurrency reuse, formatDistanceToNow reuse, etc.); 15 lower-value findings noted and deferred

Rollout

Backward-compatible. Schema migration is purely additive. Feature is silent until an operator (1) provisions a Workflows webhook in Teams, (2) sets TEAMS_WEBHOOK_URL on Vercel Production — full steps in how-to.html. Backout is just unsetting the env var.

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings May 21, 2026 14:09
@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ai-developer-hub Ready Ready Preview, Comment May 21, 2026 5:57pm

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 a server-only shim).

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.

Comment on lines +12 to +16
resolve: {
alias: {
"server-only": path.resolve(__dirname, "tests/shims/server-only.ts"),
},
},
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/lib/teams/evaluator.ts Outdated
Comment on lines +49 to +53

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";
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@studert studert merged commit 7708c0f into main May 21, 2026
7 checks passed
@studert studert deleted the worktree-ms-teams-report branch May 21, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants