feat: implement weather dashboard components, state management, and API routes for terminal scanning and assistant chat

This commit is contained in:
2569718930@qq.com
2026-04-25 06:42:24 +08:00
parent 3083d6b8fe
commit d5fb40a544
13 changed files with 2109 additions and 357 deletions
+54
View File
@@ -455,6 +455,43 @@ export interface ScanOpportunityRow {
temp_symbol?: string | null;
current_temp?: number | null;
current_max_so_far?: number | null;
metar_context?: {
source?: string | null;
station?: string | null;
station_label?: string | null;
obs_count?: number | null;
last_time?: string | null;
last_temp?: number | null;
max_temp?: number | null;
max_time?: string | null;
trend_delta?: number | null;
stale_for_today?: boolean;
available_for_today?: boolean;
last_observation_time?: string | null;
airport_current_temp?: number | null;
airport_max_so_far?: number | null;
airport_obs_time?: string | null;
airport_report_time?: string | null;
airport_raw_metar?: string | null;
airport_wx_desc?: string | null;
airport_cloud_desc?: string | null;
airport_visibility_mi?: number | null;
airport_wind_speed_kt?: number | null;
airport_wind_dir?: number | null;
airport_humidity?: number | null;
today_obs?: Array<{ time?: string; temp?: number | null }>;
recent_obs?: Array<{ time?: string; temp?: number | null }>;
settlement_today_obs?: Array<{ time?: string; temp?: number | null }>;
} | null;
metar_recent_obs?: Array<{ time?: string; temp?: number | null }>;
metar_today_obs?: Array<{ time?: string; temp?: number | null }>;
settlement_today_obs?: Array<{ time?: string; temp?: number | null }>;
metar_status?: {
available_for_today?: boolean;
stale_for_today?: boolean;
last_observation_time?: string | null;
last_temp?: number | null;
} | null;
deb_prediction?: number | null;
airport?: string | null;
risk_level?: RiskLevel | null;
@@ -559,8 +596,25 @@ export interface ScanOpportunityRow {
ai_city_thesis_en?: string | null;
ai_city_confidence?: string | null;
ai_city_model_cluster_note?: string | null;
ai_predicted_max?: number | null;
ai_predicted_low?: number | null;
ai_predicted_high?: number | null;
ai_forecast_unit?: string | null;
ai_forecast_confidence?: string | null;
ai_peak_window_zh?: string | null;
ai_peak_window_en?: string | null;
ai_airport_metar_read_zh?: string | null;
ai_airport_metar_read_en?: string | null;
ai_forecast_reason_zh?: string | null;
ai_forecast_reason_en?: string | null;
ai_forecast_match?: "core" | "edge" | "outside" | "watch" | string | null;
ai_forecast_match_reason_zh?: string | null;
ai_forecast_match_reason_en?: string | null;
ai_watchlist_reason_zh?: string | null;
ai_watchlist_reason_en?: string | null;
v4_metar_decision?: "approve" | "veto" | "downgrade" | "watchlist" | string | null;
v4_metar_reason_zh?: string | null;
v4_metar_reason_en?: string | null;
}
export interface PrimarySignal extends ScanOpportunityRow {}
+83
View File
@@ -0,0 +1,83 @@
import type { ProAccessState } from "@/lib/dashboard-types";
const LOCAL_DEV_EXPIRY = "2099-12-31T23:59:59Z";
const LOCAL_DEV_POINTS = 999_999;
function readLocalAccessFlag() {
const raw =
process.env.NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS ??
process.env.POLYWEATHER_LOCAL_FULL_ACCESS;
if (raw == null) return true;
return !/^(0|false|off|no)$/i.test(String(raw).trim());
}
export function isLocalHostname(value?: string | null) {
const normalized = String(value || "")
.trim()
.toLowerCase()
.replace(/^\[/, "")
.replace(/\]$/, "");
return (
normalized === "localhost" ||
normalized === "127.0.0.1" ||
normalized === "::1"
);
}
export function extractHostname(value?: string | null) {
const firstHost = String(value || "")
.split(",")[0]
.trim();
if (!firstHost) return "";
try {
return new URL(
firstHost.includes("://") ? firstHost : `http://${firstHost}`,
).hostname;
} catch {
if (firstHost.startsWith("[")) {
const endIndex = firstHost.indexOf("]");
return endIndex > 0 ? firstHost.slice(1, endIndex) : firstHost;
}
return firstHost.split(":")[0] || firstHost;
}
}
export function isLocalFullAccessHost(value?: string | null) {
return readLocalAccessFlag() && isLocalHostname(extractHostname(value));
}
export function isBrowserLocalFullAccess() {
if (typeof window === "undefined") return false;
return readLocalAccessFlag() && isLocalHostname(window.location.hostname);
}
export function getLocalDevAuthPayload() {
return {
authenticated: true,
user_id: "local-dev",
email: "local-dev@polyweather.local",
subscription_active: true,
subscription_plan_code: "local-full-access",
subscription_expires_at: LOCAL_DEV_EXPIRY,
subscription_total_expires_at: LOCAL_DEV_EXPIRY,
subscription_queued_days: 0,
subscription_queued_count: 0,
points: LOCAL_DEV_POINTS,
local_dev_full_access: true,
};
}
export function getLocalDevProAccessState(): ProAccessState {
return {
loading: false,
authenticated: true,
userId: "local-dev",
subscriptionActive: true,
subscriptionPlanCode: "local-full-access",
subscriptionExpiresAt: LOCAL_DEV_EXPIRY,
subscriptionTotalExpiresAt: LOCAL_DEV_EXPIRY,
subscriptionQueuedDays: 0,
points: LOCAL_DEV_POINTS,
error: null,
};
}