Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion algorithm-catalog/data/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@
"name": "Hurricane Isaias October 2026",
"stacName": "202610_Hurricane_Isaias",
"start": "2026-10-09",
"end": "2026-10-09",
"hazards": ["TropicalCyclone", "Flood"],
"locations": ["Florida", "Alabama"],
"places": ["US-FL", "US-AL"],
Expand Down
17 changes: 16 additions & 1 deletion algorithm-catalog/scripts/e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,17 @@ await page.goto(BASE, { waitUntil: "networkidle" });
await page.locator("button", { hasText: /event/i }).first().click();
await page.waitForTimeout(300);
check("imported events are listed (8)", (await page.locator(".ecard").count()) === 8);
check(
"an event with no end date reads as ongoing",
/9 Oct 2026 – ongoing/.test(await page.locator(".ecard", { hasText: "Hurricane Isaias" }).innerText()),
);
await page.locator('input[type="date"]').first().fill("2026-10-15");
await page.waitForTimeout(300);
check(
"an ongoing event still matches a later date filter",
(await page.locator(".ecard", { hasText: "Hurricane Isaias" }).count()) === 1,
);
await page.locator("button", { hasText: "Reset" }).click();
await page.locator(".ecard", { hasText: "Hurricane Isaias October 2026" }).click();
await page.locator('[role="tab"]', { hasText: "Page preview" }).click();
const preview = await page.locator(".page-preview").innerText();
Expand All @@ -234,6 +245,7 @@ await page.locator("button", { hasText: "Edit page" }).click();
await page.waitForTimeout(400);
check("Edit page opens Submit prefilled", (await page.locator("#req-name").inputValue()) === "Hurricane Isaias October 2026");
check("…with the event's places as chips", (await page.locator(".place-chip").count()) === 2);
check("…and the end date blank (optional — ongoing)", (await page.locator("#req-end").inputValue()) === "");
await page.locator("#req-subtitle").fill('As Hurricane Isaias made landfall, NASA "activated" to support FEMA.');
await page.locator("button.req-haz", { hasText: /^Respond$/ }).click();
await page.locator("#req-intro").fill("Hurricane Isaias brought storm surge to Florida and Alabama.\n\nNASA is sharing maps.");
Expand All @@ -257,7 +269,10 @@ const opened = await page.evaluate(() => window.__opened[0] || "");
const doc = opened ? JSON.parse(new URL(opened).searchParams.get("value"))[0] : {};
check(
"Open PR carries the page + places in the request file",
doc.page?.intro?.startsWith("Hurricane Isaias") && doc.page?.needs?.[0] === "respond" && doc.event?.places?.length === 2,
doc.page?.intro?.startsWith("Hurricane Isaias") &&
doc.page?.needs?.[0] === "respond" &&
doc.event?.places?.length === 2 &&
!("end" in (doc.event ?? {})),
opened ? new URL(opened).searchParams.get("filename") : "no window.open",
);

Expand Down
8 changes: 4 additions & 4 deletions algorithm-catalog/scripts/import_xlsx.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,16 +288,16 @@ def main(argv: list[str] | None = None) -> int:
report.append(f"{where} {len(external)} layer(s) have no catalog algorithm — kept as externalLayers: {', '.join(external)}")

start = excel_date(row["F"]) if row.get("F") else ""
end = excel_date(row["G"]) if row.get("G") else start
if not row.get("G"):
report.append(f"{where} no end date in the sheet — set end = start ({start}).")
end = excel_date(row["G"]) if row.get("G") else ""
if not end:
report.append(f"{where} no end date in the sheet — imported as ongoing (end is optional).")

ev = {
"id": stac,
"name": name,
"stacName": stac,
"start": start,
"end": end,
**({"end": end} if end else {}),
"hazards": hz,
"locations": [*(gaz.label(i) for i in places), *free],
}
Expand Down
2 changes: 2 additions & 0 deletions algorithm-catalog/scripts/rules_parity_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ def check_inline_regexes() -> None:
# --- date ordering -----------------------------------------------------------------------------
("date-order", ("2025-01-07", "2025-01-31"), True, None, "start before end"),
("date-order", ("2025-01-07", "2025-01-07"), True, None, "single-day event"),
("date-order", ("2026-10-09", ""), True, None, "no end date — the event is ongoing (end is optional)"),
("date-order", ("2026-10-09", "2026-13-01"), False, "format", "an end date, if given, must still be valid"),
("date-order", ("2025-01-31", "2025-01-07"), False, "order", "end before start"),
# --- bbox --------------------------------------------------------------------------------------
("bbox", "", True, None, "bbox is optional"),
Expand Down
7 changes: 3 additions & 4 deletions algorithm-catalog/scripts/validate_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,16 +266,15 @@ def _is(h: dict[str, Any]) -> bool:


def check_dates(start: str, end: str) -> list[Violation]:
"""ISO shape + ordering. Upstream only checks shape; start<=end is our own catalog invariant."""
"""ISO shape + ordering. Upstream only checks shape; start<=end is our own catalog invariant.
The END date is OPTIONAL: blank means the event is still ongoing. Mirrors rules.ts checkDates."""
out: list[Violation] = []
if not start:
out.append(_err("start", "required", "Start date is required."))
elif not matches(_ISO_DATE, start):
out.append(_err("start", "format", f'Start date "{start}" must be YYYY-MM-DD.'))

if not end:
out.append(_err("end", "required", "End date is required."))
elif not matches(_ISO_DATE, end):
if end and not matches(_ISO_DATE, end):
out.append(_err("end", "format", f'End date "{end}" must be YYYY-MM-DD.'))

if start and end and matches(_ISO_DATE, start) and matches(_ISO_DATE, end) and start > end:
Expand Down
4 changes: 3 additions & 1 deletion algorithm-catalog/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,10 @@ export function formatDate(iso: string): string {
return `${Number(m[3])} ${MONTHS[Number(m[2]) - 1]} ${m[1]}`;
}

export function formatRange(start: string, end: string): string {
/** A missing end date means the event is still ongoing. */
export function formatRange(start: string, end?: string): string {
if (!start && !end) return "no dates";
if (!end) return `${formatDate(start)} – ongoing`;
if (start === end) return formatDate(start);
return `${formatDate(start)} – ${formatDate(end)}`;
}
8 changes: 4 additions & 4 deletions algorithm-catalog/src/filters/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export function applyEvent(f: Filters, u: Universe, ev: ActivationEvent | null):
eventId: ev.id,
hazards: new Set(known.length > 0 ? known : u.hazards),
start: ev.start,
end: ev.end,
end: ev.end ?? "",
aoi: ev.locations[0] ?? "",
};
}
Expand Down Expand Up @@ -229,9 +229,9 @@ export function eventHaystack(
.toLowerCase();
}

/** Two windows overlap unless one ends before the other begins. */
function overlaps(evStart: string, evEnd: string, start: string, end: string): boolean {
if (start && evEnd < start) return false;
/** Two windows overlap unless one ends before the other begins. No end = ongoing, never ended. */
function overlaps(evStart: string, evEnd: string | undefined, start: string, end: string): boolean {
if (start && evEnd && evEnd < start) return false;
if (end && evStart > end) return false;
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion algorithm-catalog/src/pageExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export function pageView(ev: PageEvent, algorithms: Algorithm[], hazards: Hazard
hazards: ev.hazards.map((h) => hazardLabel(h, hazards)),
region: ev.page?.region?.trim() ? [ev.page.region.trim()] : ev.locations,
startDate: ev.start,
endDate: ev.end,
endDate: ev.end ?? "",
govLink: ev.page?.govLink ?? "",
paragraphs: (ev.page?.intro ?? "").split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean),
stacName: ev.stacName,
Expand Down
9 changes: 7 additions & 2 deletions algorithm-catalog/src/request/RequestForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ function deriveInput(input: AlgorithmInput, d: RequestDraft, tokens: string[]):
return { input, value: d.start, source: "start date" };
}
if (has("end") || n === "date_to" || n === "to_date") {
return { input, value: d.end, source: "end date" };
return d.end
? { input, value: d.end, source: "end date" }
: { input, value: "", source: "no end date — the event is ongoing; set by the operator at run time" };
}
if (has("bbox") || has("bound") || has("extent")) {
return { input, value: d.bbox, source: "bounding box" };
Expand Down Expand Up @@ -460,7 +462,9 @@ export default function RequestForm({ algorithms, hazards, events }: Props) {
<Issues all={violations} field="start" show={shown("start")} />
</div>
<div>
<label htmlFor="req-end">End date</label>
<label htmlFor="req-end">
End date <span className="opt">(optional)</span>
</label>
<input
id="req-end"
type="date"
Expand All @@ -469,6 +473,7 @@ export default function RequestForm({ algorithms, hazards, events }: Props) {
onChange={(e) => patch({ end: e.target.value })}
onBlur={() => touch("end")}
/>
<div className="req-help">Leave blank while the event is ongoing.</div>
<Issues all={violations} field="end" show={shown("end")} />
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions algorithm-catalog/src/request/drafts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface RequestDraft {
name: string; // human-readable, e.g. "California Wildfires January 2025"
stacName: string; // YYYYMM_Hazard_Location, e.g. "202501_Fire_CA"
start: string; // YYYY-MM-DD
end: string; // YYYY-MM-DD
end: string; // YYYY-MM-DD — optional; blank = ongoing
bbox: string; // "min_lon,min_lat,max_lon,max_lat" — optional
mgrsTiles: string; // optional, space-separated Sentinel-2 MGRS tiles, e.g. "T17RLN T17RLM"
wrs2Tiles: string; // optional, space-separated Landsat WRS-2 path/rows, e.g. "171035"
Expand Down Expand Up @@ -140,7 +140,7 @@ export function draftToRequest(d: RequestDraft, ts: string): AlgorithmRequest {
name: d.name.trim(),
stacName: d.stacName.trim(),
start: d.start,
end: d.end,
...(d.end ? { end: d.end } : {}),
hazards: [...d.hazards],
locations: allLocations(d),
},
Expand Down Expand Up @@ -193,7 +193,7 @@ export function requestToDraft(r: AlgorithmRequest): RequestDraft {
d.name = r.event.name;
d.stacName = r.event.stacName;
d.start = r.event.start;
d.end = r.event.end;
d.end = r.event.end ?? "";
d.bbox = r.event.bbox ?? "";
d.mgrsTiles = (r.event.tiles?.mgrs ?? []).join(" ");
d.wrs2Tiles = (r.event.tiles?.wrs2 ?? []).join(" ");
Expand Down
9 changes: 6 additions & 3 deletions algorithm-catalog/src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,15 +214,18 @@ export function checkStacName(value: string, hazards: Hazard[] = [], selected: s
return out;
}

/** ISO shape + ordering. Upstream only checks shape; start<=end is our own catalog invariant. */
/**
* ISO shape + ordering. Upstream only checks shape; start<=end is our own catalog invariant.
* The END date is OPTIONAL: blank means the event is still ongoing (the portal itself records only
* a startDate). Only a malformed or out-of-order end is an error.
*/
export function checkDates(start: string, end: string): Violation[] {
const out: Violation[] = [];
if (!start) out.push(err("start", "required", "Start date is required."));
else if (!ISO_DATE_RE.test(start))
out.push(err("start", "format", `Start date "${start}" must be YYYY-MM-DD.`));

if (!end) out.push(err("end", "required", "End date is required."));
else if (!ISO_DATE_RE.test(end)) out.push(err("end", "format", `End date "${end}" must be YYYY-MM-DD.`));
if (end && !ISO_DATE_RE.test(end)) out.push(err("end", "format", `End date "${end}" must be YYYY-MM-DD.`));

if (start && end && ISO_DATE_RE.test(start) && ISO_DATE_RE.test(end) && start > end) {
out.push(err("end", "order", `End date ${end} is before start date ${start}.`));
Expand Down
5 changes: 3 additions & 2 deletions algorithm-catalog/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ export interface ActivationEvent {
name: string; // human-readable, visible on the website
stacName: string; // YYYYMM_Hazard_Location
start: string; // "YYYY-MM-DD"
end: string; // "YYYY-MM-DD"
/** "YYYY-MM-DD", or absent while the event is ongoing. */
end?: string;
hazards: string[]; // hazard ids
/**
* Display strings, shown as the page's "Region". The first `places.length` of them are the
Expand Down Expand Up @@ -201,7 +202,7 @@ export interface AlgorithmRequest {
name: string;
stacName: string;
start: string;
end: string;
end?: string; // absent = ongoing
hazards: string[];
locations: string[];
places?: string[];
Expand Down
1 change: 1 addition & 0 deletions docs/ALGORITHM_CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ hand-kept `hazard_event_info.xlsx`; the Submit form's **2 · Page content** sect
| US-gov link | "What the U.S. government is doing" | `page.govLink` (https only) |
| Intro paragraph | Overview | `page.intro` (blank line = paragraph) |
| Start date | Start Date | `start` |
| End date | — (the portal records none) | `end` — **optional**; blank = ongoing (shown "– ongoing", never filtered out) |
| Need/Theme | Need | `page.needs` — the portal's theme **ids** `prepare/respond/recover/resilience` |
| Hazards | Hazard / categories | `hazards` → `hazards.json` `portal` (its `CONTENT_CATEGORIES`) |
| Locations | Region | `locations` (+ `places`), or `page.region` override |
Expand Down
Loading