Improve launch status UI and alert on SpaceDevs payload errors
This commit is contained in:
parent
458dfc5136
commit
7a50ea6016
3 changed files with 78 additions and 7 deletions
|
|
@ -68,7 +68,10 @@
|
||||||
<div class="col-md-1 text-md-nowrap">
|
<div class="col-md-1 text-md-nowrap">
|
||||||
<span class="d-md-none">launch status:</span>
|
<span class="d-md-none">launch status:</span>
|
||||||
<abbr title="{{ launch.status.name }}">{{ launch.status.abbrev }}</abbr>
|
<abbr title="{{ launch.status.name }}">{{ launch.status.abbrev }}</abbr>
|
||||||
{% if launch.probability %}{{ launch.probability }}%{% endif %}
|
{% if launch.is_active_crewed %}
|
||||||
|
<span class="badge text-bg-info">In space</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if launch.is_future and launch.probability %}{{ launch.probability }}%{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -121,7 +124,7 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>No description.</p>
|
<p>No description.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if launch.weather_concerns %}
|
{% if launch.weather_concerns and launch.status.name != "Launch Successful" %}
|
||||||
<h4>Weather concerns</h4>
|
<h4>Weather concerns</h4>
|
||||||
{% for line in launch.weather_concerns.splitlines() %}
|
{% for line in launch.weather_concerns.splitlines() %}
|
||||||
<p>{{ line }}</p>
|
<p>{{ line }}</p>
|
||||||
|
|
|
||||||
65
update.py
65
update.py
|
|
@ -247,9 +247,52 @@ def is_test_flight(launch: StrDict) -> bool:
|
||||||
|
|
||||||
def get_launch_by_slug(data: StrDict, slug: str) -> StrDict | None:
|
def get_launch_by_slug(data: StrDict, slug: str) -> StrDict | None:
|
||||||
"""Find last update for space launch."""
|
"""Find last update for space launch."""
|
||||||
return {item["slug"]: typing.cast(StrDict, item) for item in data["results"]}.get(
|
results = data.get("results")
|
||||||
slug
|
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:
|
def update_thespacedevs(config: flask.config.Config) -> None:
|
||||||
|
|
@ -283,12 +326,26 @@ def update_thespacedevs(config: flask.config.Config) -> None:
|
||||||
t0 = time()
|
t0 = time()
|
||||||
data = agenda.thespacedevs.next_launch_api_data(rocket_dir)
|
data = agenda.thespacedevs.next_launch_api_data(rocket_dir)
|
||||||
if not data:
|
if not data:
|
||||||
|
send_thespacedevs_payload_alert(
|
||||||
|
config,
|
||||||
|
reason="API request failed or returned invalid JSON",
|
||||||
|
data=None,
|
||||||
|
)
|
||||||
return # thespacedevs API call failed
|
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
|
# Identify test-flight slugs present in the current data
|
||||||
cur_test_slugs: set[str] = {
|
cur_test_slugs: set[str] = {
|
||||||
typing.cast(str, item["slug"])
|
typing.cast(str, item["slug"])
|
||||||
for item in data.get("results", [])
|
for item in data_results
|
||||||
if is_test_flight(typing.cast(StrDict, item))
|
if is_test_flight(typing.cast(StrDict, item))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,7 +374,7 @@ def update_thespacedevs(config: flask.config.Config) -> None:
|
||||||
time_taken = time() - t0
|
time_taken = time() - t0
|
||||||
if not sys.stdin.isatty():
|
if not sys.stdin.isatty():
|
||||||
return
|
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(rockets), "launches")
|
||||||
print(len(active_crewed or []), "active crewed missions")
|
print(len(active_crewed or []), "active crewed missions")
|
||||||
print(f"took {time_taken:.1f} seconds")
|
print(f"took {time_taken:.1f} seconds")
|
||||||
|
|
|
||||||
13
web_view.py
13
web_view.py
|
|
@ -177,11 +177,22 @@ async def recent() -> str:
|
||||||
@app.route("/launches")
|
@app.route("/launches")
|
||||||
def launch_list() -> str:
|
def launch_list() -> str:
|
||||||
"""Web page showing List of space launches."""
|
"""Web page showing List of space launches."""
|
||||||
now = datetime.now()
|
now = datetime.now(timezone.utc)
|
||||||
data_dir = app.config["DATA_DIR"]
|
data_dir = app.config["DATA_DIR"]
|
||||||
rocket_dir = os.path.join(data_dir, "thespacedevs")
|
rocket_dir = os.path.join(data_dir, "thespacedevs")
|
||||||
launches = agenda.thespacedevs.get_launches(rocket_dir, limit=100)
|
launches = agenda.thespacedevs.get_launches(rocket_dir, limit=100)
|
||||||
assert launches
|
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")
|
mission_type_filter = flask.request.args.get("type")
|
||||||
rocket_filter = flask.request.args.get("rocket")
|
rocket_filter = flask.request.args.get("rocket")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue