Expand the catalog to 70 worlds, add night art, and fix place naming - #2
Merged
Conversation
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>
✅ Deploy Preview for galactic-weather ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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.mdwas frozen; the amendment at the foot of it records that this clearsnone 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_coldwith slot copywritten 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.sunsetwere already arrivingon 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.artVarianton iOS; web shows it to everyone, exactly as web alreadyshows 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/headlinecame 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.pygenerates 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
AppSettings,not on a
SavedLocation: those are keyed by coordinates and the device page'scoordinates change by definition, so "Home" would come unstuck the first time
you left the house.
data/2.5/weatherdoesn'tname 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.
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.
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
recognise — nothing has shipped with these.
Pipeline
import-planet-art.pyresizes on read, so no full-resolution copy is everwritten. 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.pynow reads the poster shop's own folder. Its old source,public/posters/, was removed byIP-REVIEW.mdas broadcast surface, so thescript had been dead. All 70 worlds have a poster;
CreditsPosterTestsgrewfrom 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
Store limit, but it roughly quadruples the download.
SCOPE.mdflags it as theone line to weigh rather than wave through.
IP-REVIEW.mdis reopened: all 44 audited images are gone, andspot-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