feat: Microsoft Teams notifications + real-time PI Program Board - #1
Merged
Conversation
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
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
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>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 threeNotificationsemit paths.Settingstore behindcap-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).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 theSettingstore, behindcap-schedule-gated endpoints.ProgramBoardview (keeps the SignalR client out of the main PI chunk), pure board maths inpip/board.ts(unit-tested), and auseBoardRealtimehook. Dev proxy (/hubsws) + nginx WebSocket upgrade added.Security & compliance (ISO 27001 / NIST / GDPR)
access_tokenquery only for/hubs(the WS handshake can't carry a header). All writes stay behindcap-*REST checks + the audit log — the socket can't escalate past REST.GET /settingspayload.Verification
npm run build, fulleslint, and the unit suite (72 tests, +8 for the board) all pass locally.API (build · test)CI job (dotnet build+dotnet testonAtlas.sln) is the real gate — please watch it on this PR.Follow-ups (documented in ADR-0061)
PiObjective.IterationIdcolumn once a migration can be generated.🤖 Generated with Claude Code
https://claude.ai/code/session_01F1g44VLAeYdf3FAoXsXRu7
Generated by Claude Code