Compare commits

..

431 Commits

Author SHA1 Message Date
2569718930@qq.com 219fda39d9 发布 v1.7.0:市场监控面板、天气日报、后台重写、新数据源、LGBM 移除
Constraint: CHANGELOG 覆盖 v1.6.0 以来 ~340 个提交
Confidence: high
Tested: git diff --stat 11 files, sync_version.py 通过
2026-05-23 20:31:34 +08:00
2569718930@qq.com 6cbc56ac5a 修复业务测试:AI 预测最高温需等待 AI 就绪,不再回退行数据 2026-05-23 12:45:19 +08:00
2569718930@qq.com f5fc3ca0cd 加速 AI 报文解读:精简 snapshot 负载,默认 max_tokens 1200→800 2026-05-23 12:40:46 +08:00
2569718930@qq.com 5817d4157d AI 未就绪时不再用集群中位数冒充 AI 预测最高温 2026-05-23 12:34:26 +08:00
2569718930@qq.com 348dfe132a 移除 Polymarket 价格层 UI:删除 MarketDecisionLine 组件及相关数据流 2026-05-23 12:24:49 +08:00
2569718930@qq.com eeb5f563db 移除 Polymarket 价格拉取:删除 _market_layer、market_scan 返回空、清理健康检查和配置验证 2026-05-23 12:04:40 +08:00
2569718930@qq.com 442a9c8560 修复支付代币匹配问题:direct 模式遍历所有支持代币查 Transfer 事件,未知代币不再伪装 USDC
- _extract_direct_transfer_event: 遍历 supported_tokens 所有合约而不仅是 intent.token_address
- _default_token_meta: 未知代币显示地址缩写而非 USDC
- _token_symbol_for: 未知代币同样不伪装 USDC
2026-05-23 11:41:01 +08:00
2569718930@qq.com f661350990 新增 GET /api/cities/model-range 端点:返回 7 城 DEB 预测和模型区间 2026-05-23 10:42:53 +08:00
2569718930@qq.com 2aa44dd5c7 天气日报末尾添加粗略预测免责提醒(代码拼接,不依赖 AI) 2026-05-23 10:33:46 +08:00
2569718930@qq.com 678b94000c 简化 AI prompt 为纯文本格式,降低 MiMo 空响应概率 2026-05-23 10:27:07 +08:00
2569718930@qq.com 0a7aaf19a6 修复 CMA 温度提取:_extract_first 添加 re.DOTALL 标志支持多行 HTML 内容 2026-05-23 10:20:59 +08:00
2569718930@qq.com c18ff504e4 修复 CMA 温度提取:改用纯文本数字匹配替代 HTML 标签猜测,天气优先 CMA、温度回退 OM 2026-05-23 10:13:16 +08:00
2569718930@qq.com 53225118c1 修复 Telegram HTML 解析错误:禁止 AI 使用 br 标签,添加 CMA 数据诊断日志 2026-05-23 10:06:52 +08:00
2569718930@qq.com 160dd65a54 接入中国气象局 weather.com.cn 预报数据作为天气日报主数据源
- 新增 CMA 7日预报页 HTML 爬虫,提取白天天气描述和最高/最低温
- 7 城优先使用 CMA 数据,Open-Meteo 仅做 fallback
- 数据来源在 prompt 中标注 weather.com.cn
2026-05-23 09:59:37 +08:00
2569718930@qq.com cfda3a796c 在代码层将 WMO weather_code 转译中文,杜绝 AI 天气误判 2026-05-23 09:53:12 +08:00
2569718930@qq.com a80f34137c 修复 AI 空响应:移除 prompt 中的 HTML 标签示例,加入 finish_reason 诊断日志 2026-05-23 09:50:08 +08:00
2569718930@qq.com dd65adb9d8 强制天气日报逐城格式:天气现象 + 最高温 + 体感建议,禁止结尾客套 2026-05-23 09:45:39 +08:00
2569718930@qq.com faf88387a1 禁止天气日报结尾废话:总结段落、免责声明等 AI 套话 2026-05-23 09:40:49 +08:00
2569718930@qq.com 73a51d73e8 优化天气日报 AI prompt:移除实时观测温度,聚焦预报最高温 + 天气现象代码
- 去掉当前温度和 METAR 报文,仅保留 forecast_high 和 weather_code
- 传入当前日期,防止 AI 猜测日期错误
- 禁止 AI 编造数据缺失声明
- 精简输出至 400 字以内
2026-05-23 09:38:08 +08:00
2569718930@qq.com f49a5bfbb2 新增中国城市天气日报:AI 生成每日天气摘要并推送至 Telegram 论坛群 General 话题
- 新建 src/utils/daily_weather_report.py,覆盖 7 个中国城市(北京、上海、广州、成都、重庆、武汉、青岛)
- 复用 Open-Meteo + METAR 数据源,MiMo AI 生成自然语言日报
- 注册为 runtime_coordinator 独立后台循环,默认每日 8:00 Asia/Shanghai 发送
- 环境变量:DAILY_WEATHER_REPORT_ENABLED / _HOUR / _MINUTE / _TIMEZONE

Constraint: message_thread_id=0 发送到论坛群 General 子话题
Tested: ruff check pass, Python 语法验证通过
2026-05-23 09:24:18 +08:00
2569718930@qq.com f8f0d69e5c 修复 AEROWEB Cookie 提取:httpx session.cookies 替代 resp.cookies.jar
httpx 的 cookies API 与 requests 库不同,resp.cookies.jar 在 httpx 中不存在,
导致 PHPSESSID 提取失败、登录报错。改用 self.session.cookies.get() 直接读取。
2026-05-23 01:04:01 +08:00
2569718930@qq.com b1b1f76bcb 修复测试:Paris settlement_source 已切换为 aeroweb 2026-05-22 21:59:42 +08:00
2569718930@qq.com 891b4d2422 新增 AEROWEB (Météo-France) 和 NCM (沙特) 实时气象数据源
AEROWEB: 接入 aviation.meteo.fr,LFPB METAR 2 分钟内可获取,
实测温度替代 AROME 模式预报作为 Paris 电报推送数据源。
登录流程: PHPSESSID + MD5 密码 → ajax/login_valid.php,
Session 20 分钟自动续期,XML 解析提取 tempe/td/dd/ff/qnh。

NCM: 沙特气象局 Meteomatics API 代码框架,待凭证激活。
Jeddah settlement_source 从废弃的 wunderground 迁移至 ncm。

同时清理: 日志文件 trading_system.log → polyweather.log,
删除 2026-02-07 的 1.2MB 废弃交易引擎日志。

Constraint: AEROWEB 需 AEROWEB_USERNAME/AEROWEB_PASSWORD 环境变量
Constraint: NCM 需 NCM_API_USERNAME/NCM_API_PASSWORD 环境变量
Confidence: high
Tested: AEROWEB 登录→取数→解析端到端验证通过
2026-05-22 21:50:37 +08:00
2569718930@qq.com 41d740d611 IMS 数据源切换至 10 分钟频率 hourly_observations_full 端点
原 hourly_observations 端点仅整点更新 TT 字段。
hourly_observations_full 提供每 10 分钟 TD (Temperature Dry) 字段,
精度 0.01°C,与 TT 完全一致(r=1.000)。
WS 风速单位 m/s → 内部转换为 km/h 和 knots。
日最高/最低从当天全部 10 分钟槽位扫描计算。

Constraint: TD 字段命名与露点温度易混淆,已在 docstring 注明
Confidence: high
Tested: TD vs TT 在 23 个整点时刻完全吻合
2026-05-22 19:31:09 +08:00
2569718930@qq.com 60ac4bae31 Tel Aviv 机场观测切换至 IMS Lod Airport 实时数据源
新增 IMS (Israel Meteorological Service) 数据抓取模块,以 Lod Airport
(station 225) 官方观测替代 NOAA METAR LLBG 作为 airport_primary。
IMS 数据接入 _airport_primary_from_raw 优先级链,高于 plain METAR
但保留 METAR 集群回退路径。

Constraint: IMS API (ims.gov.il/en/hourly_observations) 仅每小时更新
Confidence: high
Scope-risk: 仅影响 Tel Aviv 城市,其余 51 城数据流不变
Tested: 端到端验证 IMS 取数 → airport_primary → provider 路由链
2026-05-22 19:16:34 +08:00
2569718930@qq.com 79b33599ac 修正韩国 AMOS 跑道显示条件:有 runway_temps 即可展示
Rejected: 韩国 AMOS 无 TDZ/MID/END 点温,display 层自动退化为跑道温度格式
2026-05-22 06:06:20 +08:00
2569718930@qq.com b8a3c223f5 首尔釜山推送模板启用跑道观测格式,与中国城市一致
Directive: is_amsc 扩展为识别 amos 和 amsc_awos 两种源
2026-05-22 05:59:32 +08:00
2569718930@qq.com 105713c799 @
支付提交增加 Tx 预校验:提交前链上验签收款地址与金额,防止转错地址;409 错误展示友好中文提示并自动对账恢复

- 新增 validate_intent_tx 方法及 POST /api/payments/intents/{id}/validate 端点,
  在提交前查链上 receipt 对比收款地址和金额,mismatch 直接拦截
- 新增 handleSubmit409 辅助函数,根据后端错误详情分流处理:
  已支付→自动 reconcile,已过期→提示重下单,其他→透传具体原因
- submit/validate 路由透传后端 detail 字段,生产环境也能看到具体错误
- 手动转账面板粘贴 tx hash 后自动触发验证,绿色/红色提示,
  验证不通过时禁用提交按钮

Tested: tsc --noEmit + ruff check . 均通过
@
2026-05-22 04:44:14 +08:00
2569718930@qq.com 5e0dc3ec53 移动端城市列表移除可交易城市过滤,始终显示全部监测城市
Constraint: MobileCityPicker 数据源仅使用 store.cities
Scope-risk: 仅影响 cityListRows 构建逻辑
2026-05-22 04:24:01 +08:00
2569718930@qq.com dc164205f6 跑道观测推送追加 AMSC 实时 METAR 报文
Constraint: 仅中国城市 is_amsc 时显示,不影响其他城市
2026-05-22 04:12:53 +08:00
2569718930@qq.com 5176dff82d 中国城市电报推送接入 AMSC AWOS 跑道报文温度
Constraint: 仅当 amos 数据含 raw_metar 时才追加 AMSC 块,不影响非中国城市
2026-05-22 03:15:57 +08:00
2569718930@qq.com fe6f8b43b0 DEB 接入小时级误差计算,多模型权重基于每日+小时 MAE 融合
新增 compute_hourly_model_errors 聚合逐模型小时 MAE/RMSE

新增 _blend_mae 按样本数加权混合每日/小时误差(24样本=70%小时权重)

calculate_dynamic_weights 从 daily_record 读取 hourly_error 参与权重计算

update_daily_record 接受并持久化 hourly_error 字段

Tested: ruff check, pytest 186/186
2026-05-21 21:03:49 +08:00
2569718930@qq.com 24f2a82893 feat: fetch and cache hourly multi-model forecast data in SQLite for all cities 2026-05-21 19:50:54 +08:00
2569718930@qq.com dbc8923b76 修复 Polymarket 价格层在 MacBook 上被压缩成一列的问题
grid 改为 flex-wrap 自适应,描述列设 min-width:180px 防止文字被挤压
2026-05-21 18:40:32 +08:00
2569718930@qq.com fb81e01aae 将 scratch/ 加入 .gitignore,避免临时脚本误触发 pre-push lint
Directive: scratch/ 目录用于本地调试,不应提交也不应参与 CI 检查
2026-05-21 18:04:55 +08:00
2569718930@qq.com a39a74de2f @
修复决策卡片 grid minmax(0,1fr) 导致的内容溢出

MacBook 上 minmax(0,1fr) 会使列宽坍缩为 0,内容被截断。
改为 1fr + width:100% 确保决策带和市场决策区正常展示。
@
2026-05-21 17:58:33 +08:00
2569718930@qq.com 4fc4b538a5 @
调低地图详情面板隐藏断点 1680→1400px,MacBook 14" 可见

原 1680px 导致 MacBook Pro 14" (1512px) 地图模式详情面板被隐藏。
同时 max-height 从 900→860 避免误触发。
@
2026-05-21 17:31:43 +08:00
2569718930@qq.com ecec3fc087 @
修复 MacBook Safari 布局崩溃:100vw/dvh 和 -webkit-backdrop-filter

Safari 将滚动条宽度计入 100vw 导致内容被裁切,100vh 被地址栏撑破。
- root 容器: 100vw → width:100% + max-width:100vw
- 详情面板/扫描终端: 100vh → 叠加 100dvh 兼容 Safari 视口
- 详情面板: 添加 -webkit-backdrop-filter 前缀
@
2026-05-21 17:27:45 +08:00
2569718930@qq.com 89a82b5fb5 @
优化分析漏斗标签文案:付费相关节点更准确

- "点击付费" → "点击高级功能"
- "看到入口" → "看到付费墙"
@
2026-05-21 17:08:51 +08:00
2569718930@qq.com 9b9f548de9 从系统彻底移除 Lagos 城市
该城市已不再需要,从城市注册表、时区映射、缓存巡检脚本、测试
中全部清理。52 城市 → 51 城市。

Tested: pytest test_country_networks.py (19/19), ruff check
2026-05-21 14:30:18 +08:00
2569718930@qq.com 1edee81cf3 修复 paymentShell 测试断言,匹配移除 Matic 后的新文案
Tested: npm run test:business (20/20)
2026-05-21 13:09:21 +08:00
2569718930@qq.com 8022464e78 移除所有用户可见的 Matic 过时引用,统一使用 Polygon / POL
Polygon 已于 2021 年从 Matic 更名,2024 年 token 从 MATIC 迁移为 POL。
- polygonChain 标签: "Polygon (Matic) Network" → "Polygon Network"
- chainIdToDisplayName: "Polygon (Matic)" → "Polygon"
- paymentGasWarning: "POL/MATIC" → "POL"
- chainSwitchPrompt: "Polygon (Matic)" → "Polygon"
- 错误检测正则保留 matic 关键字以兼容旧钱包

Tested: tsc --noEmit
2026-05-21 13:04:37 +08:00
2569718930@qq.com 982d192499 支付管理摘要区新增支付网络信息行
用户反馈支付管理区未标明链网络,在账号/钱包/收款合约行下方
新增"支付网络 (Payment Network)" InfoRow,明确显示 Polygon (Matic)。

Tested: tsc --noEmit
2026-05-21 12:54:18 +08:00
2569718930@qq.com 0234104a63 支付方式选择区域补全中英文 i18n,链网络标签明确 Polygon (Matic)
- 支付方式选择区域全部硬编码中文替换为 copy 对象引用
- 新增 20 个 i18n key:支付方式标签、描述、警告、手动转账表单
- 链相关标签从模糊的 "Polygon Chain" 改为明确的双语 "Polygon (Matic)"
- 链切换错误提示支持中英文
- TypeScript 类型检查通过

Tested: tsc --noEmit
2026-05-21 12:25:57 +08:00
2569718930@qq.com 2f0b496066 ops 订阅开通改为 Next.js 直连 Supabase,免去 VPS 鉴权链路 2026-05-20 22:23:28 +08:00
2569718930@qq.com cb625a2b0e ops 代理路由直接使用 entitlement token 作为 Bearer 鉴权 2026-05-20 22:11:57 +08:00
2569718930@qq.com 0f8d160c54 entitlement 无转发头时返回占位身份,交由 ops admin 裁决 2026-05-20 21:57:27 +08:00
2569718930@qq.com 1766a71f63 纯 entitlement token 也可通过 ops admin 鉴权 2026-05-20 21:53:22 +08:00
2569718930@qq.com b36c712ee4 同步文件 2026-05-20 21:18:59 +08:00
2569718930@qq.com a0005b1e37 同步前端文件更新 2026-05-20 21:08:54 +08:00
2569718930@qq.com 717ad3c6f4 前端订阅操作:移除季付年付选项,新增开通时扣除积分 2026-05-20 20:56:21 +08:00
2569718930@qq.com b0662f6fef 开通 Pro 时支持同时扣除用户积分 2026-05-20 20:52:25 +08:00
2569718930@qq.com d04d6c9efb 移除季付和年付计划,仅保留月付 2026-05-20 20:44:13 +08:00
2569718930@qq.com 75dd7464cc 新增积分转账功能:支持管理员手动扣除和划转用户积分 2026-05-20 20:39:51 +08:00
2569718930@qq.com db955d59ea 修复管理员手动开通失败:移除 ops 后台的订阅要求检查 2026-05-20 20:28:54 +08:00
2569718930@qq.com 133d341605 更新 .env.example 反映埋点默认启用的变更 2026-05-20 20:15:59 +08:00
2569718930@qq.com 2f9889d63a 修复后台漏斗无数据:埋点默认启用、补全 signup_completed 和 dashboard_active 事件上报 2026-05-20 20:13:18 +08:00
2569718930@qq.com ef2691a37b 同步文件换行符 2026-05-20 20:00:32 +08:00
2569718930@qq.com 84a2d4ce06 恢复币安注入提供者列表但添加 WalletConnect 提示,更新支付测试 2026-05-20 19:48:13 +08:00
2569718930@qq.com 8ba9567f64 移除深圳宝安机场跑道数据采集,深圳市场结算使用流浮山 HKO 数据 2026-05-20 19:38:07 +08:00
2569718930@qq.com 0c24a3ed6d 训练数据页面图表化:KPI 概览、DEB/概率命中率柱状图、MAE/Brier Score 可视化 2026-05-20 19:26:52 +08:00
2569718930@qq.com dd01383b3a 优化钱包兼容性:移除 value:0x0 以兼容更多钱包、增强币安检测和网络切换提示 2026-05-20 19:18:29 +08:00
2569718930@qq.com edf41efad6 过滤币安 Web3 注入提供者,引导用户使用 WalletConnect 支付 2026-05-20 19:00:07 +08:00
2569718930@qq.com 871d21792e 修复账户页 Pro 状态偶发性丢失 2026-05-20 18:50:17 +08:00
2569718930@qq.com 5fa3bb1b59 非跑道城市推送恢复普通机场格式 2026-05-20 18:41:59 +08:00
2569718930@qq.com 10a0788398 修复 AMSC 风向取第一条跑道可能为空的问题 2026-05-20 18:35:35 +08:00
2569718930@qq.com 573393409d 更新业务状态测试适配全跑道展示 2026-05-20 18:25:37 +08:00
2569718930@qq.com 9b2ac614cb 重构跑道观测系统:全跑道展示、结算跑道标注、热力模型、风场分析 2026-05-20 18:16:27 +08:00
2569718930@qq.com 4e59c41c81 添加 ops API 代理路由 2026-05-20 17:35:39 +08:00
2569718930@qq.com b00790850c 移除示例配置中的个人钱包地址 2026-05-20 17:18:19 +08:00
2569718930@qq.com 44a273c9d1 修复未使用变量 2026-05-20 17:13:05 +08:00
2569718930@qq.com 6f7e847b01 添加后台 Telegram 审计面板 2026-05-20 17:12:32 +08:00
2569718930@qq.com 66512d2623 完善后台管理功能:订阅管理增强、训练精度面板、支付记录查询 2026-05-20 16:50:19 +08:00
2569718930@qq.com 05f5d3c2dd 添加后台支付成功记录列表 2026-05-20 14:12:35 +08:00
2569718930@qq.com 452dfe2218 @
移除 IMGW / Synoptic 健康检查

两者均为可选 fallback 数据源,VPS 未配置且非核心链路
Synoptic 检查还存在变量名错误(查 SYNOPTIC_API_TOKEN 而非 NOAA_WRH_MESO_TOKEN)
@
2026-05-20 12:15:52 +08:00
2569718930@qq.com 56bcb89d3d @
移除 OpenWeather / VisualCrossing 健康检查

两个数据源有实现但从未在主流程中被调用,属于死代码
@
2026-05-20 12:07:35 +08:00
2569718930@qq.com 5ef0b49299 @
后台健康检查超时从 3s 提高到 8s

aviationweather.gov 从亚洲 VPS 连接经常超过 3 秒导致误报
@
2026-05-20 12:01:21 +08:00
2569718930@qq.com 82d7c0ea6d @
修复 Telegram 内联按钮点击无响应

infinity_polling allowed_updates 未包含 callback_query,导致确认绑定按钮无响应
@
2026-05-20 11:50:32 +08:00
2569718930@qq.com 781d8cf476 @
修复后台管理 CWA 状态显示为未配置

ops_api 读取 CWA_API_KEY,但实际环境变量是 CWA_OPEN_DATA_AUTH
@
2026-05-20 11:44:07 +08:00
2569718930@qq.com 9a5f9abf21 @
修复 trial 用户无法打开付款入口

canOpenCheckoutOverlay 缺少 isTrialPlan 条件,导致试用用户无法升级付费
@
2026-05-20 11:27:46 +08:00
2569718930@qq.com 60093d3162 @
统一月付价格为 10 USDC 并清理所有 trial 文案

- 默认月付 fallback 从 5U 改为 10U(contract_checkout.py、AccountCenter.tsx)
- 移除 LoginClient / HeaderBar / AccountCenter / UnlockProOverlay 中的试用推广文案
- VPS 同步: PLAN_CATALOG_JSON、GROUP_MEMBER_PRICE_USDC 更新为 10
- SIGNUP_TRIAL_ENABLED=false 保持关闭
@
2026-05-20 11:12:29 +08:00
2569718930@qq.com 6e3b7f60a2 fix(payment): display payment management panel for non-subscribed users to allow manual transfer tx submission 2026-05-20 09:59:27 +08:00
2569718930@qq.com 5e4070ad27 feat(ops): add rich analytics charts for overview, health, and payments pages 2026-05-20 09:48:12 +08:00
2569718930@qq.com 4de009b401 feat: unified map click selection & switch to decision cards with paywall overlay for non-Pro users 2026-05-20 09:41:44 +08:00
2569718930@qq.com 23f7e29acc Update payment token configuration examples to show dual USDC/USDC.e setup 2026-05-20 09:25:54 +08:00
2569718930@qq.com 9ff0685756 Unify subscription pricing to 10 USDC, update documentation, and improve manual payment address copy experience 2026-05-20 09:20:07 +08:00
2569718930@qq.com 95192e8b58 Fix manual payment receiver address truncation 2026-05-20 09:05:14 +08:00
2569718930@qq.com 72e93f8e93 Optimize multi-model caching and frontend revalidation 2026-05-20 08:58:16 +08:00
2569718930@qq.com 1af33c3ab8 chore(account): separate payment methods into tabs and fix noWallet copy 2026-05-20 08:33:59 +08:00
2569718930@qq.com df892ce9a6 chore(ops): fall back to local trading_system.log in get_ops_logs when docker logs is empty 2026-05-20 08:20:16 +08:00
2569718930@qq.com 76d5a0abe8 refactor: remove redundant telegram pricing, align daily forecast date, and hide sunrise/sunset from UI 2026-05-20 07:55:47 +08:00
2569718930@qq.com 3fd52ad9d4 feat: add ops_api service for administrative management of users, subscriptions, and system analytics 2026-05-19 23:57:08 +08:00
2569718930@qq.com a714817cdf feat: implement health check dashboard client and admin service APIs 2026-05-19 23:39:33 +08:00
2569718930@qq.com 4488d6a9b1 关闭3天试用:VPS 禁用 signup trial,不合格入群申请直接拒绝 2026-05-19 19:41:39 +08:00
2569718930@qq.com e1e000e854 后台会员页新增增长趋势图表:累计曲线、每日新增堆叠面积图、统计卡片 2026-05-19 19:28:38 +08:00
2569718930@qq.com d50ced1b8f 将群 -1003965137823 纳入发言积分体系:新增积分群白名单、防误计分 2026-05-19 18:55:47 +08:00
2569718930@qq.com f4949212d8 修复 ruff lint:移除未使用的变量 2026-05-19 18:34:37 +08:00
2569718930@qq.com 82d60cf05f feat: implement Telegram-to-Web account binding flow and add supporting database and UI components 2026-05-19 18:26:45 +08:00
2569718930@qq.com a5c508cce8 feat: implement SQLite database management and Supabase integration for user authentication and points synchronization 2026-05-19 18:07:39 +08:00
2569718930@qq.com 0c2e22e770 feat: implement ScanTerminalShellParts component for loading, topbar, and paywall UI 2026-05-19 17:52:31 +08:00
2569718930@qq.com ac4f70e33d feat: implement basic command handlers, orchestrator, database manager, and account binding UI 2026-05-19 17:47:51 +08:00
2569718930@qq.com 89914a296b feat: add AccountCenter component for user profile and subscription management 2026-05-19 17:31:31 +08:00
2569718930@qq.com 6e2baa14ad 账户页付费用户新增城市话题群入口链接 2026-05-19 17:15:16 +08:00
2569718930@qq.com 9d6eb54a4f 移除分布视图地图点击时的 flyTo 放大动画 2026-05-19 17:09:59 +08:00
2569718930@qq.com bddf7f1372 修复地图点击城市跳转决策卡:Pro 用户自动切换到分析视图 2026-05-19 17:06:00 +08:00
2569718930@qq.com ae37e79fbb 修复测试:移除 _attach_russia_official_nearby mock 2026-05-19 16:59:35 +08:00
2569718930@qq.com a6e7d6682d 移除俄罗斯 pogodaiklimat 数据源:周边观测站不需要 2026-05-19 16:57:00 +08:00
2569718930@qq.com f2ab62ba83 修复 NMC 移除后的测试回归:删除 NMC 相关测试用例 2026-05-19 16:25:01 +08:00
2569718930@qq.com 3ca5b71299 移除 NMC(中国国家气象中心)数据源:无实际使用价值 2026-05-19 16:20:48 +08:00
2569718930@qq.com a7be2adbbb 修复测试:AMSC/NMC 用 monkeypatch.setattr 替换模块级常量 2026-05-19 16:03:07 +08:00
2569718930@qq.com 49238cf420 修复测试:AMSC/NMC 测试适配环境变量 URL 模式 2026-05-19 15:56:28 +08:00
2569718930@qq.com 6b741cc1ec 修复 Russia 数据源 f-string 嵌套引号语法错误 2026-05-19 15:44:52 +08:00
2569718930@qq.com 3b8098051e 将商业核心数据源 URL 从源码迁移到环境变量,VPS .env 已补全 2026-05-19 15:42:03 +08:00
2569718930@qq.com 7e0d955d22 修复语法错误:多余括号 2026-05-19 15:19:32 +08:00
2569718930@qq.com 4c6a93498c 移除源码中硬编码的 API token:NOAA MesoWest 密钥和 CWA 占位符 2026-05-19 15:18:49 +08:00
2569718930@qq.com 96b642ffac 重写 Pro 升级公告文案:突出交易决策价值,而非功能清单 2026-05-19 15:10:35 +08:00
2569718930@qq.com 3d4a0148fe 修复 KNMI 健康检查 404:列表 API 已移除,改用实际数据端点 2026-05-19 15:00:10 +08:00
2569718930@qq.com 8cb5b3b310 整理文档:删除 4 个已废弃 EMOS/LGBM 文档,更新 8 个引用了已删除功能的其他文档 2026-05-19 14:53:37 +08:00
2569718930@qq.com d07aaf49b4 触发 CI 自动部署验证(ed25519) 2026-05-19 14:33:26 +08:00
2569718930@qq.com d83733399e 清理测试注释,准备 ed25519 CI 部署验证 2026-05-19 14:28:20 +08:00
2569718930@qq.com 9db37c6d7b 修复 CI 部署:使用原生 ssh 替代 appleboy action 2026-05-19 14:22:43 +08:00
2569718930@qq.com cd9de0926b 触发 CI 自动部署验证 2026-05-19 14:14:48 +08:00
2569718930@qq.com 3c013e300a CI 全流程自动化:测试通过后自动部署到 VPS 2026-05-19 13:24:02 +08:00
2569718930@qq.com e56040a855 添加一键部署脚本:deploy.sh (bash) 和 deploy.ps1 (PowerShell) 2026-05-19 13:19:03 +08:00
2569718930@qq.com 53c9ed2118 修复 CI:移除 package.json 中残留的 husky prepare 脚本 2026-05-19 13:15:48 +08:00
2569718930@qq.com b9130cb30a 修复测试:移除已删除的 artifacts 字段断言 2026-05-19 13:09:44 +08:00
2569718930@qq.com c4d4d0cdbc 修复回归测试:移除已删除模块的 mock 引用 2026-05-19 13:04:28 +08:00
2569718930@qq.com cb7526e799 修复 scrub_secrets.py 未使用的 os import 2026-05-19 12:49:18 +08:00
2569718930@qq.com 6d132e6a7f 移除未使用的 husky 依赖,pre-push hook 用手写脚本替代 2026-05-19 12:48:36 +08:00
2569718930@qq.com 476a4f85d8 修复移动端点击城市列表触发 Leaflet flyTo NaN 崩溃:隐藏地图容器跳过动画 2026-05-19 12:41:38 +08:00
2569718930@qq.com b93a75516d 移除未使用的 Groq 和 Meteoblue 服务代码及配置 2026-05-19 00:05:07 +08:00
2569718930@qq.com 19bd8f3636 @
合并 feat/mobile-layout:修复移动端城市列表搜索无数据显示
@
2026-05-19 00:00:10 +08:00
2569718930@qq.com 1326176a85 @
修复移动端城市列表搜索无数据显示:非 Pro 用户回退到基础城市数据源

MobileCityPicker 原依赖扫描终端 API rows,该 API 仅 Pro 用户触发,
导致访客/免费用户 cityListRows 为空,所有城市被 pickCityRow 过滤掉。
现新增 cityListRows fallback:无 scan 数据时从 store.cities +
citySummariesByName 构建行数据,保证所有用户可见并搜索城市列表。

Constraint: fallback rows 缺少 metar/target 字段,但 MobileCityPicker 仅消费 display/temp/deb/airport 字段,不影响渲染。
@
2026-05-18 23:50:18 +08:00
2569718930@qq.com c38dc80f58 后台新增 API 状态检测页:实时检测 Supabase/Open-Meteo/METAR/KNMI/MADIS/Telegram 连通性 2026-05-18 23:25:16 +08:00
2569718930@qq.com 5b1402fc7c refactor: extract analysis signal builders 2026-05-18 23:11:56 +08:00
2569718930@qq.com eaa695ec47 回滚 web 端口 localhost 绑定,Cloudflare Worker 需要外部访问 2026-05-18 23:10:08 +08:00
2569718930@qq.com 4d5a5b674d 修复 web 容器 healthcheck:用 python 替代不存在的 curl 2026-05-18 23:00:03 +08:00
2569718930@qq.com 473ed82202 Docker healthcheck、.env.example 清理死变量并补全缺失配置、web 端口绑定 localhost 2026-05-18 22:57:00 +08:00
2569718930@qq.com d4640a578d 项目体检收尾:更新 CLAUDE.md 移除 EMOS/LGBM 引用,清理前端死字段,移除 git 跟踪的临时文件,VPS 关闭退役钱包监控 2026-05-18 22:54:00 +08:00
2569718930@qq.com 0e4e7aebb2 项目体检修复:删除破损的 LGBM 导入、8个死测试、2个死脚本、移除 pytz/lightgbm 依赖 2026-05-18 22:43:34 +08:00
2569718930@qq.com 2b6b18ec20 修复 city_runtime.py 引用已删除的 probability_snapshot_archive 模块 2026-05-18 22:30:56 +08:00
2569718930@qq.com 0e0aad3171 删除 LGBM 全部代码和模型文件,EMOS 简化为纯 legacy 高斯分桶模式 2026-05-18 22:05:55 +08:00
2569718930@qq.com aec47adda1 缓存分析饼图:移除零值过滤,即使很小的命中数也显示 2026-05-18 21:34:17 +08:00
2569718930@qq.com 2fa27f54ac 修复转化漏斗:前后端数据格式对齐,正确解析 events/rates 结构 2026-05-18 21:29:06 +08:00
2569718930@qq.com 671a862400 总览页升级为综合数据大屏:双列布局、缓存桶图、会员分布饼图、转化漏斗、城市覆盖 2026-05-18 21:18:22 +08:00
2569718930@qq.com a9d71c18aa 后台数据可视化:Recharts 漏斗图、总览页 KPI 卡片和缓存饼图 2026-05-18 20:53:27 +08:00
2569718930@qq.com e12d935cde 会员订阅区分体验用户和付费用户:类型标签、筛选器、来源字段 2026-05-18 20:29:33 +08:00
2569718930@qq.com 546bc0c21e 删除旧的 OpsDashboard 1694 行单页组件 2026-05-18 20:26:24 +08:00
2569718930@qq.com 67701a4715 后台管理系统后端 API:在线配置编辑、手动订阅管理、日志查看(logs 目录避开 gitignore) 2026-05-18 20:18:42 +08:00
2569718930@qq.com 073d0efbb0 后台管理系统基础框架:侧边栏布局、9 个页面模块、共享类型和 API 客户端 2026-05-18 20:10:08 +08:00
2569718930@qq.com 2d1cb55151 非 Pro 用户隐藏城市决策卡标签页和深度分析视图 2026-05-18 19:35:16 +08:00
2569718930@qq.com 2d459f3052 城市 panel/nearby 数据对游客开放,market/full 保留鉴权 2026-05-18 19:28:54 +08:00
2569718930@qq.com 6bdad2eae9 修复游客无法加载城市简报:middleware 白名单遗漏 detail 端点导致 401 2026-05-18 19:22:51 +08:00
2569718930@qq.com ff420c4bec 修复地图点击城市后跳转到决策卡而非右侧简报的问题 2026-05-18 19:13:21 +08:00
2569718930@qq.com 426bd7deab 统一城市决策卡 Pro 门禁:游客和免费用户点击 Today 引导至账户页 2026-05-18 19:06:04 +08:00
2569718930@qq.com 3e941a238a 移除分布视图的 Pro 权限门禁,游客和免费用户可查看地图和城市简报 2026-05-18 18:56:32 +08:00
2569718930@qq.com ad16ae6b3b 账户页 Telegram 区块仅付费用户可见,移除过期的频道升级通知和市场监控链接 2026-05-18 18:44:20 +08:00
2569718930@qq.com 1c4287380e 修复 KNMI 维度名称为字符串类型的兼容性问题 2026-05-18 18:30:12 +08:00
2569718930@qq.com 8c640b8616 适配 KNMI netCDF 新数据布局:(station,time) 替代 (time,station) 2026-05-18 18:28:18 +08:00
2569718930@qq.com 8a5e8957c5 适配 KNMI station ID 格式变更:3位码→5位WMO码,修复前导零丢失 2026-05-18 18:23:57 +08:00
2569718930@qq.com 06ac6bab32 修复 KNMI S3 下载被 Auth header 污染导致 netCDF 解析失败 2026-05-18 18:19:05 +08:00
2569718930@qq.com abdce3cd3b 适配 NOAA MADIS HFMETAR netCDF 新格式:stationId 替代 icaoId,开尔文转摄氏度,气压 Pa 转 hPa 2026-05-18 17:48:56 +08:00
2569718930@qq.com c6321253fe 修复 MADIS HFMETAR 目录路径:NOAA 已将文件迁移到 netCDF 子目录 2026-05-18 17:33:35 +08:00
2569718930@qq.com 4a8eeaae5e 修复 KNMI API key 未传递到 HTTP 请求的 bug 2026-05-18 17:28:56 +08:00
2569718930@qq.com 78b23ef361 feat: implement Telegram account binding and update project structure in documentation 2026-05-18 17:10:44 +08:00
2569718930@qq.com 1b2731ed12 fix: prefer dedicated telegram pricing group 2026-05-18 16:22:16 +08:00
2569718930@qq.com 6041d25f23 feat: add telegram group pricing and direct payments 2026-05-18 16:18:26 +08:00
2569718930@qq.com d99a3c25e9 Add safe Open-Meteo cache diagnostic 2026-05-18 00:21:18 +08:00
2569718930@qq.com fd59cd018d Harden Open-Meteo rate limiting 2026-05-18 00:13:51 +08:00
2569718930@qq.com 6cee86ec7b 修复 ruff E401: 拆分 check_city_cache.py 的合并 import 2026-05-17 23:01:21 +08:00
2569718930@qq.com 44e8921e5c 前端移除 Lagos 城市及相关本地化文案 2026-05-17 22:59:58 +08:00
2569718930@qq.com 4acd0d9da6 机场推送新增 Tel Aviv(LLBG / Ben Gurion)
- HIGH_FREQ_AIRPORT_CITIES 从 30 城扩到 31 城
- 新增论坛子话题 thread_id=3408
- ICAO: LLBG,数据源: IMS + METAR
2026-05-17 22:56:43 +08:00
2569718930@qq.com bd21b05fbc 完全移除预热(prewarm)功能
- 删除 src/utils/prewarm_dashboard.py
- 删除 scripts/prewarm_dashboard_cache.py、prewarm_dashboard_worker.py
- docker-compose.yml 移除 polyweather_prewarm 服务
- runtime_coordinator.py 移除预热循环启动逻辑
- web/core.py 移除 prewarm status 上报
- web/routers/system.py 移除 /api/system/prewarm 端点
- web/services/system_api.py 移除 run_system_prewarm
- city_runtime.py DEFAULT_PREWARM_CITIES 改名为 DEFAULT_STATUS_CITIES
- 清理 env.example、测试、VPS .env 中的预热配置
2026-05-17 22:43:17 +08:00
2569718930@qq.com 22e2409e13 修复 Open-Meteo 冷却期无限循环导致多模型数据缺失
- fetch_all_sources 新增 om_from_cache_only 模式:force_refresh_observations_only
  时直接从内存缓存取 OM 数据,不发起 HTTP 请求。缓存未命中则跳过,
  避免机场推送 60s 周期持续触发 429 限流
- nws_open_meteo_sources 三类 fetch 函数冷却期加磁盘缓存兜底:
  内存未命中时 force-reload SQLite 磁盘缓存再查一次
- 预热停止后冷却期自然过期(900s),下一次正常请求会填充缓存
2026-05-17 22:31:10 +08:00
2569718930@qq.com 415f03466d 修复 CI: mobileAnnouncement 测试适配公告移除 2026-05-17 21:52:50 +08:00
2569718930@qq.com 4c6eaa6390 feat: add useAiCityForecast hook to manage streamed AI city weather forecasting logic 2026-05-17 21:50:20 +08:00
2569718930@qq.com 3351991fe1 feat: add AiCityTemperatureChart component and useAiPinnedCityWorkspace hook for deep-analysis city tracking 2026-05-17 21:37:51 +08:00
2569718930@qq.com 1645fd88d0 移除 v1.5.6 升级公告横幅
- ScanTerminalDashboard: 删除 showAnnouncement 状态、localStorage 检测逻辑、渲染代码
- ScanTerminalShellParts: 删除 ScanUpgradeAnnouncement 组件
2026-05-17 21:24:28 +08:00
2569718930@qq.com 9d8c59f741 修复城市决策卡切换时地图白屏和第二城市加载失败
- MapCanvas 改为始终挂载,视图切换用 display:none 隐藏而非卸载,
  避免 Leaflet 重初始化时容器尺寸为 0 导致白屏
- handleMapCitySelect 在 matchedRow 为空时主动调用 ensureCityDetail
  预加载城市详情,不再依赖 hydration 队列异步补拉
2026-05-17 21:20:17 +08:00
2569718930@qq.com deec8221ea 重构温度曲线图表数据模块,修复 DEB offset 基准
- 新建 temperature-chart-paths.ts:抽取 8 个纯函数(buildChartTimeAxis、
  buildDebBaselinePath、buildCalibratedPath、buildObservationGrid 等)
- DEB offset 基准改为优先用 hourly 曲线自身 max,forecast.today_high
  降级为 fallback,防止不可靠的 today_high 整体抬升/压低曲线
- chart-utils.ts 精简 ~280 行,清除重写的 normalizeTafHm/chartHmToMinutes
- temperatureChartData.test.ts 新增 Moscow/Ankara/正常城市 3 个测试场景
2026-05-17 20:57:52 +08:00
2569718930@qq.com 9e400a3802 Document airport push worker cap 2026-05-17 20:25:27 +08:00
2569718930@qq.com c74c193b02 移除市场监控推送功能及相关代码
- telegram_push.py: 删除 MARKET_MONITOR_CITIES/INTERVAL、_build_market_monitor_message、
  _run_market_monitor_cycle、start_market_monitor_push_loop、_format_percent/_format_prob
- telegram_chat_ids.py: 删除 get_market_monitor_chat_ids_from_env
- runtime_coordinator.py: 删除 _start_market_monitor_push_loop 及调用
- 移除 #市场监控 hashtag
2026-05-17 20:22:42 +08:00
2569718930@qq.com 07f61f8ca9 Bump city detail cache for chart rebuild 2026-05-17 19:48:09 +08:00
2569718930@qq.com 38ac9844c1 Ensure DEB chart path covers full day 2026-05-17 19:34:42 +08:00
2569718930@qq.com 1ab10a9c90 Add MGM hourly support for Ankara models 2026-05-17 19:19:12 +08:00
2569718930@qq.com a83200d505 Fix Ankara decision chart model coverage 2026-05-17 19:12:28 +08:00
2569718930@qq.com 8ade6dd7d2 Improve scan decision card hydration 2026-05-17 18:57:02 +08:00
2569718930@qq.com 5c2977fe71 修复温度曲线图 DEB 路径不显示:后端 v2 API 的 timeseries.hourly 映射到前端 CityDetail.hourly
- normalizeCityDetailPayload 新增 timeseries.hourly → hourly 字段提升
- 预热列表 DEFAULT_CITIES 扩展到 33 城,覆盖全部机场推送城市
2026-05-17 18:41:53 +08:00
2569718930@qq.com ff6d6c550f 预热城市列表扩展到 33 城:覆盖全部机场推送城市
- 原 19 城缺少 amsterdam/helsinki/lau fau shan 及 11 个美国城市
- 导致这些城市 detail API 冷启动时 OM 缓存未命中,hourly 数据为空,温度曲线图无法渲染 DEB 路径
2026-05-17 18:34:51 +08:00
2569718930@qq.com 55099c3dd8 恢复城市决策卡温度曲线图
- AiPinnedCityCard: 重新引入 AiCityTemperatureChart 渲染
- MobileDecisionCard: 恢复温度走势图折叠区
2026-05-17 18:19:31 +08:00
2569718930@qq.com 02fd7d9e49 修复 CI: 适配 MobileCityPicker 测试断言 + 清理 f-string 和无用 import
- removedMonitorRunwayTabs.test.ts: 检查 MobileCityPicker 替代旧 scan-mobile-city-list-view
- stableServerRefreshPolicy.test.ts: 同上,MobileCityPicker 替代旧视图标识
- scan_forum_topics.py: 移除无占位符的 f-string 和无用 json import
2026-05-17 18:08:59 +08:00
2569718930@qq.com d39534dff4 机场推送重构:观测缓存分离 + 全城市覆盖 + 四路并发
- 新增 force_refresh_observations_only 模式:机场推送仅刷新 METAR/AMOS
  观测缓存,多模型预报缓存保留 15 分钟,杜绝 Open-Meteo 429 限流后
  DEB 回退到实测温度的 bug
- 砍掉夜间静默和温度状态机,每条新观测无条件推送
- HIGH_FREQ_AIRPORT_CITIES 从 19 城扩到 30 城(新增 11 个美国城市)
- 美国城市走 airport_primary (MADIS) 优先取温
- 机场周期从串行改为 ThreadPoolExecutor(max_workers=4),周期时间从
  ~120s 压缩到 ~33s

Constraint: 单核 VPS 安全并发上限
Tested: docker compose up -d --build polyweather 重建后推送正常
2026-05-17 18:04:05 +08:00
2569718930@qq.com 58e6557c66 feat: implement mobile scan terminal dashboard and city picker with backend forum automation scripts 2026-05-17 17:05:47 +08:00
2569718930@qq.com 2d9aa576dd feat: implement scan terminal mobile dashboard components and data management logic 2026-05-17 15:32:47 +08:00
2569718930@qq.com 04e0369255 feat: implement Telegram push notification utility with state management and market filtering 2026-05-17 15:06:27 +08:00
2569718930@qq.com c60baaa1e8 feat: implement utility modules and AI-pinned city dashboard components for temperature forecasting 2026-05-17 14:33:23 +08:00
2569718930@qq.com 341c6d0a93 feat: implement Telegram push utility and add new dashboard components for scan terminal and paywall management 2026-05-17 13:59:40 +08:00
2569718930@qq.com a83d9e7649 feat: implement 60-second analysis cache TTL for high-frequency airport cities 2026-05-17 13:41:44 +08:00
2569718930@qq.com 07bb8e1d15 feat: implement Telegram push utility and corresponding hashtag validation tests 2026-05-17 13:37:51 +08:00
2569718930@qq.com 8e2317592d feat: implement chart utilities for temperature forecasting, calibration, and observation processing 2026-05-16 22:19:10 +08:00
2569718930@qq.com 80ca892611 回退 debTemps 首尾填充,修复 canvas CSS 无尺寸导致的图表缩塌
- 首尾填充会画成水平直线不反映预测,回退;past_days=1 已从后端补齐全天数据
- canvas 保留 width/height:100% 但去掉 !important,Chart.js 自管分辨率
2026-05-16 22:05:09 +08:00
2569718930@qq.com a24185e6dc DEB 预测线填充首尾空值,确保全天 00:00-23:00 铺满 2026-05-16 21:40:56 +08:00
2569718930@qq.com 4a938395be 修复温度曲线三个渲染问题:数据点过少、张力过高、canvas被CSS拉伸
- 小时数据插值为每半小时一点(24→47点)
- 全线 tension 从 0.28-0.32 降至 0.1-0.12
- 移除 canvas CSS width/height !important 声明,交由 Chart.js ResizeObserver 管理
2026-05-16 21:30:47 +08:00
2569718930@qq.com 2e02133696 修复图表时间轴不全与城市决策卡模型补齐卡顿
- 后端 Open-Meteo 请求加 past_days=1,小时数据从 00:00 开始
- 前端图表层填充完整 00:00-23:00 时间轴,缺失时段置空
- 城市深度分析门槛从 >1 降为 >=1,单模型城市不再被拦
- hydration 队列加最大重试 3 次,防止永久卡在等待模型补齐
- 移除右侧面板历史对账按钮
2026-05-16 20:14:05 +08:00
2569718930@qq.com b27bedbca3 修复网站 API 阻塞与积分同步问题
- uvicorn 改用 import string 格式启动 4 workers,防止数据采集阻塞 event loop
- prewarm 去掉 --force-refresh,仅依赖缓存预热避免每 5 分钟全量采集
- 积分变动时同步写入 Supabase user_metadata,避免前端回退路径失败时显示 0 积分
- /api/auth/me 积分解析增加 Supabase email 回退路径
2026-05-16 13:15:30 +08:00
2569718930@qq.com 884a7899f3 修复图表横轴时间标签不完整:用tickLabels替代index取模过滤 2026-05-16 00:12:56 +08:00
2569718930@qq.com 04d0ae1125 卡片结构重组为三层:当前/日高/DEB显式展示→模型区间→市场叙述
叙述函数改为多行解释,增加具体delta和状态描述
2026-05-15 23:59:57 +08:00
2569718930@qq.com 6f14ef20db 重构跑道观测卡片为市场结构卡:核心指标压缩+多模型结构+市场叙述 2026-05-15 23:54:19 +08:00
2569718930@qq.com 9d3aa7fb53 修复凌晨伪冲顶误判:夜间直接停推,早晨需日高已形成才激活Peak Watch 2026-05-15 23:42:46 +08:00
2569718930@qq.com 5d514a4950 推送消息增加市场状态标签:🚀超预期 🔥升温中 ⚠️冲顶观察 ❄️降温中
取消DEB兑现即停推规则,超预期行情为重点信号
2026-05-15 23:36:03 +08:00
2569718930@qq.com 87b24aa071 重写跑道观测推送规则为热度状态机:盯今日最高而非DEB,夜间停推 2026-05-15 23:30:41 +08:00
2569718930@qq.com e7a13c9be7 跑道观测推送增加规则:今日实测最高已达DEB预报则跳过,避免夜间降温噪音 2026-05-15 23:26:33 +08:00
2569718930@qq.com 6a4ec08b12 修复CI测试用例以匹配最新推送格式、间隔、AMSC数据结构 2026-05-15 18:01:53 +08:00
2569718930@qq.com 6746650bb1 跑道观测推送及前端刷新间隔改回60秒,匹配AMSC每分钟更新频率 2026-05-15 17:58:13 +08:00
2569718930@qq.com 861b394e49 跑道观测推送时间修正为AMSC/AMOS观测时间,显示HH:MM格式 2026-05-15 17:49:37 +08:00
2569718930@qq.com cbf6a12f79 跑道观测推送增加市场最高选项对比,超过市场覆盖范围则跳过 2026-05-15 17:43:41 +08:00
2569718930@qq.com 767e0e7de1 feat: implement telegram alert notification utility with state persistence and market filtering logic 2026-05-15 17:10:16 +08:00
2569718930@qq.com e93f052559 feat: implement telegram notification push utilities with state management and alert filtering 2026-05-15 16:38:02 +08:00
2569718930@qq.com a614cbd298 feat: implement runway observations monitoring panel and Telegram alert utilities 2026-05-15 16:22:42 +08:00
2569718930@qq.com f7fb2ec83b feat: add utility module for Telegram alert push notifications and state management 2026-05-15 16:20:07 +08:00
2569718930@qq.com 30de937d04 feat: add Telegram push notification utility for weather alerts with state persistence 2026-05-15 16:09:32 +08:00
2569718930@qq.com 7aea20c956 feat: implement telegram notification utilities with alert state management and market filtering 2026-05-15 15:46:02 +08:00
2569718930@qq.com b45010a92e feat: implement runway observations dashboard panel, add telegram alert utility, and create city payload service 2026-05-15 15:37:25 +08:00
2569718930@qq.com 5e8548999a feat: implement ScanTerminalDashboard UI and state management with supporting tests 2026-05-15 15:02:15 +08:00
2569718930@qq.com 747c8aa401 feat: tune scan dashboard and telegram monitor 2026-05-15 14:36:43 +08:00
2569718930@qq.com e7fc3c97cb feat: add scan terminal dashboard components, monitoring panels, and associated utility hooks 2026-05-15 12:52:39 +08:00
2569718930@qq.com 1a284c9990 跑道观测卡片移除机场报文展示 2026-05-15 04:37:40 +08:00
2569718930@qq.com 77488e561a 跑道观测面板每 60 秒自动刷新观测时间 2026-05-15 04:36:45 +08:00
2569718930@qq.com c2a78e7b62 Dockerfile 启用 BuildKit 缓存挂载加速构建 2026-05-15 04:25:57 +08:00
2569718930@qq.com 3845432ac2 测试 mock 方法名同步为 _amsc_http_get_json 2026-05-15 04:20:52 +08:00
2569718930@qq.com c031dd0e28 prewarm 加 --force-refresh 确保按时拉取最新 AMSC 跑道数据 2026-05-15 04:15:58 +08:00
2569718930@qq.com c6f1eac6c1 跑道观测面板添加移动端响应式布局(768px / 480px 断点) 2026-05-15 04:04:54 +08:00
2569718930@qq.com f47ea93ec1 AMSC 重命名 _http_get_json 为 _amsc_http_get_json,避免 MRO 被 WeatherDataCollector 的同名方法覆盖 2026-05-15 03:47:15 +08:00
2569718930@qq.com 856e9aa6d1 AMSC 添加 stderr 调试输出,确认 urllib 代码路径被执行 2026-05-15 03:42:14 +08:00
2569718930@qq.com e981188bf8 AMSC 请求从 httpx 改为 urllib,彻底绕过代理干扰 2026-05-15 03:33:06 +08:00
2569718930@qq.com d8639f40f9 AMSC 请求绕过代理 trust_env=False,修复容器内 SSL 验证失败 2026-05-15 03:19:33 +08:00
2569718930@qq.com 6655476fd4 AMSC SSL 硬编码 verify=False,跳过证书验证环境变量依赖 2026-05-15 03:13:30 +08:00
2569718930@qq.com 06df0a87dd AMSC 请求添加调试日志,排查 SSL verify 状态 2026-05-15 03:03:26 +08:00
2569718930@qq.com 00400f1392 AMSC 请求添加 app: AMS header,对齐浏览器完整请求头 2026-05-15 02:49:34 +08:00
2569718930@qq.com bbb47b1634 AMSC sessionId 从 Cookie 改为自定义 header,对齐浏览器请求格式 2026-05-15 02:46:56 +08:00
2569718930@qq.com 189a77d03d AMSC SSL 改用 httpx.Client(verify=False) 确保跳过证书验证 2026-05-15 02:42:03 +08:00
2569718930@qq.com 0f7322dbd8 feat: add RunwayObservationsPanel component to display AMSC airport weather data 2026-05-15 02:28:26 +08:00
2569718930@qq.com 9f7bbffca1 AMSC AWOS 请求添加 SSL 验证开关,兼容国内证书 2026-05-15 02:25:33 +08:00
2569718930@qq.com 9868784494 移除跑道观测面板刷新按钮,数据随面板加载自动拉取 2026-05-15 02:21:54 +08:00
2569718930@qq.com d4892a5294 feat: add RunwayObservationsPanel component for tracking major Chinese airport runway temperatures 2026-05-15 02:15:03 +08:00
2569718930@qq.com e590150fa9 补全 scan/terminal/overview Next.js API route handler,修复生产 404
MarketOverviewBanner 改为使用 fetchBackendApi() 统一调用模式,
同时创建缺失的 app/api/scan/terminal/overview/route.ts 代理到后端。
其他 scan/terminal 子路由 (ai, ai-city, stream) 已有对应 handler。
2026-05-15 02:08:12 +08:00
2569718930@qq.com 644b592fe8 修复 MarketOverviewBanner 未使用 fetchBackendApi 导致生产环境 404
组件内裸 fetch() 请求到 Vercel 前端域名,缺少路由 handler 返回 404。
统一使用 fetchBackendApi() 走后端代理,其他 scan-terminal 组件已正确使用。
2026-05-15 02:01:41 +08:00
2569718930@qq.com 4cc579ccb3 feat: add AMSC runway observations 2026-05-15 01:41:49 +08:00
2569718930@qq.com c4b1844a67 fix: stabilize pro checkout loading 2026-05-15 00:58:40 +08:00
2569718930@qq.com f9154ff0f2 修复 Istanbul/Ankara MGM 实测链接指向机场站点页面
Istanbul: mgm.gov.tr → mgm.gov.tr/?il=Istanbul&ilce=Istanbul Havalimani
    Ankara:   mgm.gov.tr → mgm.gov.tr/?il=Ankara&ilce=Esenboga
2026-05-15 00:41:53 +08:00
2569718930@qq.com c96a630d1b 账户页添加本地错误边界,支付崩溃时提示钱包冲突排查
用户反馈点击"立即订阅并激活服务"后页面崩溃。
    新增 account/error.tsx 本地错误边界,支付流程出错时提示:
    - 常见原因:多钱包插件冲突(MetaMask + Rabby 同时开启)
    - 建议操作:关闭其他钱包插件后刷新重试

    Error boundary is scoped to /account route only, won't affect dashboard.

    Scope-risk: LOW — TypeScript 零错误
    Tested: npx tsc --noEmit (0 errors)
2026-05-15 00:35:34 +08:00
2569718930@qq.com 3bca2093f0 修复高频机场数据源:AMOS接入airport_primary、Taipei CWA补充mgm_nearby、Paris AROME缓存
- Seoul/Busan: AMOS 跑道传感器纳入 airport_primary 链路(MADIS之后、MGM之前)
    - Seoul/Busan: 跑道温度生效时同步更新 current_obs_time,去重不再依赖慢速METAR
    - Taipei: 新增 _attach_cwa_settlement_nearby,将CWA数据注入 mgm_nearby(icao=RCSS)
    - Paris: AROME HD 抓取新增 10分钟内存缓存(模型15分钟更新),obs_time 为空时补 UTC 时间
    - Istanbul/Ankara MGM 链接修复为直接跳转机场站点页面

    Scope-risk: LOW — 170 测试通过,ruff 零告警
    Tested: python -m pytest -q (170 passed), ruff check .
2026-05-15 00:10:03 +08:00
2569718930@qq.com 2ee00f8016 MiMo AI 能力扩展:TAF解读、概率分布解读、异常检测、市场概览
AI 解读字段扩展:
    - 新增 taf_read_zh/en:解读机场预报中影响今日峰值窗口的变化
    - 新增 probability_read_zh/en:描述概率分布形态(最高桶、偏左/偏右)
    - stream max_tokens 900→1200 容纳新输出字段
    - 缓存 key 简化为 METAR原文+观测时间,大幅提升命中率
    - 兜底函数补全 TAF 和概率字段的确定性生成

    异常检测:
    - 纯数学计算,零 AI 延迟:实测温度 vs 全部模型预测上下限
    - 三级告警:breakout_above / breakout_below / deviation

    市场概览:
    - 新增 POST /api/scan/terminal/overview(MiMo 批量解读,缓存10分钟)
    - 前端 MarketOverviewBanner 可折叠横幅(顶栏与标签栏之间)
    - 移动端适配 640px/768px 断点,暗色/亮色双主题

    Scope-risk: MEDIUM — 170 测试通过,TypeScript 零错误,ruff 零告警
    Tested: python -m pytest -q (170 passed), npx tsc --noEmit (0 errors), ruff check .
2026-05-14 22:41:31 +08:00
2569718930@qq.com 6c08a68413 @
性能与用户体验全面优化

    前端性能:
    - 移除 Three.js 依赖(~600KB),天气粒子改为纯 CSS 动画 + Canvas 2D
    - Google Fonts 切换为 next/font 自托管,消除跨域字体请求
    - 合并 ScanTerminalLightTheme.module.css (37KB) 到主 CSS,亮/暗主题统一用 CSS 变量
    - 新增 /api/dashboard/init 聚合端点,首次加载 4 次往返 → 1 次
    - 添加 Service Worker 静态资源缓存,修复 PWA manifest 配置

    用户体验:
    - 新增全局错误边界 error.tsx / global-error.tsx,崩溃不再白屏
    - 决策卡和城市详情的更新时间改为相对时间("15秒前"),每秒自动刷新
    - 数据陈旧时状态标签从青色切换为琥珀色提示

    DEB 算法增强:
    - 市场扫描路径接入 Open-Meteo 多模型数据(ECMWF/GFS/ICON/JMA/HRDPS 等)
    - MAE 计算加入时间衰减(decay_factor=0.85),近期模型误差权重更高

    Scope-risk: MEDIUM — 全量 170 测试通过,前端 TypeScript/build 通过,ruff 零告警
    Tested: python -m pytest -q (170 passed), npx tsc --noEmit (0 errors), npm run build (success), ruff check .
@
2026-05-14 21:31:05 +08:00
2569718930@qq.com 37494a7192 @
将 web/routes.py 拆分为模块化 router + service 架构

    - 新增 web/app_factory.py 集中注册 7 个域名 router
    - 新增 web/routers/ 薄壳路由层(auth/city/system/scan/ops/payments/analytics)
    - 新增 web/services/ 业务函数下沉(每域独立 service 文件)
    - web/routes.py 缩减为 city_runtime 的兼容重导出 facade
    - analysis_service.py/app.py 适配新入口并清理冗余导入

    Scope-risk: LOW — 全量 170 测试通过,router 注册顺序与原路由一致
    Tested: python -m pytest -q (170 passed), ruff check . (All checks passed)
@
2026-05-14 20:01:26 +08:00
2569718930@qq.com a79abc02de 清理已移除服务的残留环境变量和测试引用
- .env.example:移除 PROMETHEUS/ALERTMANAGER/GRAFANA/ALERT_RELAY 端口配置
- .env.example:移除 TELEGRAM_ALERT_* 市场提醒配置
- test_bot_runtime_coordinator:移除 trade_alert_push 断言
2026-05-14 18:45:34 +08:00
2569718930@qq.com f4a37e4bdd 移除 market_alert_engine 对应的测试文件
该测试文件引用已删除的 src.analysis.market_alert_engine 模块。
2026-05-14 18:42:41 +08:00
2569718930@qq.com 5617e5b112 拆分 FutureForecastModalContent:提取 TodayLayout 组件
- 新建 FutureForecastTodayLayout:封装今日视图双栏布局(左侧卡片+右侧图表)
- FutureForecastModalContent 从 1098 行降至 999 行
- 纯 JSX 提取,不修改任何业务逻辑、状态或数据流

Tested: npx tsc --noEmit ✓
2026-05-14 18:39:10 +08:00
2569718930@qq.com eb056a890b 移除 PolyWeather 市场提醒功能及监控基础设施
- 删除 market_alert_engine.py:交易预警引擎
- 删除 alertmanager_telegram_relay.py:Alertmanager 到 Telegram 转发
- 移除 telegram_push.py 中市场监控推送循环
- 移除 runtime_coordinator.py 中 trade_alert_push 协程
- 移除 docker-compose.yml 中 Prometheus/Alertmanager/Grafana 服务
- 移除 monitoring/ 目录:prometheus/alertmanager/grafana 配置

Tested: ruff check . ✓
2026-05-14 18:23:39 +08:00
2569718930@qq.com 5ad89b4c29 移除未使用的 HistoryModal 历史对账组件
该组件未被任何地方引用,属于死代码。同时移除关联 CSS Module
及 scan-root-styles.ts 中的 barrel 注册。
2026-05-14 18:13:41 +08:00
2569718930@qq.com ac90ef9206 修复 CSS Module spin 动画::global(spin) 改为本地 @keyframes spin
PostCSS 将 :global(spin) 解析为伪元素 :: 导致 Vercel 构建失败。
改用在每个 CSS Module 中定义本地 @keyframes spin。
2026-05-14 18:06:53 +08:00
2569718930@qq.com c8103179e1 修正市场监控新高提醒:统一数据源并修复 HKO/跑道城市逻辑
- resolveMaxSoFar:HKO 城市优先使用 current.max_so_far(天文台结算锚点)
- trendClass:跑道城市跳过比较(跑道表面温度 vs 空气温度无意义)
- newHigh badge / audio alert:跑道城市屏蔽新高判断
- 所有调用处传入 key 参数确保 HKO fallback 生效
2026-05-14 17:51:08 +08:00
2569718930@qq.com 4f13faa311 优化日内温度曲线图加载性能并修复图标旋转动画
- AiCityTemperatureChart:React.memo 包裹 + useMemo 依赖移除 detail 避免每帧重算
- ScanTerminalCard 等 9 个 CSS Module:animation: spin 改为 :global(spin) 修复 CSS Modules 作用域问题
2026-05-14 17:31:25 +08:00
2569718930@qq.com 8dd9aa59b3 接入新加坡 MSS 1分钟实时温度及 MGM/JMA/FMI/KNMI 高频源到 airport_primary
- 新建 singapore_mss_sources.py:拉取 data.gov.sg 1分钟干球温度(S24 樟宜站)
- country_networks.py:_airport_primary_from_raw 新增 MGM/JMA/FMI/KNMI/SG_MSS 分支
- weather_sources.py:注入 jma_current/fmi_current/knmi_current/singapore_mss_current
- 前端 MonitorPanel:resolveSourceLabel 根据 airport_primary.source_code 显示数据源标签
- 文档:更新 AIRPORT_REALTIME_SOURCES.md 新增新加坡

Tested: ruff check . ✓  npx tsc --noEmit ✓
2026-05-14 17:12:11 +08:00
2569718930@qq.com 96676e7097 修正市场监控温度数据源:接入 NOAA MADIS 5分钟高频数据并优化展示
- 首尔/釜山:隐藏大号跑道温度值,改为"跑道温度"标签(跑道表面温度 ≠ 空气温度)
- US 城市:MADIS HFMETAR 5分钟小数温度接入 airport_primary,前端优先读取
- 其他城市:整数值不再强制 toFixed(1) 追加虚假 .0 精度
- 后端:weather_sources.py 注入 madis_hfmetar_current 到 results
- 后端:country_networks.py 的 _airport_primary_from_raw 新增 MADIS 优先分支
- 文档:更新 AIRPORT_REALTIME_SOURCES.md 新增 11 个 US 城市
- 文档:更新 CLAUDE.md 补充市场监控、高频数据管道、Country Network Provider 架构

Tested: npx tsc --noEmit ✓  ruff check . ✓
2026-05-14 16:00:55 +08:00
2569718930@qq.com c9d03fd3e1 feat: implement dashboard state management, API proxy layer, and modular UI components for weather monitoring and payments 2026-05-14 15:05:13 +08:00
2569718930@qq.com 02add6d994 feat: implement city weather detail API routing, dashboard data models, and monitoring infrastructure 2026-05-14 14:21:28 +08:00
2569718930@qq.com 0e220d890f feat: add component styles for future forecast modal v2 2026-05-14 02:55:16 +08:00
2569718930@qq.com 4bda402270 feat: implement MonitorPanel component for real-time weather monitoring and temperature tracking 2026-05-14 02:47:37 +08:00
2569718930@qq.com 8bfe23138c feat: implement MonitorPanel component with real-time airport weather tracking and concurrency-controlled polling 2026-05-14 02:41:51 +08:00
2569718930@qq.com f5acae3c81 feat: implement ScanTerminalDashboard with integrated monitoring, paywall, and AI-driven city analysis features 2026-05-14 02:32:56 +08:00
2569718930@qq.com 294ac30026 feat: add MonitorPanel component for real-time airport weather tracking and automated refresh coordination 2026-05-14 02:15:49 +08:00
2569718930@qq.com fb4ba622a8 feat: implement MonitorPanel component for tracking real-time airport weather data with automated concurrency and stale-data prioritization 2026-05-14 02:02:30 +08:00
2569718930@qq.com d7d1744313 feat: implement MonitorPanel dashboard component with status indicators and grid view 2026-05-14 02:00:00 +08:00
2569718930@qq.com 357e01be3c feat: add monitor panel component with consolidated CSS root styles 2026-05-14 01:51:55 +08:00
2569718930@qq.com 3529d97d58 移除国内城市(NMC并非机场温度,无用) 2026-05-14 01:41:16 +08:00
2569718930@qq.com 329e3afe38 监控页新增 7 个国内城市(上海/北京/成都等,NMC 5min数据) 2026-05-14 01:37:37 +08:00
2569718930@qq.com 6eca1da3f9 修复测试:aurora→denver 城市 key 重命名后更新断言 2026-05-14 01:27:04 +08:00
2569718930@qq.com 4ab749c136 市场监控新增 11 个美国城市(NY/LA/Chicago/Denver 等) 2026-05-14 01:17:09 +08:00
2569718930@qq.com d2472fa0c2 市场监控 1 分钟强制刷新 11 城数据 2026-05-14 01:13:53 +08:00
2569718930@qq.com 6c4f43fba6 Aurora→Denver 重命名 + 接入 MADIS HFMETAR 5 分钟数据源 2026-05-14 01:09:17 +08:00
2569718930@qq.com 79944432fb MonitorPanel 自动触发未加载城市的 ensureCityDetail 2026-05-14 00:52:14 +08:00
2569718930@qq.com c006d13fea MonitorPanel 从独立 API 改为复用 DashboardStore 数据,砍掉 /api/m 和 /m/json 2026-05-14 00:43:19 +08:00
2569718930@qq.com 7a28810f25 监控页刷新间隔 30s → 60s 2026-05-14 00:32:30 +08:00
2569718930@qq.com c5fdb93ae5 监控页:后端 30s 缓存 + 前端 loading 态 2026-05-14 00:29:51 +08:00
2569718930@qq.com 2202f8e211 砍掉 /m HTML 页面,只保留 /m/json 给 React 组件用 2026-05-14 00:23:03 +08:00
2569718930@qq.com fc48795299 监控页从 iframe 改为原生 React 组件:无加载延迟,30s JSON 轮询 2026-05-14 00:18:26 +08:00
2569718930@qq.com fd12c15518 Monitor tab 中文名+iframe 常驻避免重新加载 2026-05-14 00:13:32 +08:00
2569718930@qq.com 22b414f69e 监控页时间改为用户本地时间(JS toLocaleTimeString) 2026-05-14 00:10:43 +08:00
2569718930@qq.com 5e653c2ec1 前端接入监控页面:新增 Monitor 标签页 + /api/m 代理 2026-05-14 00:04:26 +08:00
2569718930@qq.com 9dd59ceb4e 修复 ruff 风格:多行语句、bare except → except Exception 2026-05-13 23:55:54 +08:00
2569718930@qq.com a9b1449680 放弃 Jinja2,改用 f-string 直接拼 HTML,零模板引擎依赖 2026-05-13 23:54:53 +08:00
2569718930@qq.com a7ba7aed3a Debug:简化 context 排查 Jinja2 500 2026-05-13 23:46:18 +08:00
2569718930@qq.com 1240fbb673 跑道数据改用 tuple 避免 Jinja2 dict unhashable 错误 2026-05-13 23:41:31 +08:00
2569718930@qq.com d56889c2e5 修复 Jinja2 模板 dict 访问方式和条件表达式 2026-05-13 23:38:06 +08:00
2569718930@qq.com 1fba498c37 添加 jinja2 依赖 2026-05-13 23:28:52 +08:00
2569718930@qq.com b94037f328 路由 /monitor → /m,URL 更短 2026-05-13 23:24:26 +08:00
2569718930@qq.com 1493c72138 删除 Rust 监控项目及旧文档,已用 Python 重写替代 2026-05-13 23:22:13 +08:00
2569718930@qq.com a221b3e969 用 Python 重写市场监控网页版:FastAPI+Jinja2+HTMX,复用 _analyze() 2026-05-13 23:19:49 +08:00
2569718930@qq.com bfffbdedd0 恢复模板中被吃掉的跑道数据显示区块 2026-05-13 22:07:36 +08:00
2569718930@qq.com 26fff84c3a 修复跑道数据:用 LIKE 查询所有 RKSI_RWY_%,不再硬编码索引 2026-05-13 22:00:00 +08:00
2569718930@qq.com 7038b14904 网页版卡片时间改用 obs_time(数据源时间戳),支持 ISO/epoch/Naive 多种格式解析 2026-05-13 21:47:55 +08:00
2569718930@qq.com c9006f250f 网页版卡片时间改为观测数据时间(基于 created_at),不再用当前本地时间 2026-05-13 21:45:05 +08:00
2569718930@qq.com 1a4d75c126 推送消息时间改为观测数据时间,不再用当前本地时间 2026-05-13 21:43:26 +08:00
2569718930@qq.com 7555da8e6a 更新html 2026-05-13 21:26:26 +08:00
2569718930@qq.com d97bb76480 移除 city_daily_max 表及 upsert 逻辑,日最高已改用 intraday_path_snapshots_store 2026-05-13 21:14:28 +08:00
2569718930@qq.com c779b20a4f 日最高改用 intraday_path_snapshots_store(已有实时数据,无需等 Docker rebuild) 2026-05-13 21:10:39 +08:00
2569718930@qq.com cac84dcc0c 修复今日最高:新增 city_daily_max 表,所有数据源写入日最高
- DB: city_daily_max(icao,max_temp,obs_date,max_time) upsert
- AMOS/JMA/KNMI/FMI/HKO/METAR集群/CWA 采集后均写入
- Rust: 读 city_daily_max 获取准确日最高 + 时间
- 卡片恢复 High 行,显示最高温 + 达成时间
- new_high 提醒也基于真实日最高重新生效

Tested: cargo build + ruff check 均通过
2026-05-13 20:50:46 +08:00
2569718930@qq.com e734c93b34 移除不准确的今日最高,改为按当前温度从高到低排序
- airport_obs_log 仅保留 2h 数据,max_so_far 不准确
- 去掉卡片 Today High 行,趋势箭头移到 Obs 同行
- 卡片按 current_temp 降序排列,无数据沉底
- 通知文案简化
2026-05-13 20:44:09 +08:00
2569718930@qq.com 067ee63ba4 首尔/釜山跑道数据:Python 存 RKSI_RWY_0/1,Rust 查询展示
- weather_sources: AMOS 采集时每条跑道单独写 airport_obs_log
- Rust: 按 RKSI_RWY_0、RKSI_RWY_1 等 icao 查询跑道温度
- 跑道标签 18L/36R、18R/36L 写死在 config 中
2026-05-13 20:39:15 +08:00
2569718930@qq.com 88918677c0 监控页面:英文城市名 + 新高浏览器提醒(可开关)
- 卡片标题改为英文名(Seoul/Busan/Tokyo 等)
- 温度突破今日最高 0.3°C 时触发浏览器 Notification
- 右上角 🔔 按钮开关提醒,状态存 localStorage
- 同日同城同一温度不重复提醒(notify_highs 去重)
- 页面标题和标签全英文化

Tested: cargo build --release 通过
2026-05-13 20:33:59 +08:00
2569718930@qq.com 041c9fb4fa 撤回 METAR 兜底,改走 CWA 10min 实时数据(需配 API key) 2026-05-13 20:20:16 +08:00
2569718930@qq.com 1d3ccbb7cf 台北走 METAR 集群兜底:ICAO 从 CWA 站号 466920 改为 RCSS
- weather_sources: 放开 cwa settlement_source 的 METAR 拦截
- Rust: ICAO 改用 RCSS,匹配 METAR 集群数据
- 原因: CWA_OPEN_DATA_AUTH 未配置,CWA 接口静默失败致 airport_obs_log 无数据
2026-05-13 20:17:12 +08:00
2569718930@qq.com 8dd212f0af 卡片放大:3列网格、52px温度大字、加宽间距、整体放大 ~30% 2026-05-13 20:05:16 +08:00
2569718930@qq.com ded09df749 监控页面布局对齐文档:时区后缀、暖色高温、新高紫光、趋势同行
- 当地时间加时区缩写(KST/JST/HKT 等)
- 温度 >= 30°C 暖橙色高亮
- 新高标记:紫色边框 + 紫色温度数字
- "今日最高" 与趋势箭头同行,对齐 docs 卡片布局
- CSS: temp-value.warm, new-high-card/high-val 样式
2026-05-13 20:03:18 +08:00
2569718930@qq.com 239c9c446d 监控页面增强:温度变化闪绿光、观测N分钟前、更新戳可见
- 每张卡片加"观测 X 分钟前"时间感知
- 温度值变化时卡片边框闪绿光(HTMX afterSwap 事件)
- 更新时间戳移入 HTMX 局部刷新区,30s 刷新可见
- CSS 加 obs-age 样式
2026-05-13 19:58:19 +08:00
2569718930@qq.com 538cc7db2d 监控页面刷新间隔 60s → 30s 2026-05-13 19:53:13 +08:00
2569718930@qq.com 6055f8328e 修正 DB 路径:/var/lib/polyweather/polyweather.db(非 data/ 本地副本) 2026-05-13 19:51:25 +08:00
2569718930@qq.com 611811101b 修正 service DB 路径为 /root/PolyWeather/data/polyweather.db 2026-05-13 19:39:26 +08:00
2569718930@qq.com 9e80719cbe 添加 market-monitor systemd service 文件 2026-05-13 19:34:21 +08:00
2569718930@qq.com db9071101a @
添加市场监控 Rust 网页版:Axum+Askama+HTMX,直读 SQLite

- 11 城市卡片网格,暗色主题,响应式布局
- 60s HTMX 轮询局部刷新
- 当前温度、今日最高、趋势箭头(线性回归)
- 首尔/釜山跑道温度(预留)
- 零 Python 依赖,编译后 ~3MB 二进制

Constraint: 读 POLYWEATHER_DB_PATH 指向的 SQLite
@
2026-05-13 19:24:24 +08:00
2569718930@qq.com 86467d4e92 HK/LFS 数据延迟重试:obs_time 未变且距上次推送超 9min 时等 4s 重拉
HKO API 在 x7 分发布数据但有 3-5s 延迟,推送检查可能刚好在
API 更新前拿到旧数据被去重跳过。现在检测到数据过期时等待重拉。

Constraint: 仅影响 hong kong / lau fau shan,其他城市无额外延迟
2026-05-13 15:29:28 +08:00
2569718930@qq.com 1680bd5871 香港/流浮山推送间隔改为 60s:obs_time去重防重复,x7分放数据即时捕获
HKO 每 10min 在 x7 分(07/17/27…)发布数据,600s 间隔容易与
发布时刻错位导致延迟一整轮。改为 60s 轮询,obs_time 去重机制
保证同一观测不重复推送。
2026-05-13 15:08:34 +08:00
2569718930@qq.com 6802f647b3 Busan 高温窗口 fallback:peak 偏窄(13-14)时拓宽到 12-16
沿海城市受海风影响,Open-Meteo 算出的 peak 窗口仅 1 小时,
导致 time_ok 在 16:01 就关窗。加 _AIRPORT_PEAK_FALLBACK,
last_h - first_h < 3 时用 fallback 值。

Constraint: 仅影响 Busan,其他城市无 fallback 保持原逻辑
2026-05-13 15:04:54 +08:00
2569718930@qq.com 557882600e 添加市场监控频道 Rust 独立网页版方案文档
直读 SQLite airport_obs_log,零 Python 依赖,Axum+Askama+HTMX 纯展示
2026-05-13 14:42:19 +08:00
2569718930@qq.com 40f231b76d 优化 Vercel Fluid CPU:API加CDN缓存、缩小middleware范围、跳过公共API的Supabase身份转发
- 7条GET路由加 s-maxage + stale-while-revalidate,fetch 改为 next revalidate
- middleware matcher 从全匹配缩小到9条需auth的路径,移除 isStaticAsset
- 8条公共API路由加 includeSupabaseIdentity: false
- backend-auth getSession 为空时跳过 getUser
- subscription-help 加 I18nProvider,移除 force-dynamic → 静态预渲染
- next.config.mjs 加静态资源 immutable 缓存头

Tested: npx tsc --noEmit 通过,npm run build 通过
2026-05-13 14:23:04 +08:00
2569718930@qq.com 30b289ec8f 修复机场高频推送:东京ICAO不匹配、釜山缺趋势数据、港/流浮频率及obs_time去重
- 东京 HIGH_FREQ_AIRPORT_ICAO 从 RJTT 改为 JMA 站号 44166
- 釜山 METAR 集群数据写入 airport_obs_log 供趋势检测
- 香港/流浮山推送间隔从 60s 改为 600s(匹配实际 10min 更新)
- 新增 obs_time 去重:同一观测数据不重复推送

Constraint: JMA AMeDAS 使用站号而非 ICAO 作为站点标识
Tested: ruff check 通过
2026-05-13 13:45:00 +08:00
2569718930@qq.com 9adcfdb203 放宽时间窗口:peak-4h 到 peak+2h(6小时) 2026-05-13 01:13:07 +08:00
2569718930@qq.com cfb746300a 推送改为三条件高温窗口:时间 + 温度 + 趋势
1. 时间窗口:DEB 预测峰值时刻前后(peak_hour-2h 到 peak_hour+1.5h)
2. 温度窗口:按地形分三类(大陆 3.0°C / 海洋 2.0°C / 强海风 1.5°C)
3. 趋势窗口:最近 30-60 分钟持续升温(最后 3 条递增或当前 > 30min 前)

三个条件同时满足才推送。流浮山移除结算温度行。
2026-05-13 01:10:31 +08:00
2569718930@qq.com a173da1b00 推送判断改为使用机场站点温度,统一判断与展示数据源
之前 proximity 检查用 city_weather.current.temp(通用温度/METAR),
但消息展示用 mgm_nearby 机场站点温度,两者不一致导致推送窗口错位。
现在统一从 mgm_nearby/amos 提取机场温度用于判断。
2026-05-13 00:54:19 +08:00
2569718930@qq.com 7372b7a73f 接入台北松山 RCSS CWA 10分钟实时温度;HKO 显示结算温度
台北通过现有 CWA 开放数据 API(站号 466920)接入高频推送。
香港/流浮山消息新增"结算温度"行,显示向下取整后的结算值。

Tested: pytest 176 passed, ruff check 通过
2026-05-13 00:47:58 +08:00
2569718930@qq.com 8a8a47e31d 首尔釜山改为10分钟推送;当前温度破日高时加新高标记
AMOS 1分钟数据改为每10分钟推送一次避免刷屏。
当当前温度超过今日实测最高≥0.3°C时,标题行追加🔶新高标记。
2026-05-13 00:38:05 +08:00
2569718930@qq.com fe9bf61ad6 接入香港天文台 HKO + 流浮山 LFS 1分钟实时温度
HKO 公共天气 API 免费无注册,提供 1 分钟温度 CSV。
两个站点加入高频推送,与首尔/釜山同级。

Station: HK Observatory (HKO) 27.0°C, Lau Fau Shan (LFS) 25.9°C
Interval: 60s

Tested: pytest 176 passed, HKO API 实测通过
2026-05-13 00:33:56 +08:00
2569718930@qq.com 7084bdf1ec 修复首尔/釜山跑道温度偶发不展示:过滤 None 值并增加回退
AMOS 部分跑道对温度可能为 None(传感器暂时不可用),
之前直接 format None 导致静默跳过整组跑道显示。
改为只展示有效跑道对,全部不可用时回退到当前实测。

Tested: pytest 176 passed, AMOS 实测 runway_pairs=4 temperatures 2/4 valid
2026-05-13 00:07:46 +08:00
2569718930@qq.com 470cd6c95f 巴黎温度行标注"AROME预报"以区分于站点实测
八城中仅巴黎为模型数据,其余七城为机场站点实测。
消息中明确标注避免混淆。

Tested: pytest 176 passed, ruff check 通过
2026-05-13 00:01:38 +08:00
2569718930@qq.com 6f83b9d7ea 接入巴黎 Le Bourget AROME HD 15分钟模型预报数据
Météo-France 站点实测无法获取,改为通过 Open-Meteo 取 AROME France HD
15分钟模型温度。非实测数据,标注为模型预报类型。
巴黎跳过 obs_log 峰值检测(无站点观测日志)。

Constraint: AROME 为模型格点值非实测,与 AMOS/JMA/FMI 精度不同
Tested: pytest 176 passed, AROME API 实测返回 16.2°C
2026-05-12 23:59:06 +08:00
2569718930@qq.com a3bef5185e 更新浏览器插件文档和版本:移除 Wunderground 引用,补充机场实时数据源
README 更新为当前数据源列表(AMOS/JMA/MGM/FMI/KNMI),
删除已废弃的 Wunderground/Manila/Karachi 引用。
版本升至 0.1.11。

Tested: ruff check 通过
2026-05-12 22:44:02 +08:00
2569718930@qq.com 963e717523 新增机场高频实时数据源参考文档 docs/AIRPORT_REALTIME_SOURCES.md
汇总已接入 7 城的数据源、频率、推送机制、消息模板和未接入原因。
2026-05-12 21:17:42 +08:00
2569718930@qq.com a29ebfae27 今日实测最高改用 airport_current.max_so_far(METAR历史观测)
项目已有 airport_current 字段存储 METAR/AMOS 当天最高温及时间,
无需从 obs_log 自建。obs_log 保留期恢复为 2 小时。

Tested: pytest 176 passed, ruff check 通过
2026-05-12 21:15:57 +08:00
2569718930@qq.com b1d0e41fd4 今日实测最高改为从 airport_obs_log 取值,延长保留至 24h
不再依赖 city_weather.current.max_so_far(可能来自 METAR 等非机场源),
直接从 airport_obs_log 的机场站点观测历史中计算当日最高温及时间。
obs_log 保留期从 2h 延长到 24h 以支撑跨天查询。

Tested: pytest 176 passed, ruff check 通过
2026-05-12 21:12:53 +08:00
2569718930@qq.com c8eed2f315 统一机场消息模板:英文名/机场 + 当前实测 + DEB预报 + 实测最高
所有城市改为统一三段式:
Seoul / Incheon 16:03
当前实测:14.6°C
今日DEB预报最高:18.2°C
今日实测最高:16.5°C(15:30)

首尔/釜山保留跑道对温度展示。

Tested: pytest 176 passed, ruff check 通过
2026-05-12 21:08:43 +08:00
2569718930@qq.com f7a453e39b 机场消息新增当日已出现最高温及时间
每城显示三行:当前温度、日内最高+时间、DEB 预测。
所有六城统一格式。

Tested: pytest 176 passed, ruff check 通过
2026-05-12 21:04:48 +08:00
2569718930@qq.com 191be9c5fd 机场温度取值增加 METAR 兜底:避免高频源不可用时出现空行
当 KNMI/FMI/JMA/MGM 数据未就绪时回退到 current.temp,
确保消息至少显示一个温度值而非空白。

Constraint: 兜底温度为 METAR 整数精度,待高频源恢复后自动切回小数
Tested: ruff check 通过
2026-05-12 21:01:04 +08:00
2569718930@qq.com 12c2b39b16 接入伊斯坦布尔机场 MGM 17058 高频推送
伊斯坦布尔新机场 (LTFM, MGM 17058) 加入 10 分钟级推送队列,
obs_log 已有 MGM 数据覆盖,只需加入高频城市列表。

马德里 AEMET 注册地址:https://opendata.aemet.es/centrodedescargas/registro

Tested: pytest 176 passed, ruff check 通过
2026-05-12 20:48:02 +08:00
2569718930@qq.com 84e7518b04 修复安卡拉温度取值错误:精确匹配 MGM 17128 机场站
mgm_nearby 包含安卡拉 26 个站点的混合列表,直接取 [0] 可能拿到非机场站。
改为按 ICAO/istNo 精确匹配 17128,其余城市回退到 [0]。

Constraint: 东京/赫尔辛基/阿姆斯特丹的 mgm_nearby 只有一条,不受影响
Tested: pytest 176 passed, ruff check 通过
2026-05-12 20:40:44 +08:00
2569718930@qq.com 209027afb3 机场消息加英文城市名,时间移到标题行
格式变更:英文名 + 中文标签 + 当地时间放第一行,
温度单独一行,更简洁。

Constraint: 跑道对城市(首尔/釜山)同样受益
Tested: pytest 176 passed, ruff check 通过
2026-05-12 20:35:02 +08:00
2569718930@qq.com 83d2de5438 修复非 AMOS 城市温度显示为整数:改用机场站点温度替代 METAR
city_weather.current.temp 来自 METAR(整数),改为从 mgm_nearby[0].temp
取机场高频数据源的站点温度(小数精度)。JMA/FMI/KNMI/MGM 源均保留一位小数。

Constraint: 首尔/釜山走 runway_temps 已有精度,不受影响
Tested: pytest 176 passed, ruff check 通过
2026-05-12 20:17:32 +08:00
2569718930@qq.com 4d584f4828 推送频率改为按数据源原生速率:AMOS 1分钟,其余 10分钟
不再统一 2 分钟,各城市按实际数据刷新周期独立推送:
首尔/釜山 60s,东京/安卡拉/赫尔辛基/阿姆斯特丹 600s。
循环轮询间隔降至 60s 以匹配最快频率。

Constraint: 循环每 60s 跑一轮,但 interval 判断让各城市按自有节奏推送
Tested: pytest 176 passed, ruff check 通过
2026-05-12 20:13:15 +08:00
2569718930@qq.com 5d630bb910 接入 KNMI 阿姆斯特丹史基浦机场 10 分钟数据源
通过 KNMI Open Data API 获取 Schiphol 机场 10 分钟观测数据(NetCDF 格式)。
需设置 KNMI_API_KEY 环境变量。Docker 镜像新增 libhdf5-dev/netCDF4 依赖。

Constraint: KNMI API key 为 JWT 格式,通过 Authorization header 传递
Scope-risk: 中高,新增系统依赖 libhdf5-dev + netCDF4
Tested: pytest 176 passed, KNMI API 连通性验证通过
2026-05-12 20:10:06 +08:00
2569718930@qq.com a32a49f3c7 接入 FMI 赫尔辛基-万塔机场 10 分钟实时数据源
新增 fmi_sources.py,通过 FMI Open Data WFS API 获取 Helsinki-Vantaa
机场观测数据(FMISID 100968, WMO 2974)。每 10 分钟更新,参数含温度/
风速/气压。免费无需 API key。集成至高频机场推送体系。

Constraint: FMI WFS 无 rate limit,无需注册
Scope-risk: 中,新增数据源
Tested: pytest 176 passed, FMI 实测 temp=13.1°C wind=9.5kt pressure=1000.9hPa
2026-05-12 19:51:32 +08:00
2569718930@qq.com 203094a97c 修复测试:移除 momentum_spike 断言 + Python 3.8 类型兼容
build_trading_alerts 已移除 momentum_spike 规则,对应测试也更新。
get_airport_obs_recent 返回类型改用 List[Dict] 兼容 3.8。

Tested: pytest 176 passed
2026-05-12 19:13:06 +08:00
2569718930@qq.com a5522b4b16 推送窗口改为 DEB 曲线动态判断,替代固定 08:00-20:00
当前温度距 DEB 预测最高 ≤3°C 时开始推送,一旦确认已过峰值
(近 1h 内最高已过且回落 >0.5°C)自动停止。不再依赖固定时段。

Constraint: DEB 预测本身就是每日最高温,比时钟判断更贴合实际峰值
Scope-risk: 中,核心推送逻辑变更
Tested: ruff check 通过
2026-05-12 19:08:44 +08:00
2569718930@qq.com 9026ccf4c0 机场推送间隔改为 2 分钟,适配峰值期高频需求
默认 120s per-city 独立推送,可通过 TELEGRAM_AIRPORT_PUSH_INTERVAL_SEC 覆盖。
最短允许 30s,避免过于频繁。

Constraint: _analyze 内部缓存 TTL 可能长于 2min,同数据可能重复推送
Scope-risk: 低,仅改间隔参数
Tested: ruff check 通过
2026-05-12 19:04:21 +08:00
2569718930@qq.com e062fedb3a 移除温度急变检测,改为 per-city 定时推送温度+DEB
去掉 0.5°C/10min 阈值触发、最高温锁定、冷却期等机制。
四座机场城市各自 10 分钟间隔独立推送当前温度和 DEB 预测,
仅当地 08:00-20:00 时段发送,无触发条件、无警报标题。

Constraint: 首尔/釜山展示跑道对温度,东京/安卡拉展示单站温度+本地时间
Scope-risk: 高,核心逻辑变更
Tested: ruff check 通过
2026-05-12 19:02:36 +08:00
2569718930@qq.com 4006e82ded 机场急变消息追加本地时间
安卡拉/东京单站温度行改为"当前 X°C (13:52)"格式,
从 city_weather.local_time 提取当地时间。

Constraint: 首尔/釜山跑道对行暂不加时间
Tested: ruff check 通过
2026-05-12 18:57:47 +08:00
2569718930@qq.com fc146b6e0c @
实测修正 MGM 安卡拉刷新频率:5-15 分钟不定,非固定 10 分钟

三次采样 09:50→09:56(6min)→10:10(14min),波动较大。
更新文档为实际观测值,非预估。

Tested: curl 实测 servis.mgm.gov.tr 端点
@
2026-05-12 18:26:50 +08:00
2569718930@qq.com 106dd3305b @
修正文档:区分跑道对温度和站点实时温度

首尔/釜山为 AMOS 跑道传感器(每对独立温度),东京/安卡拉为机场
气象站单点实时温度,二者数据类型不同,文档和数据表已据此更新。

Constraint: 代码逻辑无需改动,仅文档修正
Scope-risk: 无
Tested: ruff check 通过
@
2026-05-12 18:07:35 +08:00
2569718930@qq.com 574f007607 @
主循环移除动量突变规则,温度急变检测由机场高频循环独立承担

30 分钟主循环不再做 momentum_spike 检测,避免对机场城市产生
冗余告警(含市场分布/AI 建议的格式)。其余 47 城原本就没有
高频机场数据,动量检测也无实际意义。

Constraint: 其余 3 条规则(Ankara DEB、预报突破、暖平流)保持不变
Scope-risk: 低,仅影响主循环告警规则集
Tested: ruff check 通过
@
2026-05-12 18:05:11 +08:00
2569718930@qq.com 88b8366067 @
首尔/釜山温度急变消息展示各跑道对温度

AMOS 两个跑道对独立展示(如 15L/33R 14.6°C / 15R/33L 15.2°C),
东京和安卡拉仍显示单站温度。

Constraint: 跑道对信息从 city_weather.amos.runway_obs 提取,仅首尔/釜山有效
Scope-risk: 低,仅改消息格式
Tested: ruff check 通过
@
2026-05-12 17:55:20 +08:00
2569718930@qq.com 2061dfe8b5 @
简化机场温度急变消息:只报当前温度和 DEB 预测最高温

去掉温度变化幅度、风、emoji 等冗余信息,消息只保留两行核心数据。

Constraint: 触发规则不变(0.5°C/10min 阈值、20min 窗口、3 样本最低)
Scope-risk: 极低,仅改消息格式
Tested: ruff check 通过
@
2026-05-12 17:53:22 +08:00
2569718930@qq.com 12b0c76caf @
砍掉机场快照,改为 per-city 独立告警;新增安卡拉 MGM 17128 高频监控

快照定时推送无实际价值(没变化也报),改为仅温度急变时触发告警。
各城市独立检测、独立冷却,不再捆绑推送。
同时接入安卡拉 Esenboğa 机场 MGM 站点 17128 的实时温度数据。

Constraint: 快照已运行验证格式无误,砍掉不影响现有急变告警功能
Scope-risk: 中低,仅影响高频通道逻辑,主循环不变
Tested: ruff check 通过
@
2026-05-12 17:51:40 +08:00
2569718930@qq.com 5a6a487a97 @
修复机场高频推送状态与主循环冲突:使用独立的状态文件

_load_airport_state / _save_airport_state 在 SQLite 模式下错误地复用了
_telegram_state_repo,与市场监控主循环共享同一个状态存储,
导致两个循环互相覆盖对方的 last_by_city / last_snapshot_ts 等字段。
改为始终使用独立文件 data/airport_push_state.json 隔离状态。

Constraint: 机场状态与主循环状态必须物理隔离
Scope-risk: 低,仅影响新功能的状态持久化
Tested: ruff check 通过
@
2026-05-12 17:39:26 +08:00
2569718930@qq.com 8b1abf3d56 @
修复东京快照温度缺失:JMA 数据在 mgm_nearby 而非 jma_official_nearby

_attach_japan_official_nearby 将 JMA 数据写入 raw["mgm_nearby"],
但 _analyze 未透传 jma_official_nearby 字段到 city_weather 输出层。
改为从 mgm_nearby[0].temp 取东京 JMA 实时温度。

Tested: ruff check 通过
@
2026-05-12 17:21:05 +08:00
2569718930@qq.com a6fbbb4bef @
机场快照改进:首尔/釜山展示各跑道对温度,东京补齐 JMA 实时温度

快照数据源从仅依赖 airport_obs_log 改为优先取 city_weather 中的 AMOS/JMA 实时数据:
- 首尔/釜山:展示每条跑道对温度(如 15L/33R: 14.6°C / 15R/33L: 15.2°C)
- 东京:取 JMA official_nearby 实时温度,obs_log 为空时回退到 current.temp

Tested: ruff check 通过
@
2026-05-12 17:16:08 +08:00
2569718930@qq.com 9a8ed0e15e @
修复机场快照时间显示:CST → KST(UTC+9 当地时间)
@
2026-05-12 17:08:56 +08:00
2569718930@qq.com 64f8ff21ec @
新增机场高频推送:10分钟级温度监控 + DEB预测快报

为首尔/釜山/东京三大机场城市实现高频温度监控通道:
- 新增 airport_obs_log 表积累观测数据,支持趋势检测
- AMOS/JMA 成功后自动写入观测日志
- 新增 airport_rapid_temp_change 告警规则(20min窗口、0.5°C/10min阈值)
- 10分钟间隔高频子循环 + 30分钟机场快照(含 DEB 预测最高温)
- 最高温锁定后自动跳过,快照仅当地 08:00-20:00 发送

Tested: ruff check 通过
@
2026-05-12 17:04:17 +08:00
AmandaloveYang ff4c8b0139 修复手机端无法滚动:添加 viewport meta 标签并在移动端允许根容器垂直滚动
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 10:06:12 +08:00
AmandaloveYang c56c490b60 新增机场高频数据接入市场监控频道方案文档
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 18:34:39 +08:00
AmandaloveYang c50a057562 首尔、釜山不再展示周边站(AMOS 跑道传感器已取代 KMA 站网)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 18:11:03 +08:00
AmandaloveYang 349f3e53c7 更新 README:日期至 2026-05-11,城市数 52→51,KMA→AMOS,积分改造,版本 v1.6.0
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 18:07:41 +08:00
AmandaloveYang eb47e0a078 更新深度评估报告:日期至 2026-05-11,城市数 52→51,新增 AMOS 与积分改造内容
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 18:04:04 +08:00
AmandaloveYang ff7938187c 更新文档:KMA→AMOS、积分来源补全、信心指标移除、缓存 TTL 修正
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 17:56:45 +08:00
AmandaloveYang 3d4e803488 城市决策卡移除信心指标显示
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 17:33:01 +08:00
AmandaloveYang e344fae75f 首尔/釜山 AMOS 数据缓存 TTL 从 300s 降至 60s,对齐官网 1 分钟刷新频率
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 17:28:53 +08:00
AmandaloveYang 52ad2dfde4 AMOS 跑道面板仅限首尔/釜山显示,其余城市无真实 AMOS 数据
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 17:11:57 +08:00
AmandaloveYang 55bb06d213 移除 Masroor Air Base 机场城市(数据源、别名、时区、前端面板、文档、测试)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 16:39:04 +08:00
AmandaloveYang 76b4a5df59 修复转化漏斗:后端返回原始比率,避免前端二次乘以 100 导致显示 3750%
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:49:39 +08:00
AmandaloveYang 9ff2a99618 修复测试:guard mock 补充 check_daily_query_limit 方法
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:24:35 +08:00
AmandaloveYang 3c2d1ce6fa 修复 ruff 检查:移除未使用的 CITY_QUERY_COST / DEB_QUERY_COST 导入
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:18:50 +08:00
AmandaloveYang 787d04619b feat: revamp points/reward system for inclusive engagement
- /city /deb now free, capped at 10/day each (was 2 pts cost)
- Welcome bonus +20 pts on first-ever valid message
- First-message-of-day bonus +2 pts
- Weekly winner point bonuses reduced (500→200, 300→100, 150→50)
- Weekly participation rewards for all active users (+5 base, +15 for ≥20 pts)
- Pro-day rewards for top 3 unchanged

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:35:52 +08:00
AmandaloveYang 81e9aeb98f fix: remove "Now" vertical line from intraday temperature chart
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 09:05:44 +08:00
2569718930@qq.com 78cdb006e5 subscription-help: 添加 dynamic=force-dynamic 跳过预渲染 2026-05-10 20:48:20 +08:00
2569718930@qq.com 2ad4c7f1b4 修复 subscription-help 构建:拆分为服务端页面 + 客户端组件避免 prerender 报错 2026-05-10 20:29:54 +08:00
2569718930@qq.com 17b93803af 修复业务测试:observedHighBreak 断言适配新的行动指引文案 2026-05-10 20:25:05 +08:00
2569718930@qq.com b26ec05b81 AMOS 跑道温度范围:城市决策卡头部展示"跑道实况 14.6~15.2℃"
后端:
- amos_station_sources.py 新增 runway_temp_range (跑道温度 min,max)
- 取所有有效跑道温度的最小值和最大值

前端:
- CityCardHeader 新增 observedLabel prop(标签自定义)
- 有 AMOS 数据时显示"跑道实况"替代"当前温度"
- 温度展示格式:14.6~15.2℃(跑道温度范围)
- 无 AMOS 时回退原有 METAR 温度显示
2026-05-10 20:19:48 +08:00
2569718930@qq.com 8075fb66b7 AMOS 增加 info 级别日志追踪数据流
- weather_sources.py: _attach_korean_amos_data 记录 fetch 开始、成功/失败、温度/跑道数据
- amos_station_sources.py: _amos_get_page 记录页面匹配/不匹配
- amos_station_sources.py: fetch_amos_official_current 记录 HTML 获取和解析
- analysis_service.py: _analyze 记录 AMOS 数据是否到达分析层

重启后端后在日志中搜索 "AMOS" 可追踪完整数据流:
  AMOS: fetching for city=seoul
  AMOS page matched icao=RKSI length=...
  AMOS fetch_amos_official_current: got HTML for RKSI...
  AMOS: got data for city=seoul temp_c=... source=...
  AMOS _analyze: found amos data for city=seoul temp_c=...
2026-05-10 19:46:26 +08:00
2569718930@qq.com 23e5d10f65 Fix Korean AMOS runway parsing 2026-05-10 19:34:18 +08:00
2569718930@qq.com f4f613ad02 Fix AMOS runway observations 2026-05-10 19:20:52 +08:00
2569718930@qq.com 5512ebf133 修复 AMOS 数据不显示:移出 include_nearby 条件判断
根因:_attach_korean_amos_data 在 include_nearby 块内
面板模式(depth=panel)时 include_nearby=False
→ AMOS 数据从不获取 → detail.amos 始终为空 → 跑道面板不显示

修复:AMOS 调用移到 include_nearby 之外
AMOS 是主观测源,不是 nearby 站网数据
无论 depth 模式都应获取
2026-05-10 19:10:38 +08:00
2569718930@qq.com 40cd8fc2a6 修复机场观测面板不显示 + 放宽 AMOS 展示条件 2026-05-10 19:06:22 +08:00
2569718930@qq.com 3f351ac60c Busan 机场观测面板:METAR 数据兜底展示
问题:AMOS 仅支持仁川 RKSI,釜山 RKPK 无法获取跑道级数据
解决:AmosRunwayPanel 新增 airportCurrent 回退模式

- 有 AMOS 数据时:展示完整跑道卡片网格(首尔/仁川)
- 无 AMOS 时:展示单张机场观测卡片(釜山/所有其他机场)
  包含:温度、风向风速、气压 QNH、能见度
  标注数据来源(METAR/AMOS)和是否过旧
- AirportCurrentConditions 类型新增 pressure_hpa 字段

这样首尔有完整的 4 对跑道数据,釜山至少展示机场官方观测值
2026-05-10 18:58:42 +08:00
2569718930@qq.com e1d21c6ce3 简化 AMOS 获取:仅支持 RKSI(仁川),Busan 回退标准 METAR
原因:AMOS 页面默认始终显示仁川 RKSI,机场切换用 JS 实现
无法通过 URL 参数或 POST form data 切换到其他机场
尝试了 GET ?icao=、?stn=、?airport= 和 POST form data 均无效

变更:
- _amos_get_page 简化为仅处理 RKSI
- 非 RKSI 直接返回 None,走标准 METAR 回退链
- 移除无效的 AMOS_STATION_IDS 和 POST 策略代码
- Busan 通过 aviationweather.gov METAR 正常获取温度/风/气压
- 跑道面板仅在 AMOS 数据存在时显示(即仅 Seoul/仁川)

Known: Busan 无跑道级数据,但标准 METAR 仍然可用
2026-05-10 18:53:09 +08:00
2569718930@qq.com 163ad8cb4e 修复 AmosRunwayPanel TS strict null check 2026-05-10 18:40:22 +08:00
2569718930@qq.com e23a90a961 修复釜山 AMOS 数据获取 + 跑道面板容错
AMOS 页面用 JS 切换机场,GET 参数无效。新增策略:
- 先 GET 建立 session,再 POST form data 切换机场
- 尝试多种 form data 组合(icao/stn/airport/code)
- 回退到 GET 参数尝试

跑道面板容错:
- 无温度数据时仍展示跑道风/能见度/RVR
- 温度缺失显示 "--" 而非隐藏整行
- analysis_service 输出条件放宽:有 temp_c 或 runway_obs 即可

Tested: python -m ruff check ., npx tsc --noEmit
2026-05-10 18:39:37 +08:00
2569718930@qq.com 13a67cdc85 修复温度走势图不显示:移除 IntersectionObserver 延迟渲染
问题:图表只在 IntersectionObserver 检测到视口交叉后才渲染
- 卡片折叠时图表区域永远不可见,IntersectionObserver 不触发
- 小屏幕/快速滚动时图表可能来不及渲染

修复:
- 移除 IntersectionObserver 懒加载逻辑
- 图表始终立即渲染(useChart 在挂载时初始化)
- Chart.js animation:false 确保快速渲染,无性能代价
- 清理未使用的 useState/useEffect 导入
2026-05-10 18:34:00 +08:00
2569718930@qq.com eed0345045 移除误提交的 grep.exe.stackdump 2026-05-10 18:31:17 +08:00
2569718930@qq.com fe3f48f255 城市决策卡新增 AMOS 跑道温度面板
- 新增 AmosRunwayPanel 组件:展示每条跑道的温度/露点/能见度/RVR/风速
- 跑道卡片网格布局 (auto-fit minmax 160px)
- 每条跑道独立显示:跑道编号、温度(含露点)、能见度、RVR、风速范围
- 官方 METAR 标签 vs 跑道中位数标签
- CityDetail 类型新增 AmosData 接口
- 暗色/浅色主题均已适配
- 仅首尔/釜山(有 AMOS 数据)时显示
2026-05-10 18:30:59 +08:00
2569718930@qq.com ba352feb34 首尔/釜山机场报文优先使用 AMOS(更新更快)
- analysis_service.py:AMOS 数据覆盖 airport_current 的 raw_metar、wind、pressure
- source_label 设为 "AMOS"(区别于 aviationweather.gov METAR)
- stale_for_today 强制 false(AMOS 数据本身证明了时效性)
- observation_time 优先使用 AMOS 时间戳
- AI 读取的 city_snapshot.current.raw_metar 来自 AMOS 跑道传感器

优先级:AMOS raw_metar > aviationweather.gov METAR(首尔/釜山)

Reason: AMOS 直连韩国机场系统,延迟更低,更新更快
2026-05-10 18:18:40 +08:00
2569718930@qq.com 04b2f0e4a3 AMOS 数据接入分析层和 AI 预测判定
问题:AMOS 数据在采集层取出后从未被 _analyze() 读取,AI 无法看到跑道级数据

修复:
- analysis_service.py:_analyze() 新增 AMOS 温度读取优先级
  观测来源顺序:结算源 > AMOS跑道传感器 > METAR > MGM > NMC
- AMOS 数据自动覆盖 current.wind_speed_kt、current.pressure_hpa、current.raw_metar
- 输出新增 "amos" 字段携带完整跑道数据(temp_source、runway_temps)
- scan_terminal_service.py:AI prompt 的 current 区块新增 pressure_hpa 和 observation_source
- AI 现在知晓数据来自 AMOS 跑道传感器(observation_source="amos"),可据此判断数据权威性

数据流:
  AMOS fetch -> raw["amos"] -> _analyze() -> current.observation_source="amos"
  -> result["amos"] -> _build_city_ai_prompt -> city_snapshot.current
  -> AI reads runway sensor temp/wind/pressure as authoritative observation

Tested: python -m ruff check ., npx tsc --noEmit
2026-05-10 18:16:10 +08:00
2569718930@qq.com bf5e92e656 AMOS 温度处理:METAR 优先,跑道中位数兜底
温度优先级:
1. METAR 温度(官方机场传感器,权威值)
2. 跑道传感器中位数(fallback;不同跑道传感器因位置/海拔可能差 0.5-1°C)

- 新增 temp_source 字段标注来源("metar" / "runway_median")
- 新增 runway_temps 数组保留每条跑道的原始 (温度, 露点)
- 回退逻辑用中位数而非第一个值(跑道数据可能是乱序的)
- 温度合理性检查:只取 -50°C ~ 60°C 范围内的值

Tested: python -m ruff check ., npx tsc --noEmit
2026-05-10 18:07:33 +08:00
2569718930@qq.com 27010d5fb3 移除 KMA 数据源:已被更精确的 AMOS 跑道级传感器取代
- weather_sources.py:移除 KmaStationSourceMixin 导入和继承
- 移除 _attach_korea_official_nearby() 函数
- 移除 kma_cache 初始化代码
- 首尔/釜山现在使用 AMOS 跑道传感器替代 KMA 地面站
- kma_station_sources.py 保留为参考文件

Replaced-by: AMOS (global.amo.go.kr) runway-level sensor data
2026-05-10 17:59:26 +08:00
2569718930@qq.com 058ab7a619 新增 AMOS 跑道级实时气象数据源(韩国仁川/釜山)
- src/data_collection/amos_station_sources.py:新增 AmosStationSourceMixin
- 从 global.amo.go.kr 爬取跑道级观测:温度/露点/气压/风向风速/能见度/RVR/云层
- 解析 METAR + TAF 报文,提取温/湿/压/风数值
- 解析跑道级表格数据(每跑道独立风分量、侧风、视程)
- 覆盖首尔/仁川 (RKSI) 和釜山/金海 (RKPK)

- weather_sources.py:集成 AmosStationSourceMixin
- fetch_all_sources 中为 seoul/busan 自动追加 AMOS 数据
- 结果存入 results["amos"],包含原始 METAR/TAF 和解析后的跑道数据

Tested: python -m ruff check ., npx tsc --noEmit
2026-05-10 17:55:39 +08:00
2569718930@qq.com fe681503ae UX 审查收尾:AI 解读过渡标记 + 极端温度视觉强化
P2-17:AI 快速判断→完整解读过渡标记
- AiEvidencePanel 新增 useTransitionMarker hook
- AI 从 loading→ready 后显示"✓ Updated/已更新"徽标(4 秒后消失)
- 绿色动画徽标 fadeUpIn,避免用户以为信息没变

P2-18:极端温度视觉强化
- globals.css 新增 temp-extreme-hot(橙红辉光 ≥40°C/≥104°F)
- globals.css 新增 temp-extreme-cold(冰蓝辉光 ≤-5°C/≤23°F)
- PanelSections Hero 温度值自动应用极端温度样式

Tested: npx tsc --noEmit
2026-05-10 17:44:45 +08:00
2569718930@qq.com 341825747f UX 审查 P2 修复:X轴标签密度、极端温度视觉强化
- AiCityTemperatureChart:X轴标签从每4个→每3个显示,maxTicksLimit 6→8
- globals.css:新增 temp-extreme-hot(≥40°C)和 temp-extreme-cold(≤-5°C)样式
- PanelSections:Hero 温度值自动应用极端温度 CSS 类(橙色辉光/蓝色辉光)
- 华氏度自动适配:≥104°F 为极热,≤23°F 为极冷

Tested: npx tsc --noEmit
2026-05-10 17:39:22 +08:00
2569718930@qq.com d94476f943 UX 审查修复:统一修正术语、图表"现在"标记、专业术语解释、异常行动建议
P0-1 术语统一:
- WeatherDecisionBand 改用"上修/下修/维持"替代"偏高温/暂不追/等待确认"
- 与 AI 后端提示词使用的术语体系一致

P0-9 图表"现在"标记:
- chart-utils.ts 导出 currentIndex
- AiCityTemperatureChart 在 currentIndex 处绘制竖线(蓝色虚线)

P0-13 专业术语解释:
- DataFreshnessBar 新增 labelTitle 属性(hover tooltip)
- METAR → "机场气象观测报文" / "Meteorological Aerodrome Report"
- HKO → "香港天文台官方实测" / "Hong Kong Observatory official readings"

P0-16 异常行动建议:
- primaryReason 追加行动指引(实测突破→建议关注偏高温区间等待确认 / 峰值已过→建议避免追高 / 观测过旧→建议等待新报文)

P1-8 DEB 路径分段样式:
- 过去部分实线(已确定),未来部分虚线(预测不确定)

P1-11 HistoryChart 单位:
- tooltip 使用 temp_symbol 替代硬编码 °

Tested: npx tsc --noEmit
2026-05-10 17:33:15 +08:00
2569718930@qq.com bf70d0b42a 新增 UX 研究员审查报告:修正逻辑可读性、图表误导、专业术语、异常体验
核心发现:
- AI 用"上修/下修/维持",前端用"偏高温/暂不追/等待确认"——两套语言体系不一致(P0)
- 日内图表没有"现在"标记,用户不知道哪里是当前时间(P0)
- METAR/DEB/TAF 全站 50+ 处出现,无任何 tooltip 或解释(P0)
- 实测突破时没有行动建议,用户不知道下一步该做什么(P0)
- DEB 路径全是虚线(含过去部分),本应实线→虚线过渡(P1)
- "DEB 融合"对普通用户无意义(P1)

共识别 18 个问题:P0 4 项、P1 4 项、P2 4 项
2026-05-10 17:24:35 +08:00
2569718930@qq.com e2bea367a1 校准漂移检测增加日志告警 + 更新架构文档
- core.py:drift.drifted=true 时输出 loguru warning,包含 delta% 和 sample 数量
- data-architecture-review.md:标记 2 项误诊(METAR TTL 实际 600s、轮询已有 AbortController 保护)
- 剩余真正待办:校准自动重训练(需算力)、缓存键细化(低优先级)
2026-05-10 17:06:43 +08:00
2569718930@qq.com 27095025e7 更新数据架构审查文档:标记 8/8 已修复,保留 4 项低优先级待办 2026-05-10 17:02:02 +08:00
2569718930@qq.com 2b2784d811 数据链路 P2 修复:stale-while-revalidate + 扫描数据复用
P2-7 stale-while-revalidate:
- ensureCityDetail 过期缓存不再阻塞等待刷新
- 立即返回缓存数据,后台异步更新
- 用户打开已有缓存的城市时不再看到 loading spinner

P2-8 扫描终端数据复用:
- 新增 store.preloadCityFromRow():从 ScanOpportunityRow 预填充 cityDetails 缓存
- handleSelectRow / handleMapCitySelect / handleOpenDecisionRow 均调用预加载
- 用户从地图/列表/决策卡选城市后,详情面板立即显示缓存数据
- 后台自动拉取完整 detail(stale-while-revalidate)

Tested: npx tsc --noEmit
2026-05-10 17:00:12 +08:00
2569718930@qq.com b3ea8dcfa7 数据链路 P1 修复:ETag 缓存 + 校准漂移检测
P1-5 ETag 支持:
- 后端新增 _etag_middleware:GET /api/* 自动返回 ETag (MD5)
- 支持 If-None-Match 请求头,匹配时返回 304 + 30s Cache-Control
- 前端 cache: no-store → default,浏览器自动处理 ETag/304 节省带宽

P1-6 校准漂移检测:
- probability_calibration.py 新增 check_calibration_drift()
- 对比最近 200 条 daily_records 的 CRPS 与校准基线
- 漂移 >15% 时返回 warning 提示重新训练
- 集成到 /api/system/status 的 probability.drift 字段

Tested: python -m ruff check ., npx tsc --noEmit
2026-05-10 16:54:48 +08:00
2569718930@qq.com c0bb2acf78 修复数据链路 P0 瓶颈:缓存炸弹、Context 重渲染、LGBM 循环、TTL 不匹配
P0-1 sessionStorage 限制:
- writeCityDetailCacheBundle 只保留最近 3 个城市的详情
- 避免 3-10MB JSON 序列化阻塞主线程

P0-2 Context 拆分:
- 新增 CityDetailsContext 独立管理 cityDetailsByName 变更
- 新增 useCityDetails hook,只订阅详情的组件不再因 cities/proAccess 变化重渲染
- DashboardStoreContext 保持不变,向后兼容

P0-3 扫描终端 TTL:
- SCAN_TERMINAL_PAYLOAD_TTL_SEC 30s → 120s
- 匹配 ThreadPoolExecutor(4) x 60 城的实际重算耗时

P0-4 LGBM 循环依赖:
- LGBM 预测值不再作为 DEB 输入参与权重计算
- 保留为独立参考字段 lgbm.prediction 输出给前端展示
- 消除 DEB → LGBM 训练 → DEB 的循环

新增 docs/data-architecture-review.md 完整数据链路审查报告

Tested: npx tsc --noEmit, python -m ruff check .
2026-05-10 16:48:28 +08:00
2569718930@qq.com 8cc7e9a996 移除"已完成"章节:文档只保留待办事项和产品亮点 2026-05-10 16:31:22 +08:00
2569718930@qq.com c9d3a27e88 精简产品审查文档:已解决问题移入"已完成"章节,保留待办 2026-05-10 16:29:10 +08:00
2569718930@qq.com e52f1a46f6 更新产品审查文档:标记修复进度 12/14,账户设置标记为不做 2026-05-10 16:23:37 +08:00
2569718930@qq.com ce1da6a686 完成产品审查剩余修复:新手指引、反馈入口、付费墙预览
P1-4:新增 WelcomeOverlay 新手指引
- 首次访问时显示 3 步引导:①从地图选城市 ②查看城市简报 ③解锁 Pro 深入分析
- 圆点进度指示、跳过/下一步按钮、点背景可关闭
- 看过一次后 localStorage 标记不再显示

P1-5:付费墙增加功能预览
- HistoryModal 在付费墙上方展示功能说明文案
- 新增 i18n 键 history.previewTitle / history.previewDesc(中英双语)

P2-9:新增反馈入口
- 顶栏增加 Telegram 反馈按钮(MessageCircle 图标)
- 点击跳转 PolyWeather 社群

Tested: npx tsc --noEmit
2026-05-10 16:19:21 +08:00
2569718930@qq.com de0f037bf4 产品体验修复:P0 阻塞 + P1/P2 改善(产品审查 14 项中修复 10 项)
P0 阻塞:
- Pro 加载不再阻塞整个看板:只显示精简顶栏 + 加载状态,不再遮盖整个页面
- Scan 失败加重试按钮:所有用户(含免费)均可手动重试
- Detail Panel 同步超时后显示提示:"同步时间较长,当前展示的数据可能不完整"

P1 体验:
- 登录页增加"忘记密码"链接:调用 Supabase 密码重置流程
- 登录失败/邮箱未验证增加明确提示:"如刚注册,请先点击验证链接"
- 公告横幅增加 ✕ 关闭按钮:点击后永久隐藏

P2 改善:
- entitlement-required 页面重设计:品牌化、增加返回首页和登录按钮
- 订阅帮助页中英双语:所有 FAQ 和 UI 文案支持中英文切换
- 新增产品审查文档 docs/product-review-jun-2026.md

Tested: npx tsc --noEmit
2026-05-10 16:13:34 +08:00
2569718930@qq.com 9635387beb 移除日历视图:功能与决策卡重叠,维护成本高于价值
- 删除 CalendarView.tsx、calendar-action-utils.ts 及其测试文件
- 删除 ScanTerminalCalendar.module.css
- ScanTerminalDashboard:移除日历 tab 按钮、日历渲染分支、ContentView 类型中的 calendar
- ScanTerminalLightTheme:清理所有 .scan-calendar-* 规则(~40 行)
- scan-root-styles.ts:移除 ScanTerminalCalendar 导入

同时完善地理排序:
- scan_terminal_filters.py:market_region_from_tz_offset 细化为 7 个区域(东亚/东南亚/中亚/西亚/欧洲非洲/南美/北美)并增加 sort_order
- scan_terminal_city_row.py:传递 trading_region_sort
- dashboard-types.ts:增加 trading_region_sort 字段
- decision-utils.ts:sortRowsByUserTime 按 trading_region_sort 优先排序

Rejected: keep-calendar-view, calendar-actions-overlap-with-decision-cards
Tested: npx tsc --noEmit, python -m ruff check .
2026-05-10 15:48:18 +08:00
2569718930@qq.com 5ff9fade5f 重新设计日历视图:以用户本地时区为主视角
- CalendarView 组件重写:添加实时用户时钟头部(HH:MM:SS + 完整日期)
- 卡片重新设计:用户本地时间为主要展示,城市窗口时间为次要
- 新增 urgency badge(进行中/即将/稍后/已过)视觉标识
- 优化卡片布局:顶部城市+徽标 → 用户时间 → 倒计时 → 原因 → 底部 DEB+阶段
- CSS 全面重写:用户时钟头部、卡片分层、计时器颜色编码
- 浅色主题同步更新所有新 class 名称
2026-05-10 15:34:46 +08:00
2569718930@qq.com 0e529358c9 移除 scan-topbar-logo 品牌标记
- 从 ScanTerminalDashboard.tsx 移除 logo div 和 brand 容器
- 从 ScanTerminalShell.module.css 移除 .scan-topbar-brand / .scan-topbar-logo 样式
- 从 ScanTerminalLightTheme.module.css 移除对应的浅色主题覆盖
2026-05-10 15:13:32 +08:00
2569718930@qq.com 32019c89f5 更新项目文档至 v1.6.0:同步前端设计系统重构成果
- CHANGELOG.md:新增 v1.6.0 条目,完整记录 15 项前端设计审查修复
- CLAUDE.md:新增 CSS Variables First / 避免 !important 代码规范、scan-root-styles.ts 桶文件说明、Quality Gate 增加硬编码颜色检查
- README.md / README_ZH.md:产品状态补充前端设计系统重构摘要
- docs/TECH_DEBT_ZH.md:标记前端工程债务已关闭、更新债务比例 95%→97%、版本号 v1.5.4→v1.6.0
2026-05-10 14:35:36 +08:00
2569718930@qq.com 41acb2aa51 移除 AGENTS.md:oh-my-codex 自动生成的非项目文档 2026-05-10 14:31:29 +08:00
2569718930@qq.com ca0b25d84f 清理冗余文档:移除过期报告、重复文件和 AI 生成配置
- 移除 FRONTEND_REDESIGN_REPORT.md(v1.5.1 旧报告,已被 frontend-ui-design-review.md 取代)
- 移除 docs/TECH_DEBT.md(与 TECH_DEBT_ZH.md 完全重复)
- docs/ 目录集中管理所有项目文档,无需移动

Cleaned: outdated-report, duplicate-doc
2026-05-10 14:31:10 +08:00
2569718930@qq.com 00e1845f2c 全面修复前端 UI 设计审查问题:消除工程债务、统一 token 体系、提升可维护性
- 消除 !important 滥用:134 → 49(仅保留 Leaflet/图表所必需项),浅色主题使用 html.light 选择器获得更高优先级
- 修复 font-weight:13 个文件中所有 760/850/860/880/950 等非标准值已映射为 Inter 支持的 300–800
- 移除未加载的 Geist 字体声明,替换为 Inter
- 添加全局 :focus-visible 轮廓环、跳过链接、Tab ARIA 属性(role/aria-selected)
- 统一断点体系:18 → 10(480/640/768/960/1024/1200/1280/1360/1440/1680)
- 创建 scan-root-styles.ts 桶文件,将 22 个 CSS Module 导入合并为 1 个
- Token 迁移:10 个文件中数百处硬编码颜色(#4DA3FF/#E6EDF3/#9FB2C7/#6B7A90)已替换为 CSS 变量
- 去重 @keyframes:spin 4→1、loading-spin 2→0、pulse-pending 已移至 globals.css
- 添加统一的 empty/error/retry 状态组件
- 添加全局 prefers-reduced-motion 支持
- 修复 accent-primary 与 accent-secondary 相同值的问题
- 修复 accent-green 类错误渲染为蓝色
- 添加 CSS 渐变品牌 Logo
- 移除死代码(1,697 行):public/static/style.css + public/legacy/index.html
- Dashboard.module.css 本地变量已桥接至全局 token
- 提升文字对比度:#6B7A90 → #7D8FA3

Fixed: !important-134-to-49, font-weight-13-files, Geist-removal, focus-visible, breakpoints-18-to-10, CSS-module-barrel, token-migration-10-files, keyframe-dedup, dead-code-removal, accent-color-fix, contrast-improvement
Scope: frontend CSS architecture, design tokens, accessibility, responsive breakpoints
Tested: npx tsc --noEmit
2026-05-10 14:21:10 +08:00
2569718930@qq.com f47de115c8 Reduce duplicate scan terminal decision copy and chrome
Removed the scan KPI bar, trimmed redundant decision-card content, and shortened the primary reason text so the city analysis cards read like concise signals instead of repeating the same state across sections.

Constraint: Keep existing city decision logic and data flow unchanged while simplifying the UI
Rejected: Rework the full card layout or mobile card flow | larger surface area than requested
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep hero, decision band, and market line responsibilities separate to avoid duplicate copy returning
Tested: frontend npm run build
Not-tested: Manual visual QA in browser across desktop/mobile breakpoints
2026-05-10 12:20:22 +08:00
2569718930@qq.com 3e14a23e02 修复 markets 测试:移除对已删除函数 build_market_monitor_digest 的 monkeypatch
Tested: python -m pytest tests/test_bot_basic_handler.py -xvs
2026-05-07 22:27:12 +08:00
2569718930@qq.com 2183c0399d 移除错误定价信号和市场聚焦摘要功能
保留关键市场警报推送(4 条触发规则不变),
移除 mispricing 信号分类/推送、focus digest 评分/聚合/定时推送、
/markets 手动查询等全套逻辑,净删除 1049 行。
2026-05-07 21:42:06 +08:00
2569718930@qq.com 30faac989c 秒开 AI 预测最高温:preview 事件提前下发确定性预测值
之前 predicted_max 只在 AI 流式 final 事件返回后才显示(10-25 秒),
但后端 fallback 早在 1ms 内就算好了 cluster median / DEB 中枢。
现在 preview 事件同步下发确定性 predicted_max + range_low/high,
前端也在 fallback payload 中自动从 multi_model + DEB 计算预测值,
用户点击城市后 ~100ms 即可看到 AI 预测最高温。
2026-05-07 21:04:26 +08:00
2569718930@qq.com 0f9469f3cb README 新增 Star History 历史曲线 2026-05-07 20:19:40 +08:00
2569718930@qq.com 734462a3b9 chore: CLAUDE.md 新增环境配置章节
明确工作目录、前端/后端端口、中文 commit 强制规则、npm/python 工具链偏好。
配套代码风格与质量门禁章节形成完整的开发行为约束。

Directive: 后续所有会话必须遵守此环境配置
Tested: ruff + tsc 全过
2026-05-07 00:44:35 +08:00
2569718930@qq.com a7421b3147 chore: CLAUDE.md 新增质量门禁与编码规范
四道强制自检:tsc/ruff、禁止 Unicode 转义、双主题 CSS 同步、diff 展示。
补上中文 commit 规范、禁止 \uXXXX 转义、UI 双主题同步三条编码规则。

Directive: 每次标记任务完成前必须通过全部质量门禁
2026-05-07 00:42:44 +08:00
2569718930@qq.com 30a6ff7986 新增 v1.5.6 升级公告,3 天后自动消失
公告展示决策终端 hero 重设计、DEB 统一、亮色补全等变更摘要。
localStorage 记录首次访问时间,超过 3 天自动隐藏。
补全 scan-upgrade-announcement 暗色主题基础样式。

Tested: tsc 全过
2026-05-06 18:59:20 +08:00
423 changed files with 31003 additions and 177025 deletions
+50 -55
View File
@@ -13,17 +13,12 @@ POLYWEATHER_MAP_URL=https://polyweather-pro.vercel.app/
POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather
POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db
OPEN_METEO_DISK_CACHE_PATH=/var/lib/polyweather/open_meteo_cache.json
UVICORN_WORKERS=1
# Optional: host user/group mapping for Docker on Linux.
# Windows / macOS can usually keep the defaults.
UID=1000
GID=1000
POLYWEATHER_STATE_STORAGE_MODE=sqlite
POLYWEATHER_PROMETHEUS_PORT=9090
POLYWEATHER_ALERTMANAGER_PORT=9093
POLYWEATHER_ALERT_RELAY_PORT=9099
POLYWEATHER_GRAFANA_PORT=3001
POLYWEATHER_GRAFANA_ADMIN_USER=admin
POLYWEATHER_GRAFANA_ADMIN_PASSWORD=polyweather
# Backend CORS allowlist. Add your Vercel production/preview domains when
# NEXT_PUBLIC_POLYWEATHER_API_BASE_URL points browsers directly at this backend.
WEB_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,https://polyweather-pro.vercel.app
@@ -34,21 +29,35 @@ WEB_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,https://polyweather
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
TELEGRAM_CHAT_IDS=
POLYWEATHER_TELEGRAM_GROUP_ID=
# Optional: restrict message-points accrual to these chat IDs.
# Example: POLYWEATHER_BOT_POINTS_CHAT_IDS=-1003965137823
POLYWEATHER_BOT_POINTS_CHAT_IDS=
POLYWEATHER_GROUP_MEMBER_PRICE_USDC=5
POLYWEATHER_PUBLIC_PRICE_USDC=10
TELEGRAM_QUERY_TOPIC_CHAT_ID=
TELEGRAM_QUERY_TOPIC_ID=
TELEGRAM_QUERY_TOPIC_MAP=
POLYWEATHER_BOT_GROUP_INVITE_URL=
POLYWEATHER_APP_URL=https://polyweather-pro.vercel.app
# High-frequency airport push loop. Keep this at 1 on shared 1CPU VPS.
TELEGRAM_AIRPORT_PUSH_ENABLED=true
TELEGRAM_AIRPORT_PUSH_INTERVAL_SEC=60
TELEGRAM_AIRPORT_PUSH_MAX_WORKERS=1
########################################
# 3) Weather + cache
########################################
OPEN_METEO_CACHE_TTL_SEC=7200
OPEN_METEO_ENSEMBLE_CACHE_TTL_SEC=7200
OPEN_METEO_MULTI_MODEL_CACHE_TTL_SEC=7200
OPEN_METEO_CACHE_TTL_SEC=21600
OPEN_METEO_ENSEMBLE_CACHE_TTL_SEC=21600
OPEN_METEO_MULTI_MODEL_CACHE_TTL_SEC=21600
OPEN_METEO_MULTI_MODEL_CACHE_VERSION=v2
OPEN_METEO_RATE_LIMIT_COOLDOWN_SEC=900
OPEN_METEO_RATE_LIMIT_COOLDOWN_SEC=3600
OPEN_METEO_RATE_CACHE_TTL_SEC=3600
OPEN_METEO_MIN_CALL_INTERVAL_SEC=1
OPEN_METEO_MIN_CALL_INTERVAL_SEC=5
POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS=1
POLYWEATHER_SCAN_TERMINAL_PAYLOAD_TTL_SEC=600
POLYWEATHER_SCAN_TERMINAL_BUILD_TIMEOUT_SEC=45
POLYWEATHER_HTTP_TIMEOUT_SEC=8
POLYWEATHER_HTTP_RETRY_COUNT=0
POLYWEATHER_HTTP_RETRY_BACKOFF_SEC=0.2
@@ -57,26 +66,19 @@ POLYWEATHER_METAR_TIMEOUT_SEC=4
POLYWEATHER_METAR_CLUSTER_TIMEOUT_SEC=3.5
METAR_CACHE_TTL_SEC=600
JMA_AMEDAS_CACHE_TTL_SEC=120
METEOBLUE_CACHE_TTL_SEC=7200
# Probability engine modes:
# - legacy: production-safe primary path.
# - emos_shadow: user-facing probability stays legacy, EMOS is generated for comparison.
# - emos_primary: only after offline evaluation passes and manual rollout is approved.
POLYWEATHER_PROBABILITY_ENGINE=legacy
POLYWEATHER_EMOS_AUTO_MIN_SAMPLES=50
POLYWEATHER_EMOS_AUTO_MAX_DELTA_CRPS=0
POLYWEATHER_EMOS_AUTO_MAX_DELTA_MAE=0.05
POLYWEATHER_EMOS_AUTO_MIN_DELTA_BUCKET_HIT_RATE=-0.05
# Optional: cap recent probability snapshots used by EMOS retraining.
# Recommended on VPS: do not train there; pull the SQLite DB to a local machine.
# POLYWEATHER_EMOS_TRAINING_SNAPSHOT_LIMIT=20000
# Optional: set this to a writable runtime path if you manually deploy a
# locally trained EMOS calibration file.
# POLYWEATHER_PROBABILITY_CALIBRATION_FILE=/var/lib/polyweather/probability_calibration/default.json
POLYWEATHER_LGBM_ENABLED=false
POLYWEATHER_LGBM_MODEL_PATH=/app/artifacts/models/lgbm_daily_high.txt
POLYWEATHER_LGBM_SCHEMA_PATH=/app/artifacts/models/lgbm_daily_high_schema.json
POLYWEATHER_LGBM_MIN_HISTORY_POINTS=3
# ── Country-specific data source URLs ──
# These are kept in .env to avoid exposing competitive data-source discovery
# work on the public GitHub repository. Leave empty to use built-in defaults.
# AMSC_AWOS_BASE_URL=https://www.amsc.net.cn/gateway/api/saas/rest/amc/AwosController/getWindPlate
# KMA_BASE_URL=https://www.weather.go.kr
# AMOS_BASE_URL=https://global.amo.go.kr/amosobsnew/AmosRealTimeImage.do
# JMA_AMEDAS_BASE_URL=https://www.jma.go.jp
# MGM_BASE_URL=https://servis.mgm.gov.tr/web
# MGM_ORIGIN_URL=https://www.mgm.gov.tr
# FMI_BASE_URL=https://opendata.fmi.fi/wfs
# HKO_BASE_URL=https://data.weather.gov.hk/weatherAPI/hko_data/regional-weather
# SINGAPORE_MSS_BASE_URL=https://api.data.gov.sg/v1/environment/air-temperature
########################################
# 4) Auth / entitlement
@@ -92,24 +94,12 @@ SUPABASE_HTTP_TIMEOUT_SEC=8
SUPABASE_AUTH_CACHE_TTL_SEC=30
SUPABASE_SUB_CACHE_TTL_SEC=60
POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN=
POLYWEATHER_SIGNUP_TRIAL_ENABLED=false
POLYWEATHER_TELEGRAM_JOIN_INELIGIBLE_ACTION=decline
########################################
# 5) Alerts / operations
# 5) Operations
########################################
TELEGRAM_ALERT_PUSH_ENABLED=true
TELEGRAM_ALERT_PUSH_INTERVAL_SEC=300
TELEGRAM_ALERT_PUSH_COOLDOWN_SEC=1800
TELEGRAM_ALERT_MIN_TRIGGER_COUNT=2
TELEGRAM_ALERT_MIN_SEVERITY=medium
TELEGRAM_ALERT_MISPRICING_ONLY=true
TELEGRAM_ALERT_MISPRICING_INTERVAL_SEC=7200
TELEGRAM_MARKET_FOCUS_DIGEST_ENABLED=true
TELEGRAM_MARKET_FOCUS_DIGEST_INTERVAL_SEC=1800
TELEGRAM_MARKET_FOCUS_DIGEST_TOP_N=5
TELEGRAM_MARKET_FOCUS_PUSH_START_LOCAL_HOUR=8
TELEGRAM_MARKET_FOCUS_PUSH_BEFORE_PEAK_MIN=480
TELEGRAM_MARKET_FOCUS_PUSH_AFTER_PEAK_MIN=300
TELEGRAM_ALERT_CITIES=ankara,london,paris,seoul,hong kong,shanghai,singapore,tokyo,tel aviv,toronto,buenos aires,wellington,new york,chicago,dallas,miami,atlanta,seattle,lucknow,sao paulo,munich
POLYWEATHER_MONITORING_ALERT_CHAT_IDS=
########################################
@@ -124,17 +114,21 @@ NEXT_PUBLIC_POLYWEATHER_DISABLE_EAGER_SUMMARIES=false
# bypass Vercel Functions / Fluid Compute instead of going through Next.js API proxies.
# Example: NEXT_PUBLIC_POLYWEATHER_API_BASE_URL=https://api.example.com
NEXT_PUBLIC_POLYWEATHER_API_BASE_URL=
# Set to "false" to disable app analytics event tracking (conversion funnel etc.)
# Default: enabled. Only set this if you need to opt out.
NEXT_PUBLIC_POLYWEATHER_APP_ANALYTICS=true
########################################
# 7) Optional modules
# 7) Admin / Ops
########################################
# Comma-separated admin email list for /ops dashboard access
POLYWEATHER_OPS_ADMIN_EMAILS=
# KNMI 10-minute observation data (Amsterdam)
KNMI_API_KEY=
# Optional Groq commentary rewrite for intraday structure cards
POLYWEATHER_GROQ_COMMENTARY_ENABLED=false
GROQ_API_KEY=
POLYWEATHER_GROQ_COMMENTARY_MODEL=openai/gpt-oss-20b
POLYWEATHER_GROQ_COMMENTARY_TIMEOUT_SEC=8
POLYWEATHER_GROQ_COMMENTARY_CACHE_TTL_SEC=1800
########################################
# 8) Optional modules
########################################
# Optional OpenAI-compatible market scan review for Pro users
# Temporary default provider: MiMo via https://token-plan-cn.xiaomimimo.com/v1.
@@ -157,7 +151,6 @@ POLYWEATHER_SCAN_AI_MAX_ROWS=40
POLYWEATHER_SCAN_AI_MAX_TOKENS=3200
POLYWEATHER_SCAN_CITY_AI_MAX_TOKENS=900
POLYWEATHER_SCAN_AI_PROXY_TIMEOUT_MS=55000
POLYWEATHER_PREWARM_CITIES=ankara,istanbul,shanghai,beijing,shenzhen,guangzhou,wuhan,chengdu,chongqing,hong kong,taipei,singapore,tokyo,seoul,busan,london,paris,madrid
POLYWEATHER_CITY_SUMMARY_CACHE_TTL_SEC=1800
POLYWEATHER_CITY_PANEL_CACHE_TTL_SEC=1800
POLYWEATHER_CITY_NEARBY_CACHE_TTL_SEC=1800
@@ -188,7 +181,8 @@ POLYWEATHER_PAYMENT_CHAIN_ID=137
POLYWEATHER_PAYMENT_RPC_URL=https://polygon-rpc.com
POLYWEATHER_PAYMENT_RPC_URLS=https://polygon-rpc.com
POLYWEATHER_PAYMENT_RECEIVER_CONTRACT=
POLYWEATHER_PAYMENT_TOKEN_ADDRESS=0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174
POLYWEATHER_PAYMENT_DIRECT_RECEIVER_ADDRESS=
POLYWEATHER_PAYMENT_TOKEN_ADDRESS=0x3c499c542cef5e3811e1192ce70d8cc03d5c3359
POLYWEATHER_PAYMENT_TOKEN_DECIMALS=6
POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON=
POLYWEATHER_PAYMENT_CONFIRMATIONS=2
@@ -273,3 +267,4 @@ POLYMARKET_WALLET_ACTIVITY_MIN_VALUE_EXEMPT_USERS=
########################################
HTTPS_PROXY=
HTTP_PROXY=
POLYWEATHER_TELEGRAM_JOIN_INELIGIBLE_ACTION=decline
+4
View File
@@ -6,6 +6,9 @@
# Telegram
########################################
TELEGRAM_BOT_TOKEN=
POLYWEATHER_TELEGRAM_GROUP_ID=
POLYWEATHER_GROUP_MEMBER_PRICE_USDC=10
POLYWEATHER_PUBLIC_PRICE_USDC=10
########################################
# Supabase
@@ -32,6 +35,7 @@ METEOBLUE_API_KEY=
########################################
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
POLYWEATHER_PAYMENT_RECEIVER_CONTRACT=
POLYWEATHER_PAYMENT_DIRECT_RECEIVER_ADDRESS=
POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON=
POLYWEATHER_PAYMENT_PLAN_CATALOG_JSON=
+18
View File
@@ -62,3 +62,21 @@ jobs:
- name: Build Docker image
run: docker build -t polyweather-ci .
deploy:
needs: [python-quality, frontend-quality, docker-build]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Deploy to VPS
run: |
mkdir -p ~/.ssh
echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh -o StrictHostKeyChecking=accept-new ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} "
cd /root/PolyWeather
git pull origin main
docker compose up -d --build
sleep 10
curl -s http://localhost:8000/healthz
"
+9
View File
@@ -1,6 +1,9 @@
# Secrets
.env
# Scratch / temp scripts
scratch/
# Data and Logs
data/*.db
data/*.db-*
@@ -63,3 +66,9 @@ frontend/.next-start.log
.codex/skills/.system/**
!.codex/prompts/
!.codex/prompts/**
tmp_apikey.js
tmp_obs.js
tmp_rctp.html
playwright-home-check.png
.codex-backend-*.log
frontend-next-*.log
-447
View File
@@ -1,447 +0,0 @@
<!-- AUTONOMY DIRECTIVE — DO NOT REMOVE -->
YOU ARE AN AUTONOMOUS CODING AGENT. EXECUTE TASKS TO COMPLETION WITHOUT ASKING FOR PERMISSION.
DO NOT STOP TO ASK "SHOULD I PROCEED?" — PROCEED. DO NOT WAIT FOR CONFIRMATION ON OBVIOUS NEXT STEPS.
IF BLOCKED, TRY AN ALTERNATIVE APPROACH. ONLY ASK WHEN TRULY AMBIGUOUS OR DESTRUCTIVE.
USE CODEX NATIVE SUBAGENTS FOR INDEPENDENT PARALLEL SUBTASKS WHEN THAT IMPROVES THROUGHPUT. THIS IS COMPLEMENTARY TO OMX TEAM MODE.
<!-- END AUTONOMY DIRECTIVE -->
<!-- omx:generated:agents-md -->
# oh-my-codex - Intelligent Multi-Agent Orchestration
You are running with oh-my-codex (OMX), a coordination layer for Codex CLI.
This AGENTS.md is the top-level operating contract for the workspace.
Role prompts under `prompts/*.md` are narrower execution surfaces. They must follow this file, not override it.
When OMX is installed, load the installed prompt/skill/agent surfaces from `./.codex/prompts`, `./.codex/skills`, and `./.codex/agents` (or the project-local `./.codex/...` equivalents when project scope is active).
<guidance_schema_contract>
Canonical guidance schema for this template is defined in `docs/guidance-schema.md`.
Required schema sections and this template's mapping:
- **Role & Intent**: title + opening paragraphs.
- **Operating Principles**: `<operating_principles>`.
- **Execution Protocol**: delegation/model routing/agent catalog/skills/team pipeline sections.
- **Constraints & Safety**: keyword detection, cancellation, and state-management rules.
- **Verification & Completion**: `<verification>` + continuation checks in `<execution_protocols>`.
- **Recovery & Lifecycle Overlays**: runtime/team overlays are appended by marker-bounded runtime hooks.
Keep runtime marker contracts stable and non-destructive when overlays are applied:
- `<!-- OMX:RUNTIME:START --> ... <!-- OMX:RUNTIME:END -->`
- `<!-- OMX:TEAM:WORKER:START --> ... <!-- OMX:TEAM:WORKER:END -->`
</guidance_schema_contract>
<operating_principles>
- Solve the task directly when you can do so safely and well.
- Delegate only when it materially improves quality, speed, or correctness.
- Keep progress short, concrete, and useful.
- Prefer evidence over assumption; verify before claiming completion.
- Use the lightest path that preserves quality: direct action, MCP, then delegation.
- Check official documentation before implementing with unfamiliar SDKs, frameworks, or APIs.
- Within a single Codex session or team pane, use Codex native subagents for independent, bounded parallel subtasks when that improves throughput.
<!-- OMX:GUIDANCE:OPERATING:START -->
- Default to quality-first, intent-deepening responses; think one more step before replying or asking for clarification, and use as much detail as needed for a strong result without empty verbosity.
- Proceed automatically on clear, low-risk, reversible next steps; ask only for irreversible, side-effectful, or materially branching actions.
- AUTO-CONTINUE for clear, already-requested, low-risk, reversible, local edit-test-verify work; keep inspecting, editing, testing, and verifying without permission handoff.
- ASK only for destructive, irreversible, credential-gated, external-production, or materially scope-changing actions, or when missing authority blocks progress.
- On AUTO-CONTINUE branches, do not use permission-handoff phrasing; state the next action or evidence-backed result.
- Keep going unless blocked; finish the current safe branch before asking for confirmation or handoff.
- Ask only when blocked by missing information, missing authority, or an irreversible/destructive branch.
- Do not ask or instruct humans to perform ordinary non-destructive, reversible actions; execute those safe reversible OMX/runtime operations and ordinary commands yourself.
- Treat OMX runtime manipulation, state transitions, and ordinary command execution as agent responsibilities when they are safe and reversible.
- Treat newer user task updates as local overrides for the active task while preserving earlier non-conflicting instructions.
- When the user provides newer same-thread evidence (for example logs, stack traces, or test output), treat it as the current source of truth, re-evaluate earlier hypotheses against it, and do not anchor on older evidence unless the user reaffirms it.
- Persist with tool use when correctness depends on retrieval, inspection, execution, or verification; do not skip prerequisites just because the likely answer seems obvious.
- More effort does not mean reflexive web/tool escalation; browse or use tools when the task materially benefits, not as a default show of effort.
<!-- OMX:GUIDANCE:OPERATING:END -->
</operating_principles>
## Working agreements
- Write a cleanup plan before modifying code for cleanup/refactor/deslop work.
- Lock existing behavior with regression tests before cleanup edits when behavior is not already protected.
- Prefer deletion over addition.
- Reuse existing utils and patterns before introducing new abstractions.
- No new dependencies without explicit request.
- Keep diffs small, reviewable, and reversible.
- Run lint, typecheck, tests, and static analysis after changes.
- Final reports must include changed files, simplifications made, and remaining risks.
<lore_commit_protocol>
## Lore Commit Protocol
Every commit message must follow the Lore protocol — structured decision records using native git trailers.
Commits are not just labels on diffs; they are the atomic unit of institutional knowledge.
### Format
```
<intent line: why the change was made, not what changed>
<body: narrative context — constraints, approach rationale>
Constraint: <external constraint that shaped the decision>
Rejected: <alternative considered> | <reason for rejection>
Confidence: <low|medium|high>
Scope-risk: <narrow|moderate|broad>
Directive: <forward-looking warning for future modifiers>
Tested: <what was verified (unit, integration, manual)>
Not-tested: <known gaps in verification>
```
### Rules
1. **Intent line first.** The first line describes *why*, not *what*. The diff already shows what changed.
2. **Trailers are optional but encouraged.** Use the ones that add value; skip the ones that don't.
3. **`Rejected:` prevents re-exploration.** If you considered and rejected an alternative, record it so future agents don't waste cycles re-discovering the same dead end.
4. **`Directive:` is a message to the future.** Use it for "do not change X without checking Y" warnings.
5. **`Constraint:` captures external forces.** API limitations, policy requirements, upstream bugs — things not visible in the code.
6. **`Not-tested:` is honest.** Declaring known verification gaps is more valuable than pretending everything is covered.
7. **All trailers use git-native trailer format** (key-value after a blank line). No custom parsing required.
### Example
```
Prevent silent session drops during long-running operations
The auth service returns inconsistent status codes on token
expiry, so the interceptor catches all 4xx responses and
triggers an inline refresh.
Constraint: Auth service does not support token introspection
Constraint: Must not add latency to non-expired-token paths
Rejected: Extend token TTL to 24h | security policy violation
Rejected: Background refresh on timer | race condition with concurrent requests
Confidence: high
Scope-risk: narrow
Directive: Error handling is intentionally broad (all 4xx) — do not narrow without verifying upstream behavior
Tested: Single expired token refresh (unit)
Not-tested: Auth service cold-start > 500ms behavior
```
### Trailer Vocabulary
| Trailer | Purpose |
|---------|---------|
| `Constraint:` | External constraint that shaped the decision |
| `Rejected:` | Alternative considered and why it was rejected |
| `Confidence:` | Author's confidence level (low/medium/high) |
| `Scope-risk:` | How broadly the change affects the system (narrow/moderate/broad) |
| `Reversibility:` | How easily the change can be undone (clean/messy/irreversible) |
| `Directive:` | Forward-looking instruction for future modifiers |
| `Tested:` | What verification was performed |
| `Not-tested:` | Known gaps in verification |
| `Related:` | Links to related commits, issues, or decisions |
Teams may introduce domain-specific trailers without breaking compatibility.
</lore_commit_protocol>
---
<delegation_rules>
Default posture: work directly.
Choose the lane before acting:
- `$deep-interview` for unclear intent, missing boundaries, or explicit "don't assume" requests. This mode clarifies and hands off; it does not implement.
- `$ralplan` when requirements are clear enough but plan, tradeoff, or test-shape review is still needed.
- `$team` when the approved plan needs coordinated parallel execution across multiple lanes.
- `$ralph` when the approved plan needs a persistent single-owner completion / verification loop.
- **Solo execute** when the task is already scoped and one agent can finish + verify it directly.
Delegate only when it materially improves quality, speed, or safety. Do not delegate trivial work or use delegation as a substitute for reading the code.
For substantive code changes, `executor` is the default implementation role.
Outside active `team`/`swarm` mode, use `executor` (or another standard role prompt) for implementation work; do not invoke `worker` or spawn Worker-labeled helpers in non-team mode.
Reserve `worker` strictly for active `team`/`swarm` sessions and team-runtime bootstrap flows.
Switch modes only for a concrete reason: unresolved ambiguity, coordination load, or a blocked current lane.
</delegation_rules>
<child_agent_protocol>
Leader responsibilities:
1. Pick the mode and keep the user-facing brief current.
2. Delegate only bounded, verifiable subtasks with clear ownership.
3. Integrate results, decide follow-up, and own final verification.
Worker responsibilities:
1. Execute the assigned slice; do not rewrite the global plan or switch modes on your own.
2. Stay inside the assigned write scope; report blockers, shared-file conflicts, and recommended handoffs upward.
3. Ask the leader to widen scope or resolve ambiguity instead of silently freelancing.
Rules:
- Max 6 concurrent child agents.
- Child prompts stay under AGENTS.md authority.
- `worker` is a team-runtime surface, not a general-purpose child role.
- Child agents should report recommended handoffs upward.
- Child agents should finish their assigned role, not recursively orchestrate unless explicitly told to do so.
- Prefer inheriting the leader model by omitting `spawn_agent.model` unless a task truly requires a different model.
- Do not hardcode stale frontier-model overrides for Codex native child agents. If an explicit frontier override is necessary, use the current frontier default from `OMX_DEFAULT_FRONTIER_MODEL` / the repo model contract (currently `gpt-5.5`), not older values such as `gpt-5.2`.
- Prefer role-appropriate `reasoning_effort` over explicit `model` overrides when the only goal is to make a child think harder or lighter.
</child_agent_protocol>
<invocation_conventions>
- `$name` — invoke a workflow skill
- `/skills` — browse available skills
- Prefer skill invocation and keyword routing as the primary user-facing workflow surface
</invocation_conventions>
<model_routing>
Match role to task shape:
- Low complexity: `explore`, `style-reviewer`, `writer`
- Research/discovery: `explore` for repo lookup, `researcher` for official docs/reference gathering, `dependency-expert` for SDK/API/package evaluation
- Standard: `executor`, `debugger`, `test-engineer`
- High complexity: `architect`, `executor`, `critic`
For Codex native child agents, model routing defaults to inheritance/current repo defaults unless the caller has a concrete reason to override it.
</model_routing>
<specialist_routing>
Leader/workflow routing contract:
<!-- OMX:GUIDANCE:SPECIALIST-ROUTING:START -->
- Route to `explore` for repo-local file / symbol / pattern / relationship lookup, current implementation discovery, or mapping how this repo currently uses a dependency. `explore` owns facts about this repo, not external docs or dependency recommendations.
- Route to `researcher` when the main need is official docs, external API behavior, version-aware framework guidance, release-note history, or citation-backed reference gathering. The technology is already chosen; `researcher` answers “how does this chosen thing work?” and is not the default dependency-comparison role.
- Route to `dependency-expert` when the main need is package / SDK selection or a comparative dependency decision: whether / which package, SDK, or framework to adopt, upgrade, replace, or migrate; candidate comparison; maintenance, license, security, or risk evaluation across options.
- Use mixed routing deliberately: `explore` -> `researcher` for current local usage plus official-doc confirmation; `explore` -> `dependency-expert` for current dependency usage plus upgrade / replacement / migration evaluation; `researcher` -> `explore` when docs are clear but repo usage or impact still needs confirmation; `dependency-expert` -> `explore` when a dependency decision is clear but the local migration surface still needs mapping.
- Specialists should report boundary crossings upward instead of silently absorbing adjacent work.
- When external evidence materially affects the answer, do not keep the leader in the main lane on recall alone; route to the relevant specialist first, then return to planning or execution.
<!-- OMX:GUIDANCE:SPECIALIST-ROUTING:END -->
</specialist_routing>
---
<agent_catalog>
Key roles:
- `explore` — fast codebase search and mapping
- `planner` — work plans and sequencing
- `architect` — read-only analysis, diagnosis, tradeoffs
- `debugger` — root-cause analysis
- `executor` — implementation and refactoring
- `verifier` — completion evidence and validation
Research/discovery specialists:
- `explore` — first-stop repository lookup and symbol/file mapping
- `researcher` — official docs, references, and external fact gathering
- `dependency-expert` — SDK/API/package evaluation before adopting or changing dependencies
Specialists remain available through the role catalog and native child-agent surfaces when the task clearly benefits from them.
</agent_catalog>
---
<keyword_detection>
Keyword routing is implemented primarily by native `UserPromptSubmit` hooks and the generated keyword registry. Treat hook-injected routing context as authoritative for the current turn, then load the named `SKILL.md` or prompt file as instructed.
Fallback behavior when hook context is unavailable:
- Explicit `$name` invocations run left-to-right and override implicit keywords.
- Bare skill names do not activate skills by themselves; skill-name activation requires explicit `$skill` invocation. Natural-language routing phrases may still map to a workflow when they are not just the bare skill name. Examples: `analyze` / `investigate``$analyze` for read-only deep analysis with ranked synthesis, explicit confidence, and concrete file references; `deep interview`, `interview`, `don't assume`, or `ouroboros``$deep-interview`; `ralplan` / `consensus plan``$ralplan`; `cancel`, `stop`, or `abort``$cancel`.
- Keep the detailed keyword list in `src/hooks/keyword-registry.ts`; do not duplicate that table here.
Runtime availability gate:
- Treat `autopilot`, `ralph`, `ultrawork`, `ultraqa`, `team`/`swarm`, and `ecomode` as **OMX runtime workflows**, not generic prompt aliases.
- Auto-activate runtime workflows only when the current session is actually running under OMX CLI/runtime (for example, launched via `omx`, with OMX session overlay/runtime state available, or when the user explicitly asks to run `omx ...` in the shell).
- In Codex App or plain Codex sessions without OMX runtime, do **not** treat those keywords alone as activation. Explain that they require OMX CLI runtime support, and continue with the nearest App-safe surface (`deep-interview`, `ralplan`, `plan`, or native subagents) unless the user explicitly wants you to launch OMX from the shell.
- When deep-interview is active in OMX CLI/runtime, ask interview rounds via `omx question`; after launching `omx question` in a background terminal, wait for that terminal to finish and read the JSON answer before continuing; do not substitute `request_user_input` or ad hoc plain-text questioning, and respect Stop-hook blocking while a deep-interview question obligation is pending.
<triage_routing>
## Triage: advisory prompt-routing context
The keyword detector is the first and deterministic routing surface. Triage runs only when no keyword matches.
When active, triage emits **advisory prompt-routing context** — a developer-context string that the model may follow. It does not activate a skill or workflow by itself. It is a best-effort hint, not a guarantee.
Note: `explore`, `executor`, and `designer` are agent role-prompt files under `prompts/`, not workflow skills.
Explicit keywords remain the deterministic control surface when you want explicit, guaranteed routing — use them whenever exact behavior matters.
To opt out per prompt with phrases such as `no workflow`, `just chat`, or `plain answer` — the triage layer will suppress context injection for that prompt.
</triage_routing>
Ralph / Ralplan execution gate:
- Enforce **ralplan-first** when ralph is active and planning is not complete.
- Planning is complete only after both `.omx/plans/prd-*.md` and `.omx/plans/test-spec-*.md` exist.
- Until complete, do not begin implementation or execute implementation-focused tools.
</keyword_detection>
---
<skills>
Skills are workflow commands.
Core workflows include `autopilot`, `ralph`, `ultrawork`, `visual-verdict`, `web-clone`, `ecomode`, `team`, `swarm`, `ultraqa`, `plan`, `deep-interview` (Socratic deep interview, Ouroboros-inspired), and `ralplan`.
Utilities include `cancel`, `note`, `doctor`, `help`, and `trace`.
</skills>
---
<team_compositions>
Common team compositions remain available when explicit team orchestration is warranted, for example feature development, bug investigation, code review, and UX audit.
</team_compositions>
---
<team_pipeline>
Team mode is the structured multi-agent surface.
Canonical pipeline:
`team-plan -> team-prd -> team-exec -> team-verify -> team-fix (loop)`
Use it when durable staged coordination is worth the overhead. Otherwise, stay direct.
Terminal states: `complete`, `failed`, `cancelled`.
</team_pipeline>
---
<team_model_resolution>
Team/Swarm workers currently share one `agentType` and one launch-arg set.
Model precedence:
1. Explicit model in `OMX_TEAM_WORKER_LAUNCH_ARGS`
2. Inherited leader `--model`
3. Low-complexity default model from `OMX_DEFAULT_SPARK_MODEL` (legacy alias: `OMX_SPARK_MODEL`)
Normalize model flags to one canonical `--model <value>` entry.
Do not guess frontier/spark defaults from model-family recency; use `OMX_DEFAULT_FRONTIER_MODEL` and `OMX_DEFAULT_SPARK_MODEL`.
</team_model_resolution>
<!-- OMX:MODELS:START -->
## Model Capability Table
Auto-generated by `omx setup` from the current `config.toml` plus OMX model overrides.
| Role | Model | Reasoning Effort | Use Case |
| --- | --- | --- | --- |
| Frontier (leader) | `gpt-5.5` | high | Primary leader/orchestrator for planning, coordination, and frontier-class reasoning. |
| Spark (explorer/fast) | `gpt-5.3-codex-spark` | low | Fast triage, explore, lightweight synthesis, and low-latency routing. |
| Standard (subagent default) | `gpt-5.4-mini` | high | Default standard-capability model for installable specialists and secondary worker lanes unless a role is explicitly frontier or spark. |
| `explore` | `gpt-5.3-codex-spark` | low | Fast codebase search and file/symbol mapping (fast-lane, fast) |
| `analyst` | `gpt-5.5` | medium | Requirements clarity, acceptance criteria, hidden constraints (frontier-orchestrator, frontier) |
| `planner` | `gpt-5.5` | medium | Task sequencing, execution plans, risk flags (frontier-orchestrator, frontier) |
| `architect` | `gpt-5.5` | high | System design, boundaries, interfaces, long-horizon tradeoffs (frontier-orchestrator, frontier) |
| `debugger` | `gpt-5.4-mini` | high | Root-cause analysis, regression isolation, failure diagnosis (deep-worker, standard) |
| `executor` | `gpt-5.5` | medium | Code implementation, refactoring, feature work (deep-worker, standard) |
| `team-executor` | `gpt-5.5` | medium | Supervised team execution for conservative delivery lanes (deep-worker, frontier) |
| `verifier` | `gpt-5.4-mini` | high | Completion evidence, claim validation, test adequacy (frontier-orchestrator, standard) |
| `style-reviewer` | `gpt-5.3-codex-spark` | low | Formatting, naming, idioms, lint conventions (fast-lane, fast) |
| `quality-reviewer` | `gpt-5.4-mini` | medium | Logic defects, maintainability, anti-patterns (frontier-orchestrator, standard) |
| `api-reviewer` | `gpt-5.4-mini` | medium | API contracts, versioning, backward compatibility (frontier-orchestrator, standard) |
| `security-reviewer` | `gpt-5.5` | medium | Vulnerabilities, trust boundaries, authn/authz (frontier-orchestrator, frontier) |
| `performance-reviewer` | `gpt-5.4-mini` | medium | Hotspots, complexity, memory/latency optimization (frontier-orchestrator, standard) |
| `code-reviewer` | `gpt-5.5` | high | Comprehensive review across all concerns (frontier-orchestrator, frontier) |
| `dependency-expert` | `gpt-5.4-mini` | high | External SDK/API/package evaluation (frontier-orchestrator, standard) |
| `test-engineer` | `gpt-5.5` | medium | Test strategy, coverage, flaky-test hardening (deep-worker, frontier) |
| `quality-strategist` | `gpt-5.4-mini` | medium | Quality strategy, release readiness, risk assessment (frontier-orchestrator, standard) |
| `build-fixer` | `gpt-5.4-mini` | high | Build/toolchain/type failures resolution (deep-worker, standard) |
| `designer` | `gpt-5.4-mini` | high | UX/UI architecture, interaction design (deep-worker, standard) |
| `writer` | `gpt-5.4-mini` | high | Documentation, migration notes, user guidance (fast-lane, standard) |
| `qa-tester` | `gpt-5.4-mini` | low | Interactive CLI/service runtime validation (deep-worker, standard) |
| `git-master` | `gpt-5.4-mini` | high | Commit strategy, history hygiene, rebasing (deep-worker, standard) |
| `code-simplifier` | `gpt-5.5` | high | Simplifies recently modified code for clarity and consistency without changing behavior (deep-worker, frontier) |
| `researcher` | `gpt-5.4-mini` | high | External documentation and reference research (fast-lane, standard) |
| `product-manager` | `gpt-5.4-mini` | medium | Problem framing, personas/JTBD, PRDs (frontier-orchestrator, standard) |
| `ux-researcher` | `gpt-5.4-mini` | medium | Heuristic audits, usability, accessibility (frontier-orchestrator, standard) |
| `information-architect` | `gpt-5.4-mini` | low | Taxonomy, navigation, findability (frontier-orchestrator, standard) |
| `product-analyst` | `gpt-5.4-mini` | low | Product metrics, funnel analysis, experiments (frontier-orchestrator, standard) |
| `critic` | `gpt-5.5` | high | Plan/design critical challenge and review (frontier-orchestrator, frontier) |
| `vision` | `gpt-5.5` | low | Image/screenshot/diagram analysis (fast-lane, frontier) |
<!-- OMX:MODELS:END -->
---
<verification>
Verify before claiming completion.
Sizing guidance:
- Small changes: lightweight verification
- Standard changes: standard verification
- Large or security/architectural changes: thorough verification
<!-- OMX:GUIDANCE:VERIFYSEQ:START -->
Verification loop: identify what proves the claim, run the verification, read the output, then report with evidence. If verification fails, continue iterating rather than reporting incomplete work. Default to quality-first evidence summaries: think one more step before declaring completion, and include enough detail to make the proof actionable without padding.
- Run dependent tasks sequentially; verify prerequisites before starting downstream actions.
- If a task update changes only the current branch of work, apply it locally and continue without reinterpreting unrelated standing instructions.
- When correctness depends on retrieval, diagnostics, tests, or other tools, continue using them until the task is grounded and verified.
<!-- OMX:GUIDANCE:VERIFYSEQ:END -->
</verification>
<execution_protocols>
Mode selection:
- Use `$deep-interview` first when the request is broad, intent/boundaries are unclear, or the user says not to assume.
- Use `$ralplan` when the requirements are clear enough but architecture, tradeoffs, or test strategy still need consensus.
- Use `$team` when the approved plan has multiple independent lanes, shared blockers, or durable coordination needs.
- Use `$ralph` when the approved plan should stay in a persistent completion / verification loop with one owner.
- Otherwise execute directly in solo mode.
- Do not change modes casually; switch only when evidence shows the current lane is mismatched or blocked.
Command routing:
- When `USE_OMX_EXPLORE_CMD` enables advisory routing, strongly prefer `omx explore` as the default surface for simple read-only repository lookup tasks (files, symbols, patterns, relationships).
- For simple file/symbol lookups, use `omx explore` FIRST before attempting full code analysis.
When to use what:
- Use `omx explore --prompt ...` for simple read-only lookups.
- Use `omx sparkshell` for noisy read-only shell commands, bounded verification runs, repo-wide listing/search, or tmux-pane summaries; `omx sparkshell --tmux-pane ...` is explicit opt-in.
- Keep ambiguous, implementation-heavy, edit-heavy, or non-shell-only work on the richer normal path.
- `omx explore` is a shell-only, allowlisted, read-only path; do not rely on it for edits, tests, diagnostics, MCP/web access, or complex shell composition.
- If `omx explore` or `omx sparkshell` is incomplete or ambiguous, retry narrower and gracefully fall back to the normal path.
Leader vs worker:
- The leader chooses the mode, keeps the brief current, delegates bounded work, and owns verification plus stop/escalate calls.
- Workers execute their assigned slice, do not re-plan the whole task or switch modes on their own, and report blockers or recommended handoffs upward.
- Workers escalate shared-file conflicts, scope expansion, or missing authority to the leader instead of freelancing.
Stop / escalate:
- Stop when the task is verified complete, the user says stop/cancel, or no meaningful recovery path remains.
- Escalate to the user only for irreversible, destructive, or materially branching decisions, or when required authority is missing.
- Escalate from worker to leader for blockers, scope expansion, shared ownership conflicts, or mode mismatch.
- `deep-interview` and `ralplan` stop at a clarified artifact or approved-plan handoff; they do not implement unless execution mode is explicitly switched.
Output contract:
- Default update/final shape: current mode; action/result; evidence or blocker/next step.
- Keep rationale once; do not restate the full plan every turn.
- Expand only for risk, handoff, or explicit user request.
Parallelization:
- Run independent tasks in parallel.
- Run dependent tasks sequentially.
- Use background execution for builds and tests when helpful.
- Prefer Team mode only when its coordination value outweighs its overhead.
- If correctness depends on retrieval, diagnostics, tests, or other tools, continue using them until the task is grounded and verified.
Anti-slop workflow:
- Cleanup/refactor/deslop work still follows the same `$deep-interview` -> `$ralplan` -> `$team`/`$ralph` path; use `$ai-slop-cleaner` as a bounded helper inside the chosen execution lane, not as a competing top-level workflow.
- Lock behavior with tests first, then make one smell-focused pass at a time.
- Prefer deletion, reuse, and boundary repair over new layers.
- Keep writer/reviewer pass separation for cleanup plans and approvals.
Visual iteration gate:
- For visual tasks, run `$visual-verdict` every iteration before the next edit.
- Persist verdict JSON in `.omx/state/{scope}/ralph-progress.json`.
Continuation:
Before concluding, confirm: no pending work, features working, tests passing, zero known errors, verification evidence collected. If not, continue.
Ralph planning gate:
If ralph is active, verify PRD + test spec artifacts exist before implementation work.
</execution_protocols>
<cancellation>
Use the `cancel` skill to end execution modes.
Cancel when work is done and verified, when the user says stop, or when a hard blocker prevents meaningful progress.
Do not cancel while recoverable work remains.
</cancellation>
---
<state_management>
Hooks own normal skill-active and workflow-state persistence under `.omx/state/`.
OMX persists runtime state under `.omx/`:
- `.omx/state/` — mode state
- `.omx/notepad.md` — session notes
- `.omx/project-memory.json` — cross-session memory
- `.omx/plans/` — plans
- `.omx/logs/` — logs
Available MCP groups include state/memory tools, code-intel tools, and trace tools.
Agents may use OMX state/MCP tools for explicit lifecycle transitions, recovery, checkpointing, cancellation cleanup, or compaction resilience.
Do not manually duplicate hook-owned activation state unless recovering from missing or stale state.
</state_management>
---
## Setup
Execute `omx setup` to install all components. Execute `omx doctor` to verify installation.
+55 -1
View File
@@ -1,5 +1,59 @@
# Changelog
## 1.7.0 - 2026-05-23
### 新增能力
- 市场监控面板(MonitorPanel):22 城实时温度监控,温度分辨率链(AMOS 跑道 → airport_primary → airport_current → current),按数据源新鲜度驱动刷新
- 中国城市天气日报:AI 生成每日天气摘要,接入 CMA weather.com.cn 预报数据,推送至 Telegram 论坛群
- 后台管理系统重写:从 1694 行单页拆分为 9 个模块(总览、会员、订阅、支付、训练、Telegram 审计、健康检查、配置、日志),含漏斗图、KPI 卡片、缓存饼图、增长趋势图
- 跑道观测系统重构:全跑道展示、结算跑道标注、热力模型、风场分析,推送增加市场状态标签(超预期/升温中/冲顶观察/降温中)
- 新增 6 个高频数据源:AEROWEB (Météo-France)、NCM (沙特)、IMS Lod (以色列)、AMSC AWOS (中国跑道)、MSS 1 分钟 (新加坡)、AROME HD 15 分钟 (巴黎)
- 接入 HKO 1 分钟、流浮山 LFS 1 分钟、CWA 10 分钟 (台北松山) 实时温度
- NOAA MADIS HFMETAR 适配新格式(netCDF stationId 替代 icaoId+ 目录迁移适配
- KNMI 适配新数据布局 (station,time) + 5 位 WMO 码 + S3 下载认证修复
- 新增 GET /api/cities/model-range 端点
- 积分转账功能:管理员手动扣除/划转用户积分
- 支付提交前 Tx 预校验:链上验签收款地址与金额
- CI 全流程自动化:测试通过后自动 SSH 部署到 VPS
- 一键部署脚本:deploy.sh + deploy.ps1
### 移除
- 删除 LGBM 全部代码和模型文件,EMOS 简化为纯 legacy 高斯分桶
- 删除 Polymarket 价格拉取与 UI 层(MarketDecisionLine
- 删除 Groq、Meteoblue、NMC、俄罗斯 pogodaiklimat 数据源
- 删除预热(prewarm)系统
- 删除市场提醒引擎(market_alert_engine
- 删除 Lagos、Masroor Air Base 城市
- 移除季付/年付计划,统一月付 10 USDC
### 修复与优化
- 修复移动端城市列表搜索无数据、Leaflet flyTo NaN 崩溃
- 修复 MacBook Safari 布局崩溃(100vw/dvh、-webkit-backdrop-filter、grid minmax 溢出)
- 修复温度曲线图三个渲染问题:数据点过少、张力过高、canvas CSS 拉伸
- 修复 Open-Meteo 冷却期无限循环导致多模型数据缺失
- 修复转化漏斗数据显示 3750%(前端重复乘以 100)
- 多模型缓存优化 + ETag 缓存 + stale-while-revalidate
- 性能优化:Context 重渲染、LGBM 循环移除、TTL 对齐
- 账户页 Pro 状态偶发性丢失修复
- 机场推送重构:观测缓存分离 + 全城市覆盖 + 四路并发
- 全面修复前端 UI 设计审查 15 项问题:消除工程债务、统一 token 体系、提升可维护性
- CSS 架构:消除 !important 滥用(134→49,仅保留 Leaflet/图表所必需项)、浅色主题重构为 `html.light` 选择器体系
- 统一断点体系:18→10480/640/768/960/1024/1200/1280/1360/1440/1680),对齐 Tailwind 标准
- CSS 变量迁移:10 个文件中数百处硬编码颜色(#4DA3FF/#E6EDF3/#9FB2C7/#6B7A90)替换为 token 变量
- 字体系统修复:13 个文件中所有非标准 font-weight760/850/860/880/950)映射为 Inter 支持值
- 移除未加载的 Geist 字体声明、提升文字对比度 #6B7A90#7D8FA3
- 修复 accent-green 类错误渲染为蓝色、accent-primary 与 accent-secondary 相同值问题
- 创建 scan-root-styles.ts 桶文件,将 22 个 CSS Module 导入合并为 1 个
- 添加全局 :focus-visible 轮廓环、跳过链接、Tab ARIA 属性
- 添加统一的 empty/error/retry 状态组件、prefers-reduced-motion 支持
- 去重 @keyframesspin 4→1、loading-spin 2→0、pulse-pending 移至全局
- 添加 CSS 渐变品牌 Logo、按钮层级文档化
- 移除 dead code1,697 行):public/static/style.css + public/legacy/index.html
- Dashboard.module.css 本地变量桥接至全局 token
- 清理冗余文档:移除 FRONTEND_REDESIGN_REPORT.md、TECH_DEBT.md 重复文件、AGENTS.md
- 参考:docs/frontend-ui-design-review.md 完整修复记录
## 1.5.5 - 2026-04-27
- Dashboard 新增 v1.5.5 升级公告,提示所有会员已额外延长 7 天,并集中说明 DeepSeek 机场报文解读、日历行动视图、本地时间峰值窗口和 AI 证据护栏
@@ -35,7 +89,7 @@
- 右侧详情面板识别稀疏 detail / 单日 forecast 中间态,并显示同步占位卡,避免用户把未补齐数据误认为完整结果
- 概率区改为“校准模型概率”:有 LGBM 时展示 LGBM 校准概率;模型共识与市场价格降级为辅助参考
- 模型层补齐 DWD ICON、ECMWF AIFS、ECCC GEM/GDPS/RDPS/HRDPS 等开放模型说明,并明确 AIFS 不称作“AI 预报”
- 新增 / 补齐 Manila、Karachi、Masroor Air Base 等城市说明;机场市场以 METAR / 机场主站为结算锚点,Wunderground 仅作为历史页面或参考入口
- 新增 / 补齐 Manila、Karachi 等城市说明;机场市场以 METAR / 机场主站为结算锚点,Wunderground 仅作为历史页面或参考入口
- 历史对账、模型栈、LGBM、监控、前端 README 与网页 `/docs` 文档同步更新到当前产品口径
## 1.5.3 - 2026-04-10
+81 -21
View File
@@ -6,6 +6,27 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
PolyWeather Pro — a production weather-intelligence stack for temperature settlement markets. Aggregates observations and forecasts for 52 monitored cities globally, blends multi-model highs using DEB (Dynamic Error Balancing), generates calibrated probability buckets for settlement, maps weather to Polymarket quotes for mispricing scans, and serves both a Next.js dashboard (Vercel) and a Telegram bot.
## Environment & Preferences (ALWAYS follow)
### Working Directory
- All commands run from the repo root
- Python virtual env: `venv\Scripts\activate` (Windows) / `source venv/bin/activate` (Linux/macOS)
- Frontend dev server: `cd frontend && npm run dev` → http://localhost:3000
- Backend API server: `uvicorn web.app:app --reload --host 0.0.0.0 --port 8000` → http://localhost:8000
- When I say "start the server", assume the working directory is the repo root
### Git Conventions
- **Commit language: Chinese (简体中文) ONLY**
- Format: Lore Commit Protocol — intent line in Chinese, trailers in English
- Examples: `重构城市决策卡 hero 布局` or `统一 DEB 数据源为单一计算路径`
- **NEVER** use English for commit subject lines
### Tooling
- Package manager: **npm** (not yarn/pnpm)
- Python: `python` (not python3), venv at `venv/`
- Lint: `ruff check .` (Python) + `npx tsc --noEmit` (TypeScript)
- NEVER ask me about these preferences again — commit to memory
## Architecture
```
@@ -18,12 +39,15 @@ Users (Web / Telegram) → Next.js Frontend (Vercel) → FastAPI /web/app.py
Payment Layer (Intent + Event + Confirm Loop)
```
- **Backend**: FastAPI on port 8000 (`web/app.py``web/core.py` + `web/routes.py` + `web/analysis_service.py`)
- **Frontend**: Next.js 15 + React 19 + TypeScript + Tailwind CSS 3 on port 3000 (dev)
- **Backend**: FastAPI on port 8000 (`web/app.py``web/app_factory.py` `web/routers/` (8 route modules: `system`, `city`, `auth`, `analytics`, `scan`, `payments`, `ops`, `routes` (legacy)) + `web/services/` (14 service modules) + `web/core.py`)
- **Frontend**: Next.js 15 + React 19 + TypeScript + Tailwind CSS 3 + shadcn/ui (new-york style) on port 3000 (dev)
- **Bot**: Telegram bot via `bot_listener.py``src/bot/`
- **Shared analysis core** in `src/` is used by both web API and bot
- **Scan Terminal**: Real-time city opportunity scanning (`web/scan_terminal_service.py` and `frontend/components/dashboard/scan-terminal/`)
- **Dashboard**: Main dashboard with interactive map, city sidebar, detail panels, and probability views
- **Market Monitor** (`MonitorPanel`): Real-time temperature monitoring board for 22 trading cities. Uses a temperature resolution chain (AMOS runway → AMOS → `airport_primary``airport_current``current`) defined in `frontend/components/dashboard/monitoring/monitor-temperature.ts`. Per-city refresh decisions driven by source-aware freshness (`source-freshness.ts`) instead of uniform `obs_age_min`. Seoul/Busan display runway surface temperature from AMOS; US cities get 5-min MADIS HFMETAR via `airport_primary`; others fall back to METAR.
- **High-Freq Airport Pipeline**: 19 of 22 monitor cities have dedicated realtime sources (AMOS, MADIS, JMA, MGM, FMI, KNMI, AROME). Data flows: `weather_sources.py` (fetch) → `country_networks.py` (`_airport_primary_from_raw`, per-country providers) → API `airport_primary` field. Plain METAR stays in `airport_current`. Documented in `docs/AIRPORT_REALTIME_SOURCES.md`.
- **Country Network Providers**: `country_networks.py` routes per-city to the right provider (Turkey→MGM, Korea→KMA, Japan→JMA, etc.) via `get_country_network_provider()`. Each provider controls `airport_primary_current`, `official_nearby_current`, and `official_network_status`. US cities use the default `GlobalMetarNetworkProvider` but get MADIS overrides injected via `results["madis_hfmetar_current"]`.
## Commands
@@ -31,9 +55,12 @@ Users (Web / Telegram) → Next.js Frontend (Vercel) → FastAPI /web/app.py
```bash
cd frontend
npm ci
npm run dev # Next.js dev server
npm run build # Production build
npm run lint # ESLint via next lint
npm run dev # Next.js dev server (runs sync-next-server-chunks.mjs first)
npm run build # Production build (runs sync-next-server-chunks.mjs after)
npm run start # Production server
npm run lint # ESLint via next lint
npm run typecheck # tsc --noEmit
npm run test:business # Business state tests via scripts/run-business-state-tests.mjs (also runs in CI)
```
### Backend (dev on port 8000)
@@ -50,17 +77,23 @@ python run.py
### Docker (production-like stack)
```bash
docker compose up -d --build # bot + web API
docker compose --profile workers up -d # + prewarm worker
docker compose --profile monitoring up -d # + Prometheus/Grafana/Alertmanager
docker compose up -d --build # bot + web API (polyweather + polyweather_web)
```
The compose file defines two services: `polyweather` (bot) and `polyweather_web` (FastAPI on :8000). Prewarm worker and monitoring profiles were removed in v1.6.0.
### Python tests
```bash
pytest tests/ # all tests
pytest tests/test_web_observability.py # single test file
python -m pytest tests/ # all tests
python -m pytest tests/test_web_observability.py # single test file
```
### Version bump (see RELEASE.md)
```bash
python scripts/bump_version.py patch # or minor / major / 1.5.0
python scripts/sync_version.py # verify sync across files
```
`VERSION` file is the single source of truth; frontend `package.json` and docs sync from it.
### Lint & Format
```bash
ruff check . # Python lint (pycodestyle + Pyflakes, line-length 88)
@@ -78,21 +111,25 @@ curl http://127.0.0.1:8000/metrics
| Directory | Purpose |
|-----------|---------|
| `src/data_collection/` | Weather sources (METAR, TAF, Open-Meteo, JMA, KMA, MGM, NMC, Russia stations, settlement sources), city registry (52 cities), Polymarket readonly layer |
| `src/analysis/` | DEB algorithm, trend engine, probability calibration (EMOS/LGBM), market alert engine, settlement rounding |
| `src/models/` | LightGBM daily-high model training and feature engineering |
| `src/payments/` | Onchain checkout, event listener, confirm loop, contract audit |
| `src/analysis/` | DEB algorithm, trend engine, market alert engine, settlement rounding |
| `src/auth/` | Supabase entitlement checks, Telegram group pricing |
| `src/bot/` | Telegram bot handlers and orchestrator |
| `src/database/` | SQLite-based runtime state, DB manager, daily/truth/training feature repositories |
| `web/` | FastAPI app, routes (~65K), analysis service (~130K), scan terminal service (~56K), AI scan modules |
| `src/data_collection/` | Weather sources (METAR, TAF, Open-Meteo, JMA, KMA, MGM, NMC, Russia stations, settlement sources), city registry (52 cities), Polymarket readonly layer. Also: `madis_sources.py` (NOAA 5-min NetCDF), `amos_station_sources.py` (Korean runway sensors), `country_networks.py` (per-country provider routing + `_airport_primary_from_raw`) |
| `src/data_mining/` | Historical data fetch utilities |
| `src/onchain/` | Polygon wallet watcher, Polymarket wallet activity watcher |
| `src/payments/` | Onchain checkout, event listener, confirm loop, contract audit |
| `src/strategy/` | Trading strategy modules |
| `src/trading/` | Trading execution modules |
| `src/utils/` | Shared utilities: config loader, logging, metrics, Telegram push, chat ID helpers |
| `web/` | FastAPI app (`app.py``app_factory.py`), `routers/` (8 route modules), `services/` (14 service modules), `core.py`, scan terminal modules (AI fallback, AI prompts, METAR gate, city rows, ranker, cache) |
| `frontend/app/` | Next.js App Router pages (dashboard, account, auth, docs, ops, probabilities, scan) |
| `frontend/components/dashboard/` | Dashboard UI components (map, sidebar, detail panel, modals, charts, scan terminal) |
| `frontend/lib/` | Shared client logic: types, API client, chart utils, i18n, dashboard utils |
| `frontend/components/dashboard/` | Dashboard UI components (map, sidebar, detail panel, modals, charts, scan terminal). `scan-root-styles.ts` is the CSS Module barrel, combining 22 module roots into one pre-composed className. `monitoring/` subdirectory: `MonitorPanel`, `monitor-temperature.ts` (temp resolution chain), `monitor-refresh-policy.ts`. |
| `frontend/lib/` | Shared client logic: types (`dashboard-types.ts`, including `AirportCurrentConditions`, `CityDetail`), API client, chart utils, i18n, `source-freshness.ts` (per-source freshness with `expected_next_update_at`), dashboard utils |
| `frontend/hooks/` | React hooks: dashboard store (global state), Leaflet map, chart helper |
| `scripts/` | Operational scripts: probability calibration training, backfills, payment reconciliation, prewarm worker |
| `scripts/` | Operational scripts: backfills, payment reconciliation. `supabase/` subdirectory: DB schema and migration SQL. |
| `config/` | YAML config (city list, weather settings, logging) |
| `docs/` | Bilingual product & technical docs |
| `monitoring/` | Prometheus/Grafana/Alertmanager configs |
## Key Technical Details
@@ -101,11 +138,34 @@ curl http://127.0.0.1:8000/metrics
- **Frontend package manager**: npm
- **State storage**: SQLite primary path (set via `POLYWEATHER_STATE_STORAGE_MODE=sqlite` + `POLYWEATHER_DB_PATH`). Legacy JSON/JSONL files are migration/fallback only.
- **Runtime data**: External dir recommended (`POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather`) to avoid git conflicts
- **Auth gating** (frontend middleware): Token-based (`POLYWEATHER_DASHBOARD_ACCESS_TOKEN`) or Supabase session-based (`POLYWEATHER_AUTH_ENABLED`). Local dev hosts bypass auth.
- **Configuration**: `.env.example` is the comprehensive reference (8 config sections: runtime, Telegram, weather cache, auth, ops, frontend, optional modules, Polygon monitor). Copy to `.env` and fill in secrets.
- **Auth gating** (frontend middleware): Three-tier priority in `middleware.ts` — (1) local dev hosts (localhost / 127.0.0.1 / ::1) bypass auth entirely, (2) Supabase session-based when `POLYWEATHER_AUTH_ENABLED=true` via `handleSupabaseAuthGate` or `handleSupabaseOptionalSession`, (3) legacy token fallback via `POLYWEATHER_DASHBOARD_ACCESS_TOKEN` cookie/query-param. Public pages (`/`, `/docs`, `/auth/*`, `/entitlement-required`) and public API routes are always accessible.
- **CORS**: Allowed origins from `WEB_CORS_ORIGINS` env var (defaults: localhost:3000, polyweather-pro.vercel.app)
- **EMOS/CRPS calibration**: Trainable but production should use `legacy` or `emos_shadow` engine; `emos_primary` only after local evaluation + manual rollout
- **API proxy**: Frontend uses Next.js rewrites to proxy `/api/*` to the FastAPI backend; see `frontend/lib/api-proxy.ts` and `frontend/lib/backend-api.ts`
## Commit Convention
This repo uses the **Lore Commit Protocol** — structured decision records with git trailers (`Constraint:`, `Rejected:`, `Confidence:`, `Scope-risk:`, `Directive:`, `Tested:`, `Not-tested:`). Intent line first (why, not what).
- Always write git commit messages in **Chinese (简体中文)**.
## Code Style
- Never use Unicode escape sequences (`\uXXXX`) in source code; write characters directly in UTF-8 encoding.
- When modifying UI components, update both **dark-mode and light-mode CSS files** in the same edit batch.
- **CSS Variables First**: Prefer `var(--color-*)` / `var(--color-signal-*)` tokens over hardcoded hex values. The token system is defined in `globals.css` with light-theme overrides under `html.light`.
- **Avoid `!important`**: Only use it for Leaflet map overrides (inline style conflict) and chart canvas sizing. For light-theme overrides, use `html.light .root` prefix for higher specificity.
- **Monitoring CSS note**: `MonitorPanel.module.css` scopes its light-theme overrides to `.scan-terminal.light` (the terminal's built-in toggle), NOT `html.light`. When adding light styles for monitoring components, match this scoping.
- **New CSS Modules**: Add the module root class to `scan-root-styles.ts` barrel file instead of importing it separately in `ScanTerminalDashboard.tsx`.
## Quality Gates (MANDATORY)
Before marking any task as complete, you MUST:
1. **Type check** — Run `npx tsc --noEmit` (frontend) or `python -m ruff check .` (backend) on modified files
2. **No Unicode escapes** — Verify that NO `\uXXXX` sequences were introduced; if found, revert and fix
3. **Dual-theme CSS** — For any UI change, confirm BOTH dark and light styles. Most components need `ScanTerminalLightTheme.module.css` updated; monitoring components (`MonitorPanel.module.css`) contain their own `.scan-terminal.light` blocks inline.
4. **No new hardcoded palette colors** — Use `var(--color-*)` token references instead of `#4DA3FF` / `#E6EDF3` / `#9FB2C7` / `#6B7A90` hex values
5. **Show the diff** — Output `git diff --stat` and test results before declaring success
If any gate fails, fix it BEFORE reporting success.
+8 -9
View File
@@ -1,26 +1,25 @@
# syntax=docker/dockerfile:1
FROM python:3.11-slim
# 设置工作目录
WORKDIR /app
# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_ROOT_USER_ACTION=ignore \
TZ=UTC
# 安装系统依赖 (如果有必要的包可以取消注释)
# RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/*
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
gcc libhdf5-dev libnetcdf-dev && \
rm -rf /var/lib/apt/lists/*
# 复制 requirements 文件
COPY requirements.txt .
# 安装 Python 依赖
RUN pip install --no-cache-dir --prefer-binary -r requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --prefer-binary -r requirements.txt
# 复制项目代码
COPY . .
# 启动机器人
CMD ["python", "bot_listener.py"]
-82
View File
@@ -1,82 +0,0 @@
# 前端交付与重构报告(v1.5.1)
最后更新:`2026-03-14`
## 1. 报告目的
说明当前线上前端(`frontend/`)在收费阶段的实际交付状态。
## 2. 当前前端架构
```mermaid
flowchart LR
B["Browser"] --> N["Next.js App Router (Vercel)"]
N --> RH["Route Handlers /api/*"]
RH --> F["FastAPI (VPS)"]
N --> STORE["Dashboard Store"]
STORE --> MAP["MapCanvas"]
STORE --> SIDEBAR["CitySidebar"]
STORE --> PANEL["DetailPanel + Modal"]
STORE --> ACCOUNT["Account Center + Pro Overlay"]
```
## 3. 已落地能力
### 3.1 信息架构与交互
- 风险分组侧栏折叠(持久化)。
- 选中城市状态持久化。
- 今日分析、历史对账、未来日期分析联动。
### 3.2 收费相关
- 账户中心(登录态、积分、订阅状态、钱包管理)。
- Pro 解锁浮层(套餐、积分抵扣、FAQ、社群入口)。
- 钱包绑定:浏览器扩展钱包 + WalletConnect 扫码。
- 支付流程:create intent -> submit -> confirm。
- `confirm pending` 时自动轮询 intent 状态,确认后自动刷新订阅态。
### 3.3 缓存与性能
- BFF `ETag/304``cities` / `summary` / `history`
- `summary?force_refresh=true` => `no-store`
- `sessionStorage` + in-flight 去重。
- `localStorage`:选中城市、侧栏折叠状态。
### 3.4 可访问性与稳定性
- 详情面板 `inert + blur` 焦点冲突修复。
- 关键支付错误文案标准化(用户取消、gas 不足、pending)。
## 4. 当前明确未做
- 离线能力(Service Worker / IndexedDB
- 前端级财务报表与退款后台(后端/运营侧)
## 5. 验收建议
### 5.1 前端构建
```bash
cd frontend
npm run build
```
### 5.2 缓存验收
```bash
./scripts/validate_frontend_cache.sh "https://polyweather-pro.vercel.app"
```
### 5.3 支付验收
- 绑定钱包
- 创建 intent
- 发交易
- 验证 `intent` 状态从 `submitted -> confirmed`
- 校验账户页订阅状态更新
## 6. 结论
前端已具备收费阶段的核心能力(账户、支付、权限展示、状态回收),可支持持续商业迭代。
+18 -11
View File
@@ -17,10 +17,16 @@ Public docs center: `/docs/intro` on the main site (bilingual product documentat
![PolyWeather Ankara analysis](docs/images/demo_ankara.png)
## Product Status (2026-04-27)
## Star History
- Subscription live: `Pro Monthly 5 USDC`.
- Points redemption live: `500 points = 1 USDC`, max `3 USDC` off.
[![Star History Chart](https://api.star-history.com/svg?repos=yangyuan-zhen/PolyWeather&type=Date)](https://star-history.com/#yangyuan-zhen/PolyWeather&Date)
## Product Status (2026-05-11)
- Subscription live: `Pro Monthly 10 USDC`.
- Points system live: earn via group chat, welcome bonus (+20), first-message-of-day bonus (+2), weekly participation rewards.
- `/city` and `/deb` now free (daily cap 10 each); points redeemable for payment discount (`500 pts = 1 USDC`, max `3 USDC`).
- Weekly leaderboard rewards restructured: smaller point bonuses for winners (200/100/50), all active users receive participation rewards.
- Onchain checkout live: Polygon contract checkout (USDC / USDC.e).
- Auto-reconciliation live: event listener + periodic confirm loop.
- Ops dashboard live: `/ops` for memberships, leaderboard, manual point grants, and payment incident triage.
@@ -38,12 +44,13 @@ Public docs center: `/docs/intro` on the main site (bilingual product documentat
- Temperature chart now overlays `TAF Timing` markers near the expected peak window.
- Trade cue now combines upper-air structure, `TAF`, market crowding, and `edge_percent`.
- Browser extension now uses `DEB` for multi-day forecast and stays positioned as a lightweight lead-in to the main site.
- Official nearby-network layer now covers `MGM` (Turkey), `CMA/NMC` (Mainland China), `JMA AMeDAS` (Japan), `KMA` (Korea), `HKO` (Hong Kong), and `CWA` (Taiwan).
- Official nearby-network layer now covers `MGM` (Turkey), `CMA/NMC` (Mainland China), `JMA AMeDAS` (Japan), `AMOS` (Korea, runway-level, Seoul/Busan), `HKO` (Hong Kong), and `CWA` (Taiwan).
- Tokyo now ingests Haneda `JMA AMeDAS` 10-minute temperature as the official enhancement layer.
- Dashboard prewarm is now supported through a dedicated worker / cron path, with runtime status exposed in `/api/system/status` and `/ops`.
- `/ops` now exposes cache bucket counts, summary cache hit / miss rate, and prewarm runtime heartbeat.
- Intraday commentary can optionally use `Groq` as a bilingual rewrite layer, while rule-based commentary remains the fallback.
- Vercel frontend guidance now includes cost controls for analytics, eager fetches, and edge-side scanner blocking.
- Frontend design system overhauled: unified CSS token system, eliminated `!important` abuse (68→6 in light theme), consolidated breakpoints (18→10), migrated hardcoded colors to CSS variables, added ARIA attributes and focus-visible keyboard navigation. See `docs/frontend-ui-design-review.md` for the full audit trail.
## License & Commercial Boundary
@@ -57,7 +64,7 @@ See: [AGPL-3.0 & Commercial Boundary](docs/OPEN_CORE_POLICY.md)
## Core Capabilities
- Aggregates observations and forecasts for 52 monitored cities.
- Aggregates observations and forecasts for 51 monitored cities.
- Uses DEB (Dynamic Error Balancing) to blend multi-model highs.
- Generates settlement-oriented calibrated probability buckets (`mu` + bucket distribution), with `LGBM` metadata surfaced when the calibrated engine is active.
- Maps weather view to Polymarket quotes for mispricing scan.
@@ -66,7 +73,7 @@ See: [AGPL-3.0 & Commercial Boundary](docs/OPEN_CORE_POLICY.md)
- Adds payment audit trails, replay tooling, and incident visibility in ops.
- Adds peak-window-oriented intraday analysis with meteorology headline, path buckets, evidence chain, invalidation rules, and confirmation rules.
- Adds airport-side `TAF` timing overlays and airport suppression/disruption interpretation for non-Hong Kong airport cities.
- Adds official nearby-network enhancement layers for China, Japan, Korea, Hong Kong, Taiwan, and Turkey without replacing airport settlement anchors.
- Adds official nearby-network and runway-level enhancement layers for China, Japan, Korea (AMOS runway sensors for Seoul/Busan), Hong Kong, Taiwan, and Turkey without replacing airport settlement anchors.
- Adds optional dashboard prewarm worker so hot cities can be refreshed before user clicks.
## Reference Architecture
@@ -84,7 +91,7 @@ flowchart LR
WX --> MGM["MGM (Turkey station network)"]
WX --> OM["Open-Meteo"]
WX --> JMA["JMA AMeDAS (Japan)"]
WX --> KMA["KMA (Korea)"]
WX --> AMOS["AMOS runway sensors (Korea)"]
WX --> HKO["HKO / CWA / NOAA / Official settlement sources"]
API --> ANA["DEB + Trend + Probability + Market Scan"]
@@ -94,12 +101,12 @@ flowchart LR
API --> PREWARM["Dashboard Prewarm API / Worker"]
```
## Monitored Cities (52)
## Monitored Cities (51)
- Europe / Middle East / Africa: Ankara, Istanbul, Moscow, London, Paris, Munich, Milan, Warsaw, Madrid, Tel Aviv, Amsterdam, Helsinki, Lagos, Cape Town, Jeddah
- APAC: Seoul, Busan, Hong Kong, Lau Fau Shan, Taipei, Shanghai, Beijing, Qingdao, Wuhan, Chengdu, Chongqing, Shenzhen, Guangzhou, Singapore, Tokyo, Kuala Lumpur, Jakarta, Manila, Wellington
- Americas: Toronto, New York, Los Angeles, San Francisco, Aurora, Austin, Houston, Chicago, Dallas, Miami, Atlanta, Seattle, Mexico City, Buenos Aires, Sao Paulo, Panama City
- South Asia: Lucknow, Karachi, Masroor Air Base
- South Asia: Lucknow, Karachi
## Quick Start
@@ -236,5 +243,5 @@ docker compose logs -f polyweather | egrep "polymarket wallet activity watcher s
## Version
- Version: `v1.5.4`
- Last Updated: `2026-04-19`
- Version: `v1.7.0`
- Last Updated: `2026-05-11`
+14 -11
View File
@@ -14,10 +14,12 @@
![PolyWeather Ankara 分析页](docs/images/demo_ankara.png)
## 当前产品状态(2026-04-27
## 当前产品状态(2026-05-11
- 已上线订阅制:`Pro 月付 5 USDC`
- 已上线积分抵扣:`500 积分 = 1 USDC`,最多抵扣 `3 USDC`
- 已上线订阅制:`Pro 月付 10 USDC`
- 已上线积分体系:群内发言赚分 + 首次发言欢迎奖励 (+20) + 每日首条消息奖励 (+2) + 每周全员参与奖
- `/city``/deb` 已改为免费(每日各 10 次);积分可用于支付抵扣(`500 分 = 1 USDC`,最多抵 `3 USDC`)。
- 周榜奖励已改造:降低赢家积分加成 (200/100/50),所有周活跃用户均享参与奖。
- 已上线链上支付:Polygon 合约支付(USDC / USDC.e)。
- 已上线自动补单:事件监听 + 周期确认双链路。
- 已上线支付运行态与审计接口:`/api/payments/runtime`
@@ -30,7 +32,7 @@
- `MGM`(土耳其)
- `CMA/NMC`(中国内地)
- `JMA AMeDAS`(日本)
- `KMA`(韩国)
- `AMOS`(韩国,跑道级传感器,首尔/釜山
- `HKO`(香港)
- `CWA`(台湾)
- 东京现已接入羽田 `JMA AMeDAS` 10 分钟温度作为官方增强层。
@@ -45,6 +47,7 @@
- 概率区已改为“校准模型概率”;默认展示生产概率引擎输出,EMOS/LGBM 只在通过评估或作为 shadow 时进入解释层。
- 今日日内结构解读已支持可选 `Groq` 改写层,失败时自动回退规则文案。
- 前端部署文档已补充 Vercel 节流建议,包括 analytics 关闭、eager fetch 开关与扫描流量防火墙规则。
- 前端设计系统全面重构:统一 CSS token 体系、消除 !important 滥用(134→49)、合并断点(18→10)、数百处硬编码颜色迁移至 CSS 变量、添加 ARIA 无障碍属性和键盘导航。完整审查记录见 `docs/frontend-ui-design-review.md`
## 许可证与商用边界(重要)
@@ -58,14 +61,14 @@
## 核心能力
- 聚合 52 个监控城市的实测与预报数据。
- 聚合 51 个监控城市的实测与预报数据。
- DEBDynamic Error Balancing)融合多模型最高温。
- 输出结算导向校准概率分布(`mu` + 温度桶),并在 LGBM 生效时展示校准引擎元数据。
- 将模型观点映射到 Polymarket 行情,做错价扫描。
- 地图城市决策卡把 AI 机场报文解读、最高温中枢、完整市场温度桶和模型-市场差放在同一张卡中展示。
- Web 仪表盘与 Telegram Bot 复用同一分析内核。
- 支付链路具备事件重放、SQLite 审计事件与 RPC 容灾能力。
- 官方增强层支持按国家 provider 统一接入,不替代机场主站、METAR 或明确官方结算站。
- 官方增强层与跑道级传感器支持按国家 provider 统一接入(含韩国 AMOS 首尔/釜山跑道实测),不替代机场主站、METAR 或明确官方结算站。
- 支持后台预热热点城市,降低用户点击城市后的冷启动成本。
## 参考架构
@@ -81,7 +84,7 @@ flowchart LR
WX --> METAR["Aviation WeatherMETAR"]
WX --> MGM["MGM(土耳其站网)"]
WX --> JMA["JMA AMeDAS(日本)"]
WX --> KMA["KMA(韩国)"]
WX --> AMOS["AMOS 跑道传感器(韩国)"]
WX --> OM["Open-Meteo"]
WX --> HKO["HKO / CWA / NOAA 等官方结算源"]
@@ -94,12 +97,12 @@ flowchart LR
ANA --> STATE["SQLite runtime state<br/>legacy files only for migration/export fallback"]
```
## 监控城市(52
## 监控城市(51
- 欧洲/中东/非洲:Ankara、Istanbul、Moscow、London、Paris、Munich、Milan、Warsaw、Madrid、Tel Aviv、Amsterdam、Helsinki、Lagos、Cape Town、Jeddah
- 亚太:Seoul、Busan、Hong Kong、Lau Fau Shan、Taipei、Shanghai、Beijing、Wuhan、Chengdu、Chongqing、Shenzhen、Guangzhou、Singapore、Tokyo、Kuala Lumpur、Jakarta、Manila、Wellington
- 美洲:Toronto、New York、Los Angeles、San Francisco、Aurora、Austin、Houston、Chicago、Dallas、Miami、Atlanta、Seattle、Mexico City、Buenos Aires、Sao Paulo、Panama City
- 南亚:Lucknow、Karachi、Masroor Air Base
- 南亚:Lucknow、Karachi
## 快速启动
@@ -257,5 +260,5 @@ docker compose logs -f polyweather | egrep "polymarket wallet activity watcher s
## 当前版本
- 版本:`v1.5.4`
- 文档最后更新:`2026-04-19`
- 版本:`v1.7.0`
- 文档最后更新:`2026-05-11`
+1 -1
View File
@@ -1 +1 @@
1.5.5
1.7.0
File diff suppressed because it is too large Load Diff
@@ -1,66 +0,0 @@
{
"model_type": "LightGBMRegressor",
"target": "actual_high",
"horizon": "D0",
"feature_names": [
"actual_high_lag_1",
"actual_high_lag_2",
"actual_high_lag_3",
"actual_high_lag_7",
"actual_high_mean_7",
"actual_high_mean_14",
"actual_high_trend_3",
"open_meteo",
"ecmwf",
"gfs",
"gem",
"jma",
"icon",
"mgm",
"nws",
"deb_prediction",
"model_median",
"model_spread",
"current_temp",
"max_so_far",
"humidity",
"wind_speed_kt",
"visibility_mi",
"local_hour",
"month",
"weekday",
"peak_status_code"
],
"base_model_columns": [
"open_meteo",
"ecmwf",
"gfs",
"gem",
"jma",
"icon",
"mgm",
"nws"
],
"model_path": "artifacts\\models\\lgbm_daily_high.txt",
"sample_count": 1247,
"train_count": 998,
"validation_count": 249,
"metrics": {
"validation": {
"sample_count": 249,
"lgbm_mae": 2.626,
"deb_mae": 2.745,
"best_single_mae": 1.733,
"median_mae": 2.866
},
"full_sample": {
"sample_count": 1247,
"lgbm_mae": 0.953,
"deb_mae": 1.872,
"best_single_mae": 1.052,
"median_mae": 1.952
}
},
"generated_at": "2026-05-06T10:45:04.319587Z",
"trained_at": "2026-05-06T10:45:04.319587Z"
}
@@ -1,361 +0,0 @@
{
"version": "emos-auto-20260421122743",
"trained_at": "2026-04-21T12:28:05.031165+00:00",
"global": {
"mu": {
"intercept": 0.34330438,
"raw_mu_coef": 0.48749629,
"deb_coef": 0.45733479,
"ens_median_coef": 0.05955297,
"max_so_far_gap_coef": -0.104935
},
"sigma": {
"intercept": -0.62765582,
"raw_sigma_coef": 0.38492261,
"spread_coef": 0.29095921,
"peak_flag_coef": 0.33966327,
"max_so_far_gap_coef": 0.08357376
}
},
"sigma_constraints": {
"min_ratio": 0.85,
"max_ratio": 1.35,
"absolute_min": 0.25,
"absolute_max": 3.0
},
"selection_guardrails": {
"max_mae_increase": 0.02,
"max_bucket_hit_drop": 0.01,
"max_bucket_brier_increase": 0.05
},
"blending": {
"alpha_mu": 0.25,
"alpha_sigma": 1.0
},
"cities": {
"istanbul": {
"samples": 20,
"mu_bias": -0.438949,
"sigma_scale": 0.553793,
"confidence": 1.0
},
"buenos aires": {
"samples": 22,
"mu_bias": -1.044971,
"sigma_scale": 1.389753,
"confidence": 1.0
},
"ankara": {
"samples": 23,
"mu_bias": 0.067406,
"sigma_scale": 1.270298,
"confidence": 1.0
},
"london": {
"samples": 22,
"mu_bias": -0.010833,
"sigma_scale": 2.0,
"confidence": 1.0
},
"paris": {
"samples": 22,
"mu_bias": -0.21677,
"sigma_scale": 1.095669,
"confidence": 1.0
},
"new york": {
"samples": 21,
"mu_bias": 1.072189,
"sigma_scale": 2.0,
"confidence": 1.0
},
"lagos": {
"samples": 10,
"mu_bias": -0.092732,
"sigma_scale": 1.558646,
"confidence": 1.0
},
"madrid": {
"samples": 22,
"mu_bias": -0.627862,
"sigma_scale": 1.761239,
"confidence": 1.0
},
"toronto": {
"samples": 22,
"mu_bias": -0.424101,
"sigma_scale": 1.326675,
"confidence": 1.0
},
"wellington": {
"samples": 22,
"mu_bias": -0.347796,
"sigma_scale": 0.893459,
"confidence": 1.0
},
"los angeles": {
"samples": 20,
"mu_bias": -0.659983,
"sigma_scale": 2.0,
"confidence": 1.0
},
"san francisco": {
"samples": 20,
"mu_bias": 0.941818,
"sigma_scale": 1.979403,
"confidence": 1.0
},
"aurora": {
"samples": 20,
"mu_bias": 2.040009,
"sigma_scale": 0.982492,
"confidence": 1.0
},
"austin": {
"samples": 20,
"mu_bias": 0.990023,
"sigma_scale": 2.0,
"confidence": 1.0
},
"houston": {
"samples": 20,
"mu_bias": 0.188787,
"sigma_scale": 1.819594,
"confidence": 1.0
},
"chicago": {
"samples": 21,
"mu_bias": 2.285098,
"sigma_scale": 2.0,
"confidence": 1.0
},
"dallas": {
"samples": 21,
"mu_bias": 0.258095,
"sigma_scale": 2.0,
"confidence": 1.0
},
"atlanta": {
"samples": 22,
"mu_bias": -2.093105,
"sigma_scale": 2.0,
"confidence": 1.0
},
"seattle": {
"samples": 21,
"mu_bias": -0.982188,
"sigma_scale": 1.695172,
"confidence": 1.0
},
"sao paulo": {
"samples": 22,
"mu_bias": -0.786242,
"sigma_scale": 1.427037,
"confidence": 1.0
},
"munich": {
"samples": 22,
"mu_bias": 0.096559,
"sigma_scale": 2.0,
"confidence": 1.0
},
"milan": {
"samples": 23,
"mu_bias": -0.045038,
"sigma_scale": 1.626331,
"confidence": 1.0
},
"warsaw": {
"samples": 23,
"mu_bias": -0.50853,
"sigma_scale": 1.239146,
"confidence": 1.0
},
"helsinki": {
"samples": 17,
"mu_bias": 0.442247,
"sigma_scale": 2.0,
"confidence": 1.0
},
"amsterdam": {
"samples": 17,
"mu_bias": 0.129889,
"sigma_scale": 2.0,
"confidence": 1.0
},
"panama city": {
"samples": 17,
"mu_bias": -0.040792,
"sigma_scale": 1.393259,
"confidence": 1.0
},
"cape town": {
"samples": 10,
"mu_bias": 0.507293,
"sigma_scale": 1.420004,
"confidence": 1.0
},
"miami": {
"samples": 22,
"mu_bias": -2.736316,
"sigma_scale": 2.0,
"confidence": 1.0
},
"manila": {
"samples": 5,
"mu_bias": 1.772229,
"sigma_scale": 2.0,
"confidence": 0.625
},
"singapore": {
"samples": 22,
"mu_bias": 0.289639,
"sigma_scale": 1.124006,
"confidence": 1.0
},
"tokyo": {
"samples": 22,
"mu_bias": -0.332129,
"sigma_scale": 0.729483,
"confidence": 1.0
},
"seoul": {
"samples": 22,
"mu_bias": 1.6533,
"sigma_scale": 0.82648,
"confidence": 1.0
},
"busan": {
"samples": 16,
"mu_bias": 0.343239,
"sigma_scale": 1.886868,
"confidence": 1.0
},
"mexico city": {
"samples": 20,
"mu_bias": 0.087122,
"sigma_scale": 1.108884,
"confidence": 1.0
},
"shanghai": {
"samples": 23,
"mu_bias": 0.348022,
"sigma_scale": 0.810527,
"confidence": 1.0
},
"beijing": {
"samples": 20,
"mu_bias": -0.288176,
"sigma_scale": 0.952972,
"confidence": 1.0
},
"shenzhen": {
"samples": 21,
"mu_bias": -0.006114,
"sigma_scale": 0.728494,
"confidence": 1.0
},
"wuhan": {
"samples": 20,
"mu_bias": -0.771154,
"sigma_scale": 1.30329,
"confidence": 1.0
},
"chengdu": {
"samples": 20,
"mu_bias": 0.445906,
"sigma_scale": 0.796989,
"confidence": 1.0
},
"chongqing": {
"samples": 20,
"mu_bias": -0.021301,
"sigma_scale": 1.106566,
"confidence": 1.0
},
"hong kong": {
"samples": 24,
"mu_bias": 0.37527,
"sigma_scale": 0.517756,
"confidence": 1.0
},
"taipei": {
"samples": 23,
"mu_bias": 0.600671,
"sigma_scale": 0.5,
"confidence": 1.0
},
"guangzhou": {
"samples": 6,
"mu_bias": 0.7082,
"sigma_scale": 1.975103,
"confidence": 0.75
},
"lau fau shan": {
"samples": 19,
"mu_bias": 0.41371,
"sigma_scale": 0.656578,
"confidence": 1.0
},
"kuala lumpur": {
"samples": 16,
"mu_bias": 0.499197,
"sigma_scale": 1.361171,
"confidence": 1.0
},
"jakarta": {
"samples": 16,
"mu_bias": 0.293537,
"sigma_scale": 0.751498,
"confidence": 1.0
},
"lucknow": {
"samples": 22,
"mu_bias": -1.593437,
"sigma_scale": 2.0,
"confidence": 1.0
},
"karachi": {
"samples": 5,
"mu_bias": -0.035595,
"sigma_scale": 0.588612,
"confidence": 0.625
},
"jeddah": {
"samples": 10,
"mu_bias": 0.516879,
"sigma_scale": 2.0,
"confidence": 1.0
},
"moscow": {
"samples": 20,
"mu_bias": -0.609602,
"sigma_scale": 0.783723,
"confidence": 1.0
},
"tel aviv": {
"samples": 22,
"mu_bias": 0.436837,
"sigma_scale": 1.46524,
"confidence": 1.0
}
},
"metrics": {
"sample_count": 978,
"mean_crps": 1.330733,
"legacy_mean_crps": 1.376936,
"legacy_mean_mae": 1.589438,
"legacy_bucket_hit_rate": 0.43865,
"legacy_bucket_brier": 0.824571,
"selected_mean_crps": 1.311763,
"selected_mean_mae": 1.553658,
"selected_bucket_hit_rate": 0.432515,
"selected_bucket_brier": 0.789817,
"selected_score": 2.910718,
"legacy_score": 2.996333,
"filled_actual_from_history": 0,
"settlement_history_city_count": 30,
"legacy_archive_samples": 54
},
"source": "artifacts\\local_runtime\\probability_calibration\\candidates\\emos-auto-20260421122743\\default.json"
}
@@ -1,293 +0,0 @@
{
"summary": {
"sample_count": 74,
"filled_actual_from_history": 0,
"legacy": {
"mean_crps": 3.474108,
"mean_mae": 3.679324,
"bucket_hit_rate": 0.5
},
"emos": {
"mean_crps": 3.33124,
"mean_mae": 3.622584,
"bucket_hit_rate": 0.5
},
"delta": {
"crps": -0.142868,
"mae": -0.056741,
"bucket_hit_rate": 0.0
}
},
"by_city": {
"ankara": {
"samples": 6,
"legacy_mean_crps": 1.365135,
"emos_mean_crps": 1.392829,
"legacy_mean_mae": 1.466667,
"emos_mean_mae": 1.444819,
"legacy_bucket_hit_rate": 0.666667,
"emos_bucket_hit_rate": 0.666667
},
"atlanta": {
"samples": 2,
"legacy_mean_crps": 30.449382,
"emos_mean_crps": 29.886236,
"legacy_mean_mae": 32.015,
"emos_mean_mae": 31.322804,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0
},
"buenos aires": {
"samples": 2,
"legacy_mean_crps": 9.113412,
"emos_mean_crps": 8.463522,
"legacy_mean_mae": 10.27,
"emos_mean_mae": 10.024533,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0
},
"busan": {
"samples": 3,
"legacy_mean_crps": 0.276202,
"emos_mean_crps": 0.326485,
"legacy_mean_mae": 0.3,
"emos_mean_mae": 0.239636,
"legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0
},
"chengdu": {
"samples": 1,
"legacy_mean_crps": 0.306469,
"emos_mean_crps": 0.378812,
"legacy_mean_mae": 0.3,
"emos_mean_mae": 0.153224,
"legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0
},
"chicago": {
"samples": 1,
"legacy_mean_crps": 1.250268,
"emos_mean_crps": 0.714669,
"legacy_mean_mae": 0.0,
"emos_mean_mae": 0.319756,
"legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0
},
"dallas": {
"samples": 1,
"legacy_mean_crps": 2.173363,
"emos_mean_crps": 0.840297,
"legacy_mean_mae": 0.0,
"emos_mean_mae": 1.028146,
"legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 0.0
},
"hong kong": {
"samples": 7,
"legacy_mean_crps": 0.706099,
"emos_mean_crps": 0.731926,
"legacy_mean_mae": 0.657143,
"emos_mean_mae": 0.669911,
"legacy_bucket_hit_rate": 0.714286,
"emos_bucket_hit_rate": 0.571429
},
"istanbul": {
"samples": 3,
"legacy_mean_crps": 1.264103,
"emos_mean_crps": 1.214967,
"legacy_mean_mae": 1.5,
"emos_mean_mae": 1.469837,
"legacy_bucket_hit_rate": 0.333333,
"emos_bucket_hit_rate": 0.666667
},
"london": {
"samples": 2,
"legacy_mean_crps": 3.885033,
"emos_mean_crps": 3.799841,
"legacy_mean_mae": 4.135,
"emos_mean_mae": 4.027503,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.5
},
"lucknow": {
"samples": 2,
"legacy_mean_crps": 2.487193,
"emos_mean_crps": 2.107441,
"legacy_mean_mae": 3.205,
"emos_mean_mae": 2.876943,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0
},
"madrid": {
"samples": 2,
"legacy_mean_crps": 6.27726,
"emos_mean_crps": 5.810228,
"legacy_mean_mae": 7.33,
"emos_mean_mae": 7.195133,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0
},
"miami": {
"samples": 2,
"legacy_mean_crps": 28.637631,
"emos_mean_crps": 27.592664,
"legacy_mean_mae": 30.175,
"emos_mean_mae": 29.284979,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0
},
"milan": {
"samples": 3,
"legacy_mean_crps": 4.401392,
"emos_mean_crps": 3.782613,
"legacy_mean_mae": 4.06,
"emos_mean_mae": 3.989808,
"legacy_bucket_hit_rate": 0.666667,
"emos_bucket_hit_rate": 0.666667
},
"munich": {
"samples": 2,
"legacy_mean_crps": 3.145192,
"emos_mean_crps": 2.990712,
"legacy_mean_mae": 3.64,
"emos_mean_mae": 3.61502,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0
},
"new york": {
"samples": 1,
"legacy_mean_crps": 3.692845,
"emos_mean_crps": 3.021798,
"legacy_mean_mae": 4.94,
"emos_mean_mae": 4.54437,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0
},
"paris": {
"samples": 2,
"legacy_mean_crps": 4.013782,
"emos_mean_crps": 3.915303,
"legacy_mean_mae": 4.265,
"emos_mean_mae": 4.179694,
"legacy_bucket_hit_rate": 0.5,
"emos_bucket_hit_rate": 0.5
},
"sao paulo": {
"samples": 2,
"legacy_mean_crps": 5.540967,
"emos_mean_crps": 5.018605,
"legacy_mean_mae": 6.57,
"emos_mean_mae": 6.314389,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0
},
"seattle": {
"samples": 1,
"legacy_mean_crps": 0.315488,
"emos_mean_crps": 0.524119,
"legacy_mean_mae": 0.0,
"emos_mean_mae": 0.673609,
"legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 0.0
},
"seoul": {
"samples": 3,
"legacy_mean_crps": 0.508331,
"emos_mean_crps": 0.520762,
"legacy_mean_mae": 0.2,
"emos_mean_mae": 0.247862,
"legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0
},
"shanghai": {
"samples": 3,
"legacy_mean_crps": 0.299116,
"emos_mean_crps": 0.402453,
"legacy_mean_mae": 0.1,
"emos_mean_mae": 0.189437,
"legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0
},
"shenzhen": {
"samples": 1,
"legacy_mean_crps": 1.198351,
"emos_mean_crps": 1.490963,
"legacy_mean_mae": 1.3,
"emos_mean_mae": 1.628189,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0
},
"singapore": {
"samples": 2,
"legacy_mean_crps": 0.281993,
"emos_mean_crps": 0.370374,
"legacy_mean_mae": 0.15,
"emos_mean_mae": 0.118005,
"legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0
},
"taipei": {
"samples": 5,
"legacy_mean_crps": 0.950739,
"emos_mean_crps": 1.042332,
"legacy_mean_mae": 0.94,
"emos_mean_mae": 0.951241,
"legacy_bucket_hit_rate": 0.6,
"emos_bucket_hit_rate": 0.6
},
"tel aviv": {
"samples": 2,
"legacy_mean_crps": 0.446758,
"emos_mean_crps": 0.578691,
"legacy_mean_mae": 0.3,
"emos_mean_mae": 0.116966,
"legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0
},
"tokyo": {
"samples": 5,
"legacy_mean_crps": 0.879366,
"emos_mean_crps": 0.876608,
"legacy_mean_mae": 1.022,
"emos_mean_mae": 1.008317,
"legacy_bucket_hit_rate": 0.2,
"emos_bucket_hit_rate": 0.4
},
"toronto": {
"samples": 2,
"legacy_mean_crps": 5.497916,
"emos_mean_crps": 5.268783,
"legacy_mean_mae": 6.33,
"emos_mean_mae": 6.388522,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0
},
"warsaw": {
"samples": 3,
"legacy_mean_crps": 1.618875,
"emos_mean_crps": 1.524219,
"legacy_mean_mae": 2.056667,
"emos_mean_mae": 2.006524,
"legacy_bucket_hit_rate": 0.333333,
"emos_bucket_hit_rate": 0.333333
},
"wellington": {
"samples": 2,
"legacy_mean_crps": 0.364919,
"emos_mean_crps": 0.484124,
"legacy_mean_mae": 0.15,
"emos_mean_mae": 0.123335,
"legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0
},
"wuhan": {
"samples": 1,
"legacy_mean_crps": 0.476225,
"emos_mean_crps": 0.578405,
"legacy_mean_mae": 0.8,
"emos_mean_mae": 0.962852,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0
}
}
}
@@ -1,74 +0,0 @@
{
"evaluation_report_path": "E:\\web\\PolyWeather\\artifacts\\probability_calibration\\evaluation_report.json",
"shadow_report_path": "E:\\web\\PolyWeather\\artifacts\\probability_calibration\\shadow_report.json",
"evaluation_report_exists": true,
"shadow_report_exists": true,
"decision": {
"decision": "hold",
"ready_for_primary": false,
"summary": "当前指标不足以切换 emos_primary,应继续保持 shadow。",
"thresholds": {
"evaluation_min_samples": 80,
"shadow_min_samples": 50,
"max_delta_mae": 0.05,
"min_delta_crps": -0.02,
"min_delta_bucket_hit_rate": 0.0,
"max_delta_bucket_brier_promote": 0.02,
"max_delta_bucket_brier_observe": 0.15
},
"evaluation": {
"sample_count": 54,
"delta_crps": -0.086732,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0
},
"shadow": {
"sample_count": 48,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.041666,
"delta_bucket_brier": 0.123252
},
"blocking_reasons": [
"离线评估样本不足:54 < 80",
"shadow 样本不足:48 < 50",
"shadow bucket brier 退化超限:delta=0.123252"
],
"worst_shadow_regressions": [
{
"city": "dallas",
"samples": 1,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.792585
},
{
"city": "chicago",
"samples": 1,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.791878
},
{
"city": "seattle",
"samples": 1,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.61609
},
{
"city": "wellington",
"samples": 2,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.509203
},
{
"city": "tel aviv",
"samples": 2,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.439879
}
]
}
}
File diff suppressed because it is too large Load Diff
@@ -1,933 +0,0 @@
{
"generated_at": "2026-04-02T16:23:24.376528Z",
"summary": {
"samples": 48,
"legacy_mean_mae": 3.04125,
"shadow_mean_mae": 3.04125,
"legacy_bucket_hit_rate": 0.5,
"shadow_bucket_hit_rate": 0.5,
"legacy_bucket_brier": 0.68666,
"shadow_bucket_brier": 0.814079,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.127419
},
"by_city": {
"ankara": {
"samples": 2,
"legacy_mean_mae": 0.1,
"shadow_mean_mae": 0.1,
"legacy_bucket_hit_rate": 1.0,
"shadow_bucket_hit_rate": 1.0,
"legacy_bucket_brier": 0.494847,
"shadow_bucket_brier": 0.654648,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.159801
},
"atlanta": {
"samples": 1,
"legacy_mean_mae": 17.06,
"shadow_mean_mae": 17.06,
"legacy_bucket_hit_rate": 0.0,
"shadow_bucket_hit_rate": 0.0,
"legacy_bucket_brier": 1.029097,
"shadow_bucket_brier": 1.064101,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.035004
},
"buenos aires": {
"samples": 2,
"legacy_mean_mae": 10.27,
"shadow_mean_mae": 10.27,
"legacy_bucket_hit_rate": 0.0,
"shadow_bucket_hit_rate": 0.0,
"legacy_bucket_brier": 1.117726,
"shadow_bucket_brier": 1.116732,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": -0.000994
},
"chicago": {
"samples": 1,
"legacy_mean_mae": 0.0,
"shadow_mean_mae": 0.0,
"legacy_bucket_hit_rate": 1.0,
"shadow_bucket_hit_rate": 1.0,
"legacy_bucket_brier": 0.0,
"shadow_bucket_brier": 0.791878,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.791878
},
"dallas": {
"samples": 1,
"legacy_mean_mae": 0.0,
"shadow_mean_mae": 0.0,
"legacy_bucket_hit_rate": 1.0,
"shadow_bucket_hit_rate": 1.0,
"legacy_bucket_brier": 0.0,
"shadow_bucket_brier": 0.792585,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.792585
},
"hong kong": {
"samples": 3,
"legacy_mean_mae": 0.1,
"shadow_mean_mae": 0.1,
"legacy_bucket_hit_rate": 0.333333,
"shadow_bucket_hit_rate": 0.666667,
"legacy_bucket_brier": 0.882203,
"shadow_bucket_brier": 0.437187,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.333334,
"delta_bucket_brier": -0.445016
},
"london": {
"samples": 2,
"legacy_mean_mae": 4.135,
"shadow_mean_mae": 4.135,
"legacy_bucket_hit_rate": 0.0,
"shadow_bucket_hit_rate": 0.0,
"legacy_bucket_brier": 0.929652,
"shadow_bucket_brier": 1.039551,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.109899
},
"lucknow": {
"samples": 2,
"legacy_mean_mae": 3.205,
"shadow_mean_mae": 3.205,
"legacy_bucket_hit_rate": 0.0,
"shadow_bucket_hit_rate": 0.0,
"legacy_bucket_brier": 1.673607,
"shadow_bucket_brier": 0.96272,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": -0.710887
},
"madrid": {
"samples": 2,
"legacy_mean_mae": 7.33,
"shadow_mean_mae": 7.33,
"legacy_bucket_hit_rate": 0.0,
"shadow_bucket_hit_rate": 0.0,
"legacy_bucket_brier": 1.148213,
"shadow_bucket_brier": 1.133888,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": -0.014325
},
"miami": {
"samples": 1,
"legacy_mean_mae": 11.04,
"shadow_mean_mae": 11.04,
"legacy_bucket_hit_rate": 0.0,
"shadow_bucket_hit_rate": 0.0,
"legacy_bucket_brier": 1.182605,
"shadow_bucket_brier": 1.067257,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": -0.115348
},
"milan": {
"samples": 3,
"legacy_mean_mae": 4.06,
"shadow_mean_mae": 4.06,
"legacy_bucket_hit_rate": 0.666667,
"shadow_bucket_hit_rate": 0.666667,
"legacy_bucket_brier": 0.587548,
"shadow_bucket_brier": 0.78616,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.198612
},
"munich": {
"samples": 2,
"legacy_mean_mae": 3.64,
"shadow_mean_mae": 3.64,
"legacy_bucket_hit_rate": 0.0,
"shadow_bucket_hit_rate": 0.0,
"legacy_bucket_brier": 0.918378,
"shadow_bucket_brier": 0.945234,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.026856
},
"new york": {
"samples": 1,
"legacy_mean_mae": 4.94,
"shadow_mean_mae": 4.94,
"legacy_bucket_hit_rate": 0.0,
"shadow_bucket_hit_rate": 0.0,
"legacy_bucket_brier": 1.111711,
"shadow_bucket_brier": 1.099556,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": -0.012155
},
"paris": {
"samples": 2,
"legacy_mean_mae": 4.265,
"shadow_mean_mae": 4.265,
"legacy_bucket_hit_rate": 0.5,
"shadow_bucket_hit_rate": 0.5,
"legacy_bucket_brier": 0.90186,
"shadow_bucket_brier": 0.9967,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.09484
},
"sao paulo": {
"samples": 2,
"legacy_mean_mae": 6.57,
"shadow_mean_mae": 6.57,
"legacy_bucket_hit_rate": 0.0,
"shadow_bucket_hit_rate": 0.0,
"legacy_bucket_brier": 1.263988,
"shadow_bucket_brier": 1.159352,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": -0.104636
},
"seattle": {
"samples": 1,
"legacy_mean_mae": 0.0,
"shadow_mean_mae": 0.0,
"legacy_bucket_hit_rate": 1.0,
"shadow_bucket_hit_rate": 1.0,
"legacy_bucket_brier": 0.0,
"shadow_bucket_brier": 0.61609,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.61609
},
"seoul": {
"samples": 2,
"legacy_mean_mae": 0.15,
"shadow_mean_mae": 0.15,
"legacy_bucket_hit_rate": 1.0,
"shadow_bucket_hit_rate": 1.0,
"legacy_bucket_brier": 0.258977,
"shadow_bucket_brier": 0.548485,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.289508
},
"shanghai": {
"samples": 2,
"legacy_mean_mae": 0.15,
"shadow_mean_mae": 0.15,
"legacy_bucket_hit_rate": 1.0,
"shadow_bucket_hit_rate": 1.0,
"legacy_bucket_brier": 0.1156,
"shadow_bucket_brier": 0.501323,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.385723
},
"singapore": {
"samples": 2,
"legacy_mean_mae": 0.15,
"shadow_mean_mae": 0.15,
"legacy_bucket_hit_rate": 1.0,
"shadow_bucket_hit_rate": 1.0,
"legacy_bucket_brier": 0.306527,
"shadow_bucket_brier": 0.553647,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.24712
},
"taipei": {
"samples": 3,
"legacy_mean_mae": 0.1,
"shadow_mean_mae": 0.1,
"legacy_bucket_hit_rate": 1.0,
"shadow_bucket_hit_rate": 0.333333,
"legacy_bucket_brier": 0.195261,
"shadow_bucket_brier": 0.684998,
"delta_mae": 0.0,
"delta_bucket_hit_rate": -0.666667,
"delta_bucket_brier": 0.489737
},
"tel aviv": {
"samples": 2,
"legacy_mean_mae": 0.3,
"shadow_mean_mae": 0.3,
"legacy_bucket_hit_rate": 1.0,
"shadow_bucket_hit_rate": 1.0,
"legacy_bucket_brier": 0.257895,
"shadow_bucket_brier": 0.697774,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.439879
},
"tokyo": {
"samples": 2,
"legacy_mean_mae": 0.25,
"shadow_mean_mae": 0.25,
"legacy_bucket_hit_rate": 0.5,
"shadow_bucket_hit_rate": 1.0,
"legacy_bucket_brier": 0.284759,
"shadow_bucket_brier": 0.69701,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.5,
"delta_bucket_brier": 0.412251
},
"toronto": {
"samples": 2,
"legacy_mean_mae": 6.33,
"shadow_mean_mae": 6.33,
"legacy_bucket_hit_rate": 0.0,
"shadow_bucket_hit_rate": 0.0,
"legacy_bucket_brier": 1.256736,
"shadow_bucket_brier": 1.221422,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": -0.035314
},
"warsaw": {
"samples": 3,
"legacy_mean_mae": 2.056667,
"shadow_mean_mae": 2.056667,
"legacy_bucket_hit_rate": 0.333333,
"shadow_bucket_hit_rate": 0.333333,
"legacy_bucket_brier": 0.86425,
"shadow_bucket_brier": 0.750985,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": -0.113265
},
"wellington": {
"samples": 2,
"legacy_mean_mae": 0.15,
"shadow_mean_mae": 0.15,
"legacy_bucket_hit_rate": 1.0,
"shadow_bucket_hit_rate": 1.0,
"legacy_bucket_brier": 0.095481,
"shadow_bucket_brier": 0.604684,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.0,
"delta_bucket_brier": 0.509203
}
},
"by_date": {
"2026-03-17": {
"samples": 4,
"legacy_mean_mae": 0.15,
"shadow_mean_mae": 0.15,
"legacy_bucket_hit_rate": 1.0,
"shadow_bucket_hit_rate": 0.5,
"legacy_bucket_brier": 0.101543,
"shadow_bucket_brier": 0.6636,
"delta_mae": 0.0,
"delta_bucket_hit_rate": -0.5,
"delta_bucket_brier": 0.562057
},
"2026-03-18": {
"samples": 25,
"legacy_mean_mae": 2.5476,
"shadow_mean_mae": 2.5476,
"legacy_bucket_hit_rate": 0.52,
"shadow_bucket_hit_rate": 0.56,
"legacy_bucket_brier": 0.657516,
"shadow_bucket_brier": 0.74574,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.04,
"delta_bucket_brier": 0.088224
},
"2026-03-19": {
"samples": 19,
"legacy_mean_mae": 4.299474,
"shadow_mean_mae": 4.299474,
"legacy_bucket_hit_rate": 0.368421,
"shadow_bucket_hit_rate": 0.421053,
"legacy_bucket_brier": 0.848191,
"shadow_bucket_brier": 0.935678,
"delta_mae": 0.0,
"delta_bucket_hit_rate": 0.052632,
"delta_bucket_brier": 0.087487
}
},
"recent_observations": [
{
"city": "wellington",
"date": "2026-03-19",
"actual_high": 18.0,
"actual_bucket": 18,
"legacy_mu": 18.3,
"shadow_mu": 18.3,
"legacy_top_bucket": 18,
"shadow_top_bucket": 18,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "warsaw",
"date": "2026-03-19",
"actual_high": 7.0,
"actual_bucket": 7,
"legacy_mu": 12.03,
"shadow_mu": 12.03,
"legacy_top_bucket": 12,
"shadow_top_bucket": 12,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "toronto",
"date": "2026-03-19",
"actual_high": -2.0,
"actual_bucket": -2,
"legacy_mu": 5.67,
"shadow_mu": 5.67,
"legacy_top_bucket": 6,
"shadow_top_bucket": 6,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "tokyo",
"date": "2026-03-19",
"actual_high": 16.0,
"actual_bucket": 16,
"legacy_mu": 16.5,
"shadow_mu": 16.5,
"legacy_top_bucket": 17,
"shadow_top_bucket": 16,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "tel aviv",
"date": "2026-03-19",
"actual_high": 21.0,
"actual_bucket": 21,
"legacy_mu": 21.3,
"shadow_mu": 21.3,
"legacy_top_bucket": 21,
"shadow_top_bucket": 21,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "taipei",
"date": "2026-03-19",
"actual_high": 22.0,
"actual_bucket": 22,
"legacy_mu": 21.7,
"shadow_mu": 21.7,
"legacy_top_bucket": 22,
"shadow_top_bucket": 21,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "singapore",
"date": "2026-03-19",
"actual_high": 32.0,
"actual_bucket": 32,
"legacy_mu": 32.3,
"shadow_mu": 32.3,
"legacy_top_bucket": 32,
"shadow_top_bucket": 32,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "shanghai",
"date": "2026-03-19",
"actual_high": 12.0,
"actual_bucket": 12,
"legacy_mu": 12.3,
"shadow_mu": 12.3,
"legacy_top_bucket": 12,
"shadow_top_bucket": 12,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "seoul",
"date": "2026-03-19",
"actual_high": 10.0,
"actual_bucket": 10,
"legacy_mu": 10.3,
"shadow_mu": 10.3,
"legacy_top_bucket": 10,
"shadow_top_bucket": 10,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "sao paulo",
"date": "2026-03-19",
"actual_high": 21.0,
"actual_bucket": 21,
"legacy_mu": 26.5,
"shadow_mu": 26.5,
"legacy_top_bucket": 26,
"shadow_top_bucket": 26,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "paris",
"date": "2026-03-19",
"actual_high": 8.0,
"actual_bucket": 8,
"legacy_mu": 16.06,
"shadow_mu": 16.06,
"legacy_top_bucket": 16,
"shadow_top_bucket": 16,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "munich",
"date": "2026-03-19",
"actual_high": 5.0,
"actual_bucket": 5,
"legacy_mu": 11.62,
"shadow_mu": 11.62,
"legacy_top_bucket": 12,
"shadow_top_bucket": 12,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "milan",
"date": "2026-03-19",
"actual_high": 5.0,
"actual_bucket": 5,
"legacy_mu": 16.58,
"shadow_mu": 16.58,
"legacy_top_bucket": 17,
"shadow_top_bucket": 17,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "madrid",
"date": "2026-03-19",
"actual_high": 8.0,
"actual_bucket": 8,
"legacy_mu": 18.23,
"shadow_mu": 18.23,
"legacy_top_bucket": 18,
"shadow_top_bucket": 18,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "lucknow",
"date": "2026-03-19",
"actual_high": 30.0,
"actual_bucket": 30,
"legacy_mu": 35.3,
"shadow_mu": 35.3,
"legacy_top_bucket": 35,
"shadow_top_bucket": 35,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "london",
"date": "2026-03-19",
"actual_high": 8.0,
"actual_bucket": 8,
"legacy_mu": 15.71,
"shadow_mu": 15.71,
"legacy_top_bucket": 16,
"shadow_top_bucket": 16,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "hong kong",
"date": "2026-03-19",
"actual_high": 27.3,
"actual_bucket": 27,
"legacy_mu": 27.6,
"shadow_mu": 27.6,
"legacy_top_bucket": 28,
"shadow_top_bucket": 27,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "buenos aires",
"date": "2026-03-19",
"actual_high": 16.0,
"actual_bucket": 16,
"legacy_mu": 27.39,
"shadow_mu": 27.39,
"legacy_top_bucket": 27,
"shadow_top_bucket": 27,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "ankara",
"date": "2026-03-19",
"actual_high": 11.0,
"actual_bucket": 11,
"legacy_mu": 11.0,
"shadow_mu": 11.0,
"legacy_top_bucket": 11,
"shadow_top_bucket": 11,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "wellington",
"date": "2026-03-18",
"actual_high": 21.0,
"actual_bucket": 21,
"legacy_mu": 21.0,
"shadow_mu": 21.0,
"legacy_top_bucket": 21,
"shadow_top_bucket": 21,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "warsaw",
"date": "2026-03-18",
"actual_high": 13.0,
"actual_bucket": 13,
"legacy_mu": 13.84,
"shadow_mu": 13.84,
"legacy_top_bucket": 14,
"shadow_top_bucket": 14,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "toronto",
"date": "2026-03-18",
"actual_high": -6.0,
"actual_bucket": -6,
"legacy_mu": -1.01,
"shadow_mu": -1.01,
"legacy_top_bucket": -1,
"shadow_top_bucket": -1,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "tokyo",
"date": "2026-03-18",
"actual_high": 17.0,
"actual_bucket": 17,
"legacy_mu": 17.0,
"shadow_mu": 17.0,
"legacy_top_bucket": 17,
"shadow_top_bucket": 17,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "tel aviv",
"date": "2026-03-18",
"actual_high": 30.0,
"actual_bucket": 30,
"legacy_mu": 30.3,
"shadow_mu": 30.3,
"legacy_top_bucket": 30,
"shadow_top_bucket": 30,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "taipei",
"date": "2026-03-18",
"actual_high": 29.0,
"actual_bucket": 29,
"legacy_mu": 29.0,
"shadow_mu": 29.0,
"legacy_top_bucket": 29,
"shadow_top_bucket": 29,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "singapore",
"date": "2026-03-18",
"actual_high": 32.0,
"actual_bucket": 32,
"legacy_mu": 32.0,
"shadow_mu": 32.0,
"legacy_top_bucket": 32,
"shadow_top_bucket": 32,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "shanghai",
"date": "2026-03-18",
"actual_high": 13.0,
"actual_bucket": 13,
"legacy_mu": 13.0,
"shadow_mu": 13.0,
"legacy_top_bucket": 13,
"shadow_top_bucket": 13,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "seoul",
"date": "2026-03-18",
"actual_high": 8.0,
"actual_bucket": 8,
"legacy_mu": 8.0,
"shadow_mu": 8.0,
"legacy_top_bucket": 8,
"shadow_top_bucket": 8,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "seattle",
"date": "2026-03-18",
"actual_high": 55.9,
"actual_bucket": 56,
"legacy_mu": 55.9,
"shadow_mu": 55.9,
"legacy_top_bucket": 56,
"shadow_top_bucket": 56,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "sao paulo",
"date": "2026-03-18",
"actual_high": 22.0,
"actual_bucket": 22,
"legacy_mu": 29.64,
"shadow_mu": 29.64,
"legacy_top_bucket": 30,
"shadow_top_bucket": 30,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "paris",
"date": "2026-03-18",
"actual_high": 14.0,
"actual_bucket": 14,
"legacy_mu": 14.47,
"shadow_mu": 14.47,
"legacy_top_bucket": 14,
"shadow_top_bucket": 14,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "new york",
"date": "2026-03-18",
"actual_high": 32.0,
"actual_bucket": 32,
"legacy_mu": 36.94,
"shadow_mu": 36.94,
"legacy_top_bucket": 37,
"shadow_top_bucket": 37,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "munich",
"date": "2026-03-18",
"actual_high": 10.0,
"actual_bucket": 10,
"legacy_mu": 10.66,
"shadow_mu": 10.66,
"legacy_top_bucket": 11,
"shadow_top_bucket": 11,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "milan",
"date": "2026-03-18",
"actual_high": 14.0,
"actual_bucket": 14,
"legacy_mu": 14.3,
"shadow_mu": 14.3,
"legacy_top_bucket": 14,
"shadow_top_bucket": 14,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "miami",
"date": "2026-03-18",
"actual_high": 59.0,
"actual_bucket": 59,
"legacy_mu": 70.04,
"shadow_mu": 70.04,
"legacy_top_bucket": 70,
"shadow_top_bucket": 70,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "madrid",
"date": "2026-03-18",
"actual_high": 14.0,
"actual_bucket": 14,
"legacy_mu": 18.43,
"shadow_mu": 18.43,
"legacy_top_bucket": 18,
"shadow_top_bucket": 18,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "lucknow",
"date": "2026-03-18",
"actual_high": 34.0,
"actual_bucket": 34,
"legacy_mu": 35.11,
"shadow_mu": 35.11,
"legacy_top_bucket": 35,
"shadow_top_bucket": 35,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "london",
"date": "2026-03-18",
"actual_high": 16.0,
"actual_bucket": 16,
"legacy_mu": 16.56,
"shadow_mu": 16.56,
"legacy_top_bucket": 17,
"shadow_top_bucket": 17,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "hong kong",
"date": "2026-03-18",
"actual_high": 27.8,
"actual_bucket": 27,
"legacy_mu": 27.8,
"shadow_mu": 27.8,
"legacy_top_bucket": 28,
"shadow_top_bucket": 27,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "dallas",
"date": "2026-03-18",
"actual_high": 73.9,
"actual_bucket": 74,
"legacy_mu": 73.9,
"shadow_mu": 73.9,
"legacy_top_bucket": 74,
"shadow_top_bucket": 74,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "chicago",
"date": "2026-03-18",
"actual_high": 46.0,
"actual_bucket": 46,
"legacy_mu": 46.0,
"shadow_mu": 46.0,
"legacy_top_bucket": 46,
"shadow_top_bucket": 46,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "buenos aires",
"date": "2026-03-18",
"actual_high": 17.0,
"actual_bucket": 17,
"legacy_mu": 26.15,
"shadow_mu": 26.15,
"legacy_top_bucket": 26,
"shadow_top_bucket": 26,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "atlanta",
"date": "2026-03-18",
"actual_high": 37.0,
"actual_bucket": 37,
"legacy_mu": 54.06,
"shadow_mu": 54.06,
"legacy_top_bucket": 54,
"shadow_top_bucket": 54,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "ankara",
"date": "2026-03-18",
"actual_high": 15.0,
"actual_bucket": 15,
"legacy_mu": 15.2,
"shadow_mu": 15.2,
"legacy_top_bucket": 15,
"shadow_top_bucket": 15,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "warsaw",
"date": "2026-03-17",
"actual_high": 11.0,
"actual_bucket": 11,
"legacy_mu": 11.3,
"shadow_mu": 11.3,
"legacy_top_bucket": 11,
"shadow_top_bucket": 11,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "taipei",
"date": "2026-03-17",
"actual_high": 26.7,
"actual_bucket": 27,
"legacy_mu": 26.7,
"shadow_mu": 26.7,
"legacy_top_bucket": 27,
"shadow_top_bucket": 26,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "milan",
"date": "2026-03-17",
"actual_high": 17.0,
"actual_bucket": 17,
"legacy_mu": 17.3,
"shadow_mu": 17.3,
"legacy_top_bucket": 17,
"shadow_top_bucket": 17,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
},
{
"city": "hong kong",
"date": "2026-03-17",
"actual_high": 24.0,
"actual_bucket": 24,
"legacy_mu": 24.0,
"shadow_mu": 24.0,
"legacy_top_bucket": 24,
"shadow_top_bucket": 23,
"calibration_version": "emos-20260320130245",
"calibration_mode": "emos_shadow"
}
]
}
@@ -1,981 +0,0 @@
{
"sample_count": 54,
"snapshot_sample_count": 1,
"daily_record_sample_count": 53,
"filled_actual_from_history": 2,
"samples": [
{
"city": "shenzhen",
"date": "2026-03-25",
"timestamp": "2026-03-25T08:57:11.783182+00:00",
"actual_high": 28.0,
"raw_mu": 26.7,
"raw_sigma": 0.18016764322916676,
"deb_prediction": 28.1,
"ens_median": 31.4,
"ensemble_spread": 0.5078125000000002,
"max_so_far_gap": 1.4000000000000021,
"peak_flag": 1.0,
"sample_source": "snapshot",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "ankara",
"date": "2026-03-18",
"actual_high": 15.0,
"raw_mu": 15.2,
"raw_sigma": 1.2000000000000002,
"deb_prediction": 15.4,
"ens_median": 15.8,
"ensemble_spread": 1.2000000000000002,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "ankara",
"date": "2026-03-19",
"actual_high": 11.0,
"raw_mu": 11.0,
"raw_sigma": 2.05,
"deb_prediction": 9.8,
"ens_median": 10.1,
"ensemble_spread": 2.05,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "ankara",
"date": "2026-03-29",
"actual_high": 10.0,
"raw_mu": 10.0,
"raw_sigma": 0.9000000000000004,
"deb_prediction": 9.7,
"ens_median": 10.0,
"ensemble_spread": 0.9000000000000004,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "atlanta",
"date": "2026-03-18",
"actual_high": 37.0,
"raw_mu": 54.06,
"raw_sigma": 4.0,
"deb_prediction": 52.5,
"ens_median": 52.2,
"ensemble_spread": 4.0,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "atlanta",
"date": "2026-03-19",
"actual_high": 18.6,
"raw_mu": 65.57,
"raw_sigma": 1.5500000000000007,
"deb_prediction": 65.7,
"ens_median": 65.9,
"ensemble_spread": 1.5500000000000007,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "buenos aires",
"date": "2026-03-18",
"actual_high": 17.0,
"raw_mu": 26.15,
"raw_sigma": 2.0,
"deb_prediction": 26.4,
"ens_median": 26.0,
"ensemble_spread": 2.0,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "buenos aires",
"date": "2026-03-19",
"actual_high": 16.0,
"raw_mu": 27.39,
"raw_sigma": 2.0999999999999996,
"deb_prediction": 26.8,
"ens_median": 27.9,
"ensemble_spread": 2.0999999999999996,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "chicago",
"date": "2026-03-18",
"actual_high": 46.0,
"raw_mu": 46.0,
"raw_sigma": 5.350000000000001,
"deb_prediction": 42.7,
"ens_median": 44.2,
"ensemble_spread": 5.350000000000001,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "dallas",
"date": "2026-03-18",
"actual_high": 73.9,
"raw_mu": 73.9,
"raw_sigma": 9.299999999999997,
"deb_prediction": 75.2,
"ens_median": 74.4,
"ensemble_spread": 9.299999999999997,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "hong kong",
"date": "2026-03-17",
"actual_high": 24.0,
"raw_mu": 24.0,
"raw_sigma": 1.9000000000000004,
"deb_prediction": 23.1,
"ens_median": 23.1,
"ensemble_spread": 1.9000000000000004,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "hong kong",
"date": "2026-03-18",
"actual_high": 27.8,
"raw_mu": 27.8,
"raw_sigma": 0.6,
"deb_prediction": 24.5,
"ens_median": 24.9,
"ensemble_spread": 0.6,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "hong kong",
"date": "2026-03-19",
"actual_high": 27.3,
"raw_mu": 27.6,
"raw_sigma": 0.6,
"deb_prediction": 24.9,
"ens_median": 25.0,
"ensemble_spread": 0.6,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "hong kong",
"date": "2026-03-23",
"actual_high": 27.4,
"raw_mu": 27.4,
"raw_sigma": 1.6999999999999993,
"deb_prediction": 25.2,
"ens_median": 25.1,
"ensemble_spread": 1.6999999999999993,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "london",
"date": "2026-03-18",
"actual_high": 16.0,
"raw_mu": 16.56,
"raw_sigma": 1.299999999999999,
"deb_prediction": 16.5,
"ens_median": 16.8,
"ensemble_spread": 1.299999999999999,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "london",
"date": "2026-03-19",
"actual_high": 8.0,
"raw_mu": 15.71,
"raw_sigma": 0.6,
"deb_prediction": 15.9,
"ens_median": 15.8,
"ensemble_spread": 0.6,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "lucknow",
"date": "2026-03-18",
"actual_high": 34.0,
"raw_mu": 35.11,
"raw_sigma": 1.3000000000000007,
"deb_prediction": 34.3,
"ens_median": 34.6,
"ensemble_spread": 1.3000000000000007,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "lucknow",
"date": "2026-03-19",
"actual_high": 30.0,
"raw_mu": 35.3,
"raw_sigma": 1.75,
"deb_prediction": 34.5,
"ens_median": 34.7,
"ensemble_spread": 1.75,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "madrid",
"date": "2026-03-18",
"actual_high": 14.0,
"raw_mu": 18.43,
"raw_sigma": 1.8499999999999996,
"deb_prediction": 18.3,
"ens_median": 18.7,
"ensemble_spread": 1.8499999999999996,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "madrid",
"date": "2026-03-19",
"actual_high": 8.0,
"raw_mu": 18.23,
"raw_sigma": 1.8999999999999995,
"deb_prediction": 18.4,
"ens_median": 18.8,
"ensemble_spread": 1.8999999999999995,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "miami",
"date": "2026-03-18",
"actual_high": 59.0,
"raw_mu": 70.04,
"raw_sigma": 2.8999999999999986,
"deb_prediction": 69.1,
"ens_median": 70.4,
"ensemble_spread": 2.8999999999999986,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "miami",
"date": "2026-03-19",
"actual_high": 24.6,
"raw_mu": 73.91,
"raw_sigma": 2.5500000000000043,
"deb_prediction": 74.2,
"ens_median": 74.0,
"ensemble_spread": 2.5500000000000043,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "milan",
"date": "2026-03-17",
"actual_high": 17.0,
"raw_mu": 17.3,
"raw_sigma": 9.1,
"deb_prediction": 12.5,
"ens_median": 15.1,
"ensemble_spread": 9.1,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "milan",
"date": "2026-03-18",
"actual_high": 14.0,
"raw_mu": 14.3,
"raw_sigma": 0.6,
"deb_prediction": 13.7,
"ens_median": 13.8,
"ensemble_spread": 0.6,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "milan",
"date": "2026-03-19",
"actual_high": 5.0,
"raw_mu": 16.58,
"raw_sigma": 1.25,
"deb_prediction": 16.9,
"ens_median": 17.0,
"ensemble_spread": 1.25,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "munich",
"date": "2026-03-18",
"actual_high": 10.0,
"raw_mu": 10.66,
"raw_sigma": 0.6,
"deb_prediction": 10.6,
"ens_median": 10.6,
"ensemble_spread": 0.6,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "munich",
"date": "2026-03-19",
"actual_high": 5.0,
"raw_mu": 11.62,
"raw_sigma": 1.3000000000000007,
"deb_prediction": 11.5,
"ens_median": 11.8,
"ensemble_spread": 1.3000000000000007,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "new york",
"date": "2026-03-18",
"actual_high": 32.0,
"raw_mu": 36.94,
"raw_sigma": 2.25,
"deb_prediction": 36.8,
"ens_median": 37.0,
"ensemble_spread": 2.25,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "paris",
"date": "2026-03-18",
"actual_high": 14.0,
"raw_mu": 14.47,
"raw_sigma": 0.9000000000000004,
"deb_prediction": 14.2,
"ens_median": 14.8,
"ensemble_spread": 0.9000000000000004,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "paris",
"date": "2026-03-19",
"actual_high": 8.0,
"raw_mu": 16.06,
"raw_sigma": 0.6,
"deb_prediction": 16.2,
"ens_median": 16.3,
"ensemble_spread": 0.6,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "sao paulo",
"date": "2026-03-18",
"actual_high": 22.0,
"raw_mu": 29.64,
"raw_sigma": 2.450000000000001,
"deb_prediction": 30.0,
"ens_median": 29.4,
"ensemble_spread": 2.450000000000001,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "sao paulo",
"date": "2026-03-19",
"actual_high": 21.0,
"raw_mu": 26.5,
"raw_sigma": 1.200000000000001,
"deb_prediction": 25.5,
"ens_median": 26.5,
"ensemble_spread": 1.200000000000001,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "seattle",
"date": "2026-03-18",
"actual_high": 55.9,
"raw_mu": 55.9,
"raw_sigma": 1.3500000000000014,
"deb_prediction": 55.8,
"ens_median": 55.9,
"ensemble_spread": 1.3500000000000014,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "seoul",
"date": "2026-03-18",
"actual_high": 8.0,
"raw_mu": 8.0,
"raw_sigma": 0.7999999999999998,
"deb_prediction": 7.4,
"ens_median": 7.4,
"ensemble_spread": 0.7999999999999998,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "seoul",
"date": "2026-03-19",
"actual_high": 10.0,
"raw_mu": 10.3,
"raw_sigma": 1.7999999999999998,
"deb_prediction": 7.6,
"ens_median": 6.3,
"ensemble_spread": 1.7999999999999998,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "shanghai",
"date": "2026-03-18",
"actual_high": 13.0,
"raw_mu": 13.0,
"raw_sigma": 1.1500000000000004,
"deb_prediction": 13.2,
"ens_median": 13.0,
"ensemble_spread": 1.1500000000000004,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "shanghai",
"date": "2026-03-19",
"actual_high": 12.0,
"raw_mu": 12.3,
"raw_sigma": 0.7999999999999998,
"deb_prediction": 10.7,
"ens_median": 11.2,
"ensemble_spread": 0.7999999999999998,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "shanghai",
"date": "2026-03-29",
"actual_high": 18.0,
"raw_mu": 18.0,
"raw_sigma": 1.6999999999999993,
"deb_prediction": 18.4,
"ens_median": 17.6,
"ensemble_spread": 1.6999999999999993,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "singapore",
"date": "2026-03-18",
"actual_high": 32.0,
"raw_mu": 32.0,
"raw_sigma": 0.9500000000000011,
"deb_prediction": 30.2,
"ens_median": 30.1,
"ensemble_spread": 0.9500000000000011,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "singapore",
"date": "2026-03-19",
"actual_high": 32.0,
"raw_mu": 32.3,
"raw_sigma": 1.3499999999999996,
"deb_prediction": 31.5,
"ens_median": 32.1,
"ensemble_spread": 1.3499999999999996,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "taipei",
"date": "2026-03-17",
"actual_high": 26.7,
"raw_mu": 26.7,
"raw_sigma": 2.25,
"deb_prediction": 24.9,
"ens_median": 25.4,
"ensemble_spread": 2.25,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "taipei",
"date": "2026-03-18",
"actual_high": 29.0,
"raw_mu": 29.0,
"raw_sigma": 1.0500000000000007,
"deb_prediction": 27.2,
"ens_median": 27.5,
"ensemble_spread": 1.0500000000000007,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "taipei",
"date": "2026-03-19",
"actual_high": 22.0,
"raw_mu": 21.7,
"raw_sigma": 1.1500000000000004,
"deb_prediction": 21.5,
"ens_median": 21.3,
"ensemble_spread": 1.1500000000000004,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "tel aviv",
"date": "2026-03-18",
"actual_high": 30.0,
"raw_mu": 30.3,
"raw_sigma": 2.1500000000000004,
"deb_prediction": 29.0,
"ens_median": 28.9,
"ensemble_spread": 2.1500000000000004,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "tel aviv",
"date": "2026-03-19",
"actual_high": 21.0,
"raw_mu": 21.3,
"raw_sigma": 1.5,
"deb_prediction": 20.7,
"ens_median": 21.1,
"ensemble_spread": 1.5,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "tokyo",
"date": "2026-03-18",
"actual_high": 17.0,
"raw_mu": 17.0,
"raw_sigma": 1.5499999999999998,
"deb_prediction": 15.4,
"ens_median": 15.8,
"ensemble_spread": 1.5499999999999998,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "tokyo",
"date": "2026-03-19",
"actual_high": 16.0,
"raw_mu": 16.5,
"raw_sigma": 2.0999999999999996,
"deb_prediction": 17.8,
"ens_median": 18.5,
"ensemble_spread": 2.0999999999999996,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "toronto",
"date": "2026-03-18",
"actual_high": -6.0,
"raw_mu": -1.01,
"raw_sigma": 0.8,
"deb_prediction": -0.6,
"ens_median": -1.1,
"ensemble_spread": 0.8,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "toronto",
"date": "2026-03-19",
"actual_high": -2.0,
"raw_mu": 5.67,
"raw_sigma": 2.1500000000000004,
"deb_prediction": 6.4,
"ens_median": 6.3,
"ensemble_spread": 2.1500000000000004,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "warsaw",
"date": "2026-03-17",
"actual_high": 11.0,
"raw_mu": 11.3,
"raw_sigma": 0.6499999999999995,
"deb_prediction": 10.4,
"ens_median": 10.5,
"ensemble_spread": 0.6499999999999995,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "warsaw",
"date": "2026-03-18",
"actual_high": 13.0,
"raw_mu": 13.84,
"raw_sigma": 1.4000000000000004,
"deb_prediction": 13.6,
"ens_median": 14.2,
"ensemble_spread": 1.4000000000000004,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "warsaw",
"date": "2026-03-19",
"actual_high": 7.0,
"raw_mu": 12.03,
"raw_sigma": 1.5999999999999996,
"deb_prediction": 11.8,
"ens_median": 12.3,
"ensemble_spread": 1.5999999999999996,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "wellington",
"date": "2026-03-18",
"actual_high": 21.0,
"raw_mu": 21.0,
"raw_sigma": 0.9500000000000011,
"deb_prediction": 19.2,
"ens_median": 19.1,
"ensemble_spread": 0.9500000000000011,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
},
{
"city": "wellington",
"date": "2026-03-19",
"actual_high": 18.0,
"raw_mu": 18.3,
"raw_sigma": 2.0999999999999996,
"deb_prediction": 17.9,
"ens_median": 17.1,
"ensemble_spread": 2.0999999999999996,
"max_so_far_gap": null,
"peak_flag": 0.0,
"sample_source": "daily_record",
"settlement_source": null,
"settlement_station_code": null,
"truth_version": null,
"truth_updated_by": null,
"truth_updated_at": null
}
]
}
+10
View File
@@ -0,0 +1,10 @@
$VPS = "root@38.54.27.70"
$PROJECT = "/root/PolyWeather"
Write-Host "🚀 Deploying to $VPS..." -ForegroundColor Cyan
ssh $VPS "cd $PROJECT && git pull && docker compose up -d --build"
Write-Host "✅ Deploy complete. Checking health..." -ForegroundColor Green
Start-Sleep 8
ssh $VPS "curl -s http://localhost:8000/healthz"
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
set -e
VPS="root@38.54.27.70"
PROJECT="/root/PolyWeather"
echo "🚀 Deploying to $VPS..."
ssh "$VPS" "cd $PROJECT && git pull && docker compose up -d --build"
echo "✅ Deploy complete. Checking health..."
sleep 8
ssh "$VPS" "curl -s http://localhost:8000/healthz"
+11 -84
View File
@@ -10,15 +10,15 @@ services:
container_name: polyweather_bot
restart: unless-stopped
volumes:
# Persist runtime data outside git workspace.
# Host path defaults to /var/lib/polyweather and can be overridden in .env.
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/var/lib/polyweather
# Keep /app/data compatibility for existing cache/state defaults.
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data
- ./bot.log:/app/bot.log # 挂载日志文件
# UID/GID are mainly useful on Linux hosts to avoid root-owned output files.
# Windows / macOS can usually keep the fallback values.
- ./bot.log:/app/bot.log
user: "${UID:-1000}:${GID:-1000}"
healthcheck:
test: ["CMD", "python", "-c", "import sqlite3; c=sqlite3.connect('/var/lib/polyweather/polyweather.db'); c.execute('SELECT 1'); c.close()"]
interval: 60s
timeout: 10s
retries: 3
polyweather_web:
<<: *polyweather-base
@@ -26,87 +26,14 @@ services:
restart: unless-stopped
command: python web/app.py
volumes:
# Web service shares the same runtime data directory as bot/state tasks.
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/var/lib/polyweather
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data
ports:
- "8000:8000"
# UID/GID are mainly useful on Linux hosts to avoid root-owned output files.
user: "${UID:-1000}:${GID:-1000}"
healthcheck:
test: ["CMD", "python", "-c", "from urllib.request import urlopen; urlopen('http://localhost:8000/healthz')"]
interval: 30s
timeout: 5s
retries: 3
polyweather_prewarm:
<<: *polyweather-base
container_name: polyweather_prewarm
restart: unless-stopped
profiles: ["workers"]
command: python scripts/prewarm_dashboard_worker.py --include-detail --include-market
volumes:
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/var/lib/polyweather
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data
user: "${UID:-1000}:${GID:-1000}"
polyweather_prometheus:
image: prom/prometheus:v3.4.1
container_name: polyweather_prometheus
restart: unless-stopped
profiles: ["monitoring"]
depends_on:
- polyweather_web
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=15d"
- "--web.enable-lifecycle"
volumes:
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./monitoring/prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}/monitoring/prometheus:/prometheus
ports:
- "${POLYWEATHER_PROMETHEUS_PORT:-9090}:9090"
polyweather_alertmanager:
image: prom/alertmanager:v0.28.1
container_name: polyweather_alertmanager
restart: unless-stopped
profiles: ["monitoring"]
depends_on:
- polyweather_alert_relay
command:
- "--config.file=/etc/alertmanager/alertmanager.yml"
- "--storage.path=/alertmanager"
volumes:
- ./monitoring/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}/monitoring/alertmanager:/alertmanager
ports:
- "${POLYWEATHER_ALERTMANAGER_PORT:-9093}:9093"
polyweather_alert_relay:
<<: *polyweather-base
container_name: polyweather_alert_relay
restart: unless-stopped
profiles: ["monitoring"]
command: python scripts/alertmanager_telegram_relay.py
volumes:
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/var/lib/polyweather
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data
ports:
- "${POLYWEATHER_ALERT_RELAY_PORT:-9099}:9099"
user: "${UID:-1000}:${GID:-1000}"
polyweather_grafana:
image: grafana/grafana-oss:12.0.2
container_name: polyweather_grafana
restart: unless-stopped
profiles: ["monitoring"]
depends_on:
- polyweather_prometheus
environment:
GF_SECURITY_ADMIN_USER: ${POLYWEATHER_GRAFANA_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${POLYWEATHER_GRAFANA_ADMIN_PASSWORD:-polyweather}
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}/monitoring/grafana:/var/lib/grafana
ports:
- "${POLYWEATHER_GRAFANA_PORT:-3001}:3000"
+135
View File
@@ -0,0 +1,135 @@
# 机场高频数据接入市场监控频道方案
## 背景
### 现有数据
| 城市 | 站点 | ICAO/站点 | 数据类型 | 数据源 | 刷新频率 |
|------|------|-----------|---------|--------|---------|
| 首尔 | 仁川国际 | RKSI | 跑道对温度(2 对) | AMOS | 1 分钟 |
| 釜山 | 金海国际 | RKPK | 跑道对温度(1 对) | AMOS | 1 分钟 |
| 东京 | 羽田 | RJTT | 机场站点实时温度 | JMA AMeDAS | 10 分钟 |
| 安卡拉 | Esenboğa | 17128 | 机场站点实时温度 | MGM | 不定,约 5-15 分钟 |
### 现有 Telegram 推送系统
- **循环**: `start_trade_alert_push_loop`,默认每 30 分钟跑一轮
- **覆盖城市**: `TELEGRAM_ALERT_CITIES`(默认全部 51 城)
- **3 条规则**: Ankara Center DEB 命中、预报突破、暖平流
- **门禁**: 严重度/触发数/冷却期 多层过滤
- **消息**: 中英双语,包含触发类型、实况温度
### 问题
四座机场城市的实时数据已就绪,但现有推送系统 30 分钟一轮对所有城市一视同仁。1-10 分钟级高频数据在接近交易高峰期时,温度变化可能比 30 分钟窗口更快,需要更灵敏的监控。
---
## 方案设计
### 核心思路
在现有 30 分钟主循环之上叠加高频通道,对四座机场城市用 10 分钟间隔独立检测温度急变。温度波动达到阈值时推送告警,包含当前温度 + DEB 预测最高温。不做市场分析、不输出 AI 建议、不约定时快照。
### 1. 高频机场城市快速通道
在现有 30 分钟主循环之外,为 `{seoul, busan, tokyo, ankara}` 单独跑一个 10 分钟间隔的子循环,每个城市独立检测温度急变。
**配置(写死在代码中)**:
```python
HIGH_FREQ_AIRPORT_CITIES = {"seoul", "busan", "tokyo", "ankara"}
HIGH_FREQ_PUSH_INTERVAL_SEC = 600 # 10 分钟
HIGH_FREQ_MOMENTUM_THRESHOLD_C = 0.5 # 比默认 0.8°C 更灵敏
HIGH_FREQ_COOLDOWN_SEC = 7200 # 同一城市冷却 2 小时
```
**逻辑**:
- 主循环 30 分钟照常跑全部城市(不变)
- 每 10 分钟对四座机场城市各检查一次温度急变
- 高频轮次仅检查 `airport_rapid_temp_change` 一条规则
- 各城市独立冷却,触发后 2 小时内同一城市不再重复推送
- **最高温已锁定则跳过**:当日最高已过且持续下降,不再推送
### 2. 机场观测积累与趋势检测
**新增数据库表**: `airport_obs_log`
```sql
CREATE TABLE IF NOT EXISTS airport_obs_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
icao TEXT NOT NULL,
city TEXT NOT NULL,
temp_c REAL,
wind_kt REAL,
pressure_hpa REAL,
obs_time TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_airport_obs_log_icao_time
ON airport_obs_log(icao, created_at DESC);
```
**写入**: 在 AMOS/JMA/MGM 成功获取数据后自动调用 `append_airport_obs()` 写入。自动清理 2 小时前的旧数据。
**读取**: `get_airport_obs_recent(icao, minutes=30)` 返回最近 N 分钟观测列表,用于计算温度变化斜率。
### 3. 温度突变即时告警
基于积累的观测日志,新增告警规则 `airport_rapid_temp_change`。每条告警 per-city 独立推送。
| 参数 | 值 | 说明 |
|------|-----|------|
| 滑动窗口 | 20 分钟 | 取最近 20 分钟内的观测 |
| 最少样本 | 3 条 | 确保有足够数据点 |
| 触发阈值 | > 0.5°C/10min | 比默认 0.8°C/30min 更灵敏 |
| 冷却期 | 2 小时 | 同城市两次推送最小间隔 |
| 锁定跳过 | 最高温已锁定 | 当日最高已过且持续下降,不推送 |
**告警消息示例**:
首尔/釜山(跑道对温度):
```
🚨 首尔/仁川 温度急变
15L/33R 14.6°C
15R/33L 15.2°C
DEB 预测最高 18.2°C
```
东京/安卡拉(站点实时温度):
```
🚨 东京/羽田 温度急变
当前 24.1°C
DEB 预测最高 26.5°C
```
---
## 改动文件清单
| 优先级 | 文件 | 改动 |
|--------|------|------|
| 1 | `src/database/db_manager.py` | 新增 `airport_obs_log` 表、`append_airport_obs()``get_airport_obs_recent()` |
| 2 | `src/data_collection/weather_sources.py` | AMOS/JMA/MGM 成功后调用 `append_airport_obs()` 写日志 |
| 3 | `src/analysis/market_alert_engine.py` | 新增 `airport_rapid_temp_change` 规则 |
| 4 | `src/utils/telegram_push.py` | 10 分钟高频子循环、温度急变告警推送、最高温锁定跳过 |
| 5 | `src/bot/runtime_coordinator.py` | 注册机场高频推送循环 |
---
## 实施顺序
1. **Phase 1 — DB 层**: `airport_obs_log` 表 + 读写方法
2. **Phase 2 — 采集层**: AMOS/JMA/MGM 成功后自动写日志,部署观察 1-2 天确认数据积累正常
3. **Phase 3 — 告警引擎**: `airport_rapid_temp_change` 规则 + 单元测试
4. **Phase 4 — 推送层**: 高频快速通道,直接推送市场监控频道
5. **Phase 5 — 调参**: 观察 3-7 天调整阈值
---
## 风险与注意事项
- **AMOS/JMA/MGM 站点可用性**: 各数据源可能偶发性不可用,需容错处理
- **告警频率控制**: 高频循环可能产生过多告警,需要严格的冷却期和去重机制
- **数据库体积**: `airport_obs_log` 每 1-10 分钟写入 4 条记录,2 小时约 48-480 条,自动清理后体积可控
- **安卡拉 MGM 刷新频率**: `servis.mgm.gov.tr` 实测更新间隔 5-15 分钟不等,非固定周期
+100
View File
@@ -0,0 +1,100 @@
# 机场高频实时数据源
## 已接入城市
| 城市 | 机场 | ICAO/站点 | 数据源 | 频率 | 类型 | 费用 |
|------|------|-----------|--------|------|------|------|
| 首尔 | 仁川国际 | RKSI | AMOS (`global.amo.go.kr`) | 1 分钟 | 跑道对温度(2对) | 免费 |
| 釜山 | 金海国际 | RKPK | AMOS (`global.amo.go.kr`) | 1 分钟 | 跑道对温度(1对) | 免费 |
| 东京 | 羽田 | RJTT | JMA AMeDAS (`jma.go.jp`) | 10 分钟 | 机场站点实时温度 | 免费 |
| 安卡拉 | Esenboğa | 17128 | MGM (`servis.mgm.gov.tr`) | 5-15 分钟 | 机场站点实时温度 | 免费 |
| 伊斯坦布尔 | 伊斯坦布尔机场 | 17058 | MGM (`servis.mgm.gov.tr`) | 5-15 分钟 | 机场站点实时温度 | 免费 |
| 赫尔辛基 | Vantaa | EFHK | FMI (`opendata.fmi.fi`) | 10 分钟 | 机场站点实时温度 | 免费 |
| 阿姆斯特丹 | Schiphol | EHAM | KNMI (`dataplatform.knmi.nl`) | 10 分钟 | 机场站点实时温度 | 免费(需注册) |
| 巴黎 | Le Bourget | LFPB | AROME HD (`api.open-meteo.com`) | 15 分钟 | 模型预报(非实测) | 免费 |
| 新加坡 | Changi | WSSS | Singapore MSS (`api.data.gov.sg`) | 1 分钟 | 机场站点实时温度 (S24 站) | 免费 |
| 纽约 | LaGuardia | KLGA | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
| 洛杉矶 | LAX | KLAX | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
| 芝加哥 | O'Hare | KORD | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
| 丹佛 | Buckley | KBKF | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
| 亚特兰大 | Hartsfield | KATL | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
| 迈阿密 | MIA | KMIA | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
| 旧金山 | SFO | KSFO | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
| 休斯顿 | Hobby | KHOU | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
| 达拉斯 | Love Field | KDAL | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
| 奥斯汀 | Bergstrom | KAUS | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
| 西雅图 | SeaTac | KSEA | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
> **Singapore MSS**: 新加坡气象局(MSS)通过 data.gov.sg 开放数据平台提供全国 15 个站点
> 的干球温度(1 分钟均值),更新频率 ~1 分钟。选取 S24 Upper Changi Road North 站
> 作为樟宜机场 (WSSS) 的实时温度锚点。数据公开免费,无需 API 密钥。
> 后端通过 `singapore_mss_sources.py` 拉取并注入 `airport_primary`。
> **NOAA MADIS HFMETAR**: 美国 11 个城市的机场高频实时数据通过 NOAA MADIS 公共档案获取。
> 数据源为 NetCDF 格式(`madis-data.ncep.noaa.gov/madisPublic1/data/LDAD/hfmetar/`),
> 每 5 分钟全量更新一次,温度保留一位小数。匿名公开访问,无需 API 密钥。
> 后端通过 `weather_sources.py` 拉取并注入 `airport_primary`,前端市场监控通过
> `resolveMonitorTemperature` 优先读取 `airport_primary.temp` 获得小数精度温度。
## 推送机制
- 每城按原生频率独立推送,不捆绑
- 首尔/釜山 60s,其余 600s
- 循环轮询 60s 以匹配最快频率
- 仅当当前温度距 DEB 预测最高 ≤3°C 时推送
- 确认过峰值后自动停止
## 前端市场监控 freshness 契约
后端城市详情接口会在 `current.freshness` / `airport_current.freshness` 返回源感知更新时间信息,前端市场监控不再用统一的 `obs_age_min` 判断所有城市。
关键字段:
```json
{
"source_code": "amos",
"source_label": "AMOS",
"observed_at": "2026-05-14T11:59:10+00:00",
"observed_at_local": "20:59",
"native_update_interval_sec": 60,
"expected_next_update_at": "2026-05-14T12:00:10+00:00",
"freshness_status": "fresh",
"freshness_reason": "within_native_fresh_window",
"age_sec": 50
}
```
前端刷新规则:
- 首次进入市场监控:强制刷新全部城市,绕过 30 分钟前端缓存。
- 定时轮询:仍以 60s tick 检查,但只刷新已到 `expected_next_update_at``delayed``stale` 或缺失的城市。
- BFF 代理:`force_refresh=true` 时使用 `no-store`,避免 Next fetch revalidate 缓存吞掉强刷。
- 展示:卡片 tooltip 显示源端名称、原生更新间隔和当前 freshness 状态。
## 消息模板
```
Seoul / Incheon 16:03
15L/33R 14.6°C
15R/33L 15.2°C
今日DEB预报最高:18.2°C
今日实测最高:16.5°C15:30
```
## 环境变量
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `TELEGRAM_AIRPORT_PUSH_ENABLED` | 启用机场推送 | `true` |
| `TELEGRAM_AIRPORT_PUSH_INTERVAL_SEC` | 循环轮询间隔 | `60` |
| `KNMI_API_KEY` | KNMI API 密钥(阿姆斯特丹必填) | — |
## 未接入城市
| 城市 | 原因 |
|------|------|
| 马德里/Barajas | AEMET 注册页面失效 |
| 伦敦/Heathrow | Met Office 仅 1 小时更新 |
| 慕尼黑 | DWD 延迟 ~1 小时 |
| 米兰/华沙/莫斯科 | 无已知实时源 |
+8 -12
View File
@@ -1,4 +1,4 @@
# PolyWeather API 文档(v1.5.4
# PolyWeather API 文档(v1.7.0
最后更新:`2026-04-27`
@@ -126,18 +126,14 @@ SSE 事件:
#### 2. `probabilities`
概率层现在按“校准模型概率”对外解释,而不是直接把模型票数或市场价格当成概率
概率层基于 legacy 高斯分桶,以 DEB 融合预测 μ 和 ensemble spread σ 生成 1°C 粒度概率分布
新增 / 重点字段:
概率字段:
- `engine`概率引擎名称,例如 `lgbm_calibrated``emos``legacy`
- `calibration_mode`:校准运行模式
- `calibration_version`:校准产物版本
- `raw_mu` / `raw_sigma`:原始分布参数
- `calibrated_mu` / `calibrated_sigma`:校准后分布参数
- `shadow_distribution`:shadow / 对照分布,供回归与灰度验证
当前前端展示 `probabilities.engine` 对应的生产概率分布;`EMOS` / `LGBM` 只有在评估通过、显式启用或 shadow 对照时才进入展示/解释层。模型共识与市场价格只作为辅助参考,不再作为主结论。
- `engine`固定为 `legacy`
- `mu`DEB 融合预测中心值
- `distribution`:当天合约桶概率分布
- `distribution_all`:包含外围桶的完整分布
#### 3. `detail_depth`
@@ -206,7 +202,7 @@ SSE 事件:
- 多数机场市场以 `METAR` / 机场主站实况为结算锚点。
- `Wunderground` 是历史页面或参考入口,不应在产品文案里被描述成“站”。
- `MGM / NMC / JMA / KMA / HKO / CWA` 等官方站网属于增强层或明确官方站点层;只有合约规则明确指定时,才作为最终结算站点。
- `MGM / NMC / JMA / AMOS / HKO / CWA` 等官方站网属于增强层或明确官方站点层;只有合约规则明确指定时,才作为最终结算站点。
## 4. 鉴权与账户接口
+4 -4
View File
@@ -8,7 +8,7 @@ PolyWeather 是面向温度结算场景的气象决策层,不是通用天气
核心价值:
- 观测优先(METAR / 机场主站 / 明确官方站点;MGM、NMC、JMA、KMA 等作为增强层)
- 观测优先(METAR / 机场主站 / 明确官方站点;MGM、NMC、JMA、AMOS 等作为增强层)
- 结算导向(DEB + 校准概率桶)
- 气象判断优先(证据链、失效条件、下一观测点)
- 市场映射(行情对照 + 错价雷达),但不把交易建议放在第一层产品承诺
@@ -18,7 +18,7 @@ PolyWeather 是面向温度结算场景的气象决策层,不是通用天气
| 能力 | 状态 | 备注 |
| :-- | :-- | :-- |
| 登录注册(Google + 邮箱) | 已上线 | Supabase 鉴权 |
| 订阅套餐(Pro 月付) | 已上线 | `5 USDC / 30天` |
| 订阅套餐(Pro 月付) | 已上线 | `10 USDC / 30天` |
| 积分抵扣 | 已上线 | `500分=1U`,最多 `3U` |
| 合约支付 | 已上线 | PolygonUSDC + USDC.e |
| 支付自动确认 | 已上线 | Event Loop + Confirm Loop |
@@ -32,13 +32,13 @@ PolyWeather 是面向温度结算场景的气象决策层,不是通用天气
- Pro 用户:
- 今日日内深度分析(含高温时段)
- 专业气象结论条、证据链、失效条件、确认条件
- LGBM / EMOS 等校准概率层
- 概率分布层(基于 DEB 融合 + 高斯分桶)
- 历史对账 + 未来日期分析
- 全平台智能气象推送
## 4. 收费与积分规则(默认)
- 套餐:`pro_monthly`5 USDC / 30 天)
- 套餐:`pro_monthly`10 USDC / 30 天)
- 抵扣:500 积分抵 1 USDC,最高抵 3 USDC
- 实付下限:2 USDC(当积分满额时)
-19
View File
@@ -111,7 +111,6 @@ PolyWeather 的环境变量很多,但不是所有变量都属于同一层级
- `TELEGRAM_MARKET_FOCUS_DIGEST_ENABLED`
- `POLYMARKET_WALLET_ACTIVITY_ENABLED`(已退役,建议保持 `false`
- `POLYWEATHER_DASHBOARD_PREWARM_ENABLED`
- `POLYWEATHER_GROQ_COMMENTARY_ENABLED`
### 4.3 L3:运行调优项
@@ -137,9 +136,6 @@ PolyWeather 的环境变量很多,但不是所有变量都属于同一层级
- `POLYWEATHER_PREWARM_INCLUDE_DETAIL`
- `POLYWEATHER_PREWARM_INCLUDE_MARKET`
- `POLYWEATHER_PREWARM_FORCE_REFRESH`
- `POLYWEATHER_GROQ_COMMENTARY_MODEL`
- `POLYWEATHER_GROQ_COMMENTARY_TIMEOUT_SEC`
- `POLYWEATHER_GROQ_COMMENTARY_CACHE_TTL_SEC`
策略:
@@ -167,7 +163,6 @@ PolyWeather 的环境变量很多,但不是所有变量都属于同一层级
- `METEOBLUE_API_KEY`
- `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID`
- `POLYMARKET_SECRET_KEY`
- `GROQ_API_KEY`
## 5. 推荐部署矩阵
@@ -266,10 +261,6 @@ POLYWEATHER_PREWARM_JITTER_SEC=20
POLYWEATHER_PREWARM_INCLUDE_DETAIL=true
POLYWEATHER_PREWARM_INCLUDE_MARKET=true
POLYWEATHER_BACKEND_URL=http://polyweather_web:8000
POLYWEATHER_GROQ_COMMENTARY_ENABLED=false
POLYWEATHER_GROQ_COMMENTARY_MODEL=openai/gpt-oss-20b
POLYWEATHER_GROQ_COMMENTARY_TIMEOUT_SEC=8
POLYWEATHER_GROQ_COMMENTARY_CACHE_TTL_SEC=1800
POLYWEATHER_SCAN_AI_ENABLED=false
POLYWEATHER_SCAN_AI_API_KEY=...
POLYWEATHER_SCAN_AI_PROVIDER=mimo
@@ -292,8 +283,6 @@ POLYWEATHER_SCAN_CITY_AI_MODEL=mimo-v2.5-pro
- `TELEGRAM_MARKET_FOCUS_DIGEST_INTERVAL_SEC` 表示主动推送间隔,默认 `1800` 秒(30 分钟)。
- `POLYMARKET_WALLET_ACTIVITY_ENABLED` 已退役,保留为 `false` 即可,不建议再启用钱包异动监听。
- `POLYWEATHER_DASHBOARD_PREWARM_ENABLED=true` 时,建议同时启用独立 worker 或 bot 内嵌预热线程。
- `POLYWEATHER_BACKEND_URL` 仅在独立 `polyweather_prewarm` worker 容器中使用,建议设为 `http://polyweather_web:8000`,不要写 `127.0.0.1`
- `POLYWEATHER_GROQ_COMMENTARY_ENABLED=false` 表示默认仍走规则文案;只有在确实配置了 `GROQ_API_KEY` 时才建议开启。
### 6.3 Dashboard 预热 worker 推荐变量
@@ -311,22 +300,14 @@ POLYWEATHER_BACKEND_URL=http://polyweather_web:8000
说明:
- 这组变量用于后台定向预热热点城市,避免用户点击城市时才冷启动拉 detail。
- 如果使用独立 `polyweather_prewarm` 容器,`POLYWEATHER_BACKEND_URL` 必须指向容器网络中的 `polyweather_web`
### 6.4 Groq 解读增强层
```env
POLYWEATHER_GROQ_COMMENTARY_ENABLED=true
GROQ_API_KEY=...
POLYWEATHER_GROQ_COMMENTARY_MODEL=openai/gpt-oss-20b
POLYWEATHER_GROQ_COMMENTARY_TIMEOUT_SEC=8
POLYWEATHER_GROQ_COMMENTARY_CACHE_TTL_SEC=1800
```
说明:
- 这层只负责把结构化信号改写成短摘要,不替代真实模型、机场锚点和结算逻辑。
- Groq 调用失败时,系统会自动回退到规则文案。
### 6.5 机器人市场监控建议配置
-685
View File
@@ -1,685 +0,0 @@
# EMOS + LGBM 系统说明(中文)
最后更新:`2026-04-19`
本文档用于完整说明 PolyWeather 当前的两条统计/机器学习链路:
- `EMOS`:概率后处理与校准链路
- `LGBM`:日最高温点预测辅助模型
重点不只是“模型怎么训练”,还包括:
- 这些模型依赖什么历史数据
- 真值和训练特征现在如何长期保存
- 为什么过去样本一直不够
- 当前线上到底运行在哪个模式
- 现在能做什么,不能做什么
本文档基于仓库当前实现与最近一轮重建结果,适合作为:
- 项目内部模型说明
- 运维与数据治理说明
- 未来继续扩展 EMOS/LGBM 的基线文档
---
## 1. 总览
PolyWeather 当前不是“用一个模型替代所有东西”,而是多层结构:
1. 多源天气采集层
2. `DEB` 业务主预测层
3. `LGBM` 轻量点预测辅助层
4. `EMOS` 概率校准层
5. 市场概率/桶命中评估层
可以简化理解为:
```text
天气源 / 观测 / 历史真值
DEB 主预测
LGBM 辅助点预测
EMOS 对概率分布做后处理
市场概率 / shadow / rollout 门禁
```
其中:
- `DEB` 仍然是当前业务主路径
- `LGBM` 是辅助预测源,不是主路径
- `EMOS` 是概率后处理,不是基础天气模型
---
## 2. 两条链路各自负责什么
### 2.1 EMOS 负责什么
`EMOS` 的全称通常指 Ensemble Model Output Statistics。
在本项目里,它的角色不是重新预测温度,而是:
- 把已有的预测结果做概率后处理
- 让输出分布更“可校准”
- 让桶概率和市场评估更稳定
EMOS 关注的是:
- `raw_mu`
- `raw_sigma`
- `deb_prediction`
- `ens_median`
- `ensemble_spread`
- `max_so_far_gap`
- `peak_flag`
- 最终真实 `actual_high`
它最终输出的是一套“经过校准的概率分布”,而不是单一温度值。
所以 EMOS 的核心衡量指标不是单纯 MAE,而更看重:
- `CRPS`
- `bucket_hit_rate`
- `bucket_brier`
### 2.2 LGBM 负责什么
`LGBM` 是一个轻量级的回归模型,用来预测:
- `actual_high`(日最高温)
它吃的是:
- 历史真值 lag 特征
- 多模型 forecast
- `deb_prediction`
- 当前观测特征
- 时间特征
它输出的是:
- 一个点预测 `actual_high`
然后这个点预测可以作为:
- 额外 forecast 源
- 供 DEB / 运营 / 研究参考
所以它和 EMOS 的区别非常重要:
- `LGBM`:做点预测
- `EMOS`:做概率校准
---
## 3. 当前代码结构
### 3.1 EMOS 相关
核心文件:
- [probability_calibration.py](/E:/web/PolyWeather/src/analysis/probability_calibration.py)
- [probability_rollout.py](/E:/web/PolyWeather/src/analysis/probability_rollout.py)
- [fit_probability_calibration.py](/E:/web/PolyWeather/scripts/fit_probability_calibration.py)
- [evaluate_probability_calibration.py](/E:/web/PolyWeather/scripts/evaluate_probability_calibration.py)
- [build_probability_shadow_report.py](/E:/web/PolyWeather/scripts/build_probability_shadow_report.py)
- [judge_probability_rollout.py](/E:/web/PolyWeather/scripts/judge_probability_rollout.py)
核心产物:
- [default.json](/E:/web/PolyWeather/artifacts/probability_calibration/default.json)
- [evaluation_report.json](/E:/web/PolyWeather/artifacts/probability_calibration/evaluation_report.json)
- [shadow_report.json](/E:/web/PolyWeather/artifacts/probability_calibration/shadow_report.json)
- [rollout_report.json](/E:/web/PolyWeather/artifacts/probability_calibration/rollout_report.json)
- [training_samples.json](/E:/web/PolyWeather/artifacts/probability_calibration/training_samples.json)
### 3.2 LGBM 相关
核心文件:
- [lgbm_daily_high.py](/E:/web/PolyWeather/src/models/lgbm_daily_high.py)
- [lgbm_features.py](/E:/web/PolyWeather/src/models/lgbm_features.py)
- [train_lgbm_daily_high.py](/E:/web/PolyWeather/scripts/train_lgbm_daily_high.py)
- [report_lgbm_daily_high.py](/E:/web/PolyWeather/scripts/report_lgbm_daily_high.py)
核心产物:
- [lgbm_daily_high.txt](/E:/web/PolyWeather/artifacts/models/lgbm_daily_high.txt)
- [lgbm_daily_high_schema.json](/E:/web/PolyWeather/artifacts/models/lgbm_daily_high_schema.json)
---
## 4. 为什么之前样本总是上不去
这件事是理解当前状态的关键。
过去项目里有一个结构性问题:
- `daily_records_store` 同时承担了
- 运行态缓存
- 历史训练数据来源
但运行态层会把 `daily_records` 硬裁成最近 14 天。
这意味着:
- 对线上运行来说没问题
- 对训练来说,历史监督样本会不断被删掉
结果就是:
- 城市越来越多
- 训练历史反而越来越稀
- `LGBM` 很容易只有二十几条样本
- `EMOS` 也只能靠有限 snapshot/daily_record 拼起来
这不是“模型太差”,而是“数据主存设计不对”。
---
## 5. 这次历史真值治理做了什么
现在已经把“运行态缓存”和“长期训练主存”拆开了。
### 5.1 `daily_records_store`
继续保留,但只作为:
- 最近 14 天运行态缓存
它不再承担长期训练历史职责。
### 5.2 `truth_records_store`
新增永久真值表,作为长期训练真值主存。
当前核心字段包括:
- `city`
- `target_date`
- `actual_high`
- `settlement_source`
- `settlement_station_code`
- `settlement_station_label`
- `truth_version`
- `updated_by`
- `updated_at`
- `source_payload_json`
- `is_final`
这张表的意义是:
- 长期保存监督真值
- 不再被 14 天缓存裁剪
- 真值来源变得可追溯
### 5.3 `truth_revisions_store`
新增真值修订审计表。
它记录:
- 老值是什么
- 新值是什么
- 来源怎么变了
- 谁改的
- 为什么改
- 什么时候改
所以现在回填不会再是“静默覆盖”。
### 5.4 `training_feature_records_store`
新增长期训练特征表。
它长期留存:
- forecasts
- deb_prediction
- mu
- probability_features
- prob_snapshot
- shadow_prob_snapshot
- calibration 摘要
它的作用是:
- 从现在开始,不再继续丢失历史训练特征
- 让未来 EMOS/LGBM 样本自然累积
---
## 6. 训练数据现在怎么来
### 6.1 EMOS 训练样本
EMOS 训练不只是需要真值,还要有“当时那一刻的预测快照”。
所以一条 EMOS 样本,本质上需要两部分:
1. 历史预测特征
2. 对应日期最终真值
当前导出的 EMOS 样本里,核心字段包括:
- `city`
- `date`
- `actual_high`
- `raw_mu`
- `raw_sigma`
- `deb_prediction`
- `ens_median`
- `ensemble_spread`
- `max_so_far_gap`
- `peak_flag`
- `sample_source`
- `settlement_source`
- `settlement_station_code`
- `truth_version`
- `truth_updated_by`
- `truth_updated_at`
也就是说,EMOS 训练样本现在已经带了真值 provenance。
### 6.2 LGBM 训练样本
LGBM 训练样本会优先从:
1. 永久真值表取监督目标
2. 长期训练特征表取历史特征
3. 再回退到必要的运行态/快照补充
当前 LGBM 样本会用到:
- 历史 `actual_high` lag
- 历史均值/趋势
- 多模型 forecast
- `deb_prediction`
- 当前观测
- 时间特征
---
## 7. Wunderground 历史回填为什么重要
这次治理里一个重点是:
- `Taipei`
- `Shenzhen`
这两个城市配置了 `Wunderground` 历史页面作为历史观测取数入口。
这里要注意产品文案口径:
- `Wunderground` 不是物理观测站
- 它只是历史页面 / 数据入口
- 机场类市场仍应以 METAR / 机场主站作为结算锚点
- 明确官方站点市场才以规则指定的官方站点作为最终结算锚点
之前的问题是:
- 城市注册表已经写成 `wunderground`
- 但历史回填链路还没有真正支持按指定历史日期抓 WU 历史页
所以过去它们的 `actual_high` 可能:
- 没有被正确回填
- 或者被错误来源污染
现在已经补了正式历史回填函数:
- [wunderground_sources.py](/E:/web/PolyWeather/src/data_collection/wunderground_sources.py)
它会:
1.`city + target_date` 拼出对应历史页
2. 解析该日观测序列
3. 取当日最高温
4. 按市场规则做整度结算
5. 写入永久真值表
6. 记录来源与审计信息
这一步对 `Taipei/Shenzhen` 尤其关键,因为它们的历史页面取数和普通 METAR bootstrap 不同。
---
## 8. 当前线上/离线运行模式
### 8.1 概率引擎模式
当前生产主概率应保持:
- `legacy`
如果需要观察 EMOS 对照,可切:
- `emos_shadow`
而不是:
- `emos_primary`
原因不是工程没接好,而是主概率发布必须由离线评估结果决定。VPS 轻量训练候选未通过门禁;本地训练候选虽通过门禁,但仍建议先 shadow 观察,再人工决定是否切主。
### 8.2 LGBM 角色
当前 `LGBM` 仍然只能算:
- 辅助预测源
- 研究/观测链路
- 校准概率层的一个可用引擎输入
不适合替代 `DEB` 主路径。
前端展示上,`LGBM 校准概率` 代表概率层已使用 LGBM 上下文生成桶分布;它不是把模型四舍五入票数直接当成概率。模型共识仍只是解释层,市场价格也只作为参考层。
---
## 9. 当前最新状态
以下状态来自最近一轮恢复、回填和重训产物。
### 9.1 永久真值
当前永久真值表已恢复到长期历史:
- `truth_records_store`
- 最早:`2023-01-01`
- 最晚:`2026-04-02`
- 行数:约 `35138`
- 城市数:`30`
运行态缓存仍然只有近 14 天:
- `daily_records_store`
- 仍然是近两周范围
这说明:
- 长期真值主存已经从运行态缓存里分离出来了
### 9.2 真值修订
当前已有 revision 审计记录:
- `truth_revisions_store`
- 行数:`2`
这说明审计链路已经在工作。
### 9.3 Wunderground 回填
`Taipei``Shenzhen` 已按 WU 历史页完成回填。
当前这两城已经补到:
- `2026-04-02`
### 9.4 长期训练特征
当前 `training_feature_records_store` 已经接通,但历史上真正留存下来的特征仍然很少。
这意味着:
- 从现在开始不会继续丢
- 但过去没留下的那部分特征,不会凭空恢复
这也是为什么:
- 真值恢复了
- `EMOS` 样本量却没有同步大幅增长
---
## 10. 当前 EMOS 结果怎么理解
最近两轮评估给出了更清晰的结论。
VPS 轻量训练候选:
- 版本:`emos-auto-20260418204203`
- `sample_count = 791`
- `delta_crps = +0.004652`
- `delta_mae = +0.102623`
- `delta_bucket_hit_rate = -0.137800`
- 结论:`hold`
本地训练候选:
- 版本:`emos-auto-20260418212046`
- `sample_count = 847`
- `delta_crps = -0.036170`
- `delta_mae = -0.007896`
- `delta_bucket_hit_rate = -0.009445`
- 结论:`promote`
这说明:
- EMOS 工程链路有效,本地用更多 snapshot 训练时可以超过 legacy 的 CRPS/MAE。
- 低配 VPS 不适合做主训练环境。
- 通过门禁不等于立即默认主用,仍应先 `emos_shadow` 观察。
当前生产策略仍然是:
- 用户主概率默认 `legacy`
- EMOS 通过本地训练产生候选
- 通过门禁后先以 `emos_shadow` 灰度
- 连续稳定后才考虑 `emos_primary`
这不是“EMOS 无效”,而是:
- 它还没有稳定到能切主路径
### 10.1 当前阻塞点
主要阻塞仍然是:
- 有效样本仍然不大,城市级样本分布不均
- 桶概率容易受结算边界影响
- 需要避免 VPS 训练消耗线上资源
- `emos_primary` 发布需要明确人工门禁
也就是说,当前 EMOS 状态可以总结成:
- 工程链路完整
- 数据治理大幅改善
- 本地训练可通过门禁
- 生产主用仍需 shadow 观察与人工发布
---
## 11. 当前 LGBM 结果怎么理解
最近一轮 LGBM 训练后,样本数已经从以前更少的状态提升到:
- `sample_count = 54`
- `train_count = 42`
- `validation_count = 12`
验证集指标大致为:
- `lgbm_mae = 1.349`
- `deb_mae = 0.875`
这说明:
- LGBM 比以前样本更充足了
- 但在验证集上仍然不如 DEB
所以当前它的定位仍然应该是:
- 辅助参考
- 不替代 DEB
---
## 12. 为什么现在 EMOS 没有像 LGBM 那样明显涨样本
这点很容易误解。
答案不是“恢复失败”,而是两条链路对数据要求不一样。
### 12.1 LGBM
LGBM 更依赖:
- 长期真值
- 基础 forecast 特征
这部分通过:
- `truth_records_store`
- `training_feature_records_store`
已经改善很多。
### 12.2 EMOS
EMOS 更依赖:
- 某一时刻的概率快照/分布特征
如果过去那些 snapshot 没有长期保存下来,那么即使今天把真值补齐了:
- 也无法凭空重建完整 EMOS 样本
所以当前现实是:
- 真值问题已经大幅改善
- 未来特征不会再继续丢
- 但过去缺失的 EMOS 快照历史仍然限制样本增长
---
## 13. 当前最重要的工程判断
### 13.1 已经完成的
这些现在可以认为已经完成:
- 真值主存从运行态缓存里拆出
- 真值 provenance 落库
- revision 审计表落地
- Wunderground 历史回填接通
- `Taipei/Shenzhen` 真值口径修正
- 长期训练特征表接通
- `/ops` 已能可视化 truth / feature / EMOS / LGBM 覆盖情况
### 13.2 还没完成的
这些仍然是后续重点:
- EMOS 样本继续自然积累
- shadow bucket brier 稳定下来
- LGBM 验证效果超过 DEB
- 让更多城市开始持续积累训练特征
---
## 14. 运维怎么看当前状态
现在最直接的入口是:
- `/ops`
这页已经能看到:
- 历史真值主表统计
- 真值来源分布
- 真值修订数量
- 长期训练特征统计
- `Taipei/Shenzhen` 的 WU 回填状态
- 城市覆盖缺口
- 模型城市覆盖
- 城市覆盖矩阵
因此,运维现在可以快速回答:
- 哪些城市真值已经长期化
- 哪些城市还没有特征积累
- 哪些城市已经能支撑 EMOS/LGBM
- 哪些城市目前仍然只能主要依赖 DEB
---
## 15. 推荐工作流
### 15.1 日常
1. 查看 `/ops`
2.`truth / feature / EMOS / LGBM` 覆盖有没有继续增长
3.`Taipei/Shenzhen` 的 WU 行数是否继续更新
4. 看本地 EMOS 候选是否通过门禁
5. 看 VPS 是否只加载已批准参数,不在低配机器上训练
### 15.2 周期性重训
建议在本地开发机执行,不建议在低配 VPS 上执行:
```powershell
scp root@38.54.27.70:/var/lib/polyweather/polyweather.db E:\web\PolyWeather\data\polyweather-prod.db
$env:POLYWEATHER_DB_PATH="E:\web\PolyWeather\data\polyweather-prod.db"
$env:POLYWEATHER_RUNTIME_DATA_DIR="E:\web\PolyWeather\artifacts\local_runtime"
python scripts\auto_retrain_probability_calibration.py --verbose --snapshot-limit 50000
```
只有 `auto_retrain_report.json``ready_for_promotion=true` 时,才允许把候选 `default.json` 传回 VPS。
### 15.3 真值恢复/补数
当有新的历史真值补数或回填需要时:
```bash
./venv/Scripts/python.exe scripts/restore_training_truth_history.py
./venv/Scripts/python.exe scripts/restore_training_feature_history.py
./venv/Scripts/python.exe scripts/backfill_recent_daily_actuals_from_metar.py --cities taipei shenzhen --lookback-days 14
```
说明:
- 脚本名里虽然还保留 `from_metar`
- 但当前实现已经会按 `settlement_source` 自动分发
- `wunderground` 会走 WU 历史回填分支
---
## 16. 当前最务实的结论
如果只用一句话概括当前状态:
**EMOS 和 LGBM 的工程基础已经补齐,但生产主概率仍必须由评估门禁控制;当前最正确的策略是继续以 `DEB/legacy` 为主路径,在本地训练 EMOS 候选,VPS 只加载已批准参数。**
更具体一点:
- `EMOS`
- 已接好
- 可训练
- 可评估
- 可 shadow
- 通过门禁后可灰度
- 不应在低配 VPS 上自动训练或自动主用
- `LGBM`
- 已接好
- 样本比以前更多
- 但验证集还不如 DEB
- 目前只能做辅助参考
- 数据层
- 这次治理的真正价值,是防止未来继续丢历史
- 这对两条模型链路都比继续“微调参数”更关键
---
## 17. 相关文档
若需要看更细分的历史说明,可继续参考:
- [EMOS_TRAINING_REPORT_ZH.md](/E:/web/PolyWeather/docs/EMOS_TRAINING_REPORT_ZH.md)
- [LGBM_DAILY_HIGH_ZH.md](/E:/web/PolyWeather/docs/LGBM_DAILY_HIGH_ZH.md)
- [PROBABILITY_SNAPSHOT_ARCHIVE_ZH.md](/E:/web/PolyWeather/docs/PROBABILITY_SNAPSHOT_ARCHIVE_ZH.md)
- [deep-research-report.md](/E:/web/PolyWeather/docs/deep-research-report.md)
-263
View File
@@ -1,263 +0,0 @@
# EMOS 训练与发布报告(2026-04-19
## 1. 当前结论
- `EMOS` 工程链路已经接通:可以训练、评估、生成候选参数,并在前端以校准概率层展示。
- 生产主概率当前不应默认使用 `emos_primary`。默认建议为 `legacy`;需要观察时使用 `emos_shadow`
- `emos_primary` 只允许在本地离线训练通过门禁、人工复核后手动灰度。
- 低配 VPS(例如 1 vCPU / 2GB RAM)不适合做 EMOS 全量训练;VPS 只负责采集、服务和加载已批准的参数文件。
- `LGBM` 当前仍不建议作为主路径,继续保持 `POLYWEATHER_LGBM_ENABLED=false`
## 2. 最近两次训练结果
### 2.1 VPS 轻量训练:不通过
VPS 使用最近 `5000` 条 snapshot 训练的候选:
- 版本:`emos-auto-20260418204203`
- 样本数:`791`
- 结论:`hold`
| 指标 | 变化 |
| :-- | --: |
| `delta_crps` | `+0.004652` |
| `delta_mae` | `+0.102623` |
| `delta_bucket_hit_rate` | `-0.137800` |
解读:CRPS、MAE、桶命中全部弱于 legacy,因此不能晋级。
### 2.2 本地训练:通过门禁,但仍需灰度
本地电脑使用生产 SQLite 副本与最近 `50000` 条 snapshot 训练的候选:
- 版本:`emos-auto-20260418212046`
- 样本数:`847`
- 结论:`promote`
| 指标 | 变化 |
| :-- | --: |
| `delta_crps` | `-0.036170` |
| `delta_mae` | `-0.007896` |
| `delta_bucket_hit_rate` | `-0.009445` |
解读:
- CRPS 与 MAE 有改善,候选通过当前门禁。
- 桶命中率轻微下降,虽然在门禁允许范围内,但仍建议先以 `emos_shadow` 观察,再决定是否切 `emos_primary`
## 3. 生产运行策略
推荐生产 `.env`
```env
POLYWEATHER_PROBABILITY_ENGINE=legacy
POLYWEATHER_PROBABILITY_CALIBRATION_FILE=/var/lib/polyweather/probability_calibration/default.json
```
观察 EMOS 时:
```env
POLYWEATHER_PROBABILITY_ENGINE=emos_shadow
POLYWEATHER_PROBABILITY_CALIBRATION_FILE=/var/lib/polyweather/probability_calibration/default.json
```
只有在候选连续通过评估、前端展示稳定、业务侧确认后,才切:
```env
POLYWEATHER_PROBABILITY_ENGINE=emos_primary
POLYWEATHER_PROBABILITY_CALIBRATION_FILE=/var/lib/polyweather/probability_calibration/default.json
```
验证线上加载状态:
```bash
docker compose exec -T polyweather_web python - <<'PY'
from src.analysis.probability_calibration import load_calibration, resolve_probability_engine_mode
cal = load_calibration()
print("engine_mode =", resolve_probability_engine_mode())
print("loaded_version =", cal.get("version"))
print("sample_count =", (cal.get("metrics") or {}).get("sample_count"))
print("has_global =", bool(cal.get("global")))
PY
```
## 4. 本地训练 SOP
### 4.1 拉取生产 SQLite 副本
推荐先在 VPS 上用 SQLite 在线备份生成快照:
```bash
sqlite3 /var/lib/polyweather/polyweather.db ".backup '/var/lib/polyweather/polyweather-train-copy.db'"
```
本地 PowerShell 拉取:
```powershell
cd E:\web\PolyWeather
scp root@38.54.27.70:/var/lib/polyweather/polyweather-train-copy.db E:\web\PolyWeather\data\polyweather-prod.db
```
如果生产库写入压力很低,也可以直接拉主库副本:
```powershell
scp root@38.54.27.70:/var/lib/polyweather/polyweather.db E:\web\PolyWeather\data\polyweather-prod.db
```
### 4.2 本地训练
```powershell
cd E:\web\PolyWeather
$env:POLYWEATHER_DB_PATH="E:\web\PolyWeather\data\polyweather-prod.db"
$env:POLYWEATHER_RUNTIME_DATA_DIR="E:\web\PolyWeather\artifacts\local_runtime"
python scripts\auto_retrain_probability_calibration.py --verbose --snapshot-limit 50000
```
如果本地机器仍然较慢,可先降到:
```powershell
python scripts\auto_retrain_probability_calibration.py --verbose --snapshot-limit 20000
```
训练报告:
```powershell
Get-Content E:\web\PolyWeather\artifacts\local_runtime\probability_calibration\auto_retrain_report.json
```
候选目录:
```text
E:\web\PolyWeather\artifacts\local_runtime\probability_calibration\candidates\<version>\
```
### 4.3 晋级判断
只有报告满足以下条件时,候选才可进入部署流程:
```json
"ready_for_promotion": true
```
同时人工检查:
- `delta_crps <= 0`
- `delta_mae <= 0.05`
- `delta_bucket_hit_rate >= -0.05`
- 城市级结果没有出现关键城市大幅退化
- 前端概率分布没有明显过度摊平或异常偏桶
## 5. 部署通过的候选
把本地候选上传到 VPS
```powershell
scp E:\web\PolyWeather\artifacts\local_runtime\probability_calibration\candidates\<version>\default.json root@38.54.27.70:/var/lib/polyweather/probability_calibration/default.json
```
VPS 上优先设置为 `emos_shadow`
```env
POLYWEATHER_PROBABILITY_ENGINE=emos_shadow
POLYWEATHER_PROBABILITY_CALIBRATION_FILE=/var/lib/polyweather/probability_calibration/default.json
```
重启:
```bash
cd /root/PolyWeather
docker compose up -d polyweather_web
```
观察稳定后再考虑 `emos_primary`
## 6. VPS 定时训练策略
当前策略:**不在 VPS 上做 EMOS 定时训练**。
原因:
- 生产 SQLite 的 `probability_training_snapshots_store` 会持续增长。
- 低配 VPS 全量扫描会造成 CPU/IO 飙升,严重时影响 SSH 和线上服务。
- VPS 训练用较小 `--snapshot-limit` 虽然安全,但训练效果可能弱于本地。
如果曾经加过 cron,应删除:
```bash
crontab -l | grep -v 'auto_retrain_probability_calibration.py' | crontab -
```
确认:
```bash
crontab -l
```
## 7. 自动重训脚本说明
脚本:
```text
python scripts\auto_retrain_probability_calibration.py
```
默认行为:
- 生成新的 EMOS candidate。
- 对 candidate 跑离线评估。
- 写入候选目录和门禁报告。
- 不覆盖线上 `default.json`
重要参数:
- `--verbose`:输出训练/评估进度。
- `--snapshot-limit N`:只使用最近 N 条 snapshot。
- `--promote-if-passed`:门禁通过后覆盖目标参数文件。
- `--run-tests`:晋级前跑测试。
当前不建议在 VPS 使用 `--promote-if-passed`。本地训练通过后,仍优先人工上传并使用 `emos_shadow`
## 8. 门禁阈值
默认阈值:
- `POLYWEATHER_EMOS_AUTO_MIN_SAMPLES=50`
- `POLYWEATHER_EMOS_AUTO_MAX_DELTA_CRPS=0`
- `POLYWEATHER_EMOS_AUTO_MAX_DELTA_MAE=0.05`
- `POLYWEATHER_EMOS_AUTO_MIN_DELTA_BUCKET_HIT_RATE=-0.05`
解释:
- `CRPS` 不允许比 legacy 更差。
- `MAE` 最多允许轻微退化 `0.05`
- `bucket_hit_rate` 是业务参考指标,但对结算边界敏感,不单独作为唯一判断。
## 9. 前端说明
今日日内分析中的概率区展示的是当前生产概率引擎输出:
- `legacy`:展示现有动态概率。
- `emos_shadow`:用户主概率仍为 legacy,EMOS 仅用于对照和评估。
- `emos_primary`:用户主概率使用 EMOS 校准分布。
对外文案应避免暗示“EMOS 一定更准”。推荐解释为:
> EMOS 是 PolyWeather 基于 DEB 路径、多模型集合、METAR 实测进度和历史误差结构生成的统计校准概率,不是外部天气模型,也不是直接 API 结果。
## 10. 已验证
本地训练链路已验证:
```text
python scripts\auto_retrain_probability_calibration.py --verbose --snapshot-limit 50000
```
测试链路已验证:
```text
python -m pytest tests\test_auto_retrain_probability_calibration.py tests\test_probability_calibration.py tests\test_probability_rollout.py
```
当前工程结论:
**EMOS 可以继续本地训练与 shadow 观察,但生产主概率不应因为“机制接好”而默认切到 `emos_primary`。**
-325
View File
@@ -1,325 +0,0 @@
# LightGBM 日最高温模型(中文)
最后更新:`2026-04-18`
## 1. 目标
这套 `LightGBM` 模型是给 PolyWeather 增加一个轻量级的统计学习预测源。
它的定位不是替代:
- `DEB`
- `EMOS`
- `ECMWF / GFS / GEM / JMA / ICON / Open-Meteo / MGM / NWS`
而是作为一个新的点预测源:
`现有模型 + 观测特征 -> LGBM -> 并入 current_forecasts -> DEB -> EMOS`
第一版只做:
- `D0` 当日最高温预测
不做:
- `D1-D3`
- 小时级曲线
- 原始独立概率分布
- 独立结算源
注意:前端出现的“LGBM 校准概率”不是把 LGBM 模型票数直接当成概率,而是概率层基于 LGBM / DEB / 观测上下文输出的校准分布。模型共识只保留为解释性参考。
## 2. 适用场景
这条链路是为低资源 VPS 准备的。
当前项目线上环境只有 `2GB RAM` 时,不适合引入 `TimesFM` 这类大模型,但适合用 `LightGBM` 做轻量推理。
当前方案是:
1. 训练离线完成
2. 训练产物直接提交到仓库
3. VPS 线上只加载模型文件并推理
4. VPS 不训练,不起额外服务
## 3. 文件结构
核心文件如下:
- 运行时推理:
- [src/models/lgbm_daily_high.py](/E:/web/PolyWeather/src/models/lgbm_daily_high.py)
- 特征构建:
- [src/models/lgbm_features.py](/E:/web/PolyWeather/src/models/lgbm_features.py)
- 训练脚本:
- [scripts/train_lgbm_daily_high.py](/E:/web/PolyWeather/scripts/train_lgbm_daily_high.py)
- 训练报告脚本:
- [scripts/report_lgbm_daily_high.py](/E:/web/PolyWeather/scripts/report_lgbm_daily_high.py)
- 模型文件:
- [artifacts/models/lgbm_daily_high.txt](/E:/web/PolyWeather/artifacts/models/lgbm_daily_high.txt)
- 模型 schema / 指标:
- [artifacts/models/lgbm_daily_high_schema.json](/E:/web/PolyWeather/artifacts/models/lgbm_daily_high_schema.json)
接入链路位置:
- Web API 聚合:
- [web/analysis_service.py](/E:/web/PolyWeather/web/analysis_service.py)
- 共享趋势引擎:
- [src/analysis/trend_engine.py](/E:/web/PolyWeather/src/analysis/trend_engine.py)
## 4. 特征说明
第一版特征固定为以下几组。
### 4.1 历史日高温特征
- `actual_high_lag_1`
- `actual_high_lag_2`
- `actual_high_lag_3`
- `actual_high_lag_7`
- `actual_high_mean_7`
- `actual_high_mean_14`
- `actual_high_trend_3`
### 4.2 当天模型特征
- `Open-Meteo`
- `ECMWF`
- `GFS`
- `GEM`
- `JMA`
- `ICON`
- `MGM`
- `NWS`
- `deb_prediction`
- `model_median`
- `model_spread`
### 4.3 当前观测特征
- `current_temp`
- `max_so_far`
- `humidity`
- `wind_speed_kt`
- `visibility_mi`
### 4.4 时间与状态特征
- `local_hour`
- `month`
- `weekday`
- `peak_status_code`
其中:
- `before = 0`
- `in_window = 1`
- `past = 2`
## 5. 训练数据来源
训练数据主要来自两份运行时历史文件:
- [data/daily_records.json](/E:/web/PolyWeather/data/daily_records.json)
- [data/probability_training_snapshots.jsonl](/E:/web/PolyWeather/data/probability_training_snapshots.jsonl)
作用分工:
- `daily_records.json`
- 提供 `actual_high`
- 提供当天各模型 forecast
- 提供历史 `deb_prediction`
- `probability_training_snapshots.jsonl`
- 提供 `max_so_far`
- 提供 `peak_status`
- 提供观测特征快照
为后续重训,概率快照归档现在还会额外写入:
- `current_temp`
- `humidity`
- `wind_speed_kt`
- `visibility_mi`
- `local_hour`
对应代码:
- [src/analysis/probability_snapshot_archive.py](/E:/web/PolyWeather/src/analysis/probability_snapshot_archive.py)
## 6. 训练流程
训练脚本:
```bash
./venv/Scripts/python.exe scripts/train_lgbm_daily_high.py
```
训练流程如下:
1. 从历史文件构造监督样本
2. 目标值固定为 `actual_high`
3. 按日期做简单的时间顺序切分
4. 最后约 20% 做验证集
5. 先训练并评估验证集
6. 再用全量样本训练最终模型
7. 输出模型文件和 schema 文件
输出产物:
- [artifacts/models/lgbm_daily_high.txt](/E:/web/PolyWeather/artifacts/models/lgbm_daily_high.txt)
- [artifacts/models/lgbm_daily_high_schema.json](/E:/web/PolyWeather/artifacts/models/lgbm_daily_high_schema.json)
## 7. 如何看训练结果
查看训练报告:
```bash
./venv/Scripts/python.exe scripts/report_lgbm_daily_high.py
```
这个脚本会读取 schema,并打印:
- `Sample Count`
- `Train Count`
- `Valid Count`
- `LGBM MAE`
- `DEB MAE`
- `Best Single MAE`
- `Median MAE`
- `Winner`
当前这版训练结果是:
- `sample_count = 29`
- `validation_count = 12`
- `validation.lgbm_mae = 2.975`
- `validation.deb_mae = 2.267`
- `validation.best_single_mae = 1.167`
这说明:
- 当前 `LGBM` 链路已经可用
- 但现阶段验证集表现还没有超过 `DEB`
- 所以默认配置仍建议保持关闭
## 8. 线上运行逻辑
运行时推理逻辑不是“直接替代 DEB”,而是:
1. 先收集现有模型 forecast
2. 先算一版基线 `DEB`
3. 把这版 `DEB` 当作 `LGBM` 的一个输入特征
4. 输出 `LGBM` 点预测
5.`LGBM` 注入 `current_forecasts`
6. 重新计算最终 `DEB`
这样做的原因是:
- `LGBM` 需要吃到 `deb_prediction` 特征
- 但最终 `DEB` 又要把 `LGBM` 当成一个新的输入模型
## 8.1 前端概率展示口径
当前网页的概率区按以下顺序解释:
1. 如果后端 `probabilities.engine` 表示 LGBM 校准概率可用,则标题显示为 `LGBM 校准概率`
2. 如果 LGBM 不可用,但 EMOS / legacy 概率可用,则显示为 `校准模型概率`
3. 模型舍入票数只保留为“模型共识参考”,用于说明哪些模型四舍五入后落在同一温度档,不作为最终命中概率。
4. 市场价格只保留为“市场参考”,不和校准概率混成同一结论。
这能避免用户把 `4/8 模型支持 82°F` 误读成 `82°F 有 50% 概率`。模型共识是解释层,概率引擎才是结论层。
## 9. 环境变量
示例配置见:
- [.env.example](/E:/web/PolyWeather/.env.example)
相关变量:
```env
POLYWEATHER_LGBM_ENABLED=false
POLYWEATHER_LGBM_MODEL_PATH=/app/artifacts/models/lgbm_daily_high.txt
POLYWEATHER_LGBM_SCHEMA_PATH=/app/artifacts/models/lgbm_daily_high_schema.json
POLYWEATHER_LGBM_MIN_HISTORY_POINTS=3
```
说明:
- `POLYWEATHER_LGBM_ENABLED`
- 是否启用运行时推理
- `POLYWEATHER_LGBM_MODEL_PATH`
- 模型文件路径
- `POLYWEATHER_LGBM_SCHEMA_PATH`
- schema 文件路径
- `POLYWEATHER_LGBM_MIN_HISTORY_POINTS`
- 某城市最低历史样本门槛
默认是 `3`,原因不是最理想,而是当前整体样本仍然偏少。
如果门槛设太高,很多城市现在根本不会触发 `LGBM`
## 10. VPS 部署建议
如果你的 VPS 只有 `2GB RAM`
- 可以跑这套 `LightGBM`
- 不要在 VPS 上训练
- 不要起额外模型服务
推荐方式:
1. 在本地或开发环境训练
2. 提交模型产物
3. VPS 拉代码
4. 开启 `POLYWEATHER_LGBM_ENABLED=true`
5. 重启主服务
不推荐:
- 在 VPS 上跑训练脚本
-`LightGBM` 当成长任务服务单独部署
- 同时引入大模型推理
## 11. 当前结论
这条链路已经完成了:
- 离线训练
- 模型产物固化
- 运行时懒加载
- Web / 共享分析链路注入
- 前端模型类型兼容
但当前样本量仍偏少,所以建议运营策略是:
1. 先继续积累历史 `actual_high`
2. 继续积累概率快照观测字段
3. 定期重训
4. 只有当验证集 `MAE` 持续接近或优于 `DEB` 时,再考虑默认线上开启
## 12. 常用命令
### 训练
```bash
./venv/Scripts/python.exe scripts/train_lgbm_daily_high.py
```
### 查看训练报告
```bash
./venv/Scripts/python.exe scripts/report_lgbm_daily_high.py
```
### 本地测试
```bash
./venv/Scripts/python.exe -m pytest tests/test_lgbm_features.py tests/test_lgbm_daily_high.py
```
### 编译检查
```bash
./venv/Scripts/python.exe -m compileall src web scripts tests
```
+1 -4
View File
@@ -99,7 +99,7 @@ Web API 会把这部分元数据挂到:
- RDPS
- HRDPS
亚洲城市更依赖本地观测增强层,例如 JMA、KMA、NMC、HKO、CWA、METAR、TAF。
亚洲城市更依赖本地观测增强层,例如 JMA、AMOS(首尔/釜山)、NMC、HKO、CWA、METAR、TAF。
## 4. DEB 家族去重
@@ -151,7 +151,6 @@ HRDPS > RDPS > GDPS > GEM
- MGM
- NWS
- HKO
- LGBM
- Open-Meteo
ECMWF IFS 与 ECMWF AIFS 分开保留,因为前者是传统 NWP,后者是 AIFS 模型。
@@ -219,7 +218,6 @@ raw current_forecasts
当前前端把三层拆开展示:
- `模型区间与分歧`:解释不同模型当前给出的最高温范围和分歧,不直接等于命中概率。
- `校准模型概率`:由当前生产概率引擎输出温度桶概率;默认可保持 legacy,EMOS / LGBM 只在评估通过、显式启用或 shadow 对照时进入展示。
- `市场参考`:只展示市场价格和错价背景,不再作为主判断,也不默认输出 BUY YES / BUY NO。
模型票数只用于解释“哪些模型支持某个档位”,不等于最终概率。最终概率应优先读取 `probabilities.engine` 对应的校准分布。
@@ -230,7 +228,6 @@ raw current_forecasts
- `tests/test_multi_model_sources.py`
- `tests/test_deb_model_family.py`
- `tests/test_lgbm_features.py`
重点覆盖:
-3
View File
@@ -25,7 +25,6 @@ POLYWEATHER_OPS_ADMIN_EMAILS=yhrsc30@gmail.com
- 系统健康
- SQLite / rollout / metrics 摘要
- 支付运行态
- prewarm worker 运行态
- 缓存桶状态与 summary cache hit/miss
- 当前会员
- 周榜
@@ -101,11 +100,9 @@ python scripts/reconcile_subscription_by_email.py --email <user_email>
## 7. 备注
### 7.1 当前 prewarm / 缓存观测项
`/ops` 里的系统状态卡目前已额外展示:
- `prewarm` 是否启用
- `thread_alive` / `heartbeat_age_sec`
- 最近一轮:
- `cycle_count`
-347
View File
@@ -1,347 +0,0 @@
# 概率训练样本归档说明(中文)
最后更新:`2026-04-19`
## 1. 目的
这份文档说明两件事:
1. 为什么 `EMOS` 训练不能只依赖历史实测天气
2. 未来如何持续沉淀“历史预测记录”,让概率引擎越训越稳
一句话结论:
- 历史实测天气只能补 `actual_high`
- 真正决定 `EMOS` 训练质量的是“当时那一刻的预测快照”
## 2. 什么是“历史预测记录”
对 PolyWeather 来说,一条可训练的历史预测记录,至少应该包含这些字段:
- `city`
- `timestamp`
- `date`
- `raw_mu`
- `raw_sigma`
- `deb_prediction`
- `ensemble p10 / p50 / p90`
- `multi-model forecasts`
- `max_so_far`
- `peak_status`
- `prob_snapshot`
- `probability_engine`
- `calibration_mode`
- `calibration_version`
- `raw_mu / raw_sigma`
- `calibrated_mu / calibrated_sigma`
- `shadow_distribution`
- 当天最终 `actual_high`
- 当天最终 `settlement bucket`
这类记录的核心价值是:
- 还原“当时系统实际看到什么”
- 再对照“后来真实发生了什么”
只有这两者成对,`EMOS` 才能学习偏差。
## 3. 为什么不能只用历史天气实测
历史天气 CSV 只能告诉你:
- 当天最高温是多少
- 某小时温度是多少
但它不能告诉你:
- 当天早上 09:00 时,系统的 `mu` 是多少
- 当时的 `ensemble spread` 是多少
- 当时 `DEB` 怎么看
- 当时的 top bucket 是什么
所以:
- 历史实测天气是标签
- 历史预测记录才是训练输入
缺少后者,EMOS 只能学到很有限的东西。
## 4. 当前项目里已经有的基础
### 4.1 已有历史日记录
文件:
- [daily_records.json](/E:/web/PolyWeather/data/daily_records.json)
当前已经保存了一部分训练相关字段,例如:
- `forecasts`
- `actual_high`
- `deb_prediction`
- `mu`
- `prob_snapshot`
- `shadow_prob_snapshot`
- `probability_calibration`
- `probability_features`
这已经是“历史预测记录”的雏形。
### 4.2 已有历史天气 CSV
目录:
- [data/historical](/E:/web/PolyWeather/data/historical)
它们可以帮助补:
- `actual_high`
- `settlement history`
但不能替代预测快照归档。
## 5. 未来应该怎么存历史预测记录
推荐做法是:
### 5.1 固定时点归档
每天为每个重点城市固定存几次快照,例如:
- 当地 `09:00`
- 当地 `12:00`
- 当地 `15:00`
这样能确保每个交易日都有稳定可比样本。
### 5.2 关键变化时补充归档
除了固定时点,还应该在以下情况额外存一次:
- `max_so_far` 创新高
- `mu` 变化超过阈值
- `top bucket` 发生变化
- `shadow top bucket` 发生变化
这样能捕捉真正有训练价值的转折点。
### 5.3 建议的存储格式
建议新增一个文件,例如:
- `data/probability_training_snapshots.jsonl`
每一行保存一条 JSON 记录。
优点:
- 追加写入简单
- 后续导出训练集方便
- 不容易因为单个大 JSON 文件损坏而全盘受影响
## 6. 一条建议的快照结构
示例:
```json
{
"city": "ankara",
"timestamp": "2026-03-20T12:00:00+03:00",
"date": "2026-03-20",
"raw_mu": 15.2,
"raw_sigma": 1.2,
"deb_prediction": 15.4,
"ensemble": {
"p10": 14.8,
"median": 15.8,
"p90": 17.9
},
"multi_model": {
"ECMWF": 15.8,
"GFS": 14.1,
"ICON": 15.9,
"GEM": 16.5,
"JMA": 14.5
},
"max_so_far": 15.0,
"peak_status": "before",
"prob_snapshot": [
{"v": 15, "p": 0.552},
{"v": 16, "p": 0.377}
],
"shadow_prob_snapshot": [
{"v": 15, "p": 0.324},
{"v": 16, "p": 0.238}
],
"probability_engine": "legacy",
"probability_mode": "emos_shadow",
"calibration_mode": "emos_shadow",
"calibration_version": "emos-20260320130245",
"calibrated_mu": 15.4,
"calibrated_sigma": 1.1
}
```
当天结束后,再由后处理脚本回填:
- `actual_high`
- `settlement_bucket`
当前前端把这类快照解释为“校准模型概率”。如果 `probability_engine` 为 LGBM 相关值,则显示为 LGBM 校准概率;模型舍入票数和市场价格只用于解释,不直接作为最终概率。
## 7. 现阶段你可以执行的命令
### 7.1 回填历史天气 CSV
```bash
python scripts/backfill_historical_weather.py
```
作用:
- 补全 30 城市历史天气时序 CSV
### 7.2 从历史 CSV 构建日级结算标签
```bash
python scripts/build_settlement_history_from_csv.py
```
作用:
- 生成 [settlement_history.json](/E:/web/PolyWeather/artifacts/probability_calibration/settlement_history.json)
### 7.3 导出当前训练样本
```bash
python scripts/export_probability_training_dataset.py
```
作用:
- 生成 [training_samples.json](/E:/web/PolyWeather/artifacts/probability_calibration/training_samples.json)
### 7.4 重训 EMOS
推荐在本地电脑使用生产 SQLite 副本训练,不建议在低配 VPS 上训练:
```powershell
scp root@38.54.27.70:/var/lib/polyweather/polyweather.db E:\web\PolyWeather\data\polyweather-prod.db
$env:POLYWEATHER_DB_PATH="E:\web\PolyWeather\data\polyweather-prod.db"
$env:POLYWEATHER_RUNTIME_DATA_DIR="E:\web\PolyWeather\artifacts\local_runtime"
python scripts\auto_retrain_probability_calibration.py --verbose --snapshot-limit 50000
```
作用:
- 生成新的候选 `default.json`
- 同时生成 `evaluation_report.json``auto_retrain_report.json`
- 不自动覆盖线上参数
### 7.5 离线评估训练效果
```bash
python scripts/evaluate_probability_calibration.py
```
作用:
- 生成 [evaluation_report.json](/E:/web/PolyWeather/artifacts/probability_calibration/evaluation_report.json)
### 7.6 回填 shadow 结果到历史记录
```bash
python scripts/backfill_probability_shadow_history.py
```
作用:
-`shadow_prob_snapshot``probability_calibration` 回填到 [daily_records.json](/E:/web/PolyWeather/data/daily_records.json)
### 7.7 生成线上 shadow 滚动报表
```bash
python scripts/build_probability_shadow_report.py
```
作用:
- 生成 [shadow_report.json](/E:/web/PolyWeather/artifacts/probability_calibration/shadow_report.json)
## 8. 推荐的一整套重训流程
如果过了十天、半个月,想重新训练一次,当前推荐流程是:
```powershell
scp root@38.54.27.70:/var/lib/polyweather/polyweather.db E:\web\PolyWeather\data\polyweather-prod.db
$env:POLYWEATHER_DB_PATH="E:\web\PolyWeather\data\polyweather-prod.db"
$env:POLYWEATHER_RUNTIME_DATA_DIR="E:\web\PolyWeather\artifacts\local_runtime"
python scripts\auto_retrain_probability_calibration.py --verbose --snapshot-limit 50000
```
只有 `auto_retrain_report.json``ready_for_promotion=true`,才把候选参数传回 VPS,并优先用 `emos_shadow` 观察。
如果只是做历史真值补数,才需要额外执行:
```bash
python scripts/backfill_historical_weather.py
python scripts/build_settlement_history_from_csv.py
```
## 9. 怎么判断这次训练有没有进步
重训后,不要只看一个指标。
至少看这 4 个:
1. `CRPS`
- 越低越好
2. `MAE`
- 越低越好
- 至少不要明显变差
3. `Bucket Hit Rate`
- 越高越好
- 这是业务上非常关键的指标
4. `Bucket Brier`
- 越低越好
- 反映概率分布质量
当前自动门禁至少要求:
- `CRPS` 下降
- `MAE` 最多轻微退化 `0.05`
- `Bucket Hit Rate` 退化不超过 `0.05`
人工复核还应看城市级结果,避免少数关键城市大幅退化。`Bucket Hit Rate` 受整数结算边界影响大,不能单独作为唯一判断。
## 10. 当前最重要的现实判断
过去的“完整历史预测记录”通常没法完全补出来,除非:
1. 你之前就存过
2. 你接入了支持 forecast archive 的商业数据源
所以现实里最重要的不是“把过去全补齐”,而是:
- 从现在开始系统化归档
- 每天稳定沉淀可训练样本
- 定期离线重训
## 11. 推荐的下一步
最值得做的改造是:
1. 新增 `probability_training_snapshots.jsonl`
2. 每次分析时自动追加一条快照
3. 当天结束后自动回填 `actual_high`
4. 每 1-2 周在本地电脑重新训练一次
5. VPS 只加载通过评估的参数文件,不做全量训练
## 12. 总结
如果只记住一句话,就记这个:
**EMOS 要想越训越好,关键不是多下载一点历史天气,而是持续保存“当时系统看到的预测快照”。**
+56
View File
@@ -0,0 +1,56 @@
# 外部服务依赖总览
最后更新:`2026-05-19`
项目调用了 24 个外部服务,按状态分为三类。
## 核心(必须有,挂了服务不可用)
| 服务 | 用途 | 状态 |
| ---------------------- | --------------------- | ---- |
| Open-Meteo | 52 城天气预报 | ✅ |
| AviationWeather (NOAA) | METAR/TAF 航空观测 | ✅ |
| MADIS (NOAA) | 美国 5 分钟高频观测 | ✅ |
| Supabase | 用户认证 + 订阅 | ✅ |
| Telegram Bot API | Bot 消息 + 群成员检查 | ✅ |
| KNMI | Amsterdam 10 分钟观测 | ✅ |
| Polymarket Gamma/CLOB | 市场数据读取 | ✅ |
## 国家气象源(特定城市必须)
| 服务 | 城市 | 状态 |
| -------------------- | --------------------- | ----------- |
| JMA (日本) | Tokyo | ✅ |
| KMA + AMOS (韩国) | Seoul, Busan | ✅ |
| AMSC AWOS (中国) | 北京/上海/广州等 6 城 | ✅ |
| MGM (土耳其) | Ankara, Istanbul | ✅ |
| FMI (芬兰) | Helsinki | ✅ |
| HKO (香港) | Hong Kong | ✅ |
| CWA (台湾) | Taipei | ✅ |
| NMC (中国) | 国内城市 fallback | ✅ |
| Singapore MSS | Singapore | ✅ |
| IMGW (波兰) | Warsaw | ⚠️ 未配 key |
| Russia pogodaiklimat | Moscow | ✅ |
## 可选 / 已禁用
| 服务 | 用途 | 状态 |
| -------------- | ------------- | ----------- |
| OpenWeatherMap | 天气 fallback | ⚠️ 未配 key |
| VisualCrossing | 历史天气 | ⚠️ 未配 key |
| Meteoblue | 天气预报 | ⚠️ 未配 key |
| SynopticData | 美国站点观测 | ⚠️ 未配 key |
## AI / 其他
| 服务 | 用途 | 状态 |
| ----------------- | ---------------- | ----------- |
| MiMo (xiaomimimo) | 城市分析 AI 评论 | ✅ 当前使用 |
| DeepSeek | AI fallback | - 备用 |
| Groq | AI commentary | ⚠️ 未配 key |
| Polygon RPC | 链上支付 | ✅ |
| WalletConnect | 前端钱包连接 | ⚠️ 未配 key |
## 合计
18 个在用,6 个可选/未配置。
+1 -1
View File
@@ -1,4 +1,4 @@
# Supabase + 登录 + 支付接入说明(v1.5.1
# Supabase + 登录 + 支付接入说明(v1.7.0
最后更新:`2026-03-14`
-81
View File
@@ -1,81 +0,0 @@
# 技术债与工程待办(v1.5.4
最后更新:`2026-04-19`
目标:在收费上线后,优先保证状态一致性、支付可靠性、可观测性和概率引擎发布可控。
## 1. 债务快照
当前估计:**95% 稳定 / 5% 技术债**。
```mermaid
flowchart TD
A["技术债"]
subgraph P["支付与订阅"]
P1["合约 V2 升级(SafeERC20 / Pausable"]
P2["退款与工单流程"]
P3["多 RPC 与链上对账面板"]
end
subgraph E["权限与运营"]
E1["前后端/Bot 权限矩阵回归"]
E2["积分来源明细与补分审计"]
end
subgraph O["可观测性"]
O1["外部监控抓取与告警阈值"]
O2["业务监控看板"]
end
subgraph S["状态与概率"]
S1["EMOS 本地训练与 shadow 发布门禁"]
end
A --> P
A --> E
A --> O
A --> S
```
## 2. 近期已关闭
- 支付主链路已上线(intent -> submit -> confirm)。
- 支付自动补单已上线(Event Loop + Confirm Loop)。
- 支付事件重放脚本已补齐。
- 支付运行态 API 与 SQLite 审计事件已补齐。
- 钱包绑定支持浏览器钱包 + WalletConnect。
- 账户中心与 Pro 权限展示链路打通。
- 钱包异动支持独立频道路由。
- 运行态状态/缓存与核心离线训练、评估、回填链路已完成 SQLite 主路径收口。
- 轻量可观测性已上线(`/healthz``/api/system/status``/metrics`)。
- EMOS/CRPS 校准链路已接通;生产主概率保持 `legacy``emos_shadow``emos_primary` 只允许本地训练通过门禁后人工灰度。
## 3. 高优先级技术债
| 项目 | 影响 | 建议动作 |
| :-- | :-- | :-- |
| EMOS 发布门禁 | 低配 VPS 不适合训练,主概率不能绕过评估 | 本地拉生产 SQLite 训练,`ready_for_promotion=true` 后先 `emos_shadow` |
| 外部监控与告警 | 只有轻量指标,无外部抓取 | 接 Prometheus/Grafana 或最小巡检 |
| 退款与售后链路 | 商业闭环不完整 | 增加退款状态机与工单系统 |
## 4. 中优先级技术债
| 项目 | 影响 | 建议动作 |
| :-- | :-- | :-- |
| 积分发放可解释性 | 用户理解成本高 | 输出积分来源明细(发言/奖励/手动补分) |
| 支付合约 V2 升级 | 当前仍是最小可用合约 | 升级到 SafeERC20 + Pausable + plan 绑定 |
| 支付失败文案标准化 | 转化率受影响 | 建立错误码 -> 文案映射表 |
## 5. 低优先级技术债
| 项目 | 影响 | 建议动作 |
| :-- | :-- | :-- |
| 前端离线缓存能力 | 非核心 | 评估 Service Worker + IndexedDB |
| 冷启动波动 | 首屏抖动 | 热点城市预热 |
## 6. 下阶段里程碑
1. 固化 EMOS 本地训练流程,禁止低配 VPS 自动训练和自动主用。
2. 补外部监控抓取与告警阈值。
3. 评估并推进支付合约 V2 升级。
+5 -8
View File
@@ -1,12 +1,12 @@
# 技术债与工程待办(v1.5.4
# 技术债与工程待办(v1.7.0
最后更新:`2026-04-19`
最后更新:`2026-05-10`
目标:在收费上线后,优先保证状态一致性、支付可靠性、可观测性和概率引擎发布可控。
## 1. 债务快照
当前估计:**95% 稳定 / 5% 技术债**。
当前估计:**97% 稳定 / 3% 技术债**。前端设计系统债务已在 v1.6.0 全面消除。
```mermaid
flowchart TD
@@ -29,7 +29,6 @@ flowchart TD
end
subgraph S["状态与概率"]
S1["EMOS 本地训练与 shadow 发布门禁"]
end
A --> P
@@ -40,6 +39,7 @@ flowchart TD
## 2. 近期已关闭
- **前端设计系统工程债务(2026-05-10**:消除 !important 滥用(134→49)、统一断点体系(18→10)、数百处硬编码颜色迁移至 CSS 变量、修复 accent-green 蓝色 Bug、添加 ARIA 无障碍属性、去重 @keyframes、移除死代码(1,697 行)。详见 `docs/frontend-ui-design-review.md`
- 支付主链路已上线(intent -> submit -> confirm)。
- 支付自动补单已上线(Event Loop + Confirm Loop)。
- 支付事件重放脚本已补齐。
@@ -49,13 +49,11 @@ flowchart TD
- 钱包异动支持独立频道路由。
- 运行态状态/缓存与核心离线训练、评估、回填链路已完成 SQLite 主路径收口。
- 轻量可观测性已上线(`/healthz``/api/system/status``/metrics`)。
- EMOS/CRPS 校准链路已接通;生产主概率保持 `legacy``emos_shadow``emos_primary` 只允许本地训练通过门禁后人工灰度。
## 3. 高优先级技术债
| 项目 | 影响 | 建议动作 |
| :-- | :-- | :-- |
| EMOS 发布门禁 | 低配 VPS 不适合训练,主概率不能绕过评估 | 本地拉生产 SQLite 训练,`ready_for_promotion=true` 后先 `emos_shadow` |
| 外部监控与告警 | 只有轻量指标,无外部抓取 | 接 Prometheus/Grafana 或最小巡检 |
| 退款与售后链路 | 商业闭环不完整 | 增加退款状态机与工单系统 |
@@ -63,7 +61,7 @@ flowchart TD
| 项目 | 影响 | 建议动作 |
| :-- | :-- | :-- |
| 积分发放可解释性 | 用户理解成本高 | 输出积分来源明细(发言/奖/手动补分) |
| 积分发放可解释性 | 用户理解成本高 | 输出积分来源明细(发言/首次消息奖励/欢迎奖励/周排名奖励/周参与奖/手动补分) |
| 支付合约 V2 升级 | 当前仍是最小可用合约 | 升级到 SafeERC20 + Pausable + plan 绑定 |
| 支付失败文案标准化 | 转化率受影响 | 建立错误码 -> 文案映射表 |
@@ -76,6 +74,5 @@ flowchart TD
## 6. 下阶段里程碑
1. 固化 EMOS 本地训练流程,禁止低配 VPS 自动训练和自动主用。
2. 补外部监控抓取与告警阈值。
3. 评估并推进支付合约 V2 升级。
+102
View File
@@ -0,0 +1,102 @@
# PolyWeather 数据链路架构审查
> 审查日期:2026-06 | 视角:系统架构师 | 范围:完整数据采集→分析→API→前端状态
>
> **修复状态:8/8 已完成**
## 一、数据架构总览
```
外部数据源 Python 后端 Next.js 前端
=========== ========== ===========
Open-Meteo (预报+多模型) ─┐
METAR/TAF (航空气象) ─┤
NWS (美国) / MGM (土耳其) ─┤
JMA/AMOS/NMC/HKO/CWA ─┤
Wunderground / NOAA ─┤
Polymarket Gamma/CLOB ─┤
├─ WeatherDataCollector ├─ dashboard-client.ts
│ (内存缓存 + SQLite磁盘缓存) │ (ETag浏览器缓存 + SWR)
│ │
├─ _analyze() ├─ useDashboardStore
│ ├─ DEB 融合 (11模型加权) │ (双Context拆分)
│ └─ 趋势引擎 │ (扫描数据预加载)
│ │
├─ scan_terminal_service.py ├─ 扫描终端查询
│ ├─ ThreadPoolExecutor(4) │ (120s TTL)
│ └─ AI 增强层 (DeepSeek) │
│ │
└─ FastAPI routes └─ API代理 (Next.js rewrites)
(36个端点 + ETag 304)
```
## 二、数据采集层
### 源端(14个外部源)
| 源 | 类型 | 覆盖 | TTL |
|------|------|---------|------|
| Open-Meteo | 预报 + 多模型集合 | 全球 | 300s |
| METAR | 机场观测 | 全球 ICAO | 60s |
| TAF | 机场预报 | 全球 ICAO | 600s |
| NWS | 国家预报 | 美国 | 按请求 |
| MGM | 国家官方 | 土耳其 | 300s |
| ECMWF/GFS/ICON/GEM/JMA | 多模型 NWP | 全球 | 300s |
| HKO/CWA/NOAA/AMOS/NMC | 结算观测 | 特定国家 | 60s (AMOS) / 300s |
| Wunderground | 个人气象站 | 全球备用 | 按请求 |
| Polymarket Gamma | 市场发现 | 所有温度市场 | 60s |
| Polymarket CLOB | 订单簿 | 匹配市场 | 30s |
### 待改进
| # | 问题 | 优先级 |
|---|------|------|
| 1 | 无源端健康状态检测 | 🟡 |
| 2 | METAR TTL 60s 过于激进(机场每小时发一次) | 🟡 |
| 3 | 无请求重试(`POLYWEATHER_HTTP_RETRY_COUNT` 默认 0 | 🟡 |
## 三、分析层
### DEB 动态集成混合
自适应加权:11 模型按过去 7 天 MAE 动态分配权重。回退链完善。
### 概率校准
**已修复:校准漂移检测**`check_calibration_drift()` 对比最近 CRPS 与基线,漂移 >15% 时告警,集成在 `/api/system/status``probability.drift` 字段。
| # | 问题 | 优先级 |
|---|------|------|
| 4 | 校准系数静态 JSON 文件,数据分布变化需手动重新训练 | 🟡 |
## 四、API 与缓存层
**已修复:ETag 304** — 后端 `_etag_middleware` 对 GET /api/* 自动返回 ETag (MD5),支持 `If-None-Match`,匹配返回 304 + `Cache-Control: private, max-age=30`
**已修复:TTL 匹配**`SCAN_TERMINAL_PAYLOAD_TTL_SEC` 30s → 120s,匹配 ThreadPoolExecutor(4)×60 城的实际重算耗时。
| # | 问题 | 优先级 |
|---|------|------|
| 5 | 缓存键过粗(city::mode),微小变化也触发完整重算 | 🟡 |
## 五、前端状态管理
**已修复:sessionStorage 限制** — 只保留最近 3 个城市的详情,避免 3-10MB JSON 序列化阻塞主线程。
**已修复:Context 拆分**`CityDetailsContext` 独立管理 `cityDetailsByName` 变更,新增 `useCityDetails` hook。只读详情数据的组件不因其他状态变化而重渲染。
**已修复:Stale-while-revalidate**`ensureCityDetail` 过期缓存立即返回 + 后台异步刷新,用户不再看到 loading spinner。
**已修复:扫描数据复用**`preloadCityFromRow()` 从扫描终端行预填充城市详情缓存,选城市后详情面板立即显示。
## 六、待办
| # | 问题 | 优先级 | 说明 |
|---|------|------|------|
| 1 | 校准系数需手动重新训练 | 🟡 | 漂移检测已有,但自动触发重训练需要 GPU/算力资源 |
| 2 | 缓存键过粗 — `city::mode` 粒度 | 🟢 | 微小温度变化触发完整重算,可考虑内容 hash 键 |
> 注:原审查中 METAR TTL 60s 实际为 600s(误诊);扫描终端轮询已有 `AbortController` + `requestSeq` 保护(误诊)。
+10 -26
View File
@@ -3,21 +3,17 @@
## 执行摘要
PolyWeather(仓库:`yangyuan-zhen/PolyWeather`)定位为**面向温度类结算预测市场(如 Polymarket 的温度结算合约)**的“生产级气象情报系统”,核心在于把多源天气观测/预报转化为**结算导向的概率桶(μ + bucket distribution**,并进一步映射到市场报价完成**错价扫描**;同时提供 Web 仪表盘与 Telegram Bot 两套交互入口,并包含 Polygon 链上 USDC/USDC.e 支付、自动补单与订阅/积分体系。项目 README 现明确仓库代码采用 `AGPL-3.0-only`,同时将品牌、商标、生产私有数据与运营阈值保留在代码许可证之外。
从工程实现看,截至 `2026-04-27`,项目已经完成一轮更明确的工程化收口:多源天气采集仍保持现有业务能力,同时已完成采集层与 Web API 大文件拆分、CI 质量门禁、配置分级(`.env.example` / `.env.secrets.example` / 中文部署文档)、EMOS/CRPS 校准链路、运行态状态与缓存迁移到 SQLite 主路径,以及最小外部监控链路(`/healthz``/api/system/status``/metrics` + Prometheus + Alertmanager + Grafana + Telegram relay)。除此之外,项目还补上了**历史真值治理**:`daily_records` 继续只保留近 14 天运行态缓存,但新增了永久真值表、真值 revision 审计表和长期训练特征表,并开始把监督真值与训练特征从“短期缓存”正式拆到“长期可追溯存储”。2026-04 下旬新增的前端城市决策卡把“多模型 + METAR + 市场桶”进一步组合成面向单城点击的解释层:AI 机场报文解读、最高温中枢、完整市场桶匹配与“模型-市场差”已成为 Scan Terminal 的核心决策入口。
这意味着报告里最初最突出的“工程地基缺失”问题,已经有一部分被关闭:`src/data_collection/weather_sources.py``web/app.py` 不再是原来的超大单文件;GitHub Actions 已覆盖 Python、前端和 Docker build;配置与密钥治理已成体系;运行态状态不再只能依赖 JSON/JSONL 文件;EMOS 也不再只是概念,而是进入了可训练、可评估、可 shadow、可门禁判断的阶段;更重要的是,监督真值与训练特征不再只能附着在 14 天运行态缓存上。
但项目仍处在“从可用走向稳态”的中段,而不是终局。当前真正的高优先级问题已进一步收敛:**EMOS 仍未达到生产切换标准**,当前门禁结论明确为 `hold`,阻塞原因是 shadow bucket brier 明显退化,同时历史长期特征仍处在“刚开始积累”的阶段。SQLite 迁移方面,运行态主读切换和核心离线训练/回填链路已经完成验收:在移除 `data/*.json` / `data/*.jsonl` 后,训练、评估、shadow report 与关键 backfill 脚本仍可仅依赖运行时数据库正常执行;当前保留的 legacy 文件路径主要用于迁移、导出、校验和显式回退输入。历史真值治理方面,新增的永久真值表、revision 审计表与长期训练特征表已经落地,`Taipei` / `Shenzhen` 的历史页面回填也已接通,因此当前缺口已从“历史真值是否会继续丢失”转为“历史特征是否能持续增长并支撑 EMOS/LGBM 评估”。可观测性方面,最小外部监控链路已经补齐:Prometheus 抓取、Alertmanager 规则、Grafana 面板、Telegram 告警 relay 与巡检脚本均已落地;当前剩余缺口已从“有没有外部监控”转为“监控覆盖深度是否足够”,例如节点级资源、数据库体积趋势、支付细粒度指标、按城市/来源拆分的业务 SLA。支付链路方面,链下审计与容灾已明显增强:事件重放、SQLite 审计事件、RPC 多节点容灾、合约静态检查、`/ops` 支付异常单都已补齐;当前剩余风险主要集中在**链上合约本身仍是最小实现**,尚未升级到 SafeERC20、Pausable、链上套餐绑定等更强防护版本。
因此,当前阶段最正确的策略已经不是继续做“大范围基础重构”,而是围绕**EMOS 上线门禁稳定化、长期训练特征持续积累、监控覆盖深挖、城市决策卡可观测性、支付合约防护升级**这五条线持续收口。短中期内更高 ROI 的方向依然不是引入新的大模型,而是把现有“采集→后处理→市场映射→前端决策→支付/订阅”的链路做成**状态一致、指标可见、发布可控、回退明确**的生产平台。
## 项目概览
PolyWeather 的目标与范围在 README/README_ZH 中定义得较清楚:为温度结算市场提供气象情报(多源采集→融合→概率→对照市场报价),并提供“官方看板(Vercel 前端)+ VPS 后端 + Telegram Bot”。
项目主功能可归纳为五层:
**天气层(数据源/采集)**:聚合 52 个城市的实测与预报;支持 AviationWeather METAR(机场观测)、土耳其 MGM 站网、Open-Meteo(含多模型与集合预报)、美国 NWS(仅美国城市)、以及部分城市使用明确官方站点或历史页面入口(香港 HKO、台湾/深圳相关历史页面等)等。机场类市场仍以 METAR / 机场主站为结算锚点,Wunderground 不描述为物理观测站。
**天气层(数据源/采集)**:聚合 51 个城市的实测与预报;支持 AviationWeather METAR(机场观测)、韩国 AMOS 跑道级观测(首尔/釜山)、土耳其 MGM 站网、Open-Meteo(含多模型与集合预报)、美国 NWS(仅美国城市)、以及部分城市使用明确官方站点或历史页面入口(香港 HKO、台湾/深圳相关历史页面等)等。机场类市场仍以 METAR / 机场主站为结算锚点,Wunderground 不描述为物理观测站。
**分析层(DEB/趋势/概率/结算口径)**
DEBDynamic Error Balancing)基于过去 N 天模型误差(MAE)倒数加权,输出融合预报;运行态仍维护近 14 天 `daily_records` 缓存做当前对账,但长期监督真值与训练特征已经迁到 SQLite 永久表中,并支持基于 WUWeather Underground 口径)四舍五入的结算命中评估。
趋势/概率引擎在 `trend_engine.py` 中实现:综合“集合预报区间→σ/μ→高温窗口→死盘判定→温度桶概率分布→边界提示”等,用于 bot 展示与 web 结构化数据输出。
**城市决策层(Scan Terminal / AI 机场报文解读)**:地图点击城市后加入城市决策卡,前端拉取 full detail、多模型区间、最新 METAR,并通过 `/api/scan/terminal/ai-city/stream` 生成城市级 AI 解读。该解读由 `final_judgment``metar_read``reasoning``model_cluster_note``risks` 与原始 METAR 证据组成;最高温中枢优先使用 AI `predicted_max`,再回退到 DEB、多模型中心、日内 pace 或当前实测。
**市场层(Polymarket 行情对照)**:只读模式从 Gamma API 发现市场、从 CLOB`py-clob-client` 或 REST 回退)读取价格/盘口,并用完整 `all_buckets` 对目标温度桶做 exact/range/“or higher”/“or lower” 严格匹配,计算“模型-市场差”(模型概率 − 市场隐含概率)生成信号标签。
**商业化与支付**:订阅(`Pro Monthly 5 USDC`)、积分抵扣、Polygon 链上收款合约(USDC/USDC.e),并提供“事件监听 + 周期确认”的自动补单机制。
**商业化与支付**:订阅(`Pro Monthly 10 USDC`)、积分抵扣、Polygon 链上收款合约(USDC/USDC.e),并提供“事件监听 + 周期确认”的自动补单机制。
**支持的数据集/数据源**:项目不是传统“训练数据集+模型训练”的机器学习仓库;其“数据集”本质是外部实时/预报 API 与站点观测数据。对外部数据的使用需要遵守来源方的访问与速率限制,例如 AviationWeather Data API 明确限制请求频率(含每分钟请求上限/建议降低频率与使用缓存文件)。
**许可证**:仓库根目录 `LICENSE` 当前为 `AGPL-3.0-only`。同时 README 与策略文档明确:品牌、商标、生产私有数据与运营策略不随代码许可证一并授权。
(插图:项目 README 中包含产品截图,可用于快速理解信息架构与 UI 形态)
@@ -33,9 +29,8 @@ DEBDynamic Error Balancing)基于过去 N 天模型误差(MAE)倒数加
| 运行时组件 | `frontend/` | Next.js 前端(Vercel | 前端重构报告提到 App Router、Route HandlersBFF)、缓存策略、支付与账户中心等;Scan Terminal 已新增城市决策卡、AI 机场报文解读、页面内存/localStorage 双层缓存、AI stream 小并发队列与完整市场桶映射。 |
| 运行时组件 | `web/app.py` + `web/core.py` + `web/routes.py` + `web/analysis_service.py` + `web/scan_terminal_service.py` | FastAPI 后端 API | 已从单文件入口拆为启动入口、核心上下文、路由层、分析服务层;Scan Terminal 侧提供 `/api/scan/terminal/ai-city/stream`,城市 AI 默认 30s 超时并支持 stream parse failure 的非流式重试。 |
| 运行时组件 | `bot_listener.py` + `src/bot/*` | Telegram Bot | 入口 `bot_listener.py``start_bot()`,并由 `StartupCoordinator` 启动多个后台 loop。 |
| Python 域模块 | `src/data_collection/*` | 天气采集 + 城市注册 + 市场读取 | 采集层已拆为 `weather_sources.py` 编排层 + `open_meteo_cache.py``settlement_sources.py``metar_sources.py``mgm_sources.py``nws_open_meteo_sources.py`。 |
| Python 域模块 | `src/data_collection/*` | 天气采集 + 城市注册 + 市场读取 | 采集层已拆为 `weather_sources.py` 编排层 + `open_meteo_cache.py``settlement_sources.py``metar_sources.py``mgm_sources.py``amos_station_sources.py``jma_amedas_sources.py``nws_open_meteo_sources.py`。 |
| Python 域模块 | `src/analysis/*` | DEB/趋势/概率/结算口径 | `deb_algorithm.py``trend_engine.py``settlement_rounding.py`。 |
| Python 域模块 | `src/analysis/probability_calibration.py` + `src/analysis/probability_rollout.py` | 概率校准与上线门禁 | 已支持 `legacy / emos_shadow / emos_primary`,并可产出 rollout 判断。 |
| Python 域模块 | `src/payments/*` + `contracts/*` | 支付合约 + 事件监听/补单 | Solidity 合约 + Python 侧事件扫描/确认循环 + SQLite 审计事件 + RPC 多节点容灾 + 合约静态检查。 |
| Python 域模块 | `src/auth/*``docs/SUPABASE_SETUP_ZH.md``scripts/supabase/schema.sql` | Supabase 鉴权/订阅/积分 | 使用 `/auth/v1/user` 校验 JWT、`/rest/v1/subscriptions` 查订阅(服务端角色 key 必须保密)。 |
| Python 域模块 | `src/database/runtime_state.py` | 运行态状态、永久真值与训练特征仓储 | 已接入 `daily_records``telegram_alert_state``probability_training_snapshots``open_meteo` 持久缓存,并新增永久真值表、真值修订审计表、长期训练特征表。 |
@@ -69,6 +64,7 @@ JSON[Legacy JSON files<br/>migration/export/explicit fallback only]
OM[Open-Meteo Forecast/Ensemble/Multi-model]
AW[AviationWeather Data API<br/>METAR]
MGM[MGM Turkey]
AMOS[global.amo.go.kr<br/>AMOS runway sensors]
NWS[api.weather.gov]
HKO[data.weather.gov.hk]
CWA[opendata.cwa.gov.tw]
@@ -95,6 +91,7 @@ JSON[Legacy JSON files<br/>migration/export/explicit fallback only]
WX --> OM
WX --> AW
WX --> MGM
WX --> AMOS
WX --> NWS
WX --> HKO
WX --> CWA
@@ -118,7 +115,7 @@ JSON[Legacy JSON files<br/>migration/export/explicit fallback only]
### 城市决策卡工作流(2026-04 更新)
### 城市决策卡工作流(2026-05 更新)
Scan Terminal 的城市决策卡现在承担“从天气分析到市场动作解释”的前端决策层:
@@ -148,13 +145,12 @@ Web/Telegram 请求 → FastAPI 调用采集器抓取/复用缓存 → 分析引
**测试**:仓库存在 `tests/test_trend_engine.py`,覆盖 μ 计算、死盘判定、预报崩盘提示、趋势方向等核心逻辑(通过 patch 隔离外部依赖)。前端侧已通过 `npm run build` 验证 Scan Terminal 改动可以编译;后续仍建议为城市决策卡补固定 fixture,覆盖 `all_buckets` 匹配、温度单位渲染、AI 缓存 key 与 stream 队列行为。
**CI/CD**:已补齐 GitHub Actions 工作流,至少覆盖 Python lint/test、前端 build、Docker build 三条门禁;当前缺口不再是“有没有 CI”,而是“是否已在 GitHub 分支保护中强制执行”。
**运维验收**:除 `scripts/validate_frontend_cache.sh` 外,现已新增配置校验、运行态迁移/核验、EMOS rollout 判断等脚本,并提供 `/healthz``/api/system/status``/metrics` 作为基础观测入口。
**部署/更新**Compose 用于启动服务;另有 `update.sh` 通过 `pkill` + `nohup` 重启 bot 与 web。
## 优势与薄弱点
### 优势
**产品闭环完整、目标明确**:从“天气→结算→市场→错价信号→付费体系(订阅/积分/链上支付)”形成可商业化闭环,并在 README 清晰列出当前产品状态(订阅、积分抵扣、链上支付、自动补单等已上线)。
**产品闭环完整、目标明确**:从“天气→结算→市场→错价信号→付费体系(订阅/积分/链上支付)”形成可商业化闭环,并在 README 清晰列出当前产品状态(订阅、积分抵扣、链上支付、自动补单等已上线)。2026-05 完成积分制度改造:`/city` `/deb` 改为免费(每日各 10 次),新增首次发言欢迎奖励与每日首条消息奖励,周奖励降低赢家积分差距并增加全员参与奖。
**复用一套分析内核服务多端**:趋势/概率/DEB 等核心逻辑被抽成分析模块,并被 web 与 bot 共用,避免“两套逻辑漂移”。前端城市决策卡在此基础上补足“机场报文解释 + 市场桶动作口径”,让用户从地图点击可以直接进入可解释决策。
**面向外部 API 的工程防护意识较强**Open-Meteo 429 冷却期、最小调用间隔、磁盘缓存、缓存 TTL 等措施表明作者已遭遇并处理速率限制与冷启动问题。 同时 AviationWeather 官方文档也明确建议控制频率并可使用 cache 文件降低负载,项目后续可进一步对齐最佳实践。
**支付侧有“事件监听 + 确认补单”的双通路**:支付链路天然存在“交易 pending / RPC 延迟 / 日志索引不完整”等问题,项目通过 event loop 与 confirm loop 双机制提升最终一致性。
@@ -164,38 +160,30 @@ Web/Telegram 请求 → FastAPI 调用采集器抓取/复用缓存 → 分析引
**可复现性已从“缺模板”进入“模板与生产对齐”的阶段**`.env.example``.env.secrets.example`、中文配置文档、前端部署文档、运行时配置校验器都已存在;当前风险主要在于线上历史 `.env` 与新模板并存、旧变量命名残留、以及密钥轮换与分层是否真正落实。
**CI 已建立,但组织级质量门禁未必完全收口**:CI 现已覆盖 Python、前端与 Docker build。当前问题不再是“缺 CI”,而是是否把这些 status check 绑定到 `main` 保护策略,以及是否逐步引入更严格的 pre-merge 审查。
**运行态状态/缓存与核心离线链路的 SQLite 收口已完成**`daily_records``telegram_alert_state``probability_training_snapshots``open_meteo` 缓存已经支持并在生产中主读 SQLite,迁移/校验脚本可用;进一步地,在临时移除 `data/*.json` / `data/*.jsonl` 后,训练集导出、概率拟合、评估报告、shadow report 和关键 backfill 脚本已验证仍可运行。当前 legacy 文件路径主要是显式回退入口,而不再是默认主输入。
**历史真值治理已从设计缺陷修复到可追溯运行**`daily_records` 继续作为近 14 天运行态缓存,但已经不再承担长期监督真值职责;项目新增了永久真值表、真值 revision 审计表和长期训练特征表,并为 `Taipei` / `Shenzhen` 补上了历史页面回填链路。当前风险已不再是“监督真值会不会继续被 14 天裁剪吞掉”,而是“历史长期特征能否持续积累到足够支撑 EMOS/LGBM 重新评估”。
**第三方服务合规与稳定性风险**
项目强依赖外部 APIOpen-Meteo、AviationWeather、NWS、HKO、CWA、Polymarket、Supabase)以及城市 AI providerOpenAI-compatible stream,当前临时 MiMo)。其中 AviationWeather Data API 有明确速率限制;Polymarket 官方说明 Gamma/Data/CLOB 三套 API 分属不同域,CLOB 交易端点需鉴权且策略可能变化;Supabase 明确强调 `service_role`/secret keys 绝不可暴露。若缺乏集中治理(重试/退避/熔断/降级/配额监控/密钥轮换),稳定性与合规不可控。城市 AI 解读已经通过前端 2 并发队列、30s timeout、stream parse retry 与缓存 key 稳定化降低第三/第四城市失败概率,但仍需持续记录 stream duration、cache hit、retry、degraded 与 queue depth。
**可观测性最小闭环已完成,但监控深度仍待加强**:项目现在已有 `/healthz``/api/system/status``/metrics`,并已补齐 Prometheus 抓取、Alertmanager 规则、Grafana 面板、Telegram relay 与巡检脚本。与此同时,`/ops` 已经逐步演进为后台管理台而不只是状态页:除支付、会员、用户与 EMOS 门禁外,还新增了训练数据治理卡片、城市覆盖矩阵,以及 `/ops/truth-history` 这种可直接查询 `actual_high / settlement_source / station_code / truth_version / updated_by / updated_at` 的真值表浏览页。当前缺口不再是“有没有外部监控”,而是节点级资源、数据库体积趋势、更细粒度支付指标、按城市/来源拆分的业务 SLA,以及是否需要进一步补 `truth revision` 明细页、趋势图和运营日报。
**EMOS 已完成工程接入,但未完成生产发布**EMOS/CRPS 校准、shadow 观测、rollout report、上线门禁都已实现;当前真实门禁结果为 `hold`,阻塞原因是 shadow bucket brier 明显退化。因此概率引擎标准化并非未做,而是“工程完成、发布未通过”。
项目强依赖外部 APIOpen-Meteo、AviationWeather、global.amo.go.kr AMOS、NWS、HKO、CWA、Polymarket、Supabase)以及城市 AI providerOpenAI-compatible stream,当前临时 MiMo)。其中 AviationWeather Data API 有明确速率限制;Polymarket 官方说明 Gamma/Data/CLOB 三套 API 分属不同域,CLOB 交易端点需鉴权且策略可能变化;Supabase 明确强调 `service_role`/secret keys 绝不可暴露。若缺乏集中治理(重试/退避/熔断/降级/配额监控/密钥轮换),稳定性与合规不可控。城市 AI 解读已经通过前端 2 并发队列、30s timeout、stream parse retry 与缓存 key 稳定化降低第三/第四城市失败概率,但仍需持续记录 stream duration、cache hit、retry、degraded 与 queue depth。
**许可证/商业使用的潜在冲突点**:仓库自身现为 `AGPL-3.0-only`,但如果未来尝试引入外部神经天气模型,仍需单独核验第三方代码与权重的商用条件:GraphCast 仓库代码 Apache-2.0,但权重使用 CC BY-NC-SA 4.0(非商业),Pangu-Weather 权重同样 BY-NC-SA 且明确禁止商业用途;不加区分地把这些模型用于付费产品会留下法律风险。
## 对标分析
为满足“至少 3 个相似开源项目或近期论文”对标,本报告选择三类代表:
1**AI 气象预报模型**GraphCast / FourCastNet / Pangu-Weather):用于评估“若 PolyWeather 未来扩展到更强预测能力”的技术与许可边界;
2)**概率后处理方法**EMOS):作为 PolyWeather 概率引擎的更标准化替代/对照;
3)**预测市场 API 客户端生态**Polymarket/py-clob-client、aiopolymarket):用于评估市场层的工程选型。
### 关键对比表
| 项目/论文 | 解决的问题 | 输出形态 | 性能/效果(公开描述) | 易用性与依赖 | 许可证要点 |
| --------------------------------------------------------- | ---------------------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| **PolyWeather**(本仓库) | 温度结算市场气象情报:多源→校准概率桶→错价扫描→城市决策卡→订阅/支付 | 生产级应用(Web+Bot+API+支付) | 以工程能力为主;内置 DEB、LGBM/EMOS 校准概率、死盘判定、市场扫描;当前覆盖 52 城市,并已补齐真值治理、后台运维视图、AI 机场报文解读与 full bucket 决策映射。 | 主要依赖外部 API 与城市 AI providerDocker Compose 一键启动。 | 仓库 `AGPL-3.0-only`;品牌、生产私有数据与运营规则不随代码许可证授权。 |
| **GraphCast**google-deepmind/graphcast | 10 天全球中期预报(ML 替代/增强 NWP) | 模型代码+权重+notebooks | 论文与介绍提到在大量指标上优于主流确定性系统;仓库提供预训练权重与示例数据入口,并提示 ERA5/HRES 数据条款需另行遵守。 | 完整训练需 ERA5 等;更适合科研/平台级推理,不是产品级 BFF。 | 代码 Apache-2.0;权重 CC BY-NC-SA 4.0(商业限制)。 |
| **FourCastNet**NVlabs/FourCastNet | 高分辨率 data-driven 全球预报(AFNO/ViT) | 模型训练/推理代码+数据/权重链接 | README 描述:0.25° 分辨率、周尺度推理非常快,并可做大规模集合;适合平台型预报。 | 训练/数据依赖大(ERA5 子集 TB 级);工程集成成本高。 | BSD 3-Clause(代码)。 |
| **Pangu-Weather**198808xc/Pangu-Weather + Nature 论文) | 3D Transformer 架构的中期全球预报 | ONNX 推理代码+预训练模型 | Nature 论文称在 reanalysis 上对比 IFS 有更强确定性预报表现,并强调速度优势;仓库提供 ONNX 推理与 lite 版训练说明。 | 模型文件大(多份 ~GB 级),训练资源需求高;更适合科研推理或内部平台。 | 权重 BY-NC-SA 4.0、明确禁止商业用途。 |
| **EMOS**Gneiting & Raftery 等) | 集合预报校准:纠偏与解决 underdispersion | 统计后处理方法 | 提出用回归形式输出概率分布(常见为高斯),并以 CRPS 等指标拟合,属于成熟的气象概率校准路线。 | 易落地:对 PolyWeather 而言只需“历史库+拟合器”。 | 方法论(论文);可自行实现,无额外许可约束(注意论文版权)。 |
| **Polymarket/py-clob-client** | Polymarket CLOB 读写 SDK | Python SDK | 官方 SDK,支持 read-only 与交易接口;协议与端点在官方文档中给出。 | 易用,适合增强 PolyWeather 市场层。 | MIT。 |
| **aiopolymarket** | Polymarket APIs 的 async 客户端 | Python async 客户端 | 强调类型安全(Pydantic)、自动分页、重试与 backoff,适合高并发与健壮性诉求。 | 适合替换/补强当前同步 requests 与自定义缓存。 | 以仓库许可为准(此处建议上线前核验)。 |
**对标结论**PolyWeather 与这类“全球神经天气模型”不在同一层级:PolyWeather 是“面向结算市场的产品化情报系统”,其价值核心是**将预测转成可交易/可结算的决策信息**。短中期内更高 ROI 的方向不是“自训大模型”,而是把现有“采集+后处理+市场映射”的链路做成**可复现、可观测、可评测、可扩展**的工程平台;在许可合规前提下,再评估引入外部模型推理作为额外信号源。
## 优先级改进建议
下表按截至 `2026-04-27` 的真实状态重排优先级。已完成项不再继续列为“待做”,只保留当前仍需推进的事项。
下表按截至 `2026-05-11` 的真实状态重排优先级。已完成项不再继续列为“待做”,只保留当前仍需推进的事项。
| 优先级 | 改进项 | 预估工作量 | 主要收益 | 主要风险 | 可执行步骤(建议顺序) |
| ------ | --------------------------------------------------------------------------------------------------------------------------------- | -------------------: | ------------------------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 高 | **稳定 EMOS shadow 并收紧上线门禁** | 1–2 周 | 让概率引擎升级具备明确发布条件,避免拍脑袋切换 | 当前 shadow bucket brier 退化明显,存在误上线风险 | 1) 持续积累 snapshot 样本 → 2) 定期重训与生成 `evaluation_report` / `shadow_report` / `rollout_report` → 3) 重点压 `bucket_brier` 退化 → 4) 只有门禁从 `hold` 进入 `observe/promote` 后才考虑上线 |
| 高 | **持续积累长期训练特征,验证 SQLite 真值治理后的样本增长** | 12 周 | 让 EMOS/LGBM 的重训真正建立在长期可信样本上,而不是继续被短期特征缺口卡住 | 当前真值已长期化,但历史长期特征仍偏少,EMOS/LGBM 样本增长会滞后 | 1) 持续写入 `training_feature_records_store` → 2) 每日检查 `/ops` 训练数据与 `/ops/truth-history` → 3) 定期对 `Taipei` / `Shenzhen` 的历史页面回填做抽查 → 4) 观察样本是否自然增长后再重训 |
| 中 | **把最小外部监控继续补深**:从“可告警”提升到“可运营” | 3–7 天 | 不再只知道服务坏没坏,还能看资源趋势、来源 SLA 和支付波动 | 指标过多会带来维护噪音 | 1) 增加节点 CPU/内存/磁盘 → 2) 增加 SQLite/支付体积与事件趋势 → 3) 把 HTTP/来源指标细分到城市/来源维度 → 4) 增加日报或异常摘要 |
| 中 | **城市决策卡 AI 解读可观测性与回放测试** | 3–5 天 | 降低第三/第四/第五城市 AI 解读失败,验证缓存与队列是否真正生效 | 外部 AI stream 仍可能超时或输出截断,若无指标很难复盘 | 1) 记录 city-ai stream status/duration/retry/degraded/cache-hit/queue-depth → 2) 增加固定 METAR + detail + all_buckets fixture → 3) 回归断言 bucket 匹配、模型-市场差、温度单位与缓存 key → 4) 将生产 env 建议同步进部署文档 |
| 中 | **市场层升级为 async + 类型安全**:引入 `aiopolymarket` 或在现有层加重试/backoff/连接池 | 4–7 天 | 行情层更稳,减少短时网络抖动;更易扩展更多市场/分页 | 依赖升级带来的行为差异 | 1) 把 requests.Session 替换为 aiohttp/httpx → 2) 在 Gamma/CLOB 调用侧实现指数退避 → 3) 引入 typed models,减少解析失败 |
@@ -221,19 +209,16 @@ PolyWeather 的评测应围绕“结算场景”而非传统数值天气预报
**指标**
1)确定性误差:MAE、RMSE(按城市、按季节、按风险等级分组);
2)结算命中率:`WU_round(pred) == WU_round(actual)`(项目已有统计口径);
3)概率质量:Brier Score(对离散温度桶),以及建议补充 CRPS(连续变量概率评分,EMOS 体系常用)。
4)校准曲线:预测概率分箱的可靠性图(reliability diagram)与 Sharpness(分布集中度)。
**基线**
- Baseline AOpen-Meteo 当日最高温(或 forecast median)作为点预测;
- Baseline B:等权平均(DEB 在历史少时也会回退此策略);
- Baseline C:当前 DEB
- Baseline DEMOS(以 ensemble 均值/方差为输入,拟合 μ 与 σ,优化 CRPS)。
**预期结果(定性)**
- 若历史样本足够,DEB 应在“系统性偏差明显”的城市提升 MAE;
- EMOS 类方法通常能在概率校准(可靠性与 CRPS)上更稳定,尤其当 ensemble 信息可用(项目已接入 Open-Meteo ensemble/p10/p90
**算力**:以上评测全部可在 CPU 上完成;数据量按“52 城市 × 180 天”级别,pandas/duckdb 即可。若引入更复杂拟合(如分层贝叶斯/分位数回归),也通常不需要 GPU。
**算力**:以上评测全部可在 CPU 上完成;数据量按“51 城市 × 180 天”级别,pandas/duckdb 即可。若引入更复杂拟合(如分层贝叶斯/分位数回归),也通常不需要 GPU
### 错价信号与市场有效性基准
**数据集**
@@ -257,7 +242,6 @@ PolyWeather 的评测应围绕“结算场景”而非传统数值天气预报
| ----------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| 第 1 周 | 城市决策卡稳定性补强 | city-ai stream/cache/queue 指标;固定 METAR + `all_buckets` fixture;温度桶匹配与模型-市场差回归测试;生产 env 文档化 | 前端为主,后端补指标 |
| 第 2 周 | 市场层与 Scan Terminal 数据回放 | 保存 `bucket_label/bucket_direction/model_market_diff/matching_reason`;支持回放第三/第四/第五城市 AI 解读失败案例 | 用真实失败样本压回归 |
| 第 3–4 周 | EMOS 与长期特征继续收口 | 持续积累 `training_feature_records_store`;定期生成 evaluation/shadow/rollout report;继续观察 `bucket_brier` 是否退出 hold | 数据工程为主 |
| 第 4–5 周 | 监控深挖与运维日报 | 来源 SLA、城市维度延迟、AI stream 状态、SQLite 体积、支付事件趋势、异常摘要 | 避免指标过多,先覆盖高频故障 |
| 第 6 周 | 支付合约与发布门禁升级 | SafeERC20/Pausable 方案评审;CI required checks 与 release/tag 流程绑定;热修例外流程 | 合约升级需单独部署验证 |
+347
View File
@@ -0,0 +1,347 @@
# PolyWeather Frontend UI 设计审查报告
> 审查日期:2026-05-10 | 审查范围:`frontend/` 全部组件、样式、布局
## 整体评价
前端设计系统有**扎实的基础** — 完善的 CSS 自定义属性 token 体系、不错的暗色模式、合理的排版层级、以及语义化的颜色编码。但存在一些**工程性债务**需要关注。
最突出的 5 个问题:
1. `!important` 在浅色主题中泛滥(~960 行),维护成本极高
2. `font-weight: 760/850/950` 等无效值在 CSS 中大量使用
3. Geist 字体声明但从未加载,始终 fallback 到 Inter
4. 键盘导航和 focus-visible 样式缺失
5. 14 个不同的响应式断点值,无统一体系
---
## 一、设计系统 & Token 体系
### 优点
1. **完整的 CSS 自定义属性体系** (`globals.css:9-93`):拥有从背景色阶、文字色阶、语义色、阴影、间距、圆角、毛玻璃到动效的完整 token 体系,覆盖面广。
2. **4px 间距网格**:从 `--space-1: 4px``--space-12: 48px`,提供了系统化的间距基础。
3. **三层毛玻璃系统**`--glass-blur-1/2/3` + `--glass-opacity-1/2/3` 的组合创造视觉深度层次,在暗色背景下效果出色。
4. **Tailwind 桥接**`tailwind.config.ts` 提供了 `pw-bg-*``pw-text-*``pw-accent-*` 等语义化 utility class,兼顾了设计 token 的语义化和 Tailwind 的效率。
### 问题
1. **"Fintech 3-Color" 名不副实** (`globals.css:25-27`)
```css
--color-accent-primary: #4DA3FF; /* 蓝 */
--color-accent-secondary: #4DA3FF; /* 同上 — 完全一样 */
--color-accent-tertiary: #93C5FD; /* 浅蓝 */
```
primary 和 secondary 是同一个颜色,实际只有 2 种不同色值。"3-Color" 声明具有误导性。
2. **Token 未充分利用**:大量 CSS Module 中硬编码了颜色值(如 `#4DA3FF`、`#E6EDF3`、`rgba(77, 163, 255, ...)`)而不是引用 CSS 变量。例如 `ScanTerminalCard.module.css` 中几乎每一行都是硬编码色值。
3. **shadcn/ui HSL token 遗留** (`globals.css:95-106`):保留了 shadcn 的 HSL token 但几乎未被使用,增加了 token 体系的认知负担。
---
## 二、布局 & 响应式
### 优点
1. **Scan Terminal 的 CSS Grid 布局** (`ScanTerminal.module.css:524-537`)`grid-template-columns: minmax(0, 1fr) minmax(380px, 420px)` 的 2 列布局干净、现代。
2. **毛玻璃面板系统**:所有主面板都有 `22px border-radius`、`backdrop-filter: blur(14px)`、渐变背景和微妙的阴影,视觉统一。
3. **Sticky detail rail**:右侧面板 `position: sticky; top: 16px; height: calc(100vh - 32px)` 保证了用户滚动时详情始终可见。
### 问题
1. **两套布局系统并存**
- 旧版 "Home Intelligence" 使用 **absolute positioning** 定位 header/sidebar/map/panel
- 新版 "Scan Terminal" 使用 **CSS Grid**
- 两套 CSS Module 都在 `ScanTerminalDashboard.tsx` 中被 import 合并,增加了复杂度
2. **断点碎片化严重**:系统使用了 12+ 个不同断点值 (1680, 1480, 1360, 1240, 1100, 1020, 900, 820, 768, 720, 640, 600, 560, 520),没有统一的体系。同一个 tablet 过渡在一些模块用 768px,在另一些用 720px。
3. **Map View 中的 absolute positioning** (`DashboardShell.module.css`):使用 `top/right/bottom/left` 绝对定位来排列面板,脆弱且难以维护。
---
## 三、色彩 & 对比度
### 优点
1. **暗色模式基础扎实**`#0B1220` 的深邃底色配合三层 radial gradient 营造了深度感和科技感。
2. **语义色系统清晰**success/green、warning/amber、danger/red 的映射关系明确。
3. **渐变运用得当**body 背景的三层 radial gradient(绿/紫/青)创造了微妙的大气光感。
### 问题
1. **文字对比度偏低**
- 静音文字 `#6B7A90` 在 `#0B1220` 上的对比度约为 4.6:1,刚好达到 WCAG AA 标准但仍偏暗
- 大量 10-11px 的辅助文字使用 `#6B7A90`,可读性不足
2. **Accent Color 视觉突出度**`#4DA3FF`(蓝色)在暗色背景上的人眼敏感度不如 `#00E0A4`(青绿色),但前者被用作主要强调色。
3. **语义色 class 命名不一致**`accent-green` class 实际渲染为 `#4DA3FF`(蓝色),"green" 命名的 class 显示蓝色。
---
## 四、字体排印
### 优点
1. **字体选择合理**Inter(正文) + JetBrains Mono(数据/代码) 是 fintech/数据密集类产品的标准选择。
2. **tabular-nums 广泛使用**:几乎所有温度数值都设置了 `font-variant-numeric: tabular-nums`,保证了数字对齐。
3. **层级明确**:从 10px 的 kicker 到 56px 的 hero temperature,形成了清晰的视觉层级。
### 问题
1. **Geist 字体声明但未加载** (`layout.tsx:35-37`)
- `globals.css` 声明了 `--font-display: "Geist", "Inter", ...`
- 但 HTML 中只加载了 Inter 和 JetBrains MonoGeist 永远不会生效
2. **非标准 font-weight 值泛滥**
- `font-weight: 760`、`850`、`860`、`880`、`900`、`950` 在 CSS 中大量出现
- Inter 字体只支持 300/400/500/600/700/800
- 非标准 weight 会被浏览器取整到最近支持值,视觉不可预测
- 例如 `font-weight: 950` 在 Inter 上实际渲染为 `800`
3. **字体大小偏小**:大量 UI 文字使用 10-11px,在高分辨率屏幕上可读性差。
4. **大写字母过多**kicker/overline/chip 几乎全部使用 `text-transform: uppercase` + `letter-spacing: 0.08em`,降低了中文/双语场景下的可读性。
---
## 五、组件设计
### 优点
1. **City Decision Card 视觉层次优秀** (`ScanTerminalCard.module.css`):hero 区的渐变背景、蓝色左边框选中态、决策 band 的色彩编码(warm=red 边框、cold=green 边框、watch=amber 边框),信息密度高但不杂乱。
2. **Status Tags 色彩编码清晰**green/blue/amber/red/muted 五种 tag 变体覆盖了数据状态的所有场景。
3. **双卡预测对比** (AI vs DEB):青色渐变卡 vs 蓝色边框卡的视觉区分让用户能一眼区分 AI 预测和模型预测。
4. **Decision Band 语义化**`warm`(red) = 看涨、`cold`(green) = 看跌、`watch`(amber) = 待观察,视觉编码与交易语义对齐。
### 问题
1. **Topbar 设计过于简单**:28px 标题 + 几个按钮,缺乏品牌标识(Logo)和视觉焦点。
2. **Tab 下划线指示器不够明显**:`2px` 高度 + `opacity: 0.8` 的蓝色下划线容易被忽略。
3. **按钮层级不够清晰**`.scan-primary-button`(蓝紫渐变)、`.scan-ai-button`(青绿渐变)、`.scan-ai-city-icon-button`(蓝色边框半透明)、`.scan-theme-button`(无边框无背景)四种视觉权重混在一起,用户难以判断优先级。
4. **空状态/加载状态设计不一致**:
- 地图加载有精美的云/雷达/热力动画
- Scan terminal 加载是简单的脉冲 placeholder
- 空状态是一个简单的文字居中块
- 缺乏统一的 loading/empty/error 设计规范
5. **Mobile Decision Card 采用 `<details>` 元素**:原生 `<details>/<summary>` 样式控制有限、动画困难,与桌面端的自定义折叠按钮体验不一致。
---
## 六、深色/浅色主题
### 优点
1. **浅色模式覆盖全面**`ScanTerminalLightTheme.module.css` 约 960 行,覆盖了 scan terminal 的所有元素。
2. **localStorage 持久化**:主题选择保存在 `polyweather_scan_theme` 中,刷新不丢失。
3. **浅色配色方案合理**:从深色 `#0B1220` 到浅色 `#F7F9FC#EEF2F7` 的映射关系合理。
### 问题
1. **`!important` 严重滥用** (`ScanTerminalLightTheme.module.css`)
- 几乎每条浅色规则都使用了 `!important`
- 这是 specificity war 的症状,维护成本极高
- 多处出现 `!important` 叠加
2. **浅色主题分散在 8+ 个文件中**:缺乏集中管理,修改一个颜色需要跨多个文件搜索。
3. **浅色模式地图图块滤镜处理**:Leaflet tile 滤镜切换可能影响其他 overlay 的浅色适配。
---
## 七、动效 & 过渡
### 优点
1. **Cubic-bezier 缓动选择正确**`(0.4, 0, 0.2, 1)` (Material standard) + `(0.16, 1, 0.3, 1)` (spring-like) 的组合符合现代 UI 动效标准。
2. **入场动画有层次**detail panel 从右侧滑入 (400ms)、opportunity strip 有 120ms 延迟。
3. **Hover 微交互**:按钮 `translateY(-1px)`、卡片 `scale(1.002)`、边框颜色过渡等微交互提升了操作反馈感。
### 问题
1. **`@keyframes spin` 重复定义 4 次**:在 `DashboardShell.module.css`、`DashboardModalGuide.module.css`、`DocsLayout.module.css`、`DashboardMap.module.css` 中各自定义了一次完全相同的 spin 动画。
2. **Loading 动画风格不统一**`DashboardMap` 有复杂的 weather-themed 动画(radar swipe、cloud drift、thermal bars、wind shift),而 scan terminal 只有简单的扫光 placeholder。
3. **缺少 `prefers-reduced-motion` 支持**:没有任何 `@media (prefers-reduced-motion: reduce)` 的声明。
---
## 八、代码组织 & 可维护性
### 优点
1. **CSS Module 组件隔离**:每个组件有对应的 `.module.css`,样式作用域控制良好。
2. **命名约定一致**:CSS 类名使用 `scan-` 前缀 + kebab-case,全局可识别。
### 问题
1. **CSS Module 堆叠模式过度耦合** (`ScanTerminalDashboard.tsx:143-159`)
```tsx
const scanTerminalRootClassName = clsx(
styles.root,
dashboardHomeStyles.root,
dashboardMapStyles.root,
// ... 共 22 个 CSS Module 的 .root 合并
);
```
单个组件 import 了 22 个 CSS Module,破坏了 CSS Module 的隔离优势。
2. **`:global()` 绕过了 CSS Module 的哈希**:几乎所有规则都使用 `:global(.class-name)`class 名不会被哈希。CSS Module 降级为"命名约定"工具。
3. **样式与逻辑耦合**`ScanTerminalDashboard.tsx` 约 650 行,同时负责状态管理和渲染布局。
4. **重复的 CSS 变量声明**`Dashboard.module.css` 中重新声明了 `--bg-primary` 等本地变量,与 `globals.css` 的全局 token 形成冗余。
---
## 九、无障碍性
### 优点
1. 部分元素有 aria-label(如 topbar 按钮、locale switch
2. 主题切换按钮有 title 属性
3. ProFeaturePaywall 使用了 `role="dialog"` 和 `aria-modal="true"`
### 问题
1. **键盘导航不足**
- 城市卡片没有 `tabindex` 或 `role="button"`
- Tab 切换缺少 `role="tablist"`/`role="tab"`/`aria-selected`
- 折叠按钮缺少 `aria-expanded`
2. **焦点指示器不可见**:自定义按钮(如 `scan-theme-button`、`scan-ai-city-icon-button`)没有 focus-visible 样式
3. **颜色不是唯一的信息传达方式**:风险等级、Market decision 的色彩编码缺少对应的文字标签或图标补充
4. **没有 skip-to-content 链接**
5. **10-11px 小字体对视力障碍用户不友好**
---
## 总结与优先级建议
### 高优先级(影响用户体验和可维护性)
| # | 问题 | 位置 |
|---|------|------|
| 1 | `!important` 滥用导致浅色主题不可维护 | `ScanTerminalLightTheme.module.css` |
| 2 | 非标准 font-weight 值无效(760/850/950 等) | 多个 CSS Module |
| 3 | Geist 字体声明但未加载 | `layout.tsx:35-37` / `globals.css:55` |
| 4 | 键盘导航和 focus-visible 缺失 | 全局 |
| 5 | 断点碎片化(14 个断点值无体系) | 所有响应式 CSS |
### 中优先级(影响设计一致性)
| # | 问题 | 位置 |
|---|------|------|
| 6 | 22 个 CSS Module 堆叠耦合 | `ScanTerminalDashboard.tsx:143-159` |
| 7 | Token 未充分利用(硬编码色值) | 多个 CSS Module |
| 8 | `@keyframes spin` 重复定义 4 次 | 多个 CSS Module |
| 9 | Loading 状态设计不一致 | `DashboardMap` vs `ScanTerminalState` |
| 10 | 缺少 `prefers-reduced-motion` 支持 | 全局 |
### 低优先级(增强和优化)
| # | 问题 | 位置 |
|---|------|------|
| 11 | "Fintech 3-Color" 实际只有 2 色 | `globals.css:25-27` |
| 12 | Topbar 缺少 Logo/品牌标识 | `ScanTerminalDashboard.tsx` |
| 13 | 按钮视觉层级不够清晰 | 多个组件 |
| 14 | Shadcn UI 组件存在但未被使用 | `components/ui/` |
| 15 | `--color-text-muted` 对比度刚达标 | `globals.css:21` |
---
## 修复路线图建议
1. **Phase 1** — 修复 `font-weight` 无效值:全局搜索 `font-weight: 760`、`850`、`860`、`880`、`900`、`950`,替换为 Inter 支持的 300-800 等效值
2. **Phase 2** — 重构浅色主题:将分散在 8+ 个文件中的浅色覆盖集中到一个 `light-theme.css`,使用 CSS 变量覆盖而非 `!important`
3. **Phase 3** — 补充无障碍:为 Tab/Button/Card 组件添加 ARIA 属性和 focus-visible 样式
4. **Phase 4** — 统一断点:定义 4-5 个标准断点(如 480/768/1024/1280/1440),逐步替换现有碎片化断点
5. **Phase 5** — 设计一致性:创建统一的 loading/empty/error 组件,清理未使用的 shadcn 组件
---
## 修复完成记录
> 修复日期:2026-05-10 | 变更范围:28 个文件,+693 / 2,198 行
### 高优先级 — 5/5 完成
| # | 问题 | 修复 | 涉及文件 |
|---|------|------|----------|
| 1 | `!important` 滥用 | 134 → 49(仅保留 Leaflet/图表所必需项),将 `.root:global(.light)` 替换为 `html.light` 以获得更高优先级 | `ScanTerminalLightTheme.module.css`、`globals.css` 等 |
| 2 | 非标准 font-weight | 所有 760/850/860/880/950 等映射为 Inter 支持的 300800 | 13 个 CSS 文件 |
| 3 | 未加载 Geist | 从 `--font-display` 中移除,替换为 Inter | `globals.css` |
| 4 | 键盘 / 焦点可见 | 添加了全局 `:focus-visible` 轮廓环、跳过链接、Tab ARIA(`role="tablist"`/`role="tab"`/`aria-selected` | `globals.css`、`layout.tsx`、`ScanFilterPanel.tsx`、`ScanTerminalDashboard.tsx` |
| 5 | 断点碎片化 | 18 → 10:合并 520/600/720/820/900/1020/1100/1240 → 640/768/960/1024/1280 | 9 个 CSS 文件 |
### 中优先级 — 5/5 完成
| # | 问题 | 修复 | 涉及文件 |
|---|------|------|----------|
| 6 | 22 个 CSS Module 耦合 | 新建 `scan-root-styles.ts` 桶文件,将 22 个独立导入合并为 1 个预组合的 className | `scan-root-styles.ts`(新建)、`ScanTerminalDashboard.tsx` |
| 7 | Token 使用不足 | 将主要颜色(`#4DA3FF`/`#E6EDF3`/`#9FB2C7`/`#6B7A90`/`#6FB7FF`)从 0 个变量引用替换为数百个 | `ScanTerminalCard`、`ScanTerminalList`、`ScanTerminalBoard`、`ScanTerminalOpportunity`、`ScanTerminalMobile`、`ScanTerminal`、`DashboardHomeIntelligence` 等 |
| 8 | `@keyframes spin` 重复 4 次 | 移至 `globals.css``loading-spin` 去重 2 处;`pulse-pending` 移至全局 | `globals.css`、4 个 CSS 文件 |
| 9 | Loading 状态不一致 | 添加了 `.scan-error-state`、`.scan-retry-button`、`.scan-empty-icon` 用于统一状态呈现 | `ScanTerminalState.module.css`、`ScanTerminalLightTheme.module.css` |
| 10 | 无 `prefers-reduced-motion` | 在 `globals.css` 中添加了全局动画/过渡禁用 | `globals.css` |
### 低优先级 — 5/5 完成
| # | 问题 | 修复 | 涉及文件 |
|---|------|------|----------|
| 11 | "3-Color" 仅 2 种颜色 | 将 accent-primary(blue) 和 accent-secondary(light-blue) 区分为不同颜色 | `globals.css`、`Dashboard.module.css` |
| 12 | Topbar 缺少 Logo | 添加了 CSS 渐变品牌标记 | `ScanTerminalShell.module.css`、`ScanTerminalDashboard.tsx` |
| 13 | 按钮层级不清晰 | 在 CSS 中添加了文档化的层级注释标题 | `ScanTerminalShell.module.css` |
| 14 | Shadcn 未使用 | 已验证 6 个组件被 5 个文件使用(保留),更新了注释 | `globals.css` |
| 15 | 文字对比度不足 | `--color-text-muted` 从 `#6B7A90` 提升至 `#7D8FA3` | `globals.css` |
### 其他修复
| 问题 | 修复 |
|------|------|
| `accent-green` 类错误渲染为蓝色 | `ScanTerminal.module.css``.scan-condition-value.accent-green` 从 `#4DA3FF` 修正为 `#22C55E` |
| `Dashboard.module.css` 重复的 CSS 变量 | 将本地 `--bg-*`/`--accent-*` 变量桥接至全局 token |
| 死代码 | 移除 `public/static/style.css`1,459 行)和 `public/legacy/index.html`238 行)— 均未被引用 |
| 浅色主题 Token 基础设施 | 在 `globals.css` 中添加了 `html.light` CSS 自定义属性覆盖 |
| 品牌 Logo 浅色主题 | 在 `ScanTerminalLightTheme.module.css` 中添加了浅色主题 Logo 样式 |
| 空/错误状态浅色主题 | 在 `ScanTerminalLightTheme.module.css` 中添加了空/错误/重试的浅色覆盖 |
### 最终指标
| 指标 | 之前 | 之后 |
|------|------|------|
| `!important`(可避免项) | ~85 | 0 |
| `!important`(必需项) | ~49 | 49Leaflet 内联样式、图表 canvas 属性、减少动态效果) |
| 硬编码调色板颜色 | 数百个 | 0(仅 `globals.css` 中的变量定义) |
| 断点 | 18 个唯一值 | 10480/640/768/960/1024/1200/1280/1360/1440/1680 |
| 重复的 `@keyframes` | 7 | 0 |
| 非标准 font-weight | 全 13 个文件 | 0 |
| CSS Module 导入 | 22 个独立导入 | 2(桶文件 + 共享) |
| 死代码 | 1,697 行 | 0 |
| 净代码行数 | — | 1,505 行 |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 KiB

After

Width:  |  Height:  |  Size: 947 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
# PolyWeatherCheckout PolygonScan 验证(v1.5.1
# PolyWeatherCheckout PolygonScan 验证(v1.7.0
最后更新:`2026-03-20`
+70
View File
@@ -0,0 +1,70 @@
# PolyWeather 前端产品审查报告
> 审查日期:2026-06 | 视角:产品经理 | 范围:`frontend/` 全部页面、组件、用户流程
## 一、产品概览
PolyWeather 是一个面向天气衍生品交易者的气象情报平台。核心价值主张:**结合多模型气象预报 + AI 机场报文解读 + Polymarket 市场价格,为交易决策提供一站式证据链。**
### 产品分层
| 层级 | 功能 | 门槛 |
|------|------|------|
| 免费 | 交互式全球天气地图 + 城市简报 | 无需登录 |
| Pro 试用 | 3 天全功能 | 注册后自动获得 |
| Pro 订阅 | 城市决策卡(AI 机场报文 + 模型证据 + 市场层)、日内分析、历史对账、未来预报 | 10 USDC/月(积分抵扣最多 3 USDC |
### 页面结构(9 个路由)
| 路由 | 功能 | 是否必需登录 |
|------|------|-------------|
| `/` | 主看板 — AI 天气决策台 | 否 |
| `/account` | 账户中心 — 身份/订阅/钱包/积分/Bot 绑定 | 是 |
| `/auth/login` | 登录页(Google OAuth + 邮箱密码) | 否 |
| `/docs/[...slug]` | 产品文档中心(8 篇双语文档) | 否 |
| `/subscription-help` | 订阅 FAQ(双语) | 否 |
| `/entitlement-required` | 访问被拒页面 | 否 |
| `/ops` | 运营管理后台 | 是(管理员) |
| `/ops/truth-history` | 真值历史查看器 | 是(管理员) |
## 二、用户流程分析
### 主看板的两个视图
```
┌─ 分布视图(地图) ──────────────────────────────┐
│ Leaflet 交互式地图,城市彩色气泡 │
│ 点击城市 → 自动添加到决策卡工作区 + 切换到卡片视图 │
│ 免费用户和 Pro 用户均可使用 │
├─ 决策卡(分析) ──────────────────────────────┤
│ 钉选的城市卡片:AI 机场解读 + 模型集群 + 市场层 + 图表 │
│ 需要 Pro 订阅 │
└──────────────────────────────────────────────┘
└── 右侧栏:城市简报面板(始终可见,免费可用)
```
### 关键用户路径
1. **新用户落地** → 看到 3 步引导 → 地图 + 城市列表 → 点击城市 → 看到城市简报 → 想深入分析 → 遇到 Pro 付费墙(含功能说明)
2. **Pro 用户工作流** → 地图选城市 → 自动钉选到决策卡 → 展开卡片 → 阅读 AI 报文解读 → 查看市场层 → 判断交易方向
## 三、做得好的地方
1. **地图 → 决策卡的自动流转设计** — 点击地图城市自动钉选到分析工作区并切换视图,"零步骤发现"
2. **双语覆盖完整** — 所有 UI 文案、文档、AI 解读都有中英文对照,覆盖率接近 100%
3. **数据新鲜度可视化** — DataFreshnessBar 让用户一眼看到 METAR/模型/市场数据的新鲜度
4. **AI 解读的产品化程度高** — 分层展示:快速判断 → 完整解读 → 证据链 → 风险提示
5. **免费层有实际价值** — 地图 + 城市简报不是"空壳",用户可以看真实气象数据
6. **支付链路完整** — 从钱包绑定到链上签约到支付恢复,处理了多种异常情况
7. **空状态有引导文字** — "Click a city on the map" 告诉用户下一步做什么
8. **浅色/深色主题都有** — 两个主题都经过完整设计
9. **Ops 面板功能齐全** — 系统健康、转化漏斗、缓存状态、支付异常、用户管理一览无余
## 四、存在的问题与待办
| # | 问题 | 优先级 |
|---|------|------|
| 1 | **Docs 无搜索** — 8 篇文档没有搜索功能,用户必须逐篇浏览 | 🟢 待做 |
| 2 | **Ops 面板无审计日志** — 管理员补发积分等操作没有审计记录 | 🟢 需后端 |
| 3 | **注册后邮件验证引导** — 未验证邮箱的用户反复登录失败 | 🟡 需后端 |
@@ -0,0 +1,102 @@
# AMSC AWOS Runway Observation Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a China-only AMSC AWOS runway observation source and expose it as a runway observation tab next to Market Monitor.
**Architecture:** Backend fetches and normalizes AMSC `getWindPlate?cccc=...` payloads into the existing `amos`/`runway_obs` shape so current dashboard consumers can reuse runway display logic. Frontend adds a dedicated `runway` scan terminal tab that fetches a domestic city whitelist and renders runway TDZ/MID/END air temperatures without changing settlement anchors.
**Tech Stack:** Python data collection + pytest, Next.js/React TypeScript, existing business-state test runner.
---
### Task 1: Backend parser and source
**Files:**
- Create: `src/data_collection/amsc_awos_sources.py`
- Create: `tests/test_amsc_awos_sources.py`
- Modify: `src/data_collection/weather_sources.py`
- Modify: `src/data_collection/country_networks.py`
- [ ] **Step 1: Write failing parser tests**
Add tests that import `_amsc_parse_wind_plate_payload`, `_amsc_supported_city_codes`, and `AmscAwosSourceMixin`, parse a ZBAA-style sample, assert runway point temperatures, UTC observation conversion, `runway_temp_range`, and unauthorized/no-data fallback.
- [ ] **Step 2: Run red test**
Run: `python -m pytest tests/test_amsc_awos_sources.py -q`
Expected: FAIL because `src.data_collection.amsc_awos_sources` does not exist.
- [ ] **Step 3: Implement minimal backend source**
Create a source module with China whitelist: `shanghai=ZSPD`, `beijing=ZBAA`, `guangzhou=ZGGG`, `shenzhen=ZGSZ`, `chengdu=ZUUU`, `chongqing=ZUCK`, `wuhan=ZHHH`, `qingdao=ZSQD`. Fetch `https://www.amsc.net.cn/gateway/api/saas/rest/amc/AwosController/getWindPlate?cccc=<ICAO>`, optionally using `POLYWEATHER_AMSC_COOKIE` or `POLYWEATHER_AMSC_SESSION_ID`, and return existing-compatible `amos` payload with `source="amsc_awos"`.
- [ ] **Step 4: Run green backend tests**
Run: `python -m pytest tests/test_amsc_awos_sources.py tests/test_amos_station_sources.py -q`
Expected: PASS.
### Task 2: Backend integration
**Files:**
- Modify: `src/data_collection/weather_sources.py`
- Modify: `src/data_collection/country_networks.py`
- [ ] **Step 1: Attach AMSC after AMOS**
Add `AmscAwosSourceMixin` to `WeatherDataCollector`, call `_attach_china_amsc_awos_data` in both Open-Meteo and fallback paths, and persist aggregate plus first runway rows to `airport_obs_log` like AMOS.
- [ ] **Step 2: Normalize airport primary source labels**
Teach `_airport_primary_from_raw` that `raw["amos"].source == "amsc_awos"` should use `source_code="amsc_awos"`, `source_label="AMSC AWOS"`.
- [ ] **Step 3: Compile check**
Run: `python -m py_compile src/data_collection/amsc_awos_sources.py src/data_collection/weather_sources.py src/data_collection/country_networks.py`
Expected: exit 0.
### Task 3: Frontend runway tab
**Files:**
- Create: `frontend/components/dashboard/scan-terminal/RunwayObservationsPanel.tsx`
- Create: `frontend/components/dashboard/scan-terminal/__tests__/runwayObservationTab.test.ts`
- Modify: `frontend/components/dashboard/scan-terminal/ScanTerminalShellParts.tsx`
- Modify: `frontend/components/dashboard/ScanTerminalDashboard.tsx`
- Modify: `frontend/lib/dashboard-types.ts`
- Modify: `frontend/components/dashboard/monitoring/monitor-temperature.ts`
- Modify: `frontend/components/dashboard/monitoring/MonitorPanel.tsx`
- [ ] **Step 1: Write failing frontend business-state test**
Add a source-scan test asserting `ScanTerminalContentView` includes `runway`, dashboard has a `跑道观测` tab, and the panel includes `AMSC AWOS` plus TDZ/MID/END labels.
- [ ] **Step 2: Run red frontend test**
Run: `cd frontend; npm run test:business`
Expected: FAIL because the runway tab/panel strings do not exist yet.
- [ ] **Step 3: Implement tab and panel**
Add `runway` view next to Monitor. The panel fetches domestic whitelist details with `ensureCityDetail(key, false, "panel")`, displays city cards with runway rows and local-time labels, and uses a not-available message for cities without AMSC data.
- [ ] **Step 4: Run green frontend checks**
Run: `cd frontend; npm run test:business; npm run typecheck`
Expected: PASS.
### Task 4: Final verification and publish
**Files:**
- All changed files from Tasks 1-3.
- [ ] **Step 1: Full verification**
Run backend tests, Python compile, frontend business tests, typecheck, and build.
- [ ] **Step 2: Completion audit**
Map user requirement “国内几个城市的机场跑道温度,放在市场监控旁边 Tab” to changed backend source, frontend tab, tests, and build evidence.
- [ ] **Step 3: Commit/push/deploy**
If verification passes, commit, push `main`, and rely on configured deployment.
@@ -0,0 +1,61 @@
# Telegram Group Pricing Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
**Goal:** Add Telegram Login verification so backend decides Pro price: group member 5U, non-member 10U.
**Architecture:** Keep Supabase as the website account session, add Telegram Login as an identity link/price verification step. Backend verifies Telegram Login hash, checks getChatMember, stores the Telegram link in existing supabase_bindings, and payment intent creation recalculates price server-side.
**Tech Stack:** FastAPI, existing DBManager bindings, Telegram Bot HTTP API, Next.js proxy routes, React account center, pytest.
---
### Task 1: Telegram auth service
**Files:**
- Create: src/auth/telegram_group_pricing.py
- Test: ests/test_telegram_group_pricing.py
- [ ] Verify Telegram Login payload HMAC using bot token.
- [ ] Call Telegram getChatMember and treat member, dministrator, creator as group members.
- [ ] Return 5U/10U pricing payload.
### Task 2: Backend auth route
**Files:**
- Modify: web/core.py
- Modify: web/services/auth_api.py
- Modify: web/routers/auth.py
- [ ] Add TelegramLoginRequest model.
- [ ] Add POST /api/auth/telegram/login requiring Supabase identity.
- [ ] Link Telegram id to current Supabase user via DBManager.bind_supabase_identity.
- [ ] Return Telegram member status and effective price.
### Task 3: Payment dynamic price
**Files:**
- Modify: src/payments/contract_checkout.py
- Modify: web/services/payment_api.py
- [ ] At payment intent creation, check linked Telegram id and group membership.
- [ ] Override pro_monthly amount to 5U for members, 10U otherwise.
- [ ] Store pricing source in metadata.
### Task 4: Frontend Telegram Login entry
**Files:**
- Modify: rontend/components/account/AccountCenter.tsx
- Create: rontend/app/api/auth/telegram/login/route.ts
- [ ] Load Telegram Login widget with configured bot username.
- [ ] Send payload to backend proxy.
- [ ] Show group/member price status before checkout.
### Task 5: Verification
**Commands:**
- python -m pytest tests\test_telegram_group_pricing.py tests\test_direct_payment.py tests\test_payments_runtime.py -q
- python -m py_compile src\auth\telegram_group_pricing.py src\payments\contract_checkout.py web\core.py web\services\auth_api.py
- python -m ruff check src\auth\telegram_group_pricing.py src\payments\contract_checkout.py web\core.py web\services\auth_api.py tests\test_telegram_group_pricing.py
- cd frontend; npm run typecheck
+189
View File
@@ -0,0 +1,189 @@
# PolyWeather UX 研究员审查报告
> 审查日期:2026-06 | 视角:UX 研究员 | 范围:用户理解修正逻辑、第一眼认知、图表误导风险、普通用户语言、天气异常体验
## 一、修正逻辑:用户是否看懂"上修/下修/维持"
### 当前状态
AI 后端提示词明确要求 AI 使用**上修 / 下修 / 维持**三个方向词(`scan_city_ai_prompt.py:47-51`)。但前端 `WeatherDecisionBand` 完全不用这三个词,而是用:
| AI 判断方向 | 前端实际展示 | 中文原文 |
|------------|------------|---------|
| 上修(偏暖) | "Watch hotter range" | "关注偏高温区间" |
| 下修(偏冷) | "Avoid chasing high" | "暂不追高温" |
| 维持(中性) | "Wait for peak-window confirmation" | "等待峰值窗口确认" |
### 问题
| # | 问题 | 严重度 |
|---|------|------|
| 1 | **AI 输出与前端展示词汇不一致** — AI 用"上修/下修/维持",用户看到的是"关注偏高温/暂不追/等待确认"。这是两套完全不同的语言体系,用户读完 AI 解读再看决策条可能对不上号 | 🔴 |
| 2 | **没有"维持/不变"标签** — 中性状态被表述为动作("等待确认"),而不是状态("维持不变")。用户想知道"现在是什么判断"而不是"现在该做什么" | 🟡 |
| 3 | **"偏高温区间" vs "暂不追高温" 不对称** — 上修方向指向一个温度区间,下修方向指向一个行为。一个说 where,一个说 what。逻辑结构不一致 | 🟡 |
| 4 | **颜色语义双重解读** — warm=红色边框=偏暖(上修),cold=绿色边框=偏冷(下修)。天气直觉是"热=红、冷=蓝/绿",但金融直觉是"红=跌、绿=涨"。两类用户可能得出相反的解读 | 🟡 |
### 建议
统一词汇体系,前端和 AI 使用同一套语言:
| 方向 | 建议前端标签 | 建议说明 |
|------|------------|---------|
| 上修 | **"预计最高温上修"** / "Revise upward" | 比 DEB/模型集群基准偏高 |
| 下修 | **"预计最高温下修"** / "Revise downward" | 比 DEB/模型集群基准偏低 |
| 维持 | **"维持模型基准"** / "Stay with model base" | 无需显著上修或下修 |
---
## 二、第一眼理解:用户打开卡片看到什么?
### 视觉层级
```
1. CityCardHeader
├─ Kicker: "城市深度分析" / "Deep analysis"
├─ 城市名(大号标题)
├─ 状态标签(实测突破 / METAR 过旧 / 模型高度一致 等)
├─ 数据新鲜度条(METAR / 模型 / 市场 / AI 各自的新鲜度)
└─ 三指标:当前温度 | 预计最高温 | 峰值时间
2. WeatherDecisionBand(修正条)
├─ Kicker: "天气优先判断 · 市场价格另列"
├─ 主体判断(大号粗体): "关注偏高温区间" / "暂不追高温" / "等待峰值窗口确认"
├─ 原因文字(高亮 pill 内)
└─ 三指标:天气区间 | 路径偏差 | 报价状态
3. 图表 + AI 证据 + 模型证据
```
### 问题
| # | 问题 | 严重度 |
|---|------|------|
| 5 | **Kicker 说了两遍"市场"** — Header 的 kicker 是"城市深度分析"Decision band 的 kicker 是"天气优先判断 · 市场价格另列"。两个 kicker 都提到了市场,但新手不知道"市场"指的是 Polymarket 温度合约。这个词对圈外人完全无意义 | 🟡 |
| 6 | **"预计最高温"没有解释来源** — 用户看到这个数字,不知道它是 AI 独立判断的、还是 DEB 融合的、还是某一个模型给的。只有一个数字,没有可信度标签 | 🟡 |
| 7 | **状态标签过多** — 一个卡片可能同时显示 3-4 个标签:实测突破 + 峰值窗口已过 + METAR 过旧 + 模型高度一致。这些标签颜色不同、含义各异,用户需要逐一解码 | 🟢 |
---
## 三、图表是否误导?
### 日内温度图表(`AiCityTemperatureChart`
三条线:
- 灰色虚线:DEB 原始路径(过去+未来都是虚线)
- 蓝色实线:METAR 修正路径
- 绿色散点:METAR 实测
### 问题
| # | 问题 | 严重度 |
|---|------|------|
| 8 | **DEB 路径永远是虚线** — 即使过去部分(已发生的几小时)也是虚线。虚线在图形语言中普遍表示"不确定/预测",但过去几小时 DEB 已经是根据已知观测计算的,不应该看起来不确定 | 🟡 |
| 9 | **没有"现在"标记** — 图表上没有任何竖线或标记指示当前时间。用户不知道图表上哪里是"现在",哪里是"未来" | 🔴 |
| 10 | **X 轴标签稀疏** — 每 4 个小时才显示一个标签,最多 6 个。用户可能以为数据只在标记的小时上有,实际上每个小时都有数据 | 🟢 |
| 11 | **HistoryChart 缺 °C/°F** — 历史图 tooltip 只显示 `°`,没有 `C``F`。在混合温度单位的环境下可能混淆 | 🟡 |
| 12 | **没有轴标题** — Y 轴只有数字 + °C,没有"温度"标签。对新手来说不够自解释(虽然常见于仪表盘类产品) | 🟢 |
### 建议
| 建议 | 实现 |
|------|------|
| 添加"现在"竖线 | `chart-utils.ts` 中在 `currentIndex` 位置画一条 annotation line |
| 过去 DEB 改为实线 | `segment: { borderDash: past=[], future=[6,4] }` 给过去和未来不同样式 |
| HistoryChart tooltip 补全单位 | 使用 `data.temp_symbol` 替代硬编码 `°` |
---
## 四、普通用户能看懂多少?
### 高频出现的专业术语
| 术语 | 出现次数 | 用户理解难度 | 是否有解释 |
|------|---------|------------|----------|
| **METAR** | ~50+ 处 | 高 — 只有飞行员/气象人员知道 | ❌ 无 |
| **DEB** | ~30+ 处 | 高 — 项目内部术语 | ❌ 无 |
| **TAF** | ~15 处 | 高 — 航空术语 | ❌ 无 |
| **峰值窗口** | ~20 处 | 中 — 可推测含义 | ❌ 无 |
| **模型集群** | ~10 处 | 中 — 可推测含义 | ❌ 无 |
| **边界层** | 3 处 | 极高 — 气象学专业术语 | ❌ 无 |
| **冷平流/暖平流** | 2 处 | 极高 — 气象学专业术语 | ❌ 无 |
| **中枢** | ~8 处 | 中 — 在这个语境下表示中心值 | ❌ 无 |
### 问题
| # | 问题 | 严重度 |
|---|------|------|
| 13 | **所有专业术语都没有 tooltip 或解释** — METAR、DEB、TAF 在全站出现数十次,没有任何地方解释它们是什么。用户的唯一学习途径是 `/docs` 页面,但需要主动离开看板去查阅 | 🔴 |
| 14 | **AI 输出的"最终判断"字段直接暴露给用户** — 如果 AI 提到了"冷平流支撑"、"边界层逆温"等术语,前端不做任何改写或解释,直接原样展示。用户要么读懂,要么跳过 | 🟡 |
| 15 | **"DEB 融合"对普通用户完全无意义** — 这是一个内部算法名称。用户需要的是"综合预报"或"多模型加权平均",而不是一个缩写 | 🟡 |
### 建议
| 建议 | 实现 |
|------|------|
| 核心术语加 tooltip | 首次出现的 METAR/DEB/TAF 加 `title` 属性或悬浮解释 |
| DEB 改用用户语言 | "DEB 融合" → "多模型综合预报" 或保留 DEB 但加括号说明 |
| AI 术语过滤 | 在后端 `scan_city_ai_fallback.py` 或前端展示层过滤掉过于专业的术语 |
---
## 五、天气异常时的体验
### 当前异常信号和用户看到的反馈
| 异常事件 | 前端展示 | 评估 |
|---------|---------|------|
| 实测温度突破模型上沿 | 红色标签"实测突破" + "Observation has broken above the model range" | ✅ 清晰 |
| METAR 数据过旧(超过一天) | 黄色标签"METAR 过旧" + "已过旧,仅作背景参考" + 数据新鲜度条标红 | ✅ 清晰 |
| 峰值窗口已过 | 灰色标签"峰值窗口已过" + 温度图表可能显示下降趋势 | ⚠️ 图表不标注窗口起止 |
| 模型数据不足(<2个模型) | "等待模型补齐" | ⚠️ 没说为什么模型少、什么时候能补上 |
| 市场价格不可用 | "市场价暂不可用" + "天气证据可参考,但暂无可交易价格" | ✅ 清晰 |
| 快速判断与完整解读不一致 | 先显示"快速判断已完成",再异步合并完整 AI 解读 | ⚠️ 两种状态切换可能让用户困惑 |
### 问题
| # | 问题 | 严重度 |
|---|------|------|
| 16 | **异常没有"下一步"指引** — 当实测突破模型上沿时,用户看到"Observation has broken above the model range",但不知道这意味着该做什么。是应该买入?卖出?等待?系统不给建议 | 🔴 |
| 17 | **"快速判断"和"完整解读"的切换可能造成 flicker** — 卡片先显示快速判断文案,然后完整 AI 返回后替换。如果两者结论一致,用户感知不到变化;如果不一致,用户会困惑"刚才不是这么说的" | 🟡 |
| 18 | **极端天气没有特别处理** — 如果城市出现 40°C+ 或 -10°C 以下的极端温度,卡片和普通温度展示方式完全一样,没有视觉强调 | 🟢 |
### 建议
| 建议 | 实现 |
|------|------|
| 异常加行动建议 | 在 `primaryReason` 后追加一句行动建议("建议等待下一报文后再做判断" / "建议关注更高温区间" |
| 极端温度视觉强化 | 温度超过历史极值时加大字号或添加红色脉冲高亮 |
| 快速→完整过渡加标记 | 文案更新时显示 "✓ 已更新" 小标记,避免用户以为信息没变 |
---
## 六、优先级总结
### P0 — 用户理解障碍
| # | 问题 | 建议 |
|---|------|------|
| 1 | AI 用"上修/下修",前端用"偏高温/暂不追" | 统一为"上修/下修/维持"三词 |
| 9 | 图表没有"现在"标记 | 在 currentIndex 位置画竖线 |
| 13 | METAR/DEB/TAF 全站无解释 | 加 title tooltip + 首次出现时加括号说明 |
| 16 | 异常没有行动建议 | 在 primaryReason 后追加引导文字 |
### P1 — 体验提升
| # | 问题 | 建议 |
|---|------|------|
| 3 | 上修/下修文案不对称 | 统一用"方向 + 幅度"格式 |
| 4 | 红/绿颜色双重解读 | 保留但加文字标签确认 |
| 8 | DEB 路径全是虚线 | 过去部分用实线,未来部分用虚线 |
| 15 | "DEB 融合"对普通用户无意义 | 改为"多模型综合预报" |
### P2 — 优化打磨
| # | 问题 | 建议 |
|---|------|------|
| 11 | HistoryChart 缺 °C/°F | 使用 temp_symbol |
| 12 | 图表无轴标题 | 可加可不加(仪表盘惯例) |
| 17 | 快速→完整 flicker | 加过渡标记 |
| 18 | 极端温度无强调 | 加视觉强化 |
+11 -10
View File
@@ -1,12 +1,12 @@
# PolyWeather Side Panel
`PolyWeather Side Panel` 是一个面向天气交易场的 Chrome / Edge 浏览器侧边栏工具。
`PolyWeather Side Panel` 是一个面向天气交易场的 Chrome / Edge 浏览器侧边栏工具。
## 功能
1. 自动识别当前 Polymarket 页面中的城市,也支持手动切换。
2. 展示城市档案:结算站点、站点距离、观测更新时间、周边站点数量。
3. 展示今日日内走势(简版):`DEB` 走势与官方观测`METAR / HKO / CWA / NOAA`对照,可悬停查看时间与温度。
3. 展示今日日内走势(简版):`DEB` 走势与机场/官方观测对照,可悬停查看时间与温度。
4. 展示多日最高温预报(简版),当前以 `DEB` 优先。
5. 支持一键刷新,强制拉取最新温度数据。
6. 支持本地缓存,提升打开速度;插件版本更新时会刷新城市列表缓存。
@@ -15,8 +15,9 @@
## 数据说明
- 香港使用 `HKO`(香港天文台)结算源。
- 其他城市按配置使用 `METAR / NOAA / 官方数据源`
- 其他城市按配置使用 `METAR / NOAA / AMOS / JMA / MGM / FMI / KNMI`官方数据源。
- 城市展示名以主站返回值为准,例如 `aurora` 市场在插件中会显示为 `Denver`
- 首尔/釜山/东京/安卡拉/伊斯坦布尔/赫尔辛基/阿姆斯特丹已接入高频机场实时数据(1-10 分钟级)。
## 权限说明
@@ -33,14 +34,14 @@
1. 打开 Chrome/Edge 扩展页面:
- Chrome`chrome://extensions`
- Edge`edge://extensions`
2. 打开开发者模式
3. 选择加载已解压的扩展程序
2. 打开"开发者模式"
3. 选择"加载已解压的扩展程序"
4. 选择目录:`extension/`
5. 点击扩展图标,侧边栏会打开。
## 设置
首次建议打开扩展选项页并确认:
首次建议打开扩展"选项页"并确认:
- `网站基础地址`:你的前端域名(例如 `https://polyweather-pro.vercel.app`
- `API 基础地址`:你的后端 API 域名(若同域也可填前端域名)
@@ -48,9 +49,9 @@
## 说明
- 当前版本仍是轻量产品,重点是监控 + 基础判断 + 导流回站,未接入支付链路。
- 当前版本仍是轻量产品,重点是"监控 + 基础判断 + 导流回站",未接入支付链路。
- 若你的 API 做了严格鉴权,请先在设置页填写 token 再使用。
- 插件城市列表来自主站 `/api/cities`不是插件内置静态列表;本版已升级缓存版本,安装更新后会重新拉取 Manila、Karachi、Masroor Air Base 等最新城市。
- 插件走势图与主站保持一致:`Wunderground / weather.com` 是历史参考页,不是物理实测站;机场市场统一显示机场 `METAR` / 官方观测点位。
- 点击打开网站查看更多会回到主站继续查看完整分析。
- 插件城市列表来自主站 `/api/cities`内置静态列表;安装更新后自动刷新最新城市。
- 机场市场统一显示机场 `METAR` / 官方观测点位,部分城市已覆盖 1 分钟级实时数据
- 点击"打开网站查看更多"会回到主站继续查看完整分析。
- 插件不会承载完整分析;完整结构判断、历史对账和更多信号仍以主站为准。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 B

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "PolyWeather Side Panel",
"description": "Weather side panel for Polymarket.",
"version": "0.1.10",
"version": "0.1.11",
"icons": {
"16": "icon-16.png",
"32": "icon-32.png",
-10
View File
@@ -1,10 +0,0 @@
> polyweather-frontend@1.5.4 start
> next start -p 3002
▲ Next.js 15.5.12
- Local: http://localhost:3002
- Network: http://172.23.64.1:3002
✓ Starting...
✓ Ready in 541ms
+1
View File
@@ -40,3 +40,4 @@ NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS=polyweather-pro.vercel.app
POLYWEATHER_OPS_ADMIN_EMAILS=yhrsc30@gmail.com
NEXT_PUBLIC_TELEGRAM_GROUP_URL=https://t.me/your_group
NEXT_PUBLIC_TELEGRAM_BOT_URL=https://t.me/WeatherQuant_bot
NEXT_PUBLIC_TELEGRAM_LOGIN_BOT_USERNAME=WeatherQuant_bot
+42
View File
@@ -1 +1,43 @@
# PolyWeather 前端最小配置(本地 / Vercel)
# 只部署天气看板时,先填下面 4 项即可。
# 必填:后端 FastAPI 基础地址
# 默认供 Next.js API Route 在服务端代理后端使用。
POLYWEATHER_API_BASE_URL=http://127.0.0.1:8000
# 可选:浏览器直连后端 FastAPI 基础地址。
# 在 Vercel 免费额度下建议配置为 VPS HTTPS 域名,让 AI / METAR / scan 等
# 长耗时请求绕过 Vercel Functions / Fluid Compute。
# 例如:https://api.example.com
NEXT_PUBLIC_POLYWEATHER_API_BASE_URL=
# 必填:Supabase 前端公钥(鉴权开启时必须)
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
# 常用:前端鉴权开关
# true: 启用 Supabase 登录
# false: 关闭登录能力,访客模式
POLYWEATHER_AUTH_ENABLED=false
# 常用:是否强制登录
# true: middleware 强制登录后才能访问主页面
# false: 登录可选,访客可浏览
POLYWEATHER_AUTH_REQUIRED=false
# 可选:分享式看板访问令牌
# 设置后,可通过 /?access_token=<token> 打开受保护看板
POLYWEATHER_DASHBOARD_ACCESS_TOKEN=
# 可选:前端 API Route 转发到后端时附带的共享令牌
# 仅当后端启用了 entitlement / 订阅校验时需要
POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN=
# 可选:钱包支付 / Telegram 入口
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com
NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS=polyweather-pro.vercel.app
POLYWEATHER_OPS_ADMIN_EMAILS=yhrsc30@gmail.com
NEXT_PUBLIC_TELEGRAM_GROUP_URL=https://t.me/your_group
NEXT_PUBLIC_TELEGRAM_BOT_URL=https://t.me/WeatherQuant_bot
NEXT_PUBLIC_TELEGRAM_LOGIN_BOT_USERNAME=WeatherQuant_bot
+2 -6
View File
@@ -21,8 +21,8 @@ PolyWeather Pro 的生产前端工程。
## 当前前端能力
- 主站 Dashboard 支持地图、城市详情、今日日内分析、历史准确率对账和账户中心
- `/docs` 已提供公开双语产品文档中心,解释日内分析、校准概率、模型栈、TAF结算来源和历史对账
- 主站 Dashboard 支持地图、城市详情、今日日内分析和账户中心
- `/docs` 已提供公开双语产品文档中心,解释日内分析、校准概率、模型栈、TAF结算来源
- 今日日内分析支持:
- `锚点状态`
- `当前节奏`
@@ -31,9 +31,6 @@ PolyWeather Pro 的生产前端工程。
- `专业气象结论条`
- `气象证据链 / 失效条件 / 确认条件`
- 非香港机场城市的 `TAF` 时段提示与走势图联动
- 历史对账支持:
- `DEB / 最佳单模型 / 实测最高温` 对比
- 峰值前 12 小时 `DEB` 参考(近似)
- `/ops` 已支持桌面表格 + 手机端卡片化视图
- 点击城市图标后会显示地图顶部同步提醒与详情面板内同步徽标,避免用户误判为卡住
- 城市详情会自动识别“单模型 / 单日”的稀疏缓存并主动刷新,避免误把残缺 detail 当作完整结果
@@ -117,7 +114,6 @@ NEXT_PUBLIC_POLYWEATHER_EAGER_CITY_SUMMARIES=false
- `GET /api/city/[name]`
- `GET /api/city/[name]/summary`
- `GET /api/city/[name]/detail`
- `GET /api/history/[name]`
鉴权:
+91
View File
@@ -0,0 +1,91 @@
"use client";
import { RefreshCw } from "lucide-react";
import { useEffect } from "react";
export default function AccountErrorPage({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error("Account page error:", error);
}, [error]);
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
padding: "2rem",
gap: "1rem",
backgroundColor: "var(--color-bg-base, #0B1220)",
color: "var(--color-text-primary, #E6EDF3)",
fontFamily: "var(--font-data, Inter, sans-serif)",
textAlign: "center",
}}
>
<h1
style={{
fontSize: "1.25rem",
fontWeight: 600,
margin: 0,
color: "var(--color-accent-primary, #4DA3FF)",
}}
>
</h1>
<p
style={{
color: "var(--color-text-secondary, #9FB2C7)",
fontSize: "0.875rem",
margin: 0,
maxWidth: 420,
lineHeight: 1.7,
}}
>
MetaMask
Rabby
</p>
<p
style={{
color: "var(--color-text-muted, #7D8FA3)",
fontSize: "0.8rem",
margin: 0,
maxWidth: 420,
lineHeight: 1.6,
}}
>
If this happened during payment or wallet binding, the most common cause is
conflicting wallet extensions. Try disabling other wallet extensions (e.g.
MetaMask + Rabby) and refresh.
</p>
<button
type="button"
onClick={reset}
style={{
marginTop: "0.5rem",
display: "inline-flex",
alignItems: "center",
gap: "0.5rem",
padding: "0.5rem 1.25rem",
borderRadius: "var(--radius-md, 10px)",
border: "1px solid var(--color-border-default, rgba(159,178,199,0.16))",
backgroundColor: "var(--color-bg-raised, #111A2E)",
color: "var(--color-accent-primary, #4DA3FF)",
cursor: "pointer",
fontSize: "0.875rem",
fontWeight: 500,
}}
>
<RefreshCw size={14} />
</button>
</div>
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 542 KiB

After

Width:  |  Height:  |  Size: 542 KiB

+4 -2
View File
@@ -10,7 +10,7 @@ import {
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
const ANALYTICS_ENABLED =
process.env.NEXT_PUBLIC_POLYWEATHER_APP_ANALYTICS === "true";
process.env.NEXT_PUBLIC_POLYWEATHER_APP_ANALYTICS !== "false";
export async function POST(req: NextRequest) {
if (!ANALYTICS_ENABLED) {
@@ -26,7 +26,9 @@ export async function POST(req: NextRequest) {
try {
const body = await req.json();
const auth = await buildBackendRequestHeaders(req);
const auth = await buildBackendRequestHeaders(req, {
includeSupabaseIdentity: false,
});
const headers = new Headers(auth.headers);
headers.set("Content-Type", "application/json");
const res = await fetch(`${API_BASE}/api/analytics/events`, {
@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
export async function POST(req: NextRequest) {
if (!API_BASE) {
return NextResponse.json(
{ error: "POLYWEATHER_API_BASE_URL is not configured" },
{ status: 500 },
);
}
try {
const body = await req.text();
const auth = await buildBackendRequestHeaders(req);
const headers = new Headers(auth.headers);
headers.set("Content-Type", "application/json");
const res = await fetch(`${API_BASE}/api/auth/telegram/bind-by-token`, {
method: "POST",
headers,
body,
cache: "no-store",
});
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw);
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data);
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
return buildProxyExceptionResponse(error, {
publicMessage: "Failed to bind Telegram account",
});
}
}
@@ -7,33 +7,22 @@ import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
export async function GET(
req: NextRequest,
context: { params: Promise<{ name: string }> },
) {
export async function POST(req: NextRequest) {
if (!API_BASE) {
const response = NextResponse.json(
return NextResponse.json(
{ error: "POLYWEATHER_API_BASE_URL is not configured" },
{ status: 500 },
);
return response;
}
const { name } = await context.params;
const url = `${API_BASE}/api/history/${encodeURIComponent(name)}`;
try {
const auth = await buildBackendRequestHeaders(req);
const fetchOptions = {
const res = await fetch(`${API_BASE}/api/auth/telegram/bot-bind-link`, {
method: "POST",
headers: auth.headers,
next: { revalidate: 60 },
} as const;
const res = await fetch(url, {
...fetchOptions,
cache: "no-store",
});
if (!res.ok) {
const raw = await res.text();
@@ -41,16 +30,11 @@ export async function GET(
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = buildCachedJsonResponse(
req,
data,
"public, max-age=0, s-maxage=60, stale-while-revalidate=300",
);
const response = NextResponse.json(data);
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const response = buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch history",
return buildProxyExceptionResponse(error, {
publicMessage: "Failed to create Telegram bot bind link",
});
return response;
}
}
@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
export async function POST(req: NextRequest) {
if (!API_BASE) {
return NextResponse.json(
{ error: "POLYWEATHER_API_BASE_URL is not configured" },
{ status: 500 },
);
}
try {
const body = await req.text();
const auth = await buildBackendRequestHeaders(req);
const headers = new Headers(auth.headers);
headers.set("Content-Type", "application/json");
const res = await fetch(`${API_BASE}/api/auth/telegram/login`, {
method: "POST",
headers,
body,
cache: "no-store",
});
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw);
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data);
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
return buildProxyExceptionResponse(error, {
publicMessage: "Failed to verify Telegram login",
});
}
}
+7 -36
View File
@@ -1,16 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
import { buildCachedJsonResponse } from "@/lib/http-cache";
import { proxyBackendJsonGet } from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
if (!API_BASE) {
@@ -21,30 +12,10 @@ export async function GET(req: NextRequest) {
return response;
}
try {
const auth = await buildBackendRequestHeaders(req, {
includeSupabaseIdentity: false,
});
const res = await fetch(`${API_BASE}/api/cities`, {
headers: auth.headers,
cache: "no-store",
});
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw);
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = buildCachedJsonResponse(
req,
data,
"no-store, max-age=0",
);
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const response = buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch cities",
});
return response;
}
return proxyBackendJsonGet(req, {
cacheControl: "public, max-age=0, s-maxage=60, stale-while-revalidate=300",
publicMessage: "Failed to fetch cities",
revalidateSeconds: 60,
url: `${API_BASE}/api/cities`,
});
}
+11 -28
View File
@@ -1,12 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
import { proxyBackendJsonGet } from "@/lib/api-proxy";
import { buildCityDetailProxyCachePolicy } from "@/lib/proxy-cache-policy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -24,6 +18,7 @@ export async function GET(
const { name } = await context.params;
const forceRefresh = req.nextUrl.searchParams.get("force_refresh") ?? "false";
const cachePolicy = buildCityDetailProxyCachePolicy(forceRefresh, 15);
const depth = req.nextUrl.searchParams.get("depth");
const marketSlug = req.nextUrl.searchParams.get("market_slug");
const targetDate = req.nextUrl.searchParams.get("target_date");
@@ -41,24 +36,12 @@ export async function GET(
}
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/detail?${searchParams.toString()}`;
try {
const auth = await buildBackendRequestHeaders(req);
const res = await fetch(url, {
headers: auth.headers,
cache: "no-store",
});
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw);
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data);
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const response = buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch city detail aggregate",
});
return response;
}
return proxyBackendJsonGet(req, {
cacheControl: cachePolicy.responseCacheControl,
fetchCache:
cachePolicy.fetchMode === "no-store" ? "no-store" : undefined,
publicMessage: "Failed to fetch city detail aggregate",
revalidateSeconds: cachePolicy.revalidateSeconds,
url,
});
}
@@ -1,12 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
import { proxyBackendJsonGet } from "@/lib/api-proxy";
import { buildForceRefreshProxyCachePolicy } from "@/lib/proxy-cache-policy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -26,6 +20,7 @@ export async function GET(
const params = new URLSearchParams();
const forceRefresh = req.nextUrl.searchParams.get("force_refresh") ?? "false";
params.set("force_refresh", forceRefresh);
const cachePolicy = buildForceRefreshProxyCachePolicy(forceRefresh, 20);
const targetDate = req.nextUrl.searchParams.get("target_date");
if (targetDate) {
@@ -44,32 +39,15 @@ export async function GET(
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/market-scan?${params.toString()}`;
try {
const auth = await buildBackendRequestHeaders(req);
const res = await fetch(url, {
headers: auth.headers,
cache: "no-store",
});
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw, {
detailLimit: 800,
error: "Backend city market scan failed",
});
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data, {
headers: {
"Cache-Control": "no-store",
},
});
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const response = buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch city market scan",
status: 502,
});
return response;
}
return proxyBackendJsonGet(req, {
cacheControl: cachePolicy.responseCacheControl,
detailLimit: 800,
error: "Backend city market scan failed",
fetchCache:
cachePolicy.fetchMode === "no-store" ? "no-store" : undefined,
publicMessage: "Failed to fetch city market scan",
revalidateSeconds: cachePolicy.revalidateSeconds,
statusOnException: 502,
url,
});
}
+34 -10
View File
@@ -7,6 +7,8 @@ import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
import { buildCachedJsonResponse } from "@/lib/http-cache";
import { buildCityDetailProxyCachePolicy } from "@/lib/proxy-cache-policy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -64,6 +66,9 @@ function buildFallbackCityDetail(name: string, depth: string, summary: Record<st
sunshine_hours: null,
},
multi_model: {},
multi_model_daily: {},
source_forecasts: {},
hourly: { times: [], temps: [], radiation: [] },
probabilities: {
mu: null,
distribution: [],
@@ -104,6 +109,12 @@ function buildFallbackCityDetail(name: string, depth: string, summary: Record<st
function normalizeCityDetailPayload(data: unknown) {
if (!data || typeof data !== "object") return data;
const payload = data as Record<string, any>;
// Backend v2 nests hourly under timeseries; chart expects it at top level.
if (!payload.hourly && payload.timeseries?.hourly) {
payload.hourly = payload.timeseries.hourly;
}
if (!payload.market_scan && payload.market_scan_payload) {
return {
...payload,
@@ -128,29 +139,38 @@ export async function GET(
const { name } = await context.params;
const forceRefresh = req.nextUrl.searchParams.get("force_refresh") ?? "false";
const depth = req.nextUrl.searchParams.get("depth") ?? "panel";
const cachePolicy = buildCityDetailProxyCachePolicy(forceRefresh, 15);
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}?force_refresh=${forceRefresh}&depth=${encodeURIComponent(depth)}`;
try {
const auth = await buildBackendRequestHeaders(req);
const auth = await buildBackendRequestHeaders(req, {
includeSupabaseIdentity: false,
});
const res = await fetch(url, {
headers: auth.headers,
cache: "no-store",
...(cachePolicy.fetchMode === "no-store"
? { cache: "no-store" as const }
: { next: { revalidate: cachePolicy.revalidateSeconds ?? 15 } }),
});
if (!res.ok) {
const raw = await res.text();
const summaryUrl = `${API_BASE}/api/city/${encodeURIComponent(name)}/summary?force_refresh=${forceRefresh}`;
const summaryRes = await fetch(summaryUrl, {
headers: auth.headers,
cache: "no-store",
...(cachePolicy.fetchMode === "no-store"
? { cache: "no-store" as const }
: { next: { revalidate: 10 } }),
});
if (summaryRes.ok) {
const summaryData = await summaryRes.json();
const response = NextResponse.json(buildFallbackCityDetail(name, depth, summaryData), {
headers: {
"Cache-Control": "no-store",
"X-PolyWeather-Fallback": "summary",
},
});
const response = buildCachedJsonResponse(
req,
buildFallbackCityDetail(name, depth, summaryData),
cachePolicy.fetchMode === "no-store"
? cachePolicy.responseCacheControl
: "public, max-age=0, s-maxage=10, stale-while-revalidate=30",
);
response.headers.set("X-PolyWeather-Fallback", "summary");
return applyAuthResponseCookies(response, auth.response);
}
@@ -158,7 +178,11 @@ export async function GET(
return applyAuthResponseCookies(response, auth.response);
}
const data = normalizeCityDetailPayload(await res.json());
const response = NextResponse.json(data);
const response = buildCachedJsonResponse(
req,
data,
cachePolicy.responseCacheControl,
);
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const response = buildProxyExceptionResponse(error, {
+11 -53
View File
@@ -1,13 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
import { buildCachedJsonResponse } from "@/lib/http-cache";
import { proxyBackendJsonGet } from "@/lib/api-proxy";
import { buildForceRefreshProxyCachePolicy } from "@/lib/proxy-cache-policy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -25,50 +18,15 @@ export async function GET(
const { name } = await context.params;
const forceRefresh = req.nextUrl.searchParams.get("force_refresh") ?? "false";
const bypassCache = forceRefresh === "true";
const cachePolicy = buildForceRefreshProxyCachePolicy(forceRefresh, 20);
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/summary?force_refresh=${forceRefresh}`;
try {
const auth = await buildBackendRequestHeaders(req, {
includeSupabaseIdentity: false,
});
const fetchOptions =
bypassCache
? {
headers: auth.headers,
cache: "no-store" as const,
}
: {
headers: auth.headers,
next: { revalidate: 20 },
};
const res = await fetch(url, {
...fetchOptions,
});
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw);
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
if (bypassCache) {
const response = NextResponse.json(data, {
headers: {
"Cache-Control": "no-store",
},
});
return applyAuthResponseCookies(response, auth.response);
}
const response = buildCachedJsonResponse(
req,
data,
"public, max-age=0, s-maxage=20, stale-while-revalidate=60",
);
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const response = buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch city summary",
});
return response;
}
return proxyBackendJsonGet(req, {
cacheControl: cachePolicy.responseCacheControl,
fetchCache:
cachePolicy.fetchMode === "no-store" ? "no-store" : undefined,
publicMessage: "Failed to fetch city summary",
revalidateSeconds: cachePolicy.revalidateSeconds,
url,
});
}
+29
View File
@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from "next/server";
import { applyAuthResponseCookies, buildBackendRequestHeaders } from "@/lib/backend-auth";
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
const BACKEND = API_BASE ? `${API_BASE}/api/ops/config` : "";
export async function GET(req: NextRequest) {
if (!API_BASE) return NextResponse.json({ error: "API_BASE not configured" }, { status: 500 });
try {
const auth = await buildBackendRequestHeaders(req);
const res = await fetch(BACKEND, { headers: auth.headers, cache: "no-store" });
const raw = await res.text();
const response = new NextResponse(raw, { status: res.status, headers: { "Content-Type": "application/json", "Cache-Control": "no-store" } });
return applyAuthResponseCookies(response, auth.response);
} catch (e) { return buildProxyExceptionResponse(e, { publicMessage: "Config fetch failed" }); }
}
export async function PUT(req: NextRequest) {
if (!API_BASE) return NextResponse.json({ error: "API_BASE not configured" }, { status: 500 });
try {
const auth = await buildBackendRequestHeaders(req);
const body = await req.text();
const res = await fetch(BACKEND, { method: "PUT", headers: { ...auth.headers, "Content-Type": "application/json" }, body, cache: "no-store" });
const raw = await res.text();
const response = new NextResponse(raw, { status: res.status, headers: { "Content-Type": "application/json", "Cache-Control": "no-store" } });
return applyAuthResponseCookies(response, auth.response);
} catch (e) { return buildProxyExceptionResponse(e, { publicMessage: "Config update failed" }); }
}
@@ -0,0 +1,16 @@
import { NextRequest, NextResponse } from "next/server";
import { applyAuthResponseCookies, buildBackendRequestHeaders } from "@/lib/backend-auth";
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
export async function GET(req: NextRequest) {
if (!API_BASE) return NextResponse.json({ error: "API_BASE not configured" }, { status: 500 });
try {
const auth = await buildBackendRequestHeaders(req);
const res = await fetch(`${API_BASE}/api/ops/health-check`, { headers: auth.headers, cache: "no-store" });
const raw = await res.text();
const response = new NextResponse(raw, { status: res.status, headers: { "Content-Type": "application/json", "Cache-Control": "no-store" } });
return applyAuthResponseCookies(response, auth.response);
} catch (e) { return buildProxyExceptionResponse(e, { publicMessage: "Health check failed" }); }
}
@@ -0,0 +1,19 @@
import { NextRequest, NextResponse } from "next/server";
import { applyAuthResponseCookies, buildBackendRequestHeaders } from "@/lib/backend-auth";
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
export async function GET(req: NextRequest) {
if (!API_BASE) return NextResponse.json({ error: "API_BASE not configured" }, { status: 500 });
try {
const auth = await buildBackendRequestHeaders(req);
const url = new URL(`${API_BASE}/api/ops/memberships/growth`);
const days = req.nextUrl.searchParams.get("days");
if (days) url.searchParams.set("days", days);
const res = await fetch(url.toString(), { headers: auth.headers, cache: "no-store" });
const raw = await res.text();
const response = new NextResponse(raw, { status: res.status, headers: { "Content-Type": "application/json", "Cache-Control": "no-store" } });
return applyAuthResponseCookies(response, auth.response);
} catch (e) { return buildProxyExceptionResponse(e, { publicMessage: "Growth fetch failed" }); }
}
@@ -0,0 +1,24 @@
import { NextRequest, NextResponse } from "next/server";
import { applyAuthResponseCookies, buildBackendRequestHeaders } from "@/lib/backend-auth";
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
const ENTITLEMENT_TOKEN = process.env.POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN?.trim() || "";
export async function POST(req: NextRequest) {
if (!API_BASE) return NextResponse.json({ error: "API_BASE not configured" }, { status: 500 });
try {
const auth = await buildBackendRequestHeaders(req);
const body = await req.text();
const headers: Record<string, string> = { ...auth.headers as Record<string, string>, "Content-Type": "application/json" };
if (ENTITLEMENT_TOKEN) {
headers.Authorization = `Bearer ${ENTITLEMENT_TOKEN}`;
}
const res = await fetch(`${API_BASE}/api/ops/subscriptions/extend`, {
method: "POST", headers, body, cache: "no-store",
});
const raw = await res.text();
const response = new NextResponse(raw, { status: res.status, headers: { "Content-Type": "application/json", "Cache-Control": "no-store" } });
return applyAuthResponseCookies(response, auth.response);
} catch (e) { return buildProxyExceptionResponse(e, { publicMessage: "Subscription extend failed" }); }
}
@@ -0,0 +1,168 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
function parseAdminEmails() {
return String(process.env.POLYWEATHER_OPS_ADMIN_EMAILS || "")
.split(",")
.map((item) => item.trim().toLowerCase())
.filter(Boolean);
}
async function getBearerEmail(req: NextRequest) {
const auth = String(req.headers.get("authorization") || "").trim();
const token = auth.replace(/^bearer\s+/i, "").trim();
const supabaseUrl = String(process.env.NEXT_PUBLIC_SUPABASE_URL || "").trim();
const anonKey = String(process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "").trim();
if (!token || !supabaseUrl || !anonKey) return "";
const res = await fetch(`${supabaseUrl.replace(/\/$/, "")}/auth/v1/user`, {
headers: {
apikey: anonKey,
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
cache: "no-store",
});
if (!res.ok) return "";
const data = (await res.json()) as { email?: string };
return String(data.email || "").trim().toLowerCase();
}
async function findSupabaseUserIdByEmail(email: string) {
const supabaseUrl = String(process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || "")
.trim()
.replace(/\/$/, "");
const serviceRoleKey = String(process.env.SUPABASE_SERVICE_ROLE_KEY || "").trim();
if (!supabaseUrl || !serviceRoleKey) {
throw new Error("Supabase service role is not configured on Vercel");
}
const res = await fetch(
`${supabaseUrl}/auth/v1/admin/users?filter=${encodeURIComponent(`email.eq.${email}`)}`,
{
headers: {
apikey: serviceRoleKey,
Authorization: `Bearer ${serviceRoleKey}`,
Accept: "application/json",
},
cache: "no-store",
},
);
const data = (await res.json().catch(() => ({}))) as {
users?: Array<{ id?: string }>;
};
if (!res.ok) throw new Error(`Supabase user lookup failed: ${JSON.stringify(data).slice(0, 200)}`);
const userId = String(data.users?.[0]?.id || "").trim();
if (!userId) {
const error = new Error(`user not found: ${email}`);
(error as Error & { status?: number }).status = 404;
throw error;
}
return { supabaseUrl, serviceRoleKey, userId };
}
async function grantSubscriptionDirectly(req: NextRequest, bodyText: string, authEmail?: string | null) {
const adminEmail = String(authEmail || (await getBearerEmail(req)) || "")
.trim()
.toLowerCase();
const allowedEmails = parseAdminEmails();
if (!adminEmail) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (!allowedEmails.includes(adminEmail)) {
return NextResponse.json({ error: "ops admin required" }, { status: 403 });
}
const body = JSON.parse(bodyText || "{}") as {
email?: string;
plan_code?: string;
days?: number;
};
const email = String(body.email || "").trim().toLowerCase();
const planCode = String(body.plan_code || "pro_monthly").trim();
const days = Math.max(1, Math.min(365, Number(body.days || 30)));
if (!email) return NextResponse.json({ error: "email is required" }, { status: 400 });
if (planCode !== "pro_monthly") {
return NextResponse.json({ error: "invalid plan_code" }, { status: 400 });
}
try {
const { supabaseUrl, serviceRoleKey, userId } = await findSupabaseUserIdByEmail(email);
const now = new Date();
const expires = new Date(now.getTime() + days * 86_400_000);
const payload = {
user_id: userId,
email,
plan_code: planCode,
"status": "active",
starts_at: now.toISOString(),
expires_at: expires.toISOString(),
source: "ops_manual_grant_next_fallback",
created_at: now.toISOString(),
updated_at: now.toISOString(),
};
const insert = await fetch(`${supabaseUrl}/rest/v1/subscriptions`, {
method: "POST",
headers: {
apikey: serviceRoleKey,
Authorization: `Bearer ${serviceRoleKey}`,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(payload),
cache: "no-store",
});
const raw = await insert.text();
if (!insert.ok) {
return NextResponse.json(
{ error: "Supabase insert failed", detail: raw.slice(0, 300) },
{ status: 500 },
);
}
return NextResponse.json({
ok: true,
user_id: userId,
plan_code: planCode,
days,
expires_at: expires.toISOString(),
fallback: "next_supabase_direct",
});
} catch (error) {
const status = Number((error as Error & { status?: number }).status || 500);
return NextResponse.json({ error: String(error) }, { status });
}
}
export async function POST(req: NextRequest) {
try {
const auth = await buildBackendRequestHeaders(req);
const body = await req.text();
if (!API_BASE) {
return grantSubscriptionDirectly(req, body, auth.authEmail);
}
const res = await fetch(`${API_BASE}/api/ops/subscriptions/grant`, {
method: "POST",
headers: { ...(auth.headers as Record<string, string>), "Content-Type": "application/json" },
body,
cache: "no-store",
});
const raw = await res.text();
if (res.status === 404) {
const fallback = await grantSubscriptionDirectly(req, body, auth.authEmail);
return applyAuthResponseCookies(fallback, auth.response);
}
const response = new NextResponse(raw, {
status: res.status,
headers: {
"Content-Type": res.headers.get("content-type") || "application/json",
"Cache-Control": "no-store",
},
});
return applyAuthResponseCookies(response, auth.response);
} catch (e) {
return buildProxyExceptionResponse(e, { publicMessage: "Subscription grant failed" });
}
}
@@ -0,0 +1,16 @@
import { NextRequest, NextResponse } from "next/server";
import { applyAuthResponseCookies, buildBackendRequestHeaders } from "@/lib/backend-auth";
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
export async function GET(req: NextRequest) {
if (!API_BASE) return NextResponse.json({ error: "API_BASE not configured" }, { status: 500 });
try {
const auth = await buildBackendRequestHeaders(req);
const res = await fetch(`${API_BASE}/api/ops/telegram/members-audit`, { headers: auth.headers, cache: "no-store" });
const raw = await res.text();
const response = new NextResponse(raw, { status: res.status, headers: { "Content-Type": "application/json", "Cache-Control": "no-store" } });
return applyAuthResponseCookies(response, auth.response);
} catch (e) { return buildProxyExceptionResponse(e, { publicMessage: "Telegram audit failed" }); }
}
@@ -0,0 +1,16 @@
import { NextRequest, NextResponse } from "next/server";
import { applyAuthResponseCookies, buildBackendRequestHeaders } from "@/lib/backend-auth";
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
export async function GET(req: NextRequest) {
if (!API_BASE) return NextResponse.json({ error: "API_BASE not configured" }, { status: 500 });
try {
const auth = await buildBackendRequestHeaders(req);
const res = await fetch(`${API_BASE}/api/ops/training/accuracy`, { headers: auth.headers, cache: "no-store" });
const raw = await res.text();
const response = new NextResponse(raw, { status: res.status, headers: { "Content-Type": "application/json", "Cache-Control": "no-store" } });
return applyAuthResponseCookies(response, auth.response);
} catch (e) { return buildProxyExceptionResponse(e, { publicMessage: "Training accuracy fetch failed" }); }
}
+21
View File
@@ -0,0 +1,21 @@
import { NextRequest, NextResponse } from "next/server";
import { applyAuthResponseCookies, buildBackendRequestHeaders } from "@/lib/backend-auth";
import { buildProxyExceptionResponse } from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
export async function GET(req: NextRequest) {
if (!API_BASE) return NextResponse.json({ error: "API_BASE not configured" }, { status: 500 });
try {
const auth = await buildBackendRequestHeaders(req);
const url = new URL(`${API_BASE}/api/ops/logs`);
const level = req.nextUrl.searchParams.get("level");
const lines = req.nextUrl.searchParams.get("lines");
if (level) url.searchParams.set("level", level);
if (lines) url.searchParams.set("lines", lines);
const res = await fetch(url.toString(), { headers: auth.headers, cache: "no-store" });
const raw = await res.text();
const response = new NextResponse(raw, { status: res.status, headers: { "Content-Type": "application/json", "Cache-Control": "no-store" } });
return applyAuthResponseCookies(response, auth.response);
} catch (e) { return buildProxyExceptionResponse(e, { publicMessage: "Log fetch failed" }); }
}
+9 -32
View File
@@ -1,12 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
import { proxyBackendJsonGet } from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -17,28 +10,12 @@ export async function GET(req: NextRequest) {
{ status: 500 },
);
}
try {
const auth = await buildBackendRequestHeaders(req);
const res = await fetch(`${API_BASE}/api/payments/config`, {
headers: auth.headers,
cache: "no-store",
});
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw, {
detailLimit: 350,
});
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data, {
headers: { "Cache-Control": "no-store" },
});
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
return buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch payment config",
});
}
return proxyBackendJsonGet(req, {
cacheControl: "public, max-age=0, s-maxage=300, stale-while-revalidate=900",
detailLimit: 350,
includeSupabaseIdentity: true,
publicMessage: "Failed to fetch payment config",
revalidateSeconds: 300,
url: `${API_BASE}/api/payments/config`,
});
}
@@ -1,12 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
import { proxyBackendJsonGet } from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -21,29 +14,11 @@ export async function GET(
);
}
const { intentId } = await context.params;
try {
const auth = await buildBackendRequestHeaders(req);
const res = await fetch(
`${API_BASE}/api/payments/intents/${encodeURIComponent(intentId)}`,
{
method: "GET",
headers: auth.headers,
cache: "no-store",
},
);
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw, {
detailLimit: 350,
});
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data);
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
return buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch payment intent",
});
}
return proxyBackendJsonGet(req, {
detailLimit: 350,
fetchCache: "no-store",
includeSupabaseIdentity: true,
publicMessage: "Failed to fetch payment intent",
url: `${API_BASE}/api/payments/intents/${encodeURIComponent(intentId)}`,
});
}
@@ -37,8 +37,14 @@ export async function POST(
);
if (!res.ok) {
const raw = await res.text();
let detail = raw.slice(0, 350);
try {
const parsed = JSON.parse(raw);
if (parsed.detail) detail = String(parsed.detail).slice(0, 350);
} catch {}
const response = buildUpstreamErrorResponse(res.status, raw, {
detailLimit: 350,
error: detail || undefined,
});
return applyAuthResponseCookies(response, auth.response);
}
@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
export async function POST(
req: NextRequest,
context: { params: Promise<{ intentId: string }> },
) {
if (!API_BASE) {
return NextResponse.json(
{ error: "POLYWEATHER_API_BASE_URL is not configured" },
{ status: 500 },
);
}
const { intentId } = await context.params;
try {
const body = await req.json();
const auth = await buildBackendRequestHeaders(req);
const proxiedHeaders = new Headers(auth.headers);
proxiedHeaders.set("Content-Type", "application/json");
const res = await fetch(
`${API_BASE}/api/payments/intents/${encodeURIComponent(intentId)}/validate`,
{
method: "POST",
headers: proxiedHeaders,
body: JSON.stringify(body ?? {}),
cache: "no-store",
},
);
if (!res.ok) {
const raw = await res.text();
let detail = raw.slice(0, 350);
try {
const parsed = JSON.parse(raw);
if (parsed.detail) detail = String(parsed.detail).slice(0, 350);
} catch {}
const response = buildUpstreamErrorResponse(res.status, raw, {
detailLimit: 350,
error: detail || undefined,
});
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data);
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
return buildProxyExceptionResponse(error, {
publicMessage: "Failed to validate payment tx",
});
}
}
+10 -32
View File
@@ -1,12 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
import { proxyBackendJsonGet } from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -18,28 +11,13 @@ export async function GET(req: NextRequest) {
);
}
try {
const auth = await buildBackendRequestHeaders(req);
const res = await fetch(`${API_BASE}/api/payments/runtime`, {
headers: auth.headers,
cache: "no-store",
});
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw, {
detailLimit: 500,
});
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data, {
headers: { "Cache-Control": "no-store" },
});
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
return buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch payment runtime",
});
}
return proxyBackendJsonGet(req, {
cacheControl: "no-store",
conditionalResponse: false,
detailLimit: 500,
fetchCache: "no-store",
includeSupabaseIdentity: true,
publicMessage: "Failed to fetch payment runtime",
url: `${API_BASE}/api/payments/runtime`,
});
}
+10 -23
View File
@@ -5,6 +5,7 @@ import {
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
proxyBackendJsonGet,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
@@ -17,29 +18,15 @@ export async function GET(req: NextRequest) {
{ status: 500 },
);
}
try {
const auth = await buildBackendRequestHeaders(req);
const res = await fetch(`${API_BASE}/api/payments/wallets`, {
headers: auth.headers,
cache: "no-store",
});
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw, {
detailLimit: 350,
});
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data, {
headers: { "Cache-Control": "no-store" },
});
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
return buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch wallets",
});
}
return proxyBackendJsonGet(req, {
cacheControl: "no-store",
conditionalResponse: false,
detailLimit: 350,
fetchCache: "no-store",
includeSupabaseIdentity: true,
publicMessage: "Failed to fetch wallets",
url: `${API_BASE}/api/payments/wallets`,
});
}
export async function DELETE(req: NextRequest) {
@@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
const OVERVIEW_PROXY_TIMEOUT_MS = Math.max(
35_000,
Number(process.env.POLYWEATHER_SCAN_OVERVIEW_PROXY_TIMEOUT_MS || "45000") || 45_000,
);
export const dynamic = "force-dynamic";
export const maxDuration = 60;
export async function POST(req: NextRequest) {
if (!API_BASE) {
return NextResponse.json(
{ error: "POLYWEATHER_API_BASE_URL is not configured" },
{ status: 500 },
);
}
let body: unknown = {};
try {
body = await req.json();
} catch {
body = {};
}
let auth: Awaited<ReturnType<typeof buildBackendRequestHeaders>> | null = null;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), OVERVIEW_PROXY_TIMEOUT_MS);
try {
auth = await buildBackendRequestHeaders(req);
const headers = new Headers(auth.headers);
headers.set("Content-Type", "application/json");
headers.set("Accept", "application/json");
const res = await fetch(`${API_BASE}/api/scan/terminal/overview`, {
method: "POST",
headers,
cache: "no-store",
signal: controller.signal,
body: JSON.stringify(body || {}),
});
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw);
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data, {
headers: {
"Cache-Control": "no-store",
},
});
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const timedOut = controller.signal.aborted;
const response = buildProxyExceptionResponse(error, {
publicMessage: timedOut
? "Market overview request timed out"
: "Failed to fetch market overview",
status: timedOut ? 504 : 500,
});
return auth ? applyAuthResponseCookies(response, auth.response) : response;
} finally {
clearTimeout(timeoutId);
}
}
+12 -35
View File
@@ -1,19 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
import { proxyBackendJsonGet } from "@/lib/api-proxy";
import { buildForceRefreshProxyCachePolicy } from "@/lib/proxy-cache-policy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
const SCAN_TERMINAL_PROXY_TIMEOUT_MS = Number(
process.env.POLYWEATHER_SCAN_TERMINAL_PROXY_TIMEOUT_MS || "28000",
);
export const dynamic = "force-dynamic";
export const maxDuration = 30;
export async function GET(req: NextRequest) {
@@ -25,6 +18,7 @@ export async function GET(req: NextRequest) {
}
const params = new URLSearchParams();
const forceRefresh = req.nextUrl.searchParams.get("force_refresh") ?? "false";
for (const key of [
"scan_mode",
"min_price",
@@ -42,41 +36,24 @@ export async function GET(req: NextRequest) {
params.set(key, value);
}
}
const cachePolicy = buildForceRefreshProxyCachePolicy(forceRefresh, 10);
const url = `${API_BASE}/api/scan/terminal?${params.toString()}`;
let auth: Awaited<ReturnType<typeof buildBackendRequestHeaders>> | null = null;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), SCAN_TERMINAL_PROXY_TIMEOUT_MS);
try {
auth = await buildBackendRequestHeaders(req);
const res = await fetch(url, {
headers: auth.headers,
cache: "no-store",
return await proxyBackendJsonGet(req, {
cacheControl: cachePolicy.responseCacheControl,
fetchCache:
cachePolicy.fetchMode === "no-store" ? "no-store" : undefined,
publicMessage: "Failed to fetch scan terminal data",
revalidateSeconds: cachePolicy.revalidateSeconds,
signal: controller.signal,
timeoutPublicMessage: "Scan terminal request timed out",
url,
});
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw);
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data, {
headers: {
"Cache-Control": "no-store",
},
});
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const timedOut = controller.signal.aborted;
const response = buildProxyExceptionResponse(error, {
publicMessage: timedOut
? "Scan terminal request timed out"
: "Failed to fetch scan terminal data",
status: timedOut ? 504 : 500,
});
return auth ? applyAuthResponseCookies(response, auth.response) : response;
} finally {
clearTimeout(timeoutId);
}
+8 -32
View File
@@ -1,12 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
import { proxyBackendJsonGet } from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -18,28 +11,11 @@ export async function GET(req: NextRequest) {
);
}
try {
const auth = await buildBackendRequestHeaders(req);
const res = await fetch(`${API_BASE}/api/system/status`, {
headers: auth.headers,
cache: "no-store",
});
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw, {
detailLimit: 500,
});
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data, {
headers: { "Cache-Control": "no-store" },
});
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
return buildProxyExceptionResponse(error, {
publicMessage: "Failed to fetch system status",
});
}
return proxyBackendJsonGet(req, {
cacheControl: "public, max-age=0, s-maxage=30, stale-while-revalidate=120",
detailLimit: 500,
publicMessage: "Failed to fetch system status",
revalidateSeconds: 30,
url: `${API_BASE}/api/system/status`,
});
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

+63 -16
View File
@@ -1,3 +1,5 @@
import Link from "next/link";
type Props = {
searchParams?: Promise<{ next?: string }>;
};
@@ -21,37 +23,82 @@ export default async function EntitlementRequiredPage({ searchParams }: Props) {
<section
style={{
width: "100%",
maxWidth: 720,
maxWidth: 480,
border: "1px solid rgba(68, 92, 140, 0.45)",
borderRadius: 16,
padding: 24,
background: "rgba(9, 18, 36, 0.88)",
boxShadow: "0 20px 50px rgba(0, 0, 0, 0.35)",
textAlign: "center",
}}
>
<h1 style={{ margin: 0, fontSize: 28, lineHeight: 1.2 }}>
Entitlement Required
<h1
style={{
margin: 0,
fontSize: 22,
lineHeight: 1.3,
fontWeight: 800,
}}
>
访
</h1>
<p style={{ marginTop: 12, color: "#9fb2da", lineHeight: 1.6 }}>
This dashboard is protected. If Supabase auth is enabled, please go to{" "}
<a
<br />
Sign in required to access this page.
</p>
<div style={{ marginTop: 20, display: "flex", gap: 12, justifyContent: "center", flexWrap: "wrap" }}>
<Link
href={`/auth/login?next=${encodeURIComponent(nextPath)}`}
style={{
color: "#8fc5ff",
display: "inline-flex",
minHeight: 36,
alignItems: "center",
gap: 6,
minHeight: 40,
padding: "8px 20px",
borderRadius: 12,
background: "linear-gradient(135deg, #2563EB, #4F46E5)",
color: "#fff",
fontWeight: 700,
textDecoration: "none",
fontSize: 14,
}}
>
/auth/login
</a>{" "}
to sign in first.
</p>
<p style={{ marginTop: 12, color: "#9fb2da", lineHeight: 1.6 }}>
Legacy mode still supports <code>?access_token=&lt;your-token&gt;</code>.
</p>
<p style={{ marginTop: 12, color: "#9fb2da", lineHeight: 1.6 }}>
Requested path: <code>{nextPath}</code>
/ Sign in
</Link>
<Link
href="/"
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
minHeight: 40,
padding: "8px 20px",
borderRadius: 12,
border: "1px solid rgba(68, 92, 140, 0.45)",
background: "rgba(68, 92, 140, 0.2)",
color: "#d6e2ff",
fontWeight: 600,
textDecoration: "none",
fontSize: 14,
}}
>
/ Back to Home
</Link>
</div>
<p
style={{
marginTop: 20,
fontSize: 12,
color: "#7891b5",
lineHeight: 1.5,
}}
>
<code style={{ background: "rgba(255,255,255,0.06)", padding: "2px 6px", borderRadius: 4 }}>?access_token=&lt;your-token&gt;</code>
<br />
<span style={{ marginTop: 4, display: "inline-block" }}>
/ Requested path: <code>{nextPath}</code>
</span>
</p>
</section>
</main>
+101
View File
@@ -0,0 +1,101 @@
"use client";
import { RefreshCw } from "lucide-react";
import { useEffect } from "react";
export default function ErrorPage({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error("Unhandled page error:", error);
}, [error]);
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
padding: "2rem",
gap: "1rem",
backgroundColor: "var(--color-bg-base, #0B1220)",
color: "var(--color-text-primary, #E6EDF3)",
fontFamily: "var(--font-data, Inter, sans-serif)",
textAlign: "center",
}}
>
<div
style={{
width: 64,
height: 64,
borderRadius: "var(--radius-xl, 20px)",
backgroundColor: "var(--color-bg-raised, #111A2E)",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: "0.5rem",
}}
>
<span style={{ fontSize: "1.8rem" }}></span>
</div>
<h1
style={{
fontSize: "1.25rem",
fontWeight: 600,
margin: 0,
}}
>
</h1>
<p
style={{
color: "var(--color-text-secondary, #9FB2C7)",
fontSize: "0.875rem",
margin: 0,
maxWidth: 400,
lineHeight: 1.6,
}}
>
</p>
{error.digest ? (
<code
style={{
fontSize: "0.75rem",
color: "var(--color-text-muted, #7D8FA3)",
fontFamily: "var(--font-mono, monospace)",
}}
>
{error.digest}
</code>
) : null}
<button
type="button"
onClick={reset}
style={{
marginTop: "0.5rem",
display: "inline-flex",
alignItems: "center",
gap: "0.5rem",
padding: "0.5rem 1.25rem",
borderRadius: "var(--radius-md, 10px)",
border: "1px solid var(--color-border-default, rgba(159,178,199,0.16))",
backgroundColor: "var(--color-bg-raised, #111A2E)",
color: "var(--color-accent-primary, #4DA3FF)",
cursor: "pointer",
fontSize: "0.875rem",
fontWeight: 500,
}}
>
<RefreshCw size={14} />
</button>
</div>
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 B

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+115
View File
@@ -0,0 +1,115 @@
"use client";
import { RefreshCw } from "lucide-react";
import { useEffect } from "react";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error("Unhandled root error:", error);
}, [error]);
return (
<html lang="zh-CN" className="dark">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PolyWeather </title>
</head>
<body
style={{
margin: 0,
padding: 0,
backgroundColor: "var(--color-bg-base, #0B1220)",
minHeight: "100vh",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
padding: "2rem",
gap: "1rem",
color: "var(--color-text-primary, #E6EDF3)",
fontFamily: "var(--font-data, Inter, sans-serif)",
textAlign: "center",
}}
>
<div
style={{
width: 64,
height: 64,
borderRadius: "var(--radius-xl, 20px)",
backgroundColor: "var(--color-bg-raised, #111A2E)",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: "0.5rem",
}}
>
<span style={{ fontSize: "1.8rem" }}></span>
</div>
<h1
style={{
fontSize: "1.25rem",
fontWeight: 600,
margin: 0,
}}
>
</h1>
<p
style={{
color: "var(--color-text-secondary, #9FB2C7)",
fontSize: "0.875rem",
margin: 0,
maxWidth: 400,
lineHeight: 1.6,
}}
>
PolyWeather
</p>
{error.digest ? (
<code
style={{
fontSize: "0.75rem",
color: "var(--color-text-muted, #7D8FA3)",
fontFamily: "var(--font-mono, monospace)",
}}
>
{error.digest}
</code>
) : null}
<button
type="button"
onClick={reset}
style={{
marginTop: "0.5rem",
display: "inline-flex",
alignItems: "center",
gap: "0.5rem",
padding: "0.5rem 1.25rem",
borderRadius: "var(--radius-md, 10px)",
border: "1px solid var(--color-border-default, rgba(159,178,199,0.16))",
backgroundColor: "var(--color-bg-raised, #111A2E)",
color: "var(--color-accent-primary, #4DA3FF)",
cursor: "pointer",
fontSize: "0.875rem",
fontWeight: 500,
}}
>
<RefreshCw size={14} />
</button>
</div>
</body>
</html>
);
}
+138 -9
View File
@@ -18,12 +18,12 @@
/* ── Text Scale ── */
--color-text-primary: #E6EDF3;
--color-text-secondary: #9FB2C7;
--color-text-muted: #6B7A90;
--color-text-disabled: #6B7A90;
--color-text-muted: #7D8FA3;
--color-text-disabled: #7D8FA3;
/* ── Accent Colors (Fintech 3-Color) ── */
/* ── Accent Colors ── */
--color-accent-primary: #4DA3FF;
--color-accent-secondary: #4DA3FF;
--color-accent-secondary: #6FB7FF;
--color-accent-tertiary: #93C5FD;
/* ── Signal / Semantic Colors ── */
@@ -50,10 +50,9 @@
--shadow-glow-secondary: 0 0 20px rgba(111, 183, 255, 0.22);
/* ── Typography ── */
--font-data:
"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-display: "Geist", "Inter", -apple-system, sans-serif;
--font-mono: "JetBrains Mono", "Fira Code", "SF Mono", monospace;
--font-data: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-display: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: var(--font-jetbrains-mono), "Fira Code", "SF Mono", monospace;
/* ── Spacing (4px grid) ── */
--space-1: 4px;
@@ -90,8 +89,29 @@
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 400ms cubic-bezier(0.16, 1, 0.3, 1);
--transition: var(--transition-base);
/* ── Legacy Compatibility (shadcn/ui HSL tokens) ── */
/* ── Legacy Variable Aliases (bridge for older CSS modules) ── */
/* These map old shorthand names → current design token names. */
--accent-cyan: var(--color-accent-primary);
--accent-blue: var(--color-accent-secondary);
--accent-green: var(--color-signal-success);
--bg-primary: var(--color-bg-base);
--bg-secondary: var(--color-bg-raised);
--bg-card: var(--color-bg-card);
--bg-glass: var(--color-bg-card);
--border-glass: var(--color-border-default);
--border-subtle: var(--color-border-subtle);
--text-primary: var(--color-text-primary);
--text-secondary: var(--color-text-secondary);
--text-muted: var(--color-text-muted);
--risk-high: var(--color-risk-high);
--risk-medium: var(--color-risk-medium);
--risk-low: var(--color-risk-low);
--shadow-lg: var(--shadow-elevation-2);
--glass-blur: 10px;
/* ── shadcn/ui Tokens (used by Tailwind @apply border-border) ── */
--background: 223 53% 4%;
--foreground: 210 40% 98%;
--card: 223 46% 8%;
@@ -105,10 +125,86 @@
--border: 221 38% 22%;
}
/* ── Light Theme Token Overrides ── */
html.light,
html[data-theme="light"] {
--color-bg-base: #F7F9FC;
--color-bg-raised: #EEF2F7;
--color-bg-overlay: #FFFFFF;
--color-bg-card: rgba(255, 255, 255, 0.92);
--color-bg-input: rgba(238, 242, 247, 0.88);
--color-text-primary: #0F172A;
--color-text-secondary: #334155;
--color-text-muted: #475569;
--color-text-disabled: #94A3B8;
--color-accent-primary: #2563EB;
--color-accent-secondary: #3B82F6;
--color-accent-tertiary: #60A5FA;
--color-border-default: rgba(148, 163, 184, 0.24);
--color-border-hover: rgba(37, 99, 235, 0.38);
--color-border-subtle: rgba(148, 163, 184, 0.12);
--shadow-elevation-1: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-elevation-2: 0 8px 24px rgba(40, 70, 110, 0.12);
--shadow-elevation-3: 0 20px 60px rgba(40, 70, 110, 0.15);
--shadow-glow-accent: 0 0 20px rgba(37, 99, 235, 0.14);
--shadow-glow-secondary: 0 0 20px rgba(96, 165, 250, 0.12);
--glass-blur-1: blur(10px);
--glass-blur-2: blur(16px);
--glass-blur-3: blur(24px);
--glass-opacity-1: 0.86;
--glass-opacity-2: 0.92;
--glass-opacity-3: 0.96;
/* ── Legacy alias overrides for light theme ── */
--accent-cyan: var(--color-accent-primary);
--accent-blue: var(--color-accent-secondary);
--bg-primary: var(--color-bg-base);
--bg-secondary: var(--color-bg-raised);
--bg-card: var(--color-bg-card);
--bg-glass: var(--color-bg-card);
--border-glass: var(--color-border-default);
--border-subtle: var(--color-border-subtle);
--text-primary: var(--color-text-primary);
--text-secondary: var(--color-text-secondary);
--text-muted: var(--color-text-muted);
--shadow-lg: var(--shadow-elevation-2);
}
* {
@apply border-border;
}
/* ── Skip-to-content link ── */
.skip-to-content {
position: absolute;
top: -100%;
left: 8px;
z-index: 9999;
padding: 10px 18px;
border-radius: var(--radius-md);
background: var(--color-accent-primary);
color: #fff;
font-weight: 700;
text-decoration: none;
transition: top 0.18s ease;
}
.skip-to-content:focus {
top: 8px;
}
/* ── Global focus-visible ring (keyboard navigation) ── */
:focus-visible {
outline: 2px solid var(--color-accent-primary);
outline-offset: 2px;
border-radius: 2px;
}
body {
font-family: var(--font-data);
background:
@@ -219,6 +315,39 @@
transform: translateY(0);
}
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulse-pending {
0%, 100% { opacity: 0.3; }
50% { opacity: 0.6; }
}
}
/* ── Reduced motion: disable all animations and transitions ── */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* ── Extreme temperature emphasis ── */
.temp-extreme-hot {
color: #f97316;
text-shadow: 0 0 12px rgba(249, 115, 22, 0.35);
}
.temp-extreme-cold {
color: #38bdf8;
text-shadow: 0 0 12px rgba(56, 189, 248, 0.35);
}
/*
+28 -12
View File
@@ -1,6 +1,22 @@
import type { Metadata } from "next";
import { Inter, JetBrains_Mono } from "next/font/google";
import { RegisterSW } from "@/components/dashboard/RegisterSW";
import "./globals.css";
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
weight: ["300", "400", "500", "600", "700", "800"],
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
display: "swap",
variable: "--font-jetbrains-mono",
weight: ["400", "500", "600", "700"],
});
export const metadata: Metadata = {
title: "PolyWeather | Weather Intelligence",
description:
@@ -23,20 +39,20 @@ export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="zh-CN" className="dark">
<html
lang="zh-CN"
className={`${inter.variable} ${jetbrainsMono.variable} dark`}
>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin=""
/>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body className="min-h-screen font-sans antialiased">{children}</body>
<body className="min-h-screen font-sans antialiased">
<a href="#main-content" className="skip-to-content">
Skip to content
</a>
<main id="main-content">{children}</main>
<RegisterSW />
</body>
</html>
);
}
+10
View File
@@ -0,0 +1,10 @@
import type { Metadata } from "next";
import { requireOpsAdmin } from "@/lib/ops-admin";
import { AnalyticsPageClient } from "@/components/ops/analytics/AnalyticsPageClient";
export const metadata: Metadata = { title: "转化分析 — PolyWeather Ops" };
export default async function AnalyticsPage() {
await requireOpsAdmin("/ops/analytics");
return <AnalyticsPageClient />;
}
+10
View File
@@ -0,0 +1,10 @@
import type { Metadata } from "next";
import { requireOpsAdmin } from "@/lib/ops-admin";
import { ConfigPageClient } from "@/components/ops/config/ConfigPageClient";
export const metadata: Metadata = { title: "系统配置 — PolyWeather Ops" };
export default async function ConfigPage() {
await requireOpsAdmin("/ops/config");
return <ConfigPageClient />;
}
+10
View File
@@ -0,0 +1,10 @@
import type { Metadata } from "next";
import { requireOpsAdmin } from "@/lib/ops-admin";
import { HealthPageClient } from "@/components/ops/health/HealthPageClient";
export const metadata: Metadata = { title: "API 状态 — PolyWeather Ops" };
export default async function HealthPage() {
await requireOpsAdmin("/ops/health");
return <HealthPageClient />;
}
+5
View File
@@ -0,0 +1,5 @@
import { AdminShell } from "@/components/ops/layout/AdminShell";
export default function OpsLayout({ children }: { children: React.ReactNode }) {
return <AdminShell>{children}</AdminShell>;
}

Some files were not shown because too many files have changed in this diff Show More