Skip to content

Mobile local access: QR pairing, mDNS, responsive dashboard #15

@jayzalowitz

Description

@jayzalowitz

Context

SkyTwin runs locally on the user's desktop. Approval requests need timely responses — but users aren't always at their computer. Today there's no way to approve actions from a phone. For daily use and the HN launch, phone access on the same WiFi network is essential: scan a QR code, open the dashboard, approve from your pocket.

Claude Code estimate: ~3-4h

Current State (verified 2026-04-04)

  • Web dashboard: static SPA served by Express, works in any browser
  • Mobile CSS: basic responsive fixes shipped in v0.1 (mobile nav backdrop, sidebar collapse) but not systematically tested
  • Authentication: no session tokens — dashboard talks directly to API on localhost
  • mDNS/Bonjour: not implemented
  • QR codes: not implemented
  • HTTPS: not implemented (HTTP only)

Proposed Change

1. mDNS service advertisement

Register Bonjour service on API start via `bonjour-service`. Phone finds desktop as `skytwin.local`.

2. QR code pairing

Desktop generates session token (crypto.randomUUID + HMAC), stores hash in DB, encodes URL as QR via `qrcode` npm package. Phone scans, stores token in localStorage, includes in `Authorization` header. 7-day expiry with auto-refresh.

3. Session auth middleware

Check `Authorization: Bearer` header for valid session token. Skip for localhost. Session management endpoints for listing and revoking.

4. Responsive dashboard

Bottom nav bar on < 768px. Full-width approve/reject buttons. Vertical card stacking. Collapsible filter controls. Min 44px touch targets.

5. Local HTTPS (stretch)

Self-signed cert on first launch. Required for future Web Push.

DB Schema Addition

```sql
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
token_hash STRING NOT NULL,
device_name STRING DEFAULT 'Phone',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL,
last_active_at TIMESTAMPTZ NOT NULL DEFAULT now(),
revoked BOOLEAN NOT NULL DEFAULT false
);

CREATE INDEX idx_sessions_token ON sessions (token_hash) WHERE revoked = false;
CREATE INDEX idx_sessions_user ON sessions (user_id) WHERE revoked = false;
```

Acceptance Criteria

  1. API server starts → `skytwin.local` resolves on same WiFi network from iOS Safari and Android Chrome (verify via `dns-sd -B _http._tcp` on macOS or equivalent)
  2. Settings page shows QR code → scan with iPhone camera → URL opens in Safari → dashboard loads within 3 seconds
  3. QR URL format: `http://skytwin.local:PORT/mobile?token=` — token is URL-safe, 128+ bits of entropy
  4. Session token stored as HMAC-SHA256 hash in DB — raw token never persisted server-side (verify by inspecting `sessions` table)
  5. Phone makes API request with `Authorization: Bearer ` → request succeeds with 200
  6. Phone makes API request with expired token → returns 401 with body `{"error": "Session expired", "message": "Scan the QR code again from your desktop"}`
  7. Phone makes API request with revoked token → returns 401 (same as expired)
  8. Desktop request from localhost without `Authorization` header → succeeds (localhost bypass)
  9. Token within 1 day of expiry + successful API request → `expires_at` extended by 7 days → `last_active_at` updated
  10. Settings page shows active sessions: device name, created date, last active relative time (e.g., "2 min ago")
  11. Click "Revoke" on a session → `DELETE /api/sessions/:id` → session marked revoked → phone gets 401 on next request
  12. Approvals page on 375px screen width → approve and reject buttons fully visible without horizontal scroll → buttons min height 48px
  13. Bottom navigation bar appears on screens < 768px with 5 items (Home, Approvals, Decisions, Twin, Settings) → approval badge count visible
  14. All 6 dashboard pages (dashboard, approvals, decisions, twin, settings, audit) render without horizontal scroll at 375px width
  15. No interactive element (button, link, input) smaller than 44x44px on mobile layout
  16. Sidebar completely hidden on < 768px (not collapsed — removed from DOM or `display: none`)
  17. Decision detail view on mobile → full-screen overlay (not inline expand)
  18. All 432 existing tests pass
  19. PR passes `/review` before merge

Testing Plan

Layer What Count
Unit Session token generation: entropy, HMAC hashing, URL-safe encoding +3
Unit Session validation: valid, expired, revoked, localhost bypass +4
Unit mDNS service registration and cleanup on shutdown +2
Integration QR scan → token auth → API access → token auto-refresh +2
Integration Session revocation → immediate 401 +1
Visual Each page at 375px, 390px, 768px widths — screenshot comparison 6 pages × 3 widths
E2E Full flow: generate QR → auth → approve from mobile → verify on desktop +1

Files Reference

File Change
`apps/desktop/src/mdns.ts` or `apps/api/src/mdns.ts` New: Bonjour service advertisement
`apps/api/src/middleware/session-auth.ts` New: token auth middleware
`apps/api/src/routes/sessions.ts` New: session CRUD
`packages/db/src/migrations/011-sessions.sql` New: sessions table
`packages/db/src/repositories/session-repository.ts` New: session persistence
`apps/web/public/css/style.css` Mobile responsive overhaul — bottom nav, card stacking, touch targets
`apps/web/public/js/app.js` Bottom nav bar component for mobile, route for `/mobile` entry point
`apps/web/public/js/pages/settings.js` QR code display, session management UI
`apps/web/public/js/pages/approvals.js` Mobile card layout, full-width buttons
`apps/web/public/js/pages/dashboard.js` Vertical stat stacking
`apps/web/public/js/pages/decisions.js` Collapsible filters, overlay detail view
`apps/web/public/js/pages/twin.js` Accordion layout for domains

Out of Scope

Related


Working Context Protocol

During implementation, maintain two sources of truth to survive context compaction:

  1. Local context file: Write progress, decisions, and blockers to .context/issue-15-mobile.md (gitignored). Update this file after each meaningful step. On compaction, re-read this file to restore state.
  2. GitHub issue: Post progress comments on #15 at key milestones (subtask complete, blocker hit, design decision made). Reference the issue URL in your conversation so it persists across compaction: Mobile local access: QR pairing, mDNS, responsive dashboard #15

This ensures no quality loss across compaction events — the local file has granular state, the GitHub issue has durable history.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions