krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

Commit c2c5268c22

c2c5268c22f581c71a5df2ad523d6785556a285a

parent: eb50ddd595

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-18 00:32 UTC

Deploy backend and point the app at it

- server/wrangler.jsonc: D1 database id for the deployed "moonrise".
- Optional LL2_API_KEY secret sent as "Authorization: Token"; anonymous
  requests from Workers hit LL2's per-IP limit (429).
- App: Debug and Release use https://moonrise-server.audit-labs.workers.dev.
- READMEs and PLAN.org: deployment state.

Layout: unified · split

App/README.org +4
@@ -11,3 +11,7 @@ open Moonrise.xcodeproj
11Simulator: an iOS 26+ runtime is required. Push registration needs a 11Simulator: an iOS 26+ runtime is required. Push registration needs a
12signed build (team =ZCNAX3VL9D=); the APNs device token is logged under 12signed build (team =ZCNAX3VL9D=); the APNs device token is logged under
13subsystem =sh.krz.moonrise=. 13subsystem =sh.krz.moonrise=.
14
15Backend: both configurations use https://moonrise-server.audit-labs.workers.dev
16(=MOONRISE_API_URL= in =project.yml=). For a local Worker, set it to
17=http://localhost:8787= and regenerate.
App/project.yml +2 −2
@@ -32,9 +32,9 @@ targets:
32 settings: 32 settings:
33 configs: 33 configs:
34 Debug: 34 Debug:
35 MOONRISE_API_URL: http://localhost:8787 35 MOONRISE_API_URL: https://moonrise-server.audit-labs.workers.dev
36 Release: 36 Release:
37 MOONRISE_API_URL: "" 37 MOONRISE_API_URL: https://moonrise-server.audit-labs.workers.dev
38 base: 38 base:
39 PRODUCT_BUNDLE_IDENTIFIER: sh.krz.moonrise 39 PRODUCT_BUNDLE_IDENTIFIER: sh.krz.moonrise
40 DEVELOPMENT_TEAM: ZCNAX3VL9D 40 DEVELOPMENT_TEAM: ZCNAX3VL9D
PLAN.org +11
@@ -374,6 +374,17 @@ MR !11.
374- Not done: tap a planet or probe for its Library page, Earth–Moon inset, 374- Not done: tap a planet or probe for its Library page, Earth–Moon inset,
375 active missions (Juno, Europa Clipper, JUICE, Psyche, Parker, Lucy). 375 active missions (Juno, Europa Clipper, JUICE, Psyche, Parker, Lucy).
376 376
377** Deployment (2026-10-18)
378- Worker =moonrise-server= at https://moonrise-server.audit-labs.workers.dev,
379 D1 =moonrise=, crons =*/5= (SWPC) and =*/10= (LL2), APNS_KEY set.
380- SWPC polling confirmed in production (=/v1/today= serving Kp).
381- LL2 from Cloudflare returned 429 on the first run: the anonymous limit
382 is per IP and Workers share egress IPs. Needs an LL2 API key
383 (=LL2_API_KEY= secret, supported). Launches stay empty until then.
384- App Debug and Release builds use the deployed URL.
385- The app does not register devices with the backend yet, so no aurora
386 pushes are sent.
387
377* roadmap 388* roadmap
378** v1: now 389** v1: now
379Sky, System, Sun, Launches, Library core (above). 390Sky, System, Sun, Launches, Library core (above).
server/README.org +10
@@ -15,6 +15,9 @@ APNs cannot be reached from =wrangler dev= (no HTTP/2); test pushes
15against a deployed Worker. 15against a deployed Worker.
16 16
17* deploy 17* deploy
18Deployed 2026-10-17: https://moonrise-server.audit-labs.workers.dev
19(D1 =moonrise= 54a0c50d-2150-47ae-aa22-449e8204ad7b, APNS_KEY set).
20
18#+begin_src sh 21#+begin_src sh
19wrangler d1 create moonrise # put the id in wrangler.jsonc 22wrangler d1 create moonrise # put the id in wrangler.jsonc
20wrangler d1 migrations apply moonrise --remote 23wrangler d1 migrations apply moonrise --remote
@@ -22,6 +25,13 @@ wrangler deploy
22wrangler secret put APNS_KEY < AuthKey_644XS4F2RG.p8 25wrangler secret put APNS_KEY < AuthKey_644XS4F2RG.p8
23#+end_src 26#+end_src
24 27
28Launch Library 2's anonymous limit is per IP, and Workers share egress IPs,
29so anonymous requests from Cloudflare often get HTTP 429. With an API key:
30
31#+begin_src sh
32wrangler secret put LL2_API_KEY
33#+end_src
34
25* API 35* API
26| Method and path | Body / result | 36| Method and path | Body / result |
27|- 37|-
server/src/index.ts +3 −1
@@ -9,6 +9,8 @@ interface Env {
9 APNS_TEAM_ID: string; 9 APNS_TEAM_ID: string;
10 APNS_TOPIC: string; 10 APNS_TOPIC: string;
11 APNS_KEY: string; 11 APNS_KEY: string;
12 /** Optional Launch Library 2 API key. */
13 LL2_API_KEY?: string;
12} 14}
13 15
14interface DeviceRequest { 16interface DeviceRequest {
@@ -99,7 +101,7 @@ async function launches(env: Env, now: Date): Promise<Response> {
99async function pollLaunches(env: Env, now: Date): Promise<void> { 101async function pollLaunches(env: Env, now: Date): Promise<void> {
100 let list: Launch[]; 102 let list: Launch[];
101 try { 103 try {
102 list = await fetchUpcoming(); 104 list = await fetchUpcoming(env.LL2_API_KEY);
103 } catch (e) { 105 } catch (e) {
104 console.error("ll2", String(e)); 106 console.error("ll2", String(e));
105 await env.DB.prepare("INSERT INTO feed_snapshots (feed, fetched_at, ok) VALUES ('ll2', ?1, 0) ON CONFLICT (feed) DO UPDATE SET fetched_at = ?1, ok = 0").bind(now.toISOString()).run(); 107 await env.DB.prepare("INSERT INTO feed_snapshots (feed, fetched_at, ok) VALUES ('ll2', ?1, 0) ON CONFLICT (feed) DO UPDATE SET fetched_at = ?1, ok = 0").bind(now.toISOString()).run();
server/src/ll2.ts +5 −2
@@ -63,8 +63,11 @@ export function normalize(r: Raw): Launch {
63 }; 63 };
64} 64}
65 65
66export async function fetchUpcoming(): Promise<Launch[]> { 66/** `apiKey` lifts the anonymous per-IP limit, which Cloudflare's shared egress IPs often exhaust (HTTP 429). */
67 const res = await fetch(UPCOMING_URL, { headers: { "user-agent": "moonrise-server (sh.krz.moonrise)" } }); 67export async function fetchUpcoming(apiKey?: string): Promise<Launch[]> {
68 const headers: Record<string, string> = { "user-agent": "moonrise-server (sh.krz.moonrise)" };
69 if (apiKey) headers.authorization = `Token ${apiKey}`;
70 const res = await fetch(UPCOMING_URL, { headers });
68 if (!res.ok) throw new Error(`LL2 ${res.status}`); 71 if (!res.ok) throw new Error(`LL2 ${res.status}`);
69 const body = (await res.json()) as { results: Raw[] }; 72 const body = (await res.json()) as { results: Raw[] };
70 return body.results.map(normalize); 73 return body.results.map(normalize);
server/wrangler.jsonc +1 −2
@@ -7,8 +7,7 @@
7 { 7 {
8 "binding": "DB", 8 "binding": "DB",
9 "database_name": "moonrise", 9 "database_name": "moonrise",
10 // Replace with the id from `wrangler d1 create moonrise` before deploying. 10 "database_id": "54a0c50d-2150-47ae-aa22-449e8204ad7b",
11 "database_id": "00000000-0000-0000-0000-000000000000",
12 "migrations_dir": "migrations" 11 "migrations_dir": "migrations"
13 } 12 }
14 ], 13 ],