---
name: pwa
description: >
  Turn a generated SmartStack client app into an installable PWA with offline
  pages — transform the existing app (service worker, manifest, icons, install/
  update banners, outbox bootstrap) and enable offline per page: 'read' (SW GET
  cache, mutations disabled offline) or 'write' (outbox capture, optimistic 202,
  idempotent replay, 409 server-wins conflicts). Thin orchestrator: derives the
  rollout from the BA pagespecs (colocated derive-pwa-spec CLI), persists the
  approved choices back into the pagespecs (SSOT), then drives the deterministic
  scaffolders (scaffold-pwa, scaffold-entity/business --versioned,
  scaffold-api-client, aggregate-outbox, scaffold-routes,
  aggregate-component-registry, scaffold-component) and audit-dev-pwa.
  Use for "make the app a PWA", "page consultable hors ligne", "saisie offline",
  "mode hors connexion", "installable sur mobile".
argument-hint: "[e.g. 'rends les feuilles de temps saisissables hors ligne']"
allowed-tools: Read, Grep, Glob, Bash
---

# /pwa — Installable PWA + offline pages for a generated client app

## What this skill does (and delegates)

This SKILL is a THIN orchestrator (ui-components pattern): every generation and
audit step is a deterministic CLI. It never reimplements their logic — it
sequences them, relays their envelopes, and has the ONE conversation that
matters: **which pages go mobile, and at which offline level**.

| Concern | Owner |
|---|---|
| SW + manifest + icons + registerSW + `initOutbox()` bootstrap + banners + OutboxStatusChip | `development/frontend/pwa` → `cli/scaffold-pwa` |
| Outbox spec module per offline-write entity + hook overlay + `rowVersion` DTO echo | `development/frontend/api-client` (`pwa.offline`, `versioned`) |
| `src/extensions/outbox.generated.ts` aggregation | `development/frontend/pwa` → `cli/aggregate-outbox` |
| Per-page `PageMobileMeta` registry emission | `development/frontend/routes` (`pwa`/`pwaByView`/`defaultPwa`) |
| Offline page behaviour (read: disabled mutations; write: queue chip) | `development/frontend/component` (`pwa`, `versioned`) |
| Backend rowversion → real 409s | `development/backend` scaffold-entity + scaffold-business (`versioned`) |
| Verification | `development/audit-dev-pwa` (DEV-PWA-001..010) |
| BA → pagespec derivation + `--write` persistence | colocated `cli/derive-pwa-spec` |
| The whole mobile shell (navigation, bottom bar, search, account…) | **`@atlashub/smartstack` — inherited, NOTHING to scaffold** |

## The mobile shell is INHERITED, not generated

Do not look for a shell to build: the client app has none to build. The package
renders the entire mobile experience around `<DynamicRouter />`, and the app
turns it on with a single config key.

| What ships in the package | What the client app does |
|---|---|
| `MobileShell` + the "descente par paliers" navigation: `MobileHomePage` (Applications) → `MobileLevelPage` (Modules → Sections) → the resource page | nothing |
| `MobileHeader` with hierarchical back + breadcrumbs, tenant chip + `MobileTenantSheet` | nothing |
| Transverse bottom bar: **Applications / Tâches / Activité / Compte** (`MobileTasksPage`, `MobileActivityPage`, `MobileAccountPage`) | nothing — feed the queues from the backend seams (below) if it has any |
| Transverse search, offline banner + `OutboxSheet`, "Reprendre" cards (`useRecentPages`) | nothing |
| Mobile kit: `MobileFilterBar`, `MobileEmptyState`, `MobileFab`, `MobileDetailTabs` | `scaffold-component` mounts the empty state + FAB on `adapted` LIST pages; the rest is available by hand |
| The `mobile: { … }` provider-config block | `scaffold-pwa` writes it (see the caveat below) |

⚠ **The shell is ON BY DEFAULT** — `MobileShellConfig.enabled` defaults to
`true` (breakpoint 768). `mobile: { enabled: true }` therefore does NOT switch
the shell on; it makes the choice explicit and is the only way to reach
`breakpoint`, `shellComponent` (white-label shell override) and `bottomNav`.
Set `enabled: false` to keep the desktop layout on narrow viewports.

**What actually decides whether a page appears in the shell** is that page's
`PageMobileMeta` — `pwa.support` in the pagespec, emitted by `scaffold-routes`
into the third argument of `PageRegistry.register`. A page with
`support: 'desktop-only'` (or with no metadata at all) is never resolved by the
shell and stays out of the mobile menu. That is the lever to pull, not the
provider config.

**The palier hierarchy is the app's own DB menu.** Applications → Modules →
Sections come straight from `/api/navigation/menu`, the same tree the desktop
sidebar renders and `scaffold-core-seed` populates. There is no mobile-specific
navigation model, no per-page bottom-nav metadata, no ordering file — a page
declares only `pwa.support` / `pwa.offline` (SSOT `lib/pwa-meta.ts`).

**Filling Tâches / Activité (optional, backend).** Both bottom-bar tabs read
platform aggregates (`GET /api/me/tasks`, `/api/me/tasks/count`,
`/api/me/activity`) that Core already feeds. A client extension merges into the
same feeds from its Infrastructure DI — declaratively via
`AddExtensionTasks<ExtensionsDbContext>(tasks => tasks.Entity<Order>(…)…)` (the
mirror of `AddExtensionSearch`), or with a hand-written `ITaskProvider` /
`IActivityProvider` registered through `AddSmartStackTaskProvider<T>()` /
`AddSmartStackActivityProvider<T>()`. Activity is **provider-only** — an
activity row is an event, not a queryable entity state. Nothing to do on the
frontend: the pages ship in the package.

## ⚠ BLOCKING preconditions (relay the CLI errors verbatim, then STOP)

1. **Client mode only** — `scaffold-pwa` refuses `mode: 'source'|'unknown'`
   (`detectFrontendMode`). Never run against the SmartStack.app source repo.
2. **Package gate** — the installed `@atlashub/smartstack` must ship the PWA
   channel, the outbox AND the mobile shell: `setServiceWorkerUpdater`,
   `initOutbox`, `MobileShell` and `useMobileNavContext` are all probed in
   `node_modules/.../dist` (version floor `MIN_SMARTSTACK_PWA_VERSION` as the
   fallback). A package that predates the shell would take
   `mobile: { enabled: true }` and do nothing with it — hence the shell exports
   are part of the gate. On failure: tell the user the socle release carrying
   the PWA/outbox/mobile-shell work must be published + installed first — do
   NOT work around it.
3. **EF migrations are governed** — offline-write entities add a `RowVersion`
   column. The CLIs only SIGNAL the migration in `nextSteps`; the sanctioned
   path is `/efcore`, the decision is the user's. NEVER run
   `dotnet ef` yourself.
4. **Canonical registry layout only** — apps generated under the legacy MCP
   flow ship a MONOLITHIC `src/extensions/componentRegistry.generated.ts`
   registering every page INLINE, with no per-module
   `{app}-{module}Registry.ts`. On that layout (or a mixed one) the routes
   chain FAIL-CLOSES: `scaffold-routes` and `aggregate-component-registry`
   exit 1 with the `registry.legacy-monolith` / `registry.mixed-layout` guard
   (re-aggregating would overwrite the monolith with an EMPTY aggregate and
   unregister every page — blank app), and `audit-dev-pwa` errs with
   DEV-PWA-011. Relay the CLI error verbatim, STOP this flow, and migrate
   first: run `split-component-registry` (frontend-routes skill — see
   "Migrating a legacy monolithic registry" there), which is routing-identical
   and re-aggregates; then re-enter `/pwa`. Never bypass with
   `--exclude`/`--out` and never hand-edit the monolith.

## Flow

1. **Locate the project** (`ss` client app root; BA tree at `.smartstack/ba`).
2. **App-level transform** — run `scaffold-pwa`:
   ```bash
   npx --prefer-offline tsx skills/development/frontend/pwa/cli/scaffold-pwa/index.ts \
     --spec '{"projectPath":"<root>","appCode":"<app>","manifest":{"name":"<App name>"}}'
   ```
   Relay warnings (e.g. `@customised` snippet fallbacks) and `nextSteps`
   (`npm install`, replace placeholder icons).
3. **Derive the rollout** — run `derive-pwa-spec` (report mode):
   ```bash
   npx --prefer-offline tsx skills/pwa/cli/derive-pwa-spec/index.ts \
     --spec '{"projectPath":"<root>"}'
   ```
   Present the coverage: pages already declared, `uncovered` pages, and the
   `requiresVersioned` cost of each offline-write choice. Propose `adapted`
   for the list/detail of the entities the user names; ask about offline level
   per entity (`read` is cheap; `write` needs rowversion + migration). Never
   propose `support: 'full'` (v2).
4. **Persist the choices** — re-run `derive-pwa-spec` with
   `"write": true, "updates": [...]` (pagespecs stay the SSOT; the changed
   machine blocks re-trigger `compute-page-diff` naturally).
5. **Scaffold, in order, per touched module** (specs derived from the report):
   1. offline-write entities: `scaffold-entity` + `scaffold-business` with
      `"versioned": true` — relay their `/efcore` nextSteps verbatim.
   2. `scaffold-api-client` with per-entity `pwa` + `versioned`.
   3. `aggregate-outbox` (always — the generated file may be empty).
   4. `scaffold-routes` with `pwa`/`pwaByView` from the report, then
      `aggregate-component-registry`.
   5. `scaffold-component` per touched (entity, view) with `pwa` + `versioned`.
6. **Audit** — run `audit-dev-pwa` (NOTE: flag-family CLI, the `audit-dev-*`
   convention — it also accepts `--spec`, unlike its siblings):
   ```bash
   npx --prefer-offline tsx skills/development/audit-dev-pwa/cli/audit-dev-pwa/index.ts \
     --project-path "<root>" --app-code "<app>" --mode audit
   # equivalent: --spec '{"projectPath":"<root>","applicationCode":"<app>"}'
   ```
   Auto-heal `err` findings by re-running the `fixSkill` named in each finding
   (max 3 rounds), re-audit. EXCEPTION: a DEV-PWA-011 err (failure kind
   `registry.legacy-monolith`) — or any step-5 CLI exiting on that guard — is
   NOT auto-healable. Never re-run `scaffold-routes` or
   `aggregate-component-registry` on a legacy/mixed layout: stop and route to
   precondition 4's migration flow.
7. **Report**: files touched, pages now mobile/offline (per level), pending
   `nextSteps` (npm install, icons, `/efcore` migration, socle version), and
   what stays v2 (offline conflict-review UX, Web Push, `.mobile.tsx`).

## When NOT to use

- The SmartStack.app source monorepo (it manages its own PWA).
- Offline WRITE conflict-review UX beyond the server-wins chip — v2.
- Web Push (backend lands in the socle's P2) — the SW handlers are
  forward-ready, nothing to generate.
- `support: 'full'` / dedicated `.mobile.tsx` variants — v2.
