Skip to content

PRD-035 — Mobile offline: right-sized assets + a downloadable corpus

Status: Shipped (v0.8.0, 2026-08-27) · Implemented by: RFC-040 · right-sized mobile imagery + offline Basic/Full tiers + installed-PWA CacheStorage all live (CHANGELOG [0.8.0]). Leftover: #482 — 62 .jpg (deep-sky / launch-ground) still to convert to a webp base for the mobile rung.

One-liner. The mobile app should (a) only ever fetch phone-sized imagery, and (b) let the user pre-download the app for offline use — in a Basic or Full tier — from Settings.

Why this is a PRD · Orrery's reach depends on being usable where a curious person actually is — on a plane, in a classroom with weak wifi, on a metered connection. Today the mobile app silently over-fetches desktop-grade imagery a phone can't show, and goes dark the moment connectivity does. Making the app right-sized and deliberately downloadable turns "needs a good connection" into "works anywhere you chose to take it" — a change in who can use Orrery and when, not just a size optimization.

1 · The problem (measured, 2026-08-25)

Two findings from an asset audit, both real:

A. Mobile over-fetches on a small, specific set — a targeted pipeline gap. The app has a responsive srcset ladder ({id}-{width}.webp rungs + a .1x1.jpg 512px thumb + an unsuffixed base = the largest). The browser picks the right rung per device. The manifest-measured reality (scripts/mobile/measure-mobile-corpus.mjs, 2026-08-25): 3146 images are already mobile-OK (a rung ≤1280px is available). The genuine over-serve was 141 images / 33 MB — deep-sky (13.7), launch-ground (8.3), essays (6.6), missions/gemini (4.9) — with no rung ≤1280, so a phone fetched the full base.

Scope correction. An earlier framing (this PRD's first draft) said "the ladder covers only 859/3089 images" and named galleries/missions/hotspots as the problem. That was a file-count artifact — an image whose base is already ≤1280 needs no separate rung file, so counting -1280.webp files massively undercounts what srcset already handles. The manifest (what the browser actually uses) shows galleries/missions/anatomy are already right-sized. hotspots (185 MB) are NOT over-serve either — the surface-scene tier dispatcher gates on projected screen px, so mobile already loads lower tiers per zoom. The real gap was the 141 above.

B. No offline mode. The mobile app is deliberately ~66 MB (bundle) and streams the heavy set (images 1.1 GB, audio 97 MB, 4K textures, non-en locales) from the CDN on demand. With no connectivity — a flight — the streamed content simply isn't there and the review/browse experience breaks. Operator hit this on a plane (2026-08).

Corpus sizes (source, measured): images 1.1 GB (fleet-galleries 406 · missions 202 · hotspots 188 · anatomy 160 · rest ~150) · audio 97 MB · 4K textures ~40 MB prune-able. Crucially, the 1.1 GB is the whole ladder at every rung (base .webp alone = 512 MB, mostly desktop-only rungs). A phone displays one rung per image.

2 · Why the two parts are one initiative

Offline depends on right-sizing. "Download everything" naively = ~1.3 GB of desktop-res a phone can't use. Once every image has a phone rung, the whole corpus at mobile resolution is ~300–450 MB, and offline tiers become sane. So Part 1 is the prerequisite for Part 2. They ship as two GitHub issues (Part 1 first), one RFC (RFC-040), one PRD (this).

3 · Part 1 — right-sized mobile assets (#482 — DONE)

Goal: every displayed image has a phone-appropriate variant, so mobile never fetches a desktop rung — online or in the offline cache.

Shipped (#482, commit 5e83ca872a):

  • scripts/mobile/measure-mobile-corpus.mjs — the reproducible audit (over-serve per bucket + the mobile-res corpus total, ~325 MB, which feeds Part 2's tier budget).
  • scripts/mobile/add-mobile-rung.mjs — generated the ≤1280 rung for the 79 webp-base over-serve images (essays 53 + missions/gemini 26) by downscaling the served base (no masters / only LFS stubs; one lanczos step avoids the full-corpus-regen degrade the guards prevent). Additive, idempotent, diff-gated, zero base images touched. Deferred: the 62 .jpg-base over-serve images (deep-sky, launch-ground, ~22 MB) can't join the webp-only ladder without a .webp-base format conversion first — a separate follow-up, not a ladder hack. So the live over-serve fix covers ~11.5 MB now; the .jpg ~22 MB remains until that conversion.
  • image-srcset.test.ts invariant: every ladder entry has a rung ≤1280 (regression guard). Over-serve now 0.

Non-goals (Part 1): changing desktop imagery; re-encoding already-right-sized buckets (galleries/missions/anatomy already are); capping hotspots (the tier dispatcher already handles mobile); the offline download itself.

4 · Part 2 — the offline download (two tiers, from Settings)

Goal: the user can pre-download the app for offline use, choosing how much.

Two tiers (operator decision — keep it simple):

TierContentsEst. size (mobile-res)
BasicCore browse: planets, moon/mars sites, science, essays, deep-sky, posters, modules, satellites, small bodies — at the mobile rung. Fully usable offline: browse the solar system, missions overview, read all content.~150–250 MB
FullEverything: all galleries + surface hotspots + audio tours (97 MB) + 4K textures. The whole corpus for a long flight.~400–550 MB
  • Lives in the Settings panel (QualitySettingsModal.svelte, the nav ⚙) — a new "Offline" section, mobile-only (Capacitor.isNativePlatform()).
  • States: not-downloaded → downloading (progress + size) → downloaded (with "Update" + "Remove offline data"). Wi-Fi-only by default with an explicit cellular override; show the size before starting.
  • Default is unchanged: the app stays ~66 MB and streams on demand. Offline is strictly opt-in.

Success: on a fresh device with the chosen tier downloaded, airplane-mode browse works for that tier's surfaces with no missing imagery/audio.

Non-goals (Part 2): offline for the web PWA (mobile-app only for now); partial/per-section downloads (just the two tiers); background/auto download.

5 · Open decisions (locked here)

  • Tiers: two — Basic / Full. Tours + 4K fold into Full (not separate toggles).
  • Surface: the existing Settings panel, not a new route.
  • Platform: mobile app only (iOS + Android).
  • Part 1 scope: in — it's the prerequisite and fixes a live over-fetch.

Architecture (persistence, manifests, width targets, Wi-Fi guard, storage budget) → RFC-040.

Orrery — architecture documentation · MIT · No tracking