fix: stabilize realtime chart axis and redis deployment
This commit is contained in:
+25
-4
@@ -1,6 +1,24 @@
|
||||
services:
|
||||
polyweather_redis:
|
||||
image: redis:7-alpine
|
||||
container_name: polyweather_redis
|
||||
command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy noeviction
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
interval: 10s
|
||||
retries: 5
|
||||
test:
|
||||
- CMD
|
||||
- redis-cli
|
||||
- ping
|
||||
timeout: 5s
|
||||
volumes:
|
||||
- polyweather_redis_data:/data
|
||||
polyweather:
|
||||
container_name: polyweather_bot
|
||||
depends_on:
|
||||
polyweather_redis:
|
||||
condition: service_healthy
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
@@ -52,10 +70,8 @@ services:
|
||||
interval: 30s
|
||||
retries: 3
|
||||
test:
|
||||
- CMD
|
||||
- wget
|
||||
- -qO-
|
||||
- http://localhost:3000
|
||||
- CMD-SHELL
|
||||
- wget -qO- http://$(hostname):3000
|
||||
timeout: 5s
|
||||
image: ghcr.io/yangyuan-zhen/polyweather-frontend:${IMAGE_TAG:-latest}
|
||||
ports:
|
||||
@@ -63,6 +79,9 @@ services:
|
||||
restart: unless-stopped
|
||||
polyweather_web:
|
||||
command: python web/app.py
|
||||
depends_on:
|
||||
polyweather_redis:
|
||||
condition: service_healthy
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
@@ -90,3 +109,5 @@ services:
|
||||
x-polyweather-base:
|
||||
env_file: *id001
|
||||
image: ghcr.io/yangyuan-zhen/polyweather-backend:${IMAGE_TAG:-latest}
|
||||
volumes:
|
||||
polyweather_redis_data:
|
||||
|
||||
+33
@@ -1109,6 +1109,39 @@ export function runTests() {
|
||||
"Full-day chart should start at local 00:00 when the DEB hourly path has a midnight point",
|
||||
);
|
||||
|
||||
const qingdaoPartialDetailFullDay = __buildTemperatureChartDataForTest(
|
||||
{
|
||||
city: "qingdao",
|
||||
local_date: "2026-05-26",
|
||||
local_time: "11:11",
|
||||
tz_offset_seconds: 8 * 60 * 60,
|
||||
deb_prediction: 22,
|
||||
runway_plate_history: {
|
||||
"16/34": [
|
||||
{ time: "2026-05-26T00:05:00+08:00", temp: 23.5 },
|
||||
{ time: "2026-05-26T09:28:00+08:00", temp: 19.8 },
|
||||
{ time: "2026-05-26T11:11:00+08:00", temp: 19.8 },
|
||||
],
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
localDate: "2026-05-26",
|
||||
localTime: "11:11",
|
||||
times: ["00:00", "06:00", "09:00", "10:00"],
|
||||
temps: [22, 18.5, 19.1, 19.6],
|
||||
debPrediction: 22,
|
||||
} as any,
|
||||
"1D",
|
||||
);
|
||||
assert(
|
||||
qingdaoPartialDetailFullDay.data[0]?.ts === qingdaoDayStart,
|
||||
"All-day view should keep the local-day start even when Qingdao detail data is partial",
|
||||
);
|
||||
assert(
|
||||
qingdaoPartialDetailFullDay.data.some((point) => point.ts === qingdaoDayEnd - 60 * 60 * 1000),
|
||||
"All-day view should keep an end-of-day axis slot even when Qingdao detail data only reaches the morning",
|
||||
);
|
||||
|
||||
const chongqingRolledToNextDay = __buildTemperatureChartDataForTest(
|
||||
{
|
||||
city: "chongqing",
|
||||
|
||||
@@ -1320,6 +1320,13 @@ function sortedTimeline(timestamps: Iterable<number>) {
|
||||
return Array.from(new Set(Array.from(timestamps).filter((ts) => Number.isFinite(ts)))).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function addLocalDayAxisSlots(timeline: Set<number>, bounds: LocalDayBounds | null) {
|
||||
if (!bounds) return;
|
||||
for (let ts = bounds.start; ts < bounds.end; ts += 60 * 60 * 1000) {
|
||||
timeline.add(ts);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveFullDayFallbackAnchor(
|
||||
row: ScanOpportunityRow | null,
|
||||
hourly: HourlyForecast,
|
||||
@@ -1582,6 +1589,7 @@ function buildFullDayChartData(
|
||||
finalSettlementObs.forEach((point) => timelineSet.add(point.ts));
|
||||
if (!isRunwaySensorAggregateSource) finalMadisObs.forEach((point) => timelineSet.add(point.ts));
|
||||
if (shouldRenderMetar) metarObs.forEach((point) => timelineSet.add(point.ts));
|
||||
addLocalDayAxisSlots(timelineSet, localDayBounds);
|
||||
|
||||
let debPath: ReturnType<typeof buildDebBaselinePath> | null = null;
|
||||
if (hourly?.times?.length && hourly?.temps?.length) {
|
||||
|
||||
@@ -18,6 +18,8 @@ from web.realtime_patch_schema import EVENT_TYPE
|
||||
DEFAULT_STREAM_KEY = "stream:city_observation"
|
||||
DEFAULT_COUNTER_KEY = "counter:city_observation_revision"
|
||||
DEFAULT_MAXLEN = 50000
|
||||
DEFAULT_SOCKET_TIMEOUT_SECONDS = 15.0
|
||||
DEFAULT_SOCKET_CONNECT_TIMEOUT_SECONDS = 5.0
|
||||
|
||||
APPEND_EVENT_SCRIPT = """
|
||||
local revision = redis.call('INCR', KEYS[2])
|
||||
@@ -60,6 +62,13 @@ def _int_or_zero(value: Any) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _float_env(name: str, default: float) -> float:
|
||||
try:
|
||||
return float(os.getenv(name) or default)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
class RedisRealtimeEventStore:
|
||||
"""Persist replayable observation patch events in a Redis Stream."""
|
||||
|
||||
@@ -94,8 +103,11 @@ class RedisRealtimeEventStore:
|
||||
url = redis_url or os.getenv("POLYWEATHER_REDIS_URL") or "redis://127.0.0.1:6379/0"
|
||||
client = redis.Redis.from_url(
|
||||
url,
|
||||
socket_timeout=5,
|
||||
socket_connect_timeout=5,
|
||||
socket_timeout=_float_env("POLYWEATHER_REDIS_SOCKET_TIMEOUT_SECONDS", DEFAULT_SOCKET_TIMEOUT_SECONDS),
|
||||
socket_connect_timeout=_float_env(
|
||||
"POLYWEATHER_REDIS_SOCKET_CONNECT_TIMEOUT_SECONDS",
|
||||
DEFAULT_SOCKET_CONNECT_TIMEOUT_SECONDS,
|
||||
),
|
||||
health_check_interval=30,
|
||||
)
|
||||
client.ping()
|
||||
|
||||
Reference in New Issue
Block a user