Mục tiêu của TODO4 là xây dựng lớp sản phẩm cuối cùng của AIS: bản đồ chất lượng không khí kiểu Windy cho Bắc Bộ và Hà Nội, dashboard dự báo PM2.5, hiển thị quỹ đạo khối khí, plume forward nếu có, và giải thích nguồn ô nhiễm theo cụm/region.
TODO4 không viết lại pipeline dữ liệu lõi. TODO1, TODO2, TODO3 được xem là đã chạy được và là upstream contract cho lớp visualization.
Quy tắc request-time bắt buộc kế thừa từ TODO3:
Frontend request
-> Visualization API / tile endpoint
-> đọc bảng gold/visualization đã materialize hoặc cache/tile đã export
-> JSON/GeoJSON/tile response
API không được chạy Spark, HYSPLIT, ML inference, training, feature engineering, hoặc raw extraction trong request handler.
TODO4 là final product layer nằm trên TODO1/TODO2/TODO3.
Trạng thái upstream:
- TODO1 tạo Hanoi PM2.5 silver/gold pipeline.
- TODO2 tạo Tier-2 trajectory/HYSPLIT features.
- TODO3 tạo Kubernetes compute layer, ML inference, PM2.5 API, prediction table, K8s manifests, và serving checks.
Quyết định kiến trúc cho TODO4:
- TODO4 chỉ materialize sản phẩm visualization-ready và phục vụ cho UI.
- Visualization không query random silver/raw tables trực tiếp từ UI hoặc request handler.
- Visualization chỉ dùng stable gold/visualization tables và API contracts.
- Thêm namespace Iceberg
ais.visualization. - Thêm visualization gold tables.
- Thêm Spark jobs chạy trên K8s để build visualization tables.
- Thêm API endpoints cho forecast, heatmap grid, backward trajectories, forward plume, station observations, và source attribution.
- Tạo mới
serving/visualization_apilàm API product mặc định cho dashboard.serving/pm25_apicó thể giữ làm forecast compatibility endpoint, nhưng không nên mở rộng thêm nếu chưa refactor request-time Spark. - Thêm frontend map dashboard kiểu Windy, kết nối production API/tile endpoints.
- Mock data trong
ui/public/mockchỉ được dùng khi bật dev mode rõ ràng. - Thêm Airflow DAG để refresh visualization data.
- Thêm K8s manifests/check jobs cho visualization API/UI.
- Storage layer vẫn nằm ngoài K8s như TODO3: Kafka/HDFS/Iceberg/Cassandra/Airflow metadata/model artifact storage không migrate vào K8s trong TODO4.
- Request-time phải lightweight: đọc cache/tile/table đã materialize, không chạy compute nặng.
Decision quan trọng về API:
- Tạo mới
serving/visualization_apiđể tránh nhồi thêm product contract vào PM2.5 API hiện tại. - PM2.5 API hiện tại có forecast endpoint, nhưng implementation đang import PySpark và tạo
SparkSessiontrong request/readiness path. TODO4 không được copy pattern này cho visualization API. - Visualization API mặc định đọc precomputed JSON/GeoJSON/tile/cache manifest do Spark jobs export. Nếu cần đọc Iceberg trực tiếp, chỉ dùng lightweight reader/query service đã cấu hình sẵn, không dùng PySpark trong web process.
Tiêu chí chấp nhận của architecture decision:
- UI production gọi visualization API/tile endpoints, không gọi
/mock/*. - API request path không import hoặc khởi tạo Spark.
- Bảng/cache visualization được build bởi Spark-on-K8s/Airflow trước khi người dùng request.
- Có tài liệu rõ bảng nào là API contract và bảng nào chỉ là upstream source.
Inventory sau khi scan repo hiện tại:
| Item | Trạng thái hiện có | Quyết định TODO4 |
|---|---|---|
SYSTEM_OVERVIEW.md |
Có. Mô tả storage ngoài K8s, compute trên K8s, PM2.5 API, UI mock data. | use as-is, modify optional sau khi TODO4 hoàn thành để cập nhật product layer. |
TODO_1.md |
Có. Hanoi PM2.5 silver/gold pipeline. | use as-is làm upstream contract. |
TODO_2.md |
Có. Tier-2 trajectory/HYSPLIT, S5P grid, trajectory features. | use as-is làm upstream trajectory/source contract. |
TODO_3.md |
Có. K8s compute, serving features, prediction table, API/checks. | use as-is làm runtime và serving boundary. |
description/hanoi_trajectory_pipeline.md |
Có. Có blueprint cho source attribution GeoJSON và forward plume probability grid. | use as reference, production hóa bằng Iceberg tables/cache thay vì local output/*.geojson hoặc CSV. |
spark_jobs/hanoi_config.py |
Có table names cho TODO1/2/3 và config HYSPLIT/trajectory. | modify để thêm Northern Vietnam visualization config, ais.visualization.* table names, cache path helpers. |
spark_jobs/ensure_iceberg_tables.py |
Có namespace weather, air_quality, satellite, features, models, trajectory, predictions; có TODO3 tables. |
modify để thêm namespace ais.visualization và schema visualization gold tables. |
ais.features.hanoi_pm25_serving_features_gold |
Có trong config/schema TODO3. | use as-is cho forecast metadata và model/source fields, không expose trực tiếp cho UI. |
ais.predictions.hanoi_pm25_forecast_gold |
Có schema trong ensure_iceberg_tables.py; PM2.5 API đọc table này. |
use as-is làm input chính cho forecast cards và dashboard gold table. |
ais.models.hanoi_pm25_model_registry_gold |
Có schema trong ensure_iceberg_tables.py. |
use as-is để hiển thị model_version, freshness, production status. |
ais.trajectory.hysplit_trajectories_silver |
Có schema direction/backward/forward point-level. | modify via new jobs: không expose trực tiếp; aggregate sang trajectory GeoJSON gold. |
ais.trajectory.hysplit_trajectories_clustered_silver |
Có schema cluster/source point-level. | modify via new jobs: aggregate sang backward trajectory paths và source markers. |
ais.features.trajectory_path_satellite_silver |
Có path NO2/AER features theo traj_id. |
use as input cho trajectory popup và attribution evidence. |
ais.features.trajectory_hourly_features_silver |
Có dominant cluster/source/path features theo giờ. | use as input cho source attribution và forecast metadata. |
ais.satellite.sentinel5p_grid_silver |
Có grid S5P theo product/date/lat/lon. | use as input cho heatmap proxy và trajectory evidence; không query trực tiếp từ UI. |
ais.air_quality.openaq_hanoi_station_hourly_silver |
Có station-level PM2.5 Hà Nội. | use as input cho station layer và observed heatmap anchor. |
serving/pm25_api/main.py |
Có FastAPI /healthz, /readyz, /api/v1/hanoi/pm25/forecast/latest. Hiện dùng PySpark trong request/readiness. |
modify optional/hardening để loại Spark request-time; không dùng làm pattern cho visualization API. |
serving/pm25_api/README.md |
Ghi rõ API không được train/inference/Spark trong request, nhưng implementation cần hardening. | use as architecture intent, update sau nếu refactor. |
ml/predict_hanoi_pm25.py |
Có inference script đọc serving features + model registry, ghi prediction gold. | use as-is upstream; TODO4 không chạy inference trong API/UI. |
airflow/dags/ais_pm25_k8s_compute_dag.py |
Có DAG build serving features và train job; chưa orchestration visualization. | modify optional nếu nối dependency, nhưng nên create new DAG ais_visualization_product_dag.py. |
airflow/dags/ais_trajectory_tier2_dag.py |
Có DAG Tier-2 nhưng đang gọi scripts/submit_spark.sh fallback Compose path. |
use as upstream, modify optional để đồng bộ K8s path sau; TODO4 DAG chỉ phụ thuộc output table. |
deploy/k8s/api |
Có PM2.5 API deployment/service. | use as reference, create new visualization API manifests. |
deploy/k8s/checks |
Có PM2.5 serving check job. | extend bằng visualization check job. |
deploy/k8s/configmap.yaml |
Có env runtime TODO3. | modify để thêm visualization env vars, API base URL, cache URI, UI config. |
deploy/k8s/kustomization.yaml |
Hiện chưa include API/check resources. | modify để include visualization API/UI/check manifests khi sẵn sàng. |
ui |
Có Vite React app với Realtime/Historical dashboards. ui/src/services/api.js fetch trực tiếp /mock/*.json. Không có map library. |
modify heavily: dashboard mới là main page, production mode gọi API thật, mock chỉ sau dev flag. |
monitoring/app.py |
Có Flask monitoring pipeline/storage/ingest. | modify optional để thêm cards freshness visualization/API/UI. |
crawler/geoBoundaries-VNM-ADM1_simplified.geojson, crawler/geoBoundaries-VNM-ADM2_simplified.geojson, crawler/hanoi_districts_clean.geojson |
Có boundary assets. | optional dùng làm map overlay/source labeling nếu license/size phù hợp; không bắt buộc cho MVP. |
Current gaps cần giải quyết trong TODO4:
- Chưa có namespace
ais.visualization. - Chưa có visualization-ready heatmap/trajectory/plume/source/dashboard tables.
- Chưa có tile/cache export manifest cho request-time lightweight serving.
- Chưa có visualization API.
- UI đang là demo mock data, chưa là product dashboard.
- Chưa có K8s deployment cho UI.
- Chưa có Airflow DAG refresh visualization.
- Chưa có smoke test end-to-end từ visualization jobs -> API -> UI.
UI cuối cùng là một dashboard bản đồ full-screen tương tự tinh thần Windy, tập trung vào chất lượng không khí Bắc Bộ và Hà Nội.
Main page:
- Full-screen map.
- Viewport mặc định bao Bắc Bộ và Hà Nội:
- center gần Hà Nội.
- zoom đủ thấy Hà Nội, Hải Phòng, Quảng Ninh, Thái Nguyên, Bắc Ninh, Hưng Yên, Nam Định, Ninh Bình, Thanh Hóa, Lào Cai nếu bbox config bao phủ.
- Panel trái/phải/dưới tùy layout:
- forecast cards.
- source attribution.
- layer controls.
- timeline/chart.
- Time selector:
latest+6h+12h+24h
- Layer control:
- PM2.5 heatmap.
- backward trajectories.
- forward plume probability.
- source attribution markers.
- station observations.
- Forecast cards:
- PM2.5 now/latest observed.
- PM2.5 +6h.
- PM2.5 +12h.
- PM2.5 +24h.
generated_at.model_version.- data freshness.
- Chart:
- observed PM2.5 history.
- predicted PM2.5 future.
- Map interactions:
- Click trajectory:
traj_idcluster_idsource_latsource_lon- path NO2/AER evidence.
- age range.
- Click heatmap cell:
pm25_valuevalid_timehorizon_hsource_methoduncertainty
- Click source marker:
- source label.
- source region.
- contribution score.
- confidence.
- evidence fields.
- Click station:
- station name/location.
- PM2.5 observed.
- coverage.
- observation time.
- Click trajectory:
UX acceptance criteria:
- UI không rely vào mock data ở production mode.
- User switch được giữa latest/+6/+12/+24.
- User toggle độc lập heatmap, trajectories, plume, source markers, station observations.
- UI hiển thị rõ freshness và model version.
- UI có state rõ khi plume forward chưa available.
- UI có error/empty/stale state, không fail trắng màn hình.
- Tất cả request từ UI đi qua configured API base/tile base, không fetch trực tiếp Iceberg/HDFS/raw file.
Luồng target:
Existing TODO1/TODO2/TODO3 outputs
-> visualization Spark jobs on K8s
-> ais.visualization.* gold tables
-> optional tile/cache export
-> visualization API / PM2.5 API compatibility endpoint
-> UI map/dashboard
Chi tiết:
ais.predictions.hanoi_pm25_forecast_gold
ais.models.hanoi_pm25_model_registry_gold
ais.air_quality.openaq_hanoi_station_hourly_silver
ais.satellite.sentinel5p_grid_silver
ais.trajectory.hysplit_trajectories_silver
ais.trajectory.hysplit_trajectories_clustered_silver
ais.features.trajectory_path_satellite_silver
ais.features.trajectory_hourly_features_silver
|
v
Spark-on-K8s visualization jobs
|
v
ais.visualization.pm25_heatmap_grid_gold
ais.visualization.backward_trajectory_paths_gold
ais.visualization.forward_plume_probability_gold
ais.visualization.pm25_forecast_dashboard_gold
ais.visualization.pm25_timeseries_gold
ais.visualization.source_attribution_gold
ais.visualization.station_observations_gold
ais.visualization.visualization_cache_manifest_gold
|
v
Exported JSON / GeoJSON / tile cache
|
v
serving/visualization_api
|
v
ui Windy-like dashboard
Request-time flow:
Browser
-> GET /api/v1/visualization/...
-> Visualization API reads cache/manifest/materialized product rows
-> response
Cấm trong request path:
- SparkSession.
spark-submit.- HYSPLIT binary.
- ML model loading/prediction.
- Feature engineering.
- Training.
- Raw OpenAQ/S5P/ERA5 extraction.
- Full-table scan trên silver/raw tables.
Mục tiêu functional:
- Có PM2.5 heat layer cho Bắc Bộ, với horizon
0/latest,+6h,+12h,+24h. - Có backward trajectory layer từ Hà Nội, trace source regions theo cluster.
- Có forward plume/probability layer nếu upstream HYSPLIT forward output tồn tại.
- Có forecast dashboard cho PM2.5 +6h/+12h/+24h.
- Có source attribution panel giải thích likely source cluster/region.
- Có production API endpoints cho map/dashboard data.
- UI dùng production API/tile endpoints, không dùng mock trong production.
- Airflow refresh được visualization data.
- K8s deploy được visualization API/UI/check jobs.
- Có final smoke test và acceptance criteria rõ.
Mục tiêu non-functional:
- Request-time API p95 latency phải thấp vì chỉ đọc cache/product table nhỏ.
- Các visualization jobs rerun idempotent theo
base_time + horizon_h + product_version. - Data freshness hiển thị trong UI và kiểm tra được bằng check job.
- Layer response có schema version để frontend không phụ thuộc vào shape không ổn định.
- Forward plume là optional layer: thiếu forward HYSPLIT không làm fail toàn dashboard.
| Component | Runtime | Input | Output | Notes |
|---|---|---|---|---|
| Visualization Spark jobs | Spark-on-K8s | TODO1/2/3 tables | ais.visualization.* |
Heavy transform, geospatial binning, GeoJSON aggregation. |
| Cache/tile export job | Spark-on-K8s hoặc Kubernetes Job | ais.visualization.* |
JSON/GeoJSON/tile files + manifest | API đọc cache để tránh Spark/table scan request-time. |
| Visualization API | K8s Deployment | cache/manifest hoặc lightweight table reader | JSON/GeoJSON/tile endpoints | Không PySpark trong request process. |
| UI dashboard | K8s Deployment hoặc static hosting | Visualization API | Browser app | Production API only; mock behind dev flag. |
| Airflow DAG | Existing Airflow control plane | Upstream table freshness | K8s/Spark jobs + checks | Orchestrates refresh and backfill. |
| K8s checks | Kubernetes Job/Airflow task | API/cache/table metadata | exit code + logs | Smoke and freshness checks. |
| Monitoring optional | Docker Compose monitoring UI | check/API status | cards/status | Not part of core serving path. |
Kiến trúc serving:
- Visualization API không trực tiếp đọc raw/silver random tables.
- Các endpoint map lớn đọc cache/tile đã export.
- Các endpoint nhỏ như
/forecast/latestcó thể đọc dashboard cache hoặcpm25_forecast_dashboard_goldqua lightweight adapter, nhưng không dùng Spark. - Cache manifest là contract giữa compute layer và API layer.
- Materialize first, serve second.
- UI chỉ gọi API/tile endpoints.
- API chỉ đọc bảng/cache đã materialize.
- Không chạy Spark/HYSPLIT/ML/feature engineering/training trong request handler.
- Không expose raw/silver schema trực tiếp ra frontend.
- Visualization tables nằm trong
ais.visualizationvà có schema ổn định. - Mỗi product table có:
product_versionschema_versiongenerated_atbase_timevalid_timenếu có horizon.source_method- freshness metadata.
- Rerun phải idempotent.
- Forward plume optional:
- nếu có
direction='forward'rows thì build probability grid. - nếu không có, manifest ghi
available=falsevà API trả response rỗng có lý do.
- nếu có
- Heatmap Bắc Bộ phải ghi rõ
source_methodvàuncertainty; không giả vờ có model PM2.5 cấp tỉnh nếu upstream chỉ có Hanoi model. - Mock data trong UI:
- chỉ dùng khi
VITE_USE_MOCK_DATA=true. - production build mặc định
false. - nếu production mode gọi
/mock/*thì fail lint/build/check.
- chỉ dùng khi
- Time semantics:
base_time: thời điểm model/visualization run dùng làm gốc.valid_time: thời điểm layer có hiệu lực.horizon_h:0,6,12,24.
- Bounds:
- Northern Vietnam bbox config nằm trong
config/hanoi_pipeline.yaml. - UI không hardcode bbox ngoài config/API manifest.
- Northern Vietnam bbox config nằm trong
Files cần sửa:
config/hanoi_pipeline.yaml- modify- Thêm
visualization.region_bbox. - Thêm grid resolution.
- Thêm horizon list.
- Thêm source cluster labels.
- Thêm cache/export config.
- Thêm
spark_jobs/hanoi_config.py- modify- Thêm
get_visualization_config(). - Thêm
get_visualization_region_bbox(). - Thêm table names
ais.visualization.*. - Thêm cache path helper.
- Thêm
spark_jobs/ensure_iceberg_tables.py- modify- Thêm namespace
ais.visualization. - Tạo visualization gold tables.
- Thêm namespace
deploy/k8s/configmap.yaml- modify- Thêm env vars cho visualization jobs/API/UI.
.env.example- modify optional- Thêm env mẫu không chứa secret.
Table names cần thêm vào spark_jobs/hanoi_config.py:
TABLES.update({
"visualization_heatmap_grid_gold": f"{ICEBERG_CATALOG}.visualization.pm25_heatmap_grid_gold",
"visualization_backward_trajectory_paths_gold": f"{ICEBERG_CATALOG}.visualization.backward_trajectory_paths_gold",
"visualization_forward_plume_probability_gold": f"{ICEBERG_CATALOG}.visualization.forward_plume_probability_gold",
"visualization_forecast_dashboard_gold": f"{ICEBERG_CATALOG}.visualization.pm25_forecast_dashboard_gold",
"visualization_pm25_timeseries_gold": f"{ICEBERG_CATALOG}.visualization.pm25_timeseries_gold",
"visualization_source_attribution_gold": f"{ICEBERG_CATALOG}.visualization.source_attribution_gold",
"visualization_station_observations_gold": f"{ICEBERG_CATALOG}.visualization.station_observations_gold",
"visualization_cache_manifest_gold": f"{ICEBERG_CATALOG}.visualization.visualization_cache_manifest_gold",
})Files cần tạo:
spark_jobs/visualization_pm25_heatmap_grid_gold.py- createspark_jobs/visualization_backward_trajectory_paths_gold.py- createspark_jobs/visualization_forward_plume_probability_gold.py- createspark_jobs/visualization_forecast_dashboard_gold.py- createspark_jobs/visualization_pm25_timeseries_gold.py- createspark_jobs/visualization_source_attribution_gold.py- createspark_jobs/visualization_station_observations_gold.py- createspark_jobs/export_visualization_cache.py- createspark_jobs/visualization_quality_checks.py- create optional nếu check cần Spark/table scan.
Files cần sửa:
scripts/submit_spark_k8s.sh- modify- Thêm job types:
visualization-heatmap-gridvisualization-backward-trajectoriesvisualization-forward-plumevisualization-forecast-dashboardvisualization-pm25-timeseriesvisualization-source-attributionvisualization-station-observationsvisualization-export-cachevisualization-quality-checks
- Thêm job types:
scripts/submit_spark.sh- modify optional- Chỉ làm dev fallback nếu cần; target runtime vẫn là K8s.
Files cần tạo:
serving/visualization_api/main.py- createserving/visualization_api/requirements.txt- createserving/visualization_api/Dockerfile- createserving/visualization_api/README.md- create
Files cần sửa optional:
serving/pm25_api/main.py- modify optional- Refactor để không dùng PySpark trong request/readiness.
- Hoặc giữ như compatibility service, nhưng UI TODO4 không phụ thuộc vào implementation này.
Files cần sửa:
ui/package.json- modify- Thêm map dependencies, ví dụ
maplibre-glhoặcleaflet. - Giữ
d3nếu dùng chart/scale.
- Thêm map dependencies, ví dụ
ui/src/App.jsx- modify- Dashboard map là default page.
ui/src/services/api.js- modify- Không fetch
/mock/*mặc định. - Tách production API client và dev mock client.
- Không fetch
ui/src/index.css- modify- Layout full-screen map/dashboard.
Files cần tạo:
ui/src/pages/AirQualityMapDashboard.jsx- createui/src/services/visualizationApi.js- createui/src/components/map/MapCanvas.jsx- createui/src/components/map/LayerControl.jsx- createui/src/components/map/TimeSelector.jsx- createui/src/components/map/ForecastPanel.jsx- createui/src/components/map/SourceAttributionPanel.jsx- createui/src/components/map/FreshnessBadge.jsx- createui/src/components/map/MapPopup.jsx- createui/src/components/charts/PM25ForecastChart.jsx- createui/Dockerfile- createui/nginx.conf- create optional nếu serve static bằng Nginx.
Mock data handling:
- Giữ
ui/public/mockchỉ cho dev/demo. - Thêm env
VITE_USE_MOCK_DATA=false. - Production check fail nếu
VITE_USE_MOCK_DATA=true.
Files cần tạo:
airflow/dags/ais_visualization_product_dag.py- create
Files cần sửa:
airflow/dags/ais_dag_utils.py- modify- Thêm helper submit visualization Spark jobs trên K8s.
- Thêm helper check API/cache nếu cần.
airflow/dags/ais_pm25_k8s_compute_dag.py- modify optional- Có thể trigger visualization DAG sau khi prediction freshness pass.
airflow/dags/ais_trajectory_tier2_dag.py- modify optional- Có thể expose dependency marker hoặc K8s path cho upstream trajectory.
Files cần tạo:
deploy/k8s/visualization-api/visualization-api-deployment.yaml- createdeploy/k8s/visualization-api/visualization-api-service.yaml- createdeploy/k8s/visualization-api/README.md- createdeploy/k8s/ui/ais-ui-deployment.yaml- createdeploy/k8s/ui/ais-ui-service.yaml- createdeploy/k8s/ui/README.md- createdeploy/k8s/checks/visualization-serving-check-job.yaml- create
Files cần sửa:
deploy/k8s/kustomization.yaml- modify- Include visualization API/UI/check manifests.
deploy/k8s/rbac.yaml- modify optional- Nếu check job cần read pods/jobs.
Files cần sửa optional:
monitoring/app.py- modify optional- Thêm cards:
- latest visualization run.
- heatmap freshness.
- plume availability.
- API readiness.
- UI readiness.
- Thêm cards:
SYSTEM_OVERVIEW.md- modify optional after implementation- Thêm TODO4 product layer.
README.md- modify optional after implementation- Thêm cách chạy dashboard.
Input:
ais.predictions.hanoi_pm25_forecast_goldais.air_quality.openaq_hanoi_station_hourly_silverais.satellite.sentinel5p_grid_silverais.features.trajectory_hourly_features_silver- Optional:
ais.visualization.forward_plume_probability_gold
Output:
ais.visualization.pm25_heatmap_grid_gold- Cache:
VIS_CACHE_BASE_URI/pm25_heatmap/latest/horizon=0/grid.geojsonVIS_CACHE_BASE_URI/pm25_heatmap/latest/horizon=6/grid.geojsonVIS_CACHE_BASE_URI/pm25_heatmap/latest/horizon=12/grid.geojsonVIS_CACHE_BASE_URI/pm25_heatmap/latest/horizon=24/grid.geojson
Notes:
horizon_h=0là latest observed/now layer.horizon_h=6/12/24dùng forecast table làm anchor và các proxy spatial để tạo visualization grid.- Vì upstream model hiện là Hanoi PM2.5 forecast, heatmap Bắc Bộ phải có
source_methodvàuncertaintyrõ.
Input:
ais.trajectory.hysplit_trajectories_clustered_silverais.features.trajectory_path_satellite_silverais.features.trajectory_hourly_features_silver
Output:
ais.visualization.backward_trajectory_paths_gold- Cache:
VIS_CACHE_BASE_URI/trajectories/backward/latest.geojson
Notes:
- Một
traj_idthành một LineString GeoJSON. - Path points order theo
age_h. - Popup properties giữ source/evidence fields.
Input:
ais.trajectory.hysplit_trajectories_silvervớidirection='forward'
Output:
ais.visualization.forward_plume_probability_gold- Cache:
VIS_CACHE_BASE_URI/plume/forward/latest/horizon=6/grid.geojsonVIS_CACHE_BASE_URI/plume/forward/latest/horizon=12/grid.geojsonVIS_CACHE_BASE_URI/plume/forward/latest/horizon=24/grid.geojson
Notes:
- Nếu không có forward rows, job vẫn ghi cache manifest
available=false. - Dashboard không fail toàn page khi plume thiếu.
Input:
ais.predictions.hanoi_pm25_forecast_goldais.models.hanoi_pm25_model_registry_goldais.air_quality.openaq_hanoi_station_hourly_silver
Output:
ais.visualization.pm25_forecast_dashboard_goldais.visualization.pm25_timeseries_gold- Cache:
VIS_CACHE_BASE_URI/dashboard/latest.jsonVIS_CACHE_BASE_URI/timeseries/hanoi/latest.json
Input:
ais.features.trajectory_hourly_features_silverais.trajectory.hysplit_trajectories_clustered_silverais.features.trajectory_path_satellite_silverais.predictions.hanoi_pm25_forecast_gold
Output:
ais.visualization.source_attribution_gold- Cache:
VIS_CACHE_BASE_URI/source_attribution/latest.geojson
Input:
ais.air_quality.openaq_hanoi_station_hourly_silver
Output:
ais.visualization.station_observations_gold- Cache:
VIS_CACHE_BASE_URI/stations/latest.geojson
Input:
- All visualization product tables.
- Exported cache files.
Output:
ais.visualization.visualization_cache_manifest_gold
Purpose:
- API biết file nào là latest.
- API trả readiness dựa trên manifest/freshness.
- Check job detect missing/stale/bad checksum.
Thêm namespace:
CREATE NAMESPACE IF NOT EXISTS ais.visualization;Purpose:
- Canonical PM2.5 heatmap grid cho map layer.
Schema đề xuất:
visualization_run_id STRING,
product_version STRING,
schema_version STRING,
base_time TIMESTAMP,
valid_time TIMESTAMP,
horizon_h INT,
cell_id STRING,
lat DOUBLE,
lon DOUBLE,
lat_min DOUBLE,
lat_max DOUBLE,
lon_min DOUBLE,
lon_max DOUBLE,
pm25_value DOUBLE,
risk STRING,
uncertainty DOUBLE,
source_method STRING,
observation_count INT,
satellite_product_count INT,
prediction_id STRING,
model_version STRING,
feature_version STRING,
source_cluster_id INT,
source_label STRING,
generated_at TIMESTAMP,
data_freshness_minutes INT,
year INT,
month INT,
day INTPartition:
PARTITIONED BY (horizon_h, year, month, day)Idempotency key:
base_time + horizon_h + cell_id + product_version
Validation:
horizon_hthuộc0|6|12|24.riskthuộclow|medium|high|very_high|unknown.pm25_valuekhông âm.source_methodkhông null.uncertaintynằm trong[0, 1]nếu dùng normalized uncertainty.
Purpose:
- Backward trajectory layer sẵn sàng trả GeoJSON.
Schema đề xuất:
visualization_run_id STRING,
product_version STRING,
schema_version STRING,
base_time TIMESTAMP,
init_time TIMESTAMP,
direction STRING,
traj_id STRING,
traj_no INT,
cluster_id INT,
source_label STRING,
source_lat DOUBLE,
source_lon DOUBLE,
source_alt_m DOUBLE,
start_lat DOUBLE,
start_lon DOUBLE,
end_lat DOUBLE,
end_lon DOUBLE,
age_start_h INT,
age_end_h INT,
point_count INT,
path_no2_mean DOUBLE,
path_aer_mean DOUBLE,
path_no2_aer_ratio DOUBLE,
geometry_geojson STRING,
properties_json STRING,
style_color STRING,
generated_at TIMESTAMP,
year INT,
month INT,
day INTPartition:
PARTITIONED BY (direction, year, month, day)Idempotency key:
base_time + direction + traj_id + product_version
Validation:
direction='backward'.geometry_geojsonlà GeoJSON LineString hợp lệ.point_count >= 2.age_start_h <= age_end_hsau khi normalize ordering.
Purpose:
- Probability grid từ forward HYSPLIT ensemble.
Schema đề xuất:
visualization_run_id STRING,
product_version STRING,
schema_version STRING,
base_time TIMESTAMP,
valid_time TIMESTAMP,
horizon_h INT,
cell_id STRING,
lat DOUBLE,
lon DOUBLE,
lat_min DOUBLE,
lat_max DOUBLE,
lon_min DOUBLE,
lon_max DOUBLE,
particle_count BIGINT,
total_particle_count BIGINT,
probability DOUBLE,
available BOOLEAN,
unavailable_reason STRING,
source_run_count INT,
source_method STRING,
geometry_geojson STRING,
generated_at TIMESTAMP,
year INT,
month INT,
day INTPartition:
PARTITIONED BY (horizon_h, year, month, day)Idempotency key:
base_time + horizon_h + cell_id + product_version
Validation:
horizon_hthuộc6|12|24.- Nếu
available=true, tổngprobabilitytheobase_time+horizon_hxấp xỉ1.0. - Nếu không có forward HYSPLIT, không fail dashboard; manifest ghi
available=false.
Purpose:
- One-row dashboard summary cho latest Hanoi forecast.
Schema đề xuất:
dashboard_id STRING,
visualization_run_id STRING,
product_version STRING,
schema_version STRING,
base_hour TIMESTAMP,
location_id STRING,
location_name STRING,
latest_observed_time TIMESTAMP,
pm25_latest_observed DOUBLE,
pm25_now DOUBLE,
pm25_6h DOUBLE,
risk_6h STRING,
pm25_12h DOUBLE,
risk_12h STRING,
pm25_24h DOUBLE,
risk_24h STRING,
dominant_cluster INT,
source_lat DOUBLE,
source_lon DOUBLE,
source_label STRING,
path_no2_mean DOUBLE,
path_aer_mean DOUBLE,
pm25_grad_mag DOUBLE,
model_version STRING,
model_version_6h STRING,
model_version_12h STRING,
model_version_24h STRING,
model_status STRING,
feature_version STRING,
feature_schema_hash STRING,
prediction_id STRING,
prediction_created_at TIMESTAMP,
generated_at TIMESTAMP,
prediction_freshness_minutes INT,
observation_freshness_minutes INT,
year INT,
month INT,
day INTPartition:
PARTITIONED BY (location_id, year, month, day)Idempotency key:
base_hour + location_id + product_version
Purpose:
- Chart observed history + predicted future.
Schema đề xuất:
series_id STRING,
visualization_run_id STRING,
product_version STRING,
schema_version STRING,
location_id STRING,
location_name STRING,
base_time TIMESTAMP,
timestamp TIMESTAMP,
series_type STRING,
horizon_h INT,
pm25_value DOUBLE,
risk STRING,
source_table STRING,
source_id STRING,
model_version STRING,
generated_at TIMESTAMP,
year INT,
month INT,
day INTPartition:
PARTITIONED BY (location_id, series_type, year, month, day)Validation:
series_typethuộcobserved|forecast.- Forecast rows có
horizon_hthuộc6|12|24. - Observed rows có
horizon_h=0.
Purpose:
- Source attribution markers/panel cho UI.
Schema đề xuất:
attribution_id STRING,
visualization_run_id STRING,
product_version STRING,
schema_version STRING,
base_time TIMESTAMP,
valid_time TIMESTAMP,
location_id STRING,
cluster_id INT,
source_label STRING,
source_region_type STRING,
source_lat DOUBLE,
source_lon DOUBLE,
contribution_score DOUBLE,
confidence DOUBLE,
traj_count INT,
age_window_start_h INT,
age_window_end_h INT,
evidence_no2_mean DOUBLE,
evidence_aer_mean DOUBLE,
evidence_no2_aer_ratio DOUBLE,
evidence_pm25_grad_mag DOUBLE,
explanation_vi STRING,
geometry_geojson STRING,
generated_at TIMESTAMP,
year INT,
month INT,
day INTPartition:
PARTITIONED BY (location_id, year, month, day)Validation:
contribution_scoretrong[0, 1].confidencetrong[0, 1].- Source marker không được thiếu
source_lat/source_lonnếucluster_idtồn tại.
Purpose:
- Station observation point layer.
Schema đề xuất:
observation_id STRING,
visualization_run_id STRING,
product_version STRING,
schema_version STRING,
observation_time TIMESTAMP,
station_id STRING,
station_name STRING,
location_id STRING,
city STRING,
lat DOUBLE,
lon DOUBLE,
pm25 DOUBLE,
risk STRING,
coverage_pct DOUBLE,
unit STRING,
provider STRING,
source STRING,
geometry_geojson STRING,
generated_at TIMESTAMP,
year INT,
month INT,
day INTPartition:
PARTITIONED BY (year, month, day)Purpose:
- Contract giữa export job và API.
Schema đề xuất:
manifest_id STRING,
visualization_run_id STRING,
product_version STRING,
schema_version STRING,
layer_name STRING,
base_time TIMESTAMP,
valid_time TIMESTAMP,
horizon_h INT,
location_id STRING,
format STRING,
content_type STRING,
cache_uri STRING,
tile_template STRING,
bbox_west DOUBLE,
bbox_south DOUBLE,
bbox_east DOUBLE,
bbox_north DOUBLE,
row_count BIGINT,
byte_size BIGINT,
checksum STRING,
available BOOLEAN,
unavailable_reason STRING,
generated_at TIMESTAMP,
expires_at TIMESTAMP,
year INT,
month INT,
day INTPartition:
PARTITIONED BY (layer_name, year, month, day)Validation:
- Mỗi required layer có latest manifest.
cache_uritồn tại nếuavailable=true.checksummatch file content nếu cache backend hỗ trợ read.
Mục đích:
- Tạo grid PM2.5 Bắc Bộ cho
horizon_h=0/6/12/24.
CLI args:
--base-time YYYY-MM-DDTHH:00:00Z
--start-date YYYY-MM-DD
--end-date YYYY-MM-DD
--horizons 0,6,12,24
--grid-resolution-deg 0.1
--product-version windy_v1
--full-refresh 0|1
--dry-run 0|1
Transform logic:
- Load bbox Northern Vietnam từ config.
- Generate grid cells theo resolution config.
- For
horizon_h=0:- lấy latest OpenAQ station observations quanh
base_time. - dùng IDW hoặc nearest-station interpolation trong phạm vi dữ liệu có coverage.
- blend satellite proxy nếu configured, nhưng output phải ghi
source_method.
- lấy latest OpenAQ station observations quanh
- For
horizon_h=6/12/24:- đọc
ais.predictions.hanoi_pm25_forecast_goldlatest production row. - map forecast value vào Hanoi anchor.
- dùng station/satellite/plume/trajectory proxy để tạo heat surface nếu có.
- không claim province-level model; set
uncertaintycao hơn ở xa Hanoi/source data.
- đọc
- Risk bands theo PM2.5 threshold dùng cùng API/TODO3.
- Ghi
ais.visualization.pm25_heatmap_grid_gold.
Expected logs:
job=visualization_pm25_heatmap_grid
base_time
horizons
grid_cell_count
input_prediction_count
input_station_count
input_satellite_count
output_count
min_pm25
max_pm25
source_method_counts
dry_run
status
Acceptance criteria:
- Rerun cùng
base_time+horizon_h+cell_id+product_versionkhông duplicate. - Output có đủ 4 horizons khi upstream forecast có 6/12/24.
source_methodvàuncertaintykhông null.- Không đọc raw paths.
Mục đích:
- Convert point-level clustered backward trajectories thành LineString GeoJSON product.
Transform logic:
- Đọc
hysplit_trajectories_clustered_silvervớidirection='backward'. - Filter date/base_time window.
- Group by
traj_id. - Sort points by
timestamphoặcage_h. - Build GeoJSON LineString
[lon, lat, alt_m]. - Join
trajectory_path_satellite_silvertheotraj_id. - Join cluster labels từ config.
- Ghi
backward_trajectory_paths_gold.
Expected logs:
input_point_count
trajectory_count
cluster_count
missing_cluster_label_count
invalid_geometry_count
output_count
status
Acceptance criteria:
- Mỗi trajectory có
point_count >= 2. - GeoJSON hợp lệ.
- Popup fields có
traj_id,cluster_id,source_lat/lon, NO2/AER evidence.
Mục đích:
- Tạo probability grid từ forward HYSPLIT output nếu tồn tại.
Transform logic:
- Đọc
hysplit_trajectories_silvervớidirection='forward'. - For
horizon_h in 6,12,24:- filter
age_h = horizon_h. - bin lat/lon vào grid resolution.
probability = particle_count / total_particle_count.- build cell GeoJSON polygon.
- filter
- Nếu không có forward rows:
- không throw fatal nếu
FORWARD_PLUME_REQUIRED=false. - ghi manifest
available=false,unavailable_reason='forward_hysplit_missing'.
- không throw fatal nếu
- Ghi
forward_plume_probability_gold.
Acceptance criteria:
- Nếu forward rows tồn tại, mỗi horizon có probability grid.
- Tổng probability theo horizon xấp xỉ 1.
- Nếu thiếu forward rows, API/UI vẫn hoạt động và layer hiển thị unavailable.
Mục đích:
- Build dashboard summary one-row/latest cho forecast cards.
Transform logic:
- Đọc latest production row từ
hanoi_pm25_forecast_gold. - Đọc latest station observed PM2.5.
- Join model metadata fields.
- Compute freshness:
- prediction age.
- observation age.
- visualization age.
- Add source label từ cluster config.
- Ghi
pm25_forecast_dashboard_gold.
Acceptance criteria:
- Dashboard row có PM2.5 +6/+12/+24 và model version.
- Missing forecast trả failure rõ trong job/check, không tạo row giả production.
Mục đích:
- Build series cho chart observed history + predicted future.
Transform logic:
- Observed:
- lấy OpenAQ station hourly, aggregate median/mean theo hour.
- window mặc định 48h hoặc config.
- Forecast:
- explode latest prediction row thành 3 rows
base_time+6h,+12h,+24h.
- explode latest prediction row thành 3 rows
- Union observed + forecast theo schema.
- Ghi
pm25_timeseries_gold.
Acceptance criteria:
- Chart API có ít nhất observed history khi data tồn tại.
- Forecast points có horizon và model version.
Mục đích:
- Tạo source markers và text explanation cho attribution panel.
Transform logic:
- Đọc
trajectory_hourly_features_silverquanh latestbase_time. - Map
dominant_clustersang source label theo config. - Compute
contribution_scoretừ:- trajectory count.
- path NO2/AER evidence.
- PM2.5 gradient magnitude.
- recency.
- Compute
confidencetừ:- trajectory coverage.
- satellite evidence availability.
- station freshness.
- Build
explanation_vingắn, factual. - Ghi
source_attribution_gold.
Acceptance criteria:
- Panel giải thích được cluster/region và evidence.
- Không đưa khẳng định quá chắc nếu confidence thấp.
Mục đích:
- Tạo point layer station observations.
Transform logic:
- Đọc OpenAQ Hanoi station hourly silver.
- Filter latest/window.
- Normalize station ids.
- Compute risk.
- Build GeoJSON Point.
- Ghi station observations gold.
Acceptance criteria:
- Mỗi station point có PM2.5, observation time, coverage.
- Missing coordinates bị drop và log count.
Mục đích:
- Export API-ready JSON/GeoJSON/tile cache từ visualization gold tables.
Transform logic:
- Read latest rows per product/layer/horizon.
- Write compact files:
- heatmap grid GeoJSON or JSON grid.
- trajectories GeoJSON.
- plume GeoJSON.
- dashboard JSON.
- timeseries JSON.
- source attribution GeoJSON.
- stations GeoJSON.
- Compute checksum/row_count/byte_size.
- Write/update
visualization_cache_manifest_gold.
Acceptance criteria:
- API có thể serve dashboard chỉ từ cache manifest + cache files.
- Missing required cache file làm check fail.
- Cache export rerun idempotent.
API mặc định:
serving/visualization_api.
Required endpoints:
GET /healthzGET /readyzGET /api/v1/visualization/manifest/latestGET /api/v1/visualization/pm25/heatmap/latest?horizon_h=0|6|12|24GET /api/v1/visualization/pm25/heatmap/tiles/{z}/{x}/{y}?horizon_h=0|6|12|24optional nếu export tile.GET /api/v1/visualization/trajectories/backward/latestGET /api/v1/visualization/plume/forward/latest?horizon_h=6|12|24GET /api/v1/visualization/forecast/latest?location_id=hanoiGET /api/v1/visualization/timeseries/latest?location_id=hanoiGET /api/v1/visualization/source-attribution/latest?location_id=hanoiGET /api/v1/visualization/stations/latest
API rules:
- Không import
pyspark. - Không tạo
SparkSession. - Không gọi
spark-submit. - Không gọi HYSPLIT.
- Không load ML model.
- Không query raw/silver tables trong request path.
/healthzchỉ check process alive./readyzcheck:- required env.
- cache manifest readable.
- required layers available/fresh.
- Logs:
- path.
- status_code.
- latency_ms.
- layer_name.
- horizon_h.
- cache_hit.
- error_code.
Response shape forecast:
{
"location_id": "hanoi",
"base_hour": "2026-05-27T12:00:00Z",
"generated_at": "2026-05-27T12:10:00Z",
"freshness": {
"prediction_freshness_minutes": 10,
"observation_freshness_minutes": 20
},
"forecast": {
"now": {"pm25": 42.1, "risk": "medium"},
"6h": {"pm25": 55.2, "risk": "medium"},
"12h": {"pm25": 78.0, "risk": "high"},
"24h": {"pm25": 63.5, "risk": "medium"}
},
"model": {
"model_version": "example",
"model_version_6h": "example",
"model_version_12h": "example",
"model_version_24h": "example",
"feature_version": "hanoi_pm25_core_v1"
}
}Response shape plume unavailable:
{
"available": false,
"layer_name": "forward_plume_probability",
"horizon_h": 6,
"reason": "forward_hysplit_missing",
"generated_at": "2026-05-27T12:10:00Z"
}Acceptance criteria:
- API returns 200 for health.
- API readiness fails with clear JSON if cache/manifest missing.
- API layer endpoints return production cache data when available.
- API returns stable empty/unavailable response for optional plume.
- No request handler uses Spark/HYSPLIT/ML.
Existing UI:
- Vite + React.
ui/src/services/api.jscurrently fetches/mock/*.json.- No map library currently in dependencies.
Target UI:
- Main page:
AirQualityMapDashboard. - Map library:
- Recommended:
maplibre-glfor vector map style and high-performance layers. - Alternative:
leafletif implementation should be simpler.
- Recommended:
- D3 can remain for forecast chart and color scales.
Required frontend env:
VITE_VIS_API_BASE=/api/v1/visualization
VITE_USE_MOCK_DATA=false
VITE_DEFAULT_HORIZON_H=0
VITE_DEFAULT_MAP_CENTER_LAT=21.0285
VITE_DEFAULT_MAP_CENTER_LON=105.8542
VITE_DEFAULT_MAP_ZOOM=7
Implementation tasks:
- Replace default page with map dashboard.
- Add API client:
getManifestLatest()getHeatmapLatest(horizon_h)getBackwardTrajectoriesLatest()getForwardPlumeLatest(horizon_h)getForecastLatest(location_id)getPM25TimeseriesLatest(location_id)getSourceAttributionLatest(location_id)getStationsLatest()
- Add state:
- selected horizon.
- enabled layers.
- selected feature popup.
- API readiness/error state.
- data freshness.
- Add map layers:
- heatmap fill/canvas layer.
- trajectory line layer.
- plume probability fill layer.
- source marker layer.
- station marker layer.
- Add panels:
- Forecast cards.
- Source attribution panel.
- PM2.5 chart.
- Layer control.
- Freshness badge.
- Mock isolation:
VITE_USE_MOCK_DATA=truecan useui/public/mock.- production mode must call API.
- add check script or lint rule to detect
/mock/in production API path.
UI acceptance criteria:
npm run buildsucceeds.- Production build does not fetch
/mock/*. - User can toggle each layer independently.
- User can switch horizon without full page reload.
- Popups show requested fields.
- Stale data is visible to user.
- Optional plume unavailable state is visible but not fatal.
File cần tạo:
airflow/dags/ais_visualization_product_dag.py
DAG role:
- Airflow là control plane.
- Kubernetes/Spark-on-K8s là runtime compute.
- DAG refreshes visualization products after upstream prediction/trajectory outputs exist.
Suggested DAG flow:
ensure_iceberg_tables
-> check_upstream_predictions
-> check_upstream_trajectory_optional
-> visualization_forecast_dashboard
-> visualization_pm25_timeseries
-> visualization_station_observations
-> visualization_backward_trajectories
-> visualization_source_attribution
-> visualization_forward_plume_optional
-> visualization_pm25_heatmap_grid
-> export_visualization_cache
-> visualization_quality_checks
-> visualization_api_ready_check
Scheduling:
- Manual trigger first.
- Later schedule every 1h or after prediction CronJob.
catchup=Falsefor near-real-time dashboard.- Backfill support by
dag_run.conf:
{
"base_time": "2026-05-27T12:00:00Z",
"start_date": "2026-05-27",
"end_date": "2026-05-27",
"horizons": "0,6,12,24",
"product_version": "windy_v1",
"dry_run": 0
}Acceptance criteria:
- DAG can run visualization refresh end-to-end.
- DAG fails early if required prediction table/cache is missing.
- DAG does not fail if forward plume is missing and
FORWARD_PLUME_REQUIRED=false. - DAG has manual backfill knobs.
- Logs include cache URIs and manifest ids.
Files:
deploy/k8s/visualization-api/visualization-api-deployment.yamldeploy/k8s/visualization-api/visualization-api-service.yaml
Deployment requirements:
- Image:
ais-visualization-api:local. - Env from
ais-runtime-config. - Liveness:
/healthz. - Readiness:
/readyz. - Resource starting point:
- requests:
100mCPU,256Mimemory. - limits:
500mCPU,512Mimemory.
- requests:
- No Spark jars/packages needed in API image.
Files:
deploy/k8s/ui/ais-ui-deployment.yamldeploy/k8s/ui/ais-ui-service.yaml
Deployment requirements:
- Build static assets.
- Serve through Nginx or simple static server.
- Configure API base URL via build args or runtime env injection.
- Service can be ClusterIP first; Ingress optional.
File:
deploy/k8s/checks/visualization-serving-check-job.yaml
Checks:
| Check | Expected | Failure |
|---|---|---|
| API health | /healthz returns 200 |
non-200 |
| API readiness | /readyz returns 200 when cache fresh |
non-200 or stale reason |
| Manifest latest | required layers found | missing manifest |
| Heatmap horizons | 0/6/12/24 available | missing required horizon |
| Forecast dashboard | latest row/cache available | missing forecast |
| Backward trajectories | latest GeoJSON available if upstream has rows | missing required layer |
| Forward plume optional | available or explicit unavailable reason | silent missing |
| UI service | returns HTML/index | non-200 |
| Production mock check | no /mock/ fetch in production bundle |
mock usage found |
Acceptance criteria:
kubectl -n ais applydeploys API/UI.kubectl -n ais waitpasses for deployments.- Check job exits non-zero on stale/missing required data.
Existing env vars reused:
ICEBERG_CATALOG
ICEBERG_WAREHOUSE
HDFS_NAMENODE
HDFS_WEBHDFS_BASE
PREDICTION_TABLE
MODEL_REGISTRY_TABLE
SERVING_FEATURE_TABLE
LOCATION_ID
LOCATION_NAME
FEATURE_VERSION
SPARK_K8S_MASTER
SPARK_K8S_NAMESPACE
SPARK_IMAGE
New visualization job env vars:
VIS_PRODUCT_VERSION=windy_v1
VIS_SCHEMA_VERSION=1
VIS_REGION_BBOX_WEST=100.0
VIS_REGION_BBOX_EAST=108.8
VIS_REGION_BBOX_SOUTH=18.0
VIS_REGION_BBOX_NORTH=24.5
VIS_GRID_RESOLUTION_DEG=0.1
VIS_HORIZONS=0,6,12,24
VIS_OBS_HISTORY_HOURS=48
VIS_FORWARD_PLUME_REQUIRED=false
VIS_CACHE_BASE_URI=hdfs://host.docker.internal:9000/visualization_cache
VIS_CACHE_FORMAT=geojson
VIS_HEATMAP_TABLE=ais.visualization.pm25_heatmap_grid_gold
VIS_TRAJECTORY_TABLE=ais.visualization.backward_trajectory_paths_gold
VIS_PLUME_TABLE=ais.visualization.forward_plume_probability_gold
VIS_DASHBOARD_TABLE=ais.visualization.pm25_forecast_dashboard_gold
VIS_TIMESERIES_TABLE=ais.visualization.pm25_timeseries_gold
VIS_SOURCE_ATTRIBUTION_TABLE=ais.visualization.source_attribution_gold
VIS_STATION_TABLE=ais.visualization.station_observations_gold
VIS_CACHE_MANIFEST_TABLE=ais.visualization.visualization_cache_manifest_gold
New visualization API env vars:
VIS_API_PORT=8080
VIS_CACHE_BASE_URI=hdfs://host.docker.internal:9000/visualization_cache
VIS_CACHE_MANIFEST_TABLE=ais.visualization.visualization_cache_manifest_gold
VIS_REQUIRED_LAYERS=pm25_heatmap,forecast_dashboard,pm25_timeseries,backward_trajectories,source_attribution,station_observations
VIS_OPTIONAL_LAYERS=forward_plume
VIS_FRESHNESS_MAX_MINUTES=180
VIS_READY_TIMEOUT_SECONDS=5
VIS_ENABLE_TABLE_FALLBACK=false
New UI env vars:
VITE_VIS_API_BASE=/api/v1/visualization
VITE_USE_MOCK_DATA=false
VITE_DEFAULT_HORIZON_H=0
VITE_DEFAULT_MAP_CENTER_LAT=21.0285
VITE_DEFAULT_MAP_CENTER_LON=105.8542
VITE_DEFAULT_MAP_ZOOM=7
Acceptance criteria:
.env.example/ConfigMap document env without real secret.- API fails readiness clearly if required env missing.
- Production UI build has
VITE_USE_MOCK_DATA=false.
Smoke tests bắt buộc:
- Bootstrap table/schema:
bash scripts/submit_spark_k8s.sh ensure-icebergPass khi:
ais.visualizationnamespace tồn tại.- Tất cả visualization tables tồn tại.
- Build forecast dashboard:
bash scripts/submit_spark_k8s.sh visualization-forecast-dashboard --dry-run 1
bash scripts/submit_spark_k8s.sh visualization-forecast-dashboard --dry-run 0Pass khi:
- Có row trong
ais.visualization.pm25_forecast_dashboard_gold. - Row có
pm25_6h,pm25_12h,pm25_24h,model_version.
- Build timeseries:
bash scripts/submit_spark_k8s.sh visualization-pm25-timeseries --dry-run 0Pass khi:
- Có observed rows.
- Có forecast rows nếu prediction tồn tại.
- Build backward trajectories:
bash scripts/submit_spark_k8s.sh visualization-backward-trajectories --dry-run 0Pass khi:
- GeoJSON LineString rows hợp lệ nếu upstream trajectory tồn tại.
- Build forward plume:
bash scripts/submit_spark_k8s.sh visualization-forward-plume --dry-run 0Pass khi:
- Nếu forward rows tồn tại: có probability grid 6/12/24.
- Nếu không tồn tại: manifest/layer state ghi
available=false, không fail toàn DAG khi optional.
- Build heatmap grid:
bash scripts/submit_spark_k8s.sh visualization-heatmap-grid --dry-run 0Pass khi:
- Có heatmap rows cho horizons 0/6/12/24.
source_method,uncertainty,generated_atkhông null.
- Export cache:
bash scripts/submit_spark_k8s.sh visualization-export-cachePass khi:
- Cache files tồn tại.
visualization_cache_manifest_goldcó latest manifest cho required layers.
- Build and run visualization API locally:
docker build -t ais-visualization-api:local -f serving/visualization_api/Dockerfile .
docker run --rm -p 8082:8080 --env-file .env ais-visualization-api:local
curl -i http://localhost:8082/healthz
curl -i http://localhost:8082/readyz
curl -i "http://localhost:8082/api/v1/visualization/forecast/latest?location_id=hanoi"
curl -i "http://localhost:8082/api/v1/visualization/pm25/heatmap/latest?horizon_h=6"Pass khi:
/healthz200./readyz200 nếu cache fresh.- Forecast/heatmap endpoints trả JSON/GeoJSON.
- Deploy API/UI on K8s:
kubectl -n ais apply -f deploy/k8s/visualization-api
kubectl -n ais apply -f deploy/k8s/ui
kubectl -n ais wait --for=condition=available --timeout=120s deployment/visualization-api
kubectl -n ais wait --for=condition=available --timeout=120s deployment/ais-uiPass khi:
- Pods running.
- Services route được.
- K8s API/UI smoke:
kubectl -n ais port-forward svc/visualization-api 8082:80
curl -i http://localhost:8082/healthz
curl -i http://localhost:8082/readyz
curl -i "http://localhost:8082/api/v1/visualization/manifest/latest"kubectl -n ais port-forward svc/ais-ui 3000:80
curl -i http://localhost:3000/Pass khi:
- API ready.
- UI returns index HTML.
- UI production build:
cd ui
npm ci
npm run buildPass khi:
- Build succeeds.
- Production bundle không chứa hardcoded
/mock/API path.
- Airflow DAG:
Trigger ais_visualization_product_dag with dry_run=0
Pass khi:
- DAG runs visualization jobs in order.
- Export cache task succeeds.
- API readiness check succeeds.
TODO4 chỉ hoàn thành khi:
-
TODO_4.mdimplementation plan đã được follow bằng implementation tương ứng. -
ais.visualizationnamespace tồn tại. - Tất cả visualization gold tables được tạo idempotent.
- Heatmap grid có data cho
latest/0h,+6h,+12h,+24h. - Backward trajectory layer hiển thị path từ Hà Nội và popup có
traj_id,cluster_id,source_lat/lon, NO2/AER evidence. - Forward plume layer hiển thị probability grid nếu forward HYSPLIT tồn tại, hoặc unavailable state rõ nếu không tồn tại.
- Forecast dashboard hiển thị PM2.5 now/latest observed, +6h, +12h, +24h,
generated_at,model_version, freshness. - Source attribution panel hiển thị likely source cluster/region, score, confidence, evidence.
- Production API endpoints phục vụ map/dashboard data.
- API request handlers không dùng Spark/HYSPLIT/ML/feature engineering/raw extraction.
- UI production không fetch
/mock/*. - UI switch được horizon và toggle layers độc lập.
- Airflow orchestration refresh được visualization tables/cache.
- K8s manifests deploy được visualization API và UI.
- Check job phát hiện stale/missing visualization data.
- Final smoke tests section 16 pass hoặc có gap được document rõ với nguyên nhân.
- TODO1/TODO2/TODO3 table contracts không bị phá.
- Không train model PM2.5 mới cho toàn bộ Bắc Bộ theo từng tỉnh/trạm.
- Không chạy HYSPLIT on-demand khi user click map.
- Không chạy ML inference on-demand trong API.
- Không migrate Kafka/HDFS/Iceberg/Cassandra/Airflow metadata DB vào Kubernetes.
- Không xây user auth, billing, public multi-tenant access.
- Không làm mobile app native.
- Không làm alerting/push notification.
- Không tối ưu tile server quy mô internet lớn; TODO4 chỉ cần cache/tile serving đủ cho product smoke và local/K8s deployment.
- Không thay đổi business logic TODO1/TODO2/TODO3 ngoài additions cần thiết cho visualization outputs.