diff --git a/templates/launches.html b/templates/launches.html index 09be1f7..f2576dc 100644 --- a/templates/launches.html +++ b/templates/launches.html @@ -68,7 +68,10 @@
No description.
{% endif %} - {% if launch.weather_concerns %} + {% if launch.weather_concerns and launch.status.name != "Launch Successful" %}{{ line }}
diff --git a/update.py b/update.py index 3d2576c..2cb8ec5 100755 --- a/update.py +++ b/update.py @@ -247,9 +247,52 @@ def is_test_flight(launch: StrDict) -> bool: def get_launch_by_slug(data: StrDict, slug: str) -> StrDict | None: """Find last update for space launch.""" - return {item["slug"]: typing.cast(StrDict, item) for item in data["results"]}.get( - slug + results = data.get("results") + if not isinstance(results, list): + return None + + by_slug: dict[str, StrDict] = {} + for item in results: + if not isinstance(item, dict): + continue + item_slug = item.get("slug") + if isinstance(item_slug, str): + by_slug[item_slug] = typing.cast(StrDict, item) + + return by_slug.get(slug) + + +def send_thespacedevs_payload_alert( + config: flask.config.Config, + reason: str, + data: StrDict | None, +) -> None: + """Alert admin when SpaceDevs update payload is missing expected fields.""" + payload = data or {} + detail = payload.get("detail") + status_code = payload.get("status_code", payload.get("status")) + + detail_text = detail if isinstance(detail, str) else "" + is_rate_limited = ( + status_code == 429 + or "rate" in detail_text.lower() + or "thrott" in detail_text.lower() ) + alert_type = "rate-limit" if is_rate_limited else "error" + + subject = f"⚠️ SpaceDevs {alert_type}: {reason}" + body = f"""SpaceDevs update returned an unexpected payload. + +Reason: {reason} +Type: {alert_type} +Status: {status_code!r} +Detail: {detail!r} +Payload keys: {sorted(payload.keys())} + +Expected payload shape includes a top-level 'results' list. +Updater: /home/edward/src/agenda/update.py +""" + agenda.mail.send_mail(config, subject, body) def update_thespacedevs(config: flask.config.Config) -> None: @@ -283,12 +326,26 @@ def update_thespacedevs(config: flask.config.Config) -> None: t0 = time() data = agenda.thespacedevs.next_launch_api_data(rocket_dir) if not data: + send_thespacedevs_payload_alert( + config, + reason="API request failed or returned invalid JSON", + data=None, + ) return # thespacedevs API call failed + data_results = data.get("results") + if not isinstance(data_results, list): + send_thespacedevs_payload_alert( + config, + reason="response missing top-level results list", + data=data, + ) + return + # Identify test-flight slugs present in the current data cur_test_slugs: set[str] = { typing.cast(str, item["slug"]) - for item in data.get("results", []) + for item in data_results if is_test_flight(typing.cast(StrDict, item)) } @@ -317,7 +374,7 @@ def update_thespacedevs(config: flask.config.Config) -> None: time_taken = time() - t0 if not sys.stdin.isatty(): return - rockets = [agenda.thespacedevs.summarize_launch(item) for item in data["results"]] + rockets = [agenda.thespacedevs.summarize_launch(item) for item in data_results] print(len(rockets), "launches") print(len(active_crewed or []), "active crewed missions") print(f"took {time_taken:.1f} seconds") diff --git a/web_view.py b/web_view.py index ad04588..4a80dc7 100755 --- a/web_view.py +++ b/web_view.py @@ -177,11 +177,22 @@ async def recent() -> str: @app.route("/launches") def launch_list() -> str: """Web page showing List of space launches.""" - now = datetime.now() + now = datetime.now(timezone.utc) data_dir = app.config["DATA_DIR"] rocket_dir = os.path.join(data_dir, "thespacedevs") launches = agenda.thespacedevs.get_launches(rocket_dir, limit=100) assert launches + active_crewed = agenda.thespacedevs.get_active_crewed_flights(rocket_dir) or [] + active_crewed_slugs = { + launch["slug"] + for launch in active_crewed + if isinstance(launch.get("slug"), str) + } + + for launch in launches: + launch_net = agenda.thespacedevs.parse_api_datetime(launch.get("net")) + launch["is_future"] = bool(launch_net and launch_net > now) + launch["is_active_crewed"] = launch.get("slug") in active_crewed_slugs mission_type_filter = flask.request.args.get("type") rocket_filter = flask.request.args.get("rocket")