diff --git a/.env.example b/.env.example index 79e9b88..a2a3ef0 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ POSTGRES_DB=platform JWT_SECRET=replace-with-at-least-32-random-characters INTERNAL_SERVICE_SECRET=replace-with-a-random-service-secret ACCESS_TOKEN_EXPIRATION_SECONDS=900 +DISPLAY_TIME_ZONE=UTC # Optional immutable images for local Compose. Leave unset to use latest. ACCOUNT_IMAGE=ghcr.io/microsserv52/account:latest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88da2d4..56b7e01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,8 +23,12 @@ jobs: steps: - uses: actions/checkout@v7 with: - submodules: recursive + submodules: false fetch-depth: 0 + - name: Checkout backend submodules + working-directory: ${{ github.workspace }} + run: >- + git -c url.https://github.com/.insteadOf=git@github.com: submodule update --init --recursive -- api/account api/auth api/gateway api/order api/order-service - uses: actions/setup-java@v5 with: distribution: temurin @@ -51,7 +55,10 @@ jobs: steps: - uses: actions/checkout@v7 with: - submodules: recursive + submodules: false + - name: Checkout backend submodules + run: >- + git -c url.https://github.com/.insteadOf=git@github.com: submodule update --init --recursive -- api/account api/auth api/gateway api/order api/order-service - name: Validate Compose topology run: docker compose config - name: Build application images diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8f2eb75..f6c8f11 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,7 +23,16 @@ jobs: steps: - uses: actions/checkout@v7 with: - submodules: recursive + submodules: false + - name: Checkout backend submodules + run: >- + git -c url.https://github.com/.insteadOf=git@github.com: submodule update --init --recursive -- api/account api/auth api/gateway api/order api/order-service + - uses: actions/setup-java@v5 + if: matrix.language == 'java-kotlin' + with: + distribution: temurin + java-version: '25' + cache: maven - uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} diff --git a/.github/workflows/deploy-eks.yml b/.github/workflows/deploy-eks.yml index 79824ea..6a0ad13 100644 --- a/.github/workflows/deploy-eks.yml +++ b/.github/workflows/deploy-eks.yml @@ -14,7 +14,10 @@ jobs: steps: - uses: actions/checkout@v7 with: - submodules: recursive + submodules: false + - name: Checkout backend submodules + run: >- + git -c url.https://github.com/.insteadOf=git@github.com: submodule update --init --recursive -- api/account api/auth api/gateway api/order api/order-service - uses: actions/setup-java@v5 with: distribution: temurin diff --git a/.gitmodules b/.gitmodules index 58215ba..316e681 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,12 @@ [submodule "api/account"] path = api/account url = https://github.com/Microsserv52/account.git +[submodule "web/landing"] + path = web/landing + url = git@github.com:Microsserv52/landing.git +[submodule "web/store"] + path = web/store + url = git@github.com:Microsserv52/store.git +[submodule "web/admin"] + path = web/admin + url = git@github.com:Microsserv52/admin.git diff --git a/README.md b/README.md index cf18a86..75d3034 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,31 @@ uv run zensical serve Start with the [project overview](docs/project/project.md) or the [getting started guide](docs/development/get_started.md). +## Web applications and local preview + +The [frontend documentation](docs/frontend/index.md) covers three +independent Next.js applications integrated as Git submodules: `web/landing` at +`/`, `web/store` at `/store`, and `web/admin` at `/admin`, on one domain. +The agreed UI is English, with a shared modern brand and a public catalog. + +This is an **educational mock store: no real payments or transactions take +place, and no goods are shipped**. Authenticated users create persisted +demo orders. The store includes an online-only PWA manifest; Grafana Faro and +Umami are integrated but disabled until their deployment values are agreed. +Each app exports static files and has a Dockerfile. Production CDN hosting and +per-app environment values will be discussed before deployment; frontend +Kubernetes deployment is deferred. + +To test the frontend independently of the missing Product/Exchange images: + +```bash +docker compose -f compose.frontend-demo.yml up --build -d +``` + +Open `http://localhost:8081/`, `/store/` or `/admin/`. This separate local-only +mode visibly labels disposable API fixtures. See the [local testing guide](docs/frontend/delivery.md) +for fixture inputs, real-service mode and verification limits. + ## CI/CD and environment variables Each service repository owns lint, format, tests, JaCoCo quality gates, CodeQL, diff --git a/api/account b/api/account index 9ee1144..edb0d71 160000 --- a/api/account +++ b/api/account @@ -1 +1 @@ -Subproject commit 9ee1144be81e524c158ccde11109880672ac1d46 +Subproject commit edb0d715ab0a42277e3a45bb04a395143693e7ad diff --git a/api/gateway b/api/gateway index 2fdcc63..70c7325 160000 --- a/api/gateway +++ b/api/gateway @@ -1 +1 @@ -Subproject commit 2fdcc639b86b763de84da3fcb1c3991ac7da2dc2 +Subproject commit 70c73256e2ca4ab94ac8e9f3dacc9616b38a6566 diff --git a/compose.frontend-demo.yml b/compose.frontend-demo.yml new file mode 100644 index 0000000..d75db14 --- /dev/null +++ b/compose.frontend-demo.yml @@ -0,0 +1,44 @@ +name: microsserv52-frontend-demo + +services: + landing: + build: + context: ./web/landing + args: + NEXT_PUBLIC_APP_ENV: fixture + store: + build: + context: ./web/store + args: + NEXT_PUBLIC_APP_ENV: fixture + admin: + build: + context: ./web/admin + args: + NEXT_PUBLIC_APP_ENV: fixture + gateway: + image: python:3.13-slim + command: + [ + "python", + "/qa/serve.py", + "--host", + "0.0.0.0", + "--port", + "8080", + "--fixtures", + "--api-only", + ] + volumes: + - ./tests/frontend:/qa:ro + nginx: + image: nginx:1.28-alpine + ports: + - "127.0.0.1:${FRONTEND_DEMO_PORT:-8081}:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - landing + - store + - admin + - gateway diff --git a/compose.yml b/compose.yml index 412773e..e09ba26 100644 --- a/compose.yml +++ b/compose.yml @@ -69,14 +69,43 @@ services: environment: - LOG_LEVEL=info - JWT_SECRET=${JWT_SECRET:-change-me-in-development-use-a-long-random-secret} + - SESSION_COOKIE_SECURE=false + - SESSION_COOKIE_PATH=/api deploy: replicas: 3 depends_on: - account - auth + landing: + image: microsserv52-landing:local + build: + context: ./web/landing + args: + NEXT_PUBLIC_API_BASE_URL: /api + NEXT_PUBLIC_DISPLAY_TIME_ZONE: ${DISPLAY_TIME_ZONE:-UTC} + hostname: landing + + store: + image: microsserv52-store:local + build: + context: ./web/store + args: + NEXT_PUBLIC_API_BASE_URL: /api + NEXT_PUBLIC_DISPLAY_TIME_ZONE: ${DISPLAY_TIME_ZONE:-UTC} + hostname: store + + admin: + image: microsserv52-admin:local + build: + context: ./web/admin + args: + NEXT_PUBLIC_API_BASE_URL: /api + NEXT_PUBLIC_DISPLAY_TIME_ZONE: ${DISPLAY_TIME_ZONE:-UTC} + hostname: admin + nginx: - image: nginx:alpine + image: nginx:1.28-alpine hostname: nginx ports: - 8080:80 @@ -84,3 +113,6 @@ services: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - gateway + - landing + - store + - admin diff --git a/docs/development/documentation.md b/docs/development/documentation.md index ef0ef89..c4a224a 100644 --- a/docs/development/documentation.md +++ b/docs/development/documentation.md @@ -22,6 +22,7 @@ has already been implemented. | [`infrastructure/`](../infrastructure/) | AWS, EKS, deployment and costs | | [`operations/`](../operations/) | Observability, performance and load testing | | [`services/`](../services/) | API contracts and service reference | +| [`frontend/`](../frontend/index.md) | Landing/store/admin content, architecture, packages, PWA, telemetry, local testing and verification | ## Commands diff --git a/docs/development/get_started.md b/docs/development/get_started.md index 1cf9df7..3255b0e 100644 --- a/docs/development/get_started.md +++ b/docs/development/get_started.md @@ -48,7 +48,7 @@ The local edge is Nginx on `http://localhost:8080`. Service containers are replicated in the Compose topology and the gateway remains the only application entry point exposed through the proxy. -For MicroK8s or EKS, see the [Kubernetes deployment guide](../../k8s/README.md). +For MicroK8s or EKS, see the [Kubernetes deployment guide](https://github.com/Microsserv52/platform/blob/main/k8s/README.md). The gateway is exposed on port `8080` by the compose configuration. The repository snapshot currently contains the Order API and documentation diff --git a/docs/frontend/architecture.md b/docs/frontend/architecture.md new file mode 100644 index 0000000..562c2df --- /dev/null +++ b/docs/frontend/architecture.md @@ -0,0 +1,119 @@ +--- +icon: lucide/network +--- + +# Frontend architecture + +## Source and runtime ownership + +`web/landing`, `web/store` and `web/admin` are independent Git submodules. Each +owns a Next.js App Router application, strict TypeScript configuration, npm +lockfile, tests, static-export Dockerfile and CI workflows. No source imports +cross a submodule boundary and no JavaScript monorepo or module federation +runtime is introduced. Shared design/API conventions are documented centrally. + +Each app uses `src/app` for routes, `src/components` for feature and shared UI, +`src/lib` for schemas/API access, and `tests/` for browser checks and fixtures. +The platform owns Compose, Nginx, the submodule commits and integration docs. +Gateway/Account own the backend changes. Kubernetes frontends are deferred. + +## One domain, static zones + +| Public path | Destination | Behavior | +| ---------------------------------------------------------------------- | -------------- | -------------------------------------------------------------- | +| `/` and landing pages | Landing export | Root assets at `/_next/*` | +| `/store/` | Store export | Fixed `/store` base path; all assets/manifests remain under it | +| `/admin/` | Admin export | Fixed `/admin` base path; independent assets | +| `/api/*` | Gateway | Nginx strips `/api` only; authenticated responses use no-store | +| Legacy `/auth`, `/accounts`, `/products`, `/orders`, `/exchange` paths | Gateway | Existing Bearer API clients remain supported | + +Exact `/store` and `/admin` requests redirect to their slash form; prefix +matching cannot capture `/storefront`. Nginx serves actual exported paths and +returns a zone-specific 404 rather than rewriting unknown URLs to a shell. +Cross-zone links are ordinary anchors and load the destination app. Next.js +routing inside the store is used after demo order creation. + +All apps use `output: 'export'`, `trailingSlash: true` and unoptimized static +images. There are no server actions, BFFs or runtime Next.js session handlers. +Product/order IDs are validated query parameters to statically exported detail +pages. Public catalog data is fetched at runtime; the initial export does not +require an available Product service or embed stale demo product data. + +The approach follows official [static export](https://nextjs.org/docs/app/guides/static-exports), +[basePath](https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath) +and [multi-zone](https://nextjs.org/docs/app/guides/multi-zones) guidance. The +future CDN needs equivalent path-to-origin behavior, trailing-slash/404 rules +and an API origin. Choose those with the owner at deployment time. + +## API-owned browser sessions + +| Gateway endpoint | Purpose | +| --------------------- | -------------------------------------------------------------------------------------- | +| `GET /session/csrf` | Return the Spring CSRF token/header; establish its cookie | +| `POST /session/login` | Call Auth, set the access token in an HttpOnly cookie, return 204 without a token body | +| `GET /session` | Validate the token and fetch the authenticated Account profile | +| `DELETE /session` | Clear the session cookie; no refresh/revocation capability is claimed | + +Through the public proxy these endpoints are `/api/session/*`. The host-only +`ms52-session` cookie has path `/api`, SameSite=Lax and HttpOnly. It is Secure +by default; local HTTP Compose explicitly sets `SESSION_COOKIE_SECURE=false`. +Its lifetime follows Auth's access-token expiry. The frontend never stores or +reads JWTs in localStorage, sessionStorage or a JavaScript cookie. + +Spring Security's cookie CSRF repository and default request-token handler +protect cookie-authenticated writes and login. Every frontend mutation obtains +a token first. The Gateway request wrapper turns the cookie into a Bearer +header for token validation and downstream routing; explicit Bearer clients +retain their existing behavior. Upstream login/profile requests have timeouts. +The signing key stays in backend secret configuration. + +Gateway maps the signed `roles` claim to authorities, requires ROLE_ADMIN for +Product writes and account list/detail/deletion, and blocks public internal +credential routes. Only GET product reads are anonymous. Account enforces its +own admin list/detail/deletion rules and exposes `GET /accounts/me` for the +authenticated user. Administrator/self deletion is rejected; account deletion +is not exposed in the frontend. Admin provisioning is an operator action, +never a registration input. + +The route guard is presentation only. Tests verify backend role enforcement, +CSRF and an actual CSRF-cookie/login/profile exchange with local upstream +fixtures. Logout clears the cookie/client caches and notifies other tabs. +Receiving tabs use TanStack's [query reset](https://tanstack.com/query/latest/docs/framework/react/reference/classes/QueryClient) +to notify active subscribers and refetch session state, so an open private view +does not retain the previous profile after another tab signs out. +An already-issued token remains valid until expiry if copied elsewhere; there +is no fabricated refresh/revocation endpoint. + +## State and optimization + +TanStack Query owns API reads/mutations and schema-checked responses. Query +keys isolate private order data by identity; session changes reset cached data. +Reads have bounded retries/cancellation, mutations never retry automatically. +The cart stores only validated IDs/positive integer quantities under +`ms52-cart-v1`, synchronizes with browser storage and revalidates catalog data +before submission. Order receives IDs/quantities and calculates authoritative +prices. An uncertain submission directs users to order history before retry. + +Search/sort operate locally on the unpaginated Product array. React deferred +rendering keeps input responsive; it is not called network debounce. TanStack +Table v9 uses explicit sorting/pagination features and 10-row pages. No virtual +list is added without evidence of a large-list rendering problem, and no +background queue is needed for the current flows. New server search/pagination, +debounce or queue packages require a separate contract/approval. + +## Remaining service prerequisites + +- Product/Exchange source and usable local images are absent. Verify Product + accepts anonymous GET, enforces its own write permissions and matches the + documented id/name/price/unit payload before real-service acceptance. +- The checked-in Order returns `productId` in items and rejects non-USD totals. + The frontend uses that payload and USD only. +- Exchange route naming differs between docs and Gateway; currency UI is deferred. +- Order creation lacks documented idempotency; the frontend prevents duplicate + clicks and automatic retries but cannot promise exactly-once order creation. +- All-orders administration, account editing/role changes, product editing, + product media/stock/categories and financial reporting require new contracts. + +The fixture-only Docker mode is for UI verification and never proves these +real-service prerequisites. Backend and frontend release commits must be +reviewed in their owning repositories before integration pointers are promoted. diff --git a/docs/frontend/delivery.md b/docs/frontend/delivery.md new file mode 100644 index 0000000..d8e43af --- /dev/null +++ b/docs/frontend/delivery.md @@ -0,0 +1,117 @@ +--- +icon: lucide/list-checks +--- + +# Local testing and delivery + +## Full backend integration mode + +From the platform root: + +```bash +git submodule update --init --recursive +cp .env.example .env +docker compose up --build -d +``` + +The local entry point is `http://localhost:8080/`, with `/store/` and `/admin/` +on that origin. The three frontend services each build from their own Dockerfile +and serve their static export with Nginx. This mode uses Gateway/Auth/Account/ +Order and the configured real Product/Exchange images. Those last two images +are external prerequisites and are not fabricated by frontend initialization. + +Use backend images built from the reviewed Gateway/Account commits when testing +the new browser endpoints. A submodule pointer does not replace an already +published service image. The mainline Compose topology also requires its +database/Auth/Order images and their normal environment configuration. The +workspace may carry additional local infrastructure work; the frontend review +branch deliberately keeps those unrelated commits separate. + +The Compose edge uses Docker's DNS resolver and dynamically resolves upstream +addresses, so recreated frontend containers do not retain stale routing. +This requires the pinned Nginx 1.28 image; see the +[official upstream documentation](https://nginx.org/en/docs/http/ngx_http_upstream_module.html#resolve). +Set `DISPLAY_TIME_ZONE` before building to choose the display zone. Public +frontend values are baked into static exports and require a rebuild to change. + +## Disposable frontend fixture mode + +For UI-only testing without missing backend images, use the separate file: + +```bash +docker compose -f compose.frontend-demo.yml up --build -d +docker compose -f compose.frontend-demo.yml ps +docker compose -f compose.frontend-demo.yml down +``` + +Open `http://localhost:8081/`, `/store/` or `/admin/`. This dedicated project binds +the endpoint to localhost and displays a **Local API fixtures** notice. +`FRONTEND_DEMO_PORT` can select another local port. Do not combine this file with +production deployment or call these fixtures live-service integration evidence. + +Only in this fixture mode, use `visitor@example.test` or `admin@example.test` +with password `fixture-only`. These are canned test inputs, not real accounts +or committed production credentials. Products and orders are disposable +in-memory records that reset when the fixture service restarts. The fixture +server in `tests/frontend/serve.py` is test infrastructure, not an application +backend, security implementation or substitute for the missing services. + +To review already-built exports without Docker: + +```bash +python3 tests/frontend/serve.py --fixtures --port 4173 +``` + +The optional Python server is loopback-only by default and requires each app's +`out/` directory. Docker is the persistent local preview used for handoff. + +## Frontend repository checks + +Use Node.js 24 and `npm ci` in each repository. The official Next.js and +Playwright generators were used. Each app exposes `format:check`, `lint`, +`typecheck`, `test`, `test:e2e`, `build` and a local pre-commit configuration that +calls the same installed npm tools without downloading hook packages. + +CI runs formatting, lint, types, Vitest, static builds and Chromium desktop/mobile +Playwright flows. Browser tests use explicit API fixtures; backend tests verify +real access-control/session code separately. CodeQL configuration is included; +private-repository execution depends on the organization's GitHub security +entitlement. The owner deferred private CodeQL setup: after enabling the required +GitHub feature, set repository variable `ENABLE_CODEQL=true` to opt in, then run +the workflow. A skipped CodeQL job is not a completed scan. +The image workflow publishes only immutable `sha-` tags for +local/integration container use. It does not deploy to a CDN or Kubernetes. + +Platform Java/CodeQL/EKS workflows initialize only the public backend submodules. +They do not try to clone the private frontend repositories with the platform's +repository-scoped token. Each private frontend runs its own checks. A future +cross-repository frontend integration job needs an owner-configured GitHub App +or read credential; no broader credential or permission has been added here. + +## Real administrator setup + +New registrations always receive ROLE_USER. There is no public promote-to-admin +endpoint or frontend role-editing control. In an explicitly local/non-production +database, an authorized operator may promote a chosen demo account after +registration. For example, connect with your local PostgreSQL tooling and use +its bound-variable support to set that known account's roles to ROLE_ADMIN. +Do not publish credentials or add an automatic production administrator seed. +Record the operator action in deployment evidence when real environments are used. + +## Release and acceptance + +- Review frontend/backend source branches in their owning repositories first. +- Pin fetchable reviewed submodule commits in the platform; do not pin unpushed + local-only commits in a shared integration release. +- Run real Product/Exchange contract checks, public reads and restricted writes + before claiming full backend integration. Confirm order ownership and USD totals. +- Use [verification evidence](verification.md) to distinguish static/unit/fixture + checks from production acceptance. Preserve the mock-store notices throughout. +- Ask the owner about CDN hosting and each app's environment values at deployment + time. Agree API origin/cookie path, TLS, cache/404 rules, telemetry policy and + rollback together. No frontend Kubernetes deployment is included now. + +Record CI runs, app/backend commit IDs, container tags, environment and rollback +artifacts in final release evidence. Keep secrets out of docs and public build +values. Update the organization profile and disclose AI-assisted source/docs per +the course's [delivery requirements](../project/delivery.md). diff --git a/docs/frontend/dependencies.md b/docs/frontend/dependencies.md new file mode 100644 index 0000000..46403a2 --- /dev/null +++ b/docs/frontend/dependencies.md @@ -0,0 +1,98 @@ +--- +icon: lucide/package +--- + +# Dependencies and official initialization + +The owner approved npm, MUI/Emotion integration/icons, TanStack Query/Table, +React Hook Form with Zod/resolvers, Faro SDK/tracing, Prettier, Vitest and +Playwright, in addition to the requested Next.js/React/TypeScript/Tailwind stack. +No additional runtime library has been introduced beyond that approval. + +The official `create-next-app@16.4.5` initialized each application with +TypeScript, ESLint, Tailwind, App Router, src directory and npm. Official +`create-playwright@1.17.139` initialized browser tooling. Pre-commit was already +installed on the workstation; its official sample/configuration guidance was +used for local hooks that call existing npm checks and install no hook packages. + +Node.js 24 is declared in `.nvmrc` and engines. The generator originally supplied +Node 20 types, which conflicted with current Vitest; matching Node 24 types +resolved the conflict without force/legacy-peer-deps. Each repository commits +its own npm-generated lockfile. Runtime additions use patch ranges and lockfiles; +framework versions supplied by the generator are explicit. + +## Responsibilities + +- MUI owns controls/dialogs and Emotion's App Router cache; Tailwind owns utility + styling. CSS layers place MUI before utilities. System/Georgia fonts avoid a + font network dependency. +- Zod validates API responses, stored cart data and forms; TypeScript checks code. + React Hook Form owns form state; TanStack Query owns server state. +- TanStack Table v9 supplies sorting/pagination with explicit selected features. + Virtualization, Pacer and use-debounce are not installed without a measured need. +- Native fetch, AbortController, URL, React state and Intl cover transport, + cancellation, cart/UI state, currency and timezone-aware display. No time library. +- Faro SDK/tracing is configurable; Umami uses its official script via next/script. +- Playwright tests exercise labeled fixtures; Vitest checks contracts/API behavior. +- iron-session was removed from the proposal: static hosting uses Gateway sessions. + +## Resolved direct packages + +| Package | Locked version | Repositories | +| ----------------------------- | -------------- | --------------------- | +| `@emotion/cache` | `11.14.0` | landing, store, admin | +| `@emotion/react` | `11.14.0` | landing, store, admin | +| `@emotion/styled` | `11.14.1` | landing, store, admin | +| `@grafana/faro-web-sdk` | `2.13.0` | landing, store, admin | +| `@grafana/faro-web-tracing` | `2.13.0` | landing, store, admin | +| `@hookform/resolvers` | `7.0.5` | store, admin | +| `@mui/icons-material` | `9.5.0` | landing, store, admin | +| `@mui/material` | `9.5.0` | landing, store, admin | +| `@mui/material-nextjs` | `9.5.0` | landing, store, admin | +| `@playwright/test` | `1.65.0` | landing, store, admin | +| `@tailwindcss/turbopack` | `4.3.5` | landing, store, admin | +| `@tanstack/react-query` | `5.107.1` | landing, store, admin | +| `@tanstack/react-table` | `9.3.1` | admin | +| `@types/node` | `24.19.2` | landing, store, admin | +| `@types/react` | `19.3.0` | landing, store, admin | +| `@types/react-dom` | `19.3.0` | landing, store, admin | +| `babel-plugin-react-compiler` | `1.0.0` | landing, store, admin | +| `eslint` | `9.39.5` | landing, store, admin | +| `eslint-config-next` | `16.4.5` | landing, store, admin | +| `next` | `16.4.5` | landing, store, admin | +| `prettier` | `3.9.10` | landing, store, admin | +| `react` | `19.3.0` | landing, store, admin | +| `react-dom` | `19.3.0` | landing, store, admin | +| `react-hook-form` | `7.91.0` | store, admin | +| `tailwindcss` | `4.3.5` | landing, store, admin | +| `typescript` | `5.9.3` | landing, store, admin | +| `vitest` | `5.1.0` | landing, store, admin | +| `zod` | `4.6.7` | landing, store, admin | + +## Audit and conditional additions + +The initial audit reported eight moderate **development-tooling** findings +through eslint-config-next/typescript-eslint's minimatch dependency, associated +with [GHSA-g3hv-75wg-m249](https://github.com/advisories/GHSA-g3hv-75wg-m249). +The advisory's fix moves to minimatch 11; npm proposes a framework-config major +change. No automatic force upgrade is performed. This is recorded for a reviewed +tooling update; the runtime export contains no Node tooling. Do not claim a clean +full dependency audit until this is resolved. + +21st.dev was used for visual research, not source copying. A chosen third-party +component requires license/accessibility review and approval of its added +packages. Offline caching, source-map CLI/upload, charting, localization, time +arithmetic, a shared component package or commercial MUI X also need separate +approval and documentation. + +## Official references + +- [Next.js generator](https://nextjs.org/docs/app/api-reference/cli/create-next-app) +- [MUI/Next.js](https://mui.com/material-ui/integrations/nextjs/) and + [MUI/Tailwind](https://mui.com/material-ui/integrations/tailwindcss/tailwindcss-v4/) +- [TanStack Query](https://tanstack.com/query/latest/docs/framework/react/overview) + and [Table](https://tanstack.com/table/latest/docs/overview) +- [React Hook Form](https://react-hook-form.com/get-started), + [resolvers](https://github.com/react-hook-form/resolvers), [Zod](https://zod.dev/) +- [Playwright](https://playwright.dev/docs/intro), [Vitest](https://vitest.dev/guide/), + [Prettier](https://prettier.io/docs/install), [pre-commit local hooks](https://pre-commit.com/#repository-local-hooks) diff --git a/docs/frontend/design.md b/docs/frontend/design.md new file mode 100644 index 0000000..ebe8394 --- /dev/null +++ b/docs/frontend/design.md @@ -0,0 +1,61 @@ +--- +icon: lucide/palette +--- + +# Design direction + +antislop is applied during implementation, as requested. The owner authorized +defaults and reference research; these are agent-selected defaults for review, +not a claim of a supplied brand specification. No optional antislop skills or +persistent AGENTS.md changes are needed for this work. + +Reading this as: an educational storefront for students and demo visitors, +using an editorial catalog language, ENERGY 2 / RHYTHM 2 / MOTION 1. +Admin uses ENERGY 1 / RHYTHM 1 / MOTION 1 for predictable repeated tasks. + +## Visual decisions + +| Choice | Reason | +| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| Existing platform mark with Microsserv52 wordmark | Preserve the actual project identity rather than inventing a new logo | +| Warm paper background, dark ink text and deep rust accent | Separate browsing surfaces from actions while giving the demo a recognizable catalog identity | +| System sans-serif body and Georgia editorial headings | Keep controls familiar and readable, give landing/product headings contrast, and avoid a new font dependency or network request | +| Product cards with visible names, units and USD prices | The Product contract supplies those fields; the catalog remains useful without invented photography | +| Neutral initials/text tile where imagery is absent | Make missing product media honest rather than creating unsupported product claims | +| Compact, stable admin sidebar and tabular data | Repeated catalog/account tasks need predictable locations rather than decorative dashboard charts | +| Generous landing introduction, denser store grid and restrained admin tables | Density follows the visitor's task rather than repeating one landing-page template | +| Small corner radii and borders; elevation only for overlays | Distinguish interactive layers without making every surface float | +| Light/dark/system theme with explicit contrast checks | Support visitor preferences and verify every shipped theme | +| Hover/focus feedback, no ornamental motion | Keep attention on browsing, validation and demo order review | +| Menu, theme and destructive-action Material icons | Identify the navigation drawer, color preference and delete action rather than decorate content | +| Plain section labels above editorial headings | Give catalog/demo context without inventing promotional status badges | + +## Reference research + +- [MUJI storefront](https://www.muji.us/): observed its prominent search, + catalog-first navigation and clear hierarchy. Do not reproduce branding, + marketing popups, photography or sales claims. +- [MUI dashboard](https://mui.com/material-ui/getting-started/templates/dashboard/): + observed sidebar, breadcrumbs and table organization. Do not copy its sample + users, analytics counts, promotional cards or charts. +- [21st.dev commerce component search](https://21st.dev/community/components?q=ecommerce%20product%20cards%20search%20navigation): + reviewed product-card, catalog-grid and search/navigation previews for + composition. Implement with the approved MUI/Tailwind stack; no third-party + component source or extra runtime dependency is copied. +- [antislop core](https://raw.githubusercontent.com/miqdadbadjuber/anti-slop/main/antislop.md): + governs design review and verification. UI compliance is assessed on the + running applications, not inferred from this document. + +## Content and verification + +Use “Browse demo catalog”, “Add to demo cart” and “Create demo order” for actions. +Make the mock-store notice visible throughout: no real payments or transactions +and no shipment. Use only real API data or explicitly labeled test fixtures. +No invented testimonials, performance statistics, revenue or inventory. + +Every page needs one clear focal point, valid navigation, functional controls +and empty/loading/error states. Verify mobile overflow, minimum 44px touch +targets, keyboard/focus behavior, reduced motion, media dimensions and both +themes. Record actual build results and control-by-control browser evidence in +the delivery report. Lighthouse/field performance are “not measured” unless +actual measurements are captured. diff --git a/docs/frontend/index.md b/docs/frontend/index.md new file mode 100644 index 0000000..c0fe492 --- /dev/null +++ b/docs/frontend/index.md @@ -0,0 +1,55 @@ +--- +icon: lucide/panels-top-left +--- + +# Frontend applications + +Three independent Next.js repositories are integrated as Git submodules and +implemented for static CDN export. The local Docker setup serves them on one +origin. **This is an educational mock store: no real payments or transactions +occur, and no goods are shipped.** Demo orders are API records, not purchases. + +## Agreed decisions + +- English UI, shared modern branding and the existing project logo. +- Public catalog browsing; authentication to create or read own demo orders. +- Next.js, TypeScript, MUI/Emotion, Tailwind and the approved TanStack/form/test tools. +- antislop applied during implementation; reference research and defaults authorized. +- Each app has a Dockerfile for local testing; no frontend Kubernetes manifests. +- Production will use CDN hosting with separate per-app environment values. + Ask the owner about provider, origin routing and variables before deployment. +- Additional libraries always require owner approval. No date library, queue + framework, animation stack or 21st.dev runtime dependency was added. + +## Sites and content + +| Repository / submodule | Path | Implemented experience | +| -------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Microsserv52/landing` / `web/landing` | `/` | Demo introduction, live catalog preview, platform explanation, privacy/diagnostic controls and documentation links | +| `Microsserv52/store` / `web/store` | `/store/` | Public search/sort catalog, product details, guest cart, login/register, authenticated review, demo order creation/history/details and read-only own account | +| `Microsserv52/admin` / `web/admin` | `/admin/` | Role-gated overview with API-derived counts, searchable/sortable/paginated products/accounts, create/delete product dialogs, account detail dialog and configured observability links | + +Detail URLs use `/store/product/?id=` and `/store/order/?id=`. +These work for arbitrary runtime IDs without a server-rendered Next.js route. +Admin accounts are read-only in the UI. No product editing, all-orders admin +view, stock, shipping, payment fields or revenue reports are invented. + +Every screen has appropriate loading, failure and empty/access states. Shopping +copy uses “Add to demo cart” and “Create demo order”; confirmation says that no +payment was taken and no goods will be shipped. Product images are clearly +unavailable placeholders because the API has no imagery contract. + +## Documentation + +| Page | Contents | +| --------------------------------------------- | ------------------------------------------------------------------------------------- | +| [Design](design.md) | Defaults, visual decisions, real UI references and antislop | +| [Architecture](architecture.md) | Static zones, API-owned sessions, authorization and current contract limits | +| [Dependencies](dependencies.md) | Approved packages, exact resolved versions and official setup | +| [PWA and observability](pwa-observability.md) | Online-only store installation, Faro/Umami behavior and environment map | +| [Local development and delivery](delivery.md) | Real-service versus fixture Docker modes, commands, CI and remaining integration work | +| [Verification](verification.md) | Actual checks, browser evidence and limits | + +The implementation is pushed on task branches for review. Source +repositories are private. No CDN deployment or production telemetry endpoint +has been configured. diff --git a/docs/frontend/pwa-observability.md b/docs/frontend/pwa-observability.md new file mode 100644 index 0000000..0d6b86b --- /dev/null +++ b/docs/frontend/pwa-observability.md @@ -0,0 +1,88 @@ +--- +icon: lucide/activity +--- + +# PWA and observability + +## Online-only Store PWA + +The store exports `/store/manifest.webmanifest` with distinct application ID, +start URL and scope `/store/`, standalone display, theme colors and a scalable +icon based on the existing platform mark. Landing/admin do not register root or +admin workers. There is **no service worker or offline caching**: private API +responses, credentials, account data and orders are never put in worker caches, +and writes are never replayed in the background. + +Installation is browser-dependent and requires HTTPS in deployment (localhost +is suitable for development). The browser's install/add-to-home-screen action +is used; there is no misleading cross-platform install button. Device-specific +installation still needs acceptance on the chosen deployment. Offline cart +storage does not mean offline checkout: orders require an online authenticated +API. See [Next.js PWA guidance](https://nextjs.org/docs/app/guides/progressive-web-apps). + +## Grafana Faro + +The approved SDK and tracing packages initialize once in the browser when +`NEXT_PUBLIC_ENABLE_TELEMETRY=true`, a Faro URL is configured and the visitor has +not opted out or enabled Do Not Track. App identity is landing/store/admin with +environment and release version. Import/init failures do not block the UI. + +Console capture is disabled. The before-send hook drops arbitrary logs/events, +removes page query strings/user metadata, strips measurement context/action data +and sends generic browser errors without raw messages, context or stack traces. +This deliberately trades stack detail for conservative initial redaction. +Source maps are not uploaded and no Faro CLI/key is added. + +Trace propagation is limited to the same-origin public catalog endpoint. Private +session/accounts/orders and individual product URLs are ignored, and traces +from private/query-bearing pages are dropped. Backend end-to-end trace +continuation is **not claimed** until its backend instrumentation is verified. +See the official [Faro Next.js guide](https://grafana.com/docs/grafana-cloud/observe-and-act/monitor-applications/frontend-observability/get-started/instrument-nextjs/). + +## Umami + +Umami uses its official script through `next/script` after interactive loading, +without an npm wrapper. It is gated by the same telemetry setting, configured +script URL/website ID, Do Not Track and the browser opt-out. Admin usage +analytics are excluded. Automatic tracking is disabled; one manual pageview is +sent per pathname change/script readiness. Private store route names normalize +to `/store/private`; raw search values, UUID query parameters, email and account +identifiers are not supplied. + +Events are named `demo_store_opened`, `demo_cart_item_added`, +`demo_order_created` and `demo_order_failed`. The only property supplied is the +application name. Creation is recorded after a successful API response, not on +button click. These are demo events, never payment/revenue records. +See [Umami tracker configuration](https://docs.umami.is/docs/tracker-configuration). + +The landing `/privacy/` page explains local cart/session behavior and optional +telemetry, and offers a persistent browser opt-out. Reload after opting out to +stop an already-loaded collector. Endpoints/hosting/retention and policy must +be chosen with the owner before collection is enabled; no compliance claim or +production collection is fabricated. + +## Per-app public build variables + +Each frontend has an `.env.example`, and each Dockerfile declares matching +build arguments. No public value is a secret. CDN build environments must +supply each application's values independently. + +| Variable | Purpose | +| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| `NEXT_PUBLIC_API_BASE_URL` | Defaults to `/api`; future CDN origin routing decided later | +| `NEXT_PUBLIC_DISPLAY_TIME_ZONE` | IANA timezone; native Intl handles display | +| `NEXT_PUBLIC_APP_ENV`, `NEXT_PUBLIC_APP_VERSION` | Environment and immutable release identifier | +| `NEXT_PUBLIC_ENABLE_TELEMETRY` | Defaults to false | +| `NEXT_PUBLIC_FARO_URL` | Approved browser collector URL | +| `NEXT_PUBLIC_UMAMI_SCRIPT_URL`, `NEXT_PUBLIC_UMAMI_WEBSITE_ID`, `NEXT_PUBLIC_UMAMI_DOMAINS` | Approved script, website and allowed hostnames | +| `NEXT_PUBLIC_GRAFANA_DASHBOARD_URL`, `NEXT_PUBLIC_UMAMI_DASHBOARD_URL` | Non-secret dashboard links, never embedded dashboard credentials | + +Changing public variables or base paths requires rebuilding the static export. +Gateway uses backend-only `AUTH_URL`, `ACCOUNT_URL`, `JWT_SECRET`, +`SESSION_COOKIE_SECURE` and `SESSION_COOKIE_PATH`. Production uses Secure cookies; +only the explicit local HTTP setup disables that cookie flag. + +Before deployment, ask the owner to choose CDN provider, public/API origins, +per-app variables, Faro/Umami hosting, allowed script/connect origins, retention +and any collection policy. Do not create production resources or Kubernetes +frontends as part of local delivery. diff --git a/docs/frontend/verification.md b/docs/frontend/verification.md new file mode 100644 index 0000000..eeafccb --- /dev/null +++ b/docs/frontend/verification.md @@ -0,0 +1,238 @@ +--- +icon: lucide/clipboard-check +--- + +# Frontend verification evidence + +Implementation review on 2026-11-09. This report covers static applications, +disposable browser fixtures and focused backend tests. It does not certify a +production deployment or integration with the unavailable Product/Exchange images. + +## Automated checks + +| Check | Observed result | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| All three frontends | Formatting, ESLint, TypeScript, five Vitest tests per app and static export passed | +| Local pre-commit hooks | Formatting, lint, types and unit/contract tests passed in each frontend | +| Docker | All three Dockerfiles built successfully; fixture Compose serves `/`, `/store/`, `/admin/` and `/api/` on localhost:8081 | +| Local proxy | `nginx -t` passed; Docker DNS resolution was added after rebuilding exposed stale upstream addresses; privacy routing and logo retrieval passed after correction | +| Gateway | `./mvnw -B -Djava.version=24 verify`: eight tests passed, including the real CSRF cookie/header exchange, HttpOnly login cookie and profile lookup; configured coverage threshold passed | +| Account | `./mvnw -B -Djava.version=24 verify`: four tests passed; own profile, restricted account reads/deletes and administrator deletion guard covered; configured coverage threshold passed | +| Runtime package audit | `npm audit --omit=dev --audit-level=moderate` passed with no production dependency findings | +| Full package audit | Eight moderate development-tool findings remain through ESLint's minimatch dependency; see [dependencies](dependencies.md) | + +Java's normal repository target remains 25. Local backend checks explicitly used +the installed JDK 24 override. Maven compilation and repository coverage checks +are the existing backend checks; no formatter or pre-commit configuration is +present in those repositories. No new Java dependency was introduced. + +Browser CI uses Chromium desktop and Pixel 5 profiles against exported files and +explicit API fixtures. See the review-branch workflow runs for exact results. +Local interactive checks below used the in-app browser rather than claiming +that a locally unavailable Playwright browser installation ran. + +| Repository | Review commit | Passing frontend CI | +| ---------- | ------------- | ------------------------------------------------------------------------------------------------------------- | +| Landing | `18eb438` | [4 browser cases and 5 unit tests](https://github.com/Microsserv52/landing/actions/runs/42581984618) | +| Store | `ecbf084` | [8 browser cases and 5 unit tests](https://github.com/Microsserv52/store/actions/runs/42581976576) | +| Admin | `3aeeb18` | [4 browser cases and 5 unit tests](https://github.com/Microsserv52/admin/actions/runs/42581985563) | +| Gateway | `70c7325` | [Java 25 tests, coverage and CodeQL passed](https://github.com/Microsserv52/gateway/actions/runs/42580966815) | +| Account | `edb0d71` | [Java 25 tests, coverage and CodeQL passed](https://github.com/Microsserv52/account/actions/runs/42580973577) | + +The linked runs exercise the final pinned application commits. The store suite +includes desktop/mobile two-tab sign-out regression coverage: active queries +reset and the other tab stops displaying the old private profile. + +The first landing/store CI run exposed an ambiguous alert locator caused by +Next.js route announcements. The tests were scoped to application feedback +without removing assertions; the full relevant workflows then passed. +All 15 application routes returned HTTP 200 through the final local proxy. +Store manifest scope/start URL and all page titles were also checked there. +The documentation build completed with no issues and both Compose files passed +configuration validation. Backend automation initializes only backend submodules +so private frontend checkout does not break the public platform checks. + +Platform [Java/Compose integration](https://github.com/Microsserv52/platform/actions/runs/42581744578) +and [Java/Actions CodeQL](https://github.com/Microsserv52/platform/actions/runs/42581744441) +passed after correcting workflow YAML, the checkout working directory and the +Java analysis toolchain. The isolated platform review excludes pre-existing +local infrastructure commits; those remain on `feature/frontend-applications-workspace`. + +The private frontend CodeQL queries ran, but GitHub rejected result uploads +because Advanced Security is not enabled. The owner explicitly chose to keep +the repositories private and configure this later. Private CodeQL is therefore +opt-in via the repository variable `ENABLE_CODEQL=true` after the entitlement +and scanning setup are enabled. A skipped job does not establish a clean scan. +Automatic build/type/unit/browser checks remain enabled. + +Coordinated draft reviews: [platform #6](https://github.com/Microsserv52/platform/pull/6), +[landing #1](https://github.com/Microsserv52/landing/pull/1), +[store #1](https://github.com/Microsserv52/store/pull/1), +[admin #1](https://github.com/Microsserv52/admin/pull/1), +[gateway #3](https://github.com/Microsserv52/gateway/pull/3), +[account #3](https://github.com/Microsserv52/account/pull/3) and +[organization profile #2](https://github.com/Microsserv52/.github/pull/2). + +## Interactive browser record + +The earlier temporary export preview used localhost:4173; the final Docker +preview uses localhost:8081. Both expose explicitly labeled disposable fixtures. +Observed actions below describe the shipped control patterns; repeated product +cards share the same components. Fixtures are never evidence of real backend +security, persistence or authorization. + +| Control or route | Observed behavior | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Landing catalog CTA | Opened `/store/` with the shared mock-store notice | +| Product preview/detail links | Opened the runtime-ID product page with name, unit and USD price | +| Catalog search | Searching coffee reduced the visible products; clearing restored the catalog | +| Catalog sort | Price ascending reordered the fixture catalog from $3.25 through $12.00 | +| Add to demo cart | Updated the cart count and showed confirmation | +| Product quantity | Zero disabled Add; one enabled it; helper text showed the supported range | +| Cart quantity and reload | Updating to two coffee bags showed $17.00; a reload preserved the selection | +| Cart Remove | Removed the last line and displayed the empty-cart catalog link | +| Guest checkout | Showed sign-in requirement and preserved the checkout return path | +| Empty login | Showed email/password validation, with the first invalid field focused | +| Fixture sign-in | Visitor credentials opened checkout; administrator fixture credentials enabled the admin area | +| Demo order submit | Disabled while pending, opened order details and stated no payment and no shipment | +| Order history/detail | Displayed the created fixture order with total and timestamp | +| Account and sign-out | Showed own profile; sign-out returned to sign-in and removed authenticated navigation | +| Registration | Empty submission showed validation; fictitious fixture inputs completed registration/login and opened the catalog | +| Admin as ordinary user | Displayed administrator-only denial without product mutation controls | +| Admin overview | Displayed API-derived product/account counts and working management links | +| Product Create dialog | Opened, validated missing fields, created a disposable fixture product and refreshed the table | +| Product search | Located the newly created fixture product; no-match state displayed after removal | +| Product Delete | Opened confirmation; Cancel preserved the row; confirming removed only the disposable test product | +| Admin sorting | USD price header reordered rows and displayed the descending direction | +| Admin pagination | One-page fixture data disabled both boundaries; TanStack supplies the row/page model | +| Account details | Opened the read-only detail dialog and closed it without mutations | +| Observability | Displayed honest unconfigured messages in place of dead dashboard buttons | +| Privacy control | Displayed browser opt-out confirmation after disabling optional analytics | +| Mobile navigation | Opened and closed; Escape dismissed the drawer and returned focus to its trigger | +| Keyboard skip link | Tab then Enter moved to the main-content anchor | +| Theme selector | System, Light and Dark changed the actual surfaces and text; screenshots inspected in both themes | +| Missing product and Retry | Displayed a useful 404 message; Retry repeated the request and preserved safe feedback | +| Missing page | Served the zone's custom 404; its catalog link recovered to `/store/` | +| Cross-zone home/privacy links | Loaded the correct independent application after the local proxy correction | +| Browser logs | Final admin inspection returned no warnings or errors; intentional missing-product/page requests were exercised separately | + +At a 390px viewport the final product page measured document width 375px, below +viewport width; catalog and admin account views also had no page overflow. +At 1280px the landing measured 1265px document width. Admin tables use an explicit +contained horizontal scroll region for wide columns rather than expanding the +page. Temporary viewport overrides were reset after checking. + +## Contrast and source inspection + +The standard sRGB luminance calculation on the actual theme tokens produced: + +| Text/background | Light ratio | Dark ratio | +| ------------------------------- | ----------- | ---------- | +| Primary text / page | 13.74:1 | 15.50:1 | +| Secondary text / page | 5.55:1 | 9.26:1 | +| Accent / page | 6.24:1 | 8.72:1 | +| Placeholder text / product tile | 4.83:1 | 6.49:1 | +| Primary button label / fill | 6.80:1 | 10.54:1 | + +MUI owns validation, focusable selects, dialog behavior and disabled states. +Source inspection confirms 44px minimum MUI buttons/inputs, a visible focus +outline, semantic links/buttons, labeled fields, image dimensions/alt text, +one primary heading per screen, page titles, landmarks, English document language, +unrestricted zoom and reduced-motion CSS. There is no hero bitmap, autoplay +media or third-party blocking script. Faro is lazy-loaded; Umami loads after +interaction readiness with automatic page tracking disabled. + +## antislop delivery gate + +The following is an implementation review, with evidence above and design +reasons in [design direction](design.md). PASS describes the inspected local UI, +not the separate production acceptance items below. Rule identifiers refer to +the [owner-requested core guide](https://raw.githubusercontent.com/miqdadbadjuber/anti-slop/main/antislop.md). + +### Hard requirements + +| Item | Result and evidence | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| R-02 | PASS: new frontend copy/source and documentation scanned for the prohibited punctuation character | +| R-03 | PASS: measured mobile views fit the viewport; source uses responsive columns, contained tables and 44px MUI controls | +| R-17 | PASS: prices/counts come from API responses; local fixture origin is visibly disclosed | +| R-18 | PASS: no review/testimonial or fictional-team section exists | +| R-23 | PASS: existing project mark is reused; missing imagery is explicitly labeled; no invented portrait/photo asset | +| R-24 | PASS: navigation targets correspond to exported pages or the existing project documentation/source organization | +| R-25 | PASS: theme-token calculations above exceed normal-text AA; both themes visually inspected | +| R-26 | PASS: the browser record covers navigation, search, ordering, forms, dialogs, theme and privacy controls; absent integrations show messages | +| R-27 | PASS: loading/failure/empty components are wired into reads; cart empty, 404, validation and access-denied states were exercised | +| R-28 | PASS: no FAQ is fabricated | +| R-32 | PASS: keyboard skip, focus styling and Escape drawer handling checked; controls use native/MUI keyboard behavior | +| R-33 | PASS: UI changes reside directly in maintained TSX/CSS; no runtime source-rewriting helper ships | +| R-34 | PASS: Light and Dark surfaces, product views and admin controls were inspected in the running applications | +| R-35 | PASS: static and Docker builds passed; control-pattern observations and browser log inspection are recorded above | +| R-36 | PASS: no customer, speed, compliance or revenue promise is displayed | +| R-37 | PASS: English/shared-brand brief, authorized defaults, reference research and explicit dials were established before implementation | +| R-38 | PASS: unavailable product media and disposable fixtures identify themselves; unsupported workflows are absent | +| R-39 | PASS: source and browser snapshots show semantic controls, labels, landmarks, names, state feedback and reduced-motion handling | +| R-40 | PASS: media space is dimensioned and optional scripts are deferred; no unmeasured performance result is claimed | + +### Purpose review + +| Item | Result and evidence | +| ---- | ---------------------------------------------------------------------------------------------------- | +| R-01 | PASS: paper/ink/rust separates reading surfaces from actions; no gradient treatment | +| R-04 | PASS: menu/theme icons identify specific controls; no decorative feature-icon collection | +| R-06 | PASS: system sans keeps controls readable; Georgia distinguishes editorial/product headings | +| R-07 | PASS: no background pattern or blueprint decoration | +| R-08 | PASS: CTA copy names the action without decorative arrow repetition | +| R-09 | PASS: section labels identify demo/catalog context without invented promotional badges | +| R-10 | PASS: no glass/backdrop-blur stack | +| R-12 | PASS: ordinary surfaces use borders; MUI overlays use elevation for layering | +| R-13 | PASS: no glow treatment | +| R-14 | PASS: product repetition supports comparing fields; explanatory content uses a different composition | +| R-19 | PASS: restrained interaction feedback matches MOTION 1; reduced-motion rules stop transitions | +| R-22 | PASS: no unrelated stock illustration; product-media absence is explicit | + +### Identity review + +| Item | Result and evidence | +| ---------------- | ------------------------------------------------------------------------------------------- | +| Dials | PASS: landing/store 2/2/1, admin 1/1/1, documented before building | +| Dial consistency | PASS: editorial landing varies content density; admin preserves stable repeated-task layout | +| Focal point | PASS: primary heading/action anchors each reviewed screen | +| Spacing | PASS: hero, catalog and explanations have different purposeful density | +| Accent | PASS: rust marks actions/context, with neutral reading and table surfaces | +| Motif | PASS: recurring editorial type and the existing 52 identity connect the three zones | +| Design read | PASS: educational audience and catalog direction recorded in the design document | + +### Craft and consistency review + +| Item | Result and evidence | +| ---- | ------------------------------------------------------------------------------------------------------------ | +| C-1 | PASS: palette, type, layout, media and motion each have recorded reasons | +| C-2 | PASS: interactive outcomes are recorded above; unavailable dashboards have no fake controls | +| C-3 | PASS: sections explain or demonstrate browsing and simulated orders | +| C-4 | PASS: mobile, themes and error/access/empty flows exercised, with remaining integration boundaries disclosed | +| C-5 | PASS: mock-store and fixture notices remain visible throughout | +| R-05 | PASS: content sequence follows catalog learning; no pricing tiers, testimonial bar or template FAQ | +| R-11 | PASS: 6px controls/panels and 4px media tiles provide restrained radius variation | +| R-15 | PASS: actions name browsing, cart changes or demo-order creation | +| R-16 | PASS: copy describes actual behavior rather than marketing superlatives | +| R-20 | PASS: editorial catalog composition and warm palette persist independently of the logo | +| R-21 | PASS: system preference plus working Light/Dark options is implemented | +| R-29 | PASS: neutral surfaces and one rust accent form the active palette | +| R-30 | PASS: references informed hierarchy; no external template source or brand assets copied | +| R-31 | PASS: one-line major design rationales are recorded in the design document | + +## Production acceptance still required + +- Real Product/Exchange contract and authorization testing, plus full-stack order + persistence/ownership checks. The fixture server does not prove these. +- Owner-selected CDN provider, same-domain path origins, TLS/API cookie rules, + per-app build variables and rollback policy. Ask before deploying. +- Faro/Umami hosting, collection policy and actual payload inspection against + configured collectors. Collection is disabled by default today. +- Device-specific PWA installation acceptance. The store manifest and icon export + exist; the app is online-only and private/API data is not cached offline. +- Lighthouse, field Web Vitals, exhaustive assistive-technology audit and load + testing: not measured. No corresponding score or performance claim is made. + +The frontend branches contain AI-assisted implementation and documentation. +Keep course delivery evidence and human review with the eventual release. diff --git a/docs/index.md b/docs/index.md index 8d07124..6a79d6a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,6 +14,7 @@ course](https://insper.github.io/platform/2026.2/exercises/project/). | --- | --- | | [Project](project/project.md) | Scope, architecture, requirements and delivery checklist | | [Architecture](project/architecture.md) | Complete repository, runtime and deployment architecture | +| [Frontends](frontend/index.md) | Landing, mock store and admin; one-domain routing, packages, PWA, Faro, Umami and local Docker testing | | [Organization](project/organization.md) | Repository model, ownership and contribution workflow | | [Get started](development/get_started.md) | Prerequisites and how to run the repository | | [Developing](development/developing.md) | Repository layout, submodules and development workflow | @@ -37,9 +38,18 @@ The platform is split into independently deployable services: - [Product API](services/product/index.md): product catalogue and prices. - [Order API](services/order/index.md): authenticated orders and totals. - [Exchange API](services/exchange/index.md): currency conversion backed by a third-party provider. -- Gateway, Auth and Account are required platform services. Their contracts and - integration responsibilities are documented in [Project](project/project.md); their - implementation is not present in this repository snapshot. +- [Gateway](services/gateway/index.md), [Auth](services/auth/index.md) and + [Account](services/account/index.md) are present as API submodules. + +## Web experience + +The [frontend documentation](frontend/index.md) describes three English-language +applications on one domain: landing at `/`, store at `/store`, and restricted +management at `/admin`. The store is an educational simulation: **no real +payments or transactions, and no shipment**. Public browsing and authenticated +demo order creation are implemented with Gateway/Account access changes. +The separate Docker fixture mode supports local UI testing; real Product/Exchange +integration and production CDN deployment still require acceptance. ```mermaid flowchart LR diff --git a/docs/operations/observability.md b/docs/operations/observability.md index e0a0d2e..b067d28 100644 --- a/docs/operations/observability.md +++ b/docs/operations/observability.md @@ -70,3 +70,17 @@ runbook and identify an owner. Prometheus and Grafana are suitable options for metrics and dashboards. The specific tools are an implementation decision and must be recorded with retention, access and cost assumptions. + +## Browser observability integration + +The three web applications integrate **Grafana Faro** for browser errors, +performance and tracing, and **Umami** for demo product-usage analytics. The +[frontend PWA and observability plan](../frontend/pwa-observability.md) defines +application identities, sanitized events, environment variables, deployment +choices and verification. Collection is disabled by default. Backend URLs, +hosting and retention remain to be confirmed at CDN deployment; admin usage +analytics are excluded. + +The store is a simulation. Analytics must describe **demo orders**, never real +payments or revenue, and must exclude credentials, personal identifiers and +raw search/order/account URLs. diff --git a/docs/project/architecture.md b/docs/project/architecture.md index 259559a..09ad160 100644 --- a/docs/project/architecture.md +++ b/docs/project/architecture.md @@ -106,6 +106,26 @@ The root repository records exact submodule commits. A service change is implemented and reviewed in its service repository first; the integration repository then updates the submodule pointer and verifies the complete stack. +### Frontend repositories + +The [frontend architecture](../frontend/architecture.md) integrates +`Microsserv52/landing`, `Microsserv52/store` and `Microsserv52/admin` as +`web/landing`, `web/store` and `web/admin`. Independent Next.js applications +share one domain through path routing at `/`, `/store` and `/admin`. +The local edge routes web pages to the static exports while Gateway remains +the business API boundary at `/api/*` and the preserved legacy API paths. + +The agreed experience is an English-language educational mock store with +public catalog browsing and authenticated demo orders. **No real payments or +transactions occur and no goods are shipped.** See the [site/content plan](../frontend/index.md) +and [service prerequisites](../frontend/architecture.md#remaining-service-prerequisites). +Each frontend has a Dockerfile for local testing. Production frontends will +use CDN hosting with separate build environments; the owner must be consulted +when deployment begins. Frontend Kubernetes deployment is deferred. Browser +telemetry is integrated but disabled by default; the store PWA is online-only. +The diagrams elsewhere on this page describe backend deployment; see the +frontend architecture for the web path and API-owned cookie session boundary. + ## Runtime services | Service | Owns | Depends on | Container image name | diff --git a/docs/project/organization.md b/docs/project/organization.md index 285a0cc..55c4a5a 100644 --- a/docs/project/organization.md +++ b/docs/project/organization.md @@ -16,12 +16,17 @@ single place to run the complete platform. | [`api/gateway`](../../api/gateway/) | Gateway submodule | | [`api/order`](../../api/order/) | Java Order API submodule | | [`api/order-service`](../../api/order-service/) | Order service-layer submodule | +| [`api/auth`](../../api/auth/) | Authentication submodule | +| [`api/account`](../../api/account/) | Account submodule | +| `web/landing`, `web/store`, `web/admin` | Independent static-export frontend submodules with local Dockerfiles and CI | | `docs/` | Project and service documentation | | `compose.yml` | Local service topology | | `.github/workflows/` | Repository automation | -Product, Exchange, Auth and Account repositories are expected by the platform -architecture but are not currently present as submodules in this snapshot. +Product and Exchange repositories are expected by the platform architecture +but are not currently present as submodules in this snapshot. The +[frontend documentation](../frontend/index.md) defines ownership, dependencies and +integration prerequisites for the three implemented web repositories. ## Ownership diff --git a/docs/project/project.md b/docs/project/project.md index 08fb4a7..5c31257 100644 --- a/docs/project/project.md +++ b/docs/project/project.md @@ -9,8 +9,11 @@ images, external systems and deployment workflows. ## Purpose -The platform lets authenticated users browse products, create orders and -request totals in another currency. The system is composed of a gateway, +The platform lets visitors browse products and authenticated users create demo +orders. This is a mock store with no real payments, transactions or shipment. +Currency conversion remains a project requirement; the current frontend uses +USD because the checked-in Order implementation rejects other currencies. +The system is composed of a gateway, authentication and account services, plus the product, order and exchange services. diff --git a/docs/services/account/index.md b/docs/services/account/index.md index 0263709..78517a7 100644 --- a/docs/services/account/index.md +++ b/docs/services/account/index.md @@ -25,6 +25,10 @@ identity from Gateway/Auth rather than querying Account directly. The implementation is tracked in the [`api/account`](../../../api/account/) submodule. It follows the hands-on split between the HTTP resource, domain -entity and persistence repository. Registration is public, profile reads and -deletion require a JWT, and Auth accesses credentials through the protected -internal header. +entity and persistence repository. Registration is public and always creates +ROLE_USER. `GET /accounts/me` returns the authenticated caller's profile. +Account list/detail/deletion require ROLE_ADMIN; deleting self or another +administrator is rejected with 409. The admin frontend exposes read-only account +details. Auth accesses credentials through the protected internal header; +Gateway blocks that internal route from public clients. See the +[frontend authorization boundary](../../frontend/architecture.md#api-owned-browser-sessions). diff --git a/docs/services/gateway/index.md b/docs/services/gateway/index.md index e640122..3d509f5 100644 --- a/docs/services/gateway/index.md +++ b/docs/services/gateway/index.md @@ -20,5 +20,11 @@ ports. The implementation is tracked in the [`api/gateway`](../../../api/gateway/) submodule. Spring Cloud Gateway MVC routes Auth, Account, Order, Product and -Exchange, validates the JWT at the public boundary, and exposes only the -health endpoint without authentication. +Exchange and validates JWTs at the API boundary. Catalog GETs, registration, +login/CSRF preparation and health are public. Product writes and account +administration require ROLE_ADMIN; orders and own profile require authentication. + +Static frontends call `/api/session/*` through the local edge. Gateway owns the +HttpOnly session cookie and CSRF checks; no Next.js session server is required. +See [browser sessions and authorization](../../frontend/architecture.md#api-owned-browser-sessions) +for the endpoint table, cookie configuration and real-service acceptance limits. diff --git a/docs/services/product/endpoints.md b/docs/services/product/endpoints.md index ab9d4f7..8a26923 100644 --- a/docs/services/product/endpoints.md +++ b/docs/services/product/endpoints.md @@ -6,7 +6,10 @@ icon: lucide/route !!! warning "Attention" - **To consume the API, the user must be authenticated.** + The selected frontend policy permits public catalog GETs and requires + ROLE_ADMIN for product writes at Gateway. Product's own authorization must + match this policy before real-service acceptance; its source/image is an + external prerequisite. See [integration limits](../../frontend/architecture.md#remaining-service-prerequisites). The Product API must expose the following endpoints: diff --git a/nginx/nginx.conf b/nginx/nginx.conf index f22aa14..2707cf9 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,20 +1,38 @@ events {} http { + resolver 127.0.0.11 valid=5s ipv6=off; upstream gateway { - server gateway:8080; + zone gateway 64k; + server gateway:8080 resolve; } + upstream landing { zone landing 64k; server landing:80 resolve; } + upstream store { zone store 64k; server store:80 resolve; } + upstream admin { zone admin 64k; server admin:80 resolve; } server { listen 80; - location / { + client_max_body_size 1m; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + location = /store { return 308 /store/; } + location = /admin { return 308 /admin/; } + location /store/ { proxy_pass http://store; } + location /admin/ { proxy_pass http://admin; } + location /api/ { + proxy_pass http://gateway/; + proxy_http_version 1.1; + add_header Cache-Control "no-store" always; + } + location ~ ^/(auth|accounts|products|orders|exchange)(/|$) { proxy_pass http://gateway; proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + add_header Cache-Control "no-store" always; } + location / { proxy_pass http://landing; } } } diff --git a/tests/frontend/serve.py b/tests/frontend/serve.py new file mode 100644 index 0000000..488675a --- /dev/null +++ b/tests/frontend/serve.py @@ -0,0 +1,159 @@ +"""Local frontend QA only. --fixtures uses disposable, in-memory API fixtures.""" + +import argparse +import json +import mimetypes +from datetime import datetime, timezone +from http.cookies import SimpleCookie +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from urllib.parse import urlsplit +from uuid import uuid4 + +ROOT = Path(__file__).resolve().parent.parent.parent +PRODUCTS = [ + {"id": f"00000000-0000-4000-8000-{i:012d}", "name": name, "price": price, "unit": unit} + for i, (name, price, unit) in enumerate([ + ("Fixture Tomato", 3.25, "kg"), ("Fixture Coffee", 8.50, "bag"), + ("Fixture Notebook", 4.00, "item"), ("Fixture Canvas bag", 12.00, "item"), + ], 1) +] +ORDERS = [] + + +class Handler(BaseHTTPRequestHandler): + def respond(self, code, body=None, cookie=None): + self.send_response(code) + self.send_header("Cache-Control", "no-store") + self.send_header("X-Microsserv52-Test-Fixtures", "true") + if cookie: + self.send_header("Set-Cookie", cookie) + if body is not None: + self.send_header("Content-Type", "application/json") + self.end_headers() + if body is not None: + self.wfile.write(json.dumps(body).encode()) + + def fixture(self): + path = urlsplit(self.path).path.removeprefix("/api").rstrip("/") + cookie = SimpleCookie(self.headers.get("Cookie", "")) + role = cookie.get("ms52-fixture") + role = role.value if role else None + profile = {"id": "00000000-0000-4000-8000-000000000100", "name": "Fixture Visitor", "email": "visitor@example.test", "roles": "ROLE_USER", "enabled": True} + if role == "admin": + profile.update(name="Fixture Administrator", email="admin@example.test", roles="ROLE_ADMIN") + body = {} + if self.command == "POST": + length = int(self.headers.get("Content-Length", "0")) + if length > 10000: + return self.respond(413) + try: + body = json.loads(self.rfile.read(length) or b"{}") + except ValueError: + return self.respond(400) + if self.command == "GET" and path == "/session/csrf": + return self.respond(200, {"token": "fixture-csrf", "headerName": "X-XSRF-TOKEN"}) + if self.command in ("POST", "DELETE") and self.headers.get("X-XSRF-TOKEN") != "fixture-csrf": + return self.respond(403) + if self.command == "POST" and path == "/session/login": + if body.get("password") != "fixture-only": + return self.respond(401) + role = "admin" if body.get("email") == "admin@example.test" else "user" + return self.respond(204, cookie=f"ms52-fixture={role}; Path=/api; HttpOnly; SameSite=Lax") + if self.command == "DELETE" and path == "/session": + return self.respond(204, cookie="ms52-fixture=; Path=/api; Max-Age=0; HttpOnly; SameSite=Lax") + if self.command == "POST" and path == "/accounts": + return self.respond(201, {**profile, "name": body.get("name", "Fixture Visitor"), "email": body.get("email", "visitor@example.test")}) + if self.command == "GET" and path == "/products": + return self.respond(200, PRODUCTS) + if self.command == "GET" and path.startswith("/products/"): + product = next((p for p in PRODUCTS if p["id"] == path.split("/")[-1]), None) + return self.respond(200 if product else 404, product) + if not role: + return self.respond(401) + if self.command == "GET" and path == "/session": + return self.respond(200, profile) + if self.command == "GET" and path == "/orders": + return self.respond(200, ORDERS) + if self.command == "GET" and path.startswith("/orders/"): + order = next((o for o in ORDERS if o["id"] == path.split("/")[-1]), None) + return self.respond(200 if order else 404, order) + if self.command == "POST" and path == "/orders": + items = [] + for item in body.get("items", []): + product = next((p for p in PRODUCTS if p["id"] == item.get("idProduct")), None) + if not product or not isinstance(item.get("quantity"), int) or item["quantity"] <= 0: + return self.respond(400) + items.append({"id": str(uuid4()), "productId": product["id"], "quantity": item["quantity"], "total": round(product["price"] * item["quantity"], 2)}) + if not items: + return self.respond(400) + order = {"id": str(uuid4()), "date": datetime.now(timezone.utc).isoformat(), "currency": "USD", "items": items, "total": round(sum(i["total"] for i in items), 2)} + ORDERS.insert(0, order) + return self.respond(201, order) + if role != "admin": + return self.respond(403) + if self.command == "GET" and path == "/accounts": + return self.respond(200, [profile]) + if self.command == "POST" and path == "/products": + if not body.get("name") or not body.get("unit") or not isinstance(body.get("price"), (int, float)) or body["price"] <= 0: + return self.respond(400) + product = {"id": str(uuid4()), "name": body["name"], "unit": body["unit"], "price": body["price"]} + PRODUCTS.append(product) + return self.respond(201, product) + if self.command == "DELETE" and path.startswith("/products/"): + PRODUCTS[:] = [p for p in PRODUCTS if p["id"] != path.split("/")[-1]] + return self.respond(204) + return self.respond(404) + + def do_GET(self): + path = urlsplit(self.path).path + if self.server.fixtures and (self.server.api_only or path.startswith("/api/")): + return self.fixture() + if self.server.api_only: + return self.respond(404) + if path in ("/store", "/admin"): + self.send_response(308) + self.send_header("Location", path + "/") + self.end_headers() + return + app, relative = "landing", path.lstrip("/") + for zone in ("store", "admin"): + if path.startswith("/" + zone + "/"): + app, relative = zone, path[len(zone) + 2:] + break + root = ROOT / "web" / app / "out" + file = (root / relative).resolve() + if not file.is_relative_to(root.resolve()): + return self.respond(404) + if file.is_dir(): + file = file / "index.html" + if not file.is_file(): + return self.respond(404) + self.send_response(200) + self.send_header("Content-Type", mimetypes.guess_type(file)[0] or "application/octet-stream") + self.send_header("Cache-Control", "no-store") + self.end_headers() + self.wfile.write(file.read_bytes()) + + def do_POST(self): + if self.server.fixtures: + return self.fixture() + self.respond(503) + + def do_DELETE(self): + if self.server.fixtures: + return self.fixture() + self.respond(503) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--port", type=int, default=4173) + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--fixtures", action="store_true") + parser.add_argument("--api-only", action="store_true") + args = parser.parse_args() + server = ThreadingHTTPServer((args.host, args.port), Handler) + server.fixtures, server.api_only = args.fixtures, args.api_only + print(f"Frontend QA: http://{args.host}:{args.port} (disposable fixtures: {args.fixtures})", flush=True) + server.serve_forever() diff --git a/web/admin b/web/admin new file mode 160000 index 0000000..3aeeb18 --- /dev/null +++ b/web/admin @@ -0,0 +1 @@ +Subproject commit 3aeeb1859e732601029009c0d172d88260cc4a6b diff --git a/web/landing b/web/landing new file mode 160000 index 0000000..18eb438 --- /dev/null +++ b/web/landing @@ -0,0 +1 @@ +Subproject commit 18eb438f05ca2de0dfc867892b86aa90b9373635 diff --git a/web/store b/web/store new file mode 160000 index 0000000..ecbf084 --- /dev/null +++ b/web/store @@ -0,0 +1 @@ +Subproject commit ecbf0846a34dc0eb709bb00f516c3953c6198e90