diff --git a/.specify/feature.json b/.specify/feature.json index 734ec3c..486603a 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/020-improve-ride-preset-options" + "feature_directory": "specs/022-pwa-local-install" } diff --git a/specs/022-pwa-local-install/checklists/requirements.md b/specs/022-pwa-local-install/checklists/requirements.md new file mode 100644 index 0000000..51299bf --- /dev/null +++ b/specs/022-pwa-local-install/checklists/requirements.md @@ -0,0 +1,35 @@ +# Specification Quality Checklist: Local PWA Installation + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-05-20 +**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 + +- Validation pass completed: all checklist items currently pass. +- No unresolved clarification markers remain. diff --git a/specs/022-pwa-local-install/contracts/pwa-installation-contract.md b/specs/022-pwa-local-install/contracts/pwa-installation-contract.md new file mode 100644 index 0000000..7819cdf --- /dev/null +++ b/specs/022-pwa-local-install/contracts/pwa-installation-contract.md @@ -0,0 +1,65 @@ +# Contract: PWA Installation and Runtime Behavior + +**Feature**: 022-pwa-local-install +**Type**: Frontend/platform behavior contract +**Status**: Draft + +## 1. Supported Environment Contract (v1) + +Install support is guaranteed only when all conditions are true: + +- Operating system: Windows desktop +- Browser family: current Chrome or current Edge +- Browser install prerequisites: satisfied for the running app + +If any condition is false, app must: + +- Keep core browser usage available +- Show explicit guidance that installed-app support is unavailable in v1 + +## 2. Install Interaction Contract + +When install is supported and prompt is available: + +- App exposes a visible install action +- Install action triggers browser-native install flow +- On success, app can be launched from OS app launcher and opens in app-style window + +Failure handling: + +- On prompt dismissal or install failure, app shows non-blocking guidance and allows retry + +## 3. Online-Only Operations Contract + +For v1 installed mode: + +- Ride operations require network connectivity +- If offline, app shows connectivity-required guidance and retry path +- App remains stable and navigable while operations are blocked + +## 4. Update Lifecycle Contract + +Installed app update behavior: + +- Checks for updates on relaunch or refresh +- Applies latest available version automatically when available +- Exposes user-facing status for update in-progress and update failure states + +State model: + +- `idle -> checking -> downloading -> ready -> applied` +- Failure path: `checking/downloading -> failed` + +## 5. Session Persistence Contract + +Authentication behavior in installed mode: + +- Session persists across launches up to 7 days inactivity +- Explicit sign-out immediately invalidates session +- On inactivity > 7 days, app requires sign-in before authenticated ride operations + +## 6. Backend API Contract Impact + +No mandatory API schema changes are introduced by this feature contract. + +If implementation discovers backend compatibility needs for session timeout semantics, any API changes must be additive and documented in a follow-up contract update. diff --git a/specs/022-pwa-local-install/data-model.md b/specs/022-pwa-local-install/data-model.md new file mode 100644 index 0000000..f058f47 --- /dev/null +++ b/specs/022-pwa-local-install/data-model.md @@ -0,0 +1,105 @@ +# Data Model: Local PWA Installation + +**Feature**: 022-pwa-local-install +**Date**: 2026-05-20 +**Status**: Complete + +## Overview + +This feature introduces client-side runtime state models for installability, launch context, update lifecycle, and 7-day inactivity session timeout. No new server database entities are required for v1. + +## Entities + +### 1. InstallationState + +Represents install capability and install progression for the current environment. + +| Field | Type | Description | +|------|------|-------------| +| `isInstallSupported` | boolean | Whether current browser+OS matches v1 support matrix and install prerequisites are satisfied | +| `installPromptAvailable` | boolean | Whether install prompt can be triggered in current session | +| `status` | enum | `unavailable` \| `available` \| `prompting` \| `installed` \| `failed` | +| `reasonCode` | enum? | Optional code for unavailable/failed states (`unsupported_os`, `unsupported_browser`, `prompt_dismissed`, `policy_blocked`) | +| `lastTransitionAtUtc` | string (ISO-8601) | Timestamp for diagnostics and telemetry correlation | + +**Validation rules**: +- `status=installed` implies `isInstallSupported=true`. +- `reasonCode` required when `status` is `unavailable` or `failed`. + +### 2. LaunchContext + +Represents app runtime context at startup. + +| Field | Type | Description | +|------|------|-------------| +| `mode` | enum | `browser_tab` \| `installed_window` | +| `isOnline` | boolean | Network availability at startup | +| `platform` | enum | `windows` \| `non_windows` | +| `browserFamily` | enum | `chrome` \| `edge` \| `other` | +| `appVersion` | string | Current loaded app version identifier | + +**Validation rules**: +- `mode=installed_window` only valid when install criteria were previously met. +- `platform=non_windows` forces `InstallationState.status=unavailable` for v1 support policy. + +### 3. SessionState + +Represents authenticated state persistence across launches with inactivity expiration. + +| Field | Type | Description | +|------|------|-------------| +| `isAuthenticated` | boolean | Rider is currently authenticated | +| `lastActivityAtUtc` | string (ISO-8601) | Last authenticated user activity timestamp | +| `expiresAtUtc` | string (ISO-8601) | Computed expiration timestamp (`lastActivityAtUtc + 7 days`) | +| `expiredByInactivity` | boolean | True when reopening after expiration threshold | + +**Validation rules**: +- When `nowUtc > expiresAtUtc`, app must set `isAuthenticated=false` and require sign-in. +- Explicit sign-out invalidates session regardless of inactivity timer. + +### 4. UpdateState + +Represents automatic update lifecycle for installed instances. + +| Field | Type | Description | +|------|------|-------------| +| `status` | enum | `idle` \| `checking` \| `downloading` \| `ready` \| `applied` \| `failed` | +| `targetVersion` | string? | Version being applied | +| `lastCheckedAtUtc` | string (ISO-8601) | Last update check timestamp | +| `failureReason` | string? | Optional failure message for user guidance | + +**Validation rules**: +- `failureReason` required when `status=failed`. +- Transition `ready -> applied` occurs on relaunch or refresh. + +## State Transitions + +### InstallationState transitions + +`unavailable -> available -> prompting -> installed` + +Failure path: + +`available -> prompting -> failed -> available` + +### SessionState transitions + +`authenticated(active) -> authenticated(inactive) -> expiredByInactivity -> unauthenticated` + +Sign-out path: + +`authenticated -> unauthenticated` + +### UpdateState transitions + +`idle -> checking -> downloading -> ready -> applied` + +Failure path: + +`checking/downloading -> failed -> checking` + +## Persistence Notes + +- `SessionState` persistence must survive installed-app relaunches. +- `InstallationState`, `LaunchContext`, and `UpdateState` are runtime and telemetry-oriented state; persistence can be ephemeral unless needed for diagnostics. +- Domain data (rides, users, projections) remains in existing SQLite storage and is unchanged by this feature. diff --git a/specs/022-pwa-local-install/plan.md b/specs/022-pwa-local-install/plan.md new file mode 100644 index 0000000..fa7cccb --- /dev/null +++ b/specs/022-pwa-local-install/plan.md @@ -0,0 +1,87 @@ +# Implementation Plan: Local PWA Installation + +**Branch**: `022-pwa-local-install` | **Date**: 2026-05-20 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/022-pwa-local-install/spec.md` + +## Summary + +Enable local desktop installation for Bike Tracking as a PWA on Windows (Chrome/Edge), with +online-only ride operations in v1, automatic app updates on relaunch/refresh, and persisted sign-in +for up to 7 days of inactivity. Implementation centers on frontend PWA enablement (manifest, +service worker lifecycle, install UX), plus authentication session persistence policy updates and +clear unsupported-environment messaging. + +## Technical Context + +**Language/Version**: C# .NET 10 (API), F# .NET 10 (domain unchanged), TypeScript + React 19 (frontend) +**Primary Dependencies**: ASP.NET Core Minimal API, EF Core SQLite, Aspire, React 19 + Vite, browser PWA capabilities +**Storage**: SQLite local file for domain data; browser local storage/session storage for client auth/session metadata +**Testing**: xUnit (backend), Vitest (frontend), Playwright (E2E) +**Target Platform**: Windows desktop end-user machines; Chrome and Edge for supported install flows +**Project Type**: Local-first web application (Aspire orchestrated API + frontend) +**Performance Goals**: Preserve existing API goal (<500ms p95 normal operations); install affordance visible within 1s of app boot; session rehydrate <300ms on launch +**Constraints**: v1 online-only for ride operations; Windows-only install support; auto-update on relaunch/refresh; re-auth required after 7 days inactivity +**Scale/Scope**: Single-user local deployment; one installable shell for existing ride tracking flows; no new backend aggregate introduced + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Gate | Status | Notes | +|------|--------|-------| +| Clean Architecture / Ports-and-Adapters | PASS | PWA/browser APIs isolated in frontend adapter layer; no domain leakage | +| Functional Core / Impure Edges | PASS | Feature is primarily UI/runtime behavior; domain logic remains unchanged | +| Event Sourcing / CQRS | PASS | No changes to event schema required for v1 install capability | +| TDD Red-Green-Refactor | PASS | quickstart defines red tests first for install, session timeout, unsupported environments | +| UX Consistency / Accessibility | PASS | Install guidance and unsupported-environment messaging must remain accessible and keyboard navigable | +| Performance / Observability | PASS | No heavy runtime background work; retain API latency and add client install/update telemetry events | +| Data Validation / Integrity | PASS | Existing server validations unchanged; client session timeout state validated by deterministic timestamp checks | +| Security / Learning | PASS | 7-day inactivity sign-in policy limits stale auth risk; no secrets added to client assets | +| Modularity / Contract-First | PASS | PWA capability contract documented in `contracts/pwa-installation-contract.md` | +| Trunk-Based Delivery / CI | PASS | No branch strategy changes; full CI matrix remains required | + +**Post-Design Constitution Re-check**: PASS. Phase 1 artifacts keep changes modular in frontend infrastructure (`src/BikeTracking.Frontend`) and avoid domain/event-store coupling. + +## Project Structure + +### Documentation (this feature) + +```text +specs/022-pwa-local-install/ +├── plan.md +├── research.md +├── data-model.md +├── quickstart.md +├── contracts/ +│ └── pwa-installation-contract.md +└── tasks.md +``` + +### Source Code (repository root) + +```text +src/ +├── BikeTracking.Api/ +│ ├── Program.cs +│ ├── Application/ +│ ├── Contracts/ +│ └── Infrastructure/ +├── BikeTracking.Api.Tests/ +├── BikeTracking.Domain.FSharp/ +├── BikeTracking.Frontend/ +│ ├── public/ +│ ├── src/ +│ │ ├── components/ +│ │ ├── pages/ +│ │ ├── services/ +│ │ └── main.tsx +│ ├── vite.config.ts +│ └── package.json +└── BikeTracking.AppHost/ +``` + +**Structure Decision**: Keep existing Aspire web application layout. Implement PWA install/update/session behavior inside `src/BikeTracking.Frontend` with minimal backend touch points only if auth-token semantics require API contract alignment. + +## Complexity Tracking + +No constitutional violations requiring justification. diff --git a/specs/022-pwa-local-install/quickstart.md b/specs/022-pwa-local-install/quickstart.md new file mode 100644 index 0000000..718e272 --- /dev/null +++ b/specs/022-pwa-local-install/quickstart.md @@ -0,0 +1,121 @@ +# Developer Quickstart: Local PWA Installation + +**Feature**: 022-pwa-local-install +**Branch**: 022-pwa-local-install +**Date**: 2026-05-20 + +## Goal + +Make Bike Tracking locally installable on Windows via Chrome/Edge PWA flows, with automatic updates, online-only ride operations, and 7-day inactivity auth timeout. + +## Prerequisites + +- DevContainer running +- App start command available from repo root: + +```bash +dotnet run --project src/BikeTracking.AppHost +``` + +- Frontend commands available in `src/BikeTracking.Frontend` + +## TDD Gate Sequence (Mandatory) + +1. Write failing tests for one behavior slice. +2. Run tests and confirm failures are behavior-related. +3. Implement minimal code to pass. +4. Re-run tests until green. +5. Refactor with tests still green. + +## Implementation Order + +### 1) PWA Baseline (Manifest + Service Worker Registration) + +- Add/verify manifest metadata for installability. +- Add/verify service worker registration and boot lifecycle. +- Ensure installed launch opens in app-style window. + +Tests first: +- Frontend unit: manifest-driven UI affordance visibility state. +- E2E: install entry point present in supported environment. + +### 2) Install UX and Unsupported Environment Guidance + +- Add clear install action when supported. +- Add clear guidance when unsupported OS/browser or install not available. + +Tests first: +- Frontend unit: unsupported environment message rendering. +- E2E: non-supported environment keeps browser usage path available. + +### 3) Online-Only Runtime Behavior + +- Detect offline state in installed window. +- Show connectivity-required guidance and retry action for ride operations. + +Tests first: +- Frontend unit: offline guard behavior. +- E2E: offline launch displays guidance without app crash. + +### 4) Automatic Update on Relaunch/Refresh + +- Implement service worker update flow signaling (`checking`, `ready`, `applied`, `failed`). +- Surface update status messaging. + +Tests first: +- Frontend unit: update state transition handling. +- E2E: outdated instance updates on relaunch/refresh in supported environment. + +### 5) 7-Day Inactivity Session Timeout + +- Persist session activity timestamp across launches. +- Enforce re-authentication after 7 days inactivity. + +Tests first: +- Frontend unit: inactivity expiration calculation. +- E2E: relaunch after simulated >7 days requires sign-in. + +## Suggested Test Inventory + +- Vitest: install capability detection, unsupported guidance, offline guard, update-state reducer/handler, session-expiry calculation. +- Playwright: supported install flow (Windows Chrome/Edge), relaunch behavior, update application behavior, inactivity timeout behavior. +- API tests: only if auth token semantics require backend validation change. + +## Verification Commands + +```bash +# Solution tests +dotnet test BikeTracking.slnx + +# Frontend quality and unit tests +cd src/BikeTracking.Frontend +npm run lint +npm run build +npm run test:unit + +# End-to-end tests (app must be running via Aspire) +npm run test:e2e +``` + +## Notes + +- Keep feature scope constrained to Windows install support in v1. +- Do not add offline ride create/edit in this feature. +- Preserve existing backend contracts unless a strictly necessary auth compatibility issue emerges. + +## Validation Results (2026-05-20) + +### Frontend Checklist (T039) + +- `npm run lint` (from `src/BikeTracking.Frontend`): passed +- `npm run build` (from `src/BikeTracking.Frontend`): passed + - Note: Vite reported a large chunk size warning for `dist/assets/index-*.js`, no build failure. +- `npm run test:unit` (from `src/BikeTracking.Frontend`): passed + - Result: 27 files passed, 163 tests passed, 0 failed. +- `npm run test:e2e` (from `src/BikeTracking.Frontend`): passed + - Result: 38 passed, 0 failed. + +### Full Solution Regression (T040) + +- `dotnet test BikeTracking.slnx` (from repo root): passed + - Result: total 353, failed 0, succeeded 351, skipped 2. diff --git a/specs/022-pwa-local-install/research.md b/specs/022-pwa-local-install/research.md new file mode 100644 index 0000000..c1c0b6f --- /dev/null +++ b/specs/022-pwa-local-install/research.md @@ -0,0 +1,65 @@ +# Research: Local PWA Installation + +**Feature**: 022-pwa-local-install +**Date**: 2026-05-20 +**Status**: Complete + +## Decision 1: Installability Approach + +**Decision**: Implement installability as a standards-based PWA shell for the existing frontend, using a valid web app manifest, service worker registration, and explicit install UI entry point in-app. + +**Rationale**: This directly satisfies local install goals for Chrome/Edge on Windows without introducing native packaging, and keeps delivery aligned with the current React + Vite architecture. + +**Alternatives considered**: +- Native desktop wrappers (Electron/Tauri): rejected for v1 due to larger runtime footprint and additional release complexity. +- Browser-only usage with no install support: rejected because it fails FR-001 through FR-005. + +## Decision 2: Supported Environment Scope + +**Decision**: v1 install support is Windows desktop on current Chrome and Edge only; all other environments are explicitly unsupported for install but remain supported for browser usage. + +**Rationale**: A focused support matrix reduces ambiguity and test matrix size, enabling reliable first-release behavior while preserving access for non-supported environments. + +**Alternatives considered**: +- Multi-OS v1 support (Windows + macOS + Linux): rejected due to increased validation and support overhead. +- Best-effort all-browser support: rejected because it weakens testability and acceptance criteria. + +## Decision 3: Offline Policy for v1 + +**Decision**: v1 installed app is online-only for ride operations. + +**Rationale**: This keeps feature scope centered on installation and launch UX, avoids introducing queue/sync conflict complexity, and aligns with current backend-dependent ride workflows. + +**Alternatives considered**: +- Full offline create/edit with later sync: rejected for v1 due to complexity and conflict-resolution requirements. +- Read-only offline mode: rejected because it still requires additional caching and invalidation logic not needed for initial install milestone. + +## Decision 4: Update Behavior + +**Decision**: Installed app updates automatically to latest available version on relaunch or refresh, with clear update-in-progress/failure messaging. + +**Rationale**: Automatic updates minimize support burden and reduce long-lived outdated clients while preserving simple user expectations. + +**Alternatives considered**: +- Manual update trigger only: rejected as higher UX friction and higher stale-version risk. +- Reinstall-only updates: rejected due to poor usability. + +## Decision 5: Session Persistence Rule + +**Decision**: Keep user signed in for up to 7 days of inactivity, then require re-authentication. + +**Rationale**: Balances convenience and security for a locally installed daily-use app; prevents indefinite stale sessions. + +**Alternatives considered**: +- Indefinite sign-in: rejected for higher security risk on shared devices. +- Sign-in every launch: rejected for unnecessary friction. + +## Decision 6: Contract Surface + +**Decision**: Treat PWA behavior as a frontend/platform contract (manifest/service-worker/install/update/session state transitions) with no mandatory backend API schema changes for v1. + +**Rationale**: The feature requirements are primarily runtime/client behavior; keeping backend contracts stable minimizes risk and implementation time. + +**Alternatives considered**: +- New backend install-state endpoints: rejected as unnecessary for initial scope. +- Persisting install metadata server-side: rejected because installability is environment-local and not cross-device in v1. diff --git a/specs/022-pwa-local-install/spec.md b/specs/022-pwa-local-install/spec.md new file mode 100644 index 0000000..616ef15 --- /dev/null +++ b/specs/022-pwa-local-install/spec.md @@ -0,0 +1,126 @@ +# Feature Specification: Local PWA Installation + +**Feature Branch**: `022-pwa-local-install` +**Created**: 2026-05-20 +**Status**: Draft +**Input**: User description: "Make this a locally installable app on a computer using PWA technology." + +## Clarifications + +### Session 2026-05-20 + +- Q: What offline support level is required for the first release of the installed app? → A: Option B (online-only in v1; network required for ride operations) +- Q: Which desktop browsers are explicitly supported for install in v1? → A: Option A (Chrome and Edge desktop install flows) +- Q: How should installed-app updates be handled in v1? → A: Option A (automatic update to latest available version on next launch or refresh) +- Q: How long should sign-in persist in the installed app without activity? → A: Option B (keep signed in for 7 days of inactivity, then require sign-in) +- Q: Which desktop operating systems are in scope for v1 install support? → A: Option A (Windows only) + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Install App Locally (Priority: P1) + +As a rider, I want to install the bike tracking app on my computer so I can launch it like a normal desktop app. + +**Why this priority**: Local installability is the core requested outcome and the primary user value. + +**Independent Test**: Open the app in a supported desktop browser, install it using the browser install flow, close browser tabs, then launch the installed app from the operating system app launcher. + +**Acceptance Scenarios**: + +1. **Given** a rider opens the app in a supported desktop browser, **When** install prerequisites are met, **Then** the rider is offered a clear install option. +2. **Given** a rider accepts installation, **When** installation completes, **Then** the app is available from the desktop operating system launcher like other installed apps. +3. **Given** the app has been installed, **When** the rider launches it from the operating system, **Then** the app opens in its own app window and is ready for normal use. + +--- + +### User Story 2 - Preserve Signed-In Experience Across Launches (Priority: P2) + +As a rider, I want the installed app to remember my active session between launches so daily use feels seamless. + +**Why this priority**: Retaining expected user context reduces friction and improves adoption of the installed experience. + +**Independent Test**: Sign in, close the installed app window, relaunch from operating system launcher, and verify user remains signed in unless they explicitly sign out. + +**Acceptance Scenarios**: + +1. **Given** a rider is signed in within the installed app, **When** the rider closes and reopens the app, **Then** the rider remains signed in. +2. **Given** a rider explicitly signs out, **When** the rider reopens the installed app, **Then** the app requires sign-in again. + +--- + +### User Story 3 - Handle Unsupported Install Environments Gracefully (Priority: P3) + +As a rider, I want clear guidance when installation is unavailable so I understand how to continue using the app. + +**Why this priority**: Prevents confusion for users on unsupported browsers or environments while keeping the app usable. + +**Independent Test**: Open the app in an environment that does not support installation and verify the app remains functional in-browser with clear guidance. + +**Acceptance Scenarios**: + +1. **Given** a rider uses a browser or environment where local installation is unavailable, **When** the rider looks for install options, **Then** the app shows a clear message that installation is unavailable in that environment. +2. **Given** installation is unavailable, **When** the rider continues in the browser, **Then** all existing core app flows remain accessible. + +### Edge Cases + +- Rider attempts to install while an install prompt has already been dismissed in the current session. +- Rider has an older installed app instance and opens a newer browser version of the app. +- Rider launches an outdated installed version: app updates to latest available version on next launch or refresh before normal use. +- Rider launches the installed app while offline: app clearly indicates network is required for ride operations and allows retry after reconnecting. +- Rider returns after more than 7 days of inactivity: app requires sign-in before ride operations. +- Rider attempts installation on non-Windows desktop OS: app shows support limitation and guidance to continue using browser mode. +- Rider clears browser/site storage after installation and before relaunch. +- Rider installs the app on multiple computers and expects each installation to remain independent. +- Rider’s operating system prevents installation due to policy restrictions. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST support local desktop installation through supported browser install flows. +- **FR-002**: System MUST present a visible and understandable install entry point whenever installation is available. +- **FR-003**: System MUST complete installation without requiring riders to use developer tools or manual packaging steps. +- **FR-004**: System MUST allow the installed app to launch from standard operating system app-launch locations. +- **FR-005**: System MUST open the installed app in an app-style window separate from normal browser tab navigation. +- **FR-006**: System MUST preserve an authenticated session across installed-app restarts unless rider explicitly signs out. +- **FR-007**: System MUST detect when installation is not available and provide clear user-facing guidance. +- **FR-008**: System MUST keep all existing ride tracking flows usable in the browser when installation is unavailable. +- **FR-009**: System MUST preserve rider data continuity between browser and installed-app launches on the same computer. +- **FR-010**: System MUST provide a clear recovery path when installation fails (for example retrying installation or continuing in browser mode). +- **FR-011**: For this feature release, installed-app ride operations MUST require an active network connection. +- **FR-012**: When network is unavailable in the installed app, system MUST present clear guidance that ride operations require connectivity and provide a retry path after reconnection. +- **FR-013**: For v1, system MUST explicitly support local installation via current desktop Chrome and desktop Edge install flows. +- **FR-014**: For browsers outside the v1 support matrix, system MUST provide clear guidance that in-browser use remains available without a guaranteed install experience. +- **FR-015**: Installed app MUST automatically update to the latest available version on next launch or refresh when network is available. +- **FR-016**: System MUST provide a clear in-app status message when an update is being applied and recover gracefully if update retrieval fails. +- **FR-017**: Installed-app authentication MUST remain valid for up to 7 days of inactivity unless rider explicitly signs out. +- **FR-018**: After more than 7 days of inactivity, system MUST require the rider to sign in again before accessing authenticated ride operations. +- **FR-019**: For v1, local install support MUST target Windows desktop environments only. +- **FR-020**: On non-Windows desktop operating systems, system MUST present clear guidance that installed-app support is not available in v1 and preserve browser-based usage. + +### Key Entities *(include if feature involves data)* + +- **Installation State**: Represents whether installation is available, in progress, completed, or unavailable for a rider’s current environment. +- **Launch Context**: Represents how the app was opened (browser or installed app window) to support appropriate UI behavior. +- **Session State**: Represents the rider’s signed-in status across app launches on a single computer. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: At least 90% of riders on supported desktop environments can complete local installation in under 2 minutes without assistance. +- **SC-002**: At least 95% of successful installations can be launched from the operating system app launcher on first attempt. +- **SC-003**: At least 95% of riders who relaunch the installed app within 7 days remain signed in unless they explicitly signed out. +- **SC-004**: 100% of riders in unsupported install environments see clear guidance and can continue core ride tracking flows in-browser. +- **SC-005**: 100% of installed-app attempts to perform ride operations while offline show a clear connectivity-required message and a retry action. +- **SC-006**: At least 95% of install attempts on current desktop Chrome and desktop Edge complete successfully on first try in validation tests. +- **SC-007**: At least 95% of outdated installed-app launches on supported browsers update to the latest available version on first relaunch when network is available. +- **SC-008**: At least 95% of install attempts on supported Windows desktop environments succeed on first attempt during validation. + +## Assumptions + +- Existing authentication and ride-tracking capabilities remain unchanged in scope for this feature. +- Primary v1 install targets are current desktop Chrome and desktop Edge. +- Primary v1 operating system target is Windows desktop. +- Offline ride create/edit/view behavior is out of scope for this feature and may be addressed in a later iteration. +- Local installation and launch behavior is evaluated on end-user computers, not kiosk or locked-down enterprise environments as a primary target. diff --git a/specs/022-pwa-local-install/tasks.md b/specs/022-pwa-local-install/tasks.md new file mode 100644 index 0000000..ad5da28 --- /dev/null +++ b/specs/022-pwa-local-install/tasks.md @@ -0,0 +1,210 @@ +# Tasks: Local PWA Installation + +**Input**: Design documents from `/specs/022-pwa-local-install/` +**Prerequisites**: plan.md (required), spec.md (required), research.md, data-model.md, contracts/pwa-installation-contract.md, quickstart.md + +**Tests**: Tests are required for this feature (TDD gate is mandatory in spec/constitution and quickstart). + +**Organization**: Tasks are grouped by user story so each story can be implemented and validated independently. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no blocking dependency) +- **[Story]**: User story label (`[US1]`, `[US2]`, `[US3]`) +- Each task includes a concrete file path. + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Prepare project-level PWA assets and test scaffolding. + +- [X] T001 Add PWA metadata dependency/scripts in src/BikeTracking.Frontend/package.json +- [X] T002 Create PWA manifest baseline in src/BikeTracking.Frontend/public/manifest.webmanifest +- [X] T003 [P] Add installable app icon assets in src/BikeTracking.Frontend/public/pwa-192.png and src/BikeTracking.Frontend/public/pwa-512.png +- [X] T004 [P] Extend browser API test shims for install/service-worker events in src/BikeTracking.Frontend/src/test/setup.ts + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Build shared runtime primitives that all user stories depend on. + +**⚠️ CRITICAL**: No user story implementation starts before this phase completes. + +- [X] T005 Create shared PWA state/type definitions in src/BikeTracking.Frontend/src/services/pwa/pwa-types.ts +- [X] T006 [P] Implement supported-environment detection utility in src/BikeTracking.Frontend/src/services/pwa/environment-support.ts +- [X] T007 [P] Implement install prompt lifecycle service in src/BikeTracking.Frontend/src/services/pwa/install-service.ts +- [X] T008 [P] Implement service worker update lifecycle service in src/BikeTracking.Frontend/src/services/pwa/update-service.ts +- [X] T009 Implement launch context and network-state helper in src/BikeTracking.Frontend/src/services/pwa/launch-context.ts +- [X] T010 Implement inactivity timeout policy helper (7-day rule) in src/BikeTracking.Frontend/src/services/pwa/session-policy.ts +- [X] T011 Wire global PWA bootstrap entrypoint in src/BikeTracking.Frontend/src/main.tsx +- [X] T012 Add top-level status outlet for install/update/offline messaging in src/BikeTracking.Frontend/src/App.tsx + +**Checkpoint**: Shared PWA/session/update infrastructure is ready; user stories can proceed. + +--- + +## Phase 3: User Story 1 - Install App Locally (Priority: P1) 🎯 MVP + +**Goal**: Provide Windows Chrome/Edge install flow with clear install action and installed-window behavior. + +**Independent Test**: In supported Windows Chrome/Edge, user sees install action, completes install, and launches app from OS launcher into app-style window. + +### Tests for User Story 1 (write first, confirm failing) + +- [X] T013 [P] [US1] Add unit tests for support matrix detection in src/BikeTracking.Frontend/src/services/pwa/environment-support.test.ts +- [X] T014 [P] [US1] Add unit tests for install prompt lifecycle transitions in src/BikeTracking.Frontend/src/services/pwa/install-service.test.ts +- [X] T015 [P] [US1] Add E2E scenario for supported install flow in src/BikeTracking.Frontend/tests/e2e/pwa-install-supported.spec.ts + +### Implementation for User Story 1 + +- [X] T016 [US1] Add manifest link/theme/start URL metadata for installability in src/BikeTracking.Frontend/index.html +- [X] T017 [US1] Create service worker registration adapter in src/BikeTracking.Frontend/src/services/pwa/register-service-worker.ts +- [X] T018 [US1] Integrate service worker registration and install bootstrap in src/BikeTracking.Frontend/src/main.tsx +- [X] T019 [US1] Add install action UI and status text in src/BikeTracking.Frontend/src/pages/settings/SettingsPage.tsx +- [X] T020 [US1] Add app-header install state indicator component wiring in src/BikeTracking.Frontend/src/components/app-header/app-header.tsx +- [X] T021 [US1] Ensure installed-window launch context handling in src/BikeTracking.Frontend/src/services/pwa/launch-context.ts + +**Checkpoint**: US1 is independently functional and installable on supported environment. + +--- + +## Phase 4: User Story 2 - Preserve Signed-In Experience Across Launches (Priority: P2) + +**Goal**: Persist auth session for up to 7 days inactivity and support automatic updates on relaunch/refresh. + +**Independent Test**: User stays signed in across normal relaunches, is forced to re-authenticate after >7 days inactivity, and sees update lifecycle messaging during auto-update. + +### Tests for User Story 2 (write first, confirm failing) + +- [X] T022 [P] [US2] Add unit tests for inactivity expiration calculation in src/BikeTracking.Frontend/src/services/pwa/session-policy.test.ts +- [X] T023 [P] [US2] Add unit tests for update lifecycle state transitions in src/BikeTracking.Frontend/src/services/pwa/update-service.test.ts +- [X] T024 [P] [US2] Add E2E scenario for 7-day inactivity re-authentication in src/BikeTracking.Frontend/tests/e2e/pwa-session-timeout.spec.ts +- [X] T025 [P] [US2] Add E2E scenario for automatic update on relaunch/refresh in src/BikeTracking.Frontend/tests/e2e/pwa-auto-update.spec.ts + +### Implementation for User Story 2 + +- [X] T026 [US2] Persist and refresh activity timestamps in auth state management in src/BikeTracking.Frontend/src/context/auth-context.tsx +- [X] T027 [US2] Enforce inactivity expiration gate before protected routes render in src/BikeTracking.Frontend/src/components/protected-route.tsx +- [X] T028 [US2] Update authenticated API usage to refresh activity timestamp in src/BikeTracking.Frontend/src/services/users-api.ts +- [X] T029 [P] [US2] Update ride API usage to refresh activity timestamp in src/BikeTracking.Frontend/src/services/ridesService.ts +- [X] T030 [US2] Add update status banner messaging (checking/downloading/failed) in src/BikeTracking.Frontend/src/components/app-header/app-header.tsx + +**Checkpoint**: US2 is independently functional with session timeout and update messaging. + +--- + +## Phase 5: User Story 3 - Handle Unsupported Install Environments Gracefully (Priority: P3) + +**Goal**: Keep browser usage available with explicit unsupported-environment and offline guidance. + +**Independent Test**: In unsupported environment or offline installed mode, app clearly explains limitation and keeps core browser flow available. + +### Tests for User Story 3 (write first, confirm failing) + +- [X] T031 [P] [US3] Add unit test coverage for unsupported-environment guidance rendering in src/BikeTracking.Frontend/src/pages/settings/SettingsPage.test.tsx +- [X] T032 [P] [US3] Add E2E scenario for unsupported OS/browser guidance and fallback in src/BikeTracking.Frontend/tests/e2e/pwa-unsupported-environment.spec.ts +- [X] T033 [P] [US3] Add E2E scenario for installed-mode offline connectivity-required behavior in src/BikeTracking.Frontend/tests/e2e/pwa-offline-guard.spec.ts + +### Implementation for User Story 3 + +- [X] T034 [US3] Render unsupported OS/browser guidance and browser-mode fallback text in src/BikeTracking.Frontend/src/pages/settings/SettingsPage.tsx +- [X] T035 [US3] Add connectivity-required guard and retry action for ride operations in src/BikeTracking.Frontend/src/pages/RecordRidePage.tsx +- [X] T036 [US3] Add online/offline status wiring for guard presentation in src/BikeTracking.Frontend/src/services/pwa/launch-context.ts + +**Checkpoint**: US3 is independently functional with graceful fallback behavior. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Final validation, docs, and cross-story cleanup. + +- [X] T037 [P] Update frontend feature documentation for install/update/session behavior in src/BikeTracking.Frontend/README.md +- [X] T038 [P] Add shared E2E helper utilities for PWA scenarios in src/BikeTracking.Frontend/tests/e2e/support/auth-helpers.ts +- [X] T039 Run full frontend validation checklist from specs/022-pwa-local-install/quickstart.md +- [X] T040 Run full solution regression validation via BikeTracking.slnx and capture results in specs/022-pwa-local-install/quickstart.md + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Phase 1 (Setup)**: starts immediately +- **Phase 2 (Foundational)**: depends on Phase 1 and blocks all user stories +- **Phase 3 (US1)**: depends on Phase 2 +- **Phase 4 (US2)**: depends on Phase 2 (can run parallel with US1 after foundation) +- **Phase 5 (US3)**: depends on Phase 2 (can run parallel with US1/US2 after foundation) +- **Phase 6 (Polish)**: depends on completion of all targeted user stories + +### User Story Dependencies + +- **US1 (P1)**: no dependency on other stories +- **US2 (P2)**: no strict dependency on US1, but reuses shared PWA infrastructure from Phase 2 +- **US3 (P3)**: no strict dependency on US1/US2, but reuses shared PWA infrastructure from Phase 2 + +### Within Each User Story + +- Test tasks must be authored and fail before implementation tasks begin +- Runtime service/utilities before UI wiring +- UI behavior before E2E stabilization/cleanup + +### Parallel Opportunities + +- Setup: T003, T004 parallel +- Foundational: T006, T007, T008 parallel after T005 +- US1 tests: T013, T014, T015 parallel +- US2 tests: T022, T023, T024, T025 parallel +- US3 tests: T031, T032, T033 parallel +- Polish: T037, T038 parallel + +--- + +## Parallel Example: User Story 1 + +```bash +# Parallel test authoring for US1 +T013 [US1] src/BikeTracking.Frontend/src/services/pwa/environment-support.test.ts +T014 [US1] src/BikeTracking.Frontend/src/services/pwa/install-service.test.ts +T015 [US1] src/BikeTracking.Frontend/tests/e2e/pwa-install-supported.spec.ts + +# Parallel implementation tasks after core wiring exists +T019 [US1] src/BikeTracking.Frontend/src/pages/settings/SettingsPage.tsx +T020 [US1] src/BikeTracking.Frontend/src/components/app-header/app-header.tsx +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1) + +1. Complete Phase 1 and Phase 2 +2. Deliver US1 (Phase 3) +3. Validate install success path in supported environment +4. Demo/release MVP scope + +### Incremental Delivery + +1. Add US1 install capability +2. Add US2 session/update lifecycle +3. Add US3 unsupported/offline guidance +4. Finish polish and full regression checks + +### Parallel Team Strategy + +1. Team completes Setup + Foundational together +2. After Phase 2: + - Developer A: US1 + - Developer B: US2 + - Developer C: US3 +3. Merge each story when independently green + +--- + +## Notes + +- `[P]` tasks indicate file-level independence, not zero coordination. +- Keep all implementation within the declared v1 scope: Windows + Chrome/Edge install support, online-only ride operations. +- Backend API changes are out-of-scope unless strictly required for auth compatibility. +- Do not proceed to `/speckit.implement` until task tests are explicitly validated as red-first then green. diff --git a/src/BikeTracking.Frontend/README.md b/src/BikeTracking.Frontend/README.md index 0f75a90..f7ab627 100644 --- a/src/BikeTracking.Frontend/README.md +++ b/src/BikeTracking.Frontend/README.md @@ -38,6 +38,41 @@ Use the project constitution command matrix: 2. Backend/domain-only changes: run `dotnet test` from repo root 3. Auth/login/cross-layer changes: run all impacted checks plus `npm run test:e2e` +## PWA Behavior (Feature 022) + +### Install Support (v1) + +- Supported install target: Windows desktop with current Chrome or Edge. +- Unsupported environments continue to work in browser mode. +- Install action and status are available in the Settings page. + +### Update Lifecycle + +- App checks for updates on relaunch/refresh while online. +- Header status messaging surfaces lifecycle states: + - checking + - downloading + - failed + +### Session Timeout Policy + +- Auth session persists across launches for up to 7 days of inactivity. +- Activity timestamps are refreshed during authenticated API usage. +- Expired sessions are forced back to login before protected routes render. + +### Connectivity Requirements (Installed Mode) + +- Ride operations in installed mode require network connectivity in v1. +- Record Ride displays a connectivity-required guard and retry action when offline. + +### E2E PWA Helpers + +- Shared helpers for PWA scenarios are in: + - `tests/e2e/support/auth-helpers.ts` +- Includes utilities to: + - emulate PWA environment values + - force online/offline state during tests + ## Notes - Playwright configuration is in `playwright.config.ts`. diff --git a/src/BikeTracking.Frontend/index.html b/src/BikeTracking.Frontend/index.html index 47432a6..92a93d1 100644 --- a/src/BikeTracking.Frontend/index.html +++ b/src/BikeTracking.Frontend/index.html @@ -3,7 +3,10 @@
+ + ++ {updateStatusMessage} +
+ ) : null} ) } diff --git a/src/BikeTracking.Frontend/src/components/protected-route.test.tsx b/src/BikeTracking.Frontend/src/components/protected-route.test.tsx index 31ea591..879720c 100644 --- a/src/BikeTracking.Frontend/src/components/protected-route.test.tsx +++ b/src/BikeTracking.Frontend/src/components/protected-route.test.tsx @@ -1,6 +1,6 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom' import { render, screen } from '@testing-library/react' -import { beforeEach, describe, expect, it } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { AuthProvider } from '../context/auth-context' import { ProtectedRoute } from './protected-route' @@ -21,6 +21,7 @@ function renderProtectedRoute() { describe('ProtectedRoute', () => { beforeEach(() => { + vi.useRealTimers() sessionStorage.clear() }) @@ -37,4 +38,24 @@ describe('ProtectedRoute', () => { expect(screen.getByText('Protected Miles Page')).toBeVisible() }) + + it('redirects expired sessions to /login and clears persisted session', () => { + vi.useFakeTimers() + vi.setSystemTime(new Date('2026-05-20T10:00:00.000Z')) + + sessionStorage.setItem( + 'bike_tracking_auth_session', + JSON.stringify({ + userId: 5, + userName: 'Alice', + lastActivityAtUtc: '2026-05-10T09:59:59.000Z', + expiresAtUtc: '2026-05-17T09:59:59.000Z', + }) + ) + + renderProtectedRoute() + + expect(screen.getByText('Login Page')).toBeVisible() + expect(sessionStorage.getItem('bike_tracking_auth_session')).toBeNull() + }) }) diff --git a/src/BikeTracking.Frontend/src/components/protected-route.tsx b/src/BikeTracking.Frontend/src/components/protected-route.tsx index 41b0b1f..da14d2e 100644 --- a/src/BikeTracking.Frontend/src/components/protected-route.tsx +++ b/src/BikeTracking.Frontend/src/components/protected-route.tsx @@ -1,10 +1,23 @@ import { Navigate, Outlet } from 'react-router-dom' import { useAuth } from '../context/auth-context' import { AppHeader } from './app-header/app-header' +import { isInactivityExpired } from '../services/pwa/session-policy' export function ProtectedRoute() { - const { user } = useAuth() - if (!user) return+ Connectivity required: this installed app needs an internet connection for ride operations. +
+ +{gasPriceSource}
: null} -+ Current mode: {pwaSnapshot.launchContext.mode === 'installed_window' ? 'Installed app window' : 'Browser tab'} +
+ {pwaSnapshot.installationState.isInstallSupported ? ( ++ {getUnsupportedEnvironmentMessage(pwaSnapshot.installationState.reasonCode)} +
+ )} +
{error}
diff --git a/src/BikeTracking.Frontend/src/services/pwa/bootstrap.ts b/src/BikeTracking.Frontend/src/services/pwa/bootstrap.ts
new file mode 100644
index 0000000..424d343
--- /dev/null
+++ b/src/BikeTracking.Frontend/src/services/pwa/bootstrap.ts
@@ -0,0 +1,107 @@
+import { createInstallService, type InstallService } from "./install-service";
+import { getLaunchContext, subscribeNetworkState } from "./launch-context";
+import { createUpdateService, type UpdateService } from "./update-service";
+import {
+ type InstallationState,
+ type LaunchContext,
+ type UpdateState,
+} from "./pwa-types";
+
+interface PwaSnapshot {
+ launchContext: LaunchContext;
+ installationState: InstallationState;
+ updateState: UpdateState;
+}
+
+type SnapshotListener = (snapshot: PwaSnapshot) => void;
+
+const APP_VERSION =
+ (import.meta.env.VITE_APP_VERSION as string | undefined) ?? "dev";
+let installService: InstallService | null = null;
+let updateService: UpdateService | null = null;
+let unsubscribeNetwork: (() => void) | null = null;
+
+const listeners = new Set