A GitHub Actions workflow (.github/workflows/launch-monitor.yml) runs every 10 minutes and monitors SpaceX launches for schedule changes and outcomes.
state/launch_state.jsonall topicWhen state/launch_state.json is empty or has no launches, the script enters bootstrap mode:
[BOOTSTRAP] Initialized state with N launches. No notifications sent.Dry-run mode also shows bootstrap behavior (without committing state).
As a safety measure, if more than MAX_NOTIFICATIONS_PER_RUN events (default: 5) are detected in a single run, only the 3 most recent (by launch NET) are sent. The rest are skipped and logged as a warning. This prevents accidental flooding if state gets corrupted or reset.
Override via environment variable:
MAX_NOTIFICATIONS_PER_RUN=10
All push notifications are in Norwegian (Bokmål) and sent to the FCM topic all.
| Event | Example title | Example body |
|---|---|---|
| Reschedule | Oppskytning flyttet | Falcon 9 Block 5 | Starlink Group 17-16 er flyttet til 27. apr kl 18:30 |
| Success | Suksess | Falcon 9 Block 5 | Starlink Group 17-16 ble vellykket skutt opp |
| Failure | Feil | Falcon 9 Block 5 | Starlink Group 17-16: oppskytning mislyktes |
| Partial Failure | Delvis vellykket | Falcon 9 Block 5 | Starlink Group 17-16: delvis vellykket oppskytning |
| Secret | Description |
|---|---|
FIREBASE_SERVICE_ACCOUNT |
Firebase service account JSON (must have firebase.messaging scope). The project_id field is used to target the correct FCM project. |
Trigger the workflow manually via workflow_dispatch with dry-run mode:
gh workflow run launch-monitor.yml -f dry_run=true
In dry-run mode the script logs what notifications would be sent without actually calling FCM or committing state changes.
Content delivery for the SpaceX Tracker iOS app. Articles and images are served via GitHub Pages and fetched at runtime by the app.
updates/
├── manifest.json # Production manifest (live to all users)
├── manifest-draft.json # Draft manifest (DEBUG preview mode only)
├── articles/ # Markdown article bodies
│ ├── welcome-to-mission-control.md
│ ├── starship-flight-test-overview.md
│ └── mission-control-premium-guide.md
└── images/ # Hero images (1200x675 JPEG)
├── hero_welcome.jpg
├── hero_starship.jpg
└── hero_premium.jpg
updates/articles/updates/images/ (1200x675 JPEG, ~85% quality)manifest-draft.json first — verify it renders via DEBUG preview mode in the appmanifest.json and commitarticle_id in the data payload to deep-link| Field | Required | Notes |
|---|---|---|
id |
yes | URL-safe; must match the .md filename |
title |
yes | |
subtitle |
no | Shown on the card |
date |
yes | ISO 8601 UTC |
category |
yes | all, starship, falcon9, dragon, etc. |
premium |
yes | true = paywall teaser for free users |
hero_image |
no | Full URL to 1200x675 JPEG |
summary |
yes | ~2 sentences; shown on card + detail view |
author |
no |
@youtube(URL_OR_ID) — inline embed on its own line@gallery(url1, url2, url3, ...) — swipeable gallery on its own lineStandard Markdown (headings, bold, italic, lists, links, images) is fully supported.
Pages is served from main branch, root folder. URL:
https://grandmasterjc.github.io/spacex-tracker-config/
| Document | URL |
|---|---|
| Privacy Policy | /privacy.html |
| Terms of Use | /terms.html |
Both documents are effective as of 8 May 2026. SpaceX Tracker is an unofficial, fan-made app — not affiliated with Space Exploration Technologies Corp.
Products are listed in merch/index.json and follow the same CMS pattern as the news feed. The app fetches this file at launch and displays product cards on the Dashboard.
Prices are intentionally omitted — they vary by Etsy currency and change without notice. The app links directly to each Etsy listing where the buyer sees the current local price.
Push a new entry to the products array in merch/index.json — no app update required. Each product entry needs:
| Field | Required | Notes |
|---|---|---|
id |
yes | URL-safe slug, unique across all products |
title |
yes | |
subtitle |
no | Short descriptor shown on the card |
category |
yes | e.g. apparel, poster, accessory |
image_url |
yes | Direct image URL (e.g. etsystatic.com CDN) |
url |
yes | Full Etsy listing URL |
The store_url field at the top level can be set to the Etsy shop landing page URL once one is available (currently null).
| Input | Required | Description |
|---|---|---|
title |
yes | Notification title |
body |
yes | Notification body |
target |
no | FCM topic or token (default: all_users) |
interruption_level |
no | active (default), time-sensitive, or passive |
article_id |
no | Article ID for in-app deep-link |
notes |
no | Free-text notes stored in the log |
push_history.csv.| Level | Sound | Priority | Use case |
|---|---|---|---|
active |
✓ | 10 | Default — plays sound, Focus modes may defer |
time-sensitive |
✓ | 10 | Breaks through Focus/DND/Sleep — use sparingly |
passive |
✗ | 5 | Silent, appears only in Notification Center |
Stats are pulled from GA4 (property 534379147) automatically every Monday at 09:00 CEST via the Refresh Push Stats workflow. You can also trigger it manually:
days to change the lookback window, default 14)The script queries GA4 for notification events in a 3-day window after each push and writes the counts back to push_history.csv.
| Column | Description |
|---|---|
timestamp_utc |
UTC timestamp of when the push was sent (YYYY-MM-DDTHH:MM:SSZ) |
timestamp_cest |
Same timestamp in Europe/Oslo timezone |
target |
FCM topic or token the push was sent to |
interruption_level |
active, time-sensitive, or passive |
title |
Notification title |
body |
Notification body |
article_id |
Article deep-link ID (empty if not set) |
message_id |
FCM v1 message ID returned by the API (projects/.../messages/...) |
api |
Always fcm_v1 |
opens |
notification_open event count (GA4, 3-day window) |
unique_users |
Unique users who opened the notification (GA4 totalUsers) |
foreground |
notification_foreground event count |
toggled |
notification_toggled event count |
notes |
Free-text notes entered at send time |
Stats columns (opens, unique_users, foreground, toggled) are blank when a push is first logged and filled in by the refresh script.
Important: Service accounts do not automatically have access to GA4 data. The refresh script will fail with a 403 error until this is configured.
To grant access:
firebase-adminsdk-XXXX@spacex-tracker-97b32.iam.gserviceaccount.com
FIREBASE_SERVICE_ACCOUNT secret JSON, field client_emailAlternatively, create a dedicated GA4 service account and store its JSON in the GA4_SERVICE_ACCOUNT secret. The refresh script checks GA4_SERVICE_ACCOUNT first, then falls back to FIREBASE_SERVICE_ACCOUNT.