feat: API für Systemspannung +5V erfolgreich implementiert

- API-Handler `getSystemspannung5VplusHandler.ts` erstellt
- JSON-Daten werden aus dem Verzeichnis `mocks/device-cgi-simulator/chartsData/systemspannung5Vplus/` geladen
- unterstützt die Parameter DIA0, DIA1, DIA2 für unterschiedliche Datenfrequenzen
- Fehlerbehandlung bei ungültigen Typen und fehlenden Dateien eingebaut
- API getestet unter `/api/cpl/getSystemspannung5VplusHandler?typ=DIA0`
This commit is contained in:
ISA
2025-07-03 10:23:04 +02:00
parent 4245d7a991
commit cee3ee0581
15 changed files with 451 additions and 168 deletions

View File

@@ -6,6 +6,6 @@ NEXT_PUBLIC_USE_MOCK_BACKEND_LOOP_START=false
NEXT_PUBLIC_EXPORT_STATIC=false NEXT_PUBLIC_EXPORT_STATIC=false
NEXT_PUBLIC_USE_CGI=false NEXT_PUBLIC_USE_CGI=false
# App-Versionsnummer # App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.6.527 NEXT_PUBLIC_APP_VERSION=1.6.530
NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter) NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter)

View File

@@ -5,5 +5,5 @@ NEXT_PUBLIC_CPL_API_PATH=/CPL
NEXT_PUBLIC_EXPORT_STATIC=true NEXT_PUBLIC_EXPORT_STATIC=true
NEXT_PUBLIC_USE_CGI=true NEXT_PUBLIC_USE_CGI=true
# App-Versionsnummer # App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.6.527 NEXT_PUBLIC_APP_VERSION=1.6.530
NEXT_PUBLIC_CPL_MODE=production NEXT_PUBLIC_CPL_MODE=production

View File

@@ -1,3 +1,18 @@
## [1.6.530] 2025-07-03
- fix: KÜ Firmwareupdate
---
## [1.6.529] 2025-07-03
- fix: KÜ Firmwareupdate
---
## [1.6.528] 2025-07-03
- fix: KÜ Firmwareupdate
---
## [1.6.527] 2025-07-03 ## [1.6.527] 2025-07-03
- fix: Firmware-Update läuft nun exakt 5 Minuten bis 100% Fortschritt - fix: Firmware-Update läuft nun exakt 5 Minuten bis 100% Fortschritt

View File

@@ -0,0 +1,96 @@
// components/main/system/DetailModal.tsx
"use client";
import React from "react";
import { Line } from "react-chartjs-2";
import { useSelector } from "react-redux";
import { RootState } from "@/redux/store";
type Props = {
isOpen: boolean;
selectedKey: string | null;
onClose: () => void;
};
export const DetailModal = ({ isOpen, selectedKey, onClose }: Props) => {
// Mapping: Welcher Redux-Zweig zu welchem selectedKey gehört
const typMap: Record<string, "DIA0" | "DIA1" | "DIA2"> = {
"+5V": "DIA1",
"+15V": "DIA1",
"-15V": "DIA1",
"-98V": "DIA1",
};
const typ = selectedKey ? typMap[selectedKey] : null;
// Redux State abrufen
const reduxData = useSelector((state: RootState) =>
typ ? state.systemspannung5Vplus[typ] : []
);
// Zeitstempel und Werte extrahieren
const labels = reduxData.map((e: any) => e.t);
const values = reduxData.map((e: any) => e.i);
const baseOptions = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false,
grid: { color: "rgba(200,200,200,0.2)" },
title: { display: true, text: "Wert" },
},
x: {
grid: { color: "rgba(200,200,200,0.2)" },
title: { display: true, text: "Zeit" },
},
},
plugins: {
legend: { position: "bottom" as const },
title: { display: true, text: `Verlauf ${selectedKey}` },
},
};
if (!isOpen || !selectedKey) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-xl w-[90%] max-w-[1200px] h-[80vh] overflow-auto shadow-2xl">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">
Detailansicht: {selectedKey}
</h2>
<button onClick={onClose} className="text-red-500 hover:text-red-700">
</button>
</div>
<div className="mb-4">
<label className="mr-2 font-medium">Zeitraum:</label>
<select className="border px-2 py-1 rounded">
<option>Letzte 24 Stunden</option>
{/* Optional: dynamische Filter */}
</select>
</div>
<div className="h-[500px]">
<Line
data={{
labels,
datasets: [
{
label: selectedKey,
data: values,
borderColor: "rgba(59,130,246,1)",
backgroundColor: "rgba(59,130,246,0.2)",
fill: false,
},
],
}}
options={baseOptions}
/>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,144 @@
// components/main/system/SystemCharts.tsx
import React from "react";
import { Line } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from "chart.js";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
type HistoryEntry = {
time: string | number | Date;
"+5V": number;
"+15V": number;
"-15V": number;
"-98V": number;
"ADC Temp": number;
"CPU Temp": number;
};
type Props = {
history: HistoryEntry[];
};
export const SystemCharts = ({ history }: Props) => {
const labels = history.map((h) => new Date(h.time).toLocaleTimeString());
const formatValue = (v: number) => v.toFixed(2);
const baseOptions = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false,
grid: { color: "rgba(200,200,200,0.2)" },
title: { display: true, text: "Wert" },
},
x: {
grid: { color: "rgba(200,200,200,0.2)" },
title: { display: true, text: "Zeit" },
},
},
plugins: {
legend: { position: "bottom" as const },
},
};
return (
<div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
<div className="h-[300px]">
<Line
data={{
labels,
datasets: [
{
label: "+5V",
data: history.map((h) => formatValue(h["+5V"])),
borderColor: "rgba(59,130,246,1)",
backgroundColor: "rgba(59,130,246,0.5)",
fill: false,
},
{
label: "+15V",
data: history.map((h) => formatValue(h["+15V"])),
borderColor: "rgba(34,197,94,1)",
backgroundColor: "rgba(34,197,94,0.5)",
fill: false,
},
{
label: "-15V",
data: history.map((h) => formatValue(h["-15V"])),
borderColor: "rgba(239,68,68,1)",
backgroundColor: "rgba(239,68,68,0.5)",
fill: false,
},
{
label: "-98V",
data: history.map((h) => formatValue(h["-98V"])),
borderColor: "rgba(234,179,8,1)",
backgroundColor: "rgba(234,179,8,0.5)",
fill: false,
},
],
}}
options={{
...baseOptions,
plugins: {
...baseOptions.plugins,
title: { display: true, text: "Systemspannungen" },
},
}}
/>
</div>
<div className="h-[300px]">
<Line
data={{
labels,
datasets: [
{
label: "ADC Temp",
data: history.map((h) => h["ADC Temp"]),
borderColor: "rgba(168,85,247,1)",
backgroundColor: "rgba(168,85,247,0.5)",
fill: false,
},
{
label: "CPU Temp",
data: history.map((h) =>
parseFloat(formatValue(h["CPU Temp"]))
),
borderColor: "rgba(251,191,36,1)",
backgroundColor: "rgba(251,191,36,0.5)",
fill: false,
},
],
}}
options={{
...baseOptions,
plugins: {
...baseOptions.plugins,
title: { display: true, text: "Systemtemperaturen" },
},
}}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,33 @@
// components/main/system/SystemOverviewGrid.tsx
import React from "react";
type Props = {
voltages: Record<string, number>;
onOpenDetail: (key: string) => void;
};
export const SystemOverviewGrid = ({ voltages, onOpenDetail }: Props) => {
const formatValue = (value: number) => value.toFixed(2);
return (
<div className="grid grid-cols-2 gap-4 mb-8">
{Object.entries(voltages).map(([key, value]) => {
const unit = key.includes("Temp") ? "°C" : "V";
return (
<div key={key} className="p-4 border rounded shadow">
<h2 className="font-semibold">{key}</h2>
<p>
{formatValue(value)} {unit}
<button
onClick={() => onOpenDetail(key)}
className="ml-2 text-blue-600 hover:underline text-sm"
>
Details
</button>
</p>
</div>
);
})}
</div>
);
};

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.527", "version": "1.6.530",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.527", "version": "1.6.530",
"dependencies": { "dependencies": {
"@fontsource/roboto": "^5.1.0", "@fontsource/roboto": "^5.1.0",
"@iconify-icons/ri": "^1.2.10", "@iconify-icons/ri": "^1.2.10",

View File

@@ -1,6 +1,6 @@
{ {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.527", "version": "1.6.530",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",

View File

@@ -0,0 +1,35 @@
// @/pages/api/cpl/getSystemspannung5VplusHandler.ts
import { NextApiRequest, NextApiResponse } from "next";
import path from "path";
import fs from "fs/promises";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { typ = "DIA0" } = req.query;
if (!["DIA0", "DIA1", "DIA2"].includes(typ as string)) {
return res
.status(400)
.json({ error: "Ungültiger Typ. Nur DIA0, DIA1 oder DIA2 erlaubt." });
}
try {
// Verwende __dirname um relativen Pfad zur API-Datei zu bestimmen
const filePath = path.join(
process.cwd(), // ← Root-Projektverzeichnis
"mocks/device-cgi-simulator/chartsData/systemspannung5Vplus",
`${typ}.json`
);
const fileContent = await fs.readFile(filePath, "utf-8");
const json = JSON.parse(fileContent);
return res.status(200).json(json);
} catch (error) {
console.error("❌ Fehler beim Lesen der Datei:", error);
return res
.status(500)
.json({ error: "Datei konnte nicht gelesen werden." });
}
}

View File

@@ -1,30 +1,12 @@
"use client"; // /pages/system.tsx // pages/system.tsx
import React, { useEffect } from "react"; "use client";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "../redux/store"; import { AppDispatch, RootState } from "../redux/store";
import { getSystemVoltTempThunk } from "../redux/thunks/getSystemVoltTempThunk"; import { getSystemVoltTempThunk } from "../redux/thunks/getSystemVoltTempThunk";
import { import { SystemOverviewGrid } from "@/components/main/system/SystemOverviewGrid";
Chart as ChartJS, import { SystemCharts } from "@/components/main/system/SystemCharts";
CategoryScale, import { DetailModal } from "@/components/main/system/DetailModal";
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
TooltipItem,
} from "chart.js";
import { Line } from "react-chartjs-2";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
const SystemPage = () => { const SystemPage = () => {
const dispatch = useDispatch<AppDispatch>(); const dispatch = useDispatch<AppDispatch>();
@@ -35,6 +17,9 @@ const SystemPage = () => {
(state: RootState) => state.systemVoltTemp.history (state: RootState) => state.systemVoltTemp.history
); );
const [selectedKey, setSelectedKey] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => { useEffect(() => {
dispatch(getSystemVoltTempThunk()); dispatch(getSystemVoltTempThunk());
const interval = setInterval(() => { const interval = setInterval(() => {
@@ -45,96 +30,14 @@ const SystemPage = () => {
const labels = history.map((h) => new Date(h.time).toLocaleTimeString()); const labels = history.map((h) => new Date(h.time).toLocaleTimeString());
const formatValue = (value: number) => value.toFixed(2); const handleOpenDetail = (key: string) => {
setSelectedKey(key);
setIsModalOpen(true);
};
const voltageDatasets = [ const handleCloseDetail = () => {
{ setSelectedKey(null);
label: "+5V", setIsModalOpen(false);
data: history.map((h) => formatValue(h["+5V"])),
borderColor: "rgba(59,130,246,1)",
backgroundColor: "rgba(59,130,246,0.5)",
fill: false,
},
{
label: "+15V",
data: history.map((h) => formatValue(h["+15V"])),
borderColor: "rgba(34,197,94,1)",
backgroundColor: "rgba(34,197,94,0.5)",
fill: false,
},
{
label: "-15V",
data: history.map((h) => formatValue(h["-15V"])),
borderColor: "rgba(239,68,68,1)",
backgroundColor: "rgba(239,68,68,0.5)",
fill: false,
},
{
label: "-98V",
data: history.map((h) => formatValue(h["-98V"])),
borderColor: "rgba(234,179,8,1)",
backgroundColor: "rgba(234,179,8,0.5)",
fill: false,
},
];
const temperatureDatasets = [
{
label: "ADC Temp",
data: history.map((h) => h["ADC Temp"]),
borderColor: "rgba(168,85,247,1)",
backgroundColor: "rgba(168,85,247,0.5)",
fill: false,
},
{
label: "CPU Temp",
data: history.map((h) => Number(formatValue(h["CPU Temp"]))),
borderColor: "rgba(251,191,36,1)",
backgroundColor: "rgba(251,191,36,0.5)",
fill: false,
},
];
const baseChartOptions = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false,
grid: {
color: "rgba(200,200,200,0.2)",
},
title: {
display: true,
text: "Wert",
},
},
x: {
grid: {
color: "rgba(200,200,200,0.2)",
},
title: {
display: true,
text: "Zeit",
},
},
},
plugins: {
legend: {
position: "bottom" as const,
},
tooltip: {
callbacks: {
label: function (context: TooltipItem<"line">) {
const label = context.dataset.label || "";
const value =
context.parsed.y !== null ? context.parsed.y.toFixed(2) : "";
const unit = label.includes("Temp") ? "°C" : "V";
return `${label}: ${value} ${unit}`;
},
},
},
},
}; };
return ( return (
@@ -142,55 +45,14 @@ const SystemPage = () => {
<h1 className="text-xl font-bold mb-4"> <h1 className="text-xl font-bold mb-4">
System Spannungen & Temperaturen System Spannungen & Temperaturen
</h1> </h1>
<SystemOverviewGrid voltages={voltages} onOpenDetail={handleOpenDetail} />
<div className="grid grid-cols-2 gap-4 mb-8"> <SystemCharts history={history} />
{Object.entries(voltages).map(([key, value]) => { <DetailModal
const formattedValue = formatValue(value); isOpen={isModalOpen}
const unit = key.includes("Temp") ? "°C" : "V"; selectedKey={selectedKey}
return ( history={history}
<div key={key} className="p-4 border rounded shadow"> onClose={handleCloseDetail}
<h2 className="font-semibold">{key}</h2> />
<p>
{formattedValue} {unit}
</p>
</div>
);
})}
</div>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
<div className="h-[300px]">
<Line
data={{ labels, datasets: voltageDatasets }}
options={{
...baseChartOptions,
plugins: {
...baseChartOptions.plugins,
title: {
display: true,
text: "Systemspannungen",
},
},
}}
/>
</div>
<div className="h-[300px]">
<Line
data={{ labels, datasets: temperatureDatasets }}
options={{
...baseChartOptions,
plugins: {
...baseChartOptions.plugins,
title: {
display: true,
text: "Systemtemperaturen",
},
},
}}
/>
</div>
</div>
</div> </div>
); );
}; };

View File

@@ -1,3 +1,10 @@
{ {
"win_systemVoltTemp": ["AAV09", "AAV11", "AAV15", "AAV16", "AAV17", "AAV18"] "win_systemVoltTemp": [
"<%=AAV09%>",
"<%=AAV11%>",
"<%=AAV15%>",
"<%=AAV16%>",
"<%=AAV17%>",
"<%=AAV18%>"
]
} }

View File

@@ -0,0 +1,48 @@
// redux/slices/systemspannung5VplusSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { getSystemspannung5VplusThunk } from "../thunks/getSystemspannung5VplusThunk";
type StateType = {
DIA0: any[]; // alle Werte
DIA1: any[]; // stündlich
DIA2: any[]; // täglich
isLoading: boolean;
error: string | null;
};
const initialState: StateType = {
DIA0: [],
DIA1: [],
DIA2: [],
isLoading: false,
error: null,
};
export const systemspannung5VplusSlice = createSlice({
name: "systemspannung5Vplus",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(getSystemspannung5VplusThunk.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(
getSystemspannung5VplusThunk.fulfilled,
(
state,
action: PayloadAction<{ typ: "DIA0" | "DIA1" | "DIA2"; data: any[] }>
) => {
state.isLoading = false;
state[action.payload.typ] = action.payload.data;
}
)
.addCase(getSystemspannung5VplusThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
},
});
export default systemspannung5VplusSlice.reducer;

View File

@@ -29,6 +29,7 @@ import messagesReducer from "./slices/messagesSlice";
import firmwareUpdateReducer from "@/redux/slices/firmwareUpdateSlice"; import firmwareUpdateReducer from "@/redux/slices/firmwareUpdateSlice";
import confirmModalReducer from "./slices/confirmModalSlice"; import confirmModalReducer from "./slices/confirmModalSlice";
import firmwareProgressReducer from "./slices/firmwareProgressSlice"; import firmwareProgressReducer from "./slices/firmwareProgressSlice";
import systemspannung5VplusReducer from "./slices/systemspannung5VplusSlice";
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
@@ -60,6 +61,7 @@ const store = configureStore({
firmwareUpdate: firmwareUpdateReducer, firmwareUpdate: firmwareUpdateReducer,
confirmModal: confirmModalReducer, confirmModal: confirmModalReducer,
firmwareProgress: firmwareProgressReducer, firmwareProgress: firmwareProgressReducer,
systemspannung5Vplus: systemspannung5VplusReducer,
}, },
}); });

View File

@@ -0,0 +1,16 @@
// redux/thunks/getSystemspannung5VplusThunk.ts
import { createAsyncThunk } from "@reduxjs/toolkit";
import { fetchSystemspannung5VplusService } from "@/services/fetchSystemspannung5VplusService";
export const getSystemspannung5VplusThunk = createAsyncThunk(
"systemspannung5Vplus/fetch",
async (typ: "DIA0" | "DIA1" | "DIA2", thunkAPI) => {
try {
const data = await fetchSystemspannung5VplusService(typ);
return { typ, data };
} catch (error) {
console.error("Fehler in getSystemspannung5VplusThunk:", error);
return thunkAPI.rejectWithValue("Fehler beim Laden der 5V-Daten");
}
}
);

View File

@@ -0,0 +1,25 @@
// services/fetchSystemspannung5VplusService.ts
type Typ = "DIA0" | "DIA1" | "DIA2";
/**
* Holt Messwerte für +5V aus der passenden JSON-Datei über die API
* @param typ - Typ der Daten: DIA0 = alle, DIA1 = stündlich, DIA2 = täglich
*/
export const fetchSystemspannung5VplusService = async (typ: Typ) => {
try {
const res = await fetch(
`/api/cpl/getSystemspannung5VplusHandler?typ=${typ}`
);
if (!res.ok) {
throw new Error(`Fehler beim Abrufen: ${res.status}`);
}
const data = await res.json();
return data;
} catch (err) {
console.error("❌ Fehler in fetchSystemspannung5VplusService:", err);
return null;
}
};