mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-09-30 05:23:14 +00:00
* feat(web): adopt SHOC design system and shell layout (ADR 0003) Port shoc-frontend-new dev's design system with its CSS-variable single-token-source mechanism: - src/styles/theme.css: SHOC token file ported verbatim (Montserrat/ DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/ shadow/sidebar/header token layers); fonts self-hosted via @fontsource - src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows tuple, component overrides; MUI v9 slot renames expressed as class selectors); theme.ts is now a re-export - Shell: SHOC composition (sidebar column + sticky gradient topbar + scrolling main); sidebar 244px/76px collapse with brand header row, grouped nav, SHOC active treatment (white card + 3px accent bar); topbar 100-degree gradient, surface hamburger, gradient avatar pill - Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as sx; wordmark subtitle localized to PROPOSAL SYSTEM) - Login: SHOC auth-card treatment (centered 384px card on #f9fafb) - Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed; remaining hardcoded hexes replaced with tokens; lucide-react for shell/nav icons per SHOC convention Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots pixel-sampled against the extracted SHOC spec (all hard values exact, no blocking deviations). * feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes Closes WEB-M5 (web hand-duplicated wire types, standing drift risk): - shared/api-contracts: rewritten as the authoritative superset of the .NET DTOs (ProposalListItem/ProposalDetail with poNumber and submittedByName, line item requests, customers, pricing library, dashboard, audit, sites, auth, presigned upload, ApiProblem); stale Proposal/UpdateLineItemsRequest shapes removed - shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to every wire type via `satisfies z.ZodType<T>` (schema/type drift is now a compile error); separate entrypoint so type-only consumers (mobile) never pull zod - web: imports @proposal-system/api-contracts (file: dep + tsconfig paths + vite preserveSymlinks); all 7 lib/api modules re-export shared types so page imports stay stable; enum unions tightened (PricingLibraryPage form state now ServiceCategory-typed) - fix(web): customer create/update sent a singular `address` field the API silently dropped (contract is addresses: string[], CustomerDtos.cs) - addresses now round-trip, extra addresses preserved on edit - api: ProblemDetails responses carry a machine-readable top-level `code` (SHOC error-code vocabulary): ValidationFailed, InvalidStateTransition, NotFound, Unauthorized, InternalError; new BusinessRuleException(code, message) maps to 422 with its code; GlobalExceptionHandlerTests cover the full mapping (wire contract) Cross-checked .NET DTOs vs TS types vs zod schemas with the orchestrator scanner (Gemini): core domains consistent; internal-only DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos admin surface) intentionally uncovered. Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc, shared tsc all green. * feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep) * feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites) Additive-only: pages still use lib/api/* and constants/queryKeys.ts until the page-migration agents run. Each domain ships api.ts (HTTP moved from lib/api), types.ts (contract re-exports + view types), schemas.ts (contract schema re-exports + form schemas with toRequest mappers), and use-cases.ts (TanStack Query v5 hooks + hierarchical query keys, mirroring current page invalidations and toast-on-error behavior). Adds an explicit vite/vitest alias for the @proposal-system/api-contracts/schemas subpath (package has no exports map) plus a schema/mapper smoke test suite. * refactor(web): proposal pages on domain layer, proposal form on react-hook-form * refactor(web): admin workspace on domain layer * refactor(web): customer management on domain layer + react-hook-form * refactor(web): pricing library on domain layer + react-hook-form * refactor(web): dashboards on domain layer * test(web): domain use-case hook coverage * refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules - SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline SimilarProposal type replaced by domain/admin/types (identical shape); query key joins the admin hierarchical key space - LineItemEditor type imports -> domain/lineItems/types - Delete now-orphaned lib/api/{proposals,lineItems,customers, pricingLibrary,admin,sites}.ts, constants/queryKeys.ts, hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per domain README rule 5) Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the authed shell renders on domain hooks. * fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup) Correctness: - State-transition mutations now invalidate every cached view via invalidateProposalViews (detail + line items + lists + stats + admin dashboard) — approving no longer leaves a stale queue for the 5-minute staleTime - Presigned S3 PUT moved to proposals/api.ts with res.ok check — a rejected upload is no longer confirmed as uploaded - toCustomerRequest always sends contactEmail ('' clears); API create path normalizes empty->null to match the update path — customer emails can now be cleared from the UI - Shared Number-based numeric form fields (domain/shared/formFields): '12abc' no longer silently coerces to 12 in the pricing library - Customer create/update invalidate customersKeys.all so cached search autocompletes see new customers - AdminWorkspace clears dirty right after a successful implicit save, before approve — no false unsaved-changes prompt when approve fails - ProposalFormPage submit gate and missing-fields caption derive from ONE checks list (missing customer is now listed) - Empty states gated on !err in ProposalListPage/AdminDashboard — no contradictory error + 'no proposals' UI - VendorDataPanel migrated to useVendorProposals (kills the divergent ['vendorProposals', id] cache key and the inline apiClient query) - useCustomerList/usePricingLibraryList get keepPreviousData — no TablePagination out-of-range flash on page change Cleanup: - Dead speculative hooks removed (useCreate/BulkUpdate/DeleteLineItem, useUpdateProposal, useProposalHistory/Audit, lineItemRowFormSchema, toUpdateLineItemEntry); tests moved to the live save path (useSaveProposalWorkspace) - Shared useDebouncedValue hook replaces 4 drifted inline debounce copies (one leaked its timer on unmount, two hardcoded 300ms); DEBOUNCE_AUTOCOMPLETE=300 named - Fix: WEB-H5 / WEB-H6 finding-ID markers restored at the relocated onError handlers (CLAUDE.md traceability) - shared/api-contracts gains an exports map; /schemas resolver alias deduplicated from 3 copies to the tsconfig paths mapping Verify: tsc clean, vitest 51/51 (tests updated to pin the new invalidation/mapper behavior + new '12abc' rejection test), vite build OK, dotnet 166/166. * refactor(web): fold Redux auth/ui slices into SHOC-shape auth context + storage module Phase 4 tail of the SHOC-alignment plan. Matches SHOC's auth shape (lib/auth storage module + providers/ context split) while keeping the deliberate divergences: - sessionStorage, not localStorage (WEB-C1 stands; SHOC's localStorage is on the SHOULD-NOT-ALIGN list) - token acquisition stays in the auth pages (Cognito code exchange / dev-login) — the provider only owns session state - 401 interceptor clears storage directly (WEB-M2 behavior preserved; full-page redirect resets provider state) Sidebar open state moves to plain layout state in App passed down as props (SHOC (protected)/_layout.tsx pattern), keeping localStorage persistence. Drops @reduxjs/toolkit and react-redux. Tests: authSlice tests replaced by authStorage + AuthProvider suites (QA-C5 coverage preserved); client interceptor tests updated for the storage-based 401 path. 59 vitest green, tsc clean, vite build OK. Verified end-to-end headless: login redirect, seeded-session shell, sidebar toggle persistence, logout, expired/malformed token handling, RoleGuard bounce; recipe persisted as web/.claude/skills/verify. * fix(web): harden auth session teardown per /sh-security-review findings - AUTH-L1 (confirmed medium): logout() now clears the react-query cache — the singleton cache survived SPA logout, serving the previous principal's cached GETs to the next login in the same tab for up to staleTime with no server round-trip. - AUTH-L3 (confirmed low): isTokenValid decodes base64url before atob — valid Cognito JWTs containing '-'/'_' in the payload segment were misclassified as expired (login lockout/loop; inherited from the old authSlice). - AUTH-L2 (unverified, hardened anyway): 401 interceptor broadcasts AUTH_SESSION_CLEARED_EVENT so AuthProvider drops in-memory state synchronously, restoring the old Redux atomic-clear semantics. - INJ-1 (unverified, hardened anyway): Authorization header only set when the stored token is a string. Each fix pinned by a test; 63 vitest green, tsc clean. * docs: web stack row reflects auth-context refactor (Redux removed, MUI v9) * ci(web): Phase 5 — prettier check + Playwright smoke via org frontend workflow Converts the web CI job from ci-typescript-cdk.yaml (typecheck only) to ci-typescript-frontend.yaml: format:check, build (tsc -b included), vitest, and a Playwright chromium smoke. Folds the standalone Web Tests job into it (aggregator needs updated). Pure CI — no AWS secrets. The smoke (e2e/smoke.spec.ts) drives dev-login → dashboard shell → proposal list, plus the unauthenticated bounce, against a fully mocked API (pathname-anchored route interception — a '**/api/**' glob would swallow vite's /src/lib/api/* module URLs). Config mirrors SHOC's playwright.config.ts (port 4173, chromium, dev-server webServer). Prettier: singleQuote + printWidth 100 to match the existing codebase style; lint intentionally not added (no ESLint config yet — run-lint false, out of Phase 5 scope). rollback = revert this workflow file. * style(web): prettier format pass (mechanical) npx prettier --write . with the new .prettierrc (singleQuote, printWidth 100). No functional changes — enforced by format:check in CI from this PR on.
210 lines
10 KiB
Markdown
210 lines
10 KiB
Markdown
# Proposal System
|
|
|
|

|
|

|
|

|
|

|
|

|
|
|
|
Internal proposal management platform for Sea Haven Industries. Dispatchers submit proposal requests, AI generates draft line items from historical data via Bedrock RAG, admins review and approve in a pricing workspace, and the system produces branded PDFs for delivery.
|
|
|
|
## Architecture Overview
|
|
|
|
Monorepo with five primary services:
|
|
|
|
- **.NET 8 API** -- Clean Architecture REST API hosted on Lambda behind API Gateway (JWT-authorized) with Function URL (AWS_IAM) for internal access
|
|
- **React 19 Web** -- MUI v9 admin/dispatcher workspace served via CloudFront + S3
|
|
- **React Native Mobile** -- iOS-first field app for dispatchers (offline-capable)
|
|
- **Python Lambdas** -- PDF extraction, PDF generation, library ingestion, AI suggestions, Aurora pgvector bootstrap
|
|
- **CDK Infrastructure** -- Three TypeScript stacks managing all AWS resources
|
|
|
|
## Repository Structure
|
|
|
|
```
|
|
proposal-system/
|
|
├── api/ .NET 8 Web API (Lambda-hosted, EF Core + PostgreSQL)
|
|
├── web/ React 19 + MUI v9 + Vite frontend
|
|
├── mobile/ React Native 0.86 iOS app
|
|
├── lambdas/ Python 3.12 processing functions (arm64)
|
|
├── infra/ CDK TypeScript (3 stacks)
|
|
├── shared/ TypeScript API contracts (shared between web + mobile)
|
|
├── scripts/ Post-deploy and utility scripts
|
|
├── .github/ CI/CD workflows
|
|
└── docker-compose.yml
|
|
```
|
|
|
|
## Tech Stack
|
|
|
|
| Component | Technologies |
|
|
|---|---|
|
|
| API | .NET 8, ASP.NET Core, EF Core + Npgsql, FluentValidation, Cognito JWT, Amazon.Lambda.AspNetCoreServer |
|
|
| Web | React 19, TypeScript, MUI v9, Vite, TanStack Query, react-hook-form + zod, axios |
|
|
| Mobile | React Native CLI 0.86, React 19, React Native Paper, React Navigation, react-native-app-auth (PKCE), amazon-cognito-identity-js (SRP), Keychain, offline draft queue |
|
|
| Lambdas | Python 3.12, arm64, pdfplumber, reportlab, httpx, boto3 |
|
|
| Infrastructure | CDK TypeScript (aws-cdk-lib pinned exact, kept current by Dependabot) |
|
|
| AI/RAG | Bedrock Knowledge Base (Titan Embeddings v2), Aurora PostgreSQL + pgvector vector store (see ADR 0001), Claude Sonnet via Bedrock cross-region inference |
|
|
| Auth | Cognito User Pool + Google OAuth IdP (groups: dispatchers, admins, sysadmins) |
|
|
|
|
## AWS Resources
|
|
|
|
All resources are in **us-east-1** (account 328440206208).
|
|
|
|
| CDK Stack | Key Resources |
|
|
|---|---|
|
|
| `proposal-system-foundation` | Aurora PostgreSQL 15 Serverless v2 (RDS Data API, pgvector), S3 buckets, SQS queue + DLQ, Cognito user pool, Secrets Manager |
|
|
| `proposal-system-compute` | API Gateway HTTP API (JWT authorizer + access logging), .NET 8 API Lambda + Function URL (AWS_IAM), Python Lambdas (pdf-extract, pdf-generate, library-ingest, suggestions, aurora-pgvector-init bootstrap), Bedrock KB (Aurora pgvector store) |
|
|
| `proposal-system-frontend` | CloudFront distribution (S3 OAC) |
|
|
|
|
| Resource Type | Names |
|
|
|---|---|
|
|
| S3 Buckets | `proposal-system-uploads`, `proposal-system-generated`, `proposal-system-library`, `seahaven-ios-certificates` |
|
|
| SQS | `proposal-system-jobs` (720s visibility, SQS-managed encryption, reportBatchItemFailures) + `proposal-system-jobs-dlq` (SQS-managed encryption, message body filtering by jobType) |
|
|
| Secrets | `proposal-system/db-credentials`, `proposal-system/internal-api-key` |
|
|
|
|
## Local Development
|
|
|
|
### Prerequisites
|
|
|
|
- .NET 8 SDK
|
|
- Node.js 24+
|
|
- Python 3.12
|
|
- PostgreSQL 16 (via docker-compose or native)
|
|
|
|
### Database
|
|
|
|
```bash
|
|
docker compose up -d # starts PostgreSQL on port 5432
|
|
# database: proposalsystem, password: localdev
|
|
```
|
|
|
|
### API
|
|
|
|
```bash
|
|
cd api
|
|
dotnet restore
|
|
dotnet run --project src/ProposalSystem.Api
|
|
# runs on http://localhost:5000
|
|
```
|
|
|
|
In development mode (`DevMode=true` in appsettings.Development.json):
|
|
- JWT auth uses a local symmetric HMAC key (no Cognito required)
|
|
- S3 service returns fake presigned URLs
|
|
- SQS publisher logs messages without sending
|
|
|
|
### Web Frontend
|
|
|
|
```bash
|
|
cd web
|
|
npm install
|
|
npm run dev
|
|
# runs on http://localhost:5173, proxies /api to localhost:5000
|
|
```
|
|
|
|
When `VITE_COGNITO_CLIENT_ID` is not set, the login screen shows role-selector buttons for local development.
|
|
|
|
### Infrastructure
|
|
|
|
```bash
|
|
cd infra
|
|
npm install
|
|
npx cdk synth
|
|
```
|
|
|
|
## CI/CD
|
|
|
|
### CI (on pull request to main)
|
|
|
|
Six parallel jobs calling org reusable workflows:
|
|
|
|
| Job | Workflow | What it checks |
|
|
|---|---|---|
|
|
| .NET Build & Test | `ci-dotnet.yaml` | Restore, build, test the API solution (123 xUnit tests) |
|
|
| Web Frontend Check | `ci-typescript-frontend.yaml` | Prettier `format:check`, build (includes `tsc -b`), vitest suite, Playwright chromium smoke (dev-login → proposal list, API mocked) |
|
|
| Mobile Typecheck | `ci-typescript-cdk.yaml` | TypeScript typecheck for mobile |
|
|
| Python Lint | `ci-python-sam.yaml` | ruff check + format on lambdas/ |
|
|
| Python Tests | inline job | pytest suite (37 tests — pdf-generate, suggestions, library-ingest, internal API signing) |
|
|
| CDK Synth | `ci-typescript-cdk.yaml` | Synthesize CDK stacks (includes .NET publish) |
|
|
|
|
### Deploy (manual)
|
|
|
|
Auto-deploy on push to main is currently disabled (PR #167) — both deploy workflows run via `workflow_dispatch` from the Actions tab. `deploy.yaml` calls the `cd-cdk.yaml` reusable workflow:
|
|
1. Publishes .NET 8 API and Python Lambdas
|
|
2. Runs `cdk deploy --all`
|
|
3. Executes `scripts/post-deploy.sh` (builds web, syncs to S3, invalidates CloudFront)
|
|
|
|
Deploy uses OIDC role `githubdeploy-proposal-system`. Concurrency group prevents parallel deploys.
|
|
|
|
### Mobile Deploy
|
|
|
|
Workflow: `deploy-mobile.yaml` -- builds and uploads to TestFlight via `cd-mobile-ios.yaml` reusable workflow on `macos-26`.
|
|
|
|
Trigger: **manual only** (`workflow_dispatch`) while the app is pre-V1, to save macOS runner cost.
|
|
|
|
## Mobile iOS
|
|
|
|
The iOS app uses **Fastlane** with **match** for code signing. Certificates and profiles are stored in the `seahaven-ios-certificates` S3 bucket (versioning enabled, public access blocked).
|
|
|
|
Build and upload to TestFlight is handled by the `cd-mobile-ios.yaml` reusable workflow. Required secrets:
|
|
|
|
| Secret | Purpose |
|
|
|---|---|
|
|
| `AWS_DEPLOY_ROLE_ARN` | OIDC role for match S3 access |
|
|
| `MATCH_PASSWORD` | Decryption passphrase for signing assets |
|
|
| `ASC_KEY_ID` | App Store Connect API key ID |
|
|
| `ASC_ISSUER_ID` | App Store Connect issuer |
|
|
| `ASC_KEY_CONTENT` | App Store Connect API key (base64) |
|
|
|
|
## Authentication & Authorization
|
|
|
|
Two-layer auth architecture with defense-in-depth:
|
|
|
|
| Path | Authorizer | Authentication |
|
|
|---|---|---|
|
|
| External clients → API Gateway `/{proxy+}` | Cognito JWT authorizer (web + mobile client IDs) | .NET JWT middleware (ValidateAudience=true) |
|
|
| `/api/health` | None (public) | None |
|
|
| `/api/auth/callback`, `/api/auth/dev-login` | None (unauthenticated) | None (pre-auth endpoints) |
|
|
| Internal Lambdas → Function URL | AWS_IAM (grantInvokeUrl) | Internal API key (`X-Internal-Api-Key` header, value from Secrets Manager) |
|
|
|
|
**Role-based access:** Cognito groups (`dispatchers`, `admins`, `sysadmins`) map to API roles via `cognito:groups` claim. Dispatchers can only see their own proposals (ownership enforced in service layer). VendorProposals and GeneratedPdfs endpoints restricted to admins/sysadmins.
|
|
|
|
**Internal API key:** Python Lambdas call the .NET API via a Lambda Function URL with AWS_IAM auth (bypasses API Gateway JWT check). The `InternalApiKeyMiddleware` validates the `X-Internal-Api-Key` header and assigns the `admins` role to the synthetic identity. Lambdas cache the API key from Secrets Manager with a 5-minute TTL.
|
|
|
|
## Data Flow
|
|
|
|
1. Dispatcher submits proposal request (web or mobile)
|
|
2. API creates proposal record (with advisory-locked number generation), publishes SQS message
|
|
3. If vendor PDF attached: `pdf-extract` Lambda parses and structures data
|
|
4. Suggestions Lambda queries Bedrock KB for similar proposals, generates line items via Claude
|
|
5. Admin reviews/edits line items in pricing workspace
|
|
6. On approval: `pdf-generate` Lambda creates branded PDF
|
|
7. On send: `library-ingest` Lambda adds approved proposal to KB for future matching
|
|
|
|
Failed SQS messages are reported via `batchItemFailures` and retried up to 3 times before moving to the DLQ.
|
|
|
|
## Testing
|
|
|
|
186 tests across three stacks, all run in CI on every PR:
|
|
|
|
| Suite | Framework | Count | Coverage |
|
|
|---|---|---|---|
|
|
| .NET API | xUnit | 123 | State machine transitions, authorization attributes, middleware, validators, ProposalNumberGenerator, LineItemService state guards |
|
|
| Web | vitest | 26 | ProtectedRoute, RoleGuard, API client interceptor (401 logout, token attachment) |
|
|
| Python Lambdas | pytest | 37 | pdf-generate, suggestions, library-ingest handler contracts, internal API signing |
|
|
|
|
```bash
|
|
cd api && dotnet test # .NET tests
|
|
cd web && npm test # vitest
|
|
cd lambdas && python -m pytest # pytest
|
|
```
|
|
|
|
## Security
|
|
|
|
Hardening applied across all layers (see AUDIT-REPORT.md for full details):
|
|
|
|
- **Auth:** Cognito JWT validation with audience check, startup fails if auth not configured, DevMode gated to `IsDevelopment()`
|
|
- **API:** FluentValidation on all DTOs, generic error responses (no stack traces or config leaks), structured audit logging with before/after diffs
|
|
- **Function URL:** AWS_IAM auth + internal API key (two-layer defense)
|
|
- **Infrastructure:** S3 `enforceSSL` + `BLOCK_ALL`, SQS managed encryption, Aurora in private subnets, Cognito optional TOTP MFA, API Gateway access logging
|
|
- **Lambdas:** Prompt injection sanitization, PDF size limits, numeric validation on AI suggestions, S3 key sanitization, idempotent SQS processing
|
|
- **CI/CD:** OIDC (no long-lived credentials), org reusable workflows (`@main`), `--require-approval broadening` on local deploys
|
|
- **Web:** sessionStorage for tokens (not localStorage), error boundaries, role guards on all admin routes, 401 interceptor clears auth state
|