Refactor forecast and historian-driven meteo dashboard

This commit is contained in:
litoral05
2026-05-29 11:41:11 +01:00
parent 289a54f455
commit 3905e2adfe
24 changed files with 2494 additions and 707 deletions
+11 -3
View File
@@ -1,8 +1,13 @@
{ {
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main and chart windows",
"windows": ["main", "chart-*"], "windows": [
"main",
"chart-*",
"maincharts-*",
"climatecharts-*"
],
"permissions": [ "permissions": [
"core:default", "core:default",
"opener:default", "opener:default",
@@ -19,6 +24,9 @@
"core:window:allow-maximize", "core:window:allow-maximize",
"core:window:allow-toggle-maximize", "core:window:allow-toggle-maximize",
"core:window:allow-start-dragging" "core:window:allow-start-dragging",
"core:event:allow-listen",
"core:event:allow-emit"
] ]
} }
+6 -2
View File
@@ -9,24 +9,24 @@ import { ConsolePage } from "../features/console/pages/ConsolePage";
import { MainChartsPage } from "../features/maincharts/pages/MainChartsPage"; import { MainChartsPage } from "../features/maincharts/pages/MainChartsPage";
import { ChartWindowPage } from "../features/chartworkspace/pages/ChartWindowPage"; import { ChartWindowPage } from "../features/chartworkspace/pages/ChartWindowPage";
import { SettingsPage } from "../features/settings/pages/SettingsPage"; import { SettingsPage } from "../features/settings/pages/SettingsPage";
import SynopticPage from "../features/synoptic/pages/SynopticPage";
export type AppPage = export type AppPage =
| "dashboard" | "dashboard"
| "meteo" | "meteo"
| "console" | "console"
| "maincharts" | "maincharts"
| "synoptic"
| "settings" | "settings"
| "climate" | "climate"
| "climateCharts" | "climateCharts"
| "climateLighting" | "climateLighting"
| "climateVentilation" | "climateVentilation"
| "climateSynoptic"
| "irrigation" | "irrigation"
| "irrigationCharts" | "irrigationCharts"
| "irrigationFilters" | "irrigationFilters"
| "irrigationConsumption" | "irrigationConsumption"
| "irrigationDrainage" | "irrigationDrainage"
| "irrigationSynoptic";
function App() { function App() {
const [activePage, setActivePage] = useState<AppPage>("dashboard"); const [activePage, setActivePage] = useState<AppPage>("dashboard");
@@ -59,6 +59,10 @@ function App() {
return <SettingsPage theme={theme} />; return <SettingsPage theme={theme} />;
} }
if (activePage === "synoptic") {
return <SynopticPage theme={theme} />;
}
return ( return (
<DashboardPage <DashboardPage
theme={theme} theme={theme}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

+3 -2
View File
@@ -302,6 +302,9 @@ function pageTitle(page: AppPage | null) {
case "meteo": case "meteo":
return "Meteorologia"; return "Meteorologia";
case "synoptic":
return "Sinótico";
case "settings": case "settings":
return "Configurações"; return "Configurações";
@@ -313,7 +316,6 @@ function pageTitle(page: AppPage | null) {
case "climateCharts": case "climateCharts":
case "climateLighting": case "climateLighting":
case "climateVentilation": case "climateVentilation":
case "climateSynoptic":
return "Clima"; return "Clima";
// ALL IRRIGATION / REGA PAGES // ALL IRRIGATION / REGA PAGES
@@ -322,7 +324,6 @@ function pageTitle(page: AppPage | null) {
case "irrigationFilters": case "irrigationFilters":
case "irrigationConsumption": case "irrigationConsumption":
case "irrigationDrainage": case "irrigationDrainage":
case "irrigationSynoptic":
return "Rega"; return "Rega";
default: default:
+3 -3
View File
@@ -37,19 +37,19 @@ const navigationItems: {
}[] = [ }[] = [
{ label: "Painel Principal", page: "dashboard", icon: Home }, { label: "Painel Principal", page: "dashboard", icon: Home },
{ label: "Meteorologia", page: "meteo", icon: CloudSun }, { label: "Meteorologia", page: "meteo", icon: CloudSun },
{ label: "Gráficos Gerais", page: "maincharts", icon: BarChart3 } { label: "Gráficos Gerais", page: "maincharts", icon: BarChart3 },
{ label: "Sinótico", page: "synoptic" , icon: MonitorDot }
]; ];
const climateItems = [ const climateItems = [
{ label: "Iluminação", icon: Lightbulb }, { label: "Iluminação", icon: Lightbulb },
{ label: "Ventilação", icon: Wind }, { label: "Ventilação", icon: Wind },
{ label: "Sinótico", icon: MonitorDot },
{ label: "Gráficos", icon: BarChart3 }, { label: "Gráficos", icon: BarChart3 },
]; ];
const irrigationItems = [ const irrigationItems = [
{ label: "Regas", icon: Droplet }, { label: "Regas", icon: Droplet },
{ label: "Sinótico", icon: MonitorDot },
{ label: "Filtros de Rega", icon: Filter }, { label: "Filtros de Rega", icon: Filter },
{ label: "Consumos", icon: Gauge }, { label: "Consumos", icon: Gauge },
{ label: "Drenagem", icon: Waves }, { label: "Drenagem", icon: Waves },
@@ -262,8 +262,10 @@ export function ChartWindowPage({ theme }: ChartWindowPageProps) {
> >
<header className={titleBarClass}> <header className={titleBarClass}>
<div <div
data-tauri-drag-region
className="flex h-full flex-1 items-center px-3 text-xs font-black" className="flex h-full flex-1 items-center px-3 text-xs font-black"
onPointerDown={() => {
void currentWindow.startDragging();
}}
> >
{chart.title} {chart.title}
</div> </div>
@@ -366,7 +366,7 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) {
}; };
const closeDetachedChartWindow = async (chartId: string) => { const closeDetachedChartWindow = async (chartId: string) => {
const label = `chart-${chartId}`; const label = `climatecharts-${chartId}`;
const existing = await WebviewWindow.getByLabel(label); const existing = await WebviewWindow.getByLabel(label);
if (!existing) return; if (!existing) return;
@@ -365,7 +365,7 @@ export function MainChartsPage({ theme }: MainChartsPageProps) {
}; };
const closeDetachedChartWindow = async (chartId: string) => { const closeDetachedChartWindow = async (chartId: string) => {
const label = `chart-${chartId}`; const label = `maincharts-${chartId}`;
const existing = await WebviewWindow.getByLabel(label); const existing = await WebviewWindow.getByLabel(label);
if (!existing) return; if (!existing) return;
@@ -0,0 +1,80 @@
import { useEffect, useState } from "react";
import type { ModuleSensorResponse } from "../../../types/meteo";
import type { HistorianPoint } from "../components/MeteoHistoryModal";
const BACKEND_URL = "http://localhost:18450";
type SensorHistoryMap = Record<string, HistorianPoint[]>;
export function useMeteoMultiHistory(
sensors: Array<ModuleSensorResponse | null>,
hours = 6,
) {
const [pointsByKey, setPointsByKey] = useState<SensorHistoryMap>({});
const [loading, setLoading] = useState(false);
const sensorKeys = sensors
.filter((sensor): sensor is ModuleSensorResponse => Boolean(sensor))
.map((sensor) => sensor.key);
useEffect(() => {
if (!sensorKeys.length) {
setPointsByKey({});
return;
}
const controller = new AbortController();
async function loadHistory() {
try {
setLoading(true);
const to = new Date();
const from = new Date(to.getTime() - hours * 60 * 60 * 1000);
const entries = await Promise.all(
sensorKeys.map(async (key) => {
const params = new URLSearchParams({
key,
from: from.toISOString(),
to: to.toISOString(),
});
const response = await fetch(
`${BACKEND_URL}/api/historian/series?${params.toString()}`,
{ signal: controller.signal },
);
if (!response.ok) {
throw new Error(`Failed to load history for ${key}`);
}
const payload = (await response.json()) as HistorianPoint[];
return [key, payload] as const;
}),
);
setPointsByKey(Object.fromEntries(entries));
} catch (error) {
if (controller.signal.aborted) return;
console.error("Failed to load meteo histories", error);
setPointsByKey({});
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}
loadHistory();
return () => controller.abort();
}, [sensorKeys.join("|"), hours]);
return {
pointsByKey,
loading,
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,206 @@
import { useMemo } from "react";
import type { LucideIcon } from "lucide-react";
import {
Droplet,
Gauge,
Home,
Layers3,
Lightbulb,
Thermometer,
Wind,
} from "lucide-react";
import type { ModuleSensorResponse } from "../../../types/meteo";
import { useClimateModuleStream } from "../../climate/hooks/useClimateModuleStream";
export type SynopticVariableGroup = "Clima" | "Rega";
export type SynopticVariable = {
id: string;
sensorId: string;
key: string;
group: SynopticVariableGroup;
label: string;
value: string | number | boolean | null;
unit: string;
timestamp?: string;
connected: boolean;
icon: LucideIcon;
color: string;
};
export function useSynopticVariables() {
const climate = useClimateModuleStream();
const variables = useMemo<SynopticVariable[]>(
() =>
toSynopticVariables({
sensors: climate.sensors,
group: "Clima",
connected: climate.connected,
fallbackTimestamp: climate.lastTimestamp,
}),
[
climate.sensors,
climate.connected,
climate.lastTimestamp,
],
);
return {
variables,
groups: groupVariables(variables),
connected: climate.connected,
climateConnected: climate.connected,
loading: !climate.module,
total: variables.length,
};
}
function toSynopticVariables({
sensors,
group,
connected,
fallbackTimestamp,
}: {
sensors: ModuleSensorResponse[];
group: SynopticVariableGroup;
connected: boolean;
fallbackTimestamp: string | null;
}): SynopticVariable[] {
return sensors.map((sensor) => {
const visual = getVariableVisual(sensor, group);
return {
id: `${group}:${sensor.sensorId ?? sensor.key}`,
sensorId: String(sensor.sensorId ?? sensor.key),
key: sensor.key,
group,
label: sensor.name || sensor.key,
value: normalizeValue(sensor.value),
unit: sensor.unit ?? "",
timestamp: sensor.timestamp ?? fallbackTimestamp ?? undefined,
connected,
icon: visual.icon,
color: visual.color,
};
});
}
function groupVariables(variables: SynopticVariable[]) {
return variables.reduce<Record<SynopticVariableGroup, SynopticVariable[]>>(
(acc, variable) => {
acc[variable.group].push(variable);
return acc;
},
{
Clima: [],
Rega: [],
},
);
}
function normalizeValue(value: unknown): string | number | boolean | null {
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean" ||
value === null
) {
return value;
}
return null;
}
function getVariableVisual(
sensor: ModuleSensorResponse,
group: SynopticVariableGroup,
): {
icon: LucideIcon;
color: string;
} {
const text = normalizeText(`${sensor.key} ${sensor.name} ${sensor.unit ?? ""}`);
if (
text.includes("temperatura") ||
text.includes("temperature")
) {
return {
icon: Thermometer,
color: "#4FD1C5",
};
}
if (
text.includes("humidade") ||
text.includes("humidity")
) {
return {
icon: Droplet,
color: "#38BDF8",
};
}
if (
text.includes("vento") ||
text.includes("wind")
) {
return {
icon: Wind,
color: "#22D3EE",
};
}
if (
text.includes("radiacao") ||
text.includes("radiação") ||
text.includes("radiation")
) {
return {
icon: Lightbulb,
color: "#FACC15",
};
}
if (
text.includes("pressao") ||
text.includes("pressão") ||
text.includes("pressure")
) {
return {
icon: Gauge,
color: "#38BDF8",
};
}
if (
text.includes("nivel") ||
text.includes("nível") ||
text.includes("level")
) {
return {
icon: Layers3,
color: "#0EA5E9",
};
}
if (group === "Clima") {
return {
icon: Home,
color: "#22C55E",
};
}
return {
icon: Gauge,
color: "#A3E635",
};
}
function normalizeText(value: string): string {
return value
.toLowerCase()
.normalize("NFD")
.replace(/\p{Diacritic}/gu, "");
}
File diff suppressed because it is too large Load Diff
+5 -14
View File
@@ -1,6 +1,5 @@
export type WeatherForecastResponse = { export type WeatherForecastResponse = {
location: WeatherLocation; location: WeatherLocation;
current: WeatherCurrent;
daily: WeatherDaily[]; daily: WeatherDaily[];
}; };
@@ -19,19 +18,6 @@ export type WeatherCondition = {
code: number; code: number;
}; };
export type WeatherCurrent = {
temperatureC: number;
feelsLikeC: number;
humidity: number;
precipitationMm: number;
windKph: number;
windDegree: number;
windDirection: string;
pressureMb: number;
uv: number;
condition: WeatherCondition;
};
export type WeatherDaily = { export type WeatherDaily = {
date: string; date: string;
maxTemperatureC: number; maxTemperatureC: number;
@@ -40,6 +26,11 @@ export type WeatherDaily = {
totalPrecipitationMm: number; totalPrecipitationMm: number;
dailyRainChance: number; dailyRainChance: number;
maxWindKph: number; maxWindKph: number;
averageWindKph: number;
averageWindDegree: number;
averageWindDirection: string;
averageHumidity: number;
averageVisibilityKm: number;
uv: number; uv: number;
sunrise: string; sunrise: string;
sunset: string; sunset: string;