diff --git a/.gitignore b/.gitignore index a91f146..cd59358 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,10 @@ data/ site/cal/ site/index.html site/next.json + +# Local assistant notes, kept out of the public repo +CLAUDE.md +.claude/ +docs/HANDOFF.md +docs/progress-log.md +docs/reviews/ diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 39fbd8e..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,39 +0,0 @@ -# launchcal (SpaceX flight calendar) - -Unofficial, self-updating public calendar of SpaceX launches that anyone can subscribe to -(Google Calendar first, ICS for Apple/Outlook), plus a one-page landing site. - -## Stack and commands - -Node 24 runs the TypeScript in `src/` directly (type stripping, no build). Imports use `.ts` extensions. -`tsc` is for type checking only. Tests use the built-in `node:test`. - -- `npm run fetch`: fetch upcoming launches and those of the last 30 days from Launch Library 2, write `data/snapshot.json`. -- `npm run build`: render `data/snapshot.json` into `site/cal/{all,crewed,starship,no-starlink}.ics`, `site/index.html` (from `src/page.html`) and `site/next.json`. -- `npm run sync [-- --dry-run] [-- --calendar all=]`: sync `data/snapshot.json` into the Google calendars. Key from env `GOOGLE_SERVICE_ACCOUNT_KEY` (JSON) or `GOOGLE_SERVICE_ACCOUNT_KEY_FILE` (locally `%USERPROFILE%\.launchcal\sa.json`, never in the repo). -- `npm test`, `npm run typecheck`. - -## Layout - -- `src/launch.ts`: launchcal's own `Launch` model. `src/ll2.ts`: LL2 client and normaliser. `src/fetch.ts`: fetch entry point. -- `src/calendars.ts`: the 4 calendars and their filters (ids are public URLs, never rename). `src/event.ts`: format-neutral event text and timing, shared by ICS and the Google sync. `src/ics.ts`: RFC 5545 writer. `src/build.ts`: build entry point. -- `src/gcal.ts`: Google event mapping and the insert/update/delete plan (launch id + hash in private extended properties; events older than the snapshot window are kept). `src/google.ts`: service account JWT and Calendar API calls. `src/sync.ts`: sync entry point. -- `src/page.ts` + `src/page.html`: landing page cards (Google add link, webcal, ICS) and `next.json`. Static, committed: `site/style.css`, `site/app.js` (countdown, copy), `site/fonts/` (D-DIN, OFL), `site/img/` (NASA public domain), `site/favicon.svg`. Preview: `launchcal-site` in the workspace-root `.claude/launch.json` (port 4173, serves `site/`). -- `test/fixtures/ll2-upcoming.json` (5) and `ll2-previous.json` (1): real, unedited LL2 detailed records (recorded 2026-10-01). -- `data/snapshot.json`, `site/cal/*.ics`, `site/index.html`, `site/next.json`: generated, git-ignored. `site/` is deployed to Pages by `.github/workflows/publish.yml` (every 2h, on push to main, manual); its `sync` job runs `npm run sync` in the `google-calendar` environment (secret `GOOGLE_SERVICE_ACCOUNT_KEY`). -- `.github/workflows/ci.yml`: typecheck + tests, required check on `main`. Actions are pinned by commit SHA. - -## Known facts - -- Data source: Launch Library 2 (`ll.thespacedevs.com/2.3.0/launches/upcoming/?lsp__name=SpaceX`). - `mode=detailed` (about 4 MB per 100 launches) is needed for boosters, crew and webcasts. - Free tier is rate limited; poll on a schedule, never per visitor. -- SpaceX publishes no official public API. The old r/SpaceX API (`api.spacexdata.com`) is unmaintained. - -## Project rules - -- `main` is protected by the "Protect main" ruleset: PR only, squash only, code-owner review; Roman merges via admin bypass. Direct pushes fail. -- MIT licensed. Launch data belongs to The Space Devs; credit them, never relicense it. -- Never imply the site is official SpaceX: "unofficial" in the footer, no SpaceX logo, assets or CSS. -- Landing page uses a SpaceX-inspired visual language only (black, D-DIN uppercase, outline buttons, NASA photos). -- Event identity = upstream launch id, so updates edit the same event instead of duplicating it. diff --git a/README.md b/README.md index ff9aba3..b4f3030 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,91 @@ # launchcal -An unofficial, self-updating calendar of upcoming SpaceX launches. Subscribe once and every -reschedule, scrub and new launch shows up in your calendar automatically. +[![ci](https://github.com/launchcal/launchcal.github.io/actions/workflows/ci.yml/badge.svg)](https://github.com/launchcal/launchcal.github.io/actions/workflows/ci.yml) +[![publish](https://github.com/launchcal/launchcal.github.io/actions/workflows/publish.yml/badge.svg)](https://github.com/launchcal/launchcal.github.io/actions/workflows/publish.yml) -**Not affiliated with, endorsed by, or connected to SpaceX.** +Free, self-updating calendars of SpaceX launches. Subscribe once and every new launch, reschedule +and scrub shows up in your calendar, in your own time zone. -> Work in progress. The subscribe links and website at https://launchcal.github.io are not live yet. +**Website: https://launchcal.github.io** + +**Unofficial.** Not affiliated with, endorsed by, or connected to SpaceX. + +## Subscribe + +| Calendar | What's in it | Google Calendar | Apple Calendar, Outlook, others (ICS) | +|---|---|---|---| +| SpaceX launches | Every launch with a known date: Falcon 9, Falcon Heavy, Starship | [Add](https://calendar.google.com/calendar/u/0?cid=MjRmNWFlMTgyNDJjMGJkODM2OThmOWVlNjRlYzJjYzJkYTE4NWNjNTM1NzcxOGVmZTNiYjFkMmQ4MDBjNDJiZkBncm91cC5jYWxlbmRhci5nb29nbGUuY29t) | `https://launchcal.github.io/cal/all.ics` | +| SpaceX crewed launches | Crew Dragon missions to the ISS and private crews | [Add](https://calendar.google.com/calendar/u/0?cid=Y2JjOGZkNDM2N2I3OTBjZjZlMThmYmY3OGFiYjkxOTExMmU1NDkzNjI1NGQzYjNiNmJkZmRkYTQ1ZWEzNzg2ZEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t) | `https://launchcal.github.io/cal/crewed.ics` | +| Starship launches | Every Starship flight, test or operational | [Add](https://calendar.google.com/calendar/u/0?cid=YTcyZWE2OThkYmVjNWMzZmM3MTE0OGI5MjhkZDViMWMxMmZlZjI3Zjk4Njc2YmZlN2I0ZmNjNGVmNDQ0YTJmYUBncm91cC5jYWxlbmRhci5nb29nbGUuY29t) | `https://launchcal.github.io/cal/starship.ics` | +| SpaceX launches without Starlink | Everything except routine Starlink deployments (Starship flights stay in) | [Add](https://calendar.google.com/calendar/u/0?cid=YzE4ODQwMDZlMDQ2ZmJkNjIxYWZlODc1NmY2YjRlYzAwM2YyYWM5ZTk1OTBjYTM4MGY1MTFkZGVhYWIyNDQ3MEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t) | `https://launchcal.github.io/cal/no-starlink.ics` | + +- **Google Calendar:** click Add, then confirm. Updates arrive within about two hours. +- **Apple Calendar:** File → New Calendar Subscription, paste the ICS link. On iPhone: Settings → Calendar → Accounts → Add Account → Other → Add Subscribed Calendar (menu names vary by iOS version). +- **Outlook:** Add calendar → Subscribe from web, paste the ICS link. + +The website's "Apple / Outlook" buttons open the subscribe dialog directly. ICS subscriptions refresh +on your app's schedule (Google: every 12 to 24 hours for ICS, which is why the native Google +calendars exist). + +## What the events look like + +- **Known time:** a one-hour event at liftoff, with the launch window, booster, landing, crew, + orbit and webcast link in the description. +- **Only the day known:** an all-day event. +- **Not confirmed yet:** title ends in "(TBD)" and the event is tentative. +- **Only the month or year known:** left out until the date firms up. +- Events show as free time and carry no reminders; add your own to launches you want to watch. +- Flown launches keep their outcome. The ICS feeds keep them for 30 days after liftoff; the Google + calendars keep them as history. ## How it works -A scheduled GitHub Actions job fetches upcoming launches, compares them with the last snapshot -and updates public Google Calendars plus `.ics` feeds for Apple Calendar and Outlook. +Every two hours a [GitHub Actions workflow](.github/workflows/publish.yml): + +1. fetches upcoming SpaceX launches and those of the last 30 days from + [Launch Library 2](https://thespacedevs.com/llapi); +2. builds the four `.ics` feeds, the website and `next.json` (next launch for the countdown); +3. deploys them to GitHub Pages; +4. syncs the four public Google Calendars through the Calendar API with a service account, + updating events in place by launch id, so a reschedule edits the event instead of duplicating it. + +If the data source is down, nothing is published and the last good version stays live. ## Development -Requires Node 24 or newer. +Requires Node 24 or newer. Node runs the TypeScript in `src/` directly; there is no build step for the code. ```bash npm ci npm test npm run typecheck npm run fetch # calls the live API, writes data/snapshot.json +npm run build # writes site/cal/*.ics, site/index.html and site/next.json ``` -## Data +Serve `site/` with any static file server to preview the website. `npm run sync` writes to Google +Calendar and needs a service account key (`GOOGLE_SERVICE_ACCOUNT_KEY_FILE`); use `--dry-run` to +only print the changes, and `--calendar all=` to sync one calendar into a test calendar. + +| Path | What it does | +|---|---| +| `src/ll2.ts`, `src/fetch.ts` | Launch Library 2 client and fetch entry point | +| `src/calendars.ts` | The four calendars, their filters and Google calendar ids | +| `src/event.ts` | Event text and timing, shared by ICS and Google | +| `src/ics.ts`, `src/build.ts` | ICS writer and build entry point | +| `src/gcal.ts`, `src/google.ts`, `src/sync.ts` | Google Calendar diff, API client and sync entry point | +| `src/page.ts`, `src/page.html`, `site/` | Website template and static assets | + +## Data and credits + +- Launch data: [Launch Library 2](https://thespacedevs.com/llapi) by The Space Devs. The data is + theirs and subject to their terms; the MIT license covers launchcal's code only. + Wrong launch data? Report it to The Space Devs so every consumer gets the fix. +- Website photo: CRS-20 launch, NASA/Tony Gray and Tim Terry. NASA does not endorse this project. +- Typeface: D-DIN by Datto, under the SIL Open Font License 1.1. -Launch data comes from [Launch Library 2](https://thespacedevs.com/llapi) by The Space Devs. -The data is theirs and is subject to their terms; the MIT license below covers launchcal's code only. +See the [terms of use](https://launchcal.github.io/terms.html) and +[privacy policy](https://launchcal.github.io/privacy.html) (no cookies, no tracking). ## Contributing diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md deleted file mode 100644 index 5b53f45..0000000 --- a/docs/HANDOFF.md +++ /dev/null @@ -1,66 +0,0 @@ -# Handoff - -## Current state - -Steps 1-5 are merged and live, no blockers. Every 2h the publish workflow fetches LL2, builds the 4 ICS feeds plus the landing page (`index.html`, `next.json`), deploys to https://launchcal.github.io and syncs the 4 public Google Calendars. Verified 2026-10-01 (run 36886410303): all jobs green; Crew-13 Go → Success propagated to Google in the same run (public feed LAST-MODIFIED 15:43:33Z), other events untouched; live page, feeds, fonts, photo serve 200, no third-party requests; Roman confirmed "Add to Google Calendar" opens Google's add prompt for the right calendar. - -## Next steps - -1. **Delete the local key and the test calendar**: `%USERPROFILE%\.launchcal\sa.json`, the copy in Downloads (`launchcal-510314-*.json`), and the `launchcal test` calendar. Keep a key only if local syncs are still wanted. -2. **Check the ICS subscription update** (from 2026-10-02): compare Roman's subscribed "SpaceX launches" (ICS) calendar with live `all.ics`; expect Crew-13 and Transporter 18 "Status: Success" and Starlink 15-23 gone. Roman can then switch himself from the ICS subscription to the native Google calendar. -3. **Step 6, go-live**: README update (subscribe links), announce. Buy Me a Coffee link once Roman has an account (one line in `src/page.html` footer). - -Roman picks the order. - -## Decisions (2026-10-01) - -- Native public Google Calendars (fast updates) plus ICS mirror of the same data. 4 calendars: all, crewed, starship, no-starlink (ids are public URLs, never rename). -- Event rules: SEC/MIN/HR precision → 1 hour event at T-0, window in description; DAY → all-day; month or coarser → no event. Not-yet-go status → "(TBD)" suffix + TENTATIVE; flown (Success/Failure) → confirmed, "(Failure)" suffix kept. Crewed = crew listed or Crew Dragon. No-Starlink keeps Starship flights carrying Starlink. Events are TRANSP:TRANSPARENT (free). -- Generated data is never committed: `main` only accepts PRs, and a failed run leaves the last deploy live. -- Google sync (2026-10-01): service account `launchcal-sync@launchcal-510314.iam.gserviceaccount.com` in GCP project `launchcal-510314` (no billing account), shared on each calendar as "Make changes and see all event details". Events older than the snapshot window are kept in Google as history; ICS drops them. Google calendar ids live in `src/calendars.ts` and are public; never change them. -- Landing page (2026-10-01): generated from `src/page.html` + `src/calendars.ts` so links never drift; Google add link is `https://calendar.google.com/calendar/u/0?cid=` (Roman-verified). Hero photo NASA KSC-20200306-PH-AWG05_0010 (public domain, credited, "NASA does not endorse" in footer); D-DIN (OFL) self-hosted. No analytics, no cookies, no third-party requests. -- Name launchcal; GitHub org `launchcal`, repo `launchcal/launchcal.github.io`, no bought domain. MIT for code; data credited to The Space Devs. - -## Repo setup - -- Ruleset "Protect main": PR required, code-owner (`@roma321m`) review, threads resolved, squash only, required check `test` (ci.yml), no force push/deletion. Roman merges via admin bypass checkbox; wait for CI green first, the bypass also skips CI. -- Secret scanning + push protection, Dependabot alerts + security PRs (version PRs off), private vulnerability reporting. Actions token read-only by default; fork PR runs need approval; never use `pull_request_target`. Actions pinned by commit SHA. -- Pages source: GitHub Actions (`build_type: workflow`); `github-pages` environment deploys from `main` only. -- Environment `google-calendar`: deployment branches `main` only, secret `GOOGLE_SERVICE_ACCOUNT_KEY` (service account JSON). Only the `sync` job uses it. -- Org: base permission none, 2FA required. - -## How to run - -- Local: `npm ci`, `npm run fetch` (live API, writes git-ignored `data/snapshot.json`), `npm run build` (writes git-ignored `site/cal/*.ics`), `npm test` (71 tests), `npm run typecheck`. -- Landing page preview: `npm run build`, then the `launchcal-site` config in the workspace-root `.claude/launch.json` serves `site/` on http://localhost:4173. -- Google sync: `GOOGLE_SERVICE_ACCOUNT_KEY_FILE=$USERPROFILE/.launchcal/sa.json npm run sync -- --dry-run` (reads only). Test a single calendar elsewhere with `-- --calendar all=`. Without a key file, local sync is impossible; CI has the key. -- Deploy: merge to `main`, or Actions → publish → Run workflow. Check: `gh run list --repo launchcal/launchcal.github.io --limit 3`. -- Every change goes branch → PR → CI green → Roman merges. Docs changes too. - -## Traps - -- LL2 dates are mostly vague (2026-10-01: of 133 upcoming, 49 year-only, 30 month-only, 14 decade, ~6 day-or-better). Vague `net` is a placeholder like `2026-12-31T00:00:00Z`; that is why coarse launches get no event. -- LL2 lists only ~1 Starlink launch ahead, so no-starlink differs little from all until close to launch. -- "Crewed" cannot use the crew list alone (Crew-15, Axiom 5 unassigned); `Human Exploration` includes uncrewed flights (Haven-1, HLS demo). -- LL2 placeholders (`Unknown F9`, `N/A` landing/orbit, `Heliocentric N/A`, null landing attempt) are normalised in `toLaunch`; null attempt means "landing TBD", not expended. -- `crewed`/`starship` feeds can legitimately be empty; `build` only refuses when `all` is empty. Unverified whether Google accepts an empty subscribed feed. -- Scheduled workflows in public repos stop after 60 days without repo activity. `publish.yml` re-enables itself each scheduled run; UNVERIFIED that this resets the timer. Check after 2026-12-01. -- LL2 rate limits per IP and runners share IPs: publish retries the fetch once after 90s. Watch the failure rate in the Actions tab. -- ICS feeds must keep CRLF. They are built on Linux in CI now; if feeds are ever committed again, Windows `autocrlf` will rewrite them (needs `*.ics -text`). -- Google refreshes subscribed ICS feeds only every ~12-24h; Pages caches 10 min. Step 4 exists because of this. -- Google sync deletes by the event's *stored* start: a flown launch whose net is later moved to before the 30-day window gets deleted instead of kept as history. Rare; accepted. -- Google sync has no retry: a rate limit or 5xx aborts the run mid-way (later calendars skipped). The next run converges because the diff is against Google itself. -- Google lists event starts in the calendar's timezone (`+03:00`), not UTC; comparisons use `Date.parse`, never string compare. -- Only events with private property `launchcal=1` are touched; anything added by hand to the public calendars stays forever. -- LL2 can be fully down (2026-10-01 ~15:10-15:40 UTC: no response at all). Then `build` fails, nothing deploys or syncs, last state stays live. -- `next.json` is only as fresh as the last build (up to 2h); the page carries 5 launches and skips ended ones on the visitor's clock. All-day launches leave the panel at UTC midnight, matching the build (deliberate). -- The in-app browser pane blocks the clipboard even on real clicks; the copy button falls back to `prompt()`. Do not read a failed copy in the pane as a site bug. -- Page assets are cached by Pages for 10 min; check a deploy with a cache-busting query if it looks stale. -- The recent/upcoming merge in `src/fetch.ts` (upcoming wins by id) is untested script code; testing it needs a small extraction (ask Roman). -- Bash tool on this PC: backslashes in heredoc-fed Python and sed get mangled; use the Edit tool for code containing `\n` or regex escapes. - -## Withdrawn claims - -- FALSE (2026-10-01): "the Action commits snapshot and ICS to the repo and diffs against the previous snapshot". `main` only accepts PRs; nothing generated is committed, and Google sync will diff against Google itself. Do not resurrect. -- FALSE (2026-10-01): "buy a domain". GitHub org `launchcal` gives `launchcal.github.io` for free. Do not resurrect. -- FALSE (2026-10-01): "`actions/configure-pages` is needed". It 403s with a read-only token and a plain static site does not need it. diff --git a/docs/progress-log.md b/docs/progress-log.md deleted file mode 100644 index b861dd6..0000000 --- a/docs/progress-log.md +++ /dev/null @@ -1,51 +0,0 @@ -# Progress log - -## 2026-10-01 (step 5 live, step 4 CI-verified) - -- Landing page merged (PR #8): generated from `src/page.html` + `src/calendars.ts`, 4 subscribe cards (Google add, webcal, copy ICS), next launch panel with countdown from `next.json`, D-DIN + NASA CRS-20 night photo, 272 KB, same-origin only. -- Verified locally at 1600/1280/375 px; countdown switching and hide via faked clock; copy fallback; Roman confirmed the Google add link. Review fixes: copy label race, https-only webcast, `$&` replacement, per-calendar link labels, copy status for screen readers, NASA no-endorsement line. 71 tests, mutation-checked. -- First green CI run with sync (36886410303): Crew-13 Go → Success updated in all/crewed/no-starlink Google calendars within the run; live site serves page, `next.json`, feeds. - -## 2026-10-01 (step 4) - -- Google Calendar sync merged (PR #6): service account JWT via `node:crypto`, plain fetch, diff by launch id + content hash in private extended properties, history kept before the snapshot window, `--dry-run`, `--calendar` override. New `sync` job in publish.yml in the `google-calendar` environment (main only). -- Verified on a test calendar: 14 inserts matching `all.ics`, re-run 0, time change updated the same Google event id, removal deleted, restore re-synced. -- 57 tests; planner, hash, JWT, listEvents mutation-checked. Independent review: fixed `--calendar` parsing, added paging/error tests; accepted no-retry and stored-start delete edge (see Traps). -- First CI run after merge failed in `build`: LL2 not responding at all. Real calendars filled locally instead: 14/1/1/10, idempotent re-run, all four public (anonymous `public/basic.ics` 200 with details). CI sync still unverified. - -## 2026-10-01 (step 3 verified live) - -- PR #4 merged; first publish run green (build + deploy), 141 launches, feeds 14/1/1/10 events. -- https://launchcal.github.io/cal/*.ics: HTTP 200, `text/calendar`, CRLF on every line, parses with Python icalendar. Root shows the placeholder page. -- Roman deleted the "launchcal test" calendar and subscribed Google to `all.ics`; Google read it immediately: 14/14 events matched (times, all-day, tentative, free, cleaned-up text). -- Open: change propagation on Google's next refresh (check from 2026-10-02). Next: step 4. - -## 2026-10-01 (step 3) - -- CI (typecheck + tests, required check `test` on main) and publish workflow (every 2h, push to main, manual) deploying `site/` to Pages. Generated snapshot and feeds removed from git. Pages source switched from branch to GitHub Actions. Review found configure-pages would 403 with a read-only token; step removed. - -## 2026-10-01 (step 2) - -- Snapshot now also holds SpaceX launches of the last 30 days (Roman's request), so flown launches stay in calendars. -- 4 calendars, event rules, hand-rolled ICS writer, `npm run build`. 40 tests; independent review found untested rules (HR precision, In Flight, CRLF escape, null window), fixed and mutation-checked. Build refuses to publish when `all` has no events; failed launches count as flown. Live: 141 launches → 14/1/1/10 events; parsed by Python icalendar; rebuild byte-identical. -- Google import check found and fixed: unknown landing shown as "expended", raw orbit codes (`PO`, `N/A`). - -## 2026-10-01 (repo hardening) - -- MIT license, README, CONTRIBUTING, SECURITY, Code of Conduct, CODEOWNERS, issue/PR templates. Repo, security, Actions settings and the "Protect main" ruleset applied via API; Roman set org base permission none and 2FA, and removed his conflicting older ruleset. - -## 2026-10-01 (step 1) - -- Fetch and normalise: LL2 client with paging and abort-on-failure guards, `Launch` model, `data/snapshot.json` (133 launches), atomic write, duplicate-id and timeout guards. 11 tests, all mutation-checked; independent review findings fixed (sort, atomic write, dedupe, null fallbacks). Live fetch run twice: identical snapshot. - -## 2026-10-01 (later) - -- Org `launchcal` created; repo transferred and renamed to `launchcal/launchcal.github.io` so Pages serves at `launchcal.github.io`. - -- Decided: native Google calendars + ICS mirror, 4 calendars, name launchcal, public repo, GitHub org instead of a bought domain, SpaceX-inspired (not cloned) design. -- Public repo created: https://github.com/roma321m/launchcal. Roadmap of 6 steps written to HANDOFF. - -## 2026-10-01 - -- Project folder created, git initialised. Planning phase started. -- Verified Launch Library 2 returns upcoming SpaceX launches (133 at the time) with status, NET precision, pad, mission type.