mirror of
https://github.com/mauricioabh/arbpulse.git
synced 2026-07-27 23:57:43 +00:00
feat(pwa): installable PWA + responsive mobile dashboard
- Add Web App Manifest + ArbPulse icons (192/512/maskable/apple-touch) under web/public/; no service worker (real-time honesty: never serve stale data). - Add mobile/install metadata to index.html (theme-color, viewport-fit=cover, apple-mobile-web-app-*, manifest + apple-touch-icon links). - Responsive layout: PriceMatrix stacked per-venue cards below sm, StatsBar reflow, ~44px touch targets in Controls/ConfigPanel, safe-area top inset. - scripts/gen-icons.ps1 derives icon sizes from the master (no new deps). - openspec: add-pwa-mobile change (proposal/design/specs/tasks). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-16
|
||||
@@ -0,0 +1,79 @@
|
||||
## Context
|
||||
|
||||
The dashboard (`web/`, React 18 + Vite 6 + Tailwind v3) is served by Express as
|
||||
static files from `web/dist` (same origin as `/api` + SSE). `App.tsx` already uses a
|
||||
`grid-cols-1 lg:grid-cols-3` layout, so it collapses to a single column on mobile,
|
||||
but two things are missing for a good phone experience: (1) the `PriceMatrix` order
|
||||
book is a 6-column `<table>` that overflows horizontally on ~360–430px screens, and
|
||||
(2) there is no install/mobile metadata (manifest, theme-color, apple-touch-icon).
|
||||
|
||||
The app is real-time: the frontend consumes `/api/state` over SSE and shows a
|
||||
`connected` badge. A domain rule (honesty) forbids ever presenting stale or fake
|
||||
market data. The target runtime is the 24/7 VPS behind HTTPS at
|
||||
`arbpulse.wayool.com` (nginx + certbot, Cloudflare DNS-only).
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Installable to the phone home screen (standalone, branded icon).
|
||||
- Responsive, touch-friendly dashboard on phones without regressing desktop.
|
||||
- Zero risk of showing cached/stale market data.
|
||||
- No new runtime dependencies; no change to build/deploy steps.
|
||||
|
||||
**Non-Goals:**
|
||||
- Offline support of any kind (no cached app shell, no cached data).
|
||||
- Push notifications / background sync.
|
||||
- A native app or app-store packaging.
|
||||
- Redesigning the dashboard; this is layout + metadata only.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: No service worker (installable via manifest + HTTPS only)
|
||||
Since Chrome 108 (mobile) / 112 (desktop), a service worker is no longer required to
|
||||
install from the browser menu / "Add to Home Screen"; iOS Safari never required one.
|
||||
No-op `fetch` handlers are now actively skipped and warned about by Chrome. Because
|
||||
this app must never serve cached data, we ship **no service worker at all**.
|
||||
- **Chosen:** manifest + icons + HTTPS → installable; SSE always fetches live data.
|
||||
- **Alternatives:** `vite-plugin-pwa`/Workbox with a shell precache (rejected: its
|
||||
value is offline/precaching, which conflicts with the no-stale-data rule and adds
|
||||
a dependency); a minimal passthrough SW just to get the auto-install banner
|
||||
(rejected: Chrome ignores no-op fetch handlers, and it adds a moving part for a
|
||||
banner we don't need — manual install from the menu is sufficient).
|
||||
- **Trade-off:** no automatic install prompt on Android; users install from the
|
||||
browser menu (the only path on iOS anyway). Acceptable.
|
||||
|
||||
### D2: PriceMatrix responsive strategy — stacked cards below `sm`
|
||||
Render per-venue cards (exchange + bid/ask/qty/spread as label–value rows) on small
|
||||
screens and keep the existing `<table>` from `sm` up.
|
||||
- **Chosen:** Tailwind responsive classes to swap layouts (`block sm:table` pattern
|
||||
or two branches gated by breakpoint) — no JS, no `matchMedia`, purely CSS.
|
||||
- **Alternative:** wrap the table in `overflow-x-auto` (rejected as primary: a
|
||||
6-column price table sideways-scrolling on a phone is poor UX for the primary
|
||||
widget; horizontal scroll can remain as a defensive fallback).
|
||||
|
||||
### D3: Icons derived from one master
|
||||
Generate a single ArbPulse master icon (pulse line → Bitcoin "B", blue→green on
|
||||
`#0a0e14`) and derive `icon-192.png`, `icon-512.png`, `icon-maskable-512.png`
|
||||
(with safe padding), and `apple-touch-icon.png` (180). Store under `web/public/` so
|
||||
Vite copies them verbatim to `web/dist`.
|
||||
- **theme_color/background_color:** `#0a0e14` (matches the dashboard background).
|
||||
|
||||
### D4: Manifest + head metadata, same-origin
|
||||
`manifest.webmanifest` uses `start_url: "/"`, `scope: "/"`, `display: "standalone"`.
|
||||
`index.html` adds `theme-color`, `apple-mobile-web-app-capable`,
|
||||
`apple-mobile-web-app-status-bar-style`, `apple-mobile-web-app-title`,
|
||||
`viewport-fit=cover` (already has `width=device-width, initial-scale=1`), and links
|
||||
to the manifest + apple-touch-icon. Everything is same-origin, so no CORS/proxy
|
||||
concerns and the existing Express static + SPA fallback serve it unchanged.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Stale data if a SW is ever added later] → Documented no-SW decision; if a SW is
|
||||
introduced in the future it MUST deny-list `/api` and never cache the SSE stream.
|
||||
- [iOS quirks: no auto-prompt, status-bar/notch rendering] → Provide
|
||||
`apple-touch-icon`, `apple-mobile-web-app-*`, and `viewport-fit=cover`; verify on a
|
||||
real iPhone (Add to Home Screen), not just Android.
|
||||
- [PriceMatrix layout duplication risk] → Keep a single data mapping and switch only
|
||||
presentation via breakpoints to avoid divergent desktop/mobile logic.
|
||||
- [Maskable icon getting cropped] → Keep the glyph within the ~80% safe zone in the
|
||||
maskable variant.
|
||||
@@ -0,0 +1,48 @@
|
||||
## Why
|
||||
|
||||
The Arb Pulse dashboard is desktop-first: the 3-column grid collapses acceptably on
|
||||
narrow screens, but the order-book table overflows horizontally and there is no
|
||||
mobile/PWA metadata. We want to demo and monitor the engine from a phone —
|
||||
installable to the home screen, full-window, and touch-friendly — while the app
|
||||
runs 24/7 on the VPS behind HTTPS (`arbpulse.wayool.com`).
|
||||
|
||||
## What Changes
|
||||
|
||||
- Make the dashboard fully responsive on phones (~360–430px): stack the
|
||||
`PriceMatrix` order book as per-venue cards below `sm`, reflow the `StatsBar`
|
||||
badges, and ensure touch targets are at least ~44px in `Controls`/`ConfigPanel`.
|
||||
- Add a Web App Manifest (`web/public/manifest.webmanifest`) so the dashboard is
|
||||
installable to the home screen (name/short_name, `start_url`, `display: standalone`,
|
||||
theme/background color, icons).
|
||||
- Add ArbPulse app icons (192, 512, a maskable variant, and a 180px
|
||||
`apple-touch-icon`) under `web/public/`.
|
||||
- Add mobile/install metadata to `web/index.html`: `theme-color`,
|
||||
`apple-mobile-web-app-*`, `viewport-fit=cover`, and links to the manifest and
|
||||
apple-touch-icon.
|
||||
- **No service worker and no offline support.** The app is real-time (SSE); it must
|
||||
never serve cached/stale market data. Installability comes from the manifest +
|
||||
HTTPS (menu / "Add to Home Screen"), not from a cached shell. When disconnected
|
||||
the UI shows its existing "reconnecting/offline" state, never fake data.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `pwa`: installability and mobile-web behavior of the dashboard — how the app is
|
||||
installable to the home screen via manifest + HTTPS with no service worker, how
|
||||
it stays responsive on small screens, and the explicit no-offline / no-stale-data
|
||||
guarantee for this real-time app.
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- None: responsiveness and install metadata are new behavior; no existing spec's requirements change. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- Frontend: `web/index.html` (head metadata), `web/public/` (new manifest + icons),
|
||||
`web/src/components/PriceMatrix.tsx` and `StatsBar.tsx` (responsive layout),
|
||||
minor touch-target tweaks in `Controls.tsx`/`ConfigPanel.tsx`.
|
||||
- Build/serve: none — `npm run build` copies `web/public/` into `web/dist`, already
|
||||
served by Express static; the Docker image bakes it in. No new deploy steps.
|
||||
- Dependencies: none added (no `vite-plugin-pwa`/Workbox, since there is no SW).
|
||||
- Behavior: no API, wire-format, or engine changes. Desktop layout unchanged.
|
||||
- Testing: verified on a real phone at `https://arbpulse.wayool.com` after the
|
||||
normal `dev → PR → main → deploy.sh` flow.
|
||||
@@ -0,0 +1,58 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Installable to the home screen
|
||||
The dashboard SHALL be installable to a device home screen via a Web App Manifest
|
||||
served over HTTPS, without requiring a service worker.
|
||||
|
||||
#### Scenario: Manifest is present and valid
|
||||
- **WHEN** the dashboard is loaded over HTTPS
|
||||
- **THEN** a linked `manifest.webmanifest` is served that includes `name`,
|
||||
`short_name`, `start_url` in scope, `display: standalone`, `theme_color`,
|
||||
`background_color`, and at least 192px and 512px PNG icons
|
||||
|
||||
#### Scenario: User installs from the browser
|
||||
- **WHEN** a user chooses "Install app" / "Add to Home Screen" from the browser menu
|
||||
- **THEN** the app is added with the ArbPulse icon and launches in a standalone,
|
||||
full-window mode at `start_url`
|
||||
|
||||
### Requirement: No offline mode and no stale data
|
||||
The app SHALL NOT register a service worker and SHALL NOT cache market data, so it
|
||||
never presents stale or fake data (real-time honesty rule).
|
||||
|
||||
#### Scenario: No service worker is registered
|
||||
- **WHEN** the dashboard runs in any environment
|
||||
- **THEN** no service worker is registered and no application cache stores `/api`
|
||||
responses or the SSE stream
|
||||
|
||||
#### Scenario: Disconnected device shows a clear non-data state
|
||||
- **WHEN** the device has no working connection to the engine
|
||||
- **THEN** the UI shows its reconnecting/offline indicator and does not display
|
||||
cached market figures as if they were live
|
||||
|
||||
### Requirement: Responsive mobile layout
|
||||
The dashboard SHALL be usable on phone-sized viewports (~360–430px wide) without
|
||||
horizontal overflow of primary widgets, while the desktop layout is unchanged.
|
||||
|
||||
#### Scenario: Order book fits a phone screen
|
||||
- **WHEN** the viewport is narrower than the `sm` breakpoint
|
||||
- **THEN** the order-book (`PriceMatrix`) is presented as per-venue stacked cards
|
||||
showing bid, ask, quantities, and spread without horizontal scrolling
|
||||
|
||||
#### Scenario: Stats and controls remain readable and tappable
|
||||
- **WHEN** the dashboard is viewed on a phone
|
||||
- **THEN** the stats bar reflows without clipping and interactive controls have
|
||||
touch targets of at least ~44px
|
||||
|
||||
#### Scenario: Desktop layout is preserved
|
||||
- **WHEN** the viewport is at the `lg` breakpoint or wider
|
||||
- **THEN** the existing multi-column layout and table order book render as before
|
||||
|
||||
### Requirement: Mobile and install metadata
|
||||
The `index.html` head SHALL include the metadata needed for correct mobile rendering
|
||||
and home-screen installation on Android and iOS.
|
||||
|
||||
#### Scenario: Head includes mobile/install tags
|
||||
- **WHEN** the page is served
|
||||
- **THEN** the head contains a `theme-color`, `viewport-fit=cover` in the viewport
|
||||
meta, a link to the manifest, an `apple-touch-icon`, and the
|
||||
`apple-mobile-web-app-*` tags for standalone iOS launch
|
||||
@@ -0,0 +1,39 @@
|
||||
## 1. Icons
|
||||
|
||||
- [x] 1.1 Finalize the ArbPulse master icon and derive `icon-192.png`,
|
||||
`icon-512.png`, `icon-maskable-512.png` (glyph within ~80% safe zone), and
|
||||
`apple-touch-icon.png` (180px)
|
||||
- [x] 1.2 Place all icons under `web/public/`
|
||||
|
||||
## 2. Manifest
|
||||
|
||||
- [x] 2.1 Create `web/public/manifest.webmanifest` with `name`, `short_name`,
|
||||
`start_url: "/"`, `scope: "/"`, `display: "standalone"`,
|
||||
`theme_color`/`background_color` `#0a0e14`, and icon entries (192, 512, and
|
||||
512 maskable with `"purpose": "maskable"`)
|
||||
|
||||
## 3. HTML head metadata
|
||||
|
||||
- [x] 3.1 In `web/index.html`, add `viewport-fit=cover` to the viewport meta and a
|
||||
`theme-color` meta (`#0a0e14`)
|
||||
- [x] 3.2 Add `<link rel="manifest">`, `<link rel="apple-touch-icon">`, and the
|
||||
`apple-mobile-web-app-capable` / `apple-mobile-web-app-status-bar-style` /
|
||||
`apple-mobile-web-app-title` tags
|
||||
|
||||
## 4. Responsive layout
|
||||
|
||||
- [x] 4.1 `PriceMatrix.tsx`: render per-venue stacked cards (exchange + bid/ask/qty/
|
||||
spread as label–value rows) below `sm`; keep the `<table>` from `sm` up, with
|
||||
no horizontal overflow on phones
|
||||
- [x] 4.2 `StatsBar.tsx`: reflow stats and badges on mobile without clipping
|
||||
- [x] 4.3 Ensure touch targets in `Controls.tsx` and `ConfigPanel.tsx` are ~44px min
|
||||
- [x] 4.4 Sanity-check `App.tsx` spacing/padding on narrow viewports
|
||||
|
||||
## 5. Verify
|
||||
|
||||
- [x] 5.1 `npm run typecheck` and `npm test` pass
|
||||
- [x] 5.2 `npm run build` succeeds and `web/dist` contains the manifest + icons
|
||||
- [x] 5.3 DevTools device emulation: no horizontal overflow at ~375px; Application →
|
||||
Manifest shows no errors; confirm no service worker is registered
|
||||
- [ ] 5.4 After `dev → PR → main → deploy.sh`, install on a real phone from
|
||||
`https://arbpulse.wayool.com` and confirm standalone launch + live SSE data
|
||||
Reference in New Issue
Block a user