Skip to content

Expand the catalog to 70 worlds, add night art, and fix place naming - #2

Merged
robleto merged 8 commits into
mainfrom
catalog-70-worlds-night-art
Aug 31, 2026
Merged

Expand the catalog to 70 worlds, add night art, and fix place naming#2
robleto merged 8 commits into
mainfrom
catalog-70-worlds-night-art

Conversation

@robleto

@robleto robleto commented Aug 31, 2026

Copy link
Copy Markdown
Owner

70 worlds (20 free / 50 premium), every one painted twice, plus the pricing and
naming changes that fell out of it. 561 files, +5993 / −942 — most of it art.

SCOPE.md was frozen; the amendment at the foot of it records that this clears
none of its three bars and why it went ahead anyway.

Catalog — 43 worlds to 70

The structural rule survives unchanged: free is exactly the set of worlds some
slot defaults to
, so the free app is complete rather than partial, and premium
is the alternates. All 27 new worlds are premium, because no new weather
conditions were added for them to be the default of. Daiyu moved free → premium
to land the free tier on exactly 20; Kamino absorbs rain_cold with slot copy
written for the cold reading.

Both halves of the rule are now asserted on both platforms — a premium world
cannot quietly become a slot default, and a free world cannot quietly stop being
one.

Night art — premium on iOS, free on web

Every world has two paintings. sys.sunrise/sys.sunset were already arriving
on every weather response and being discarded, so this costs no extra API
call
. Night follows the sun at the place being forecast, not the device, so a
saved city across the world shows its own night. Gated through
PremiumGate.artVariant on iOS; web shows it to everyone, exactly as web already
shows the premium world pool.

The night text tone is measured, never derived: 8 worlds need dark text by
day and light by night, so copying one measurement to both would have left them
unreadable after sunset with nothing going red. A test asserts the flip.

Loading gradients are generated now

primary/headline came from two hand-written tables that had drifted apart —
45 CSS classes on web, 14 bespoke gradients on iOS. They covered different
subsets with different colours for the same world, so a world loaded one way
on web and another on iOS, and nothing could catch it. Both are gone;
derive-world-colors.py generates one pair per world from the day art.

Pricing — free tier goes to 3 saved locations

One slot was enough to demonstrate saving and not enough to use it. Premium stays
20; the headline multiple drops 20× → 6.7×, which is the trade. Reasoning in
PRICING.md.

World poster layout

The mat and colour title band are gone. The band spent ~40% of a square frame on
flat colour, so the painting — the thing sold as an actual poster — got the
smaller half of its own page. Art is square and full width; type sits under it.

Naming fixes

  • The device-location page is renameable. Its label lives in AppSettings,
    not on a SavedLocation: those are keyed by coordinates and the device page's
    coordinates change by definition, so "Home" would come unstuck the first time
    you left the house.
  • A searched location keeps the name you picked. data/2.5/weather doesn't
    name the place you asked for — it names the nearest entry in OpenWeather's
    separate city list. Searching "Madrid" gave "Madrid City Center"; near
    Luxembourg it gave "Weiler", a neighbouring hamlet. Both had to be renamed by
    hand. The two precedences (searched vs. device) are deliberately opposite and a
    test pins both.
  • Passport stamps too, caught later in review — the preview branch was
    bypassing a rule the branch below it documented, so a stamp recorded "Found in
    Lugu". Worst of the three surfaces to get wrong: a stamp is permanent and is
    the one place a name is read back months later.
  • Three world ids were misspellings taken from the delivery's filenames:
    ahldanialdhani, chandrillachandrila, jaiiimjabiim.
    Comparing our ids against the poster shop's folders is what found them.
    Corrected now rather than later because a world id keys stored Atlas
    assignments and Passport stamps, and all four sanitizers drop ids they don't
    recognise — nothing has shipped with these.

Pipeline

  • import-planet-art.py resizes on read, so no full-resolution copy is ever
    written. It also reports any world whose night art is byte-identical to its day
    art — which caught three and got them re-exported.
  • sync-poster-art.py now reads the poster shop's own folder. Its old source,
    public/posters/, was removed by IP-REVIEW.md as broadcast surface, so the
    script had been dead. All 70 worlds have a poster; CreditsPosterTests grew
    from the 6 featured worlds to the whole set.

Verification

Suites: 188 web, 130 iOS (from 164 / 85). Web production build compiles.

Also checked beyond the suites: both catalogs agree field-for-field across 630
values; all 280 planet images decode and are 2868px tall so nothing upscales; all
141 imageset manifests are correct; no day/night pair is a duplicate; both
generators are idempotent.

Confirmed in the Simulator by the owner — night art rendering, night keyed to the
forecast location rather than the device clock, the searched-name fix, Dagobah's
day text flip, and the poster layout. See TESTFLIGHT.md §6.

Not verified: the sandbox purchase. It needs TestFlight or a physical device
with a Sandbox Apple Account, and it is the only revenue path in the product.
TESTFLIGHT.md §3 records why a wiped Simulator cannot answer it.

Costs, stated plainly

  • Shipped art goes ~28 MB → ~105 MB, plus 3.5 MB of posters. Under every App
    Store limit, but it roughly quadruples the download. SCOPE.md flags it as the
    one line to weigh rather than wave through.
  • IP-REVIEW.md is reopened: all 44 audited images are gone, and
    spot-checking the new art found protected vehicle designs in several
    foregrounds. In-app, so inside the existing accepted-risk rule — but App Store
    screenshots don't exist yet and are made by photographing this art.

🤖 Generated with Claude Code

robleto and others added 8 commits August 19, 2026 17:44
New 8200x6454 export, run through the same pipeline as the rest of the set:
height constrained to 2868, LANCZOS, quality 90 — 3644x2868 at 0.44 MB, and the
iOS imageset copy is byte-identical to the web one.

All four edges scanned for the flat export frame that bespin and exegol carried;
none present, so nothing is cropped.

measure-text-tone.py re-run and produced identical values — the readout region
still measures 0.316, so mandalore stays a dark-text world at 4.0:1 and neither
worlds catalog changed. The art moved, the tone did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One art delivery replaced every planet image and added a second painting of
each, so this lands the catalog expansion, the night-art feature, and the
pricing change they force, together. SCOPE.md was frozen; the amendment at the
foot of it records that this clears none of its three bars and why it went ahead
anyway.

Catalog — 43 worlds to 70, split 20 free / 50 premium
  The structural rule survives unchanged: free is exactly the set of worlds some
  slot defaults to, so the free app is complete rather than partial, and premium
  is the alternates. All 27 new worlds are premium, because no new weather
  conditions were added for them to be the default of. Daiyu moved free ->
  premium to land the free tier on exactly 20; Kamino absorbs rain_cold with
  slot copy written for the cold reading. Both halves of the rule are now
  asserted on both platforms, so a premium world cannot quietly become a slot
  default and a free world cannot quietly stop being one.

Night art — premium on iOS, free on web
  Every world is painted twice. sys.sunrise/sys.sunset were already arriving on
  every weather response and being discarded, so this costs no extra call. Night
  follows the sun at the place being forecast, not the device, so a saved city
  across the world shows its own night. Gated through PremiumGate.artVariant on
  iOS; web shows it to everyone, exactly as web already shows the premium world
  pool. Covers the three live views per platform — catalog views stay on day art.

  The night text tone is measured, never derived: 8 worlds need dark text by day
  and light by night, so copying one measurement to both would have left them
  unreadable after sunset with nothing going red. A test asserts the flip.

Loading gradients are generated now, from the art
  primary/headline came from two hand-written tables that had drifted apart — 45
  CSS classes on web, 14 bespoke gradients on iOS. They covered different
  subsets with different colours for the same world, so a world loaded one way
  on web and another on iOS, and nothing could catch it. Both are gone;
  derive-world-colors.py generates one pair per world from the day art.

Pricing — free tier goes to 3 saved locations
  One slot was enough to demonstrate saving and not enough to use it. Nobody
  builds a habit around a list of one, and a feature you never lean on generates
  no wish for more of it. Premium stays 20; the headline multiple drops 20x ->
  6.7x, which is the trade. Two copy strings had hardcoded the singular.

World poster layout
  The mat and the colour title band are gone. The band spent ~40% of a square
  frame on flat colour, so the painting — the thing sold as an actual poster —
  got the smaller half of its own page. Art is square and full width; the type
  sits under it on the sheet.

Naming fixes
  - The device-location page is renameable, like any saved location. Its label
    lives in AppSettings, not on a SavedLocation: those are keyed by coordinates
    and the device page's coordinates change by definition, so "Home" would come
    unstuck the first time you left the house.
  - A searched location now keeps the name you picked. data/2.5/weather does not
    name the place you asked for, it names the nearest entry in OpenWeather's
    separate city list — searching "Madrid" gave "Madrid City Center", and
    searching near Luxembourg gave "Weiler", a neighbouring hamlet. Both had to
    be renamed by hand. The two precedences are now deliberately opposite and a
    test pins both.
  - Three world ids were misspellings taken from the delivery's filenames:
    ahldani -> aldhani, chandrilla -> chandrila, jaiiim -> jabiim. Comparing our
    ids against the poster shop's folders is what found them. Corrected now
    rather than later because a world id keys stored Atlas assignments and
    Passport stamps, and all four sanitizers drop ids they don't recognize —
    nothing has shipped with these.

Pipeline
  - import-planet-art.py resizes on read, so no full-resolution copy is ever
    written; ID_ALIASES maps five typo'd delivery filenames. It also reports any
    world whose night art is byte-identical to its day art, which caught three
    and got them re-exported.
  - sync-poster-art.py now reads the poster shop's own folder. Its old source,
    public/posters/, was removed by IP-REVIEW.md as broadcast surface, so the
    script had been dead. All 70 worlds have a poster; CreditsPosterTests grew
    from the 6 featured worlds to the whole set.

Shipped size is the real cost: ~28 MB of art becomes ~105 MB, plus 3.5 MB of
posters. Under every App Store limit, but it roughly quadruples the download and
SCOPE.md flags it as the one line to weigh rather than wave through.

IP-REVIEW.md is reopened: all 44 audited images are gone, and spot-checking the
new art found protected vehicle designs in several foregrounds. In-app, so
inside the existing accepted-risk rule — but App Store screenshots don't exist
yet and are made by photographing this art.

Not verified: anything visual. Browser preview and Simulator control are both
blocked by the security policy in this environment, so the poster layout, the
night backdrop and the rename sheet need eyes on a real device.

Suites: 188 web, 130 iOS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The twin suns are the only thing separating this icon from a generic
weather app — cloud in front of sun over a warm gradient is the most
common composition in the category. The second sun was the least legible
element in the image, low-contrast against the sky and partly occluded by
the cloud, so below roughly 60px it read as a bump on the cloud's shoulder
and the one differentiating detail vanished at exactly the sizes people
see. Verified by downsampling to 120/80/60/40 against the old icon; it now
resolves as a distinct disc at 40.

Composition, palette and subject are unchanged, so this is not an IP
change. The rest of the icon was already fine small — that claim in
IP-REVIEW.md holds and is now checked rather than assumed.

The new master arrived at 1254x1254 RGBA, the same two App Store
validation failures the original had. Conformed the same way: LANCZOS to
1024, flattened to RGB after confirming the alpha was fully opaque.
IP-REVIEW.md now records this as a property of the export path rather than
a one-time fix, since it has now happened twice.

Web favicons regenerated from the same master — they are the same artwork,
and leaving them would have put the old icon in browser tabs while the app
showed the new one.

Also closes the stale public/icon-1024.png item in IP-REVIEW.md; the file
no longer exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md and downsample-art.py both said the pre-resize originals were
"recoverable from git history". That is wrong in both directions, and the two
directions have opposite consequences: someone could rewrite history believing
it to be a backup, or leave the folder that genuinely is one unprotected.

What history actually holds is superseded *downsampled* JPEGs — derived files at
shipping resolution, ~134 MB of the 251 MB pack. Nothing irreplaceable. The real
archive is the gitignored archive/ (~940 MB: high-res, star-wars-weather, and
the 44 PNGs this pipeline replaced) plus updated-planets/ when a delivery lands.
Git covers neither.

Checked while writing this, and it is the part worth acting on: `tmutil
destinationinfo` reports "No destinations configured", so Time Machine is not
excluding archive/, it is not running on this machine at all. The path is
[Included], so it would be covered the moment a destination exists. The folder
is under ~/Dev rather than ~/Dropbox, so that is not covering it either. One
disk away from gone, and unrelated to repo size.

Also records why serving the art from R2 was considered and rejected, because it
is the obvious idea and the reason it does not help is counterintuitive: the web
and iOS copies of an image are byte-identical, so git already stores one blob
for both. Moving the web copy to an object store saves zero git bytes — the
object stays reachable from the iOS path. The only version that shrinks the repo
is iOS not committing art either, which means fetching at runtime: a
first-launch download, offline handling, cache eviction, and blank worlds until
the fetch lands. That spends something the app has for free — art that needs no
network — and it is a feature, not a migration.

R2 is right for archive/. That is a backup problem, not a serving problem.

Two triggers would change the answer, and neither is true: the pack passing
~600 MB (four more full re-deliveries, still inside GitHub's 1 GB soft limit),
or app download size showing up in App Store conversion data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven images across four worlds: day and night for Corellia, Dagobah and Daiyu,
night only for Utapau.

Re-ran the whole import rather than picking files by timestamp, then let git say
what actually moved. Exactly these seven did, which confirms the JPEG encode is
deterministic — unchanged sources re-encode to identical bytes, so re-running
import-planet-art.py is safe and only ever touches art that really changed. The
web and iOS copies are still byte-identical per image, so git stores one blob
each rather than two.

Gradients and text tones regenerated, since both are derived from this art.
Three gradients moved (Corellia, Dagobah, Daiyu); Utapau's did not, because
only its night painting changed and the gradient comes from the day art.

Dagobah flipped to dark text by day, taking the dark-text count from 8 to 9.
Its new day painting is a bright misty swamp where the old one was dark green —
primary went #385B51 to #758280 — and the region the readout occupies is now too
bright for white text to clear 3:1. Worth flagging rather than burying: Dagobah
is free and the default for both drizzle and rain_light, so this is a change a
large share of forecasts will land on. Verified by eye against the new art as
well as by the measurement.

No identical day/night pairs remain, every image is still 2868px tall so nothing
upscales, and both catalogs agree field for field.

Suites: 188 web, 130 iOS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two corrections to TESTFLIGHT.md, both from the owner exercising premium on
2026-08-31.

§3 said the purchase test was "blocked on Apple — cannot run until Paid
Applications is Active". The agreement went Active on 2026-08-18, which the
header and Part 2 already said, so §3 was telling anyone who read it to wait for
something that had already happened. Premium is confirmed working.

The more useful correction: wiping the entire Simulator and tapping Restore
reported "No previous purchase found on this Apple ID". That reads like a
failure of the one test the no-backend architecture rests on, and it is not one.
The Simulator runs against StoreKitConfig/Products.storekit, whose transactions
live in a local store belonging to that simulator with no Apple ID involved at
any point. Wiping it erased the store, so AppStore.sync() had nothing on any
Apple ID to find and said so — accurately.

It is a genuine pass of the negative case, which was already ticked: restore
with nothing to restore neither hangs nor silently grants premium. It cannot
answer the positive case, because there is no server-side record to restore
from. Recorded so nobody re-runs it expecting a different answer.

Also split the reinstall check into two lines, because they exercise different
mechanisms and only one has ever been watched:

  - entitlement returning on launch, with no Restore tap — start() calls
    refreshEntitlement(), which reads Transaction.currentEntitlements, so on a
    real Apple ID this is what should happen. That is the architecture working,
    not a manual recovery, and it is easy to misread as a bug.
  - entitlement returning when Restore is tapped — the manual nudge for the same
    mechanism, and the branch never yet observed succeeding.

Both need sandbox: a TestFlight build, or Debug on a physical device signed into
a Sandbox Apple Account. Neither is runnable in the Simulator.

Nothing was ticked that was not actually done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…items

The owner ran the 70-world/night-art build in the Simulator and sent screenshots.
What they actually confirm is more than Dagobah's text, which is all I had asked
to see — one screenshot of Taiwan closes four things at once.

Ticked in §6, as a grouped block for this release:

  - Night art renders and its light text is legible on the dark painting.
  - Night follows the place being forecast, not the device: Taiwan showed night
    art while the simulator clock read 11:29 AM in California. This is the item
    that could not be tested headlessly, and it is the whole reason the variant
    is decided from the observation's own sys.sunrise/sys.sunset rather than
    from the device clock.
  - A searched location keeps the name the user picked — the header read
    "TAIWAN". That is the fix for the Madrid → "Madrid City Center" and
    Luxembourg → "Weiler" reports.
  - Dagobah's day text is legible as dark. Highest-risk flip in the batch: the
    new art is a bright misty swamp, so the generator moved it from light to
    dark, and Dagobah is free and the default for both drizzle and rain_light.
  - The search preview draws night art before the location is saved.

Left unticked, because they have not been seen: the poster layout, the
device-location rename, and the free tier at three saved locations.

Two items were wrong rather than merely unchecked:

  - §4 said Atlas lists 26 conditions. It lists 28 — the cloudy · scorching and
    cloudy · hot slots were added since that line was written.
  - §5 expected premium alternates in the Passport to show "No forecast leads
    here — assign it in Atlas". They do not, and should not: that copy is
    suppressed whenever the row is locked, because telling a free user how to
    hunt a world they cannot assign is a dead end. A free build shows a PREMIUM
    chip and nothing else, and that sentence is only ever seen by a premium
    user. The behaviour is correct; the checklist described the wrong outcome,
    which would have sent someone debugging working code.

Also ticked: the Passport hunt hints, confirmed on Mustafar and Exegol — the
first time that copy has existed outside a test assertion — and Poiret One
rendering. Russo One is still unconfirmed by eye and stays unticked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tion's

Searching "Taiwan" and dwelling long enough to earn a stamp recorded "Found in
Lugu" — the nearest entry in OpenWeather's own city list to those coordinates.
Spotted by the owner in the world poster.

Three surfaces read a place name and the earlier fix only corrected two. The
preview header and the saved card were right; the Passport stamp was not.

On iOS the evidence was already in the file. `pendingSighting` has two branches,
and the saved/device one carried the comment "The user's own name for a place,
not the weather station's — it's what's on screen, and it's what they'd
recognize later." The preview branch sat four lines above it passing
`weather.name`, doing exactly what that comment forbids. Web had the same leak
in the `useStampOnDwell` call.

This is the worst of the three to have gotten wrong. A stamp is permanent, and
it is the one place a place-name is read back months later, when "Lugu" means
nothing to anybody. The header at least corrects itself the next time you look
at the location.

Existing stamps are unaffected: this changes what future stamps record, not what
is stored, and the Passport keys on world id rather than city — so nothing is
orphaned, the provenance line just keeps whatever it was written with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@netlify

netlify Bot commented Aug 31, 2026

Copy link
Copy Markdown

Deploy Preview for galactic-weather ready!

Name Link
🔨 Latest commit d309178
🔍 Latest deploy log https://app.netlify.com/projects/galactic-weather/deploys/6a95a37e898a7400086af759
😎 Deploy Preview https://deploy-preview-2--galactic-weather.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 87
Accessibility: 100
Best Practices: 75
SEO: 92
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@robleto
robleto merged commit fd6b965 into main Aug 31, 2026
6 checks passed
@robleto
robleto deleted the catalog-70-worlds-night-art branch August 31, 2026 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant