fix: stabilize realtime chart axis and redis deployment
This commit is contained in:
+25
-4
@@ -1,6 +1,24 @@
|
|||||||
services:
|
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:
|
polyweather:
|
||||||
container_name: polyweather_bot
|
container_name: polyweather_bot
|
||||||
|
depends_on:
|
||||||
|
polyweather_redis:
|
||||||
|
condition: service_healthy
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
@@ -52,10 +70,8 @@ services:
|
|||||||
interval: 30s
|
interval: 30s
|
||||||
retries: 3
|
retries: 3
|
||||||
test:
|
test:
|
||||||
- CMD
|
- CMD-SHELL
|
||||||
- wget
|
- wget -qO- http://$(hostname):3000
|
||||||
- -qO-
|
|
||||||
- http://localhost:3000
|
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
image: ghcr.io/yangyuan-zhen/polyweather-frontend:${IMAGE_TAG:-latest}
|
image: ghcr.io/yangyuan-zhen/polyweather-frontend:${IMAGE_TAG:-latest}
|
||||||
ports:
|
ports:
|
||||||
@@ -63,6 +79,9 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
polyweather_web:
|
polyweather_web:
|
||||||
command: python web/app.py
|
command: python web/app.py
|
||||||
|
depends_on:
|
||||||
|
polyweather_redis:
|
||||||
|
condition: service_healthy
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
@@ -90,3 +109,5 @@ services:
|
|||||||
x-polyweather-base:
|
x-polyweather-base:
|
||||||
env_file: *id001
|
env_file: *id001
|
||||||
image: ghcr.io/yangyuan-zhen/polyweather-backend:${IMAGE_TAG:-latest}
|
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",
|
"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(
|
const chongqingRolledToNextDay = __buildTemperatureChartDataForTest(
|
||||||
{
|
{
|
||||||
city: "chongqing",
|
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);
|
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(
|
function resolveFullDayFallbackAnchor(
|
||||||
row: ScanOpportunityRow | null,
|
row: ScanOpportunityRow | null,
|
||||||
hourly: HourlyForecast,
|
hourly: HourlyForecast,
|
||||||
@@ -1582,6 +1589,7 @@ function buildFullDayChartData(
|
|||||||
finalSettlementObs.forEach((point) => timelineSet.add(point.ts));
|
finalSettlementObs.forEach((point) => timelineSet.add(point.ts));
|
||||||
if (!isRunwaySensorAggregateSource) finalMadisObs.forEach((point) => timelineSet.add(point.ts));
|
if (!isRunwaySensorAggregateSource) finalMadisObs.forEach((point) => timelineSet.add(point.ts));
|
||||||
if (shouldRenderMetar) metarObs.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;
|
let debPath: ReturnType<typeof buildDebBaselinePath> | null = null;
|
||||||
if (hourly?.times?.length && hourly?.temps?.length) {
|
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_STREAM_KEY = "stream:city_observation"
|
||||||
DEFAULT_COUNTER_KEY = "counter:city_observation_revision"
|
DEFAULT_COUNTER_KEY = "counter:city_observation_revision"
|
||||||
DEFAULT_MAXLEN = 50000
|
DEFAULT_MAXLEN = 50000
|
||||||
|
DEFAULT_SOCKET_TIMEOUT_SECONDS = 15.0
|
||||||
|
DEFAULT_SOCKET_CONNECT_TIMEOUT_SECONDS = 5.0
|
||||||
|
|
||||||
APPEND_EVENT_SCRIPT = """
|
APPEND_EVENT_SCRIPT = """
|
||||||
local revision = redis.call('INCR', KEYS[2])
|
local revision = redis.call('INCR', KEYS[2])
|
||||||
@@ -60,6 +62,13 @@ def _int_or_zero(value: Any) -> int:
|
|||||||
return 0
|
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:
|
class RedisRealtimeEventStore:
|
||||||
"""Persist replayable observation patch events in a Redis Stream."""
|
"""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"
|
url = redis_url or os.getenv("POLYWEATHER_REDIS_URL") or "redis://127.0.0.1:6379/0"
|
||||||
client = redis.Redis.from_url(
|
client = redis.Redis.from_url(
|
||||||
url,
|
url,
|
||||||
socket_timeout=5,
|
socket_timeout=_float_env("POLYWEATHER_REDIS_SOCKET_TIMEOUT_SECONDS", DEFAULT_SOCKET_TIMEOUT_SECONDS),
|
||||||
socket_connect_timeout=5,
|
socket_connect_timeout=_float_env(
|
||||||
|
"POLYWEATHER_REDIS_SOCKET_CONNECT_TIMEOUT_SECONDS",
|
||||||
|
DEFAULT_SOCKET_CONNECT_TIMEOUT_SECONDS,
|
||||||
|
),
|
||||||
health_check_interval=30,
|
health_check_interval=30,
|
||||||
)
|
)
|
||||||
client.ping()
|
client.ping()
|
||||||
|
|||||||
Reference in New Issue
Block a user