feat: implement city weather detail API routing, dashboard data models, and monitoring infrastructure

This commit is contained in:
2569718930@qq.com
2026-05-14 14:21:28 +08:00
parent 0e220d890f
commit 02add6d994
18 changed files with 990 additions and 58 deletions
@@ -0,0 +1,18 @@
import assert from "node:assert/strict";
import { formatHttpErrorMessage } from "@/lib/http-error";
export function runTests() {
assert.equal(formatHttpErrorMessage(502), "HTTP 502");
assert.equal(
formatHttpErrorMessage(403, "Forbidden", '{"detail":"missing entitlement"}'),
"HTTP 403 Forbidden: missing entitlement",
);
assert.equal(
formatHttpErrorMessage(500, null, '{"error":{"code":"boom"}}'),
'HTTP 500: {"code":"boom"}',
);
assert.equal(
formatHttpErrorMessage(504, "Gateway Timeout", " upstream timed out\n"),
"HTTP 504 Gateway Timeout: upstream timed out",
);
}
@@ -0,0 +1,19 @@
import assert from "node:assert/strict";
import {
getMonitorRefreshRequest,
MONITOR_CITY_DETAIL_DEPTH,
} from "@/components/dashboard/monitoring/monitor-refresh-policy";
export function runTests() {
const initial = getMonitorRefreshRequest("initial");
assert.equal(
initial.force,
true,
"monitor initial load must force refresh instead of showing 30-minute session cache",
);
assert.equal(initial.depth, MONITOR_CITY_DETAIL_DEPTH);
const interval = getMonitorRefreshRequest("interval");
assert.equal(interval.force, true);
assert.equal(interval.depth, MONITOR_CITY_DETAIL_DEPTH);
}
@@ -0,0 +1,25 @@
import assert from "node:assert/strict";
import {
buildCityDetailProxyCachePolicy,
buildForceRefreshProxyCachePolicy,
isForceRefreshValue,
} from "@/lib/proxy-cache-policy";
export function runTests() {
assert.equal(isForceRefreshValue("true"), true);
assert.equal(isForceRefreshValue("false"), false);
assert.equal(isForceRefreshValue(null), false);
const forced = buildCityDetailProxyCachePolicy("true");
assert.equal(forced.fetchMode, "no-store");
assert.match(forced.responseCacheControl, /no-store/);
assert.equal(forced.revalidateSeconds, undefined);
const cached = buildCityDetailProxyCachePolicy("false", 15);
assert.equal(cached.fetchMode, "revalidate");
assert.equal(cached.revalidateSeconds, 15);
assert.match(cached.responseCacheControl, /s-maxage=15/);
const scanForced = buildForceRefreshProxyCachePolicy("true", 10);
assert.equal(scanForced.fetchMode, "no-store");
}
@@ -0,0 +1,92 @@
import assert from "node:assert/strict";
import {
buildObservationFreshness,
getMonitorFreshnessLevel,
getMonitorRefreshCadenceMs,
getObservationSourceProfile,
shouldRefreshMonitorCity,
} from "@/lib/source-freshness";
import type { CityDetail } from "@/lib/dashboard-types";
function detail(extra: Partial<CityDetail>): CityDetail {
return {
current: { temp: null },
display_name: "Test",
lat: 0,
local_date: "2026-05-14",
local_time: "12:00",
lon: 0,
name: "test",
risk: { level: "low" },
temp_symbol: "°C",
...extra,
} as CityDetail;
}
export function runTests() {
const now = new Date("2026-05-14T12:00:00Z");
assert.equal(getObservationSourceProfile("amos").nativeUpdateIntervalSec, 60);
assert.equal(getObservationSourceProfile("metar").nativeUpdateIntervalSec, 900);
const amosFresh = buildObservationFreshness({
now,
observedAt: "2026-05-14T11:59:10Z",
sourceCode: "amos",
sourceLabel: "AMOS",
});
assert.equal(amosFresh.freshness_status, "fresh");
assert.equal(getMonitorFreshnessLevel(amosFresh, null), "fresh");
assert.equal(
shouldRefreshMonitorCity({
detail: detail({ airport_current: { freshness: amosFresh, obs_time: "11:59", temp: 20 } }),
now,
trigger: "interval",
}),
false,
);
const metarStillExpected = buildObservationFreshness({
now,
observedAt: "2026-05-14T11:48:00Z",
sourceCode: "metar",
sourceLabel: "METAR",
});
assert.equal(metarStillExpected.freshness_status, "expected_wait");
assert.equal(getMonitorFreshnessLevel(metarStillExpected, null), "aging");
const metarOld = buildObservationFreshness({
now,
observedAt: "2026-05-14T10:50:00Z",
sourceCode: "metar",
sourceLabel: "METAR",
});
assert.equal(metarOld.freshness_status, "stale");
assert.equal(getMonitorFreshnessLevel(metarOld, null), "stale");
assert.equal(
shouldRefreshMonitorCity({
detail: detail({ airport_current: { freshness: metarOld, obs_time: "10:50", temp: 20 } }),
now,
trigger: "interval",
}),
true,
);
assert.equal(
shouldRefreshMonitorCity({ detail: undefined, now, trigger: "interval" }),
true,
"missing city detail should always be fetched",
);
assert.equal(
shouldRefreshMonitorCity({
detail: detail({ airport_current: { obs_age_min: 5, obs_time: "11:55", temp: 20 } }),
now,
trigger: "interval",
}),
false,
"legacy METAR age under native cadence should not be force-refreshed every minute",
);
assert.equal(getMonitorRefreshCadenceMs(["amos", "metar"]), 60_000);
assert.equal(getMonitorRefreshCadenceMs(["metar"]), 300_000);
}