# PWA Deployment for Web Bluetooth

Working deployment recipe used to ship the LED PWA at
`https://miopenclaw-vnic.tail9799d2.ts.net/led/`. The site is reachable only on
Chicho's tailnet but Tailscale Funnel is also configured to expose it publicly.

## Architecture

```
~/led-pwa/
├── index.html           # single file, inline CSS+JS
├── manifest.json
├── icon-192.png         # generated via image_gen if missing
├── icon-512.png
└── sw.js                # tiny service worker (cache-first, single version)
```

Served by a systemd-managed `python3 -m http.server 3010 --bind 127.0.0.1`,
exposed on `100.87.116.90:3010` (Tailscale) and `https://*tail9799d2.ts.net/led/`
(Tailscale Funnel).

## Systemd unit (worked first try)

```ini
[Unit]
Description=LED PWA
After=network.target

[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/led-pwa
ExecStart=/usr/bin/python3 -m http.server 3010 --bind 127.0.0.1
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
```

Enable & start:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now led-pwa
sudo systemctl status led-pwa
```

## Tailscale Funnel (gives the HTTPS Web Bluetooth requires)

Tailscale Funnel was already configured on the host, but the command to expose a
new path is:
```bash
tailscale funnel 3010 on
# or for a path prefix:
tailscale funnel --set-path=/led 3010 on
```

After `on`, `tailscale funnel status` shows the public URL. The funnel terminates
TLS at the Tailscale edge, so the local service can stay HTTP.

## Manifest gotchas (PWA install on Android)

1. **`start_url` must be `"./"` not `"/"`** — when the manifest is served under
   `/led/`, an absolute `/` resolves to the host root, breaking the offline launch.
2. **`src` for icons must be `"icon-192.png"` (relative) not `"/icon-192.png"`.**
3. **`scope` must match the directory of `start_url`**: `"./"`.
4. **`display: standalone`** for full-screen app feel.
5. **No `serviceworker.js` registration in the HTML before the manifest link** —
   Chrome skips install if SW registration is broken.

Minimal working manifest:
```json
{
  "name": "LED Control",
  "short_name": "LED",
  "start_url": "./",
  "scope": "./",
  "display": "standalone",
  "background_color": "#0b0d12",
  "theme_color": "#0b0d12",
  "icons": [
    { "src": "icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "icon-512.png", "sizes": "512x512", "type": "image/png" }
  ]
}
```

## Web Bluetooth code patterns that worked

### Scanning with both LEDBLE and LEDDMX UUIDs in `optionalServices`

```javascript
const opts = {
  acceptAllDevices: true,
  optionalServices: [
    '0000ffe0-0000-1000-8000-00805f9b34fb',  // LEDBLE/LEDDMX service
    '0000fff0-0000-1000-8000-00805f9b34fb',  // ELK service
  ]
};
const device = await navigator.bluetooth.requestDevice(opts);
```

`acceptAllDevices: true` is the only way to see LEDDMX-named devices without
hardcoding the name prefix. The name comes back on the device object after
`device.name`.

### Auto-detect protocol from name

```javascript
const name = device.name || '';
const isLEDDMX = name.startsWith('LEDDMX');
const isLEDBLE = name.startsWith('LEDBLE') || name.startsWith('BLELED');
const isELK    = name.startsWith('ELK') || name.startsWith('MELK');
```

### Robust write helper

```javascript
async function send(char, bytes) {
  const u8 = new Uint8Array(bytes);
  try {
    await char.writeValueWithoutResponse(u8);
  } catch (e) {
    // Some devices/chars require a write request
    await char.writeValue(u8);
  }
}
```

### Debounce slider input to ~30ms

BLE write rate is throttled by the radio, not by your code. Sliders that fire
`input` at 60Hz flood the radio and writes start getting dropped. Debounce to
~30ms (~33 writes/sec, which is what Web Bluetooth caps at anyway).

```javascript
let timer;
slider.addEventListener('input', () => {
  updatePreview();
  clearTimeout(timer);
  timer = setTimeout(() => sendColor(), 30);
});
```

## UI structure that worked on Android (mobile-first, ~420px max-width)

1. **Connect view** — big primary button "🔍 Conectar tira", device list, manual
   protocol override chips ("Forzar LEDDMX", "Forzar LEDBLE", "Forzar ELK").
2. **Control view** (after connect):
   - Power row: green "ON" + red "OFF" buttons side by side.
   - Color preview rectangle (current color).
   - Three range sliders (R, G, B) with red/green/blue thumbs.
   - Brightness slider.
   - Color preset grid: 12 swatches in a 4×3 grid.
   - Effects grid: 8-10 buttons in a 4-col grid.
   - Disconnect button at the bottom.
3. **Debug accordion** (collapsible, hidden by default):
   - "Test ON/OFF/Color" button that fires a full sequence.
   - Live log of all sent writes.
4. **Toast/notification** strip at the bottom for status.

Avoid modals on mobile — they block the BT picker. Use bottom sheets or
collapsible sections instead.

## Web Bluetooth gotchas hit during testing

1. **Device must be disconnected from the original app first.** If `com.ledlamp`
   is in the foreground and connected, Web Bluetooth will pair but writes silently
   fail (the radio is busy). Force-stop the original app before testing.
2. **GATT services don't enumerate until you call `getPrimaryService()`.** Don't
   rely on `service.getIncludedServices()` to discover chars — call the specific
   UUID you want.
3. **User must invoke the BT picker from a user gesture** (button click). Don't
   try to auto-reconnect on page load — Chrome blocks it.
4. **iOS Safari does not support Web Bluetooth.** Only Chrome on Android, desktop
   Chrome/Edge, and ChromeOS work. Inform the user up front.
5. **Page must be served from a secure context** (HTTPS or localhost). Tailscale
   Funnel is HTTPS, so it works. `127.0.0.1` would also work for local testing.

## Iterating the PWA live

When the user is testing on their phone and you need to push a code change:

```bash
# Just edit the file in ~/led-pwa/index.html — the http.server picks it up
# immediately, no restart needed. The user just needs to refresh the page
# (or pull-to-refresh if installed as PWA).
```

For PWA-installed icons, force Chrome to refetch the manifest:
DevTools → Application → Manifest → "Update manifest".

## Reference: the working URL was

`https://miopenclaw-vnic.tail9799d2.ts.net/led/`

— served from `~/led-pwa/`, port 3010, systemd unit `led-pwa`, funneled by
Tailscale.
