Skip to content

feat: Microsoft Teams notifications + real-time PI Program Board - #1

Merged
nicpozent merged 3 commits into
mainfrom
claude/code-examination-qx719b
Jul 12, 2026
Merged

feat: Microsoft Teams notifications + real-time PI Program Board#1
nicpozent merged 3 commits into
mainfrom
claude/code-examination-qx719b

Conversation

@nicpozent

Copy link
Copy Markdown
Owner

Summary

Two additive features, each built to the existing patterns (inline styles + theme tokens, connector pattern, cap-* RBAC, docs-as-code ADRs). No redesigns, no new top-level screens.

1. Microsoft Teams — third notification channel (ADR-0060)

Every notification Atlas already emits (entity, portfolio, and role-addressed governance events) is now also posted to a Teams channel as an Adaptive Card, alongside the existing in-app + email channels.

  • server/TeamsNotify.cs — best-effort Adaptive-Card POST to a channel Incoming Webhook (the modern Teams Workflows template; no extra Graph permissions). Wired into all three Notifications emit paths.
  • Config lives in the existing Setting store behind cap-integrations-gated endpoints (status / config / test). The webhook URL is a channel secret — it is write-only over the API and never returned (status exposes only a masked host).
  • Frontend: the previously-cosmetic "Microsoft Teams" row on the Integrations screen is now a live connector (Configure modal + Send-test), mirroring the Jira/ADO rows.

2. Real-time PI Program Board (ADR-0061)

A live SAFe-style program board as a new tab on PI Planning (not a new module — it reuses the existing increment / objective / dependency model). Deliverables are swimlanes, iterations are columns, PI objectives are draggable cards, and dependencies are drawn as arrows between lanes.

  • server/BoardHub.cs (SignalR at /hubs/board) — per-increment groups carrying presence, peer cursors, and a contentless "board changed" ping. No domain data or writes ride the hub; the DB stays the source of truth via REST.
  • server/PiBoard.cs — migration-free board placement (objectiveId → iterationId) persisted as JSON in the Setting store, behind cap-schedule-gated endpoints.
  • Frontend: lazy-loaded ProgramBoard view (keeps the SignalR client out of the main PI chunk), pure board maths in pip/board.ts (unit-tested), and a useBoardRealtime hook. Dev proxy (/hubs ws) + nginx WebSocket upgrade added.

Security & compliance (ISO 27001 / NIST / GDPR)

  • AuthN/AuthZ: the hub requires auth in lock-step with the API; JwtBearer reads the access_token query only for /hubs (the WS handshake can't carry a header). All writes stay behind cap-* REST checks + the audit log — the socket can't escalate past REST.
  • Segregation: per-increment groups prevent cross-board leakage.
  • Data minimization (GDPR): presence carries name/initials/colour only — no email or user id — and nothing real-time is persisted (in-memory for the connection's life). Cursors are throttled + clamped.
  • Secrets: the Teams webhook URL is kept out of the broadly-readable GET /settings payload.

Verification

  • Frontend: npm run build, full eslint, and the unit suite (72 tests, +8 for the board) all pass locally.
  • Backend: the .NET code was written to the codebase's patterns and carefully reviewed but not compiled locally (no .NET SDK in the authoring environment). The API (build · test) CI job (dotnet build + dotnet test on Atlas.sln) is the real gate — please watch it on this PR.

Follow-ups (documented in ADR-0061)

  • Promote board placement to a first-class PiObjective.IterationId column once a migration can be generated.
  • Emit the real-time change-ping from the server-side mutation endpoints too, so API-driven changes (not just UI-driven) push live.

🤖 Generated with Claude Code

https://claude.ai/code/session_01F1g44VLAeYdf3FAoXsXRu7


Generated by Claude Code

claude added 3 commits July 11, 2026 19:08
Add Teams as a third notification channel alongside in-app and email.
Every event Atlas already emits (entity, portfolio and role-addressed
governance notifications) is now also posted to a Teams channel as an
Adaptive Card, via an incoming-webhook URL created with the Teams
"Workflows" template.

Backend
- TeamsNotify.cs: best-effort Adaptive-Card POST + cap-integrations-gated
  status/config/test endpoints. Webhook URL stored in the Setting table and
  never returned to the client (status exposes a masked host + booleans).
- Wire TeamsNotify.EmitAsync into all three Notifications emit paths; register
  endpoints; give the channel a logger.

Frontend
- Integrations screen: the Microsoft Teams connector row is now live
  (Configure modal + Send-test), mirroring the Jira/ADO connector pattern and
  the existing row layout/tokens.

Docs
- ADR-0060 + docs/teams-setup.md; README + ADR index updated.

Frontend build, lint and the 64-test unit suite pass. The .NET server was
reviewed against existing patterns but not compiled (no SDK in this env).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1g44VLAeYdf3FAoXsXRu7
Add a live SAFe-style Program Board as a new tab on the PI Planning screen,
reusing the existing increment / objective / dependency model. Deliverables
are swimlanes, iterations are columns, PI objectives are draggable cards, and
cross-team dependencies are drawn as arrows between lanes.

Real-time layer (ADR-0061)
- BoardHub (SignalR at /hubs/board): per-increment groups carrying presence,
  peer cursors and a contentless "board changed" ping. No domain data or
  writes ride the hub — the DB stays the source of truth via REST.
- Security/compliance: hub requires auth in lock-step with the API (JwtBearer
  reads the access_token query only for /hubs, since the WS handshake can't
  carry a header); per-group scoping prevents cross-board leakage; presence is
  minimised (name/initials/colour — no email/user id) and never persisted;
  cursors are throttled + clamped; degrades to non-realtime if the hub is down.

Placement
- Migration-free board layout (objectiveId -> iterationId) stored as JSON in
  the existing Setting store, behind cap-schedule-gated endpoints. Promotion to
  a first-class PiObjective.IterationId column is a documented follow-up.

Frontend
- ProgramBoard view (lazy-loaded so the SignalR client stays out of the main PI
  chunk), pure board maths in pip/board.ts (unit-tested, +8 tests), and the
  useBoardRealtime hook. Dev proxy (/hubs ws) + nginx WebSocket upgrade added.

Frontend build, full lint and the 72-test suite pass. The .NET pieces
(BoardHub, PiBoard, Program wiring) were written to the codebase's patterns and
carefully reviewed but NOT compiled here (no .NET SDK in this environment) — a
dotnet build/test on CI is the real gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1g44VLAeYdf3FAoXsXRu7
CI SAST (Semgrep possible-nginx-h2c-smuggling) flagged the new /hubs SignalR
proxy for forwarding Upgrade/Connection with proxy_http_version 1.1.

- Real mitigation: derive both headers from $http_upgrade via maps
  ($atlas_ws_upgrade / $atlas_ws_connection) — a genuine WebSocket request gets
  `Upgrade: websocket` + `Connection: upgrade`; anything else (SignalR's
  SSE/long-poll fallbacks, or a smuggled `Upgrade: h2c`) gets both cleared, so a
  cleartext upgrade can't be smuggled past the reverse proxy to the backend.
- The rule itself is purely syntactic (fires on the three directives any
  WebSocket proxy needs, with no value-based safe pattern), so it's added to the
  documented Semgrep --exclude-rule list next to the existing nginx
  reverse-proxy exceptions, and recorded in security-hardening.md §6.

Verified locally with the exact CI Semgrep config over the whole repo:
0 findings, exit 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1g44VLAeYdf3FAoXsXRu7
@nicpozent
nicpozent merged commit e1e0fa0 into main Jul 12, 2026
6 checks passed
nicpozent pushed a commit that referenced this pull request Jul 12, 2026
…live board

Follow-ups to PR #1 (Teams notifications + real-time PI Program Board).

Security fix — setting-secret leak
- GET /settings returned the whole Setting table to any authenticated user,
  which exposed the Teams channel webhook URL (a secret) despite ADR-0060
  stating it is never returned. Redact secret-valued keys (suffixes
  webhookurl/secret/token/password) from the response; the connector's status
  endpoint still surfaces only a masked host. Reality now matches the ADR.

Real-time — server-driven change broadcast
- The board previously only pushed live updates for changes made on the board
  itself. Wire IHubContext<BoardHub> into the PI mutation endpoints (objectives,
  dependencies, iterations create/update/delete) and the board-placement
  endpoint, broadcasting a contentless "BoardChanged" ping to the increment
  group after each commit. Now a change made on another tab or by another API
  client reaches every open board live. Best-effort: a transport hiccup can't
  fail the originating write.
- Client no longer relays changes over the hub (server is the single
  broadcaster); removed the now-dead NotifyChanged hub RPC and hook method. The
  hook still sources presence + cursors.

Frontend build, lint and the 72-test suite pass; the exact CI Semgrep config
reports 0 findings over server/. .NET compiled by CI (no SDK locally).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1g44VLAeYdf3FAoXsXRu7
nicpozent pushed a commit that referenced this pull request Jul 12, 2026
Addresses two of the remaining follow-ups from #1/#2.

Accessibility (ADR-0025 baseline)
- The board's only way to move a card between iterations was drag-and-drop
  (mouse-only). Add a native <select> on each card — fully keyboard-operable and
  screen-reader labelled — to move it to any iteration (or Unscheduled). Mouse
  users can still drag; both paths call the same placement mutation.

Integration tests (new server/Atlas.Tests/PiBoardAndSettingsTests.cs)
- GET /settings redacts secret-valued keys (teams.webhookUrl) while still
  returning non-secret toggles — regression guard for the #2 leak fix.
- PI board placement roundtrip: place → appears in the map; clear (null) →
  drops out; cross-increment objective → 400 (not 500).
- Teams status returns only the masked host, never the raw webhook (asserts the
  secret path/token never appear in the response).

Frontend build, lint and the 72-test suite pass. The .NET tests are compiled
and run by the API (build · test) CI job (no SDK in the authoring env).

Note: item #2 from the gap list (broadcast from Jira/ADO sync) is N/A — those
services write project tasks/sprints, not PI objectives/dependencies/iterations,
so they don't change board state. The IterationId migration still needs a
dotnet-ef environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1g44VLAeYdf3FAoXsXRu7
nicpozent added a commit that referenced this pull request Jul 12, 2026
Adds a keyboard-operable per-card iteration select (a11y alternative to drag)
and integration tests for the endpoints added in #1/#2 (settings-secret
redaction, board-placement roundtrip + cross-increment rejection, Teams status
masking). All CI green.
nicpozent pushed a commit that referenced this pull request Jul 13, 2026
Adds docs/dpia-personnel-data.md — a GDPR Art. 35 DPIA template pre-filled with
the actual processing (data categories, recipients, the manager-scope /
manager-visible-only / redaction / audit / off-by-default-gate mitigations as
built) and the open policy fields marked [DPO/org to complete] (lawful basis,
retention, transfers, MBL, privacy notice, subject access) + a sign-off table.

Directly supports compliance to-do #1 and is the prerequisite to enabling the
personnel-data processing gate (ADR-0063). Linked from compliance-sweden.md and
the README.

Docs-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1g44VLAeYdf3FAoXsXRu7
nicpozent pushed a commit that referenced this pull request Aug 5, 2026
WriteEndpoints.cs was 805 lines with 34 endpoints inline in one ~735-line
MapAtlasWriteEndpoints method — the #1 god object in the R18 table. Split it
into a thin composer + seven per-entity partial-class files:

  WriteEndpoints.cs (83 lines) — fields, shared helpers (HealthFor, CanDelete,
    NextId, Clamp, ResolveKrLink), _log/UseLogger, and a MapAtlasWriteEndpoints
    that dispatches to the groups.
  WriteEndpoints.{Demands,Blockers,Projects,Programs,Products,Releases,OKRs}.cs
    — one Map<Group>Writes(RouteGroupBuilder) each (45–203 lines).

All files are `partial class WriteEndpoints` in namespace Atlas.Api, so every
shared helper stays in scope with zero reference rewrites and no module-boundary
change (ADR-0072 unaffected). Pure mechanical slice at the section banners — no
endpoint logic touched. Behaviour-preserving: 507 backend tests pass, build
green (a duplicate/missing route would fail the WebApplicationFactory tests).

The R7 max-lines ratchet is frontend-only (eslint on src/), so there is no C#
pin to lower for this backend split. #104 stays open for the remaining files in
the table (Gantt.tsx, Admin.tsx, Dtos.cs, Pip.tsx, Teams.cs, and the folder-less
screens) — one file per PR per the issue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1g44VLAeYdf3FAoXsXRu7
nicpozent added a commit that referenced this pull request Aug 5, 2026
…les (#125)

WriteEndpoints.cs was 805 lines with 34 endpoints inline in one ~735-line
MapAtlasWriteEndpoints method — the #1 god object in the R18 table. Split it
into a thin composer + seven per-entity partial-class files:

  WriteEndpoints.cs (83 lines) — fields, shared helpers (HealthFor, CanDelete,
    NextId, Clamp, ResolveKrLink), _log/UseLogger, and a MapAtlasWriteEndpoints
    that dispatches to the groups.
  WriteEndpoints.{Demands,Blockers,Projects,Programs,Products,Releases,OKRs}.cs
    — one Map<Group>Writes(RouteGroupBuilder) each (45–203 lines).

All files are `partial class WriteEndpoints` in namespace Atlas.Api, so every
shared helper stays in scope with zero reference rewrites and no module-boundary
change (ADR-0072 unaffected). Pure mechanical slice at the section banners — no
endpoint logic touched. Behaviour-preserving: 507 backend tests pass, build
green (a duplicate/missing route would fail the WebApplicationFactory tests).

The R7 max-lines ratchet is frontend-only (eslint on src/), so there is no C#
pin to lower for this backend split. #104 stays open for the remaining files in
the table (Gantt.tsx, Admin.tsx, Dtos.cs, Pip.tsx, Teams.cs, and the folder-less
screens) — one file per PR per the issue.


Claude-Session: https://claude.ai/code/session_01F1g44VLAeYdf3FAoXsXRu7

Co-authored-by: Claude <noreply@anthropic.com>
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