Skip to content

Commit adc7aef

Browse files
Merge branch 'main' into shayna-update-readme
2 parents b25d3b5 + 0542246 commit adc7aef

124 files changed

Lines changed: 9978 additions & 2348 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"enabledPlugins": {
3+
"csharp-lsp@claude-plugins-official": true,
4+
"typescript-lsp@claude-plugins-official": true
5+
}
6+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
name: Playwright E2E
2+
3+
permissions:
4+
contents: read
5+
6+
on:
7+
push:
8+
branches: ["main"]
9+
pull_request:
10+
branches: ["main"]
11+
12+
jobs:
13+
e2e:
14+
name: E2E Tests
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 15
17+
18+
services:
19+
mssql:
20+
image: mcr.microsoft.com/mssql/server:2022-latest
21+
env:
22+
ACCEPT_EULA: Y
23+
MSSQL_SA_PASSWORD: YourStrong@Passw0rd
24+
ports:
25+
- 1433:1433
26+
options: >-
27+
--health-cmd="/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P YourStrong@Passw0rd -Q 'SELECT 1' -C -b -o /dev/null || exit 1"
28+
--health-interval=10s
29+
--health-timeout=5s
30+
--health-retries=10
31+
--health-start-period=10s
32+
33+
steps:
34+
- name: Checkout code
35+
uses: actions/checkout@v4
36+
37+
- name: Determine state connector ref
38+
id: connector-ref
39+
run: |
40+
BRANCH="${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}"
41+
FALLBACK="${{ github.event_name == 'pull_request' && github.base_ref || 'main' }}"
42+
REPO_URL="https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository_owner }}/sebt-self-service-portal-state-connector.git"
43+
if git ls-remote --exit-code --heads "$REPO_URL" "refs/heads/${BRANCH}" 1>/dev/null 2>&1; then
44+
echo "ref=${BRANCH}" >> "$GITHUB_OUTPUT"
45+
else
46+
echo "ref=${FALLBACK}" >> "$GITHUB_OUTPUT"
47+
fi
48+
49+
- name: Checkout state connector
50+
uses: actions/checkout@v4
51+
with:
52+
repository: codeforamerica/sebt-self-service-portal-state-connector
53+
ref: ${{ steps.connector-ref.outputs.ref }}
54+
path: state-connector
55+
56+
- name: Setup .NET
57+
uses: actions/setup-dotnet@v5
58+
with:
59+
dotnet-version: "10.0.200"
60+
61+
- name: Setup pnpm
62+
uses: pnpm/action-setup@v4
63+
with:
64+
version: "10"
65+
66+
- name: Setup Node.js
67+
uses: actions/setup-node@v4
68+
with:
69+
node-version: "24"
70+
cache: "pnpm"
71+
72+
- name: Install dependencies
73+
run: pnpm install --frozen-lockfile --prefer-offline
74+
75+
- name: Build backend
76+
run: ./.github/workflows/scripts/build-backend.sh --configuration Release
77+
78+
- name: Install Playwright browsers
79+
run: cd src/SEBT.Portal.Web && pnpm exec playwright install --with-deps chromium
80+
81+
- name: Install Chrome for Pa11y
82+
run: cd src/SEBT.Portal.Web && pnpm exec puppeteer browsers install chrome
83+
84+
- name: Run Pa11y and Playwright E2E tests
85+
env:
86+
CI: true
87+
SKIP_WEB_SERVER: "1"
88+
STATE: dc
89+
NEXT_PUBLIC_STATE: dc
90+
ASPNETCORE_ENVIRONMENT: Development
91+
ConnectionStrings__DefaultConnection: "Server=localhost,1433;Database=SebtPortal;User Id=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=True;"
92+
JwtSettings__SecretKey: "ci-e2e-jwt-secret-at-least-32-characters-long"
93+
IdentifierHasher__SecretKey: "ci-e2e-identifier-hasher-key-32chars"
94+
Oidc__CompleteLoginSigningKey: "ci-e2e-oidc-signing-key-at-least-32-chars"
95+
UseMockHouseholdData: "true"
96+
run: |
97+
pnpm dev &
98+
echo "Waiting for server at http://localhost:3000..."
99+
for i in $(seq 1 90); do
100+
curl -sf --max-time 15 http://localhost:3000 > /dev/null && echo "Server ready" && break
101+
sleep 2
102+
done
103+
curl -sf --max-time 15 http://localhost:3000 > /dev/null || (echo "Server failed to start" && exit 1)
104+
echo "Running Pa11y accessibility tests..."
105+
pnpm ci:test:a11y
106+
echo "Running Playwright E2E tests..."
107+
pnpm ci:test:e2e
108+
109+
- name: Upload Playwright report
110+
uses: actions/upload-artifact@v4
111+
if: ${{ !cancelled() }}
112+
with:
113+
name: playwright-report
114+
path: src/SEBT.Portal.Web/playwright-report/
115+
retention-days: 7

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ FodyWeavers.xsd
438438

439439
# AI
440440
CLAUDE.local.md
441+
.claude/settings.local.json
441442

442443
# Developer-local workflow artifacts (branch context, PR reviews, screenshots)
443444
docs/.local/

.husky/pre-commit

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ cd "$PROJECT_ROOT"
2121

2222
# Check if there are backend changes in staged files
2323
has_backend_changes() {
24-
git diff --cached --name-only | grep -vE '^src/SEBT\.Portal\.Web' | grep -qE '^SEBT\.Portal\.sln$|^src/SEBT\.Portal\.|\.(csproj|sln)$' || return 1
24+
git diff --cached --name-only | grep -vE '^src/SEBT\.Portal\.Web' | grep -qE '^SEBT\.Portal\.sln$|^src/SEBT\.Portal\.|\.(csproj|sln)$|^test/' || return 1
2525
}
2626

2727
# Check if there are frontend changes in staged files
@@ -93,12 +93,20 @@ if [ "$BACKEND_CHANGES" = "true" ]; then
9393

9494
if command -v dotnet &> /dev/null; then
9595
# Verify code formatting matches .editorconfig (fails if changes needed)
96-
dotnet format SEBT.Portal.sln --verify-no-changes --verbosity quiet || {
96+
# Exclude sibling repos (state-connector, dc-connector, co-connector) that get
97+
# pulled in via conditional ProjectReference but live outside this repository.
98+
dotnet format SEBT.Portal.sln --verify-no-changes --verbosity quiet \
99+
--exclude ../sebt-self-service-portal-state-connector/ \
100+
--exclude ../sebt-self-service-portal-dc-connector/ \
101+
--exclude ../sebt-self-service-portal-co-connector/ || {
97102
log_error ".NET code formatting check failed - run 'dotnet format SEBT.Portal.sln' to fix"
98103
exit 1
99104
}
100-
101-
dotnet format SEBT.Portal.sln analyzers --verify-no-changes --verbosity quiet || {
105+
106+
dotnet format SEBT.Portal.sln analyzers --verify-no-changes --verbosity quiet \
107+
--exclude ../sebt-self-service-portal-state-connector/ \
108+
--exclude ../sebt-self-service-portal-dc-connector/ \
109+
--exclude ../sebt-self-service-portal-co-connector/ || {
102110
log_error ".NET analyzer checks failed - fix the reported issues"
103111
exit 1
104112
}
@@ -159,7 +167,10 @@ if [ "$BACKEND_CHANGES" = "true" ]; then
159167
cd "$PROJECT_ROOT"
160168

161169
if command -v dotnet &> /dev/null; then
162-
dotnet test --configuration Debug --verbosity quiet || {
170+
# Exclude SqlServer tests (Testcontainers/SQL Server) from pre-commit.
171+
# They run in CI but cause stack overflows locally when combined with
172+
# the rest of the suite due to Docker resource pressure under parallelism.
173+
dotnet test --configuration Debug --verbosity quiet --filter "Category!=SqlServer" || {
163174
log_error "Backend tests failed"
164175
exit 1
165176
}

.idea/.gitignore

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/.idea.SEBT.Portal/.idea/.gitignore

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/.idea.SEBT.Portal/.idea/aws.xml

Lines changed: 0 additions & 17 deletions
This file was deleted.

.idea/aws.xml

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# 9. State Backend Health Checks Report Degraded, Not Unhealthy
2+
3+
Date: 2026-03-17
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
State connector plugins register health checks that verify connectivity to their backend systems (DC's SQL database, CO's CBMS API). Initially these checks reported `Unhealthy` when the backend was misconfigured or unreachable. In ASP.NET Core's health check framework, the overall `/health` endpoint status is the worst individual check status — so a single `Unhealthy` check makes the entire endpoint report `Unhealthy`, which can cause container orchestrators to kill and replace the container.
12+
13+
The portal can still serve requests (login, static pages, cached data) when a state backend is down. Recycling the container won't fix a downstream outage and can cascade into a worse situation.
14+
15+
## Decision
16+
17+
State connector health checks always report `Degraded` — never `Unhealthy` — regardless of whether the issue is missing configuration or an unreachable backend. The structured JSON response from `/health` includes per-check descriptions and exception details, so monitoring and alerting can still distinguish between misconfiguration and connectivity failures.
18+
19+
### Alternatives considered
20+
21+
- **Report `Unhealthy` for connectivity failures, `Degraded` for missing config.** More semantically precise, but the operational consequence (container recycling) is undesirable in both cases.
22+
- **Have connectors report `Unhealthy` but remap to `Degraded` at the portal level.** ASP.NET Core's overall status is the worst individual status with no built-in remapping. A portal-side wrapper could intercept results, but adds complexity for the same operational outcome.
23+
- **Make the failure status configurable via appsettings.** Flexibility for future states, but adds a configuration knob that would likely be set once and forgotten — and increases maintenance burden for state partners. Can revisit if needed as more states onboard.
24+
25+
## Consequences
26+
27+
- The `/health` endpoint never returns `Unhealthy` due to a state backend issue, preventing aggressive container recycling.
28+
- Monitoring must inspect the per-check details (description, exception) in the JSON response rather than relying solely on the top-level status to detect backend outages.
29+
- The portal's own health (e.g., its database) can still report `Unhealthy` if needed in the future — this decision applies only to state connector checks.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# 9. Vendor-Agnostic Privacy-Aware Data Layer
2+
3+
Date: 2026-03-16
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
The portal needs analytics and observability to understand user behavior, measure feature adoption, and support operational monitoring. Multiple analytics vendors (e.g., Mixpanel, Google Analytics, Adobe Analytics) may be used across different state deployments, and vendor choices may change over time. Directly integrating vendor SDKs throughout the codebase would create tight coupling, make vendor switches expensive, and risk leaking PII to vendors that should not receive it.
12+
13+
We need a single, canonical source of truth for page metadata, user attributes, and tracked events that:
14+
15+
- Decouples application code from any specific analytics vendor.
16+
- Enforces privacy scoping so PII is only accessible to authorized consumers.
17+
- Supports multiple concurrent vendor integrations without code changes.
18+
- Is understandable and maintainable by state partner agencies after handoff.
19+
20+
## Decision
21+
22+
We adopt a **vendor-agnostic data layer** following the [W3C Customer Experience Digital Data Layer (CEDDL)](https://www.w3.org/2013/12/ceddl-201312.pdf) recommendations. The implementation lives in `src/SEBT.Portal.Web/src/lib/data-layer.ts` as a framework-agnostic TypeScript class.
23+
24+
**Canonical data structure** bound to `window.digitalData`:
25+
26+
- `page` — page-level metadata with `category` and `attribute` sub-objects.
27+
- `user` — user-level data with a `profile` sub-object.
28+
- `event[]` — append-only event log with `eventName`, `eventData`, `timeStamp`, and `scope`.
29+
- `privacy.accessCategories` — declared access scopes.
30+
- `initialized` — boolean flag indicating readiness.
31+
32+
**Privacy-aware scoping:**
33+
34+
- Each data element can be assigned one or more access scopes (e.g., `"default"`, `"analytics"`, `"marketing"`).
35+
- Page data is publicly readable by default (no scope restriction).
36+
- User data automatically receives `"default"` scope, restricting access unless explicitly broadened.
37+
- Scope inheritance walks the path hierarchy from specific to general, so child elements inherit parent scope restrictions.
38+
- Scope metadata is stored in a private `Map`, not on the data objects themselves.
39+
40+
**Loose coupling via DOM CustomEvents:**
41+
42+
- All mutations emit namespaced `CustomEvent`s on `document` (e.g., `digitalData:PageElementSet`, `digitalData:UserProfileSet`, `digitalData:EventTracked`).
43+
- Vendor integration bridges subscribe to these events and forward data according to their scope permissions.
44+
- A global `DataLayer:Initialized` event signals readiness.
45+
- An `eventTypes` object on the root provides type-safe event name constants for bridge consumers.
46+
47+
**React integration** uses a `DataLayerProvider` component that initializes the data layer once on mount via `useRef`, wrapped as the outermost provider in the app layout.
48+
49+
## Consequences
50+
51+
- **Application code** calls `digitalData.page.set(...)`, `digitalData.user.set(...)`, and `digitalData.trackEvent(...)` without knowing which vendors consume the data.
52+
- **Adding a new vendor** requires only a new event listener bridge — no changes to application code or the data layer itself.
53+
- **Removing a vendor** means removing its bridge. No application code changes.
54+
- **PII protection** is enforced at the data layer boundary. A vendor bridge for `"analytics"` scope cannot read user data that only has `"default"` scope.
55+
- **State partners** can configure different vendor bridges per deployment without modifying the core portal.
56+
- **Testing** is straightforward because the class is framework-agnostic and testable with jsdom.
57+
58+
## References
59+
60+
- W3C CEDDL specification: https://www.w3.org/2013/12/ceddl-201312.pdf
61+
- Implementation: `src/SEBT.Portal.Web/src/lib/data-layer.ts`
62+
- Tests: `src/SEBT.Portal.Web/src/lib/data-layer.test.ts`
63+
- Provider: `src/SEBT.Portal.Web/src/providers/DataLayerProvider.tsx`

0 commit comments

Comments
 (0)