Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c72c286
docs: add upload flow spec and plan
Meerav29 Oct 4, 2026
e317952
chore(web): add fflate with ZIP64 fixtures and a streaming-unzip spike
Meerav29 Oct 4, 2026
7198fdc
fix(api): report a Supabase outage as 503 instead of 'no health data'
Meerav29 Oct 4, 2026
4804e9e
feat(web): upload error codes, metric map, summary stats and parse re…
Meerav29 Oct 4, 2026
fbf2edf
feat(web): streaming Apple Health parser core with fixtures and chunk…
Meerav29 Oct 4, 2026
0e12c27
test(web): prove the new parser matches the classic parser's output
Meerav29 Oct 4, 2026
8a85ff1
feat(web): read export.xml or export.zip as a stream of XML chunks
Meerav29 Oct 4, 2026
94ef8c0
feat(web): repository that saves a parsed export and keeps existing l…
Meerav29 Oct 4, 2026
eeebf5e
feat(web): parse worker and useHealthUpload hook with cancel, retry a…
Meerav29 Oct 4, 2026
a9dda15
feat(web): upload screens: guide, drop zone, progress, preview and er…
Meerav29 Oct 4, 2026
b412174
fix(web): announce reading progress only at 25% milestones
Meerav29 Oct 4, 2026
4181514
feat(web): upload page, shared health query, nav link and dashboard e…
Meerav29 Oct 4, 2026
1d42057
docs: upload flow in README and CLAUDE.md; correct two spec details
Meerav29 Oct 4, 2026
bc9fea0
fix(web): report unexpected worker errors as worker-failed, map auth …
Meerav29 Oct 5, 2026
5557a08
docs: fix upload-flow spec out-of-scope contradiction and ZIP junk ru…
Meerav29 Oct 5, 2026
fe96f5b
test(web): parity extras fixture for rounding, sleep precision, midni…
Meerav29 Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ Returns `{ url, anonKey }` for frontend initialization.
- The Settings page is where users manage their BYOK key. A provider dropdown lets them choose Vertex AI or Claude for their own key.
- BYOK localStorage keys: `userByokApiKey` (the key), `userByokProvider` (`vertex` or `claude`). The legacy key `userVertexApiKey` is still read for migration but new saves go to `userByokApiKey`.
- `syncChatMode()` no longer gates Vertex/Claude on server key availability — both are always selectable. Only BYOK requires a saved key.
- The new app's upload flow (`web/src/features/upload/`, parser in `web/src/lib/health/`): `parseExport` streams XML chunks into per-day running totals (O(days) memory) inside a Web Worker; `fileToChunks` reads `.xml` or streams the health XML entry out of `.zip` with `fflate`; `repository.ts` saves under RLS and keeps the user's existing `events`. `parity.test.ts` extracts the classic `parseHealthXML` from `index.html` and asserts identical output, so the two parsers must stay in step until the classic one is removed.
- `/api/health` and `/api/chat` answer `503` (not "No health data found") when Supabase cannot be queried.

---

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ Open it with the trailing slash (`/app/`).

If the classic page is missing at `http://localhost:3000/` under `vercel dev`, build it first with `npm --prefix web run build && node scripts/build-site.mjs`.

### Uploading Apple Health data in the new app

Open `/app/upload`, drop Apple's `export.zip` (or `export.xml`) and review the preview, then press **Save**. The file is parsed in your browser by a Web Worker (see `web/src/lib/health/`), and only aggregated daily numbers are saved to your account. Re-uploading replaces your data and keeps your life events. The classic upload at `/` still works.

---

## Features
Expand Down
9 changes: 8 additions & 1 deletion api/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from api.auth import AuthError, authenticate
from api.chat_core import (
HealthDataUnavailable,
chat_with_claude,
chat_with_vertex,
get_claude_model,
Expand Down Expand Up @@ -35,7 +36,13 @@ def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length))

health_data = load_health_data(auth=auth)
try:
health_data = load_health_data(auth=auth)
except HealthDataUnavailable:
self._send_json(
{"error": "Health data is temporarily unavailable. Please try again."}, 503
)
return
if not health_data:
self._send_json({"reply": "No health data available."})
return
Expand Down
12 changes: 9 additions & 3 deletions api/chat_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def get_claude_model():
return (os.environ.get("CLAUDE_MODEL") or "").strip() or DEFAULT_CLAUDE_MODEL


class HealthDataUnavailable(Exception):
"""Supabase could not be queried, so we cannot say whether the user has data."""


def fetch_supabase_health_data(auth):
"""Return the caller's own health data, else the shared demo data, else None.

Expand All @@ -62,14 +66,16 @@ def fetch_supabase_health_data(auth):
f"{base}&user_id=eq.{urllib.parse.quote(auth.user_id, safe='')}", headers
)
if status != 200:
# Never mask an outage as the user's data by serving the demo row.
return None
# An outage must never look like "no data" and must never serve the demo row.
raise HealthDataUnavailable(f"health_data query failed with status {status}")
if rows:
return rows[0].get("data")

# Signed in, query succeeded, no row yet: fall back to the shared demo data.
status, rows = supabase_http.http_get_json(f"{base}&is_demo=eq.true", headers)
if status == 200 and rows:
if status != 200:
raise HealthDataUnavailable(f"demo data query failed with status {status}")
if rows:
data = rows[0].get("data")
if isinstance(data, dict):
return {**data, "is_demo": True}
Expand Down
10 changes: 8 additions & 2 deletions api/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import json

from api.auth import AuthError, authenticate
from api.chat_core import load_health_data
from api.chat_core import HealthDataUnavailable, load_health_data


class handler(BaseHTTPRequestHandler):
Expand All @@ -13,7 +13,13 @@ def do_GET(self):
self._send_json({"error": str(e)}, 401)
return

data = load_health_data(auth=auth)
try:
data = load_health_data(auth=auth)
except HealthDataUnavailable:
self._send_json(
{"error": "Health data is temporarily unavailable. Please try again."}, 503
)
return
if data is None:
data = {"error": "No health data found."}
self._send_json(data)
Expand Down
Loading
Loading