feat: implement weather dashboard components, state management, and API routes for terminal scanning and assistant chat
This commit is contained in:
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user