# PWA Implementation — WorkRoot IT Solutions

## Overview

Progressive Web App (PWA) features have been added to the WorkRoot website. The implementation follows a **progressive enhancement** approach — the site works fully without JavaScript; PWA features layer on top.

---

## Files Added / Modified

| File | Type | Purpose |
|------|------|---------|
| `public/manifest.json` | New | Web App Manifest (installability) |
| `public/sw.js` | New | Service Worker (offline + background sync) |
| `src/pages/offline.astro` | New | Offline fallback page |
| `src/components/PWAInstallPrompt.astro` | New | Install prompt banner + background sync helper |
| `src/layouts/BaseLayout.astro` | Modified | Links manifest, registers SW, includes install prompt |
| `src/middleware.ts` | Modified | CSP updated: `worker-src 'self'`, `manifest-src 'self'` |

---

## Features Implemented

### 1. Web App Manifest (`public/manifest.json`)

- **Display mode:** `standalone` (hides browser chrome when installed)
- **Theme color:** `#0891b2` (matches brand primary)
- **Background color:** `#0f172a` (dark splash screen)
- **Shortcuts:** Quick links to `/contact` and `/services` from the home screen
- **Icons:** Uses existing `apple-touch-icon.png` and `favicon.svg`

### 2. Service Worker (`public/sw.js`)

**Caching Strategies:**

| Request Type | Strategy | Details |
|---|---|---|
| Static assets (JS, CSS, images, fonts) | Cache-first | Served from cache; fetched and cached on miss |
| HTML pages | Stale-while-revalidate | Serve cache instantly, update in background |
| API routes (`/api/*`) | Network-only | Never cached — always fresh |

**Pre-cached on install:**
- `/` (home)
- `/offline` (fallback page)
- `/manifest.json`
- `/favicon.svg`
- `/apple-touch-icon.png`

**Cache names (versioned for easy invalidation):**
- `workroot-static-v1`
- `workroot-pages-v1`

To invalidate caches on next deploy, increment `CACHE_VERSION` in `public/sw.js`.

### 3. Offline Fallback (`src/pages/offline.astro`)

- Served at `/offline`
- Clean branded page with "Try Again" and "Go to Home" actions
- Pre-cached by the service worker on install
- Shown automatically when a navigation request fails offline

### 4. Background Sync (`public/sw.js` + `src/components/PWAInstallPrompt.astro`)

When a form submission fails due to no network:

1. Form handler calls `window.queueFormSync(url, method, body, formType)`
2. The payload is stored in **IndexedDB** (`workroot-pwa` DB, `workroot-sync-queue` store)
3. A background sync tag `form-sync` is registered with the browser
4. When connectivity is restored, the service worker automatically retries all queued submissions
5. On success, clients receive a `pwa:sync-success` custom event they can listen to

**Usage in form pages:**
```js
// In contact or newsletter form submit handler:
try {
  const res = await fetch('/api/contact', { method: 'POST', body: JSON.stringify(data) });
  if (!res.ok) throw new Error('Server error');
  // handle success
} catch {
  if (!navigator.onLine) {
    await window.queueFormSync('/api/contact', 'POST', data, 'contact');
    showMessage('You are offline. Your message will be sent when you reconnect.');
  }
}

// Listen for sync success:
window.addEventListener('pwa:sync-success', (e) => {
  if (e.detail.formType === 'contact') {
    showMessage('Your message was sent successfully!');
  }
});
```

### 5. Install Prompt (`src/components/PWAInstallPrompt.astro`)

- Listens for `beforeinstallprompt` event (Chrome/Edge/Android)
- Shows a non-intrusive banner after 4 seconds on first visit
- Banner is suppressed for 7 days after dismissal (stored in `localStorage`)
- Hides immediately after successful installation (`appinstalled` event)
- Accessible: uses `role="banner"`, `aria-label`, button labels

---

## Security Considerations

The CSP in `src/middleware.ts` was updated to include:

```
worker-src 'self'   → allows service worker from same origin only
manifest-src 'self' → allows manifest.json from same origin only
```

These are deliberately restrictive — no external workers or manifests are permitted.

---

## Offline Functionality Testing

### Manual Testing in Chrome DevTools

1. Open DevTools → **Application** tab
2. **Service Workers** panel:
   - Confirm `sw.js` is registered and active
   - Use "Offline" checkbox to simulate offline
3. **Manifest** panel:
   - Verify all manifest fields are correct
   - Check "Add to home screen" works
4. **Cache Storage** panel:
   - Verify `workroot-static-v1` and `workroot-pages-v1` exist with expected entries
5. Navigate to `/contact` while offline → should serve cached page
6. Navigate to a page not in cache while offline → should show `/offline`

### Automated Testing

The offline behavior can be tested with Playwright:

```typescript
// In a Playwright test:
await context.setOffline(true);
await page.goto('/contact');
// Should either show cached page or /offline fallback
```

---

## Updating the Service Worker

To force all users to get the new service worker immediately:

1. Increment `CACHE_VERSION` in `public/sw.js` (e.g. `'v1'` → `'v2'`)
2. Old caches will be deleted during the `activate` event
3. `skipWaiting()` + `clients.claim()` ensure immediate takeover

---

## Lighthouse PWA Checklist

| Criterion | Status |
|-----------|--------|
| Registers a service worker | ✅ |
| Responds with 200 when offline | ✅ (cached pages + `/offline` fallback) |
| `<meta name="viewport">` set | ✅ (BaseLayout) |
| `<meta name="theme-color">` set | ✅ `#0891b2` |
| Web App Manifest with `name`, `short_name`, `icons` | ✅ |
| Icons at 192×192 and 512×512 | ⚠️ Only `apple-touch-icon.png` (180×180) — add larger icons for perfect score |
| Manifest `display: standalone` | ✅ |
| HTTPS in production | ✅ (HSTS enforced) |
| Install prompt supported | ✅ |

### Recommended Improvement

Add 192×192 and 512×512 PNG icons to `public/` and reference them in `manifest.json` to achieve a perfect Lighthouse PWA score:

```json
{
  "src": "/icon-192.png",
  "sizes": "192x192",
  "type": "image/png",
  "purpose": "any"
},
{
  "src": "/icon-512.png",
  "sizes": "512x512",
  "type": "image/png",
  "purpose": "any maskable"
}
```

---

## Known Limitations

- **iOS Safari:** Background sync is not supported. Queued forms will be retried when the user next opens the app with network access (via the SW `sync` event on supported platforms). On iOS, consider showing a manual retry prompt.
- **SSR + Service Worker:** Since Astro runs SSR, HTML responses are dynamic. The stale-while-revalidate strategy serves potentially stale HTML while fetching fresh content in the background. This is acceptable for a marketing site.
- **API routes are always network-only.** This is intentional — form submissions and the health API must never serve stale responses.
