diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 15d2ec9..2288853 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -136,5 +136,6 @@ From `src/BikeTracking.Frontend`: For additional context about technologies to be used, project structure, -shell commands, and other important information, read the current plan +shell commands, and other important information, read the current plan: +[specs/024-tauri-api-sidecar/plan.md](../specs/024-tauri-api-sidecar/plan.md) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9d49b8b..32e2f1c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -80,6 +80,18 @@ jobs: - name: Install Tauri system libraries run: sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev + - name: Publish API sidecar binary (Linux x64) + run: | + dotnet publish src/BikeTracking.Api/BikeTracking.Api.csproj \ + --configuration Release \ + --self-contained true \ + --runtime linux-x64 \ + -p:PublishSingleFile=true \ + --output src/BikeTracking.Frontend/src-tauri/binaries/ + mv src/BikeTracking.Frontend/src-tauri/binaries/BikeTracking.Api \ + src/BikeTracking.Frontend/src-tauri/binaries/BikeTracking.Api-x86_64-unknown-linux-gnu + chmod +x src/BikeTracking.Frontend/src-tauri/binaries/BikeTracking.Api-x86_64-unknown-linux-gnu + - name: Tauri config check working-directory: src/BikeTracking.Frontend run: npx tauri build --bundles deb --debug --ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f6f4bd..5f68317 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,7 +81,7 @@ jobs: package-windows: name: Package Windows (NSIS) runs-on: windows-latest - needs: [build-frontend] + needs: [build-frontend, publish-api-windows] timeout-minutes: 20 steps: @@ -118,6 +118,12 @@ jobs: working-directory: src/BikeTracking.Frontend run: npm ci --ignore-scripts + - name: Download API binary (Windows) + uses: actions/download-artifact@v4 + with: + name: api-binary-windows + path: src/BikeTracking.Frontend/src-tauri/binaries/ + - name: Build Windows installer working-directory: src/BikeTracking.Frontend run: npx tauri build --bundles nsis --ci @@ -135,7 +141,7 @@ jobs: package-linux: name: Package Linux (.deb) runs-on: ubuntu-latest - needs: [build-frontend] + needs: [build-frontend, publish-api-linux] timeout-minutes: 20 steps: @@ -175,6 +181,15 @@ jobs: working-directory: src/BikeTracking.Frontend run: npm ci --ignore-scripts + - name: Download API binary (Linux) + uses: actions/download-artifact@v4 + with: + name: api-binary-linux + path: src/BikeTracking.Frontend/src-tauri/binaries/ + + - name: Mark API binary executable + run: chmod +x src/BikeTracking.Frontend/src-tauri/binaries/BikeTracking.Api-x86_64-unknown-linux-gnu + - name: Build Linux .deb package working-directory: src/BikeTracking.Frontend run: npx tauri build --bundles deb --ci @@ -186,6 +201,89 @@ jobs: path: src/BikeTracking.Frontend/src-tauri/target/release/bundle/deb/BikeTracking_*_amd64.deb retention-days: 1 + # --------------------------------------------------------------------------- + # Job 2b: Publish self-contained API binary for Windows x64 + # --------------------------------------------------------------------------- + publish-api-windows: + name: Publish API binary (Windows) + runs-on: windows-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET from global.json + uses: actions/setup-dotnet@v5 + with: + global-json-file: global.json + + - name: Restore .NET dependencies + run: dotnet restore BikeTracking.slnx + + - name: Publish API self-contained (Windows x64) + run: | + dotnet publish src/BikeTracking.Api/BikeTracking.Api.csproj ` + --configuration Release ` + --self-contained true ` + --runtime win-x64 ` + -p:PublishSingleFile=true ` + --output api-publish-win/ ` + --no-restore + + - name: Rename binary to Tauri sidecar naming convention + run: Rename-Item api-publish-win/BikeTracking.Api.exe BikeTracking.Api-x86_64-pc-windows-msvc.exe + + - name: Upload API binary artefact (Windows) + uses: actions/upload-artifact@v4 + with: + name: api-binary-windows + path: api-publish-win/BikeTracking.Api-x86_64-pc-windows-msvc.exe + retention-days: 1 + + # --------------------------------------------------------------------------- + # Job 2c: Publish self-contained API binary for Linux x64 + # --------------------------------------------------------------------------- + publish-api-linux: + name: Publish API binary (Linux) + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET from global.json + uses: actions/setup-dotnet@v5 + with: + global-json-file: global.json + + - name: Restore .NET dependencies + run: dotnet restore BikeTracking.slnx + + - name: Publish API self-contained (Linux x64) + run: | + dotnet publish src/BikeTracking.Api/BikeTracking.Api.csproj \ + --configuration Release \ + --self-contained true \ + --runtime linux-x64 \ + -p:PublishSingleFile=true \ + --output api-publish-linux/ \ + --no-restore + + - name: Rename binary and mark executable + run: | + mv api-publish-linux/BikeTracking.Api \ + api-publish-linux/BikeTracking.Api-x86_64-unknown-linux-gnu + chmod +x api-publish-linux/BikeTracking.Api-x86_64-unknown-linux-gnu + + - name: Upload API binary artefact (Linux) + uses: actions/upload-artifact@v4 + with: + name: api-binary-linux + path: api-publish-linux/BikeTracking.Api-x86_64-unknown-linux-gnu + retention-days: 1 + # --------------------------------------------------------------------------- # Job 3b: API smoke test — verify .NET API starts and /health responds # --------------------------------------------------------------------------- @@ -279,8 +377,10 @@ jobs: echo "AUTO_BUMP=${AUTO_BUMP}" >> "$GITHUB_OUTPUT" echo "Resolved release tag: ${TAG}" - # T017a: Bump package.json + Cargo.toml when version was auto-incremented - - name: Bump version files + # Patch package.json + Cargo.toml in CI workspace only (no commit to main). + # release-please handles version bumps in the repo via PRs. + # This step ensures the built artifacts carry the correct version number. + - name: Patch version files for build (workspace only) if: steps.tag.outputs.AUTO_BUMP == 'true' run: | NEW_VER="${TAG#v}" @@ -289,13 +389,6 @@ jobs: && mv /tmp/pkg.json src/BikeTracking.Frontend/package.json sed -i "0,/^version = /s/^version = .*/version = \"${NEW_VER}\"/" \ src/BikeTracking.Frontend/src-tauri/Cargo.toml - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add src/BikeTracking.Frontend/package.json \ - src/BikeTracking.Frontend/src-tauri/Cargo.toml - git diff --cached --quiet || \ - git commit -m "chore: bump version to ${NEW_VER} [skip ci]" - git push origin HEAD:main # T018: isDraft-aware duplicate-tag guard - name: Duplicate-tag guard diff --git a/.gitignore b/.gitignore index 5bd9b04..8c3451d 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,10 @@ appsettings.Development.json *.bak *.cache node_modules/ + +# Sidecar binaries — populated by CI; do not commit +src/BikeTracking.Frontend/src-tauri/binaries/*.exe +src/BikeTracking.Frontend/src-tauri/binaries/BikeTracking.Api-* dist/ build/ coverage/ diff --git a/.specify/feature.json b/.specify/feature.json index cf0943c..f7f3ad5 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/023-pwa-desktop-packaging" + "feature_directory": "specs/024-tauri-api-sidecar" } diff --git a/README.md b/README.md index ecbc9ab..ac41d9d 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,23 @@ These are ran in the .github\workflows\ci.yml pipeline on every PR BikeTracking packages as native desktop installers for Windows (.exe) and Linux (.deb) via Tauri 2. - Flow: commit to main → release-please bumps version & tags → release.yml builds installers & publishes. +Flow: commit to main → release-please bumps version & tags → release.yml builds installers (including bundled API) & publishes. + +**The installer is fully self-contained.** The .NET API is bundled as a Tauri sidecar binary — no separate installation or manual startup required. On launch, the Tauri shell spawns the API automatically; on close, it terminates it cleanly. + +### Architecture: Tauri Sidecar + +``` +BikeTracking (Tauri shell) +├── React frontend (embedded in binary) +└── BikeTracking.Api sidecar (self-contained .NET binary) + └── SQLite DB (~/.local/share/BikeTracking/ or %APPDATA%\BikeTracking\) +``` + +- Sidecar spawned in `lib.rs setup()` via `tauri-plugin-shell` +- Sidecar killed on `WindowEvent::Destroyed` (no orphaned processes) +- API port: `5079` (fixed; configurable via `app.conf.json`) +- Frontend polls `GET /health` on startup; shows spinner until API ready, error if >10s ### Local Build (DevContainer) @@ -263,6 +279,25 @@ sudo apt-get install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3 sudo dnf install webkit2gtk4.1-devel gtk3-devel libappindicator-gtk3-devel librsvg2-devel ``` +For local Tauri dev, publish the API sidecar binary first: + +```bash +# Linux +dotnet publish src/BikeTracking.Api/BikeTracking.Api.csproj \ + --configuration Release --self-contained true --runtime linux-x64 \ + -p:PublishSingleFile=true --output src/BikeTracking.Frontend/src-tauri/binaries/ +mv src/BikeTracking.Frontend/src-tauri/binaries/BikeTracking.Api \ + src/BikeTracking.Frontend/src-tauri/binaries/BikeTracking.Api-x86_64-unknown-linux-gnu +chmod +x src/BikeTracking.Frontend/src-tauri/binaries/BikeTracking.Api-x86_64-unknown-linux-gnu + +# Windows (PowerShell) +dotnet publish src/BikeTracking.Api/BikeTracking.Api.csproj ` + --configuration Release --self-contained true --runtime win-x64 ` + -p:PublishSingleFile=true --output src/BikeTracking.Frontend/src-tauri/binaries/ +Rename-Item src/BikeTracking.Frontend/src-tauri/binaries/BikeTracking.Api.exe ` + BikeTracking.Api-x86_64-pc-windows-msvc.exe +``` + Build dev binary with hot-reload: ```bash @@ -270,7 +305,7 @@ cd src/BikeTracking.Frontend npm run tauri:dev ``` -Opens native window with live React HMR. +Opens native window. API sidecar starts automatically. Build release installers (output → `src-tauri/target/release/bundle/`): @@ -286,9 +321,11 @@ Outputs: ### Dependencies - **@tauri-apps/cli@^2**: Desktop shell + Vite integration -- **@tauri-apps/api@^2**: JS API for native features (file I/O, window control) -- **Rust 1.80+**: Minimal shell binary (Cargo.toml → src/lib.rs) +- **@tauri-apps/api@^2**: JS API for native features +- **tauri-plugin-shell ^2**: Sidecar spawn/kill lifecycle +- **Rust 1.80+**: Minimal shell binary (`src-tauri/src/lib.rs`) - **WebKit2GTK 4.1** (Linux): WebView renderer +- **.NET 10**: Compiled into sidecar binary (self-contained — not required on user machine) See `package.json` + `src-tauri/Cargo.toml` for locked versions. @@ -301,22 +338,22 @@ Triggered by: Pipeline stages: 1. **release-please**: Monitors conventional commits on `main`. Opens release PR with bumped `package.json` + `Cargo.toml` versions + auto-generated `CHANGELOG.md`. -2. **build-frontend**: Single job. Runs `npm run build` → uploads `dist/` artifact. -3. **package-windows** (parallel): Downloads `dist/`, runs `tauri build --bundles nsis` on Windows runner. -4. **package-linux** (parallel): Downloads `dist/`, runs `tauri build --bundles deb` on Ubuntu runner. (Rust cached → ~6 min build time). -5. **publish-release**: Downloads both installers, computes SHA-256 checksums, creates GitHub Release with both `.exe` + `.deb` as downloadable assets. +2. **build-frontend** + **publish-api-windows** + **publish-api-linux** (parallel): Frontend builds `dist/`; API published as self-contained binary per platform. +3. **package-windows**: Downloads `dist/` + Windows API binary → places in `src-tauri/binaries/` → `tauri build --bundles nsis`. +4. **package-linux**: Downloads `dist/` + Linux API binary → places in `src-tauri/binaries/` → `tauri build --bundles deb`. +5. **smoke-test-api**: Validates `GET /health` returns 200 on published API binary. +6. **publish-release**: Downloads both installers, computes SHA-256 checksums, creates GitHub Release. Versioning: - Conventional commit prefixes → semver bumps: `feat:` → minor, `fix:` → patch, `feat!:` / `BREAKING CHANGE:` → major -- Example: After merging `feat: add weather data`, release-please bumps `0.1.0` → `0.2.0` + opens release PR - Merge release PR → tag auto-created → pipeline triggers → installers published +- On manual dispatch without version input: version patched in CI workspace only (no commit to `main`); `release-please` owns version files in the repo Release notes auto-generated from commits since previous tag. Grouped by type (Features, Bug Fixes, Chores). ### Pre-release Builds Tag format: `v0.1.0-alpha`, `v0.1.0-rc.1` → marked `pre-release` on GitHub, not latest stable. - ## Update SpecKit https://github.com/github/spec-kit/blob/main/docs/upgrade.md diff --git a/docs/agent/delivery-and-git-workflow.md b/docs/agent/delivery-and-git-workflow.md index 9f36b96..59ef0d7 100644 --- a/docs/agent/delivery-and-git-workflow.md +++ b/docs/agent/delivery-and-git-workflow.md @@ -15,3 +15,10 @@ Branching, PR, CI, and feature-flag governance. ## CI Expectations - Validation checks must pass before merge. - Include build, formatting/linting, unit/integration checks, and E2E. + +## Release Pipeline +- Installers are fully self-contained: .NET API bundled as Tauri sidecar binary alongside React frontend. +- Release workflow (`release.yml`) runs: `build-frontend` + `publish-api-windows` + `publish-api-linux` (parallel) → `package-windows` + `package-linux` → `smoke-test-api` → `publish-release`. +- `release-please` owns version files (`package.json`, `Cargo.toml`) in the repo via PRs — never commit version bumps directly to `main`. +- On manual dispatch without version input: versions patched in CI workspace only (no commit); artifact carries correct version, repo files unchanged. +- Binary naming for sidecar must follow Tauri triple convention: `BikeTracking.Api-x86_64-pc-windows-msvc.exe` (Windows), `BikeTracking.Api-x86_64-unknown-linux-gnu` (Linux). diff --git a/specs/024-tauri-api-sidecar/checklists/requirements.md b/specs/024-tauri-api-sidecar/checklists/requirements.md new file mode 100644 index 0000000..a06f975 --- /dev/null +++ b/specs/024-tauri-api-sidecar/checklists/requirements.md @@ -0,0 +1,37 @@ +# Specification Quality Checklist: Bundle .NET API as Tauri Sidecar + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-06-15 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- All checklist items pass. Spec is ready for `/speckit.plan`. +- macOS is explicitly out of scope (Windows x64 + Linux x64 only); documented in Assumptions. +- Code-signing is explicitly out of scope; documented in Assumptions. +- Concurrent multi-instance usage is out of scope; documented in Assumptions and Edge Cases. diff --git a/specs/024-tauri-api-sidecar/contracts/ci-pipeline.md b/specs/024-tauri-api-sidecar/contracts/ci-pipeline.md new file mode 100644 index 0000000..ea16237 --- /dev/null +++ b/specs/024-tauri-api-sidecar/contracts/ci-pipeline.md @@ -0,0 +1,162 @@ +# Contract: CI Artefact Pipeline + +**Scope**: `.github/workflows/release.yml` + +--- + +## Overview + +Two new parallel jobs (`publish-api-windows`, `publish-api-linux`) produce self-contained API binaries and upload them as GitHub Actions artefacts. The existing `package-windows` and `package-linux` jobs each download their respective artefact and place it in `src-tauri/binaries/` before running `tauri build`. + +--- + +## Revised Job Dependency Graph + +``` +build-frontend ──────────────────────────────────────┐ + │ +publish-api-windows ─────────────────────────────► package-windows ──► publish-release +publish-api-linux ───────────────────────────────► package-linux ───► publish-release +smoke-test-api (unchanged) ────────────────────────────────────────────────────────────┘ +``` + +--- + +## Job: `publish-api-windows` + +| Property | Value | +|---|---| +| Runner | `windows-latest` | +| Needs | *(none — runs in parallel with `build-frontend`)* | +| Timeout | 15 minutes | + +**Steps**: +1. Checkout +2. Setup .NET from `global.json` +3. Restore: `dotnet restore BikeTracking.slnx` +4. Publish: + ```powershell + dotnet publish src/BikeTracking.Api/BikeTracking.Api.csproj ` + --configuration Release ` + --self-contained true ` + --runtime win-x64 ` + -p:PublishSingleFile=true ` + --output api-publish-win/ ` + --no-restore + ``` +5. Rename binary to Tauri naming convention: + ```powershell + Rename-Item api-publish-win/BikeTracking.Api.exe ` + BikeTracking.Api-x86_64-pc-windows-msvc.exe + ``` +6. Upload artefact: + - **name**: `api-binary-windows` + - **path**: `api-publish-win/BikeTracking.Api-x86_64-pc-windows-msvc.exe` + - **retention-days**: 1 + +--- + +## Job: `publish-api-linux` + +| Property | Value | +|---|---| +| Runner | `ubuntu-latest` | +| Needs | *(none — runs in parallel with `build-frontend`)* | +| Timeout | 15 minutes | + +**Steps**: +1. Checkout +2. Setup .NET from `global.json` +3. Restore: `dotnet restore BikeTracking.slnx` +4. Publish: + ```bash + dotnet publish src/BikeTracking.Api/BikeTracking.Api.csproj \ + --configuration Release \ + --self-contained true \ + --runtime linux-x64 \ + -p:PublishSingleFile=true \ + --output api-publish-linux/ \ + --no-restore + ``` +5. Rename and mark executable: + ```bash + mv api-publish-linux/BikeTracking.Api \ + api-publish-linux/BikeTracking.Api-x86_64-unknown-linux-gnu + chmod +x api-publish-linux/BikeTracking.Api-x86_64-unknown-linux-gnu + ``` +6. Upload artefact: + - **name**: `api-binary-linux` + - **path**: `api-publish-linux/BikeTracking.Api-x86_64-unknown-linux-gnu` + - **retention-days**: 1 + +--- + +## Changes to `package-windows` + +Add to `needs`: +```yaml +needs: [build-frontend, publish-api-windows] +``` + +Add step **before** `Build Windows installer`: +```yaml +- name: Download API binary (Windows) + uses: actions/download-artifact@v4 + with: + name: api-binary-windows + path: src/BikeTracking.Frontend/src-tauri/binaries/ +``` + +--- + +## Changes to `package-linux` + +Add to `needs`: +```yaml +needs: [build-frontend, publish-api-linux] +``` + +Add step **before** `Build Linux .deb package`: +```yaml +- name: Download API binary (Linux) + uses: actions/download-artifact@v4 + with: + name: api-binary-linux + path: src/BikeTracking.Frontend/src-tauri/binaries/ + +- name: Mark API binary executable + run: chmod +x src/BikeTracking.Frontend/src-tauri/binaries/BikeTracking.Api-x86_64-unknown-linux-gnu +``` + +--- + +## Failure Invariants (FR-008) + +1. If `publish-api-windows` fails → `package-windows` does not run (GitHub Actions `needs` dependency) +2. If `publish-api-linux` fails → `package-linux` does not run +3. If `package-windows` or `package-linux` fails → `publish-release` does not run +4. The `smoke-test-api` job continues to use `--self-contained false` (it validates API behaviour, not binary packaging) + +--- + +## `src-tauri/binaries/` Directory + +A new `src-tauri/binaries/` directory is added to the repository with: +- `.gitkeep` — keeps the directory tracked +- `README.md` — documents naming convention and instructs developers not to commit real binaries + +`.gitignore` entry (in `src/BikeTracking.Frontend/src-tauri/binaries/.gitignore` or the root `.gitignore`): +```gitignore +# Sidecar binaries — populated by CI; do not commit +src/BikeTracking.Frontend/src-tauri/binaries/*.exe +src/BikeTracking.Frontend/src-tauri/binaries/BikeTracking.Api-* +``` + +--- + +## Artefact Naming Summary + +| Artefact name | File | Produced by | Consumed by | +|---|---|---|---| +| `api-binary-windows` | `BikeTracking.Api-x86_64-pc-windows-msvc.exe` | `publish-api-windows` | `package-windows` | +| `api-binary-linux` | `BikeTracking.Api-x86_64-unknown-linux-gnu` | `publish-api-linux` | `package-linux` | diff --git a/specs/024-tauri-api-sidecar/contracts/health-poll.md b/specs/024-tauri-api-sidecar/contracts/health-poll.md new file mode 100644 index 0000000..d26fb0a --- /dev/null +++ b/specs/024-tauri-api-sidecar/contracts/health-poll.md @@ -0,0 +1,125 @@ +# Contract: Frontend Health-Check Polling Protocol + +**Scope**: `src/BikeTracking.Frontend/src/components/api-startup-guard/ApiStartupGuard.tsx` + +--- + +## Overview + +`ApiStartupGuard` is a React component that wraps the entire application router. It withholds rendering children until the API health endpoint confirms readiness, and exposes a Retry action when the timeout elapses. + +--- + +## Component Interface + +```typescript +interface ApiStartupGuardProps { + children: React.ReactNode; +} + +export function ApiStartupGuard({ children }: ApiStartupGuardProps): JSX.Element +``` + +No other props. The component reads `getApiBaseUrl()` internally at poll time — no URL prop is needed (see `src/services/api-config.ts`). + +--- + +## State Machine + +``` +type ApiStartupStatus = 'connecting' | 'ready' | 'error'; +``` + +| From | Event | To | +|---|---|---| +| `connecting` (initial / after Retry) | `GET /health` returns `200 OK` | `ready` | +| `connecting` | `GET /health` returns non-200 or network error, attempt < 20 | `connecting` (retry after 500 ms) | +| `connecting` | attempt = 20 without success | `error` | +| `error` | user clicks "Retry" button | `connecting` (attempt counter reset to 0) | + +--- + +## Polling Parameters + +| Parameter | Value | Constant Name | +|---|---|---| +| Poll interval | 500 ms | `HEALTH_POLL_INTERVAL_MS` | +| Max attempts | 20 | `HEALTH_POLL_MAX_ATTEMPTS` | +| Total timeout | 10 000 ms | derived | +| Health endpoint | `${getApiBaseUrl()}/health` | uses `getApiBaseUrl()` | +| Success condition | HTTP status `200` | | + +--- + +## Fetch Behaviour + +- Each poll uses `fetch(url, { signal })` with an `AbortController` signal +- A fresh `AbortController` is created per component mount / Retry cycle +- On unmount, the controller is aborted to cancel any in-flight request (prevents setState-after-unmount) +- Non-200 responses and network errors are treated equivalently (retry or timeout) +- No request headers required; the `/health` endpoint has no authentication gate + +--- + +## Rendered Output by State + +| Status | Rendered content | +|---|---| +| `connecting` | Spinner / "Connecting…" accessible indicator; `role="status"` | +| `ready` | `{children}` (the full app router) | +| `error` | Error message explaining the API failed to start + "Retry" button; `role="alert"` | + +**Accessibility**: +- Connecting indicator: `aria-label="Connecting to BikeTracking API…"`, `role="status"`, `aria-live="polite"` +- Error state: `role="alert"`, `aria-live="assertive"` +- Retry button: standard ` + + ) + } + + // status === 'connecting' + return ( +
+ + ) +} diff --git a/src/BikeTracking.Frontend/tests/e2e/api-startup-guard.spec.ts b/src/BikeTracking.Frontend/tests/e2e/api-startup-guard.spec.ts new file mode 100644 index 0000000..9159eb1 --- /dev/null +++ b/src/BikeTracking.Frontend/tests/e2e/api-startup-guard.spec.ts @@ -0,0 +1,55 @@ +/** + * Playwright E2E test: ApiStartupGuard startup flow + * + * Scenario: Navigate to the app root and verify the connecting state is shown + * immediately, then transitions to the main app once the health check succeeds. + * + * Prerequisites: + * - The .NET API must be running on http://localhost:55436 (or the value + * configured by VITE_API_BASE_URL / window.__BIKE_API_URL__) + * - Run with: npm run test:e2e -- api-startup-guard + * + * TDD: This test is written RED — it will fail until ApiStartupGuard is + * implemented and wraps the app router in App.tsx. + */ + +import { test, expect } from '@playwright/test' + +test.describe('ApiStartupGuard', () => { + test('shows connecting indicator within 1 second of page load, then transitions to app', async ({ + page, + }) => { + // Navigate to the app root + await page.goto('/') + + // Within 1 000 ms the connecting indicator must be visible + // The component renders role="status" with text matching /connecting/i + const connectingIndicator = page.getByRole('status', { name: /connecting/i }) + await expect(connectingIndicator).toBeVisible({ timeout: 1000 }) + + // Once the health check succeeds, the connecting indicator disappears + // and the login page (or main app) renders + await expect(connectingIndicator).not.toBeVisible({ timeout: 15000 }) + + // The login page should now be visible — there should be no connecting indicator + // and the page should have navigated or rendered the main content + await expect(page.getByRole('status', { name: /connecting/i })).not.toBeVisible() + }) + + test('shows error state with Retry button if API is unreachable after timeout', async ({ + page, + context, + }) => { + // Block all requests to the health endpoint to simulate an unreachable API + await context.route('**/health', (route) => route.abort()) + + await page.goto('/') + + // Should see the error alert after the 10 s timeout (20 attempts × 500 ms) + const errorAlert = page.getByRole('alert') + await expect(errorAlert).toBeVisible({ timeout: 15000 }) + + // Retry button must be present + await expect(page.getByRole('button', { name: /retry/i })).toBeVisible() + }) +})